@mathzetta/inlinemath 0.0.0-stage → 0.1.0

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