@mathzetta/inlinemath 0.1.0 → 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/README.md +64 -20
- package/dist/cas.external.js +2 -2
- package/dist/cas.js +2 -2
- 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-CvuSyhzk.js → html-BimAqn7x.js} +2 -2
- package/dist/chunks/html-CupluJSh.js +2 -0
- package/dist/chunks/lite-3Fb-OHbt.js +2 -0
- package/dist/inlinemath-cas.external.global.min.js +2 -2
- package/dist/inlinemath-cas.global.min.js +2 -2
- package/dist/inlinemath-lite-calc.global.min.js +2 -2
- package/dist/inlinemath-lite.external.global.min.js +2 -2
- package/dist/inlinemath-lite.global.min.js +2 -2
- package/dist/inlinemath.css +32 -1
- package/dist/inlinemath.external.global.min.js +2 -2
- package/dist/inlinemath.external.js +2 -2
- package/dist/inlinemath.global.min.js +2 -2
- package/dist/inlinemath.js +2 -2
- package/dist/lite-calc.js +2 -2
- package/dist/lite.external.js +2 -2
- package/dist/lite.js +2 -2
- package/dist/markdown.external.js +2 -2
- package/dist/markdown.js +2 -2
- package/inlinemath.css +32 -1
- package/package.json +3 -3
- package/src/cas.js +25 -15
- package/src/edit.js +96 -0
- package/src/glyphs.js +2 -0
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -1
- package/src/layout.js +55 -11
- package/src/markdown.d.ts +2 -2
- package/src/markdown.js +354 -39
- package/src/mathzetta.js +2 -1
- package/src/types.d.ts +77 -6
- package/src/variables.js +228 -0
- package/src/view.js +226 -6
- package/dist/chunks/calculate-BX9lJ7Ff.js +0 -2
- package/dist/chunks/draw-Cm4exbg7.js +0 -2
- package/dist/chunks/html-DWtfytjA.js +0 -2
- package/dist/chunks/lite-Bj_L6w2g.js +0 -2
package/src/markdown.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
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';
|
|
2
|
+
export type { MarkdownOptions, VariableDefinition } from './types.js';
|
|
3
|
+
export { markdownToHTML, renderMarkdownInto, extractMath, renderMarkdown, createVariables, variables, Variables } from './types.js';
|
package/src/markdown.js
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
* Maths: $…$ inline, $$…$$ (or a ```math fence) on its own, \(…\) and \[…\] too.
|
|
8
8
|
* Each is read by MathZetta (LaTeX, plain text or code all work) and drawn by
|
|
9
9
|
* Inline Math; {{…}} inside is worked out first (MathZetta's calculator, or the CAS:
|
|
10
|
-
* { blocks: casBlocks() } from '@mathzetta/inlinemath/cas'). A definition such as $r = 3$
|
|
11
|
-
*
|
|
10
|
+
* { blocks: casBlocks() } from '@mathzetta/inlinemath/cas'). A definition such as $r = 3$ is
|
|
11
|
+
* remembered, as a number, for the {{…}} after it ({ define: false } turns that off).
|
|
12
|
+
*
|
|
13
|
+
* Saved variables: $A := 2+2$ saves A (and draws A ≔ 2 + 2); $_A := 2+2$ saves it without drawing.
|
|
14
|
+
* After it, A in any maths is drawn as its expression and {{A}} is its value: $A = {{A}}$ draws
|
|
15
|
+
* 2 + 2 = 4. $A := [[2+2]]$ is a textbox that sets A, [[2+2 | x^2]] a dropdown, [[1..10]] a slider
|
|
16
|
+
* ([[0..1 step 0.1 = 0.5]]). renderMarkdownInto keeps every part of a page in one store
|
|
17
|
+
* (`variables`), so a variable saved in one part is used in the others, and changing an input
|
|
18
|
+
* redraws everything that uses it.
|
|
12
19
|
*
|
|
13
20
|
* A $ is maths only when it looks like it (Pandoc's rule): the opening $ is not
|
|
14
21
|
* followed by a space, the closing $ is not after a space or before a digit, so
|
|
@@ -21,8 +28,14 @@
|
|
|
21
28
|
* the maths is taken out first and put back into its output.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
|
-
import { toHTML } from './html.js';
|
|
31
|
+
import { toHTML, treeOf } from './html.js';
|
|
25
32
|
import { calculate } from './calculator.js';
|
|
33
|
+
import { nameOf, bare, substitute, unexpand, inputOf, EXPRESSIONS, variables as pageVariables } from './variables.js';
|
|
34
|
+
import { toPostfix } from './mathzetta.js';
|
|
35
|
+
import { numberTree, select, pathOf, nodeAt } from './select.js';
|
|
36
|
+
import { openEditor, replaceAt } from './edit.js';
|
|
37
|
+
|
|
38
|
+
export { createVariables, variables, Variables } from './variables.js';
|
|
26
39
|
|
|
27
40
|
// ---- finding the maths --------------------------------------------------------------------
|
|
28
41
|
|
|
@@ -34,7 +47,7 @@ const PLACEHOLDER = new RegExp(`${OPEN}(\\d+)${CLOSE}`, 'g');
|
|
|
34
47
|
/**
|
|
35
48
|
* The maths in a Markdown text, and the text with each piece replaced by a
|
|
36
49
|
* placeholder (private-use characters, which no Markdown renderer touches).
|
|
37
|
-
* @returns {{ text: string, math: Array<{ source, display, raw }> }}
|
|
50
|
+
* @returns {{ text: string, math: Array<{ source, display, raw, start, end }> }} (start, end: where the raw text is)
|
|
38
51
|
*/
|
|
39
52
|
export function extractMath(markdown, { delimiters = ['$$', '$', '\\[', '\\('] } = {}) {
|
|
40
53
|
const math = [];
|
|
@@ -43,7 +56,7 @@ export function extractMath(markdown, { delimiters = ['$$', '$', '\\[', '\\('] }
|
|
|
43
56
|
const s = String(markdown);
|
|
44
57
|
const atLineStart = (k) => k === 0 || s[k - 1] === '\n';
|
|
45
58
|
const keep = (source, display, raw) => {
|
|
46
|
-
math.push({ source, display, raw });
|
|
59
|
+
math.push({ source, display, raw, start: i, end: i + raw.length });
|
|
47
60
|
return placeholder(math.length - 1);
|
|
48
61
|
};
|
|
49
62
|
while (i < s.length) {
|
|
@@ -132,30 +145,194 @@ function closingDollar(s, from) {
|
|
|
132
145
|
|
|
133
146
|
const escape = (t) => String(t).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
134
147
|
|
|
135
|
-
/** A definition: r = 3, g
|
|
136
|
-
const DEFINITION = /^\s*([A-Za-z
|
|
148
|
+
/** A definition: r = 3, g = 9.81 (a single name on the left) — remembered as a number for {{…}}. */
|
|
149
|
+
const DEFINITION = /^\s*([A-Za-z\u0370-\u03ff](?:_\{?\w+\}?)?)\s*=\s*([^=<>≤≥]+)$/;
|
|
137
150
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Maths that saves a variable: `A := 2+2` (drawn), `_A := 2+2` or `_A = 2+2` (not drawn),
|
|
153
|
+
* `A := [[…]]` (an input). Else null; `{ error }` for a `$_…$` that saves nothing.
|
|
154
|
+
*/
|
|
155
|
+
function savingOf(source) {
|
|
156
|
+
let s = source.trim();
|
|
157
|
+
const hidden = s.startsWith('_');
|
|
158
|
+
if (hidden) s = s.slice(1).trimStart();
|
|
159
|
+
let at = s.indexOf(':=');
|
|
160
|
+
let width = 2;
|
|
161
|
+
if (at < 0 && hidden) {
|
|
162
|
+
at = s.search(/(?<![<>!≤≥=])=(?!=)/);
|
|
163
|
+
width = 1;
|
|
164
|
+
}
|
|
165
|
+
const nothing = hidden ? { error: 'a hidden $_…$ saves a variable: $_A := 2+2$' } : null;
|
|
166
|
+
if (at <= 0) return nothing;
|
|
167
|
+
const lhs = s.slice(0, at).trim();
|
|
168
|
+
const rhs = s.slice(at + width).trim();
|
|
169
|
+
let name = null;
|
|
170
|
+
try {
|
|
171
|
+
name = /\{\{/.test(lhs) ? null : nameOf(treeOf(lhs));
|
|
172
|
+
} catch {
|
|
173
|
+
// not a name
|
|
174
|
+
}
|
|
175
|
+
if (name === null || !rhs) return nothing;
|
|
176
|
+
return { hidden, lhs, name, rhs, input: inputOf(rhs) };
|
|
147
177
|
}
|
|
148
178
|
|
|
149
|
-
/**
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
/** `A = …` drawn as `A ≔ …`: the first relation is the definition's. */
|
|
180
|
+
function asDefinition(html) {
|
|
181
|
+
return html
|
|
182
|
+
.replace('<span class="mv-rel">=</span>', '<span class="mv-rel mv-rel-def">≔</span>')
|
|
183
|
+
.replace(/(aria-label="[^"]*?) equals /, '$1 is defined as ')
|
|
184
|
+
.replace('<mo>=</mo>', '<mo>≔</mo>');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The input a definition's [[…]] asks for, holding `text`. */
|
|
188
|
+
function controlHTML(def, text) {
|
|
189
|
+
const { input, name } = def;
|
|
190
|
+
const attrs = `class="mv-md-input mv-md-${input.kind}" data-mv-var="${escape(name)}" aria-label="${escape(name)}"`;
|
|
191
|
+
if (input.kind === 'dropdown') {
|
|
192
|
+
const options = input.options.map((o) => `<option value="${escape(o)}"${o === text ? ' selected' : ''}>${escape(o)}</option>`);
|
|
193
|
+
return `<select ${attrs}>${options.join('')}</select>`;
|
|
194
|
+
}
|
|
195
|
+
if (input.kind === 'slider') {
|
|
196
|
+
return `<input type="range" ${attrs} min="${input.min}" max="${input.max}" step="${input.step}" value="${escape(text)}">`
|
|
197
|
+
+ `<output class="mv-md-output" data-mv-out="${escape(name)}">${escape(text)}</output>`;
|
|
198
|
+
}
|
|
199
|
+
return `<input type="text" ${attrs} value="${escape(text)}" size="${Math.max(4, text.length + 1)}" spellcheck="false" autocomplete="off">`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Every piece of maths drawn, in reading order: a variable is known to the maths after it, and
|
|
204
|
+
* the variables already in `store` (the other parts of the page) to all of it.
|
|
205
|
+
*/
|
|
206
|
+
function drawAll(markdown, options, store, owner) {
|
|
207
|
+
const { text, math } = extractMath(markdown, options);
|
|
208
|
+
const defs = store ? store.definitions(owner) : new Map();
|
|
209
|
+
const textOf = (def) => (store ? store.textOf(def) : def.text);
|
|
210
|
+
const numbers = { ...(options.scope ?? {}) }; // options.scope and $r = 3$
|
|
211
|
+
const scope = {}; // what {{…}} sees: those and the saved variables' values
|
|
212
|
+
const mine = new Map();
|
|
213
|
+
const inputs = new Set();
|
|
214
|
+
const trees = []; // each piece's drawn tree, for finding what a double-click landed on
|
|
215
|
+
// the saved variables' values, for {{…}}: worked out until nothing more can be (B := 2A before A := 3)
|
|
216
|
+
const refresh = () => {
|
|
217
|
+
for (const k of Object.keys(scope)) delete scope[k];
|
|
218
|
+
Object.assign(scope, numbers);
|
|
219
|
+
for (const name of defs.keys()) delete scope[name]; // x^2 has no value: {{A}} draws A, drawn as x^2
|
|
220
|
+
let pending = [...defs.values()];
|
|
221
|
+
while (pending.length) {
|
|
222
|
+
const left = pending.filter((def) => {
|
|
223
|
+
try {
|
|
224
|
+
const r = calculate(textOf(def), scope);
|
|
225
|
+
// a number, or a matrix (or list): {{M^2}} works it out
|
|
226
|
+
if (typeof r.value === 'number' || typeof r.value === 'bigint' || Array.isArray(r.value)) {
|
|
227
|
+
scope[def.name] = r.exact ? r.text : r.value;
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// not (yet) a number
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
});
|
|
235
|
+
if (left.length === pending.length) break;
|
|
236
|
+
pending = left;
|
|
237
|
+
}
|
|
238
|
+
// and their expressions, for a CAS block: {{diff(f, x)}} differentiates what f stands for
|
|
239
|
+
const expressions = {};
|
|
240
|
+
const plain = lookupFor({});
|
|
241
|
+
for (const name of defs.keys()) {
|
|
242
|
+
const tree = plain(name);
|
|
243
|
+
if (!tree) continue;
|
|
244
|
+
try {
|
|
245
|
+
expressions[name] = toPostfix(substitute(tree, (n) => (n === name ? null : plain(n))).tree);
|
|
246
|
+
} catch {
|
|
247
|
+
// an expression that can't be written as postfix: the CAS sees the name
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
scope[EXPRESSIONS] = expressions;
|
|
251
|
+
};
|
|
252
|
+
const lookupFor = (drawOpts) => {
|
|
253
|
+
const trees = new Map();
|
|
254
|
+
return (name) => {
|
|
255
|
+
const def = defs.get(name);
|
|
256
|
+
if (!def) return null;
|
|
257
|
+
if (!trees.has(name)) {
|
|
258
|
+
let tree = null;
|
|
259
|
+
try {
|
|
260
|
+
tree = treeOf(textOf(def), drawOpts);
|
|
261
|
+
} catch {
|
|
262
|
+
// an input holding something that doesn't read: A is drawn as A
|
|
263
|
+
}
|
|
264
|
+
trees.set(name, tree);
|
|
265
|
+
}
|
|
266
|
+
return trees.get(name);
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
refresh();
|
|
271
|
+
|
|
272
|
+
const drawItem = (item, i) => {
|
|
273
|
+
const { source, display } = item;
|
|
274
|
+
const cls = display ? 'mv-md-display' : 'mv-md-math';
|
|
275
|
+
const wrap = (html, extra = '') => `<span class="${cls}${extra}" data-mv-md="${i}">${html}</span>`;
|
|
276
|
+
try {
|
|
277
|
+
const drawOpts = { ...options.math, display, blocks: options.blocks, scope };
|
|
278
|
+
const save = savingOf(source);
|
|
279
|
+
if (save && save.error) throw new Error(save.error);
|
|
280
|
+
if (save) {
|
|
281
|
+
const { name, input, hidden } = save;
|
|
282
|
+
const def = input
|
|
283
|
+
? { name, text: input.default, input, key: `input:${JSON.stringify(input)}` }
|
|
284
|
+
: { name, text: save.rhs, input: null, key: `expr:${save.rhs}`, start: item.start, end: item.end };
|
|
285
|
+
let html = '';
|
|
286
|
+
if (input) {
|
|
287
|
+
const control = controlHTML(def, textOf(def));
|
|
288
|
+
html = hidden ? control : `${toHTML(save.lhs, drawOpts)}<span class="mv-md-assign" aria-hidden="true">≔</span>${control}`;
|
|
289
|
+
inputs.add(i);
|
|
290
|
+
} else {
|
|
291
|
+
const rhs = substitute(treeOf(save.rhs, drawOpts), lookupFor(drawOpts)).tree;
|
|
292
|
+
if (!hidden) {
|
|
293
|
+
const tree = numberTree({ type: 'op', op: '=', args: [bare(treeOf(save.lhs)), rhs.id === undefined ? rhs : bare(rhs)] });
|
|
294
|
+
trees[i] = tree;
|
|
295
|
+
html = asDefinition(toHTML(tree, drawOpts));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
defs.delete(name);
|
|
299
|
+
defs.set(name, def);
|
|
300
|
+
mine.set(name, def);
|
|
301
|
+
refresh();
|
|
302
|
+
if (hidden && !input) return '';
|
|
303
|
+
return wrap(html, input ? ' mv-md-input-def' : '');
|
|
304
|
+
}
|
|
305
|
+
const { tree, changed } = substitute(treeOf(source, drawOpts), lookupFor(drawOpts));
|
|
306
|
+
if (changed) numberTree(tree);
|
|
307
|
+
trees[i] = tree;
|
|
308
|
+
const html = toHTML(tree, drawOpts);
|
|
309
|
+
if (!changed && options.define !== false && define(source, numbers, defs)) refresh();
|
|
310
|
+
return wrap(html);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return `<span class="mv-md-error" data-mv-md="${i}" title="${escape(error.message)}"><code>${escape(item.raw)}</code></span>`;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const drawn = math.map(drawItem);
|
|
316
|
+
return { text, math, drawn, defs: mine, inputs, trees };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** $r = 3$ (or $A = \\begin{pmatrix}…$): remember r (exactly, as the calculator's text) for later {{…}} — unless r is a saved variable. */
|
|
320
|
+
function define(source, numbers, saved) {
|
|
321
|
+
const m = DEFINITION.exec(source);
|
|
322
|
+
if (!m || /\{\{/.test(m[2])) return false;
|
|
323
|
+
const name = m[1].replace(/[{}]/g, '');
|
|
324
|
+
if (saved.has(name)) return false;
|
|
325
|
+
try {
|
|
326
|
+
const r = calculate(m[2], numbers);
|
|
327
|
+
// a number, or a matrix (or list), as for saved variables
|
|
328
|
+
if (typeof r.value === 'number' || typeof r.value === 'bigint' || Array.isArray(r.value)) {
|
|
329
|
+
numbers[name] = r.exact ? r.text : r.value;
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// not a number (x = y + 1): nothing to remember
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
159
336
|
}
|
|
160
337
|
|
|
161
338
|
// ---- the Markdown renderer ----------------------------------------------------------------
|
|
@@ -329,6 +506,18 @@ export function renderMarkdown(markdown, options = {}, sourceOf = (t) => t) {
|
|
|
329
506
|
|
|
330
507
|
// ---- together ---------------------------------------------------------------------------
|
|
331
508
|
|
|
509
|
+
/** The Markdown around the drawn maths. */
|
|
510
|
+
function assemble({ text, math, drawn }, options) {
|
|
511
|
+
const sourceOf = (t) => t.replace(PLACEHOLDER, (_, n) => math[Number(n)].raw);
|
|
512
|
+
const html = options.render ? options.render(text) : renderMarkdown(text, options, sourceOf);
|
|
513
|
+
return html
|
|
514
|
+
// display maths alone in a paragraph is a block of its own
|
|
515
|
+
.replace(new RegExp(`<p>\\s*${OPEN}(\\d+)${CLOSE}\\s*</p>`, 'g'), (whole, n) => (math[Number(n)].display && drawn[Number(n)] ? `<div class="mv-md-block">${drawn[Number(n)]}</div>` : whole))
|
|
516
|
+
.replace(PLACEHOLDER, (_, n) => drawn[Number(n)])
|
|
517
|
+
// a paragraph that only saved variables ($_A := 2+2$) leaves nothing behind
|
|
518
|
+
.replace(/<p>\s*<\/p>\n?/g, '');
|
|
519
|
+
}
|
|
520
|
+
|
|
332
521
|
/**
|
|
333
522
|
* Markdown with maths → HTML.
|
|
334
523
|
* @param {string} markdown
|
|
@@ -337,25 +526,151 @@ export function renderMarkdown(markdown, options = {}, sourceOf = (t) => t) {
|
|
|
337
526
|
* @param {Function} [options.blocks] the {{…}} handler (default the calculator; casBlocks() for the CAS)
|
|
338
527
|
* @param {object} [options.scope] values for {{…}}: { r: 3 }
|
|
339
528
|
* @param {boolean} [options.define] remember $r = 3$ for later {{…}} (default true)
|
|
529
|
+
* @param {object} [options.variables] a store (createVariables()) whose variables the maths may use
|
|
340
530
|
* @param {boolean} [options.html] let raw HTML through (default false: it is shown as text)
|
|
341
531
|
* @param {(markdown: string) => string} [options.render] another Markdown renderer
|
|
342
532
|
* @param {string[]} [options.delimiters] which of '$$', '$', '\\[', '\\(' mark maths (default all)
|
|
343
533
|
*/
|
|
344
534
|
export function markdownToHTML(markdown, options = {}) {
|
|
345
|
-
|
|
346
|
-
const scope = { ...(options.scope ?? {}) };
|
|
347
|
-
// drawn in reading order, so a definition is known to the maths after it
|
|
348
|
-
const drawn = math.map((item) => drawMath(item, options, scope));
|
|
349
|
-
const sourceOf = (t) => t.replace(PLACEHOLDER, (_, n) => math[Number(n)].raw);
|
|
350
|
-
const html = options.render ? options.render(text) : renderMarkdown(text, options, sourceOf);
|
|
351
|
-
return html
|
|
352
|
-
// display maths alone in a paragraph is a block of its own
|
|
353
|
-
.replace(new RegExp(`<p>\\s*${OPEN}(\\d+)${CLOSE}\\s*</p>`, 'g'), (whole, n) => (math[Number(n)].display ? `<div class="mv-md-block">${drawn[Number(n)]}</div>` : whole))
|
|
354
|
-
.replace(PLACEHOLDER, (_, n) => drawn[Number(n)]);
|
|
535
|
+
return assemble(drawAll(markdown, options, options.variables || null), options);
|
|
355
536
|
}
|
|
356
537
|
|
|
357
|
-
|
|
538
|
+
const bindings = new WeakMap();
|
|
539
|
+
const live = new Set(); // every bound element's binding, to notice the ones taken off the page
|
|
540
|
+
|
|
541
|
+
/** Parts taken off the page take their variables with them. */
|
|
542
|
+
function sweep() {
|
|
543
|
+
for (const b of live) if (b.connected() && !b.element.isConnected) b.stop(true);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Draw Markdown with maths into an element, live: its variables join the page's store
|
|
548
|
+
* (options.variables, default `variables`; false for none), its inputs set them, and its maths is
|
|
549
|
+
* drawn again whenever a variable it can see changes. Drawing into the element again replaces it all.
|
|
550
|
+
* Double-clicking a part drawn from a textbox variable edits it ({ editable: { toText } }, toText
|
|
551
|
+
* from 'mathzetta/text'); any other part fires 'inlinemath:select' with where its maths is in the
|
|
552
|
+
* Markdown ({ index, start, end, raw }).
|
|
553
|
+
*/
|
|
358
554
|
export function renderMarkdownInto(element, markdown, options = {}) {
|
|
359
|
-
|
|
360
|
-
|
|
555
|
+
const store = options.variables === undefined ? pageVariables : options.variables || null;
|
|
556
|
+
sweep();
|
|
557
|
+
const old = bindings.get(element);
|
|
558
|
+
if (old) old.stop(old.store !== store);
|
|
559
|
+
let result = drawAll(markdown, options, store, element);
|
|
560
|
+
element.innerHTML = assemble(result, options);
|
|
561
|
+
if (!store) {
|
|
562
|
+
bindings.delete(element);
|
|
563
|
+
return element;
|
|
564
|
+
}
|
|
565
|
+
store.own(element, result.defs);
|
|
566
|
+
|
|
567
|
+
let connected = element.isConnected;
|
|
568
|
+
const redraw = () => {
|
|
569
|
+
const next = drawAll(markdown, options, store, element);
|
|
570
|
+
next.drawn.forEach((html, i) => {
|
|
571
|
+
if (html === result.drawn[i] || next.inputs.has(i)) return;
|
|
572
|
+
const at = element.querySelector(`[data-mv-md="${i}"]`);
|
|
573
|
+
if (at) at.outerHTML = html;
|
|
574
|
+
});
|
|
575
|
+
// inputs for the same variable agree; a slider shows its value
|
|
576
|
+
for (const el of element.querySelectorAll('[data-mv-var], [data-mv-out]')) {
|
|
577
|
+
const def = next.defs.get(el.dataset.mvVar ?? el.dataset.mvOut);
|
|
578
|
+
if (!def) continue;
|
|
579
|
+
const text = store.textOf(def);
|
|
580
|
+
if (el.dataset.mvOut !== undefined) el.textContent = text;
|
|
581
|
+
else if (el.value !== text && el !== el.ownerDocument?.activeElement) el.value = text;
|
|
582
|
+
}
|
|
583
|
+
result = next;
|
|
584
|
+
};
|
|
585
|
+
const onInput = (event) => {
|
|
586
|
+
const el = event.target;
|
|
587
|
+
const name = el && el.dataset && el.dataset.mvVar;
|
|
588
|
+
if (name === undefined || !element.contains(el)) return;
|
|
589
|
+
if (el.type === 'text') {
|
|
590
|
+
try {
|
|
591
|
+
treeOf(el.value, { blocks: options.blocks, scope: {} });
|
|
592
|
+
el.removeAttribute('aria-invalid');
|
|
593
|
+
} catch {
|
|
594
|
+
el.setAttribute('aria-invalid', 'true'); // keeps what it held last
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
store.set(name, el.value);
|
|
599
|
+
};
|
|
600
|
+
// double-click a part: a part that came from a textbox variable is edited there (in place,
|
|
601
|
+
// with options.editable = { toText }; else the textbox is focused); anything else tells the
|
|
602
|
+
// page where that maths is in the Markdown, as an 'inlinemath:select' event
|
|
603
|
+
const onDoubleClick = (event) => {
|
|
604
|
+
const part = event.target.closest?.('[data-node]');
|
|
605
|
+
const piece = part && part.closest('[data-mv-md]');
|
|
606
|
+
if (!piece || !element.contains(piece)) return;
|
|
607
|
+
const index = Number(piece.dataset.mvMd);
|
|
608
|
+
const tree = result.trees[index];
|
|
609
|
+
if (!tree) return;
|
|
610
|
+
const id = Number(part.dataset.node);
|
|
611
|
+
const path = pathOf(tree, id) || [];
|
|
612
|
+
// the innermost variable the part sits in
|
|
613
|
+
let at = null;
|
|
614
|
+
for (let k = path.length; k >= 0 && !at; k--) {
|
|
615
|
+
const n = nodeAt(tree, path.slice(0, k));
|
|
616
|
+
if (n && n.from !== undefined) at = { node: n, depth: k };
|
|
617
|
+
}
|
|
618
|
+
const def = at && store.definitions().get(at.node.from);
|
|
619
|
+
if (def && def.input && def.input.kind === 'textbox') {
|
|
620
|
+
event.preventDefault();
|
|
621
|
+
const name = def.name;
|
|
622
|
+
const box = element.ownerDocument.querySelector(`[data-mv-var="${CSS.escape(name)}"]`);
|
|
623
|
+
const toText = options.editable && options.editable.toText;
|
|
624
|
+
if (!toText) {
|
|
625
|
+
if (box) { box.focus(); box.select(); }
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const named = (n) => treeOf(n);
|
|
629
|
+
const node = nodeAt(tree, path);
|
|
630
|
+
openEditor(part, toText(unexpand(node, named)), {
|
|
631
|
+
label: `Edit ${name}`,
|
|
632
|
+
parse: (text) => treeOf(text, { blocks: options.blocks }),
|
|
633
|
+
commit: (typed) => {
|
|
634
|
+
const whole = replaceAt(unexpand(at.node, named), path.slice(at.depth), typed);
|
|
635
|
+
store.set(name, toText(whole));
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// a saved variable defined here: point at its definition rather than where it is used
|
|
641
|
+
const here = def && result.defs.get(def.name);
|
|
642
|
+
const item = here && here.key === def.key && here.start !== undefined
|
|
643
|
+
? { ...result.math[index], start: here.start, end: here.end, raw: markdown.slice(here.start, here.end) }
|
|
644
|
+
: result.math[index];
|
|
645
|
+
element.dispatchEvent(new CustomEvent('inlinemath:select', {
|
|
646
|
+
bubbles: true,
|
|
647
|
+
detail: { index, start: item.start, end: item.end, raw: item.raw, source: item.source, id, path },
|
|
648
|
+
}));
|
|
649
|
+
};
|
|
650
|
+
element.addEventListener('input', onInput);
|
|
651
|
+
element.addEventListener('change', onInput);
|
|
652
|
+
element.addEventListener('dblclick', onDoubleClick);
|
|
653
|
+
const unsubscribe = store.subscribe((from) => {
|
|
654
|
+
if (from === element) return;
|
|
655
|
+
if (!element.isConnected) {
|
|
656
|
+
if (connected) stop(true); // taken off the page: its variables go with it
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
connected = true;
|
|
660
|
+
sweep();
|
|
661
|
+
redraw();
|
|
662
|
+
});
|
|
663
|
+
function stop(drop) {
|
|
664
|
+
unsubscribe();
|
|
665
|
+
element.removeEventListener('input', onInput);
|
|
666
|
+
element.removeEventListener('change', onInput);
|
|
667
|
+
element.removeEventListener('dblclick', onDoubleClick);
|
|
668
|
+
live.delete(binding);
|
|
669
|
+
bindings.delete(element);
|
|
670
|
+
if (drop) store.own(element, null);
|
|
671
|
+
}
|
|
672
|
+
const binding = { element, store, stop, connected: () => connected };
|
|
673
|
+
live.add(binding);
|
|
674
|
+
bindings.set(element, binding);
|
|
675
|
+
return element;
|
|
361
676
|
}
|
package/src/mathzetta.js
CHANGED
|
@@ -13,5 +13,6 @@ export {
|
|
|
13
13
|
operator, precedenceOf, isCustomFunction, VARIADIC,
|
|
14
14
|
GREEK, needsBrackets, unaryNeedsBrackets, isMinusOneTimes, isImplicitProduct, isVariable,
|
|
15
15
|
firstLeaf, customName, derivativeOrder, bracketedSides, bracketOp,
|
|
16
|
+
MATRICES, matrixRows,
|
|
16
17
|
} from 'mathzetta';
|
|
17
|
-
export { toSpeech, toSpeechSegments } from 'mathzetta/speech';
|
|
18
|
+
export { toSpeech, toSpeechSegments, toAudioSpeech } from 'mathzetta/speech';
|
package/src/types.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ export interface LayoutOptions {
|
|
|
30
30
|
cases?: boolean;
|
|
31
31
|
/** The accessible name: true = the spoken form, a string, a function of the tree, or false for none. */
|
|
32
32
|
label?: boolean | string | ((tree: Node) => string);
|
|
33
|
+
/** MathZetta's toMathML (from 'mathzetta/mathml'): also write the expression as visually hidden MathML, which
|
|
34
|
+
* screen readers can step through and show in math braille; it takes the place of the label. Off by default. */
|
|
35
|
+
mathml?: ((input: Node) => string) | null;
|
|
33
36
|
/** Extra attributes for a node's element. */
|
|
34
37
|
attributes?: ((node: Node) => Record<string, string>) | null;
|
|
35
38
|
/** Inline style for a node's element. */
|
|
@@ -47,6 +50,13 @@ export interface DrawOptions extends LayoutOptions {
|
|
|
47
50
|
blocks?: BlockHandler;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/** Options for a live view: everything toHTML takes, plus how steps reach a screen reader. */
|
|
54
|
+
export interface ViewOptions extends DrawOptions {
|
|
55
|
+
/** Read each new step (every set() after the first) to a screen reader: true places a polite live
|
|
56
|
+
* region after the element; or pass your own live-region element. Off by default. */
|
|
57
|
+
announce?: boolean | Element;
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
export const DEFAULTS: Required<LayoutOptions>;
|
|
51
61
|
|
|
52
62
|
/** Anything MathZetta reads, as HTML (a string; works in Node). Needs inlinemath.css on the page. */
|
|
@@ -71,11 +81,16 @@ export interface ClickInfo { node: Node; id: number; path: number[] | null; elem
|
|
|
71
81
|
|
|
72
82
|
/** A live drawing: highlight, colour, listen, step with animation. */
|
|
73
83
|
export class View {
|
|
74
|
-
constructor(element: Element, input: Input, options?:
|
|
84
|
+
constructor(element: Element, input: Input, options?: ViewOptions);
|
|
75
85
|
readonly element: Element;
|
|
76
86
|
readonly tree: Node;
|
|
77
|
-
/** Show a new expression; with animate, matching parts glide over and new ones fade in.
|
|
78
|
-
|
|
87
|
+
/** Show a new expression; with animate, matching parts glide over and new ones fade in. `true` is instant
|
|
88
|
+
* when the reader's system asks for reduced motion; `'always'` animates regardless. */
|
|
89
|
+
set(input: Input, options?: { animate?: boolean | 'always'; duration?: number; easing?: string }): this;
|
|
90
|
+
/** The expression as a listener hears it (its accessible name, else MathZetta's audio speech). */
|
|
91
|
+
spoken(): string;
|
|
92
|
+
/** Say something through the live region ({ announce } must be set); steps are announced this way. */
|
|
93
|
+
announce(text: string): this;
|
|
79
94
|
nodes(target: Target): Node[];
|
|
80
95
|
elements(target: Target): Element[];
|
|
81
96
|
elementOf(node: Node): Element | null;
|
|
@@ -83,13 +98,30 @@ export class View {
|
|
|
83
98
|
unhighlight(target?: Target | null, className?: string): this;
|
|
84
99
|
color(target: Target, colour: string): this;
|
|
85
100
|
/** Listen for an event on a part; returns a function that stops listening. */
|
|
101
|
+
/** Listen for an event on the parts. A 'click' listener also makes the drawing work from the keyboard:
|
|
102
|
+
* one tab stop, arrow keys walk the parts (each spoken), Enter or Space calls the listener for the
|
|
103
|
+
* current part — `event` is then the KeyboardEvent. */
|
|
86
104
|
on(type: string, fn: (info: ClickInfo) => void): () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Let the reader edit a part at a time: double-click it (or F2 on the keyboard's part) and a
|
|
107
|
+
* text box opens over it; Enter puts the new text in its place, Escape leaves it.
|
|
108
|
+
* toText is MathZetta's (import { toText } from 'mathzetta/text'; InlineMath.toText in the <script> build).
|
|
109
|
+
*/
|
|
110
|
+
editable(how: { toText: (input: Input) => string; onEdit?: (change: { tree: Node; text: string; path: number[]; part: string }) => void }): this;
|
|
111
|
+
/** Open the editing box over a part (editable() first). */
|
|
112
|
+
edit(target?: unknown, options?: { returnFocus?: boolean }): HTMLInputElement | null;
|
|
113
|
+
/** The part the arrow keys are on (null: the whole expression). */
|
|
114
|
+
readonly active: Node | null;
|
|
115
|
+
/** Put the keyboard on a part (null: the whole expression) and say it. */
|
|
116
|
+
focusPart(node: Node | null): this;
|
|
87
117
|
/** The expression read aloud in pieces, each tagged with the node it describes. */
|
|
88
118
|
segments(options?: SpeechOptions): { text: string; id?: number }[];
|
|
89
119
|
showSegment(segment: { id?: number } | null, className?: string): this;
|
|
90
120
|
}
|
|
91
121
|
/** Draw into an element and return the live view. */
|
|
92
|
-
export function render(element: Element, input: Input, options?:
|
|
122
|
+
export function render(element: Element, input: Input, options?: ViewOptions): View;
|
|
123
|
+
/** Should a step animate: true honours prefers-reduced-motion, 'always' does not, false never. */
|
|
124
|
+
export function shouldAnimate(animate?: boolean | 'always'): boolean;
|
|
93
125
|
|
|
94
126
|
// ---- inlinemath/markdown --------------------------------------------------------------------
|
|
95
127
|
|
|
@@ -108,10 +140,48 @@ export interface MarkdownOptions {
|
|
|
108
140
|
render?: (markdown: string) => string;
|
|
109
141
|
/** Which of '$$', '$', '\\[', '\\(' mark maths (default all). */
|
|
110
142
|
delimiters?: string[];
|
|
143
|
+
/**
|
|
144
|
+
* The saved variables ($A := 2+2$) the maths may use. markdownToHTML: none by default;
|
|
145
|
+
* renderMarkdownInto: the page's `variables` by default, false for none.
|
|
146
|
+
*/
|
|
147
|
+
variables?: Variables | false | null;
|
|
148
|
+
/**
|
|
149
|
+
* renderMarkdownInto: double-clicking a part drawn from a textbox variable edits it in place
|
|
150
|
+
* (toText from 'mathzetta/text'); without it the textbox is focused. Any other part fires
|
|
151
|
+
* 'inlinemath:select' (detail { index, start, end, raw, source, id, path }) on the element.
|
|
152
|
+
*/
|
|
153
|
+
editable?: { toText: (input: Input) => string };
|
|
154
|
+
}
|
|
155
|
+
/** What a saved variable is: its expression, or the input that sets it. */
|
|
156
|
+
export interface VariableDefinition {
|
|
157
|
+
name: string;
|
|
158
|
+
text: string;
|
|
159
|
+
input: null | { kind: 'textbox'; default: string } | { kind: 'dropdown'; options: string[]; default: string } | { kind: 'slider'; min: number; max: number; step: number; default: string };
|
|
160
|
+
key: string;
|
|
161
|
+
}
|
|
162
|
+
/** A page's saved variables: every part drawn with renderMarkdownInto sees the others'. */
|
|
163
|
+
export class Variables {
|
|
164
|
+
/** A variable's expression now (what its input holds, for an input), or undefined. */
|
|
165
|
+
get(name: string): string | undefined;
|
|
166
|
+
/** Set a variable: what its input holds, or (with no input) a definition for the whole page. */
|
|
167
|
+
set(name: string, text: string | number): this;
|
|
168
|
+
/** Forget a variable set with set(), or everything. */
|
|
169
|
+
clear(name?: string): this;
|
|
170
|
+
/** The saved names. */
|
|
171
|
+
names(): string[];
|
|
172
|
+
/** Called whenever a variable changes; returns a function that stops it. */
|
|
173
|
+
subscribe(fn: (owner: unknown, name?: string) => void): () => void;
|
|
174
|
+
definitions(except?: unknown): Map<string, VariableDefinition>;
|
|
175
|
+
textOf(def: VariableDefinition): string;
|
|
176
|
+
own(owner: unknown, defs: Map<string, VariableDefinition> | null): void;
|
|
111
177
|
}
|
|
178
|
+
/** A store of its own, for parts of a page that shouldn't share variables. */
|
|
179
|
+
export function createVariables(): Variables;
|
|
180
|
+
/** The page's store, renderMarkdownInto's default. */
|
|
181
|
+
export const variables: Variables;
|
|
112
182
|
export function markdownToHTML(markdown: string, options?: MarkdownOptions): string;
|
|
113
183
|
export function renderMarkdownInto<E extends Element>(element: E, markdown: string, options?: MarkdownOptions): E;
|
|
114
|
-
export function extractMath(markdown: string, options?: { delimiters?: string[] }): { text: string; math: Array<{ source: string; display: boolean; raw: string }> };
|
|
184
|
+
export function extractMath(markdown: string, options?: { delimiters?: string[] }): { text: string; math: Array<{ source: string; display: boolean; raw: string; start: number; end: number }> };
|
|
115
185
|
export function renderMarkdown(markdown: string, options?: MarkdownOptions): string;
|
|
116
186
|
|
|
117
187
|
// ---- inlinemath/cas (needs @mathzetta/cas) --------------------------------------------------------
|
|
@@ -122,7 +192,8 @@ export interface CASStep { id: string; name: string; from: string; to: string; p
|
|
|
122
192
|
export type CAS = Record<string, any>;
|
|
123
193
|
export function mathCAS(options?: { rules?: unknown[]; names?: string[]; complex?: boolean; [option: string]: unknown }): CAS;
|
|
124
194
|
/** A `{{…}}` handler: toHTML(input, { blocks: casBlocks() }). `last` holds the latest CAS result. */
|
|
125
|
-
|
|
195
|
+
/** steps: how much working each call keeps (true: every step, 'brief': the ones that carry the idea, false: none). */
|
|
196
|
+
export function casBlocks(options?: { cas?: CAS; steps?: boolean | 'brief' | Record<string, unknown> }): BlockHandler & { last?: { steps?: CASStep[]; text?: string; [k: string]: unknown } };
|
|
126
197
|
/** Steps as an ordered list, each rewrite highlighted, with its explanation. */
|
|
127
198
|
export function stepsHTML(steps: CASStep[], options?: DrawOptions): string;
|
|
128
199
|
/** A CAS node → MathZetta postfix tokens. */
|