temml 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/contrib/auto-render/README.md +89 -0
- package/contrib/auto-render/auto-render.js +128 -0
- package/contrib/auto-render/dist/auto-render.js +217 -0
- package/contrib/auto-render/dist/auto-render.min.js +1 -0
- package/contrib/auto-render/splitAtDelimiters.js +84 -0
- package/contrib/auto-render/test/auto-render-spec.js +234 -0
- package/contrib/auto-render/test/auto-render.js +217 -0
- package/contrib/auto-render/test/test_page.html +59 -0
- package/contrib/mhchem/README.md +26 -0
- package/contrib/mhchem/mhchem.js +1705 -0
- package/contrib/mhchem/mhchem.min.js +1 -0
- package/contrib/physics/README.md +20 -0
- package/contrib/physics/physics.js +131 -0
- package/contrib/texvc/README.md +23 -0
- package/contrib/texvc/texvc.js +61 -0
- package/dist/Temml-Asana.css +201 -0
- package/dist/Temml-Latin-Modern.css +216 -0
- package/dist/Temml-Libertinus.css +214 -0
- package/dist/Temml-Local.css +194 -0
- package/dist/Temml-STIX2.css +203 -0
- package/dist/Temml.woff2 +0 -0
- package/dist/temml.cjs +13122 -0
- package/dist/temml.js +11225 -0
- package/dist/temml.min.js +1 -0
- package/dist/temml.mjs +13120 -0
- package/dist/temmlPostProcess.js +70 -0
- package/package.json +34 -0
- package/src/Lexer.js +121 -0
- package/src/MacroExpander.js +437 -0
- package/src/Namespace.js +107 -0
- package/src/ParseError.js +64 -0
- package/src/Parser.js +977 -0
- package/src/Settings.js +49 -0
- package/src/SourceLocation.js +29 -0
- package/src/Style.js +144 -0
- package/src/Token.js +40 -0
- package/src/buildMathML.js +235 -0
- package/src/constants.js +25 -0
- package/src/defineEnvironment.js +25 -0
- package/src/defineFunction.js +69 -0
- package/src/defineMacro.js +11 -0
- package/src/domTree.js +185 -0
- package/src/environments/array.js +791 -0
- package/src/environments/cd.js +252 -0
- package/src/environments.js +8 -0
- package/src/functions/accent.js +127 -0
- package/src/functions/accentunder.js +38 -0
- package/src/functions/arrow.js +204 -0
- package/src/functions/cancelto.js +36 -0
- package/src/functions/char.js +33 -0
- package/src/functions/color.js +253 -0
- package/src/functions/cr.js +46 -0
- package/src/functions/def.js +259 -0
- package/src/functions/delimsizing.js +304 -0
- package/src/functions/enclose.js +193 -0
- package/src/functions/envTag.js +38 -0
- package/src/functions/environment.js +59 -0
- package/src/functions/font.js +123 -0
- package/src/functions/genfrac.js +333 -0
- package/src/functions/hbox.js +29 -0
- package/src/functions/horizBrace.js +32 -0
- package/src/functions/href.js +90 -0
- package/src/functions/html.js +95 -0
- package/src/functions/includegraphics.js +131 -0
- package/src/functions/kern.js +75 -0
- package/src/functions/label.js +29 -0
- package/src/functions/lap.js +75 -0
- package/src/functions/math.js +40 -0
- package/src/functions/mathchoice.js +41 -0
- package/src/functions/mclass.js +201 -0
- package/src/functions/multiscript.js +91 -0
- package/src/functions/not.js +46 -0
- package/src/functions/op.js +338 -0
- package/src/functions/operatorname.js +139 -0
- package/src/functions/ordgroup.js +9 -0
- package/src/functions/phantom.js +73 -0
- package/src/functions/pmb.js +31 -0
- package/src/functions/raise.js +68 -0
- package/src/functions/ref.js +28 -0
- package/src/functions/relax.js +16 -0
- package/src/functions/rule.js +52 -0
- package/src/functions/sizing.js +64 -0
- package/src/functions/smash.js +66 -0
- package/src/functions/sqrt.js +31 -0
- package/src/functions/styling.js +58 -0
- package/src/functions/supsub.js +135 -0
- package/src/functions/symbolsOp.js +53 -0
- package/src/functions/symbolsOrd.js +102 -0
- package/src/functions/symbolsSpacing.js +53 -0
- package/src/functions/tag.js +8 -0
- package/src/functions/text.js +75 -0
- package/src/functions/tip.js +63 -0
- package/src/functions/toggle.js +13 -0
- package/src/functions/verb.js +33 -0
- package/src/functions.js +57 -0
- package/src/linebreaking.js +159 -0
- package/src/macros.js +708 -0
- package/src/mathMLTree.js +175 -0
- package/src/parseNode.js +42 -0
- package/src/parseTree.js +40 -0
- package/src/postProcess.js +57 -0
- package/src/replace.js +225 -0
- package/src/stretchy.js +66 -0
- package/src/svg.js +110 -0
- package/src/symbols.js +972 -0
- package/src/tree.js +50 -0
- package/src/unicodeAccents.js +16 -0
- package/src/unicodeScripts.js +119 -0
- package/src/unicodeSupOrSub.js +108 -0
- package/src/unicodeSymbolBuilder.js +31 -0
- package/src/unicodeSymbols.js +320 -0
- package/src/units.js +109 -0
- package/src/utils.js +109 -0
- package/src/variant.js +103 -0
- package/temml.js +181 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.temml = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/* Temml Post Process
|
|
8
|
+
* Perform two tasks not done by Temml when it created each individual Temml <math> element.
|
|
9
|
+
* Given a block,
|
|
10
|
+
* 1. At each AMS auto-numbered environment, assign an id.
|
|
11
|
+
* 2. Populate the text contents of each \ref & \eqref
|
|
12
|
+
*
|
|
13
|
+
* As with other Temml code, this file is released under terms of the MIT license.
|
|
14
|
+
* https://mit-license.org/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const version = "0.9.1";
|
|
18
|
+
|
|
19
|
+
function postProcess(block) {
|
|
20
|
+
const labelMap = {};
|
|
21
|
+
let i = 0;
|
|
22
|
+
|
|
23
|
+
// Get a collection of the parents of each \tag & auto-numbered equation
|
|
24
|
+
const parents = block.getElementsByClassName("tml-tageqn");
|
|
25
|
+
for (const parent of parents) {
|
|
26
|
+
const eqns = parent.getElementsByClassName("tml-eqn");
|
|
27
|
+
if (eqns. length > 0 ) {
|
|
28
|
+
// AMS automatically numbered equation.
|
|
29
|
+
// Assign an id.
|
|
30
|
+
i += 1;
|
|
31
|
+
eqns[0].id = "tml-eqn-" + i;
|
|
32
|
+
// No need to write a number into the text content of the element.
|
|
33
|
+
// A CSS counter does that even if this postProcess() function is not used.
|
|
34
|
+
}
|
|
35
|
+
// If there is a \label, add it to labelMap
|
|
36
|
+
const labels = parent.getElementsByClassName("tml-label");
|
|
37
|
+
if (labels.length === 0) { continue }
|
|
38
|
+
if (eqns.length > 0) {
|
|
39
|
+
labelMap[labels[0].id] = String(i);
|
|
40
|
+
} else {
|
|
41
|
+
const tags = parent.getElementsByClassName("tml-tag");
|
|
42
|
+
if (tags.length > 0) {
|
|
43
|
+
labelMap[labels[0].id] = tags[0].textContent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Populate \ref & \eqref text content
|
|
49
|
+
const refs = block.getElementsByClassName("tml-ref");
|
|
50
|
+
[...refs].forEach(ref => {
|
|
51
|
+
let str = labelMap[ref.getAttribute("href").slice(1)];
|
|
52
|
+
if (ref.className.indexOf("tml-eqref") === -1) {
|
|
53
|
+
// \ref. Omit parens.
|
|
54
|
+
str = str.replace(/^\(/, "");
|
|
55
|
+
str = str.replace(/\($/, "");
|
|
56
|
+
} {
|
|
57
|
+
// \eqref. Include parens
|
|
58
|
+
if (str.charAt(0) !== "(") { str = "(" + str; }
|
|
59
|
+
if (str.slice(-1) !== ")") { str = str + ")"; }
|
|
60
|
+
}
|
|
61
|
+
ref.textContent = str;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
exports.postProcess = postProcess;
|
|
66
|
+
exports.version = version;
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
69
|
+
|
|
70
|
+
}));
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "temml",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "TeX to MathML conversion in JavaScript.",
|
|
5
|
+
"main": "dist/temml.js",
|
|
6
|
+
"homepage": "https://temml.org",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git://github.com/ronkok/Temml"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"temml.js",
|
|
13
|
+
"src/",
|
|
14
|
+
"contrib/",
|
|
15
|
+
"dist/"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"eslint": "^8.7.0",
|
|
20
|
+
"esm": "^3.2.25",
|
|
21
|
+
"rollup": "^2.66.1",
|
|
22
|
+
"terser": "^5.14.2"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"lint": "eslint temml.js src",
|
|
26
|
+
"unit-test": "node -r esm ./test/unit-test.js",
|
|
27
|
+
"visual-test": "node utils/buildTests.js",
|
|
28
|
+
"test": "yarn lint && node utils/buildTests.js && yarn unit-test",
|
|
29
|
+
"minify": "terser test/temml.js -o site/assets/temml.min.js -c -m && terser contrib/mhchem/mhchem.js -o site/assets/mhchem.min.js -c -m",
|
|
30
|
+
"build": "rollup --config ./utils/rollupConfig.js && yarn minify && node utils/insertPlugins.js",
|
|
31
|
+
"docs": "node utils/buildDocs.js",
|
|
32
|
+
"dist": "yarn build && node ./utils/copyfiles.js && terser contrib/auto-render/test/auto-render.js -o contrib/auto-render/dist/auto-render.min.js -c -m"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/Lexer.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Lexer class handles tokenizing the input in various ways. Since our
|
|
3
|
+
* parser expects us to be able to backtrack, the lexer allows lexing from any
|
|
4
|
+
* given starting point.
|
|
5
|
+
*
|
|
6
|
+
* Its main exposed function is the `lex` function, which takes a position to
|
|
7
|
+
* lex from and a type of token to lex. It defers to the appropriate `_innerLex`
|
|
8
|
+
* function.
|
|
9
|
+
*
|
|
10
|
+
* The various `_innerLex` functions perform the actual lexing of different
|
|
11
|
+
* kinds.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import ParseError from "./ParseError";
|
|
15
|
+
import SourceLocation from "./SourceLocation";
|
|
16
|
+
import { Token } from "./Token";
|
|
17
|
+
|
|
18
|
+
/* The following tokenRegex
|
|
19
|
+
* - matches typical whitespace (but not NBSP etc.) using its first two groups
|
|
20
|
+
* - does not match any control character \x00-\x1f except whitespace
|
|
21
|
+
* - does not match a bare backslash
|
|
22
|
+
* - matches any ASCII character except those just mentioned
|
|
23
|
+
* - does not match the BMP private use area \uE000-\uF8FF
|
|
24
|
+
* - does not match bare surrogate code units
|
|
25
|
+
* - matches any BMP character except for those just described
|
|
26
|
+
* - matches any valid Unicode surrogate pair
|
|
27
|
+
* - mathches numerals
|
|
28
|
+
* - matches a backslash followed by one or more whitespace characters
|
|
29
|
+
* - matches a backslash followed by one or more letters then whitespace
|
|
30
|
+
* - matches a backslash followed by any BMP character
|
|
31
|
+
* Capturing groups:
|
|
32
|
+
* [1] regular whitespace
|
|
33
|
+
* [2] backslash followed by whitespace
|
|
34
|
+
* [3] anything else, which may include:
|
|
35
|
+
* [4] left character of \verb*
|
|
36
|
+
* [5] left character of \verb
|
|
37
|
+
* [6] backslash followed by word, excluding any trailing whitespace
|
|
38
|
+
* Just because the Lexer matches something doesn't mean it's valid input:
|
|
39
|
+
* If there is no matching function or symbol definition, the Parser will
|
|
40
|
+
* still reject the input.
|
|
41
|
+
*/
|
|
42
|
+
const spaceRegexString = "[ \r\n\t]";
|
|
43
|
+
const controlWordRegexString = "\\\\[a-zA-Z@]+";
|
|
44
|
+
const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]";
|
|
45
|
+
const controlWordWhitespaceRegexString = `(${controlWordRegexString})${spaceRegexString}*`
|
|
46
|
+
const controlSpaceRegexString = "\\\\(\n|[ \r\t]+\n?)[ \r\t]*";
|
|
47
|
+
const combiningDiacriticalMarkString = "[\u0300-\u036f]";
|
|
48
|
+
export const combiningDiacriticalMarksEndRegex = new RegExp(`${combiningDiacriticalMarkString}+$`);
|
|
49
|
+
const tokenRegexString =
|
|
50
|
+
`(${spaceRegexString}+)|` + // whitespace
|
|
51
|
+
`${controlSpaceRegexString}|` + // whitespace
|
|
52
|
+
"(number" + // numbers (in non-strict mode)
|
|
53
|
+
"|[!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint
|
|
54
|
+
`${combiningDiacriticalMarkString}*` + // ...plus accents
|
|
55
|
+
"|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair
|
|
56
|
+
`${combiningDiacriticalMarkString}*` + // ...plus accents
|
|
57
|
+
"|\\\\verb\\*([^]).*?\\4" + // \verb*
|
|
58
|
+
"|\\\\verb([^*a-zA-Z]).*?\\5" + // \verb unstarred
|
|
59
|
+
`|${controlWordWhitespaceRegexString}` + // \macroName + spaces
|
|
60
|
+
`|${controlSymbolRegexString})`; // \\, \', etc.
|
|
61
|
+
|
|
62
|
+
/** Main Lexer class */
|
|
63
|
+
export default class Lexer {
|
|
64
|
+
constructor(input, settings) {
|
|
65
|
+
// Separate accents from characters
|
|
66
|
+
this.input = input;
|
|
67
|
+
this.settings = settings;
|
|
68
|
+
this.tokenRegex = new RegExp(
|
|
69
|
+
// Strict Temml, like TeX, lexes one numeral at a time.
|
|
70
|
+
// Default Temml lexes contiguous numerals into a single <mn> element.
|
|
71
|
+
tokenRegexString.replace("number|", settings.strict ? "" : "\\d(?:[\\d,.]*\\d)?|"),
|
|
72
|
+
"g"
|
|
73
|
+
);
|
|
74
|
+
// Category codes. The lexer only supports comment characters (14) for now.
|
|
75
|
+
// MacroExpander additionally distinguishes active (13).
|
|
76
|
+
this.catcodes = {
|
|
77
|
+
"%": 14, // comment character
|
|
78
|
+
"~": 13 // active character
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setCatcode(char, code) {
|
|
83
|
+
this.catcodes[char] = code;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This function lexes a single token.
|
|
88
|
+
*/
|
|
89
|
+
lex() {
|
|
90
|
+
const input = this.input;
|
|
91
|
+
const pos = this.tokenRegex.lastIndex;
|
|
92
|
+
if (pos === input.length) {
|
|
93
|
+
return new Token("EOF", new SourceLocation(this, pos, pos));
|
|
94
|
+
}
|
|
95
|
+
const match = this.tokenRegex.exec(input);
|
|
96
|
+
if (match === null || match.index !== pos) {
|
|
97
|
+
throw new ParseError(
|
|
98
|
+
`Unexpected character: '${input[pos]}'`,
|
|
99
|
+
new Token(input[pos], new SourceLocation(this, pos, pos + 1))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const text = match[6] || match[3] || (match[2] ? "\\ " : " ")
|
|
103
|
+
|
|
104
|
+
if (this.catcodes[text] === 14) {
|
|
105
|
+
// comment character
|
|
106
|
+
const nlIndex = input.indexOf("\n", this.tokenRegex.lastIndex);
|
|
107
|
+
if (nlIndex === -1) {
|
|
108
|
+
this.tokenRegex.lastIndex = input.length; // EOF
|
|
109
|
+
if (this.settings.strict) {
|
|
110
|
+
throw new ParseError("% comment has no terminating newline; LaTeX would " +
|
|
111
|
+
"fail because of commenting the end of math mode")
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
this.tokenRegex.lastIndex = nlIndex + 1;
|
|
115
|
+
}
|
|
116
|
+
return this.lex();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return new Token(text, new SourceLocation(this, pos, this.tokenRegex.lastIndex));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the “gullet” where macros are expanded
|
|
3
|
+
* until only non-macro tokens remain.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import functions from "./functions";
|
|
7
|
+
import symbols from "./symbols";
|
|
8
|
+
import Lexer from "./Lexer";
|
|
9
|
+
import { Token } from "./Token";
|
|
10
|
+
|
|
11
|
+
import ParseError from "./ParseError";
|
|
12
|
+
import Namespace from "./Namespace";
|
|
13
|
+
import macros from "./macros";
|
|
14
|
+
|
|
15
|
+
// List of commands that act like macros but aren't defined as a macro,
|
|
16
|
+
// function, or symbol. Used in `isDefined`.
|
|
17
|
+
export const implicitCommands = {
|
|
18
|
+
"^": true, // Parser.js
|
|
19
|
+
_: true, // Parser.js
|
|
20
|
+
"\\limits": true, // Parser.js
|
|
21
|
+
"\\nolimits": true // Parser.js
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default class MacroExpander {
|
|
25
|
+
constructor(input, settings, mode) {
|
|
26
|
+
this.settings = settings;
|
|
27
|
+
this.expansionCount = 0;
|
|
28
|
+
this.feed(input);
|
|
29
|
+
// Make new global namespace
|
|
30
|
+
this.macros = new Namespace(macros, settings.macros);
|
|
31
|
+
this.mode = mode;
|
|
32
|
+
this.stack = []; // contains tokens in REVERSE order
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Feed a new input string to the same MacroExpander
|
|
37
|
+
* (with existing macros etc.).
|
|
38
|
+
*/
|
|
39
|
+
feed(input) {
|
|
40
|
+
this.lexer = new Lexer(input, this.settings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Switches between "text" and "math" modes.
|
|
45
|
+
*/
|
|
46
|
+
switchMode(newMode) {
|
|
47
|
+
this.mode = newMode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start a new group nesting within all namespaces.
|
|
52
|
+
*/
|
|
53
|
+
beginGroup() {
|
|
54
|
+
this.macros.beginGroup();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* End current group nesting within all namespaces.
|
|
59
|
+
*/
|
|
60
|
+
endGroup() {
|
|
61
|
+
this.macros.endGroup();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the topmost token on the stack, without expanding it.
|
|
66
|
+
* Similar in behavior to TeX's `\futurelet`.
|
|
67
|
+
*/
|
|
68
|
+
future() {
|
|
69
|
+
if (this.stack.length === 0) {
|
|
70
|
+
this.pushToken(this.lexer.lex())
|
|
71
|
+
}
|
|
72
|
+
return this.stack[this.stack.length - 1]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove and return the next unexpanded token.
|
|
77
|
+
*/
|
|
78
|
+
popToken() {
|
|
79
|
+
this.future(); // ensure non-empty stack
|
|
80
|
+
return this.stack.pop();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add a given token to the token stack. In particular, this get be used
|
|
85
|
+
* to put back a token returned from one of the other methods.
|
|
86
|
+
*/
|
|
87
|
+
pushToken(token) {
|
|
88
|
+
this.stack.push(token);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Append an array of tokens to the token stack.
|
|
93
|
+
*/
|
|
94
|
+
pushTokens(tokens) {
|
|
95
|
+
this.stack.push(...tokens);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Find an macro argument without expanding tokens and append the array of
|
|
100
|
+
* tokens to the token stack. Uses Token as a container for the result.
|
|
101
|
+
*/
|
|
102
|
+
scanArgument(isOptional) {
|
|
103
|
+
let start;
|
|
104
|
+
let end;
|
|
105
|
+
let tokens;
|
|
106
|
+
if (isOptional) {
|
|
107
|
+
this.consumeSpaces(); // \@ifnextchar gobbles any space following it
|
|
108
|
+
if (this.future().text !== "[") {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
start = this.popToken(); // don't include [ in tokens
|
|
112
|
+
({ tokens, end } = this.consumeArg(["]"]));
|
|
113
|
+
} else {
|
|
114
|
+
({ tokens, start, end } = this.consumeArg());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// indicate the end of an argument
|
|
118
|
+
this.pushToken(new Token("EOF", end.loc));
|
|
119
|
+
|
|
120
|
+
this.pushTokens(tokens);
|
|
121
|
+
return start.range(end, "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Consume all following space tokens, without expansion.
|
|
126
|
+
*/
|
|
127
|
+
consumeSpaces() {
|
|
128
|
+
for (;;) {
|
|
129
|
+
const token = this.future();
|
|
130
|
+
if (token.text === " ") {
|
|
131
|
+
this.stack.pop();
|
|
132
|
+
} else {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Consume an argument from the token stream, and return the resulting array
|
|
140
|
+
* of tokens and start/end token.
|
|
141
|
+
*/
|
|
142
|
+
consumeArg(delims) {
|
|
143
|
+
// The argument for a delimited parameter is the shortest (possibly
|
|
144
|
+
// empty) sequence of tokens with properly nested {...} groups that is
|
|
145
|
+
// followed ... by this particular list of non-parameter tokens.
|
|
146
|
+
// The argument for an undelimited parameter is the next nonblank
|
|
147
|
+
// token, unless that token is ‘{’, when the argument will be the
|
|
148
|
+
// entire {...} group that follows.
|
|
149
|
+
const tokens = [];
|
|
150
|
+
const isDelimited = delims && delims.length > 0;
|
|
151
|
+
if (!isDelimited) {
|
|
152
|
+
// Ignore spaces between arguments. As the TeXbook says:
|
|
153
|
+
// "After you have said ‘\def\row#1#2{...}’, you are allowed to
|
|
154
|
+
// put spaces between the arguments (e.g., ‘\row x n’), because
|
|
155
|
+
// TeX doesn’t use single spaces as undelimited arguments."
|
|
156
|
+
this.consumeSpaces();
|
|
157
|
+
}
|
|
158
|
+
const start = this.future();
|
|
159
|
+
let tok;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let match = 0;
|
|
162
|
+
do {
|
|
163
|
+
tok = this.popToken();
|
|
164
|
+
tokens.push(tok);
|
|
165
|
+
if (tok.text === "{") {
|
|
166
|
+
++depth;
|
|
167
|
+
} else if (tok.text === "}") {
|
|
168
|
+
--depth;
|
|
169
|
+
if (depth === -1) {
|
|
170
|
+
throw new ParseError("Extra }", tok);
|
|
171
|
+
}
|
|
172
|
+
} else if (tok.text === "EOF") {
|
|
173
|
+
throw new ParseError(
|
|
174
|
+
"Unexpected end of input in a macro argument" +
|
|
175
|
+
", expected '" +
|
|
176
|
+
(delims && isDelimited ? delims[match] : "}") +
|
|
177
|
+
"'",
|
|
178
|
+
tok
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (delims && isDelimited) {
|
|
182
|
+
if ((depth === 0 || (depth === 1 && delims[match] === "{")) && tok.text === delims[match]) {
|
|
183
|
+
++match;
|
|
184
|
+
if (match === delims.length) {
|
|
185
|
+
// don't include delims in tokens
|
|
186
|
+
tokens.splice(-match, match);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
match = 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} while (depth !== 0 || isDelimited);
|
|
194
|
+
// If the argument found ... has the form ‘{<nested tokens>}’,
|
|
195
|
+
// ... the outermost braces enclosing the argument are removed
|
|
196
|
+
if (start.text === "{" && tokens[tokens.length - 1].text === "}") {
|
|
197
|
+
tokens.pop();
|
|
198
|
+
tokens.shift();
|
|
199
|
+
}
|
|
200
|
+
tokens.reverse(); // to fit in with stack order
|
|
201
|
+
return { tokens, start, end: tok };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Consume the specified number of (delimited) arguments from the token
|
|
206
|
+
* stream and return the resulting array of arguments.
|
|
207
|
+
*/
|
|
208
|
+
consumeArgs(numArgs, delimiters) {
|
|
209
|
+
if (delimiters) {
|
|
210
|
+
if (delimiters.length !== numArgs + 1) {
|
|
211
|
+
throw new ParseError("The length of delimiters doesn't match the number of args!");
|
|
212
|
+
}
|
|
213
|
+
const delims = delimiters[0];
|
|
214
|
+
for (let i = 0; i < delims.length; i++) {
|
|
215
|
+
const tok = this.popToken();
|
|
216
|
+
if (delims[i] !== tok.text) {
|
|
217
|
+
throw new ParseError("Use of the macro doesn't match its definition", tok);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const args = [];
|
|
223
|
+
for (let i = 0; i < numArgs; i++) {
|
|
224
|
+
args.push(this.consumeArg(delimiters && delimiters[i + 1]).tokens);
|
|
225
|
+
}
|
|
226
|
+
return args;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Expand the next token only once if possible.
|
|
231
|
+
*
|
|
232
|
+
* If the token is expanded, the resulting tokens will be pushed onto
|
|
233
|
+
* the stack in reverse order and will be returned as an array,
|
|
234
|
+
* also in reverse order.
|
|
235
|
+
*
|
|
236
|
+
* If not, the next token will be returned without removing it
|
|
237
|
+
* from the stack. This case can be detected by a `Token` return value
|
|
238
|
+
* instead of an `Array` return value.
|
|
239
|
+
*
|
|
240
|
+
* In either case, the next token will be on the top of the stack,
|
|
241
|
+
* or the stack will be empty.
|
|
242
|
+
*
|
|
243
|
+
* Used to implement `expandAfterFuture` and `expandNextToken`.
|
|
244
|
+
*
|
|
245
|
+
* If expandableOnly, only expandable tokens are expanded and
|
|
246
|
+
* an undefined control sequence results in an error.
|
|
247
|
+
*/
|
|
248
|
+
expandOnce(expandableOnly) {
|
|
249
|
+
const topToken = this.popToken();
|
|
250
|
+
const name = topToken.text;
|
|
251
|
+
const expansion = !topToken.noexpand ? this._getExpansion(name) : null;
|
|
252
|
+
if (expansion == null || (expandableOnly && expansion.unexpandable)) {
|
|
253
|
+
if (expandableOnly && expansion == null && name[0] === "\\" && !this.isDefined(name)) {
|
|
254
|
+
throw new ParseError("Undefined control sequence: " + name);
|
|
255
|
+
}
|
|
256
|
+
this.pushToken(topToken);
|
|
257
|
+
return topToken;
|
|
258
|
+
}
|
|
259
|
+
this.expansionCount++;
|
|
260
|
+
if (this.expansionCount > this.settings.maxExpand) {
|
|
261
|
+
throw new ParseError(
|
|
262
|
+
"Too many expansions: infinite loop or " + "need to increase maxExpand setting"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
let tokens = expansion.tokens;
|
|
266
|
+
const args = this.consumeArgs(expansion.numArgs, expansion.delimiters);
|
|
267
|
+
if (expansion.numArgs) {
|
|
268
|
+
// paste arguments in place of the placeholders
|
|
269
|
+
tokens = tokens.slice(); // make a shallow copy
|
|
270
|
+
for (let i = tokens.length - 1; i >= 0; --i) {
|
|
271
|
+
let tok = tokens[i];
|
|
272
|
+
if (tok.text === "#") {
|
|
273
|
+
if (i === 0) {
|
|
274
|
+
throw new ParseError("Incomplete placeholder at end of macro body", tok);
|
|
275
|
+
}
|
|
276
|
+
tok = tokens[--i]; // next token on stack
|
|
277
|
+
if (tok.text === "#") {
|
|
278
|
+
// ## → #
|
|
279
|
+
tokens.splice(i + 1, 1); // drop first #
|
|
280
|
+
} else if (/^[1-9]$/.test(tok.text)) {
|
|
281
|
+
// replace the placeholder with the indicated argument
|
|
282
|
+
tokens.splice(i, 2, ...args[+tok.text - 1]);
|
|
283
|
+
} else {
|
|
284
|
+
throw new ParseError("Not a valid argument number", tok);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Concatenate expansion onto top of stack.
|
|
290
|
+
this.pushTokens(tokens);
|
|
291
|
+
return tokens;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Expand the next token only once (if possible), and return the resulting
|
|
296
|
+
* top token on the stack (without removing anything from the stack).
|
|
297
|
+
* Similar in behavior to TeX's `\expandafter\futurelet`.
|
|
298
|
+
* Equivalent to expandOnce() followed by future().
|
|
299
|
+
*/
|
|
300
|
+
expandAfterFuture() {
|
|
301
|
+
this.expandOnce();
|
|
302
|
+
return this.future();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Recursively expand first token, then return first non-expandable token.
|
|
307
|
+
*/
|
|
308
|
+
expandNextToken() {
|
|
309
|
+
for (;;) {
|
|
310
|
+
const expanded = this.expandOnce();
|
|
311
|
+
// expandOnce returns Token if and only if it's fully expanded.
|
|
312
|
+
if (expanded instanceof Token) {
|
|
313
|
+
// The token after \noexpand is interpreted as if its meaning were ‘\relax’
|
|
314
|
+
if (expanded.treatAsRelax) {
|
|
315
|
+
expanded.text = "\\relax"
|
|
316
|
+
}
|
|
317
|
+
return this.stack.pop(); // === expanded
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// This pathway is impossible.
|
|
322
|
+
throw new Error(); // eslint-disable-line no-unreachable
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Fully expand the given macro name and return the resulting list of
|
|
327
|
+
* tokens, or return `undefined` if no such macro is defined.
|
|
328
|
+
*/
|
|
329
|
+
expandMacro(name) {
|
|
330
|
+
return this.macros.has(name) ? this.expandTokens([new Token(name)]) : undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Fully expand the given token stream and return the resulting list of
|
|
335
|
+
* tokens. Note that the input tokens are in reverse order, but the
|
|
336
|
+
* output tokens are in forward order.
|
|
337
|
+
*/
|
|
338
|
+
expandTokens(tokens) {
|
|
339
|
+
const output = [];
|
|
340
|
+
const oldStackLength = this.stack.length;
|
|
341
|
+
this.pushTokens(tokens);
|
|
342
|
+
while (this.stack.length > oldStackLength) {
|
|
343
|
+
const expanded = this.expandOnce(true); // expand only expandable tokens
|
|
344
|
+
// expandOnce returns Token if and only if it's fully expanded.
|
|
345
|
+
if (expanded instanceof Token) {
|
|
346
|
+
if (expanded.treatAsRelax) {
|
|
347
|
+
// the expansion of \noexpand is the token itself
|
|
348
|
+
expanded.noexpand = false;
|
|
349
|
+
expanded.treatAsRelax = false;
|
|
350
|
+
}
|
|
351
|
+
output.push(this.stack.pop());
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Fully expand the given macro name and return the result as a string,
|
|
359
|
+
* or return `undefined` if no such macro is defined.
|
|
360
|
+
*/
|
|
361
|
+
expandMacroAsText(name) {
|
|
362
|
+
const tokens = this.expandMacro(name);
|
|
363
|
+
if (tokens) {
|
|
364
|
+
return tokens.map((token) => token.text).join("");
|
|
365
|
+
} else {
|
|
366
|
+
return tokens;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Returns the expanded macro as a reversed array of tokens and a macro
|
|
372
|
+
* argument count. Or returns `null` if no such macro.
|
|
373
|
+
*/
|
|
374
|
+
_getExpansion(name) {
|
|
375
|
+
const definition = this.macros.get(name);
|
|
376
|
+
if (definition == null) {
|
|
377
|
+
// mainly checking for undefined here
|
|
378
|
+
return definition;
|
|
379
|
+
}
|
|
380
|
+
// If a single character has an associated catcode other than 13
|
|
381
|
+
// (active character), then don't expand it.
|
|
382
|
+
if (name.length === 1) {
|
|
383
|
+
const catcode = this.lexer.catcodes[name]
|
|
384
|
+
if (catcode != null && catcode !== 13) {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const expansion = typeof definition === "function" ? definition(this) : definition;
|
|
389
|
+
if (typeof expansion === "string") {
|
|
390
|
+
let numArgs = 0;
|
|
391
|
+
if (expansion.indexOf("#") !== -1) {
|
|
392
|
+
const stripped = expansion.replace(/##/g, "");
|
|
393
|
+
while (stripped.indexOf("#" + (numArgs + 1)) !== -1) {
|
|
394
|
+
++numArgs;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const bodyLexer = new Lexer(expansion, this.settings);
|
|
398
|
+
const tokens = [];
|
|
399
|
+
let tok = bodyLexer.lex();
|
|
400
|
+
while (tok.text !== "EOF") {
|
|
401
|
+
tokens.push(tok);
|
|
402
|
+
tok = bodyLexer.lex();
|
|
403
|
+
}
|
|
404
|
+
tokens.reverse(); // to fit in with stack using push and pop
|
|
405
|
+
const expanded = { tokens, numArgs };
|
|
406
|
+
return expanded;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return expansion;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Determine whether a command is currently "defined" (has some
|
|
414
|
+
* functionality), meaning that it's a macro (in the current group),
|
|
415
|
+
* a function, a symbol, or one of the special commands listed in
|
|
416
|
+
* `implicitCommands`.
|
|
417
|
+
*/
|
|
418
|
+
isDefined(name) {
|
|
419
|
+
return (
|
|
420
|
+
this.macros.has(name) ||
|
|
421
|
+
Object.prototype.hasOwnProperty.call(functions, name ) ||
|
|
422
|
+
Object.prototype.hasOwnProperty.call(symbols.math, name ) ||
|
|
423
|
+
Object.prototype.hasOwnProperty.call(symbols.text, name ) ||
|
|
424
|
+
Object.prototype.hasOwnProperty.call(implicitCommands, name )
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Determine whether a command is expandable.
|
|
430
|
+
*/
|
|
431
|
+
isExpandable(name) {
|
|
432
|
+
const macro = this.macros.get(name);
|
|
433
|
+
return macro != null
|
|
434
|
+
? typeof macro === "string" || typeof macro === "function" || !macro.unexpandable
|
|
435
|
+
: Object.prototype.hasOwnProperty.call(functions, name ) && !functions[name].primitive;
|
|
436
|
+
}
|
|
437
|
+
}
|