temml 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/contrib/auto-render/README.md +89 -0
  4. package/contrib/auto-render/auto-render.js +128 -0
  5. package/contrib/auto-render/dist/auto-render.js +217 -0
  6. package/contrib/auto-render/dist/auto-render.min.js +1 -0
  7. package/contrib/auto-render/splitAtDelimiters.js +84 -0
  8. package/contrib/auto-render/test/auto-render-spec.js +234 -0
  9. package/contrib/auto-render/test/auto-render.js +217 -0
  10. package/contrib/auto-render/test/test_page.html +59 -0
  11. package/contrib/mhchem/README.md +26 -0
  12. package/contrib/mhchem/mhchem.js +1705 -0
  13. package/contrib/mhchem/mhchem.min.js +1 -0
  14. package/contrib/physics/README.md +20 -0
  15. package/contrib/physics/physics.js +131 -0
  16. package/contrib/texvc/README.md +23 -0
  17. package/contrib/texvc/texvc.js +61 -0
  18. package/dist/Temml-Asana.css +201 -0
  19. package/dist/Temml-Latin-Modern.css +216 -0
  20. package/dist/Temml-Libertinus.css +214 -0
  21. package/dist/Temml-Local.css +194 -0
  22. package/dist/Temml-STIX2.css +203 -0
  23. package/dist/Temml.woff2 +0 -0
  24. package/dist/temml.cjs +13122 -0
  25. package/dist/temml.js +11225 -0
  26. package/dist/temml.min.js +1 -0
  27. package/dist/temml.mjs +13120 -0
  28. package/dist/temmlPostProcess.js +70 -0
  29. package/package.json +34 -0
  30. package/src/Lexer.js +121 -0
  31. package/src/MacroExpander.js +437 -0
  32. package/src/Namespace.js +107 -0
  33. package/src/ParseError.js +64 -0
  34. package/src/Parser.js +977 -0
  35. package/src/Settings.js +49 -0
  36. package/src/SourceLocation.js +29 -0
  37. package/src/Style.js +144 -0
  38. package/src/Token.js +40 -0
  39. package/src/buildMathML.js +235 -0
  40. package/src/constants.js +25 -0
  41. package/src/defineEnvironment.js +25 -0
  42. package/src/defineFunction.js +69 -0
  43. package/src/defineMacro.js +11 -0
  44. package/src/domTree.js +185 -0
  45. package/src/environments/array.js +791 -0
  46. package/src/environments/cd.js +252 -0
  47. package/src/environments.js +8 -0
  48. package/src/functions/accent.js +127 -0
  49. package/src/functions/accentunder.js +38 -0
  50. package/src/functions/arrow.js +204 -0
  51. package/src/functions/cancelto.js +36 -0
  52. package/src/functions/char.js +33 -0
  53. package/src/functions/color.js +253 -0
  54. package/src/functions/cr.js +46 -0
  55. package/src/functions/def.js +259 -0
  56. package/src/functions/delimsizing.js +304 -0
  57. package/src/functions/enclose.js +193 -0
  58. package/src/functions/envTag.js +38 -0
  59. package/src/functions/environment.js +59 -0
  60. package/src/functions/font.js +123 -0
  61. package/src/functions/genfrac.js +333 -0
  62. package/src/functions/hbox.js +29 -0
  63. package/src/functions/horizBrace.js +32 -0
  64. package/src/functions/href.js +90 -0
  65. package/src/functions/html.js +95 -0
  66. package/src/functions/includegraphics.js +131 -0
  67. package/src/functions/kern.js +75 -0
  68. package/src/functions/label.js +29 -0
  69. package/src/functions/lap.js +75 -0
  70. package/src/functions/math.js +40 -0
  71. package/src/functions/mathchoice.js +41 -0
  72. package/src/functions/mclass.js +201 -0
  73. package/src/functions/multiscript.js +91 -0
  74. package/src/functions/not.js +46 -0
  75. package/src/functions/op.js +338 -0
  76. package/src/functions/operatorname.js +139 -0
  77. package/src/functions/ordgroup.js +9 -0
  78. package/src/functions/phantom.js +73 -0
  79. package/src/functions/pmb.js +31 -0
  80. package/src/functions/raise.js +68 -0
  81. package/src/functions/ref.js +28 -0
  82. package/src/functions/relax.js +16 -0
  83. package/src/functions/rule.js +52 -0
  84. package/src/functions/sizing.js +64 -0
  85. package/src/functions/smash.js +66 -0
  86. package/src/functions/sqrt.js +31 -0
  87. package/src/functions/styling.js +58 -0
  88. package/src/functions/supsub.js +135 -0
  89. package/src/functions/symbolsOp.js +53 -0
  90. package/src/functions/symbolsOrd.js +102 -0
  91. package/src/functions/symbolsSpacing.js +53 -0
  92. package/src/functions/tag.js +8 -0
  93. package/src/functions/text.js +75 -0
  94. package/src/functions/tip.js +63 -0
  95. package/src/functions/toggle.js +13 -0
  96. package/src/functions/verb.js +33 -0
  97. package/src/functions.js +57 -0
  98. package/src/linebreaking.js +159 -0
  99. package/src/macros.js +708 -0
  100. package/src/mathMLTree.js +175 -0
  101. package/src/parseNode.js +42 -0
  102. package/src/parseTree.js +40 -0
  103. package/src/postProcess.js +57 -0
  104. package/src/replace.js +225 -0
  105. package/src/stretchy.js +66 -0
  106. package/src/svg.js +110 -0
  107. package/src/symbols.js +972 -0
  108. package/src/tree.js +50 -0
  109. package/src/unicodeAccents.js +16 -0
  110. package/src/unicodeScripts.js +119 -0
  111. package/src/unicodeSupOrSub.js +108 -0
  112. package/src/unicodeSymbolBuilder.js +31 -0
  113. package/src/unicodeSymbols.js +320 -0
  114. package/src/units.js +109 -0
  115. package/src/utils.js +109 -0
  116. package/src/variant.js +103 -0
  117. 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
+ }