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,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
+ });