Skip to content
Snippets Groups Projects
Commit b71d0101 authored by Brady James Garvin's avatar Brady James Garvin
Browse files

Initial commit.

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 543 additions and 0 deletions
# Disable line-ending conversions for this repository.
* -text
# dependencies
/node_modules
# testing
/coverage
# production
/build
# environments
.env.local
.env.development.local
.env.test.local
.env.production.local
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
*~
.DS_Store
[submodule "eslint-config"]
path = eslint-config
url = git@git.unl.edu:soft-core/soft-260/eslint-config.git
Subproject commit a85278e2bd62f637800bc32fa6b372bc2855546e
# dependencies
/node_modules
# testing
/coverage
# production
/build
# environments
.env.local
.env.development.local
.env.test.local
.env.production.local
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
*~
.DS_Store
This diff is collapsed.
{
"name": "@unlsoft/lint-latex",
"version": "1.0.0",
"description": "A simple linter to catch common student mistakes while learning LaTeX.",
"type": "module",
"private": true,
"license": "UNLICENSED",
"bin": {
"lint-latex": "./src/lint.js"
},
"scripts": {
"lint:js": "eslint --max-warnings 0 ./src",
"lint": "run-s --continue-on-error lint:**"
},
"dependencies": {
"chalk": "^5.0.1",
"npm-run-all": "^4.1.5",
"yargs": "^17.5.1"
},
"devDependencies": {
"@unlsoft/eslint-config": "file:../eslint-config",
"eslint": "^8.16.0"
},
"eslintConfig": {
"extends": "@unlsoft"
}
}
#! /usr/bin/env node
import path from 'path';
import fs from 'fs';
import yargs from 'yargs';
import chalk from 'chalk';
import { lint } from './linter.js';
let parser = yargs(process.argv.slice(2)).parserConfiguration({
'parse-numbers': false,
'parse-positional-numbers': false,
}).strictOptions().usage(`Usage: $0 [FILE]...
or: $0 --version
or: $0 --help`);
parser = parser.alias('v', 'version');
parser = parser.help().alias('h', 'help').showHelpOnFail(
false,
`See \`${path.basename(process.argv[1])} --help'.`,
);
let problemCount = 0;
for (const filename of parser.argv._) {
try {
problemCount += lint(path.resolve(filename), fs.readFileSync(filename, {
encoding: 'utf8',
}));
} catch (e) {
console.error(`Exception while linting \`${filename}': ${e}`);
++problemCount;
}
}
if (problemCount > 0) {
console.log(chalk.yellow(`✖ ${problemCount} ${problemCount === 1 ? 'problem' : 'problems'}`));
console.log();
process.exitCode = 1;
}
import chalk from 'chalk';
import { tokenize } from './tokenizer.js';
import { StructureStack } from './structure.js';
// Restricted Characters
import { NoPipeRule } from './rules/noPipe.js';
import { NoMultidotRule } from './rules/noMultidot.js';
import { NoSplitComparisonRule } from './rules/noSplitComparison.js';
// Mode-Restricted Characters
import { NoTextModeVariableRule } from './rules/noTextModeVariable.js';
import { NoTextModeComparisonRule } from './rules/noTextModeComparison.js';
import { NoTextModePlusRule } from './rules/noTextModePlus.js';
import { NoTextModeAsteriskRule } from './rules/noTextModeAsterisk.js';
import { NoMathModeAsteriskRule } from './rules/noMathModeAsterisk.js';
// Changes between Text and Math Mode
import { NoSingleDollarRule } from './rules/noSingleDollar.js';
import { NoDoubleDollarRule } from './rules/noDoubleDollar.js';
// Manual Formatting
import { NoManualSizingRule } from './rules/noManualSizing.js';
import { NoManualSpacingRule } from './rules/noManualSpacing.js';
import { NoManualLineBreakRule } from './rules/noManualLineBreak.js';
import { NoManualPageClearRule } from './rules/noManualPageClear.js';
// Malformed Function Names
import { LogAsFunctionRule } from './rules/logAsFunction.js';
import { MinOrMaxAsFunctionRule } from './rules/minOrMaxAsFunction.js';
import { LimitAsFunctionRule } from './rules/limitAsFunction.js';
const RULE_CLASSES = [
// Restricted Characters
NoPipeRule,
NoMultidotRule,
NoSplitComparisonRule,
// Mode-Restricted Characters
NoTextModeVariableRule,
NoTextModeComparisonRule,
NoTextModePlusRule,
NoTextModeAsteriskRule,
NoMathModeAsteriskRule,
// Changes between Text and Math Mode
NoSingleDollarRule,
NoDoubleDollarRule,
// Manual Formatting
NoManualSizingRule,
NoManualSpacingRule,
NoManualLineBreakRule,
NoManualPageClearRule,
// Malformed Function Names
LogAsFunctionRule,
MinOrMaxAsFunctionRule,
LimitAsFunctionRule,
];
function byLocation(left, right) {
if (left.line !== right.line) {
return left.line - right.line;
}
return left.column - right.column;
}
export function lint(path, latex) {
const rules = RULE_CLASSES.map((RuleClass) => new RuleClass());
const errors = [];
const structureStack = new StructureStack();
for (const token of tokenize(`${latex}\n`)) {
structureStack.see(token);
for (const rule of rules) {
errors.push(...rule.see(token, structureStack));
}
}
if (errors.length > 0) {
console.log(chalk.underline(path));
for (const error of errors.sort(byLocation)) {
console.log(`${error}`);
}
}
return errors.length;
}
import chalk from 'chalk';
class Error {
constructor(rule, token) {
this.rule = rule;
this.line = token.line;
this.column = token.column;
}
toString() {
const location = chalk.dim(`${this.line}:${this.column}`);
const severity = chalk.yellow('warning');
const name = chalk.dim(this.rule.name);
const detail = chalk.dim(this.rule.detail);
return ` ${location} ${severity} ${this.rule.message} ${name}\n${detail}`;
}
}
export class Rule {
constructor(name, message, detail) {
this.name = name;
this.message = message;
this.detail = detail;
}
error(token) {
return new Error(this, token);
}
}
import { Rule } from '../rule.js';
export class LimitAsFunctionRule extends Rule {
constructor() {
super(
'limit-as-function',
'Unexpected limit written as if a product',
/* eslint-disable max-len */
`
Your code has the name of a limit function (lim, limsup, or liminf) written directly in math mode. But LaTeX will typeset such names as products, formatting them as if they are l⋅i⋅m, l⋅i⋅m⋅i⋅n⋅f, or l⋅i⋅m⋅s⋅u⋅p, respectively, and it will also give the wrong spacing between the function and its argument.
If you are trying to write lim, write \\lim.
If you are trying to write liminf, write \\liminf.
If you are trying to write limsup, write \\limsup.
If you actually are trying to write a product, write spaces between the terms to avoid ambiguity in the source code.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (stack.mathMode) {
if (token.text === 'lim' || token.text === 'liminf' || token.text === 'limsup') {
yield this.error(token);
}
}
}
}
import { Rule } from '../rule.js';
export class LogAsFunctionRule extends Rule {
constructor() {
super(
'log-as-function',
'Unexpected logarithm written as if a product',
/* eslint-disable max-len */
`
Your code has the name of a logarithm function (log, ln, or lg) written directly in math mode. But LaTeX will typeset such names as products, formatting them as if they are l⋅o⋅g, l⋅n, or l⋅g, respectively, and it will also give the wrong spacing between the logarithm and its argument.
If you are trying to write log, write \\log.
If you are trying to write ln, write \\ln.
If you are trying to write lg, write \\lg.
If you actually are trying to write a product, write spaces between the terms to avoid ambiguity in the source code.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (stack.mathMode && (token.text === 'log' || token.text === 'ln' || token.text === 'lg')) {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
export class MinOrMaxAsFunctionRule extends Rule {
constructor() {
super(
'min-or-max-as-function',
'Unexpected minimum or maximum written as if a product',
/* eslint-disable max-len */
`
Your code has the name of a minimum, maximum, infimum, or supremum function (min, max, inf, or sup) written directly in math mode. But LaTeX will typeset such names as products, formatting them as if they are m⋅i⋅n, m⋅a⋅x, i⋅n⋅f, or s⋅u⋅p, respectively, and it will also give the wrong spacing between the function and its argument.
If you are trying to write min, write \\min.
If you are trying to write max, write \\max.
If you are trying to write inf, write \\inf.
If you are trying to write sup, write \\sup.
If you actually are trying to write a product, write spaces between the terms to avoid ambiguity in the source code.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (stack.mathMode) {
switch (token.text) {
case 'min':
case 'max':
case 'inf':
case 'sup':
yield this.error(token);
break;
default:
}
}
}
}
import { Rule } from '../rule.js';
export class NoDoubleDollarRule extends Rule {
constructor() {
super(
'no-double-dollar',
'Unexpected double dollar sign',
/* eslint-disable max-len */
`
Your code has a double dollar sign ($$), which is the plain TeX primitive for display math. However, LaTeX provides the delimiters \\[ and \\] for display math, which, besides giving better error messages if something goes wrong, also allow LaTeX packages to apply patches; by using $$, you are breaking these packages. See <https://texfaq.org/FAQ-dolldoll> for more information.
If you are trying to write display math, write \\[…\\] or use a dedicated math environment.
If you are trying to write a literal double dollar sign, write \\$\\$.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token) {
if (token.text === '$$') {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
export class NoManualLineBreakRule extends Rule {
constructor() {
super(
'no-manual-line-break',
'Unexpected manual line break',
/* eslint-disable max-len */
`
Your code uses a command like \\newline or \\linebreak that forcibly inserts a linebreak or else it uses \\\\ in a context where it has a similar effect. While there are rare situations where it is appropriate to force a linebreak, in the vast majority of cases it is better to use a paragraph break (blank lines in the source code separate paragraphs) or an environment that manages line wrapping for you.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (!stack.mathMode &&
(token.text === '\\newline' ||
token.text === '\\linebreak' ||
(token.text === '\\\\' && !stack.has('tabular')))) {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
export class NoManualPageClearRule extends Rule {
constructor() {
super(
'no-manual-page-clear',
'Unexpected \\newpage or \\clearpage',
/* eslint-disable max-len */
`
Your code uses \\newpage or \\clearpage to insert a page break. These commands force excess vertical space to the bottom of the page and are intended for things like building title pages or separating large multipage groups of content, like book chapters. For a homework submission, which does not have things like title pages or chapters, you should allow LaTeX to manage vertical space normally. Use the \\pagebreak command to insert a page break instead.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token) {
if (token.text === '\\newpage' || token.text === '\\clearpage') {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
export class NoManualSizingRule extends Rule {
constructor() {
super(
'no-manual-sizing',
'Unexpected manual symbol sizing',
/* eslint-disable max-len */
`
Your code uses \\big, \\Big, \\bigg, \\Bigg, or a variant of one of those commands to specify a particular larger size for a math symbol. While these macros are appropriate in some rare situations, it is almost always better to use \\left and \\right, which do not lock the symbol at a particular size that you have to manually update if the expression changes, but instead automatically choose an appropriate size based on context.
If you are trying to write large delimiters around a math expression, write \\left before the left delimiter and \\right before the right delimiter, as in \\(\\left(…\\right)\\).
If you are trying to write a large delimiter on only one side of a math expression, use the same notation, but write . on the side that does not have a delimiter, as in \\(\\left.…\\right\\rvert\\).
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token) {
switch (token.text) {
case '\\big':
case '\\Big':
case '\\bigg':
case '\\Bigg':
case '\\bigl':
case '\\Bigl':
case '\\biggl':
case '\\Biggl':
case '\\bigm':
case '\\Bigm':
case '\\biggm':
case '\\Biggm':
case '\\bigr':
case '\\Bigr':
case '\\biggr':
case '\\Biggr':
yield this.error(token);
break;
default:
}
}
}
import { Rule } from '../rule.js';
export class NoManualSpacingRule extends Rule {
constructor() {
super(
'no-manual-spacing',
'Unexpected manual spacing',
/* eslint-disable max-len */
`
Your code uses \\quad, \\qquad, \\hskip, \\vskip, \\hspace, or \\vspace to manually change spacing. While these macros can be appropriate in other situations, for most documents it is better to use an appropriate package to tweak the default spacing globally or an appropriate environment to format your content according to its type.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token) {
switch (token.text) {
case '\\quad':
case '\\qquad':
case '\\hskip':
case '\\vskip':
case '\\hspace':
case '\\vspace':
yield this.error(token);
break;
default:
}
}
}
import { Rule } from '../rule.js';
export class NoMathModeAsteriskRule extends Rule {
constructor() {
super(
'no-math-mode-asterisk',
'Unexpected asterisk (*) in math mode',
/* eslint-disable max-len */
`
Your code has a asterisk (*) in math mode, suggesting that you are trying to write a multiplication, an asterisk operator, or a star operator. But the proper symbol for a product in mathematics is ⋅ or ✕, depending on the type of product, and the ∗ and ⋆ operators are their own distinct symbols.
If you are trying to write ⋅, write \\cdot.
If you are trying to write ✕, write \\times.
If you are trying to write ∗, write \\ast.
If you are trying to write ⋆, write \\star.
`.substring(1),
/* eslint-enable max-len */
);
this.candidate = undefined;
}
*see(token, stack) {
if (this.candidate !== undefined && token.text !== '}') {
yield this.error(this.candidate);
}
if (stack.mathMode && token.text === '*') {
this.candidate = token;
} else {
this.candidate = undefined;
}
}
}
import { Rule } from '../rule.js';
export class NoMultidotRule extends Rule {
constructor() {
super(
'no-multidot',
'Unexpected consecutive dots',
/* eslint-disable max-len */
`
Your code has several dots in a row, suggesting that you are trying to write an ellipsis or dot leader. But in typography, a proper ellipsis is its own character, not a sequence of dot characters, and it is almost always better to have LaTeX generate leaders automatically instead of trying to write and space dots by hand.
If you are trying to write an ellipsis in text mode, write \\dots.
If you are trying to write an ellipsis in math mode, write one of \\ldots, \\cdots, \\vdots, or \\ddots.
If you are trying to write a dot leader, write \\dotfill.
`.substring(1),
/* eslint-enable max-len */
);
this.firstDot = undefined;
this.dotCount = 0;
}
*see(token) {
if (token.text === '.') {
++this.dotCount;
if (this.dotCount === 1) {
this.firstDot = token;
} else if (this.dotCount === 2) {
yield this.error(this.firstDot);
}
} else {
this.firstDot = undefined;
this.dotCount = 0;
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment