@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/index.d.ts +49 -0
- package/index.js +1161 -0
- 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;
|