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
import { Rule } from '../rule.js';
export class NoPipeRule extends Rule {
constructor() {
super(
'no-pipe',
'Unexpected pipe character (|) or \\vert',
/* eslint-disable max-len */
`
Your code has a pipe character (|) or the command \\vert, suggesting that you are trying to write a delimiter or relation that looks like a vertical bar. But the commands | and \\vert produce ordinary math symbols, so LaTeX will space them like they are variables, not delimiters or relations.
If you are trying to say that a quantity \\(a\\) divides a quantity \\(b\\), write \\(a\\divides b\\).
If you are trying to write the absolute value of a quantity \\(a\\), write \\(\\left\\lvert a\\right\\rvert\\) or, better yet, \\(\\abs{a}\\) if you have an \\abs command available.
Similarly, if you are trying to write the cardinality of a set \\(A\\), write \\(\\left\\lvert A\\right\\rvert\\) or, better yet, \\(\\card{A}\\) if you have a \\card command available.
If you are trying to write a set comprehension of all \\(x\\) such that \\(\\phi(x)\\), write either \\(\\{x\\mid\\phi(x)\\}\\) or \\(\\{x\\mvert\\phi(x)\\}\\).
If you are trying to write an expression \\(f(x)\\) evaluated from \\(x=a\\) to \\(x=b\\), write \\(\\left.f(x)\\right\\rvert_{x=a}^{x=b}\\).
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (token.text === '\\vert' || (token.text === '|' && !stack.has('tabular'))) {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
export class NoSingleDollarRule extends Rule {
constructor() {
super(
'no-single-dollar',
'Unexpected single dollar sign',
/* eslint-disable max-len */
`
Your code has a single dollar sign ($), which is the plain TeX primitive for inline math. However, LaTeX provides the delimiters \\( and \\) for inline 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 inline math, write \\(…\\).
If you are trying to write a literal 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 NoSplitComparisonRule extends Rule {
constructor() {
super(
'no-split-comparison',
'Unexpected multicharacter comparison',
/* eslint-disable max-len */
`
Your code has what appears to be a comparison written as several characters (!, <, =, or >) in a row. But in mathematics the proper notation is always a combined character (≪, ≤, =, ≥, ≫, or ≠).
If you are trying to write ≪, write \\ll, not <<.
If you are trying to write ≤, write \\le, not <=.
If you are trying to write =, write just =, not == or ===.
If you are trying to write ≥, write \\ge, not >=.
If you are trying to write ≫, write \\gg, not >>.
If you are trying to write ≠, write \\ne, not != or !==.
If you are writing a comparison between a factorial and some other quantity, write a space between the factorial and the comparison operator.
`.substring(1),
/* eslint-enable max-len */
);
this.firstCharacter = undefined;
this.characterCount = 0;
}
*see(token) {
if (token.text === '!') {
this.firstCharacter = token;
this.characterCount = 1;
} else if (token.text === '<' || token.text === '=' || token.text === '>') {
++this.characterCount;
if (this.characterCount === 1) {
this.firstCharacter = token;
} else if (this.characterCount === 2) {
yield this.error(this.firstCharacter);
}
} else {
this.firstCharacter = undefined;
this.characterCount = 0;
}
}
}
import { Rule } from '../rule.js';
export class NoTextModeAsteriskRule extends Rule {
constructor() {
super(
'no-text-mode-asterisk',
'Unexpected asterisk (*) in text mode',
/* eslint-disable max-len */
`
Your code has a asterisk (*) in text mode, suggesting that you are trying to write a footnote without using the \\footnote command or a multiplication without using math mode. But the formatting that LaTeX uses for a bare asterisk is incorrect for both a footnote and for a multiplication. Other math will also be affected in text mode; for instance, variables may be typeset in the wrong font.
If you are trying to write a footnote, write \\footnote{…}.
If you are trying to write inline math, write \\(…\\) and use \\cdot for a multiplication.
If you are trying to write display math, write \\[…\\] or use a dedicated math environment and use \\cdot for a multiplication.
`.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 NoTextModeComparisonRule extends Rule {
constructor() {
super(
'no-text-mode-comparison',
'Unexpected comparison character (<, =, or >) in text mode',
/* eslint-disable max-len */
`
Your code has a comparison character (<, =, or >) in text mode, suggesting that you are trying to write mathematics without using math mode. But the formatting that LaTeX uses for text is inappropriate for math and vice versa. For example, by default LaTeX will turn text-mode inequality operators like less-than and greater-than signs into inverted exclamation points (¡) and question marks (¿), and it may use too little or too much space around a text-mode equals sign. Other math will also be affected in text mode; for instance, variables may be typeset in the wrong font.
If you are trying to write inline math, write \\(…\\).
If you are trying to write display math, write \\[…\\] or use a dedicated math environment.
If you are defining a new math command, make sure to enclose the math part of its definition with \\ensuremath{…}.
`.substring(1),
/* eslint-enable max-len */
);
this.afterMacro = false;
}
*see(token, stack) {
if (!this.afterMacro && !stack.mathMode && (token.text === '<' || token.text === '=' || token.text === '>')) {
yield this.error(token);
}
this.afterMacro = token.text[0] === '\\';
}
}
import { Rule } from '../rule.js';
export class NoTextModePlusRule extends Rule {
constructor() {
super(
'no-text-mode-plus',
'Unexpected plus sign (+) in text mode',
/* eslint-disable max-len */
`
Your code has a plus sign (+) in text mode, suggesting that you are trying to write mathematics without using math mode. But the formatting that LaTeX uses for text is inappropriate for math and vice versa. For example, LaTeX will too little or too much space around a text-mode plus sign. Other math will also be affected in text mode; for instance, variables may be typeset in the wrong font.
If you are trying to write inline math, write \\(…\\).
If you are trying to write display math, write \\[…\\] or use a dedicated math environment.
If you are defining a new math command, make sure to enclose the math part of its definition with \\ensuremath{…}.
`.substring(1),
/* eslint-enable max-len */
);
}
*see(token, stack) {
if (!stack.mathMode && token.text === '+') {
yield this.error(token);
}
}
}
import { Rule } from '../rule.js';
const LOWERCASE_LATIN_LETTERS = /^[b-z]$/u;
export class NoTextModeVariableRule extends Rule {
constructor() {
super(
'no-text-mode-variable',
'Unexpected variable-like single-letter character in text mode',
/* eslint-disable max-len */
`
Your code has a lowercase single letter in text mode that is not an English word, suggesting that you may be trying to write mathematics without using math mode. But the formatting that LaTeX uses for text is inappropriate for math, especially in regards to font choice. Other math will also be affected in text mode; for instance, operators may be typeset in the wrong font and with the wrong spacing.
If you are trying to write inline math, write \\(…\\).
If you are trying to write display math, write \\[…\\] or use a dedicated math environment.
If you are defining a new math command, make sure to enclose the math part of its definition with \\ensuremath{…}.
If you are trying to make a lettered list, use the enumerate environment, possibly customized with the enumitem package.
`.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.length === 1 && LOWERCASE_LATIN_LETTERS.test(token.text) &&
stack.has('document')) {
this.candidate = token;
} else {
this.candidate = undefined;
}
}
}
const NORMAL = Symbol('NORMAL');
const AFTER_TEXT = Symbol('AFTER_TEXT');
const AFTER_ENSURE_MATH = Symbol('AFTER_ENSURE_MATH');
const AFTER_BEGIN = Symbol('AFTER_BEGIN');
const AFTER_BEGIN_AND_CURLY = Symbol('AFTER_BEGIN_AND_CURLY');
const AFTER_END = Symbol('AFTER_END');
const AFTER_END_AND_CURLY = Symbol('AFTER_END_AND_CURLY');
const IGNORED = /^[\p{Z}\p{C}]*$|^%/u;
const MATH_MODE_STRUCTURES = new Set([
'$',
'$$',
'\\(\\)',
'\\[\\]',
'math',
'displaymath',
'equation',
'equation*',
'array',
'array*',
'eqnarray',
'eqnarray*',
'align',
'align*',
'alignat',
'alignat*',
]);
class Structure {
constructor(name, previousMathMode) {
this.name = name;
this.mathMode = MATH_MODE_STRUCTURES.has(name) ? true : previousMathMode;
}
}
export class StructureStack {
constructor() {
this.accumulator = '';
this.state = NORMAL;
this.stack = [];
}
get mathMode() {
return this.stack.length > 0 ? this.stack[this.stack.length - 1].mathMode : false;
}
has(environment) {
return this.stack.some((structure) => structure.name === environment);
}
_push(environment, forcedMathMode = undefined) {
this.stack.push(new Structure(environment, forcedMathMode !== undefined ? forcedMathMode : this.mathMode));
}
_pop(environment) {
if (this.has(environment)) {
while (this.stack.length > 0) {
if (this.stack.pop().name === environment) {
return true;
}
}
}
return false;
}
_toggle(environment) {
if (!this._pop(environment)) {
this._push(environment);
}
}
see(token) {
switch (this.state) {
case NORMAL:
switch (token.text) {
case '{':
this._push('{}');
break;
case '}':
this._pop('{}');
break;
case '$':
this._toggle('$');
break;
case '$$':
this._toggle('$$');
break;
case '\\(':
this._push('\\(\\)');
break;
case '\\)':
this._pop('\\(\\)');
break;
case '\\[':
this._push('\\[\\]');
break;
case '\\]':
this._pop('\\[\\]');
break;
case '\\text':
this.state = AFTER_TEXT;
break;
case '\\ensuremath':
this.state = AFTER_ENSURE_MATH;
break;
case '\\begin':
this.state = AFTER_BEGIN;
break;
case '\\end':
this.state = AFTER_END;
break;
default:
}
break;
case AFTER_TEXT:
if (token.text === '{') {
this._push('{}', false);
this.state = NORMAL;
} else if (!IGNORED.test(token.text)) {
this.state = NORMAL;
}
break;
case AFTER_ENSURE_MATH:
if (token.text === '{') {
this._push('{}', true);
this.state = NORMAL;
} else if (!IGNORED.test(token.text)) {
this.state = NORMAL;
}
break;
case AFTER_BEGIN:
if (token.text === '{') {
this.accumulator = '';
this.state = AFTER_BEGIN_AND_CURLY;
} else if (!IGNORED.test(token.text)) {
this.state = NORMAL;
}
break;
case AFTER_BEGIN_AND_CURLY:
if (!IGNORED.test(token.text)) {
if (token.text === '}') {
this._push(this.accumulator);
this.state = NORMAL;
}
this.accumulator += token.text;
}
break;
case AFTER_END:
if (token.text === '{') {
this.accumulator = '';
this.state = AFTER_END_AND_CURLY;
} else if (!IGNORED.test(token.text)) {
this.state = NORMAL;
}
break;
case AFTER_END_AND_CURLY:
if (!IGNORED.test(token.text)) {
if (token.text === '}') {
this._pop(this.accumulator);
this.state = NORMAL;
}
this.accumulator += token.text;
}
break;
default:
console.assert(false);
}
}
}
const NORMAL = Symbol('NORMAL');
const WITHIN_WORD = Symbol('WITHIN_WORD');
const ESCAPED = Symbol('ESCAPED');
const WITHIN_ESCAPE = Symbol('WITHIN_ESCAPE');
const AFTER_DOLLAR_SIGN = Symbol('AFTER_DOLLAR_SIGN');
const COMMENTED = Symbol('COMMENTED');
const LETTERS = /\p{L}/u;
class Token {
constructor(line, column) {
this.line = line;
this.column = column;
this.text = '';
}
}
class Tokenizer {
constructor() {
this.line = 1;
this.column = 0;
this.state = NORMAL;
this.token = undefined;
}
*_transition(beginsNewToken, character, newState) {
if (beginsNewToken) {
if (this.token !== undefined) {
yield this.token;
}
this.token = new Token(this.line, this.column);
}
this.token.text += character;
this.state = newState;
}
*_seeInCommmonSituation(character, letterBeginsNewToken, letterState) {
if (LETTERS.test(character)) {
yield* this._transition(letterBeginsNewToken, character, letterState);
} else {
switch (character) {
case '$':
yield* this._transition(true, character, AFTER_DOLLAR_SIGN);
break;
case '%':
yield* this._transition(true, character, COMMENTED);
break;
case '\\':
yield* this._transition(true, character, ESCAPED);
break;
default:
yield* this._transition(true, character, NORMAL);
break;
}
}
}
*see(character) {
switch (this.state) {
case NORMAL:
yield* this._seeInCommmonSituation(character, true, WITHIN_WORD);
break;
case WITHIN_WORD:
yield* this._seeInCommmonSituation(character, false, WITHIN_WORD);
break;
case ESCAPED:
yield* this._transition(false, character, LETTERS.test(character) ? WITHIN_ESCAPE : NORMAL);
break;
case WITHIN_ESCAPE:
yield* this._seeInCommmonSituation(character, false, WITHIN_ESCAPE);
break;
case AFTER_DOLLAR_SIGN:
if (character === '$') {
yield* this._transition(false, character, NORMAL);
} else {
yield* this._seeInCommmonSituation(character, true, WITHIN_WORD);
}
break;
case COMMENTED:
yield* this._transition(false, character, character === '\n' ? NORMAL : COMMENTED);
break;
default:
console.assert(false);
}
if (character === '\n') {
++this.line;
this.column = 0;
} else {
++this.column;
}
}
}
export function*tokenize(latex) {
const tokenizer = new Tokenizer();
for (const character of `${latex}\n`) {
yield* tokenizer.see(character);
}
}
This diff is collapsed.
{
"name": "@unlsoft/lint-latex",
"version": "1.0.0",
"description": "A simple linter to catch common student mistakes while learning LaTeX.",
"private": true,
"license": "UNLICENSED",
"scripts": {
"postinstall:eslint-config": "cd eslint-config && npm install",
"postinstall:linter": "cd lint-latex && npm install",
"postinstall": "run-s postinstall:**",
"lint:linter": "cd lint-latex && npm run lint",
"lint": "run-s --continue-on-error lint:**"
},
"devDependencies": {
"ghooks": "^2.0.4",
"npm-run-all": "^4.1.5"
},
"config": {
"ghooks": {
"pre-commit": "npm run lint"
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment