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,253 @@
1
+ import defineFunction, { ordargument } from "../defineFunction"
2
+ import { wrapWithMstyle } from "../mathMLTree"
3
+ import { assertNodeType } from "../parseNode"
4
+ import ParseError from "../ParseError"
5
+ import * as mml from "../buildMathML"
6
+
7
+ // Helpers
8
+ const htmlRegEx = /^(#[a-f0-9]{3}|#?[a-f0-9]{6})$/i
9
+ const htmlOrNameRegEx = /^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i
10
+ const RGBregEx = /^ *\d{1,3} *(?:, *\d{1,3} *){2}$/
11
+ const rgbRegEx = /^ *[10](?:\.\d*)? *(?:, *[10](?:\.\d*)? *){2}$/
12
+ const xcolorHtmlRegEx = /^[a-f0-9]{6}$/i
13
+ const toHex = num => {
14
+ let str = num.toString(16)
15
+ if (str.length === 1) { str = "0" + str }
16
+ return str
17
+ }
18
+
19
+ // Colors from Tables 4.1 and 4.2 of the xcolor package.
20
+ // Table 4.1 (lower case) RGB values are taken from chroma and xcolor.dtx.
21
+ // Table 4.2 (Capitalizzed) values were sampled, because Chroma contains a unreliable
22
+ // conversion from cmyk to RGB. See https://tex.stackexchange.com/a/537274.
23
+ const xcolors = JSON.parse(`{
24
+ "Apricot": "#ffb484",
25
+ "Aquamarine": "#08b4bc",
26
+ "Bittersweet": "#c84c14",
27
+ "blue": "#0000FF",
28
+ "Blue": "#303494",
29
+ "BlueGreen": "#08b4bc",
30
+ "BlueViolet": "#503c94",
31
+ "BrickRed": "#b8341c",
32
+ "brown": "#BF8040",
33
+ "Brown": "#802404",
34
+ "BurntOrange": "#f8941c",
35
+ "CadetBlue": "#78749c",
36
+ "CarnationPink": "#f884b4",
37
+ "Cerulean": "#08a4e4",
38
+ "CornflowerBlue": "#40ace4",
39
+ "cyan": "#00FFFF",
40
+ "Cyan": "#08acec",
41
+ "Dandelion": "#ffbc44",
42
+ "darkgray": "#404040",
43
+ "DarkOrchid": "#a8548c",
44
+ "Emerald": "#08ac9c",
45
+ "ForestGreen": "#089c54",
46
+ "Fuchsia": "#90348c",
47
+ "Goldenrod": "#ffdc44",
48
+ "gray": "#808080",
49
+ "Gray": "#98949c",
50
+ "green": "#00FF00",
51
+ "Green": "#08a44c",
52
+ "GreenYellow": "#e0e474",
53
+ "JungleGreen": "#08ac9c",
54
+ "Lavender": "#f89cc4",
55
+ "lightgray": "#c0c0c0",
56
+ "lime": "#BFFF00",
57
+ "LimeGreen": "#90c43c",
58
+ "magenta": "#FF00FF",
59
+ "Magenta": "#f0048c",
60
+ "Mahogany": "#b0341c",
61
+ "Maroon": "#b03434",
62
+ "Melon": "#f89c7c",
63
+ "MidnightBlue": "#086494",
64
+ "Mulberry": "#b03c94",
65
+ "NavyBlue": "#086cbc",
66
+ "olive": "#7F7F00",
67
+ "OliveGreen": "#407c34",
68
+ "orange": "#FF8000",
69
+ "Orange": "#f8843c",
70
+ "OrangeRed": "#f0145c",
71
+ "Orchid": "#b074ac",
72
+ "Peach": "#f8945c",
73
+ "Periwinkle": "#8074bc",
74
+ "PineGreen": "#088c74",
75
+ "pink": "#ff7f7f",
76
+ "Plum": "#98248c",
77
+ "ProcessBlue": "#08b4ec",
78
+ "purple": "#BF0040",
79
+ "Purple": "#a0449c",
80
+ "RawSienna": "#983c04",
81
+ "red": "#ff0000",
82
+ "Red": "#f01c24",
83
+ "RedOrange": "#f86434",
84
+ "RedViolet": "#a0246c",
85
+ "Rhodamine": "#f0549c",
86
+ "Royallue": "#0874bc",
87
+ "RoyalPurple": "#683c9c",
88
+ "RubineRed": "#f0047c",
89
+ "Salmon": "#f8948c",
90
+ "SeaGreen": "#30bc9c",
91
+ "Sepia": "#701404",
92
+ "SkyBlue": "#48c4dc",
93
+ "SpringGreen": "#c8dc64",
94
+ "Tan": "#e09c74",
95
+ "teal": "#007F7F",
96
+ "TealBlue": "#08acb4",
97
+ "Thistle": "#d884b4",
98
+ "Turquoise": "#08b4cc",
99
+ "violet": "#800080",
100
+ "Violet": "#60449c",
101
+ "VioletRed": "#f054a4",
102
+ "WildStrawberry": "#f0246c",
103
+ "yellow": "#FFFF00",
104
+ "Yellow": "#fff404",
105
+ "YellowGreen": "#98cc6c",
106
+ "YellowOrange": "#ffa41c"
107
+ }`)
108
+
109
+ export const colorFromSpec = (model, spec) => {
110
+ let color = ""
111
+ if (model === "HTML") {
112
+ if (!htmlRegEx.test(spec)) {
113
+ throw new ParseError("Invalid HTML input.")
114
+ }
115
+ color = spec
116
+ } else if (model === "RGB") {
117
+ if (!RGBregEx.test(spec)) {
118
+ throw new ParseError("Invalid RGB input.")
119
+ }
120
+ spec.split(",").map(e => { color += toHex(Number(e.trim())) })
121
+ } else {
122
+ if (!rgbRegEx.test(spec)) {
123
+ throw new ParseError("Invalid rbg input.")
124
+ }
125
+ spec.split(",").map(e => {
126
+ const num = Number(e.trim())
127
+ if (num > 1) { throw new ParseError("Color rgb input must be < 1.") }
128
+ color += toHex((num * 255))
129
+ })
130
+ }
131
+ if (color.charAt(0) !== "#") { color = "#" + color }
132
+ return color
133
+ }
134
+
135
+ export const validateColor = (color, macros, token) => {
136
+ const macroName = `\\\\color@${color}` // from \defineColor.
137
+ const match = htmlOrNameRegEx.exec(color);
138
+ if (!match) { throw new ParseError("Invalid color: '" + color + "'", token) }
139
+ // We allow a 6-digit HTML color spec without a leading "#".
140
+ // This follows the xcolor package's HTML color model.
141
+ // Predefined color names are all missed by this RegEx pattern.
142
+ if (xcolorHtmlRegEx.test(color)) {
143
+ return "#" + color
144
+ } else if (color.charAt(0) === "#") {
145
+ return color
146
+ } else if (macros.has(macroName)) {
147
+ color = macros.get(macroName).tokens[0].text
148
+ } else if (xcolors[color]) {
149
+ color = xcolors[color]
150
+ }
151
+ return color
152
+ }
153
+
154
+ const mathmlBuilder = (group, style) => {
155
+ const inner = mml.buildExpression(group.body, style.withColor(group.color))
156
+ // Wrap with an <mstyle> element.
157
+ const node = wrapWithMstyle(inner)
158
+ node.setAttribute("mathcolor", group.color)
159
+ return node
160
+ }
161
+
162
+ defineFunction({
163
+ type: "color",
164
+ names: ["\\textcolor"],
165
+ props: {
166
+ numArgs: 2,
167
+ numOptionalArgs: 1,
168
+ allowedInText: true,
169
+ argTypes: ["raw", "raw", "original"]
170
+ },
171
+ handler({ parser, token }, args, optArgs) {
172
+ const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string
173
+ let color = ""
174
+ if (model) {
175
+ const spec = assertNodeType(args[0], "raw").string
176
+ color = colorFromSpec(model, spec)
177
+ } else {
178
+ color = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros, token)
179
+ }
180
+ const body = args[1];
181
+ return {
182
+ type: "color",
183
+ mode: parser.mode,
184
+ color,
185
+ body: ordargument(body)
186
+ }
187
+ },
188
+ mathmlBuilder
189
+ })
190
+
191
+ defineFunction({
192
+ type: "color",
193
+ names: ["\\color"],
194
+ props: {
195
+ numArgs: 1,
196
+ numOptionalArgs: 1,
197
+ allowedInText: true,
198
+ argTypes: ["raw", "raw"]
199
+ },
200
+ handler({ parser, token }, args, optArgs) {
201
+ const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string
202
+ let color = ""
203
+ if (model) {
204
+ const spec = assertNodeType(args[0], "raw").string
205
+ color = colorFromSpec(model, spec)
206
+ } else {
207
+ color = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros, token)
208
+ }
209
+
210
+ // Set macro \current@color in current namespace to store the current
211
+ // color, mimicking the behavior of color.sty.
212
+ // This is currently used just to correctly color a \right
213
+ // that follows a \color command.
214
+ parser.gullet.macros.set("\\current@color", color)
215
+
216
+ // Parse out the implicit body that should be colored.
217
+ // Since \color nodes should not be nested, break on \color.
218
+ const body = parser.parseExpression(true, "\\color")
219
+
220
+ return {
221
+ type: "color",
222
+ mode: parser.mode,
223
+ color,
224
+ body
225
+ }
226
+ },
227
+ mathmlBuilder
228
+ })
229
+
230
+ defineFunction({
231
+ type: "color",
232
+ names: ["\\definecolor"],
233
+ props: {
234
+ numArgs: 3,
235
+ allowedInText: true,
236
+ argTypes: ["raw", "raw", "raw"]
237
+ },
238
+ handler({ parser, funcName, token }, args) {
239
+ const name = assertNodeType(args[0], "raw").string
240
+ if (!/^[A-Za-z]+$/.test(name)) {
241
+ throw new ParseError("Color name must be latin letters.", token)
242
+ }
243
+ const model = assertNodeType(args[1], "raw").string
244
+ if (!["HTML", "RGB", "rgb"].includes(model)) {
245
+ throw new ParseError("Color model must be HTML, RGB, or rgb.", token)
246
+ }
247
+ const spec = assertNodeType(args[2], "raw").string
248
+ const color = colorFromSpec(model, spec)
249
+ parser.gullet.macros.set(`\\\\color@${name}`, { tokens: [{ text: color }], numArgs: 0 })
250
+ return { type: "internal", mode: parser.mode }
251
+ }
252
+ // No mathmlBuilder. The point of \definecolor is to set a macro.
253
+ })
@@ -0,0 +1,46 @@
1
+ // Row breaks within tabular environments, and line breaks at top level
2
+
3
+ import defineFunction from "../defineFunction"
4
+ import mathMLTree from "../mathMLTree"
5
+ import { calculateSize } from "../units"
6
+ import { assertNodeType } from "../parseNode"
7
+
8
+ // \DeclareRobustCommand\\{...\@xnewline}
9
+ defineFunction({
10
+ type: "cr",
11
+ names: ["\\\\"],
12
+ props: {
13
+ numArgs: 0,
14
+ numOptionalArgs: 1,
15
+ argTypes: ["size"],
16
+ allowedInText: true
17
+ },
18
+
19
+ handler({ parser }, args, optArgs) {
20
+ const size = optArgs[0];
21
+ const newLine = !parser.settings.displayMode;
22
+ return {
23
+ type: "cr",
24
+ mode: parser.mode,
25
+ newLine,
26
+ size: size && assertNodeType(size, "size").value
27
+ }
28
+ },
29
+
30
+ // The following builder is called only at the top level,
31
+ // not within tabular/array environments.
32
+
33
+ mathmlBuilder(group, style) {
34
+ // MathML 3.0 calls for newline to occur in an <mo> or an <mspace>.
35
+ // Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.linebreaking
36
+ const node = new mathMLTree.MathNode("mo")
37
+ if (group.newLine) {
38
+ node.setAttribute("linebreak", "newline")
39
+ if (group.size) {
40
+ const size = calculateSize(group.size, style)
41
+ node.setAttribute("height", size.number + size.unit)
42
+ }
43
+ }
44
+ return node
45
+ }
46
+ })
@@ -0,0 +1,259 @@
1
+ import defineFunction from "../defineFunction";
2
+ import ParseError from "../ParseError";
3
+ import { assertNodeType } from "../parseNode";
4
+
5
+ const globalMap = {
6
+ "\\global": "\\global",
7
+ "\\long": "\\\\globallong",
8
+ "\\\\globallong": "\\\\globallong",
9
+ "\\def": "\\gdef",
10
+ "\\gdef": "\\gdef",
11
+ "\\edef": "\\xdef",
12
+ "\\xdef": "\\xdef",
13
+ "\\let": "\\\\globallet",
14
+ "\\futurelet": "\\\\globalfuture"
15
+ }
16
+
17
+ const checkControlSequence = (tok) => {
18
+ const name = tok.text;
19
+ if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) {
20
+ throw new ParseError("Expected a control sequence", tok);
21
+ }
22
+ return name;
23
+ };
24
+
25
+ const getRHS = (parser) => {
26
+ let tok = parser.gullet.popToken();
27
+ if (tok.text === "=") {
28
+ // consume optional equals
29
+ tok = parser.gullet.popToken();
30
+ if (tok.text === " ") {
31
+ // consume one optional space
32
+ tok = parser.gullet.popToken();
33
+ }
34
+ }
35
+ return tok;
36
+ };
37
+
38
+ const letCommand = (parser, name, tok, global) => {
39
+ let macro = parser.gullet.macros.get(tok.text);
40
+ if (macro == null) {
41
+ // don't expand it later even if a macro with the same name is defined
42
+ // e.g., \let\foo=\frac \def\frac{\relax} \frac12
43
+ tok.noexpand = true;
44
+ macro = {
45
+ tokens: [tok],
46
+ numArgs: 0,
47
+ // reproduce the same behavior in expansion
48
+ unexpandable: !parser.gullet.isExpandable(tok.text)
49
+ };
50
+ }
51
+ parser.gullet.macros.set(name, macro, global);
52
+ };
53
+
54
+ // <assignment> -> <non-macro assignment>|<macro assignment>
55
+ // <non-macro assignment> -> <simple assignment>|\global<non-macro assignment>
56
+ // <macro assignment> -> <definition>|<prefix><macro assignment>
57
+ // <prefix> -> \global|\long|\outer
58
+ defineFunction({
59
+ type: "internal",
60
+ names: [
61
+ "\\global",
62
+ "\\long",
63
+ "\\\\globallong" // can’t be entered directly
64
+ ],
65
+ props: {
66
+ numArgs: 0,
67
+ allowedInText: true
68
+ },
69
+ handler({ parser, funcName }) {
70
+ parser.consumeSpaces();
71
+ const token = parser.fetch();
72
+ if (globalMap[token.text]) {
73
+ // Temml doesn't have \par, so ignore \long
74
+ if (funcName === "\\global" || funcName === "\\\\globallong") {
75
+ token.text = globalMap[token.text];
76
+ }
77
+ return assertNodeType(parser.parseFunction(), "internal");
78
+ }
79
+ throw new ParseError(`Invalid token after macro prefix`, token);
80
+ }
81
+ });
82
+
83
+ // Basic support for macro definitions: \def, \gdef, \edef, \xdef
84
+ // <definition> -> <def><control sequence><definition text>
85
+ // <def> -> \def|\gdef|\edef|\xdef
86
+ // <definition text> -> <parameter text><left brace><balanced text><right brace>
87
+ defineFunction({
88
+ type: "internal",
89
+ names: ["\\def", "\\gdef", "\\edef", "\\xdef"],
90
+ props: {
91
+ numArgs: 0,
92
+ allowedInText: true,
93
+ primitive: true
94
+ },
95
+ handler({ parser, funcName }) {
96
+ let tok = parser.gullet.popToken();
97
+ const name = tok.text;
98
+ if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) {
99
+ throw new ParseError("Expected a control sequence", tok);
100
+ }
101
+
102
+ let numArgs = 0;
103
+ let insert;
104
+ const delimiters = [[]];
105
+ // <parameter text> contains no braces
106
+ while (parser.gullet.future().text !== "{") {
107
+ tok = parser.gullet.popToken();
108
+ if (tok.text === "#") {
109
+ // If the very last character of the <parameter text> is #, so that
110
+ // this # is immediately followed by {, TeX will behave as if the {
111
+ // had been inserted at the right end of both the parameter text
112
+ // and the replacement text.
113
+ if (parser.gullet.future().text === "{") {
114
+ insert = parser.gullet.future();
115
+ delimiters[numArgs].push("{");
116
+ break;
117
+ }
118
+
119
+ // A parameter, the first appearance of # must be followed by 1,
120
+ // the next by 2, and so on; up to nine #’s are allowed
121
+ tok = parser.gullet.popToken();
122
+ if (!/^[1-9]$/.test(tok.text)) {
123
+ throw new ParseError(`Invalid argument number "${tok.text}"`);
124
+ }
125
+ if (parseInt(tok.text) !== numArgs + 1) {
126
+ throw new ParseError(`Argument number "${tok.text}" out of order`);
127
+ }
128
+ numArgs++;
129
+ delimiters.push([]);
130
+ } else if (tok.text === "EOF") {
131
+ throw new ParseError("Expected a macro definition");
132
+ } else {
133
+ delimiters[numArgs].push(tok.text);
134
+ }
135
+ }
136
+ // replacement text, enclosed in '{' and '}' and properly nested
137
+ let { tokens } = parser.gullet.consumeArg();
138
+ if (insert) {
139
+ tokens.unshift(insert);
140
+ }
141
+
142
+ if (funcName === "\\edef" || funcName === "\\xdef") {
143
+ tokens = parser.gullet.expandTokens(tokens);
144
+ tokens.reverse(); // to fit in with stack order
145
+ }
146
+ // Final arg is the expansion of the macro
147
+ parser.gullet.macros.set(
148
+ name,
149
+ { tokens, numArgs, delimiters },
150
+ funcName === globalMap[funcName]
151
+ );
152
+ return { type: "internal", mode: parser.mode };
153
+ }
154
+ });
155
+
156
+ // <simple assignment> -> <let assignment>
157
+ // <let assignment> -> \futurelet<control sequence><token><token>
158
+ // | \let<control sequence><equals><one optional space><token>
159
+ // <equals> -> <optional spaces>|<optional spaces>=
160
+ defineFunction({
161
+ type: "internal",
162
+ names: [
163
+ "\\let",
164
+ "\\\\globallet" // can’t be entered directly
165
+ ],
166
+ props: {
167
+ numArgs: 0,
168
+ allowedInText: true,
169
+ primitive: true
170
+ },
171
+ handler({ parser, funcName }) {
172
+ const name = checkControlSequence(parser.gullet.popToken());
173
+ parser.gullet.consumeSpaces();
174
+ const tok = getRHS(parser);
175
+ letCommand(parser, name, tok, funcName === "\\\\globallet");
176
+ return { type: "internal", mode: parser.mode };
177
+ }
178
+ });
179
+
180
+ // ref: https://www.tug.org/TUGboat/tb09-3/tb22bechtolsheim.pdf
181
+ defineFunction({
182
+ type: "internal",
183
+ names: [
184
+ "\\futurelet",
185
+ "\\\\globalfuture" // can’t be entered directly
186
+ ],
187
+ props: {
188
+ numArgs: 0,
189
+ allowedInText: true,
190
+ primitive: true
191
+ },
192
+ handler({ parser, funcName }) {
193
+ const name = checkControlSequence(parser.gullet.popToken());
194
+ const middle = parser.gullet.popToken();
195
+ const tok = parser.gullet.popToken();
196
+ letCommand(parser, name, tok, funcName === "\\\\globalfuture");
197
+ parser.gullet.pushToken(tok);
198
+ parser.gullet.pushToken(middle);
199
+ return { type: "internal", mode: parser.mode };
200
+ }
201
+ });
202
+
203
+ defineFunction({
204
+ type: "internal",
205
+ names: ["\\newcommand", "\\renewcommand", "\\providecommand"],
206
+ props: {
207
+ numArgs: 0,
208
+ allowedInText: true,
209
+ primitive: true
210
+ },
211
+ handler({ parser, funcName }) {
212
+ let name = ""
213
+ const tok = parser.gullet.popToken()
214
+ if (tok.text === "{") {
215
+ name = checkControlSequence(parser.gullet.popToken())
216
+ parser.gullet.popToken()
217
+ } else {
218
+ name = checkControlSequence(tok)
219
+ }
220
+
221
+ const exists = parser.gullet.isDefined(name);
222
+ if (exists && funcName === "\\newcommand") {
223
+ throw new ParseError(
224
+ `\\newcommand{${name}} attempting to redefine ${name}; use \\renewcommand`
225
+ );
226
+ }
227
+ if (!exists && funcName === "\\renewcommand") {
228
+ throw new ParseError(
229
+ `\\renewcommand{${name}} when command ${name} does not yet exist; use \\newcommand`
230
+ );
231
+ }
232
+
233
+ let numArgs = 0;
234
+ if (parser.gullet.future().text === "[") {
235
+ let tok = parser.gullet.popToken();
236
+ tok = parser.gullet.popToken();
237
+ if (!/^[0-9]$/.test(tok.text)) {
238
+ throw new ParseError(`Invalid number of arguments: "${tok.text}"`);
239
+ }
240
+ numArgs = parseInt(tok.text);
241
+ tok = parser.gullet.popToken();
242
+ if (tok.text !== "]") {
243
+ throw new ParseError(`Invalid argument "${tok.text}"`);
244
+ }
245
+ }
246
+
247
+ // replacement text, enclosed in '{' and '}' and properly nested
248
+ const { tokens } = parser.gullet.consumeArg();
249
+
250
+ parser.gullet.macros.set(
251
+ name,
252
+ { tokens, numArgs },
253
+ !parser.settings.strict
254
+ )
255
+
256
+ return { type: "internal", mode: parser.mode };
257
+
258
+ }
259
+ });