temml 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|