@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.
- package/LICENSE +21 -0
- package/README.md +326 -2
- package/dist/cas.external.js +2 -0
- package/dist/cas.js +2 -0
- package/dist/chunks/calculate-Cz87Lt0N.js +2 -0
- package/dist/chunks/draw-Bev17BJJ.js +2 -0
- package/dist/chunks/edit-C65xXzo8.js +2 -0
- package/dist/chunks/edit-M68aRA5o.js +2 -0
- package/dist/chunks/html-BimAqn7x.js +2 -0
- package/dist/chunks/html-CupluJSh.js +2 -0
- package/dist/chunks/lite-3Fb-OHbt.js +2 -0
- package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/dist/fonts/fonts.css +17 -0
- package/dist/fonts/lmroman12-italic.woff2 +0 -0
- package/dist/fonts/lmroman12-regular.woff2 +0 -0
- package/dist/inlinemath-cas.external.global.min.js +2 -0
- package/dist/inlinemath-cas.global.min.js +2 -0
- package/dist/inlinemath-lite-calc.global.min.js +2 -0
- package/dist/inlinemath-lite.external.global.min.js +2 -0
- package/dist/inlinemath-lite.global.min.js +2 -0
- package/dist/inlinemath.css +158 -0
- package/dist/inlinemath.external.global.min.js +2 -0
- package/dist/inlinemath.external.js +2 -0
- package/dist/inlinemath.global.min.js +2 -0
- package/dist/inlinemath.js +2 -0
- package/dist/lite-calc.js +2 -0
- package/dist/lite.external.js +2 -0
- package/dist/lite.js +2 -0
- package/dist/markdown.external.js +2 -0
- package/dist/markdown.js +2 -0
- package/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/fonts/fonts.css +17 -0
- package/fonts/lmroman12-italic.woff2 +0 -0
- package/fonts/lmroman12-regular.woff2 +0 -0
- package/inlinemath.css +158 -0
- package/package.json +95 -4
- package/src/calculator.js +6 -0
- package/src/cas.d.ts +3 -0
- package/src/cas.js +149 -0
- package/src/draw.js +72 -0
- package/src/edit.js +96 -0
- package/src/glyphs.js +40 -0
- package/src/html.js +32 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +17 -0
- package/src/layout.js +649 -0
- package/src/lite-calc.d.ts +3 -0
- package/src/lite-calc.js +15 -0
- package/src/lite.d.ts +3 -0
- package/src/lite.js +18 -0
- package/src/markdown.d.ts +3 -0
- package/src/markdown.js +676 -0
- package/src/mathzetta.js +18 -0
- package/src/select.js +108 -0
- package/src/types.d.ts +204 -0
- package/src/variables.js +228 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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';
|