@mathzetta/inlinemath 0.0.0-stage → 0.2.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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -2
  3. package/dist/cas.external.js +2 -0
  4. package/dist/cas.js +2 -0
  5. package/dist/chunks/calculate-Cz87Lt0N.js +2 -0
  6. package/dist/chunks/draw-Bev17BJJ.js +2 -0
  7. package/dist/chunks/edit-C65xXzo8.js +2 -0
  8. package/dist/chunks/edit-M68aRA5o.js +2 -0
  9. package/dist/chunks/html-BimAqn7x.js +2 -0
  10. package/dist/chunks/html-CupluJSh.js +2 -0
  11. package/dist/chunks/lite-3Fb-OHbt.js +2 -0
  12. package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
  13. package/dist/fonts/fonts.css +17 -0
  14. package/dist/fonts/lmroman12-italic.woff2 +0 -0
  15. package/dist/fonts/lmroman12-regular.woff2 +0 -0
  16. package/dist/inlinemath-cas.external.global.min.js +2 -0
  17. package/dist/inlinemath-cas.global.min.js +2 -0
  18. package/dist/inlinemath-lite-calc.global.min.js +2 -0
  19. package/dist/inlinemath-lite.external.global.min.js +2 -0
  20. package/dist/inlinemath-lite.global.min.js +2 -0
  21. package/dist/inlinemath.css +158 -0
  22. package/dist/inlinemath.external.global.min.js +2 -0
  23. package/dist/inlinemath.external.js +2 -0
  24. package/dist/inlinemath.global.min.js +2 -0
  25. package/dist/inlinemath.js +2 -0
  26. package/dist/lite-calc.js +2 -0
  27. package/dist/lite.external.js +2 -0
  28. package/dist/lite.js +2 -0
  29. package/dist/markdown.external.js +2 -0
  30. package/dist/markdown.js +2 -0
  31. package/fonts/GUST-FONT-LICENSE.txt +30 -0
  32. package/fonts/fonts.css +17 -0
  33. package/fonts/lmroman12-italic.woff2 +0 -0
  34. package/fonts/lmroman12-regular.woff2 +0 -0
  35. package/inlinemath.css +158 -0
  36. package/package.json +95 -4
  37. package/src/calculator.js +6 -0
  38. package/src/cas.d.ts +3 -0
  39. package/src/cas.js +149 -0
  40. package/src/draw.js +72 -0
  41. package/src/edit.js +96 -0
  42. package/src/glyphs.js +40 -0
  43. package/src/html.js +32 -0
  44. package/src/index.d.ts +3 -0
  45. package/src/index.js +17 -0
  46. package/src/layout.js +649 -0
  47. package/src/lite-calc.d.ts +3 -0
  48. package/src/lite-calc.js +15 -0
  49. package/src/lite.d.ts +3 -0
  50. package/src/lite.js +18 -0
  51. package/src/markdown.d.ts +3 -0
  52. package/src/markdown.js +676 -0
  53. package/src/mathzetta.js +18 -0
  54. package/src/select.js +108 -0
  55. package/src/types.d.ts +204 -0
  56. package/src/variables.js +228 -0
  57. package/src/view.js +382 -0
