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.
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
+ }