@wavelace/formula 0.1.0

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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/index.d.ts +49 -0
  4. package/index.js +1161 -0
  5. package/package.json +37 -0
package/index.js ADDED
@@ -0,0 +1,1161 @@
1
+ /*! @wavelace/formula 0.1.0 | MIT | © 2026 Daniele Moraschi | generated from js/latex.js and js/formula.js */
2
+ const window = {};
3
+ /* Wavelace · latex — a LaTeX subset translated into the formula language
4
+ * reads: nothing
5
+ * exports: fromLatex, eachBinder, dummiesOf, NAME, LETTER, WORD, NOT_IN_NAME, NAME_END, LETTER_OR_TH, SUBSCRIPTED_LETTER, NOT_AFTER_LETTER,
6
+ * GREEK, GREEK_SYMBOLS, GREEK_VARIANTS, foldVariant, COMMANDS, FUNCTIONS, BINDERS, INVERSE, CALLABLE, callShape
7
+ *
8
+ * So that textbook notation pastes straight in: \frac, \sqrt, powers in braces, \sin and
9
+ * friends, \pi and \theta, the other Greek letters as names of the formula's own (each becomes its
10
+ * glyph, so \omega is ω from here on, a letter to every scanner below and a free name to formula.js;
11
+ * LETTER is the one class that says what a letter is), a subscripted letter as a name of its own
12
+ * (x_1, \omega_0, x_{max}: the identifier x_1, and free too), 90^\circ and a pasted 90° as deg(90),
13
+ * \cdot, \left( … \right), e^{…}, and
14
+ * \int_a^b … du, which becomes the formula language's integral(…, u, a, b). LaTeX's silent products (2x, 2\pi x,
15
+ * \sin x \cos y) become explicit, and that rule runs for every formula, so cos(3x − t) works
16
+ * whether or not a backslash is in sight. Anything left over that starts with a backslash is
17
+ * reported by name rather than failing as an illegal character. The scanner of the binder calls —
18
+ * integral, sum, prod and diff, which each bind an expression to a dummy, the first three over a
19
+ * range and diff at the point the outer variable holds — and the names they bind are exported for
20
+ * formula.js, which reads this file. */
21
+ (() => {
22
+ "use strict";
23
+ const WL = window.Wavelace = window.Wavelace || {};
24
+
25
+ // The Greek letters that are names here, each as its glyph. Not the ones that already stand for
26
+ // something: \pi, \theta, \vartheta and \tau are quantities, \Gamma the gamma function, and \Sigma and
27
+ // Pi the operators, since the pasted Σ already means \sum and a plate printing Σ would paste back as
28
+ // a sum (GREEK_SYMBOLS below is that list as glyphs). Not the ones a reader cannot tell from Latin:
29
+ // omicron reads as o and Upsilon as Y, and a dial that reads as Y and is not Y is the silent kind of
30
+ // wrong. \varpi is π in another hand. The \var… forms are their plain letter, one dial each. Since
31
+ // the glyph is the name, \gamma and gamma never meet.
32
+ const GREEK = {alpha:"α", beta:"β", gamma:"γ", delta:"δ", epsilon:"ε", varepsilon:"ε", zeta:"ζ", eta:"η",
33
+ iota:"ι", kappa:"κ", varkappa:"κ", lambda:"λ", mu:"μ", nu:"ν", xi:"ξ", rho:"ρ", varrho:"ρ",
34
+ sigma:"σ", varsigma:"σ", upsilon:"υ", phi:"φ", varphi:"φ", chi:"χ", psi:"ψ", omega:"ω",
35
+ Delta:"Δ", Theta:"Θ", Lambda:"Λ", Xi:"Ξ", Phi:"Φ", Psi:"Ψ", Omega:"Ω"};
36
+ const GREEK_LETTERS = [...new Set(Object.values(GREEK))].join("");
37
+ // the pasted Greek that stands for a command: normalize (formula.js) turns each into that command,
38
+ // so one reader owns them and \int_0^\pi takes π as one limit, and what the plate prints pastes back
39
+ const GREEK_SYMBOLS = {"θ": "theta", "ϑ": "theta", "π": "pi", "τ": "tau", "Γ": "Gamma", "Σ": "sum", "Π": "prod"};
40
+ // and the pasted glyphs that are one of the letters above in another hand, folded by normalize
41
+ // before any scanner reads them, so one letter is one dial however it was typed: the variant forms,
42
+ // and what a keyboard types for Δ, Ω and μ (the increment sign, the ohm sign and the micro sign)
43
+ const GREEK_VARIANTS = {"ϵ": "ε", "ϕ": "φ", "ϱ": "ρ", "ς": "σ", "ϰ": "κ", "ϐ": "β", "ϴ": "Θ",
44
+ "∆": "Δ", "Ω": "Ω", "µ": "μ"};
45
+ const foldVariant = glyph => Object.hasOwn(GREEK_VARIANTS, glyph) ? GREEK_VARIANTS[glyph] : glyph; // the letter a glyph is
46
+ // what a letter is, to every scanner in this file and in formula.js: the table's own glyphs and
47
+ // no range, so that a Greek capital that looks Latin (Ε, Α, Κ) names no command and stays an
48
+ // illegal character rather than a dial that reads as E. WORD is what may continue a name;
49
+ // NOT_IN_NAME and NAME_END are the edges of one, the lookarounds that \b, ASCII in JS, is not,
50
+ // and every scanner that matches a name by spelling is built from them.
51
+ const LETTER = `A-Za-z${GREEK_LETTERS}`;
52
+ const WORD = `${LETTER}0-9_`;
53
+ const NOT_IN_NAME = `(?<![${WORD}])`, NAME_END = `(?![${WORD}])`;
54
+ const TOKEN_CHARS = `${WORD}.`; // what a run of letters, digits and dots is made of: 2x, x_1, 2.5
55
+ // a name of the formula's own: a letter, or th (the two letters θ is spelled with), with or without a
56
+ // subscript of letters and digits; what a free name (formula.js) and an atom of the silent product are.
57
+ // Its edge on the left is a letter or an underscore and not a digit, since 2x_1 is one glued run until
58
+ // implicitProducts puts the * in, and the passes that read the shape run before that one.
59
+ const LETTER_OR_TH = `(?:th|[${LETTER}])`;
60
+ const SUBSCRIPTED_LETTER = `${LETTER_OR_TH}(?:_[${LETTER}0-9]+)?`;
61
+ const NOT_AFTER_LETTER = `(?<![${LETTER}_])`;
62
+ const COMMANDS = {...GREEK,
63
+ sin:"sin", cos:"cos", tan:"tan", arcsin:"asin", arccos:"acos", arctan:"atan", sinh:"sinh", cosh:"cosh",
64
+ tanh:"tanh", exp:"exp", ln:"log", log:"log", min:"min", max:"max", sqrt:"sqrt",
65
+ cdot:"*", times:"*", div:"/", pi:"pi", theta:"th", vartheta:"th", tau:"tau", infty:"inf",
66
+ sec:"sec", csc:"csc", cot:"cot", sum:"sum", prod:"prod",
67
+ Gamma:"gamma", // the gamma function; lowercase \gamma is the letter γ, in GREEK above
68
+ arcsinh:"asinh", arccosh:"acosh", arctanh:"atanh",
69
+ arsinh:"asinh", arcosh:"acosh", artanh:"atanh",
70
+ // the fences are named commands standing for a call, like cdot stands for *; by
71
+ // the time this table is read the \left or \right before one has been stripped
72
+ lfloor:"floor(", rfloor:")", lceil:"ceil(", rceil:")",
73
+ // the relations, which are only meaningful inside a ternary: x \le 1 ? x : 1
74
+ le:"<=", leq:"<=", ge:">=", geq:">=", ne:"!=", neq:"!="};
75
+ const FUNCTIONS = ["sin","cos","tan","asin","acos","atan","atan2","sinh","cosh","tanh","exp","log","log2","log10",
76
+ "sqrt","cbrt","abs","sign","floor","ceil","round","min","max","pow","hypot",
77
+ "gamma","factorial","combinations","erf","besselJ","superformula","sinc","deg","sec","csc","cot",
78
+ "asinh","acosh","atanh"];
79
+ // the ones whose first argument is an expression bound to a dummy: their parts, and what to paste
80
+ // instead of writing the call. They are not in FUNCTIONS — applyFunctions would take that
81
+ // expression for an atom — but they are callable. Three bind their dummy over a range and are
82
+ // given it; diff differentiates at the point the outer variable already holds, so it needs nothing
83
+ // after the name. One entry per head, the way every other table in this file is keyed: the part
84
+ // names give the arity by their count and the signature by their spelling, so a binder is added
85
+ // here and nowhere else.
86
+ const OVER_RANGE = ["expression", "variable", "from", "to"];
87
+ const BINDERS = {
88
+ integral: {parts: OVER_RANGE, paste: String.raw`\int_a^b … du`},
89
+ sum: {parts: OVER_RANGE, paste: String.raw`\sum_{k=1}^{n}`},
90
+ prod: {parts: OVER_RANGE, paste: String.raw`\prod_{k=1}^{n}`},
91
+ diff: {parts: ["expression", "variable"], paste: String.raw`\frac{d}{dx}`},
92
+ };
93
+ const BINDER_NAMES = Object.keys(BINDERS);
94
+ const IN_WORDS = ["", "one", "two", "three", "four"];
95
+ const callShape = head => `${head}(${BINDERS[head].parts.join(", ")})`;
96
+ // every name that may head a call, which operandEnd needs to know so that e^sin(x) is exp(sin(x))
97
+ // and \sum of a \sum takes the inner call whole. Not FUNCTIONS ∪ BINDERS: FUNCTIONS is the set
98
+ // that may be applied to a bare atom, and mod is deliberately absent from it (mod x means
99
+ // nothing) while mod(x, 2) is a call like any other. The invariant: every function-valued key of
100
+ // ENV in formula.js belongs here.
101
+ const CALLABLE = new Set([...FUNCTIONS, ...BINDER_NAMES, "mod"]);
102
+ // sin^{-1} is asin, not 1/sin — only the names INVERSE lists; anything else carrying a ^{-1} is
103
+ // refused by name (applyFunctions), since reading it as a reciprocal would be a guess.
104
+ const INVERSE = {sin:"asin", cos:"acos", tan:"atan", sinh:"asinh", cosh:"acosh", tanh:"atanh"};
105
+ const INVERSES_OFFERED = Object.keys(INVERSE).map(f => `${f}^{-1}`).join(", ");
106
+ const MINUS_ONE = /^\^(?:\(\s*-\s*1\s*\)|-1)$/; // ^{-1}, the pasted ⁻¹, and the unbraced ^-1
107
+ // The separator translateCommands leaves where the author wrote none. A command and a pasted symbol
108
+ // both expand to a padded name — \theta to " th " — so that x\theta cannot become the single name
109
+ // xth; but padding is a space the author did not type, and atomEnd reads a contiguous run, so
110
+ // \sin 2\theta ended its atom at the 2 and drew sin(2)·th. LaTeX settles which spaces are real: one
111
+ // space after a control word terminates the name and means nothing, so 2\theta and 2\theta{} are the
112
+ // same atom while the typed space of \sin 2 \theta separates, as sin 2 x does. This character carries
113
+ // that distinction from translateCommands to atomEnd, which is the only scanner that reads it
114
+ // differently; skipSpaces takes it for a space, and fromLatex turns every one back into a space
115
+ // before implicitProducts, so nothing downstream of the atom scanners can tell it was ever here.
116
+ const GLUE = "\u0001";
117
+ const TOKEN = new RegExp(`^[${TOKEN_CHARS}]+`), NAME = new RegExp(`^[${LETTER}][${LETTER}0-9]*$`); // a run of letters, digits and dots (2x, x_1, 2.5); a name
118
+ // names that multiply whatever follows them: the constants spelled with more than one letter (th,
119
+ // pi, tau, inf, read off COMMANDS so one added there multiplies without a second edit) and any
120
+ // SUBSCRIPTED_LETTER, a plot variable, a constant or a free name (formula.js), an atom as a
121
+ // binder's dummy already was
122
+ const NAMED_ATOMS = [...new Set(Object.values(COMMANDS))].filter(n => n.length > 1 && NAME.test(n) && !CALLABLE.has(n));
123
+ const ATOMS = [...NAMED_ATOMS, SUBSCRIPTED_LETTER].join("|");
124
+ const EXPONENT = new RegExp(`^-?[${TOKEN_CHARS}]+`); // a token, and an exponent may be negative
125
+ // the end-anchored forms of the two above, for the one pass that scans backwards: what may sit
126
+ // before the ( of a call (a subscripted name too, so x_1(y)! is what x(y)! is), and the run that is
127
+ // an operand on its own. BANG skips the ! of !=.
128
+ // the operand run reads over GLUE as atomEnd does, so 2\theta° is (2θ)° as 2x° is (2x)°
129
+ const BANG = /!(?!=)/, DEGREE = /°/, NAME_BEFORE = new RegExp(`[${LETTER}][${WORD}]*$`);
130
+ const OPERAND_BEFORE = new RegExp(`[${TOKEN_CHARS}]+(?:${GLUE}+[${TOKEN_CHARS}]+)*$`);
131
+ const LIMIT = new RegExp(String.raw`^(?:\\[A-Za-z]+|\d+(?:\.\d+)?|[${LETTER}])`); // a bare limit, as LaTeX reads it: \infty, a number, one letter
132
+
133
+ /* ---------- scanning ---------- */
134
+ // the index just past the bracket group opening at text[i] ("(" or "{")
135
+ function groupEnd(text, i){
136
+ const open = text[i], close = open === "(" ? ")" : "}";
137
+ let depth = 0;
138
+ for(let k = i; k < text.length; k++){
139
+ if(text[k] === open) depth++;
140
+ else if(text[k] === close && --depth === 0) return k + 1;
141
+ }
142
+ throw new Error("unbalanced brackets in formula");
143
+ }
144
+ // the index of the "(" matching the ")" at text[k]: groupEnd read backwards, for the one pass that
145
+ // scans that way (a postfix operator has to find where the thing before it began)
146
+ function groupStart(text, k){
147
+ let depth = 0;
148
+ for(let i = k; i >= 0; i--){
149
+ if(text[i] === ")") depth++;
150
+ else if(text[i] === "(" && --depth === 0) return i;
151
+ }
152
+ throw new Error("unbalanced brackets in formula");
153
+ }
154
+ // where the operand ending at text[k] begins: a bracket group, carrying the name of the call in
155
+ // front of it when there is one, or the run of letters, digits and dots that stands on its own.
156
+ // sign is the postfix asking, ! or °, so the refusal names it
157
+ function operandStart(text, k, sign){
158
+ if(text[k] === ")"){
159
+ const open = groupStart(text, k);
160
+ const j = skipSpacesBack(text, open - 1); // \sin(x)° arrives as " sin (x)°", the name padded
161
+ const name = NAME_BEFORE.exec(text.slice(0, j + 1)); // sin(x)! is the factorial of sin(x)
162
+ if(!name) return open;
163
+ const adjacent = j === open - 1, known = CALLABLE.has(name[0]); // across padding, only a name the language knows
164
+ return adjacent || known ? j + 1 - name[0].length : open;
165
+ }
166
+ const operand = OPERAND_BEFORE.exec(text.slice(0, k + 1));
167
+ if(!operand) throw new Error(`${sign} needs a number, a name or a bracket before it: 5${sign}, x${sign}, (x + 1)${sign}`);
168
+ return k + 1 - operand[0].length;
169
+ }
170
+ // where the operand starting at text[i] ends: the mirror of operandStart. A bracket group, or a
171
+ // known function with its own bracket taken whole, or else an atom. Only a name the language knows
172
+ // absorbs what follows it, which is what keeps e^t(x) meaning exp(t)·x while e^sin(x) is exp(sin(x)).
173
+ function operandEnd(text, i){
174
+ const m = TOKEN.exec(text.slice(i));
175
+ if(m && CALLABLE.has(m[0])){
176
+ const call = skipSpaces(text, i + m[0].length);
177
+ if(text[call] === "(") return groupEnd(text, call);
178
+ }
179
+ return atomEnd(text, i);
180
+ }
181
+ // a power written ^(…) or ^token at text[i]; returns its end, or i when there is none. The exponent
182
+ // may lead with a minus, which TOKEN does not: x^-2 is the spelling half of LaTeX writes, and
183
+ // without it ^{-1} would be read as an inverse while ^-1 quietly became a product.
184
+ function powerEnd(text, i){
185
+ if(text[i] !== "^") return i;
186
+ if(text[i + 1] === "(") return groupEnd(text, i + 1);
187
+ const m = EXPONENT.exec(text.slice(i + 1));
188
+ return m ? i + 1 + m[0].length : i;
189
+ }
190
+ // an atom, what a bare function or e^ applies to: a bracket group, or a run of letters and digits
191
+ // carrying its own powers (2x, x^2, th), continued over every GLUE, which stands where the author
192
+ // wrote nothing: 2\theta is the one atom 2θ. Returns its end, or i when there is none.
193
+ function atomEnd(text, i){
194
+ if(text[i] === "(") return groupEnd(text, i);
195
+ let k = i;
196
+ for(;;){
197
+ const m = TOKEN.exec(text.slice(k));
198
+ if(!m) break; // nothing here, and on the first pass k is still i
199
+ k += m[0].length;
200
+ // A power binds to the token before it over GLUE as well, so \tan\theta^2 is tan(θ²), which is
201
+ // what \tan x^2 already was. powerEnd returns its own index when what follows ^ is not an
202
+ // exponent it can read (x^+, x^ ); stopping on that rather than asking again is what keeps this
203
+ // loop finite whatever it is fed.
204
+ let p = skipGlue(text, k);
205
+ while(text[p] === "^"){
206
+ const end = powerEnd(text, p);
207
+ if(end === p) break;
208
+ k = p = end;
209
+ }
210
+ // only GLUE continues the run, and only into another token: 2\pi x is one atom, 2 \pi is not,
211
+ // and 2\theta( stops at the bracket exactly as 2th( would
212
+ const glued = skipGlue(text, k);
213
+ if(glued === k || !TOKEN.test(text.slice(glued))) break;
214
+ k = glued;
215
+ }
216
+ return k;
217
+ }
218
+ // GLUE is a space to every scanner but atomEnd: it separates two names, so a function still finds
219
+ // its argument over one (\sin\theta) and a postfix ! still finds the operand before it
220
+ const skipSpaces = (text, i) => { while(text[i] === " " || text[i] === GLUE) i++; return i; };
221
+ const skipSpacesBack = (text, k) => { while(text[k] === " " || text[k] === GLUE) k--; return k; }; // its mirror, for the backward scan
222
+ const skipGlue = (text, i) => { while(text[i] === GLUE) i++; return i; };
223
+ const skipGroup = (text, k) => text[k] === "(" || text[k] === "{" ? groupEnd(text, k) - 1 : k; // a scan's last index inside a bracket group opening at k
224
+ // the arguments of the call whose "(" is at text[open], split at the commas of its own level, and the index past ")"
225
+ function argsOf(text, open){
226
+ const end = groupEnd(text, open), args = [];
227
+ let start = open + 1;
228
+ for(let k = start; k < end - 1; k++){
229
+ k = skipGroup(text, k);
230
+ if(text[k] === ","){ args.push(text.slice(start, k)); start = k + 1; }
231
+ }
232
+ args.push(text.slice(start, end - 1));
233
+ return {args, end};
234
+ }
235
+ // every binder call — head(expr, var, …) — replaced by print(head, expr, var, …), the parts done
236
+ // first so that one inside another, or in a limit, is handled too. They share the first two: an
237
+ // expression, and the dummy it is bound over. What follows differs by head, so BINDERS says how
238
+ // many to expect and a short print takes only the parts it needs.
239
+ const BINDER_CALL = String.raw`${NOT_IN_NAME}(${BINDER_NAMES.join("|")})\s*\(`;
240
+ function eachBinder(text, print){
241
+ const call = new RegExp(BINDER_CALL, "g"); // its own lastIndex, since this recurses
242
+ let out = "", last = 0;
243
+ for(let m = call.exec(text); m; m = call.exec(text)){
244
+ const head = m[1];
245
+ const {args, end} = argsOf(text, m.index + m[0].length - 1);
246
+ const {parts} = BINDERS[head];
247
+ if(args.length !== parts.length)
248
+ throw new Error(`${head} takes ${IN_WORDS[parts.length]} parts: ${callShape(head)}`);
249
+ out += text.slice(last, m.index) + print(head, ...args.map(a => eachBinder(a, print).trim()));
250
+ last = end; call.lastIndex = end;
251
+ }
252
+ return out + text.slice(last);
253
+ }
254
+ // the names the binders bind, whichever spelling wrote them. A head is never one: as an atom it
255
+ // would turn every integral( into a product before compile could refuse it
256
+ function dummiesOf(text){
257
+ const names = [];
258
+ eachBinder(text, (head, expr, name) => {
259
+ if(NAME.test(name) && !Object.hasOwn(BINDERS, name)) names.push(name);
260
+ return "";
261
+ });
262
+ return names;
263
+ }
264
+ // a number or one name needs no brackets round it, anything else does: a glued run like 2x or kx is a
265
+ // product by the time the formula is read (implicitProducts, and formula.js for a run of letters), so
266
+ // \frac{1}{2x} left bare would be (1/2)*x
267
+ const LONE_VALUE = new RegExp(`^(?:\\d*\\.?\\d+|${SUBSCRIPTED_LETTER})$`);
268
+ const wrap = a => LONE_VALUE.test(a.trim()) ? a.trim() : `(${a})`;
269
+
270
+ /* ---------- the passes, in order ---------- */
271
+ // \begin{cases} x^2 & x>0 \\ -x & \text{otherwise} \end{cases} → a chain of ternaries, which the
272
+ // language already has. It runs before everything else because stripWrappers reads a \\ followed by
273
+ // a space as a thin space; the cells then flow through the rest of the pipeline as ordinary
274
+ // expressions, so \le in a condition is translated where every other \le is.
275
+ // A row is <value> & <condition>. One with no condition, or whose condition is prose, is the last
276
+ // resort; if no row matches and there is no last resort the point is a hole, as 0/0.
277
+ const CASES = /\\begin\s*\{\s*cases\s*\}([\s\S]*?)\\end\s*\{\s*cases\s*\}/g;
278
+ const OPENS_CASES = /\\begin\s*\{\s*cases\s*\}/;
279
+ // what separates a row's cells: a lone &, not the && a condition may be built from, so that
280
+ // x>0 && y>0 stays one cell rather than becoming three
281
+ const CELL_SEP = /(?<!&)&(?!&)/;
282
+ const TEXT_CMD = String.raw`text(?:rm|it|bf)?`; // the prose commands, spelled once
283
+ const OTHERWISE = new RegExp(String.raw`^\\${TEXT_CMD}\s*\{`);
284
+ // one block's rows as a chain of ternaries, built last row first so that each becomes the else of
285
+ // the one above it. A row with no condition, or a prose one, ends the chain; if none does, nothing
286
+ // matching leaves a hole rather than a guess.
287
+ function chainOf(rowText){
288
+ const rows = rowText.split(/\\\\/).map(r => r.trim()).filter(Boolean);
289
+ if(!rows.length) throw new Error("\\begin{cases} needs a row: <value> & <condition>");
290
+ let chain = "(0/0)";
291
+ for(let i = rows.length - 1; i >= 0; i--){
292
+ const cells = rows[i].split(CELL_SEP).map(c => c.trim());
293
+ if(cells.length > 2)
294
+ throw new Error("a row of \\begin{cases} is one value and one condition: x^2 & x > 0");
295
+ const [value, when] = cells;
296
+ chain = !when || OTHERWISE.test(when) ? `(${value})` : `((${when}) ? (${value}) : ${chain})`;
297
+ }
298
+ return chain;
299
+ }
300
+ function translateCases(text){
301
+ const out = text.replace(CASES, (_, rowText) => ` ${chainOf(rowText)} `);
302
+ // anything left open would otherwise reach stripWrappers, whose \\ guard would report the rows
303
+ // as a stray line break — advice to use the construct the reader is already using
304
+ if(OPENS_CASES.test(out)) throw new Error("\\begin{cases} has no matching \\end{cases}");
305
+ return out;
306
+ }
307
+ function stripWrappers(text){
308
+ // \\ before the control symbols below: those consume a backslash and one character, so a row
309
+ // break left to them is read as \<space> and the first backslash escapes into the output as an
310
+ // illegal character — where this file promises to report anything unknown by name. Refused
311
+ // rather than turned into a space, since x^2 \\ y^2 becoming a product would be a silent
312
+ // misreading; translateCases has taken the row breaks it owns before this runs.
313
+ if(/\\\\/.test(text))
314
+ throw new Error("\\\\ is a line break: this field takes one expression, or \\begin{cases}");
315
+ return text
316
+ // a "z =" or "\theta =" head: the field already says it. Only the left-hand sides a renderer
317
+ // writes — y, r, z and w, the four in MODES' own groups, which test/latex.js checks against the
318
+ // table — and any single pasted name. Anything wider swallowed what it did not understand:
319
+ // sin(x) = 0 came out as the constant 0 and x = 1 as the constant 1, with no left side left
320
+ // anywhere to say so. What survives this is an equation, and js/formula.js refuses it by name;
321
+ // a Greek letter is a name and not a head, so \omega = x survives to that refusal as ω = x does.
322
+ .replace(/^\s*(?:\\([A-Za-z]+)|[yrzw])\s*=(?!=)\s*/, (head, cmd) => cmd && Object.hasOwn(GREEK, cmd) ? head : "")
323
+ .replace(/\$|\\\(|\\\)|\\\[|\\\]|\\displaystyle/g, " ")
324
+ .replace(/\\[,;!:]|\\quad|\\qquad|\\ /g, " ")
325
+ .replace(/\\left\s*\||\\lvert/g, " abs(").replace(/\\right\s*\||\\rvert/g, ") ")
326
+ .replace(/\\left\s*\.|\\right\s*\./g, " ").replace(/\\left|\\right/g, "")
327
+ .replace(/\\\{/g, "(").replace(/\\\}/g, ")")
328
+ .replace(/\\(?:mathrm|text|operatorname)\s*\{d\}\s*/g, "d"); // the differential's upright d
329
+ }
330
+ // \int_a^b <integrand> d<var> → integral(<integrand>, <var>, a, b): the limits go before the subscript check sees
331
+ // them, and the differential is the first d<letter> at the integrand's own bracket level
332
+ function limitAt(text, i, needs){ // the limit after the _ or ^ at text[i]: a bracket group (the plate prints round ones) or one token
333
+ i = skipSpaces(text, i + 1);
334
+ if(text[i] === "{" || text[i] === "("){ const end = groupEnd(text, i); return {value: text.slice(i + 1, end - 1), end}; }
335
+ const m = LIMIT.exec(text.slice(i));
336
+ if(!m) throw new Error(needs);
337
+ return {value: m[0], end: i + m[0].length};
338
+ }
339
+ // both limits after the \int at text[i], _ and ^ in either order: {from, to, end}
340
+ function limitsAt(text, i, needs){
341
+ const limits = {};
342
+ for(let k = 0; k < 2; k++){
343
+ i = skipSpaces(text, i);
344
+ if(text[i] !== "_" && text[i] !== "^") break;
345
+ const {value, end} = limitAt(text, i, needs);
346
+ limits[text[i]] = value; i = end;
347
+ }
348
+ if(!("_" in limits && "^" in limits)) throw new Error(needs);
349
+ return {from: limits._, to: limits["^"], end: i};
350
+ }
351
+ // \frac{du}{…}, the textbook spelling, is \frac{1}{…} du: the differential comes out where differentialAt sees it
352
+ function liftDifferentials(text){
353
+ const re = new RegExp(String.raw`\\[dt]?frac\s*\{\s*d\s*${BOUND_VAR}\s*\}\s*(?=\{)`);
354
+ for(let m = re.exec(text); m; m = re.exec(text)){
355
+ const open = m.index + m[0].length, end = groupEnd(text, open);
356
+ text = `${text.slice(0, m.index)}\\frac{1}${text.slice(open, end)} d${m[1]} ${text.slice(end)}`;
357
+ }
358
+ return text;
359
+ }
360
+ // a bound variable as LaTeX writes it: a command like \theta, a pasted Greek glyph, or a plain name. One spelling, shared
361
+ // by the integral's differential, the \frac{du}{…} lift and the derivative's head, because the three
362
+ // have to agree on what counts as a name — and a test pins that the derivative reads \theta the way
363
+ // the differential does. nameOf resolves it: a command through variableOf, a plain name as itself.
364
+ const BOUND_VAR = String.raw`(\\?[A-Za-z]+|[${GREEK_LETTERS}])`;
365
+ const DIFFERENTIAL = new RegExp(String.raw`d\s*${BOUND_VAR}(?![${WORD}])`, "y"); // sticky: read where the scan stands; dx_1 is no differential
366
+ const nameOf = (v, construct) => v[0] === "\\" ? variableOf(v.slice(1), construct) : v;
367
+ // the differential: d and a name (du, d u, d\theta), the d standing on its own (after a space, a bracket, a digit
368
+ // or the * the plate's · becomes), at the integrand's bracket level
369
+ function differentialAt(text, i){
370
+ for(let k = i; k < text.length; k++){
371
+ k = skipGroup(text, k);
372
+ if(text[k] === "d" && (k === i || /[\s)}0-9*]/.test(text[k - 1]))){
373
+ DIFFERENTIAL.lastIndex = k;
374
+ const m = DIFFERENTIAL.exec(text);
375
+ if(m) return {at: k, name: nameOf(m[1], "an integral"), end: DIFFERENTIAL.lastIndex};
376
+ }
377
+ }
378
+ throw new Error("an integral needs its d<variable> after the integrand");
379
+ }
380
+ function commandOf(cmd){ // what a LaTeX command stands for, by name
381
+ // hasOwn, not `in`: the name is text the user pasted, and toString is on every plain object
382
+ if(Object.hasOwn(COMMANDS, cmd)) return COMMANDS[cmd];
383
+ throw new Error(`LaTeX command not supported: \\${cmd}`);
384
+ }
385
+ function variableOf(cmd, construct){ // the variable a LaTeX command stands for: \theta is th, \cdot is none
386
+ const name = commandOf(cmd);
387
+ if(NAME.test(name)) return name;
388
+ throw new Error(`\\${cmd} cannot be ${construct}'s variable`);
389
+ }
390
+ // The derivative's head, in every spelling it is written, reduced to one marker. It has to happen
391
+ // before translateCommands turns \frac into a division — \frac{d}{dx} would become ((d)/(dx)) and
392
+ // then a silent product of two unknown names — but the term it applies to cannot be found until
393
+ // applyFunctions has made the calls, so the marker waits and translateDerivatives finishes the job.
394
+ // d and \partial mean one thing here, so one alternation reads both: a formula is already a
395
+ // function of all eight variables, and there is no total-versus-partial distinction to make. The
396
+ // plate's own d/dx is read too, which is what lets a printed derivative paste back; the pasted ∂
397
+ // arrives here as \partial, since normalize owns every symbol that stands for a command.
398
+ const D_OR_PARTIAL = String.raw`(?:d|\\partial)`;
399
+ const DERIV_HEADS = [
400
+ new RegExp(String.raw`\\[dt]?frac\s*\{\s*${D_OR_PARTIAL}\s*\}\s*\{\s*${D_OR_PARTIAL}\s*${BOUND_VAR}\s*\}`, "g"),
401
+ new RegExp(String.raw`(?<![${LETTER}0-9])${D_OR_PARTIAL}\s*/\s*${D_OR_PARTIAL}\s*${BOUND_VAR}`, "g"),
402
+ ];
403
+ const markDerivatives = text =>
404
+ DERIV_HEADS.reduce((marked, re) => marked.replace(re, (_, v) => ` diff_(${nameOf(v, "a derivative")}) `), text);
405
+
406
+ // the head of every integral, last first, so that an integral inside another (or in a limit) is bound before the one
407
+ // holding it; a rewrite touches nothing before its own \int, so the earlier positions hold
408
+ const NEEDS_INT = "an integral needs its limits: \\int_a^b";
409
+ const INTEGRAL = /\\int(?![A-Za-z])/g;
410
+ // every head, last first: a rewrite that begins at its own match moves only what follows it, so
411
+ // the earlier positions stay good and one scan does for all of them. translateFactorials cannot
412
+ // use this — its rewrite begins before the ! it matched, so every earlier position shifts too.
413
+ const headsIn = (re, text) => [...text.matchAll(re)].reverse();
414
+ const integralsIn = text => headsIn(INTEGRAL, text).map(m => m.index);
415
+ function translateIntegrals(text){
416
+ if(text.search(INTEGRAL) < 0) return text;
417
+ text = liftDifferentials(text);
418
+ for(const at of integralsIn(text)){
419
+ const head = at + 4 + (text.startsWith("\\limits", at + 4) ? 7 : 0);
420
+ const {from, to, end} = limitsAt(text, head, NEEDS_INT), start = skipSpaces(text, end);
421
+ const d = differentialAt(text, start);
422
+ const integrand = text.slice(start, d.at).replace(/(?:\\cdot|\\times|\*)\s*$/, "").trim() || "1"; // u \cdot du, u * du: u du
423
+ text = `${text.slice(0, at)} integral(${integrand}, ${d.name}, ${from}, ${to}) ${text.slice(d.end)}`;
424
+ }
425
+ return text;
426
+ }
427
+ // rewrite every \cmd{a}{b}… (argc brace arguments) as fn(args), searching again after each
428
+ function rewrite(text, cmd, argc, fn){
429
+ const re = new RegExp("\\\\" + cmd + "(?![A-Za-z])");
430
+ for(let m = re.exec(text); m; m = re.exec(text)){
431
+ let i = m.index + m[0].length;
432
+ const args = [];
433
+ for(let a = 0; a < argc; a++){
434
+ i = skipSpaces(text, i);
435
+ // m[0] is the command as it was actually written, backslash and all; naming `cmd` here would
436
+ // print the pattern that matched it (\(?:hat|bar|vec|…)) at the reader
437
+ if(text[i] !== "{") throw new Error(`${m[0]} needs its argument in braces`);
438
+ const end = groupEnd(text, i);
439
+ args.push(text.slice(i + 1, end - 1)); i = end;
440
+ }
441
+ text = `${text.slice(0, m.index)} ${fn(args)} ${text.slice(i)}`;
442
+ }
443
+ return text;
444
+ }
445
+ function translateCommands(text){
446
+ text = rewrite(text, "[dt]?frac", 2, ([a, b]) => `(${wrap(a)}/${wrap(b)})`);
447
+ text = rewrite(text, "[dt]?binom", 2, ([n, k]) => `combinations(${n}, ${k})`);
448
+ text = text.replace(/\\sqrt\s*\[([^\]]*)\]\s*(?=\{)/g, "\\root{$1}");
449
+ text = rewrite(text, "root", 2, ([n, a]) => `((${a})^(1/(${n})))`);
450
+ text = rewrite(text, "sqrt(?=\\s*\\{)", 1, ([a]) => `sqrt(${a})`);
451
+ // \operatorname{arcsinh} is the same name \arcsinh is, so it goes through the same table; a name
452
+ // the table does not know is passed through to be refused later as an unknown function
453
+ text = rewrite(text, "operatorname", 1, ([name]) => {
454
+ const trimmed = name.trim();
455
+ return Object.hasOwn(COMMANDS, trimmed) ? COMMANDS[trimmed] : name;
456
+ });
457
+ // an accent decorates a name without changing what it stands for: \hat{x} is still x. This runs
458
+ // after stripWrappers, which has already turned the differential's upright \text{d} into d —
459
+ // dropping \text any earlier would eat it and break every \int … d\theta.
460
+ text = rewrite(text, "(?:hat|bar|vec|tilde|overline|overrightarrow)", 1, ([a]) => `(${a})`);
461
+ text = rewrite(text, TEXT_CMD, 1, () => " "); // prose, not maths: drop it
462
+ text = text.replace(/\\log\s*_\s*\{?\s*(10|2)\s*\}?/g, " log$1 ");
463
+ // 90^\circ is the degree sign, folded before ^{ becomes ^( and \circ is refused by name; the infix
464
+ // f \circ g, composition, is left to that refusal
465
+ text = text.replace(/\^\s*\{?\s*\\circ\s*\}?/g, "°");
466
+ for(let i = text.indexOf("^{"); i >= 0; i = text.indexOf("^{")){
467
+ const end = groupEnd(text, i + 1);
468
+ text = `${text.slice(0, i)}^(${text.slice(i + 2, end - 1)})${text.slice(end)}`;
469
+ }
470
+ text = text.replace(/[{}]/g, c => c === "{" ? "(" : ")");
471
+ // The space this consumes is LaTeX's own name terminator, not an operator: \pi x is πx. A name
472
+ // that is not a function glues to its neighbours, so 2\theta and \theta x are single atoms, and
473
+ // \omega t is the one atom ωt for the same reason; a
474
+ // function name, an operator (\cdot) or a fence (\lfloor) takes a plain space instead, because
475
+ // 2\cos x is 2 · cos x and 2cos was never an atom anyone could mean.
476
+ return text.replace(/\\([A-Za-z]+) ?/g, (all, cmd) => {
477
+ const name = commandOf(cmd);
478
+ return NAME.test(name) && !CALLABLE.has(name) ? `${GLUE}${name}${GLUE}` : ` ${name} `;
479
+ });
480
+ }
481
+ // x_1, x_{12}, x_i, \omega_0 become the identifier x_1, x_12, ω_0, a SUBSCRIPTED_LETTER. The base is
482
+ // bounded so that sum_(k=1) and diff_( are left to their own passes. What follows a subscript is set
483
+ // apart, so x_ab is x_a times b and x_{a}b is too, not the name x_{ab}. What this does not read keeps
484
+ // its underscore for refuseLeftovers to name.
485
+ const BRACED_SUB = String.raw`\([ ${GLUE}]*([${LETTER}0-9]+)[ ${GLUE}]*\)`; // x_(12): the whole run, the braces being brackets by now
486
+ const PADDED_SUB = String.raw`${GLUE}+([${LETTER}]+)(?=${GLUE})`; // x_ GLUE th GLUE: a command's name, whole, as translateCommands padded it
487
+ const BARE_SUB = String.raw`([0-9]+|[${LETTER}])`; // x_12, x_i: digits, or one letter, as LaTeX reads it
488
+ const SUBSCRIPT = new RegExp(String.raw`${NOT_AFTER_LETTER}(${LETTER_OR_TH})[ ${GLUE}]*_ *(?:${BRACED_SUB}|${PADDED_SUB}|${BARE_SUB})(?=([${WORD}]?))`, "g");
489
+ const translateSubscripts = text => text.includes("_")
490
+ ? text.replace(SUBSCRIPT, (_, base, braced, padded, bare, next) => `${base}_${braced ?? padded ?? bare}${next ? " " : ""}`)
491
+ : text;
492
+ // e^… is the exponential; a bare e is the constant (a digit before it means scientific notation)
493
+ const LETTER_BEFORE_E = new RegExp(`[${LETTER}_.]`);
494
+ const BARE_E = new RegExp(`(?<![${WORD}.])e(?![${WORD}])`, "g");
495
+ function translateEuler(text){
496
+ let out = "";
497
+ for(let i = 0; i < text.length;){
498
+ const euler = text[i] === "e" && text[i + 1] === "^" && !LETTER_BEFORE_E.test(text[i - 1] || "");
499
+ if(!euler){ out += text[i++]; continue; }
500
+ // one leading minus is part of the exponent: e^-x is exp(-x), as e^{-x} already was. Taken here
501
+ // rather than by widening atomEnd, which would also decide what \sin -x means.
502
+ const k = skipSpaces(text, i + 2), signed = text[k] === "-", from = signed ? k + 1 : k;
503
+ const end = operandEnd(text, from); // e^sin(x) is exp(sin(x)), not exp(sin) times (x)
504
+ if(end === from) throw new Error("e^ needs an exponent");
505
+ out += !signed && text[k] === "(" ? ` exp${text.slice(k, end)} ` : ` exp(${text.slice(k, end)}) `;
506
+ i = end;
507
+ }
508
+ // a bare e is the constant. The lookbehind refuses a digit, so that 1e3 stays scientific notation;
509
+ // implicitProducts settles the digit-adjacent case, which only it can tell apart.
510
+ return out.replace(BARE_E, "E");
511
+ }
512
+ // a function written LaTeX style takes the next atom, and a power written on the function
513
+ // moves onto its value: sin x → sin(x), sin 2x → sin(2x), cos^2 x → cos(x)^2, cos^2(x) → cos(x)^2.
514
+ // The operand is taken whole and the scan resumes after it, so the pass runs again on the operand
515
+ // itself: \sqrt{\sin t} holds a bare function of its own, and so may what that one takes, to any
516
+ // depth. Each run scans with its own regex, since the runs nest and a shared lastIndex would not.
517
+ const FUNCTION_HEAD = `${NOT_IN_NAME}(${FUNCTIONS.join("|")})${NAME_END}`; // a whole name: ωsin is not sin
518
+ function applyFunctions(text){
519
+ const head = new RegExp(FUNCTION_HEAD, "g");
520
+ let out = "", last = 0;
521
+ for(let m = head.exec(text); m; m = head.exec(text)){
522
+ let i = skipSpaces(text, m.index + m[0].length), power = "", name = m[1];
523
+ if(text[i] === "^"){ const end = powerEnd(text, i); power = text.slice(i, end); i = skipSpaces(text, end); }
524
+ // ^{-1} on a function name is the inverse function, not the reciprocal of its value. The rule
525
+ // below (cos^2 x → cos(x)^2) would otherwise plot 1/tan, which is a plausible wrong curve and
526
+ // nothing tells the reader — so an inverse without a name here is refused rather than guessed.
527
+ if(MINUS_ONE.test(power)){
528
+ if(!(name in INVERSE))
529
+ throw new Error(`${name}^{-1} is not supported; the inverses this app has are ${INVERSES_OFFERED}`);
530
+ name = INVERSE[name]; power = "";
531
+ }
532
+ const end = operandEnd(text, i); // an atom, or a call taken whole: \sin deg(30), \sin\sqrt{x}
533
+ if(end === i) continue; // nothing follows: leave it
534
+ const operand = applyFunctions(text.slice(i, end)); // the bare functions inside it, which the scan below steps over
535
+ const arg = text[i] === "(" ? operand : `(${operand})`;
536
+ out += `${text.slice(last, m.index)}${name}${arg}${power}`;
537
+ last = end; head.lastIndex = end;
538
+ }
539
+ return out + text.slice(last);
540
+ }
541
+ // a postfix sign on its operand becomes a call: x!, 5!, (x + 1)!, sin(x)! → factorial(…), and 30°,
542
+ // x°, (x + 1)° → deg(…). The two run at different points of the pipeline: the degree before
543
+ // applyFunctions, because \sin 30° is sin(30°) and the call it leaves is what a bare function then
544
+ // takes whole; the factorial after, so a call is already written name(args) and a factorial over
545
+ // one can take the name with it. Both precede implicitProducts, so the bracket left behind still
546
+ // gets its silent product: 3!x is 3! · x. Searching again after each rewrite, as rewrite() does:
547
+ // the rewrite moves every position after its own, and each pass removes one sign.
548
+ function translatePostfix(text, sign, name){
549
+ for(let m = sign.exec(text); m; m = sign.exec(text)){
550
+ const k = skipSpacesBack(text, m.index - 1);
551
+ const start = operandStart(text, k, m[0]);
552
+ text = `${text.slice(0, start)}${name}(${text.slice(start, k + 1)})${text.slice(m.index + 1)}`;
553
+ }
554
+ return text;
555
+ }
556
+ const translateDegrees = text => translatePostfix(text, DEGREE, "deg");
557
+ const translateFactorials = text => translatePostfix(text, BANG, "factorial"); // the ! of != is left alone
558
+
559
+ // how far a summand reaches: a term, not a single atom. Σ 1/k² is Σ(1/k²) — × and ÷ bind tighter
560
+ // than the sum — while Σ k + 1 is (Σk) + 1, because the sign does not. A silent product still needs
561
+ // its brackets: at this point in the pipeline `k x` is two atoms and implicitProducts has not run.
562
+ function termEnd(text, i){
563
+ let end = operandEnd(text, i);
564
+ if(end === i) return i;
565
+ for(;;){
566
+ const op = skipSpaces(text, end);
567
+ if(text[op] !== "*" && text[op] !== "/") return end;
568
+ const next = skipSpaces(text, op + 1), after = operandEnd(text, next);
569
+ if(after === next) return end; // a dangling operator is not ours to report
570
+ end = after;
571
+ }
572
+ }
573
+ // the marker and the term after it. Last first, as the series are, so a derivative of a derivative
574
+ // has its inner one already a call when the outer looks for its term. Its reach is termEnd, the
575
+ // same as a summand's: d/dx x² + 1 is (d/dx x²) + 1. A \sum under a d/dx wants brackets, since the
576
+ // series are still written with their subscripts at this point and a term cannot take one.
577
+ const DERIV_MARKER = new RegExp(String.raw`${NOT_IN_NAME}diff\s*_\s*\(`, "g");
578
+ function translateDerivatives(text){
579
+ for(const m of headsIn(DERIV_MARKER, text)){
580
+ const {args, end} = argsOf(text, m.index + m[0].length - 1);
581
+ const opens = skipSpaces(text, end), closes = termEnd(text, opens);
582
+ if(closes === opens) throw new Error("d/dx needs something to differentiate");
583
+ text = `${text.slice(0, m.index)} diff(${text.slice(opens, closes)}, ${args[0].trim()}) ${text.slice(closes)}`;
584
+ }
585
+ return text;
586
+ }
587
+
588
+ // \sum_{k=1}^{n} <term> → sum(<term>, k, 1, n), and \prod likewise. It runs after
589
+ // applyFunctions, so a summand like \sin(kx) is already the call sin(kx) that operandEnd takes
590
+ // whole, and the head is a plain name by then because COMMANDS resolved \sum like any other.
591
+ // The last one is done first: a rewrite moves every position after its own, so an inner series
592
+ // becomes a call before the outer one goes looking for its operand.
593
+ const SERIES = new RegExp(String.raw`${NOT_IN_NAME}(?<head>sum|prod)\s*_`, "g");
594
+ // the negation of BINDER_CALL, spacing included: \s* outside the lookahead would give the space
595
+ // back and match against it, refusing `integral (u, u, 0, 1)` that eachBinder accepts
596
+ const BARE_BINDER = new RegExp(String.raw`${NOT_IN_NAME}(${BINDER_NAMES.join("|")})${NAME_END}(?!\s*\()`);
597
+ // what the binder passes did not take. A head left over would otherwise reach the compiler as the
598
+ // function itself (sum^(5) is NaN, drawn as nothing), and an underscore that is no subscript
599
+ // (2_1, xy_1, x_{k=1}, \log_3) would reach it as a name it has never heard of, or as nothing at all.
600
+ const SUBSCRIPTED_NAME = new RegExp(String.raw`${NOT_AFTER_LETTER}${SUBSCRIPTED_LETTER}${NAME_END}`, "g");
601
+ function refuseLeftovers(text){
602
+ const bare = BARE_BINDER.exec(text);
603
+ if(bare) throw new Error(`${bare[1]} is a function: write ${callShape(bare[1])}, `
604
+ + `or paste ${BINDERS[bare[1]].paste}`);
605
+ if(text.replace(SUBSCRIPTED_NAME, "").includes("_")) throw new Error("a subscript is one letter or digits: x_1, x_{12}, x_i");
606
+ return text;
607
+ }
608
+ function translateSeries(text){
609
+ for(const {index: at, groups: {head}} of headsIn(SERIES, text)){
610
+ const needs = `\\${head} needs its index and limits: \\${head}_{k=1}^{n}`;
611
+ const {from, to, end} = limitsAt(text, at + head.length, needs);
612
+ const eq = from.indexOf("=");
613
+ if(eq < 0) throw new Error(needs);
614
+ const name = from.slice(0, eq).trim(), first = from.slice(eq + 1).trim();
615
+ const opens = skipSpaces(text, end), closes = termEnd(text, opens);
616
+ if(closes === opens) throw new Error(`\\${head} needs something to ${head === "sum" ? "add" : "multiply"}`);
617
+ text = `${text.slice(0, at)} ${head}(${text.slice(opens, closes)}, ${name}, ${first}, ${to}) ${text.slice(closes)}`;
618
+ }
619
+ return text;
620
+ }
621
+
622
+ // LaTeX's silent products, made explicit; the atoms are the plot variables, the single letters and
623
+ // the names the integrals bind, each bounded by NOT_IN_NAME
624
+ const DIGIT_THEN_NAME = new RegExp(`(?<![${LETTER}][0-9]*)(\\d)\\s*(?=[a-df-hj-zA-DF-Z${GREEK_LETTERS}(])`, "g");
625
+ const DIGIT_THEN_E = new RegExp(`(\\d)([eE])(?![0-9+\\-])([${WORD}]?)`, "g");
626
+ const CLOSE_THEN_NAME = new RegExp(`\\)\\s*(?=[${LETTER}0-9(])`, "g");
627
+ function implicitProducts(text){
628
+ const atoms = [ATOMS, ...dummiesOf(text)].join("|");
629
+ return text
630
+ // 2x, 3(x+1) — not 1e-3, 0.156i, 1E5, and not the digits that end a name: the lookbehind is
631
+ // what lets log2, log10 and atan2 be called at all, rather than becoming log2*(x) and NaN
632
+ .replace(DIGIT_THEN_NAME, "$1*")
633
+ /* an e after a digit that is not an exponent. translateEuler could not settle this one: its
634
+ * lookbehind refuses a digit, so that 1e3 stays scientific notation, and until the * goes in
635
+ * 2e still looks like a number. So this pass, which is the one creating the gap, says what
636
+ * lands in it — the constant when the e stands alone, and only the * when a name follows,
637
+ * which keeps 2exp(x) a call rather than 2*E*xp(x). */
638
+ .replace(DIGIT_THEN_E, (_, d, e, next) => next ? `${d}*${e}${next}` : `${d}*E`)
639
+ .replace(/(\d)\s+(?=[eiE])/g, "$1*") // 2 e, with a space, is a product
640
+ .replace(CLOSE_THEN_NAME, ")*") // (x+1)y, sin(x)cos(y), (x)(y)
641
+ .replace(new RegExp(String.raw`${NOT_IN_NAME}(${atoms})\s*(?=\()`, "g"), "$1*") // x(y+1), a(y+1)
642
+ .replace(new RegExp(String.raw`${NOT_IN_NAME}(${atoms})\s+(?=[${LETTER}0-9])`, "g"), "$1*"); // x y, pi x, th t, a x, ω t
643
+ }
644
+
645
+ // The passes are ordered, and several of them depend on it. What each one leaves behind for the
646
+ // next is noted here, where a pass would be moved, as well as beside the code that relies on it.
647
+ function fromLatex(src){
648
+ // GLUE is this pipeline's own; a source carrying one already would be read as a join it never
649
+ // asked for, so it arrives as the space it is indistinguishable from
650
+ let text = translateCases(String(src).replaceAll(GLUE, " ")); // rows split before \\ can be read as a space
651
+ text = stripWrappers(text); // \left and \right are gone from here
652
+ text = translateIntegrals(text);
653
+ text = markDerivatives(text); // the d/dx heads, before \frac becomes a division
654
+ text = translateCommands(text); // braces are brackets: ^{-1} is ^(-1) from here
655
+ text = translateSubscripts(text); // x_(12) and ω GLUE _0 are the names x_12 and ω_0 from here
656
+ text = translateDegrees(text); // 30° is deg(30) from here, a call a bare function takes whole
657
+ text = translateEuler(text);
658
+ text = applyFunctions(text); // a call is written name(args) from here
659
+ text = translateDerivatives(text); // its term needs those calls, and precedes the series
660
+ text = translateSeries(text); // takes its summand with the same operandEnd
661
+ text = refuseLeftovers(text); // the heads and subscripts nothing above took
662
+ text = translateFactorials(text); // needs those calls, and must precede the products
663
+ text = text.replaceAll(GLUE, " "); // every join is made: a space again from here
664
+ text = implicitProducts(text);
665
+ // the spaces a call was padded with: by now every name before a ( is a function's, the products having their *
666
+ return text.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+([,()^])/g, "$1").replace(/\^\s+/g, "^").trim();
667
+ }
668
+
669
+ Object.assign(WL, {fromLatex, eachBinder, dummiesOf, NAME, LETTER, WORD, NOT_IN_NAME, NAME_END, LETTER_OR_TH, SUBSCRIPTED_LETTER, NOT_AFTER_LETTER,
670
+ GREEK, GREEK_SYMBOLS, GREEK_VARIANTS, foldVariant, COMMANDS, FUNCTIONS, BINDERS, INVERSE, CALLABLE, callShape});
671
+ })();
672
+
673
+ /* Wavelace · formula — compile a real-valued expression, and print it for the plate
674
+ * reads: fromLatex, eachBinder, dummiesOf, NAME, LETTER, WORD, NOT_IN_NAME, NAME_END, LETTER_OR_TH, SUBSCRIPTED_LETTER, NOT_AFTER_LETTER,
675
+ * GREEK_SYMBOLS, GREEK_VARIANTS, foldVariant, COMMANDS
676
+ * exports: VARS, ENV, GLOSS, normalize, readsVars, isFreeName, FREE_DEFAULT, bindable, compile, pretty, prettyTriple, prettyName, dropLhs, namesOf,
677
+ * evalParam, evalPlane, evalLine
678
+ *
679
+ * Every formula is compiled against the whole variable set (VARS); each renderer feeds what it
680
+ * has, so any formula can be drawn by any renderer. integral, sum, prod and diff are the functions
681
+ * whose first argument is an expression: compile binds it to a function of its variable before the
682
+ * body is built, so the variable named second is a dummy that shadows any plot variable of the same
683
+ * name. The first three are then given a range; diff is given the name again, which outside the
684
+ * binder is whatever the enclosing scope holds, so it differentiates where the plot already is. */
685
+ (() => {
686
+ "use strict";
687
+ const WL = window.Wavelace = window.Wavelace || {};
688
+ const {fromLatex, eachBinder, dummiesOf, NAME, LETTER, WORD, NOT_IN_NAME, NAME_END, LETTER_OR_TH, SUBSCRIPTED_LETTER, NOT_AFTER_LETTER,
689
+ GREEK_SYMBOLS, GREEK_VARIANTS, foldVariant, COMMANDS} = WL;
690
+
691
+ // a numeric integral of f over [a, b]: composite Simpson on PANELS panels, which also says whether the integrand
692
+ // had died away by the end, the |f| of the last quarter of the samples at most TAIL of the whole; one sample
693
+ // that is not a number settles both, so the rest are not taken
694
+ const PANELS = 128, REACH = 40, TAIL = 0.05;
695
+ function simpson(f, a, b){
696
+ const h = (b - a) / PANELS;
697
+ let total = 0, all = 0, tail = 0; // not `sum`: that is a function of its own below
698
+ for(let i = 0; i <= PANELS; i++){
699
+ const v = f(a + i*h), size = Math.abs(v);
700
+ if(v !== v) return {value: NaN, decayed: false};
701
+ total += (i === 0 || i === PANELS ? 1 : i & 1 ? 4 : 2) * v; all += size;
702
+ if(i >= PANELS * 3/4) tail += size;
703
+ }
704
+ return {value: total * h / 3, decayed: tail <= TAIL * all};
705
+ }
706
+ // to infinity: the first REACH units, and NaN, a blank point, where the integrand had not died away by then
707
+ function toInfinity(f, a){ const {value, decayed} = simpson(f, a, a + REACH); return decayed ? value : NaN; }
708
+ function integral(f, a, b){
709
+ if(Number.isNaN(a) || Number.isNaN(b)) return NaN; // spares the samples
710
+ if(a > b) return -integral(f, b, a);
711
+ if(a === b) return 0;
712
+ if(a === -Infinity){
713
+ if(b !== Infinity) return toInfinity(u => f(-u), -b);
714
+ const left = toInfinity(u => f(-u), 0); // a left half that did not decay decides
715
+ return left !== left ? NaN : left + toInfinity(f, 0);
716
+ }
717
+ return b === Infinity ? toInfinity(f, a) : simpson(f, a, b).value;
718
+ }
719
+
720
+ // Σ and ∏ over the integers in [a, b]. Unlike the integral, whose 128 panels are
721
+ // fixed here, a term count is the reader's to choose — so it is capped, and past the cap this
722
+ // returns NaN, a hole, rather than freezing the tab. That also settles a sum to infinity.
723
+ // A cap is not a speed promise: a thousand terms at 129² points is 16.6 million evaluations a
724
+ // frame, and a mesh stays interactive at a few dozen.
725
+ const MAX_TERMS = 1000;
726
+ function sum(f, a, b){
727
+ const from = Math.ceil(a), to = Math.floor(b);
728
+ if(Number.isNaN(from) || Number.isNaN(to)) return NaN;
729
+ if(to < from) return 0; // the empty sum
730
+ if(to - from >= MAX_TERMS) return NaN;
731
+ let s = 0;
732
+ for(let k = from; k <= to; k++){ const v = f(k); if(v !== v) return NaN; s += v; }
733
+ return s;
734
+ }
735
+ // the same shape with 1 and ×. Two functions rather than one taking a flag or a combining
736
+ // callback: the flag would switch what the function is, and the callback would be a closure call
737
+ // per term inside a loop the renderers run 129² times a frame.
738
+ function prod(f, a, b){
739
+ const from = Math.ceil(a), to = Math.floor(b);
740
+ if(Number.isNaN(from) || Number.isNaN(to)) return NaN;
741
+ if(to < from) return 1; // the empty product
742
+ if(to - from >= MAX_TERMS) return NaN;
743
+ let p = 1;
744
+ for(let k = from; k <= to; k++){ const v = f(k); if(v !== v) return NaN; p *= v; }
745
+ return p;
746
+ }
747
+
748
+ /* ---------- the special functions ----------
749
+ * Closed forms, so the wave renderer can use them at full ribbon depth: drawn through the integral
750
+ * instead, each point would cost the 128 panels above. Every one is a plotting approximation, and
751
+ * the accuracy each is good to is named beside it — well past a pixel in all cases. */
752
+
753
+ // Lanczos, g = 7, and the reflection formula below the pole-ridden half; good to about 1e-15
754
+ const LANCZOS = [0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313,
755
+ -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6,
756
+ 1.5056327351493116e-7];
757
+ function gamma(x){
758
+ // the poles: Math.sin(PI * n) is not exactly zero at a negative integer, so without this the
759
+ // reflection returns a large finite number and a plot draws a spike where it should draw a hole
760
+ if(x <= 0 && Number.isInteger(x)) return NaN;
761
+ if(x === Infinity) return Infinity;
762
+ if(x < 0.5) return Math.PI / (Math.sin(Math.PI * x) * gamma(1 - x));
763
+ x -= 1;
764
+ let a = LANCZOS[0];
765
+ for(let i = 1; i < LANCZOS.length; i++) a += LANCZOS[i] / (x + i);
766
+ const t = x + LANCZOS.length - 1.5;
767
+ return Math.sqrt(2 * Math.PI) * Math.pow(t, x + 0.5) * Math.exp(-t) * a;
768
+ }
769
+ // exact for the integers people actually type, and the gamma continuation between them: (-0.5)! is
770
+ // sqrt(pi), so only the negative integers — gamma's poles — are refused. The integers are a table
771
+ // rather than a loop because sum() calls this once per term with a growing argument: a Taylor
772
+ // polynomial, which is the headline reason sum exists, was quadratic in its own term count.
773
+ const FACTORIALS = new Float64Array(171);
774
+ FACTORIALS[0] = 1;
775
+ for(let i = 1; i <= 170; i++) FACTORIALS[i] = FACTORIALS[i - 1] * i; // 171! overflows a double
776
+ function factorial(n){
777
+ if(Number.isInteger(n)) return n < 0 ? NaN : n > 170 ? Infinity : FACTORIALS[n];
778
+ return gamma(n + 1);
779
+ }
780
+ // multiplicatively, so that combinations(50, 25) never builds 50! on the way. Exact while the
781
+ // answer is under 2^53, an approximation above it, as any double must be: Math.round is a no-op
782
+ // on combinations(100, 50) and the value carries the float error it accumulated
783
+ function combinations(n, k){
784
+ if(!Number.isInteger(n) || !Number.isInteger(k) || k < 0 || n < 0 || k > n) return NaN;
785
+ k = Math.min(k, n - k);
786
+ let r = 1;
787
+ for(let i = 1; i <= k; i++) r = r * (n - k + i) / i;
788
+ return Math.round(r);
789
+ }
790
+ // Abramowitz & Stegun 7.1.26: absolute error under 1.5e-7, three orders past a pixel
791
+ const ERF = [0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429];
792
+ function erf(x){
793
+ const s = Math.sign(x), a = Math.abs(x), t = 1 / (1 + 0.3275911 * a);
794
+ let poly = 0;
795
+ for(let i = ERF.length - 1; i >= 0; i--) poly = (poly + ERF[i]) * t;
796
+ return s * (1 - poly * Math.exp(-a * a));
797
+ }
798
+ // J0 and J1 by Abramowitz & Stegun 9.4, good to about 1e-8
799
+ function besselJ0(x){
800
+ const a = Math.abs(x);
801
+ if(a < 8){
802
+ const y = x*x;
803
+ return (57568490574 + y*(-13362590354 + y*(651619640.7 + y*(-11214424.18 + y*(77392.33017 + y*-184.9052456)))))
804
+ / (57568490411 + y*(1029532985 + y*(9494680.718 + y*(59272.64853 + y*(267.8532712 + y)))));
805
+ }
806
+ const z = 8/a, y = z*z, xx = a - 0.785398164;
807
+ const p = 1 + y*(-0.1098628627e-2 + y*(0.2734510407e-4 + y*(-0.2073370639e-5 + y*0.2093887211e-6)));
808
+ const q = -0.1562499995e-1 + y*(0.1430488765e-3 + y*(-0.6911147651e-5 + y*(0.7621095161e-6 + y*-0.934935152e-7)));
809
+ return Math.sqrt(0.636619772/a) * (Math.cos(xx)*p - z*Math.sin(xx)*q);
810
+ }
811
+ function besselJ1(x){
812
+ const a = Math.abs(x);
813
+ if(a < 8){
814
+ const y = x*x;
815
+ const r = x*(72362614232 + y*(-7895059235 + y*(242396853.1 + y*(-2972611.439 + y*(15704.48260 + y*-30.16036606)))));
816
+ return r / (144725228442 + y*(2300535178 + y*(18583304.74 + y*(99447.43394 + y*(376.9991397 + y)))));
817
+ }
818
+ const z = 8/a, y = z*z, xx = a - 2.356194491;
819
+ const p = 1 + y*(0.183105e-2 + y*(-0.3516396496e-4 + y*(0.2457520174e-5 + y*-0.240337019e-6)));
820
+ const q = 0.04687499995 + y*(-0.2002690873e-3 + y*(0.8449199096e-5 + y*(-0.88228987e-6 + y*0.105787412e-6)));
821
+ const j = Math.sqrt(0.636619772/a) * (Math.cos(xx)*p - z*Math.sin(xx)*q);
822
+ return x < 0 ? -j : j;
823
+ }
824
+ // The order must be exactly 0 or 1. Rounding it instead would answer besselJ(0.5, x) with J1, a
825
+ // smooth plausible curve that is not the function asked for, the same lesson as \tan^{-1} and the cotangent.
826
+ const besselJ = (n, x) => (n === 0 ? besselJ0(x) : n === 1 ? besselJ1(x) : NaN);
827
+
828
+ // the derivative at a point, by central difference. The step is the cube root of the machine
829
+ // epsilon, which is where a central difference's truncation error (h²) meets its rounding error
830
+ // (ε/h), multiplied by the point so that it stays relative once away from the origin. Two
831
+ // evaluations, so a diff costs twice what its expression does — and a diff of an integral twice
832
+ // its 128 panels.
833
+ const RELATIVE_STEP = Math.cbrt(Number.EPSILON); // ≈ 6.06e-6
834
+ function diff(f, at){
835
+ if(Number.isNaN(at)) return NaN;
836
+ const h = RELATIVE_STEP * Math.max(1, Math.abs(at));
837
+ const hi = f(at + h), lo = f(at - h);
838
+ if(hi !== hi || lo !== lo) return NaN; // a hole either side is a hole here
839
+ return (hi - lo) / (2 * h);
840
+ }
841
+
842
+ // Gielis's superformula, as its gloss below states it; a and b default to 1
843
+ function superformula(th, m, n1, n2, n3, a = 1, b = 1){
844
+ const phase = m * th / 4; // m lobes a turn: the pattern of |cos| and |sin| repeats every π/2 of it
845
+ return (Math.abs(Math.cos(phase) / a) ** n2 + Math.abs(Math.sin(phase) / b) ** n3) ** (-1 / n1);
846
+ }
847
+
848
+ const ENV = {
849
+ sin:Math.sin, cos:Math.cos, tan:Math.tan, asin:Math.asin, acos:Math.acos,
850
+ atan:Math.atan, atan2:Math.atan2, sinh:Math.sinh, cosh:Math.cosh, tanh:Math.tanh,
851
+ asinh:Math.asinh, acosh:Math.acosh, atanh:Math.atanh,
852
+ exp:Math.exp, log:Math.log, log2:Math.log2, log10:Math.log10, sqrt:Math.sqrt,
853
+ cbrt:Math.cbrt, abs:Math.abs, sign:Math.sign, floor:Math.floor, ceil:Math.ceil,
854
+ round:Math.round, min:Math.min, max:Math.max, pow:Math.pow, hypot:Math.hypot,
855
+ mod:(a, b) => ((a % b) + b) % b, // floored modulo: never negative for b > 0, unlike %
856
+ sec:x => 1/Math.cos(x), csc:x => 1/Math.sin(x), cot:x => 1/Math.tan(x),
857
+ gamma, factorial, combinations, erf, besselJ, superformula,
858
+ sinc:x => (x === 0 ? 1 : Math.sin(x) / x),
859
+ deg:x => x * Math.PI / 180, // what 30° and 30^\circ become (latex.js)
860
+ PI:Math.PI, pi:Math.PI, E:Math.E, tau:Math.PI*2, inf:Infinity, integral, sum, prod, diff,
861
+ };
862
+ const ENV_KEYS = Object.keys(ENV);
863
+ const ENV_VALS = ENV_KEYS.map(k => ENV[k]);
864
+ // what a function does, where its name does not say it: the documentation page's gloss beside each
865
+ // name, and the app's own reference drawer's. null means the name says it, a decision written down,
866
+ // so that a name nobody has thought about is the thing that fails the build (tools/docs.js gates
867
+ // it; the drawer, which must never refuse to open over a sentence, reads a missing one as null).
868
+ const GLOSS = {
869
+ sin: null, cos: null, tan: null, asin: null, acos: null, atan: null,
870
+ sinh: null, cosh: null, tanh: null, asinh: null, acosh: null, atanh: null,
871
+ exp: null, log2: null, log10: null, sqrt: null, abs: null, min: null, max: null,
872
+ floor: null, ceil: null, round: null,
873
+ atan2: "the angle of (y, x), over the whole turn rather than a half of it",
874
+ log: "the natural logarithm; \\ln reaches it too, and \\log_{10} reaches log10",
875
+ cbrt: "the cube root, which unlike a power of a third is defined for a negative x",
876
+ pow: "pow(a, b) is a^b, for where a power reads better as a call",
877
+ hypot: "the distance, hypot(x, y) = sqrt(x² + y²), without the overflow a squared sum can reach",
878
+ sign: "−1, 0 or 1",
879
+ mod: "the floored modulo, never negative for a positive b, unlike the % operator",
880
+ sec: "1/cos", csc: "1/sin", cot: "1/tan",
881
+ gamma: "the gamma function, carrying its reflection below ½, so gamma(−0.5) is −2√π; a hole at each pole",
882
+ factorial: "exact on the integers to 170!, and continued through gamma between them",
883
+ combinations: "combinations(n, k), exact where the factorials it is written from would have overflowed",
884
+ erf: "the error function, to 1.5e−7",
885
+ besselJ: "besselJ(n, x) for orders 0 and 1; any other order is a hole rather than a guess",
886
+ sinc: "sin(x)/x, and 1 at the origin",
887
+ superformula: "Gielis's superformula (2003), the radius of a star, a flower or a polygon at the angle θ: superformula(θ, m, n₁, n₂, n₃, a, b) is (|cos(mθ/4)/a|^n₂ + |sin(mθ/4)/b|^n₃)^(−1/n₁), m lobes a turn, a small n₁ spiky; a and b may be left out, and are then 1",
888
+ deg: "degrees, as radians: 30° and 30^\\circ both become deg(30)",
889
+ integral: null, sum: null, prod: null, diff: null, // the binders have a section of their own
890
+ pi: null, PI: null, E: null, tau: null, inf: null, // as do the constants
891
+ };
892
+ /* The left-hand side the formula field is already showing, typed back into it. MODES gives each
893
+ * renderer its own — "z =" for surface, "V(x) =" for quantum, "f(r) =" for swarm — and session.js
894
+ * hands it here because it is the only layer that knows which renderer is on show. Whitespace is
895
+ * ignored on both sides so that "V(x)=" and "V( x ) =" are the same head as the label. Anything
896
+ * else before an = is an equation, and refuseAssignment below says so. */
897
+ function dropLhs(expr, lhs){
898
+ const want = String(lhs || "").replace(/[\s=]/g, "");
899
+ if(!want) return expr;
900
+ const head = /^\s*([^=]*?)\s*=(?!=)\s*/.exec(expr);
901
+ return head && head[1].replace(/\s/g, "") === want ? expr.slice(head[0].length) : expr;
902
+ }
903
+
904
+ const ALLOWED = new RegExp(`^[0-9${LETTER}_+\\-*/^%().,<>=?:!&|\\s]*$`); // LETTER: Latin and the Greek glyphs latex.js names
905
+ // & and | are in ALLOWED for && and ||, the conditions a ternary is built from. Alone they are
906
+ // JavaScript's bitwise operators, so x & 1 quietly answers 1; and a lone | is the bare |x| this
907
+ // language declines. It goes in normalize rather than beside ALLOWED in compile because the
908
+ // complex renderer takes the other road — session.js hands normalize's output to complex.js's own
909
+ // parser and never reaches compile — and this is a rule about the language, not about one path.
910
+ /* Three rules about the language rather than about one path, which is why they live in normalize:
911
+ * the complex renderer hands normalize's output to its own parser and never reaches compile, and
912
+ * would otherwise refuse the same three in a stranger's words ("unexpected end of formula" for an
913
+ * empty call). Each is a constant regex and a sentence of its own.
914
+ *
915
+ * = the language has no assignment and no names of your own. session.js has already dropped the
916
+ * head the field was showing, so what is left is a reader writing an equation.
917
+ * & alone these are JavaScript's bitwise operators, so x & 1 quietly answers 1; and a lone | is
918
+ * the bare |x| this language declines. Both are in ALLOWED for && and ||.
919
+ * () an empty argument list reached the function as undefined, so sin() drew a blank where the
920
+ * honest answer is that sin takes one. */
921
+ const refuse = (re, say) => text => {
922
+ const m = re.exec(text);
923
+ if(m) throw new Error(say(m));
924
+ return text;
925
+ };
926
+ const refuseAssignment = refuse(/(?<![<>!=])=(?!=)/,
927
+ () => "= is not supported: a formula is one expression, not an equation "
928
+ + "(the renderer writes its own left-hand side)");
929
+ // the functions only: a constant with an empty argument list is not missing an argument, it is not
930
+ // a call at all, and inf() saying "inf needs an argument" would send a reader to inf(1)
931
+ const CALLABLE = ENV_KEYS.filter(k => typeof ENV[k] === "function");
932
+ const refuseEmptyCall = refuse(new RegExp(String.raw`${NOT_IN_NAME}(${CALLABLE.join("|")})\s*\(\s*\)`),
933
+ m => `${m[1]} needs an argument: ${m[1]}() has no value to give`);
934
+ const refuseBitwise = refuse(/(?<!&)&(?!&)|(?<!\|)\|(?!\|)/, m => m[0] === "|"
935
+ ? "| is not supported: write abs(x), or \\left|x\\right| when pasting"
936
+ : "a single & is not supported: && and || are the ones a condition is built from");
937
+
938
+ /* A comma outside every bracket is JavaScript's comma operator, which evaluates its left side,
939
+ * throws it away and answers the right: x, y drew y and nothing said so. A scan rather than a
940
+ * regex, because only a counter can tell a call's commas from a loose one — and ALLOWED has no
941
+ * bracket but ( ), no string and no comment, so the count cannot be fooled. */
942
+ function refuseLooseComma(text){
943
+ let depth = 0;
944
+ for(const c of text.match(/[(),]/g) || []){
945
+ if(c === "(") depth++;
946
+ else if(c === ")") depth--;
947
+ else if(depth === 0)
948
+ throw new Error("a comma outside brackets is not supported: a formula is one expression");
949
+ }
950
+ return text;
951
+ }
952
+
953
+ const VARS = ["x","y","z","r","th","u","v","t"];
954
+ const PROBE = [0.37, 0.2, 0.1, 0.4, 0.37, 0.37, 0.23, 0.11]; // parallel to VARS: a type check sample
955
+
956
+ // which of the plot variables a source names. Conservative on purpose: a bound dummy called v counts
957
+ // as v, because over-reporting costs a caller nothing while under-reporting would have it treat a
958
+ // formula as independent of something it reads — the wave renderer asks this to know whether a
959
+ // formula is the same function on every ribbon slice. The name is bounded by letters rather than by
960
+ // \b, since a digit is a word character and \bz\b cannot see the z in the implicit product 2z; th
961
+ // is tried before t so that it is read as one name. Asked of the normalized source, so a pasted
962
+ // \theta counts as th; a source too broken to normalize will not compile either, and is read raw.
963
+ const NAMED = new RegExp(`${NOT_AFTER_LETTER}(th|x|y|z|r|u|v|t)(?![${LETTER}_])`, "g"); // x_1 and a_x read no x
964
+ function readsVars(src){
965
+ let text = String(src);
966
+ try{ text = normalize(text); }catch(_){ /* raw, then: a name in it is still a name */ }
967
+ return new Set(text.match(NAMED));
968
+ }
969
+
970
+ // A name the language does not know becomes a dial when it is one letter that the tables do
971
+ // not already claim: not a plot variable, not a name in ENV (E is Euler's number there), and not e,
972
+ // which the pipeline reads as that constant. Read off the tables rather than spelt out, so a ninth
973
+ // variable or a one-letter constant is excluded the day it arrives. A Greek letter is one letter
974
+ // too, the glyph a command became in latex.js (LETTER is its class), so \omega and a pasted ω are
975
+ // the one name ω while omega typed in ASCII is a longer name. Anything longer is never a dial: it is
976
+ // read as a product of letters (readRunsAsProducts, below) or it keeps its refusal, which is what stops a
977
+ // mistyped sni(x), the product sni*(x) to the compiler, from growing a slider. A binder's dummy is bound,
978
+ // not free, so sum(a*k, k, 1, 3) frees a alone.
979
+ // The order is first appearance, which is the order the rail shows them in.
980
+ const RESERVED = new Set([...VARS, "e", ...ENV_KEYS]);
981
+ const IDENT = new RegExp(`${NOT_IN_NAME}[${LETTER}_][${WORD}]*`, "g");
982
+ // a SUBSCRIPTED_LETTER (latex.js) and nothing longer: x_0 and t_0 are names of their own, a fixed
983
+ // point and not the axis, which is what a textbook means by them; the whole name is what RESERVED is asked
984
+ const FREE_SHAPE = new RegExp(`^${SUBSCRIPTED_LETTER}$`);
985
+ const FREE_DEFAULT = 1; // what a free name is worth until its dial moves
986
+ const isFreeName = name => !RESERVED.has(name) && FREE_SHAPE.test(name);
987
+
988
+ // A run of letters the language does not know (kx, ωt, xy) is the product of its letters when at most
989
+ // one of them is free, since that is how a textbook sets them: \sin(kx - \omega t). Two free letters
990
+ // keep the refusal, because sni is likelier a mistyped sin than three sliders, and a run before a
991
+ // bracket is a call and never split (ta(x) is a mistyped tan). What is known is the caller's language:
992
+ // RESERVED here, the complex renderer's own names there, so its re z is not r times e times z. A
993
+ // binder's dummy is known too, and th is one letter. The product gets no brackets, as 2x gets none.
994
+ // A run that reads as notation keeps its refusal too; the e rule is there because latex.js makes e
995
+ // Euler's number only standing alone or before ^, so the e of ex would reach the compiler as a name.
996
+ const RUN = new RegExp(`${NOT_IN_NAME}[${LETTER}]{2,}${NAME_END}(?!\\s*\\()`, "g");
997
+ const LETTER_OF_RUN = new RegExp(LETTER_OR_TH, "g");
998
+ const isCommandWithoutBackslash = run => Object.hasOwn(COMMANDS, run); // mu, xi, eta
999
+ const isDifferential = letters => letters.length === 2 && letters[0] === "d"; // dx, dt
1000
+ const isNotation = (run, letters) => isCommandWithoutBackslash(run) || isDifferential(letters) || letters.includes("e");
1001
+ function readRunsAsProducts(text, known){
1002
+ const dummies = new Set(dummiesOf(text));
1003
+ const isKnown = name => known.has(name) || dummies.has(name);
1004
+ return text.replace(RUN, run => {
1005
+ const letters = run.match(LETTER_OF_RUN);
1006
+ if(isKnown(run) || isNotation(run, letters)) return run;
1007
+ const freeLetters = letters.filter(letter => !isKnown(letter));
1008
+ return freeLetters.length <= 1 ? letters.join("*") : run;
1009
+ });
1010
+ }
1011
+
1012
+ // the names of several compiled parts as one list, each once, in order of first appearance: what
1013
+ // the session binds for a triple, and what a spec's lines name between them
1014
+ const namesOf = parts => [...new Set(parts.flatMap(p => p.names))];
1015
+ function freeNamesOf(text){ // of the normalized source
1016
+ const letters = [...new Set((text.match(IDENT) || []).filter(isFreeName))];
1017
+ if(!letters.length) return letters; // the common case, and the binder scan is not free
1018
+ const dummies = new Set(dummiesOf(text));
1019
+ return letters.filter(n => !dummies.has(n));
1020
+ }
1021
+
1022
+ // tolerate pasted maths notation, the plate's own included: the Greek that stands for a command (GREEK_SYMBOLS
1023
+ // in latex.js: θ π τ Γ Σ Π) and ∫ ∞ ∂ ∏ become their LaTeX commands, so that one reader owns them and \int_0^\pi
1024
+ // takes π as one limit; the variant glyphs fold to their letter first (GREEK_VARIANTS, beside that table; the
1025
+ // letters themselves pass as names); ² → ^{2}, ₀ → _{0}, · × → *, − → -, √ → sqrt, then the LaTeX subset and
1026
+ // silent products (latex.js), then a run of letters as a product, by the names `known` holds (above).
1027
+ // The ø family is an older spelling of θ, kept for the links that carry it.
1028
+ const VARIANT_RUN = new RegExp(`[${Object.keys(GREEK_VARIANTS).join("")}]`, "g");
1029
+ const SYMBOL_RUN = new RegExp(`[${Object.keys(GREEK_SYMBOLS).join("")}]`, "g");
1030
+ const DIGITS = "0123456789-", SUPER = "⁰¹²³⁴⁵⁶⁷⁸⁹⁻", SUB = "₀₁₂₃₄₅₆₇₈₉₋"; // parallel: a digit and its scripts
1031
+ const respell = (from, to, s) => [...s].map(c => to[from.indexOf(c)] ?? c).join(""); // each character of `from` as its `to`
1032
+ const SUPER_RUN = new RegExp(`[${SUPER}]+`, "g"), SUB_RUN = new RegExp(`[${SUB}]+`, "g");
1033
+ function normalize(src, known = RESERVED){
1034
+ return refuseLooseComma(refuseEmptyCall(refuseAssignment(refuseBitwise(readRunsAsProducts(fromLatex(src
1035
+ .replace(VARIANT_RUN, foldVariant)
1036
+ .replace(/[º˚]/g, "°") // the ordinal and the ring a keyboard types for the degree sign
1037
+ .replace(SYMBOL_RUN, c => `\\${GREEK_SYMBOLS[c]} `)
1038
+ .replace(/[øØ⌀∅]/g, "\\theta ")
1039
+ .replace(/∫/g, "\\int ").replace(/∞/g, "\\infty ").replace(/∂/g, "\\partial ").replace(/∏/g, "\\prod ")
1040
+ .replace(SUPER_RUN, run => `^{${respell(SUPER, DIGITS, run)}}`).replace(SUB_RUN, run => `_{${respell(SUB, DIGITS, run)}}`)
1041
+ .replace(/[·•×∙]/g, "*")
1042
+ .replace(/[−–—]/g, "-")
1043
+ .replace(/÷/g, "/")
1044
+ .replace(/√/g, "sqrt")
1045
+ .replace(/(/g, "(").replace(/)/g, ")")), known)))));
1046
+ }
1047
+
1048
+ // the expression becomes a function of its variable, a name of the formula's own (a plot variable may be
1049
+ // shadowed: inside the binder it is the dummy)
1050
+ const bindBinders = body => eachBinder(body, (head, expr, name, from, to) => {
1051
+ if(!NAME.test(name) || name in ENV) throw new Error(`${head}'s variable must be a name, like k`);
1052
+ // the three with a range are handed it; diff is handed the name itself, which outside the arrow
1053
+ // is whatever the enclosing scope holds — the plot's variable, or a binder's dummy further out
1054
+ return head === "diff" ? `diff((${name}) => (${expr}), ${name})`
1055
+ : `${head}((${name}) => (${expr}), ${from}, ${to})`;
1056
+ });
1057
+
1058
+ // the sample evaluation, which is where an unknown name first shows. Its own message is
1059
+ // JavaScript's and session.js puts that on the plate verbatim, so the name is said plainly here
1060
+ // instead — the name, not the sentence around it, since V8 writes "x is not defined" where
1061
+ // JavaScriptCore writes "Can't find variable: x". A name on a branch the probe does not take still
1062
+ // escapes at draw time: nothing short of reading the body would catch that, and the body is not
1063
+ // ours to read.
1064
+ const UNKNOWN_NAME = new RegExp(`([${WORD}$]+)(?: is not defined)?$`); // the whole run, ωsin as much as sni
1065
+ /* JavaScript's parser is the one that reads the body, and its words are its own: "2 +" comes back
1066
+ * as an unexpected ")", the bracket this wrapper added, and the wording differs between engines the
1067
+ * way probeValue describes below for a name. The reader typed a formula, so what comes back says
1068
+ * that. Every sentence this file writes itself is thrown before here. */
1069
+ function bodyFunction(body, names = []){
1070
+ try { return new Function(...ENV_KEYS, ...names, `"use strict";return (${VARS}) => (${body});`); }
1071
+ catch(e){
1072
+ if(!(e instanceof SyntaxError)) throw e;
1073
+ throw new Error("this is not a complete formula: something is missing, or in the wrong place");
1074
+ }
1075
+ }
1076
+ function probeValue(fn){
1077
+ try{ return fn(...PROBE); }
1078
+ catch(e){
1079
+ if(!(e instanceof ReferenceError)) throw e;
1080
+ throw new Error(`${UNKNOWN_NAME.exec(e.message)?.[1] ?? "that name"} is not a function or a variable here`);
1081
+ }
1082
+ }
1083
+ // a leading minus becomes "-1*" so that -x^2 means -(x^2): JavaScript refuses a unary minus
1084
+ // directly before **, and the maths convention is the negation of the power. A minus right
1085
+ // after ^ (2^-3) is left alone, since ^ is not in the operator class
1086
+ const UNARY_MINUS = new RegExp(`(^|[(,+\\-*/%<>=?:&|!])(\\s*)-(?=\\s*[${WORD}(.])`, "g");
1087
+ // The compiled formula with its free names still open: `names`, in order of first appearance, and
1088
+ // bind(values), the (x, y, z, r, th, u, v, t) => number with each name bound to values[name] or
1089
+ // FREE_DEFAULT. The pipeline runs once, here. ENV and the free names are bound by calling the
1090
+ // outer function, and the arrow that comes back closes over them: an evaluation then costs its
1091
+ // eight arguments and nothing else (spreading ENV into every call, every value of it at 129² calls
1092
+ // a frame, was two thirds of the old cost), and a dial drag is one call of the outer function
1093
+ // again: microseconds, and never a second normalize. Throws with a readable message.
1094
+ function bindable(src){
1095
+ const text = normalize(src);
1096
+ const body = bindBinders(text).replace(UNARY_MINUS, "$1$2-1*").replace(/\^/g, "**");
1097
+ if(!ALLOWED.test(body)) throw new Error("illegal character in formula");
1098
+ if(!body.trim()) throw new Error("formula is empty");
1099
+ const names = freeNamesOf(text), outer = bodyFunction(body, names);
1100
+ const bind = (values = {}) => outer(...ENV_VALS, ...names.map(n => values[n] ?? FREE_DEFAULT));
1101
+ // the type check, and an unknown name's refusal, once: neither depends on what the names are bound to
1102
+ if(typeof probeValue(bind()) !== "number") throw new Error("formula must return a number");
1103
+ return {names, bind, text}; // text: the normalized source, for a caller that would otherwise normalize again
1104
+ }
1105
+ // → (x, y, z, r, th, u, v, t) => number, its free names bound to values (or FREE_DEFAULT)
1106
+ function compile(src, values){ return bindable(src).bind(values); }
1107
+
1108
+ // a parametric formula: u is fed to every slot it could mean, v (a second parameter, for
1109
+ // shapes) to y and to its own slot
1110
+ function evalParam(fn, u, t, v = 0){ return fn(u, v, 0, u, u, u, v, t); }
1111
+ // a height field gets x, y and their polar form; a wave slice gets x and its depth z (also as y)
1112
+ function evalPlane(fn, x, y, t){ const r = Math.hypot(x, y); return fn(x, y, 0, r, Math.atan2(y, x), r, y, t); }
1113
+ function evalLine(fn, x, z, t){ return fn(x, z, z, Math.abs(x), x, x, z, t); }
1114
+
1115
+ // the formula as it reads on the plate: 3x, θ, π, · for products, superscript powers, ∫ with its limits; what
1116
+ // it prints pastes back, so a limit is bare only when LaTeX reads it as one (a number, a letter, a symbol)
1117
+ const PLATE_DIGITS = "0123456789−"; // the minus is the plate's by the time these print
1118
+ const script = (table, digits) => respell(PLATE_DIGITS, table, digits);
1119
+ const ATOM = new RegExp(`^(?:\\d+(?:\\.\\d+)?|[${LETTER}θπ∞])$`); // a subscripted name is bracketed: LIMIT reads a bare one as a letter
1120
+ const limit = (mark, table, text) => /^−?\d+$/.test(text) ? script(table, text) : mark + (ATOM.test(text) ? text : `(${text})`);
1121
+ // ∫ prints its dummy in the differential; Σ and ∏ print theirs in the lower limit, which limit()
1122
+ // already brackets because k=1 is not an atom — and a bracketed limit is what limitAt reads back.
1123
+ // d/dx keeps its term in brackets whatever the term is: unbracketed it would reach only as far as
1124
+ // termEnd when read again, so a printed sum of two things would come back as the derivative of one.
1125
+ const SIGN = {sum: "Σ", prod: "∏"};
1126
+ const printBinders = text => eachBinder(text, (head, expr, name, from, to) =>
1127
+ head === "diff" ? `d/d${name} (${expr})`
1128
+ : head === "integral" ? `∫${limit("_", SUB, from)}${limit("^", SUPER, to)} ${expr} d${name}`
1129
+ : `${SIGN[head]}${limit("_", SUB, `${name}=${from}`)}${limit("^", SUPER, to)} ${expr}`);
1130
+ // a name as the plate and the rail show it, and as it pastes back: θ π ∞ for the ones the language
1131
+ // spells in letters (in a subscript too, x_θ), a digit subscript set low (x_1 is x₁, since normalize
1132
+ // reads ₁ as _{1}), one letter as written, and a longer one in braces, since x_max would read as x_m times ax
1133
+ const NAME_GLYPH = {th: "θ", pi: "π", inf: "∞"};
1134
+ const glyphOf = name => Object.hasOwn(NAME_GLYPH, name) ? NAME_GLYPH[name] : name;
1135
+ function prettySubscript(sub){
1136
+ if(/^\d+$/.test(sub)) return script(SUB, sub);
1137
+ return [...sub].length === 1 ? `_${sub}` : `_{${sub}}`;
1138
+ }
1139
+ function prettyName(name){
1140
+ const [base, sub] = name.split("_");
1141
+ return glyphOf(base) + (sub === undefined ? "" : prettySubscript(glyphOf(sub)));
1142
+ }
1143
+ function pretty(expr){
1144
+ const text = normalize(expr), atoms = [...VARS, "θ", ...dummiesOf(text)].join("|");
1145
+ // a whole number before a variable loses its dot (3*x is 3x, 3*th is 3θ); a free name keeps it, a
1146
+ // subscripted one included, which by now carries its digits low
1147
+ const implicit = new RegExp(`(?<![${WORD}])(\\d+(?:\\.\\d+)?)\\s*\\*\\s*(?=(?:${atoms})(?![${WORD}${SUB}_{])|\\()`, "g");
1148
+ return printBinders(text
1149
+ .replace(IDENT, prettyName)
1150
+ .replace(implicit, "$1") // 3*x reads as 3x
1151
+ .replace(/\*/g, " · ").replace(/-/g, "−").replace(/\s+/g, " ")
1152
+ .replace(/\^(?:(−?\d+)|\((−?\d+)\))/g, (_, d, e) => script(SUPER, d ?? e))); // x^2 and the pasted x² alike
1153
+ }
1154
+ // three expressions as their plate shows them, one bracket round the three
1155
+ const prettyTriple = exprs => `(${pretty(exprs.x)}, ${pretty(exprs.y)}, ${pretty(exprs.z)})`;
1156
+
1157
+ Object.assign(WL, {VARS, ENV, GLOSS, normalize, readsVars, isFreeName, FREE_DEFAULT, bindable, compile, pretty, prettyTriple, prettyName, dropLhs, namesOf,
1158
+ evalParam, evalPlane, evalLine});
1159
+ })();
1160
+
1161
+ export const {fromLatex, normalize, pretty, prettyName, bindable, compile, readsVars, isFreeName, VARS, ENV, BINDERS, GREEK, FREE_DEFAULT} = window.Wavelace;