package/src/layout.js ADDED
@@ -0,0 +1,649 @@
1
+ /**
2
+ * MathZetta tree → HTML laid out with CSS (see inlinemath.css).
3
+ *
4
+ * Every rendered piece is a box { html, h, d }: its markup, and how far it
5
+ * reaches above (h) and below (d) the baseline, in em of the surrounding
6
+ * text. Nothing is measured in a browser; the estimates place fractions on
7
+ * the maths axis, raise exponents above tall bases and size the integral
8
+ * sign, so the same string is right in Node, in a video frame or on a page.
9
+ * Delimiters, the radical's bar and borders stretch by themselves with CSS.
10
+ *
11
+ * Every tree node is one element:
12
+ *
13
+ * <span class="mv-n mv-op-frac" data-node="4" data-op="/">…</span>
14
+ *
15
+ * `data-node` is MathZetta's node id (the postfix index), the same id used
16
+ * by toSpeechSegments(), toMathML() and toHTML(). Signs, brackets and other
17
+ * decoration carry no id, so a click on them belongs to the node that drew
18
+ * them.
19
+ */
20
+
21
+ import {
22
+ GREEK, needsBrackets, unaryNeedsBrackets, isMinusOneTimes, isImplicitProduct, isVariable,
23
+ firstLeaf, customName, derivativeOrder, bracketedSides, bracketOp, firstVariable,
24
+ isCustomFunction, precedenceOf, toAudioSpeech, MATRICES, matrixRows,
25
+ } from './mathzetta.js';
26
+ import { svg } from './glyphs.js';
27
+
28
+ // Metrics, in em. They match inlinemath.css; change both together.
29
+ const LH = 0.8; // a line of text: height above the baseline (line-height 1)
30
+ const LD = 0.2; // and depth below it
31
+ const AXIS = 0.25; // the maths axis: fraction bars and big operators centre here
32
+ const RULE = 0.06; // fraction bar, radical bar
33
+ const GAP = 0.1; // between a fraction bar and its numerator or denominator
34
+ const SCRIPT = 0.7; // exponent and subscript size; deeper scripts use SCRIPT2
35
+ const SCRIPT2 = 0.8;
36
+ const MINUS = '−';
37
+
38
+ const OP_CLASS = {
39
+ '+': 'add', '-': 'sub', '*': 'mul', '/': 'frac', '^': 'pow', '_': 'subscript', 'neg': 'neg', 'pm': 'pm',
40
+ '!': 'factorial', '=': 'eq', '===': 'eq', '≠': 'neq', '≠=': 'neq', '<': 'lt', '>': 'gt', '≤': 'le', '≥': 'ge',
41
+ '≈': 'approx', '≡': 'equiv', '∈': 'in', '∉': 'notin', '∪': 'union', '∩': 'intersect', '→': 'to', '±': 'pm',
42
+ '&': 'and', '|': 'or', '\\not': 'not', '\\xor': 'xor', '\\nand': 'nand', '\\nor': 'nor', '?:': 'cases',
43
+ '()': 'tuple', '[]': 'list', '{}': 'set', '(]': 'interval', '[)': 'interval',
44
+ '\\sumidx': 'sum', '\\prodidx': 'prod', 'mod': 'mod', 'pmod': 'pmod',
45
+ pmatrix: 'matrix', bmatrix: 'matrix', vmatrix: 'matrix',
46
+ };
47
+
48
+ const RELATIONS = {
49
+ '=': '=', '===': '=', '≠': '≠', '≠=': '≠', '<': '<', '>': '>', '≤': '≤', '≥': '≥',
50
+ '∈': '∈', '∉': '∉', '→': '→', '≈': '≈', '≡': '≡',
51
+ '&': '∧', '|': '∨', '\\xor': '⊕', '\\nand': '↑', '\\nor': '↓',
52
+ };
53
+ const BINARY = { '±': '±', '∪': '∪', '∩': '∩', 'mod': 'mod' };
54
+
55
+ const TRIG = ['sin', 'cos', 'tan', 'csc', 'sec', 'cot', 'sinh', 'cosh', 'tanh'];
56
+ const INVERSE = { arcsin: 'sin', arccos: 'cos', arctan: 'tan', arcsinh: 'sinh', arccosh: 'cosh', arctanh: 'tanh' };
57
+ /** Functions written with a name and, when the argument is simple, no brackets: sin x, ln x. */
58
+ const NAMED = [...TRIG, ...Object.keys(INVERSE), 'ln', 'det'];
59
+ /** Functions always written with brackets: max(a, b). */
60
+ const CALLS = { atan2: 'atan2', max: 'max', min: 'min', gcd: 'gcd', mean: 'mean', round: 'round', fixed: 'fixed', expm1: 'expm1', random: 'random' };
61
+
62
+ const GROUPS = { '()': ['(', ')'], '[]': ['[', ']'], '{}': ['{', '}'], '(]': ['(', ']'], '[)': ['[', ')'] };
63
+ const LATEX_SYMBOLS = { '\\partial': '∂', '\\nabla': '∇', '\\ell': 'ℓ', '\\infty': '∞', '\\emptyset': '∅' };
64
+
65
+ /** Custom functions drawn as decorations: \boxed{x} parses as boxed(x). */
66
+ const ACCENTS = { vec: '→', hat: 'ˆ', widehat: 'ˆ', tilde: '˜', dot: '˙', ddot: '¨' };
67
+ const DECORATIONS = ['boxed', 'overline', 'bar', 'underline', 'cancel', ...Object.keys(ACCENTS)];
68
+
69
+ export const DEFAULTS = {
70
+ prefix: 'mv',
71
+ display: true, // big operators with limits above and below, full-size fractions
72
+ fnParens: 'auto', // 'auto': sin x, ln|x|, sin(x + 1); 'always': sin(x)
73
+ trigPowers: true, // sin(x)^2 written sin²x
74
+ inverseTrig: 'arc', // 'arc': arcsin x; 'power': sin⁻¹x
75
+ derivativeBrackets: '[', // d/dx [x² + 1], or '(' for d/dx (x² + 1)
76
+ scriptFractions: 'slash',// x^{1/2} written x^{1/2} with a slash; 'stack' stacks it
77
+ cases: true, // c ? a : b drawn as a piecewise brace
78
+ label: true, // accessible name of the whole expression: true = MathZetta's toAudioSpeech (the grouping in the words), a string, (tree) => string, or false
79
+ mathml: null, // MathZetta's toMathML (import it from 'mathzetta/mathml'): also write the expression as visually hidden MathML, which screen readers can step through and show in math braille; it replaces the label
80
+ attributes: null, // (node) => ({ ... }) extra attributes for a node's element
81
+ style: null, // (node) => 'css' inline style for a node's element
82
+ };
83
+
84
+ const escape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
85
+ const em = (x) => `${+x.toFixed(3)}em`;
86
+ const box = (html, h = LH, d = LD) => ({ html, h, d });
87
+
88
+ /** Boxes side by side. */
89
+ function row(...boxes) {
90
+ const list = boxes.flat().filter(Boolean);
91
+ return {
92
+ html: list.map((b) => b.html).join(''),
93
+ h: Math.max(LH, ...list.map((b) => b.h)),
94
+ d: Math.max(LD, ...list.map((b) => b.d)),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * @param {object} tree MathZetta tree (from parseTree) — nodes with ids
100
+ * @param {object} [options] see DEFAULTS
101
+ * @returns {{ html: string, h: number, d: number }}
102
+ */
103
+ export function layout(tree, options = {}) {
104
+ const r = new Layout({ ...DEFAULTS, ...options });
105
+ const b = r.render(tree, { level: 0 });
106
+ const cls = `${r.p}${r.o.display ? ` ${r.p}-display` : ''}`;
107
+ // Screen readers get the expression as MathML they can walk (mathml: true), or read as a teacher would (the
108
+ // label); the spans and glyphs of the drawing are never read piecemeal.
109
+ const mathml = typeof r.o.mathml === 'function' ? assistiveMathML(r.o.mathml, tree, r.p, r.o.display) : '';
110
+ const label = mathml ? null : accessibleName(tree, r.o.label);
111
+ const body = label === null && !mathml ? b.html : b.html.replace(/^<span /, '<span aria-hidden="true" ');
112
+ const attrs = label === null ? '' : ` role="math" aria-label="${escape(label)}"`;
113
+ return { html: `<span class="${cls}"${attrs}>${body}${mathml}</span>`, h: b.h, d: b.d };
114
+ }
115
+
116
+ /**
117
+ * The expression as MathML for assistive technology: no ids (two drawings on a page must not share them), hidden
118
+ * from sight. The page passes MathZetta's toMathML, so Inline Math itself imports only what every page maps.
119
+ */
120
+ function assistiveMathML(toMathML, tree, prefix, display) {
121
+ try {
122
+ return String(toMathML(tree)).replace(/ id="mz-\d+"/g, '')
123
+ .replace(/^<math>/, `<math class="${prefix}-sr-math"${display ? ' display="block"' : ''}>`);
124
+ } catch {
125
+ return ''; // nothing MathZetta can write as MathML: the label stands in
126
+ }
127
+ }
128
+
129
+ function accessibleName(tree, label) {
130
+ if (label === false || label === null) return null;
131
+ if (typeof label === 'string') return label;
132
+ try {
133
+ // a screen reader is speech with no drawing in view: the grouping has to be in the words
134
+ return typeof label === 'function' ? label(tree) : toAudioSpeech(tree);
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ class Layout {
141
+ constructor(options) {
142
+ this.o = options;
143
+ this.p = options.prefix;
144
+ }
145
+
146
+ /** An element of class `mv-<kind>` (several kinds: 'a b'). */
147
+ el(kind, content, style = '', extra = '') {
148
+ const cls = kind.split(' ').map((k) => `${this.p}-${k}`).join(' ');
149
+ return `<span class="${cls}"${style ? ` style="${style}"` : ''}${extra}>${content}</span>`;
150
+ }
151
+
152
+ /** A run of text. Remembered as a leaf so a node holding only this needs no second element. */
153
+ text(kind, s) {
154
+ const b = box(this.el(kind, escape(s)));
155
+ b.leaf = { kind, content: escape(s) };
156
+ return b;
157
+ }
158
+ /** Binary operator and relation signs carry the spacing around them. */
159
+ bin(s) { return this.text('bin', s); }
160
+ rel(s) { return this.text('rel', s); }
161
+ punct(s) { return this.text('punct', s); }
162
+ word(s) { return this.text('word', s); }
163
+
164
+ /** The element for a tree node. */
165
+ wrap(node, kinds, b) {
166
+ let attrs = '';
167
+ if (node.id !== undefined) attrs += ` data-node="${node.id}"`;
168
+ if (node.type === 'op') attrs += ` data-op="${escape(node.op)}"`;
169
+ const extra = this.o.attributes ? this.o.attributes(node) || {} : {};
170
+ for (const [k, v] of Object.entries(extra)) {
171
+ if (k !== 'class' && k !== 'style') attrs += ` ${k}="${escape(v)}"`;
172
+ }
173
+ const style = [extra.style, this.o.style && this.o.style(node)].filter(Boolean).join(';');
174
+ const all = b.leaf ? ['n', ...kinds, b.leaf.kind] : ['n', ...kinds];
175
+ const cls = all.map((k) => `${this.p}-${k}`).concat(extra.class ? [extra.class] : []).join(' ');
176
+ const content = b.leaf ? b.leaf.content : b.html;
177
+ return { html: `<span class="${cls}"${style ? ` style="${escape(style)}"` : ''}${attrs}>${content}</span>`, h: b.h, d: b.d };
178
+ }
179
+
180
+ // ---- leaves ---------------------------------------------------------------
181
+
182
+ render(node, ctx) {
183
+ if (node.type === 'number') return this.number(node);
184
+ if (node.type === 'string') return this.wrap(node, ['text'], this.text('text', node.value.replace(/^["'`]|["'`]$/g, '')));
185
+ if (node.type === 'symbol') return this.wrap(node, ['symbol'], this.symbol(node.value));
186
+ return this.operation(node, ctx);
187
+ }
188
+
189
+ number(node) {
190
+ return this.wrap(node, ['number'], this.text('num', node.value.replace(/^-/, MINUS)));
191
+ }
192
+
193
+ symbol(v) {
194
+ if (GREEK[v]) v = GREEK[v];
195
+ if (LATEX_SYMBOLS[v]) return this.text('ord', LATEX_SYMBOLS[v]);
196
+ if (v === '\\+' || v === '\\-') return this.text('ord', v[1]);
197
+ if (v === '-∞') return this.text('ord', MINUS + '∞');
198
+ if (/^[A-Za-z]$/.test(v) || /^[α-ω]$/.test(v)) return this.text('var', v);
199
+ if (/^[A-Za-z]′+$/.test(v)) return box(this.el('var', escape(v[0])) + this.el('prime', escape(v.slice(1))));
200
+ if (/^Δ[A-Za-z]$/.test(v)) return box(this.el('ord', 'Δ') + this.el('var', v[1]));
201
+ if (v.endsWith('⁻¹')) return this.script(this.symbol(v.slice(0, -2)), this.text('num', MINUS + '1'), { level: 0 });
202
+ if (/^\\[A-Za-z]+$/.test(v)) return this.text('name', v.slice(1));
203
+ if (/^[A-Za-z]{2,}$/.test(v)) return this.text('name', v); // upright, like \mathrm
204
+ return this.text('ord', v);
205
+ }
206
+
207
+ // ---- operations -----------------------------------------------------------
208
+
209
+ operation(node, ctx) {
210
+ const { op, args } = node;
211
+ const [a, b] = args;
212
+ const r = (n, c = {}) => this.render(n, { ...ctx, ...c, forceParens: c.forceParens || false, before: c.before || false });
213
+ const kind = OP_CLASS[op] ?? (/^[a-z0-9]+$/i.test(op) ? op : 'other');
214
+ const wrap = (content) => this.wrap(node, ['op', `op-${kind}`], content);
215
+ const side = (child, s) => (needsBrackets(node, child, s) && child.op !== '/' ? this.fence(r(child)) : r(child));
216
+
217
+ if (NAMED.includes(op) || op === 'log') return wrap(this.named(node, ctx));
218
+ if (CALLS[op]) return wrap(row(this.word(CALLS[op]), this.fence(this.list(args, ctx))));
219
+ if (isCustomFunction(op)) return wrap(this.custom(node, ctx));
220
+ const rows = matrixRows(node); // pmatrix, bmatrix, vmatrix, and [[1, 2], [3, 4]]
221
+ if (rows) return wrap(this.matrix(node, rows, ctx));
222
+ if (GROUPS[op]) return wrap(this.fence(this.list(args, ctx), ...GROUPS[op]));
223
+ if (RELATIONS[op] && args.length === 2) return wrap(row(side(a, 'left'), this.rel(RELATIONS[op]), side(b, 'right')));
224
+ if (BINARY[op] && args.length === 2) return wrap(row(side(a, 'left'), this.bin(BINARY[op]), side(b, 'right')));
225
+
226
+ switch (op) {
227
+ case '+': {
228
+ // a + (-1·x) and a + (-3) read as a − x and a − 3.
229
+ if (isMinusOneTimes(b)) return wrap(row(side(a, 'left'), this.bin(MINUS), this.wrap(b, ['op', 'op-mul'], this.render(b.args[1], ctx))));
230
+ if (b.type === 'number' && b.value.startsWith('-')) return wrap(row(side(a, 'left'), this.bin(MINUS), this.wrap(b, ['number'], this.text('num', b.value.slice(1)))));
231
+ return wrap(row(side(a, 'left'), this.bin('+'), this.signed(b, side(b, 'right'))));
232
+ }
233
+ case '-':
234
+ return wrap(row(side(a, 'left'), this.bin(MINUS), this.signed(b, side(b, 'right'))));
235
+ case '*':
236
+ return wrap(this.product(node, ctx));
237
+ case '/':
238
+ return wrap(this.fraction(node, ctx));
239
+ case '^':
240
+ return wrap(this.power(node, ctx));
241
+ case '_':
242
+ return wrap(this.script(r(a, { forceParens: true }), null, ctx, r(b, { level: ctx.level + 1 })));
243
+ case 'neg':
244
+ return wrap(row(this.text('sign', MINUS), unaryNeedsBrackets(op, a) ? this.fence(r(a)) : r(a, { before: ctx.before })));
245
+ case 'pm':
246
+ return wrap(row(this.text('sign', '±'), unaryNeedsBrackets(op, a) ? this.fence(r(a)) : r(a)));
247
+ case '!':
248
+ return wrap(row(unaryNeedsBrackets(op, a) ? this.fence(r(a)) : r(a, { forceParens: true }), this.text('ord', '!')));
249
+ case '\\not':
250
+ return wrap(row(this.text('sign', '¬'), unaryNeedsBrackets(op, a) ? this.fence(r(a)) : r(a)));
251
+ case 'pmod':
252
+ return wrap(row(r(a), this.fence(row(this.word('mod'), this.text('space', ''), r(b)))));
253
+ case 'sqrt':
254
+ return wrap(this.radical(r(a)));
255
+ case 'root':
256
+ return wrap(this.radical(r(b), this.render(a, { level: ctx.level + 2 })));
257
+ case 'abs':
258
+ return wrap(this.fence(r(a), '|', '|'));
259
+ case 'floor':
260
+ return wrap(this.fence(r(a), '⌊', '⌋'));
261
+ case 'ceil':
262
+ return wrap(this.fence(r(a), '⌈', '⌉'));
263
+ case 'loglog':
264
+ return wrap(row(this.word('ln'), this.fence(row(this.word('ln'), this.fence(r(a))))));
265
+ case 'logloglog':
266
+ return wrap(row(this.word('ln'), this.fence(row(this.word('ln'), this.fence(row(this.word('ln'), this.fence(r(a))))))));
267
+ case 'perm':
268
+ return wrap(this.script(this.script(box(''), this.render(a, { level: ctx.level + 1 }), ctx), null, ctx,
269
+ this.render(b, { level: ctx.level + 1 }), this.word('P')));
270
+ case 'comb':
271
+ return wrap(this.fence(this.stack(r(a), r(b), false)));
272
+ case '?:':
273
+ return wrap(this.o.cases ? this.cases(node, ctx) : row(r(a), this.bin('?'), r(b), this.bin(':'), r(args[2])));
274
+ case 'sum':
275
+ return wrap(row(args.map((n, i) => (i ? [this.bin('+'), r(n)] : [r(n)])).flat()));
276
+ case 'prod':
277
+ return wrap(row(args.map((n, i) => (i ? [this.bin('·'), r(n)] : [r(n)])).flat()));
278
+ case 'der':
279
+ return wrap(this.derivative(node, ctx));
280
+ case 'int':
281
+ return wrap(this.integral(args, ctx));
282
+ case 'lim':
283
+ return wrap(this.limit(args, ctx));
284
+ case '\\sumidx':
285
+ return wrap(this.bigOperator('sum', args, ctx));
286
+ case '\\prodidx':
287
+ return wrap(this.bigOperator('prod', args, ctx));
288
+ }
289
+ throw new Error(`Inline Math cannot draw '${op}'`);
290
+ }
291
+
292
+ /** The right side of + or −: a leading sign gets brackets, a + (−b). */
293
+ signed(node, b) {
294
+ const starts = node.type === 'number' ? node.value.startsWith('-') : node.op === 'neg' || node.op === 'pm';
295
+ return starts && !b.fenced ? this.fence(b) : b;
296
+ }
297
+
298
+ list(args, ctx) {
299
+ return row(args.map((n, i) => (i ? [this.punct(','), this.render(n, ctx)] : [this.render(n, ctx)])).flat());
300
+ }
301
+
302
+ /** 2x, x·2, (x+1)(x−2), sin x cos x. */
303
+ product(node, ctx) {
304
+ const [a, b] = node.args;
305
+ if (isMinusOneTimes(node)) return row(this.text('sign', MINUS), this.render(b, ctx));
306
+ const sides = bracketedSides(node);
307
+ const implicit = isImplicitProduct(a, b, sides);
308
+ // "sin x · y" must keep its brackets: sin(x) y. Before another function it needn't: sin x cos x.
309
+ const nextIsCall = startsWithCall(b);
310
+ const left = sides.left ? this.fence(this.render(a, ctx)) : this.render(a, { ...ctx, before: implicit && !nextIsCall });
311
+ const right = sides.right ? this.fence(this.render(b, ctx)) : this.render(b, { ...ctx, before: ctx.before });
312
+ // A thin space before a function name: 2 sin x, x² ln x.
313
+ const join = !implicit ? this.bin('·') : box(this.el(nextIsCall ? 'fnspace' : 'times', ''));
314
+ return row(left, join, right);
315
+ }
316
+
317
+ // ---- functions ------------------------------------------------------------
318
+
319
+ /** sin x, sin(x + 1), sin²x, sin⁻¹x, ln|x|, log₂ x. */
320
+ named(node, ctx, power = null) {
321
+ const { op, args } = node;
322
+ const arg = op === 'log' ? args[1] : args[0];
323
+ let name;
324
+ if (INVERSE[op] && this.o.inverseTrig === 'power') {
325
+ name = this.script(this.word(INVERSE[op]), this.text('num', MINUS + '1'), ctx);
326
+ } else {
327
+ name = this.word(op);
328
+ }
329
+ if (power) name = this.script(name, power, ctx);
330
+ if (op === 'log') name = this.script(name, null, ctx, this.render(args[0], { level: ctx.level + 1 }));
331
+ const bare = this.o.fnParens === 'auto' && !ctx.forceParens && !ctx.before && isSimpleArgument(arg);
332
+ const body = this.render(arg, { ...ctx, forceParens: false, before: false });
333
+ return row(name, bare ? row(box(this.el('fnspace', '')), body) : this.fence(body));
334
+ }
335
+
336
+ custom(node, ctx) {
337
+ const name = customName(node.op);
338
+ const args = node.args;
339
+ if (DECORATIONS.includes(name) && args.length === 1) return this.decorate(name, this.render(args[0], ctx));
340
+ // f(x), g′(x): a one-letter function is italic like a variable; sgn(x) is upright.
341
+ const head = /^[A-Za-z]′*(⁻¹)?$/.test(name) ? this.symbol(name) : this.word(name);
342
+ return row(head, box(this.el('callgap', '')), this.fence(this.list(args, ctx)));
343
+ }
344
+
345
+ decorate(name, b) {
346
+ switch (name) {
347
+ case 'boxed': return box(this.el('boxed', b.html), b.h + 0.22, b.d + 0.22);
348
+ // a drawn stroke, not a background: Windows high contrast drops backgrounds, and a cancelled term would look kept
349
+ case 'cancel': return box(this.el('cancel', b.html + svg('cancel', `${this.p}-cancel-line`)), b.h, b.d);
350
+ case 'underline': return box(this.el('underline', b.html), b.h, b.d + 0.12);
351
+ case 'overline': case 'bar': return box(this.el('over', b.html), b.h + 0.14, b.d);
352
+ default: return box(this.el('accent', this.el('accent-mark', ACCENTS[name]) + b.html), b.h + 0.3, b.d);
353
+ }
354
+ }
355
+
356
+ // ---- fractions, scripts, radicals ------------------------------------------
357
+
358
+ fraction(node, ctx) {
359
+ const [a, b] = node.args;
360
+ if (ctx.level > 0 && this.o.scriptFractions === 'slash' && isSimpleArgument(a, true) && isSimpleArgument(b, true)) {
361
+ const side = (n) => (n.type === 'op' && !isSimpleArgument(n) ? this.fence(this.render(n, ctx)) : this.render(n, ctx));
362
+ return row(side(a), this.text('ord', '/'), side(b));
363
+ }
364
+ // A fraction inside a fraction, or in running text, is set smaller.
365
+ const k = ctx.level > 0 || ctx.inFraction || !this.o.display ? 0.8 : 1;
366
+ const inner = { ...ctx, inFraction: true, forceParens: false, before: false };
367
+ return this.stack(this.render(a, inner), this.render(b, inner), true, k);
368
+ }
369
+
370
+ /** One box over another, centred on the axis; with a bar for fractions, without for binomials. */
371
+ stack(num, den, bar = true, k = 1) {
372
+ const scale = (b) => (k === 1 ? b.html : this.el('scaled', b.html, `font-size:${em(k)}`));
373
+ const t = bar ? RULE : 0;
374
+ // Numerator and denominator are lines of the surrounding size, so never shallower than one.
375
+ const lift = AXIS + t / 2 + GAP + Math.max(LD, k * num.d); // the numerator's baseline
376
+ const html = this.el(`frac${bar ? '' : ' frac-nobar'}`,
377
+ this.el('numer', scale(num)) + this.el('denom', scale(den)),
378
+ `vertical-align:${em(lift)}`);
379
+ return box(html, lift + Math.max(LH, k * num.h), Math.max(LH, k * den.h) + Math.max(LD, k * den.d) + GAP + t / 2 - AXIS);
380
+ }
381
+
382
+ /** x², (x+1)³, e^{x²}, sin²x, x₁². */
383
+ power(node, ctx) {
384
+ const [a, b] = node.args;
385
+ const up = { ...ctx, level: ctx.level + 1, forceParens: false, before: false, inFraction: false };
386
+ const exponent = () => this.render(b, up);
387
+ if (this.o.trigPowers && TRIG.includes(a.op) && isPositiveWhole(b)) {
388
+ return this.wrap(a, ['op', `op-${a.op}`], this.named(a, ctx, exponent()));
389
+ }
390
+ if (a.op === '_') {
391
+ const sub = this.render(a.args[1], up);
392
+ return this.wrap(a, ['op', 'op-subscript'], this.script(this.render(a.args[0], { ...ctx, forceParens: true }), exponent(), ctx, sub));
393
+ }
394
+ // (sin x)^{1/2}: a named function keeps its own name outside the brackets only as sin²x.
395
+ const fenceBase = needsBrackets(node, a, 'left') || (a.type === 'number' && a.value.startsWith('-')) || a.op === '/' || NAMED.includes(a.op);
396
+ const base = fenceBase ? this.fence(this.render(a, ctx)) : this.render(a, { ...ctx, forceParens: true });
397
+ return this.script(base, exponent(), ctx);
398
+ }
399
+
400
+ /**
401
+ * A base with an exponent, a subscript or both. `pre` goes between the
402
+ * base and the scripts, used for ⁿPₖ.
403
+ */
404
+ script(base, sup, ctx, sub = null, pre = null) {
405
+ const kk = ctx.level > 0 ? SCRIPT2 : SCRIPT; // relative to the current size
406
+ const head = pre ? row(base, pre) : base;
407
+ let rise = sup ? Math.max(0.42, base.h - 0.38, kk * sup.d + 0.12) : 0;
408
+ let drop = sub ? Math.max(0.2, base.d - 0.05, kk * sub.h - 0.45) : 0;
409
+ if (sup && sub) {
410
+ const clash = 0.1 - ((rise - kk * sup.d) - (kk * sub.h - drop));
411
+ if (clash > 0) drop += clash;
412
+ }
413
+ const scaled = (b) => this.el('script', b.html, `font-size:${em(kk)}`);
414
+ if (sup && !sub) {
415
+ return box(base.html + this.el('sup', sup.html, `font-size:${em(kk)};vertical-align:${em(rise / kk)}`),
416
+ Math.max(head.h, rise + kk * sup.h), Math.max(head.d, kk * sup.d - rise));
417
+ }
418
+ if (sub && !sup) {
419
+ return box(head.html + this.el('sub', sub.html, `font-size:${em(kk)};vertical-align:${em(-drop / kk)}`),
420
+ Math.max(head.h, kk * sub.h - drop), Math.max(head.d, drop + kk * sub.d));
421
+ }
422
+ const top = rise + kk * sup.h;
423
+ const bottom = drop + kk * sub.d;
424
+ const column = this.el('subsup', scaled(sup) + scaled(sub), `vertical-align:${em(rise)};height:${em(top + bottom)}`);
425
+ return box(head.html + column, Math.max(head.h, top), Math.max(head.d, bottom));
426
+ }
427
+
428
+ /** √x and ⁿ√x: the hook is drawn as tall as the radicand, the bar is its top border. */
429
+ radical(inner, index = null) {
430
+ const top = Math.max(inner.h, LH) + GAP + RULE;
431
+ const depth = Math.max(inner.d, LD);
432
+ const height = top + depth;
433
+ const hook = svg('sqrt', `${this.p}-hook`, `height:${em(height)};width:${em(0.5 + 0.06 * (height - 1))};vertical-align:${em(-depth)}`);
434
+ let pre = '';
435
+ if (index) {
436
+ const k = SCRIPT * 0.8;
437
+ pre = this.el('rootindex', index.html, `font-size:${em(k)};vertical-align:${em((-depth + 0.55 * height) / k)}`);
438
+ }
439
+ return box(this.el('sqrt', pre + hook + this.el('radicand', inner.html)), top, depth);
440
+ }
441
+
442
+ /**
443
+ * Brackets that grow with what they hold. Each side is drawn at 100% of
444
+ * the content's height, so no measuring is needed.
445
+ */
446
+ fence(inner, open = '(', close = ')') {
447
+ const height = Math.max(inner.h, LH) + Math.max(inner.d, LD);
448
+ const tall = height > 1.25;
449
+ const pad = tall ? 0.1 : 0;
450
+ const side = (ch, where) => {
451
+ if (!ch) return { html: '', w: 0 };
452
+ const w = { '(': 0.26, ')': 0.26, '{': 0.36, '}': 0.36 }[ch];
453
+ if (w) {
454
+ const width = w + (tall ? Math.min(0.2, 0.05 * (height - 1)) : 0);
455
+ return { html: svg(ch, `${this.p}-delim ${this.p}-delim-${where}`, `width:${em(width)}`), w: width + 0.1 };
456
+ }
457
+ const name = { '[': 'lbrack', ']': 'rbrack', '|': 'vert', '⌊': 'lfloor', '⌋': 'rfloor', '⌈': 'lceil', '⌉': 'rceil' }[ch];
458
+ return { html: this.el(`delim ${name} delim-${where}`, ''), w: ch === '|' ? 0.18 : 0.3 };
459
+ };
460
+ const l = side(open, 'l');
461
+ const rr = side(close, 'r');
462
+ const style = `padding:${em(pad)} ${em(rr.w)} ${em(pad)} ${em(l.w)}`;
463
+ const b = box(this.el('fence', l.html + inner.html + rr.html, style), Math.max(inner.h, LH) + pad, Math.max(inner.d, LD) + pad);
464
+ b.fenced = true;
465
+ return b;
466
+ }
467
+
468
+ // ---- calculus -------------------------------------------------------------
469
+
470
+ /** dy/dx, d²y/dx², and d/dx [f] with the body after the fraction. */
471
+ derivative(node, ctx) {
472
+ const { n, body, by, variable } = derivativeOrder(node);
473
+ const up = { ...ctx, level: ctx.level + 1 };
474
+ const d = () => this.text('d', 'd');
475
+ const power = () => (n > 1 ? this.render({ type: 'number', value: String(n) }, up) : null);
476
+ const inner = { ...ctx, inFraction: true };
477
+ const dx = row(d(), power() ? this.script(this.render(by, inner), power(), inner) : this.render(by, inner));
478
+ const top = (rest) => row(power() ? this.script(d(), power(), inner) : d(), rest || []);
479
+ const k = ctx.level > 0 || ctx.inFraction || !this.o.display ? 0.8 : 1;
480
+ if (variable) return this.stack(top(this.render(body, inner)), dx, true, k);
481
+ const compound = bracketOp(body) && precedenceOf(bracketOp(body)) < 100;
482
+ const [open, close] = this.o.derivativeBrackets === '(' ? ['(', ')'] : ['[', ']'];
483
+ const after = compound ? this.fence(this.render(body, ctx), open, close) : this.render(body, ctx);
484
+ return row(this.stack(top(), dx, true, k), after);
485
+ }
486
+
487
+ /** ∫ with its limits to the right of the sign, then the integrand and dx. */
488
+ integral(args, ctx) {
489
+ const [f, second, third, fourth] = args;
490
+ const r = (n) => this.render(n, ctx);
491
+ const lim = (n) => (n ? this.render(n, { ...ctx, level: ctx.level + 1 }) : null);
492
+ const guess = () => this.text('var', firstVariable(f) || 'x');
493
+ let lo = null;
494
+ let hi = null;
495
+ let v;
496
+ switch (args.length) {
497
+ case 1: v = guess(); break;
498
+ case 2: if (isVariable(second)) v = r(second); else { v = guess(); lo = lim(second); } break;
499
+ case 3: if (isVariable(second)) { v = r(second); lo = lim(third); } else { v = guess(); lo = lim(second); hi = lim(third); } break;
500
+ default: v = r(second); lo = lim(third); hi = lim(fourth);
501
+ }
502
+ const body = r(f);
503
+ const dx = box(this.el('dx', this.el('d', 'd') + v.html), v.h, v.d);
504
+ return row(this.bigSign('int', body, lo, hi, ctx), body, dx);
505
+ }
506
+
507
+ /** The ∫ sign, centred on the axis and at least as tall as the integrand, with limits beside it. */
508
+ bigSign(name, body, lo, hi, ctx) {
509
+ const small = !this.o.display || ctx.level > 0;
510
+ const half = Math.max(small ? 0.6 : 0.95, Math.max(body.h - AXIS, body.d + AXIS) + 0.12);
511
+ const sign = svg(name, `${this.p}-bigsign ${this.p}-${name}sign`,
512
+ `height:${em(2 * half)};width:${em(small ? 0.38 : 0.5)};vertical-align:${em(AXIS - half)}`);
513
+ if (!lo && !hi) return box(sign, AXIS + half, half - AXIS);
514
+ const k = SCRIPT;
515
+ const hiBox = hi || box('');
516
+ const loBox = lo || box('');
517
+ const rise = AXIS + half - k * hiBox.h;
518
+ const drop = half - AXIS - k * loBox.d;
519
+ const column = this.el('limits',
520
+ this.el('script', hiBox.html, `font-size:${em(k)}`) + this.el('script lower', loBox.html, `font-size:${em(k)}`),
521
+ `vertical-align:${em(rise)};height:${em(2 * half)}`);
522
+ return box(sign + column, AXIS + half, half - AXIS);
523
+ }
524
+
525
+ limit(args, ctx) {
526
+ const [f, second, third, fourth] = args;
527
+ const below = { ...ctx, level: ctx.level + 1 };
528
+ const r = (n) => this.render(n, below);
529
+ const arrow = () => this.rel('→');
530
+ let under;
531
+ switch (args.length) {
532
+ case 1: return row(this.word('lim'), box(this.el('fnspace', '')), this.render(f, ctx));
533
+ case 2: {
534
+ const v = firstVariable(f);
535
+ const approaches = second.type === 'number' || (second.type === 'symbol' && second.value.includes('∞'));
536
+ under = v && approaches ? row(this.text('var', v), arrow(), r(second)) : r(second);
537
+ break;
538
+ }
539
+ case 3: under = row(r(second), arrow(), r(third)); break;
540
+ default: {
541
+ const sign = firstLeaf(fourth).toLowerCase().startsWith('r') ? '+' : MINUS;
542
+ under = row(r(second), arrow(), this.script(r(third), this.wrap(fourth, ['symbol'], this.text('ord', sign)), below));
543
+ }
544
+ }
545
+ return row(this.under(this.word('lim'), under, ctx), this.render(f, ctx));
546
+ }
547
+
548
+ /** A word with something set small beneath it, lim over x → 0; the word keeps the baseline. */
549
+ under(top, below, ctx) {
550
+ const k = SCRIPT;
551
+ if (!this.o.display || ctx.level > 0) return this.script(top, null, ctx, below);
552
+ return box(this.el('under', top.html + this.el('script', below.html, `font-size:${em(k)}`)),
553
+ top.h, Math.max(top.d, LD) + 0.05 + k * (below.h + below.d));
554
+ }
555
+
556
+ /** ∑ and ∏ with the index below and the end above. */
557
+ bigOperator(name, args, ctx) {
558
+ const [f, from, to] = args;
559
+ const up = { ...ctx, level: ctx.level + 1 };
560
+ const lo = from ? this.render(from, up) : null;
561
+ const hi = to ? this.render(to, up) : null;
562
+ const body = this.render(f, ctx);
563
+ if (!this.o.display || ctx.level > 0) {
564
+ const half = 0.55;
565
+ const sign = svg(name, `${this.p}-bigsign`, `height:${em(2 * half)};width:${em(0.8)};vertical-align:${em(AXIS - half)}`);
566
+ return row(this.script(box(sign, AXIS + half, half - AXIS), hi, ctx, lo), body);
567
+ }
568
+ const half = 0.68;
569
+ const k = SCRIPT;
570
+ const sign = svg(name, `${this.p}-bigsign`, `height:${em(2 * half)};width:${em(name === 'sum' ? 1.0 : 1.05)}`);
571
+ // The first item sets the column's baseline: the upper limit's, or the sign's bottom edge.
572
+ const lift = hi ? AXIS + half + 0.08 + k * hi.d : AXIS - half;
573
+ const scaled = (b) => this.el('script', b.html, `font-size:${em(k)}`);
574
+ const column = this.el('bigop', (hi ? scaled(hi) : '') + sign + (lo ? scaled(lo) : ''), `vertical-align:${em(lift)}`);
575
+ const op = box(column,
576
+ AXIS + half + (hi ? 0.08 + k * (hi.h + hi.d) : 0),
577
+ half - AXIS + (lo ? 0.08 + k * (lo.h + lo.d) : 0));
578
+ return row(op, body);
579
+ }
580
+
581
+ /**
582
+ * A matrix: a grid of entries, centred on the maths axis, in brackets as tall as it is. Each
583
+ * row that is a list in the tree is an element of its own (display: contents), so a row can
584
+ * be pointed at like any part.
585
+ */
586
+ matrix(node, rows, ctx) {
587
+ const ROWGAP = 0.3;
588
+ const cells = rows.map((r) => r.map((c) => this.render(c, ctx)));
589
+ const heights = cells.map((r) => ({ h: Math.max(LH, ...r.map((b) => b.h)), d: Math.max(LD, ...r.map((b) => b.d)) }));
590
+ const total = heights.reduce((s, x) => s + x.h + x.d, 0) + ROWGAP * (rows.length - 1);
591
+ const lift = AXIS + total / 2 - heights[0].h; // the first row's baseline
592
+ const html = cells.map((r, i) => {
593
+ const inner = r.map((b) => this.el('matrix-cell', b.html)).join('');
594
+ const tree = node.args[i];
595
+ return tree && tree.type === 'op' && tree.op === '[]' ? this.wrap(tree, ['op', 'op-list', 'matrix-row'], box(inner)).html : inner;
596
+ }).join('');
597
+ const columns = rows[0].length;
598
+ const grid = box(this.el('matrix', html, `grid-template-columns:repeat(${columns},auto);vertical-align:${em(lift)}`), AXIS + total / 2, total / 2 - AXIS);
599
+ const [open, close] = MATRICES[node.op] ?? ['[', ']'];
600
+ return this.fence(grid, open, close);
601
+ }
602
+
603
+ /** c ? a : b → a brace with "a if c", "b otherwise". Nested ternaries add rows. */
604
+ cases(node, ctx) {
605
+ const rows = [];
606
+ let n = node;
607
+ while (n.type === 'op' && n.op === '?:') {
608
+ rows.push([this.render(n.args[1], ctx), row(this.word('if'), this.text('space', ''), this.render(n.args[0], ctx))]);
609
+ n = n.args[2];
610
+ }
611
+ // a case nobody wrote (`unchanged`: no else, no otherwise row) is not drawn
612
+ if (!(n.type === 'symbol' && n.value === 'unchanged')) rows.push([this.render(n, ctx), this.word('otherwise')]);
613
+ const ROWGAP = 0.25;
614
+ const heights = rows.map(([v, c]) => ({ h: Math.max(v.h, c.h), d: Math.max(v.d, c.d) }));
615
+ const total = heights.reduce((s, x) => s + x.h + x.d, 0) + ROWGAP * (rows.length - 1);
616
+ const lift = AXIS + total / 2 - heights[0].h; // the first row's baseline
617
+ const cells = rows.map(([v, c]) => this.el('case-value', v.html) + this.el('case-cond', c.html)).join('');
618
+ const grid = box(this.el('cases', cells, `vertical-align:${em(lift)}`), AXIS + total / 2, total / 2 - AXIS);
619
+ return this.fence(grid, '{', '');
620
+ }
621
+ }
622
+
623
+ // ---- helpers ------------------------------------------------------------------
624
+
625
+ /** Written without brackets after sin or ln: x, 2, θ, 2x, πx, |x|. Within scripts also x² and −x. */
626
+ function isSimpleArgument(node, inScript = false) {
627
+ if (node.type === 'number') return !node.value.startsWith('-') || inScript;
628
+ if (node.type === 'symbol') return true;
629
+ if (node.type !== 'op') return false;
630
+ if (node.op === 'abs' || matrixRows(node)) return !inScript; // |x| and a matrix bring their own brackets
631
+ if (inScript && (node.op === 'neg' || node.op === '^')) return isSimpleArgument(node.args[0], true) && (node.op === 'neg' || isSimpleArgument(node.args[1], true));
632
+ if (node.op === '*' && !isMinusOneTimes(node)) {
633
+ return node.args.every((c) => (c.type === 'number' && !c.value.startsWith('-')) || isShortSymbol(c) || (c.op === '*' && isSimpleArgument(c, inScript)));
634
+ }
635
+ return false;
636
+ }
637
+
638
+ const isShortSymbol = (n) => n.type === 'symbol' && (n.value.length === 1 || /^[α-ωΑ-Ω]$/.test(n.value) || Boolean(GREEK[n.value]));
639
+ const isPositiveWhole = (n) => n.type === 'number' && /^[1-9]\d*$/.test(n.value) && Number(n.value) < 10;
640
+
641
+ /** Does this expression begin with a function name, so that "sin x cos x" needs no brackets? */
642
+ function startsWithCall(node) {
643
+ while (node.type === 'op') {
644
+ if (NAMED.includes(node.op) || node.op === 'log' || CALLS[node.op] || isCustomFunction(node.op)) return true;
645
+ if (node.op === '^' || node.op === '*') { node = node.args[0]; continue; }
646
+ return false;
647
+ }
648
+ return false;
649
+ }
@@ -0,0 +1,3 @@
1
+ // '@mathzetta/inlinemath/lite-calc' — lite with MathZetta's calculator for {{…}} blocks. See types.d.ts.
2
+ export type { Input, Node, Target, LayoutOptions, DrawOptions } from './types.js';
3
+ export { toHTML, treeOf, layout, DEFAULTS, select, numberTree, calculateBlock, calculate } from './types.js';