@mathzetta/inlinemath 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -2
  3. package/dist/cas.external.js +2 -0
  4. package/dist/cas.js +2 -0
  5. package/dist/chunks/calculate-Cz87Lt0N.js +2 -0
  6. package/dist/chunks/draw-Bev17BJJ.js +2 -0
  7. package/dist/chunks/edit-C65xXzo8.js +2 -0
  8. package/dist/chunks/edit-M68aRA5o.js +2 -0
  9. package/dist/chunks/html-BimAqn7x.js +2 -0
  10. package/dist/chunks/html-CupluJSh.js +2 -0
  11. package/dist/chunks/lite-3Fb-OHbt.js +2 -0
  12. package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
  13. package/dist/fonts/fonts.css +17 -0
  14. package/dist/fonts/lmroman12-italic.woff2 +0 -0
  15. package/dist/fonts/lmroman12-regular.woff2 +0 -0
  16. package/dist/inlinemath-cas.external.global.min.js +2 -0
  17. package/dist/inlinemath-cas.global.min.js +2 -0
  18. package/dist/inlinemath-lite-calc.global.min.js +2 -0
  19. package/dist/inlinemath-lite.external.global.min.js +2 -0
  20. package/dist/inlinemath-lite.global.min.js +2 -0
  21. package/dist/inlinemath.css +158 -0
  22. package/dist/inlinemath.external.global.min.js +2 -0
  23. package/dist/inlinemath.external.js +2 -0
  24. package/dist/inlinemath.global.min.js +2 -0
  25. package/dist/inlinemath.js +2 -0
  26. package/dist/lite-calc.js +2 -0
  27. package/dist/lite.external.js +2 -0
  28. package/dist/lite.js +2 -0
  29. package/dist/markdown.external.js +2 -0
  30. package/dist/markdown.js +2 -0
  31. package/fonts/GUST-FONT-LICENSE.txt +30 -0
  32. package/fonts/fonts.css +17 -0
  33. package/fonts/lmroman12-italic.woff2 +0 -0
  34. package/fonts/lmroman12-regular.woff2 +0 -0
  35. package/inlinemath.css +158 -0
  36. package/package.json +95 -4
  37. package/src/calculator.js +6 -0
  38. package/src/cas.d.ts +3 -0
  39. package/src/cas.js +149 -0
  40. package/src/draw.js +72 -0
  41. package/src/edit.js +96 -0
  42. package/src/glyphs.js +40 -0
  43. package/src/html.js +32 -0
  44. package/src/index.d.ts +3 -0
  45. package/src/index.js +17 -0
  46. package/src/layout.js +649 -0
  47. package/src/lite-calc.d.ts +3 -0
  48. package/src/lite-calc.js +15 -0
  49. package/src/lite.d.ts +3 -0
  50. package/src/lite.js +18 -0
  51. package/src/markdown.d.ts +3 -0
  52. package/src/markdown.js +676 -0
  53. package/src/mathzetta.js +18 -0
  54. package/src/select.js +108 -0
  55. package/src/types.d.ts +204 -0
  56. package/src/variables.js +228 -0
  57. package/src/view.js +382 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Where Inline Math reaches into MathZetta: this file (the core and speech) and calculator.js (the
3
+ * calculator, for `{{…}}` blocks). Everything else imports from these two, so a change in
4
+ * MathZetta's API is a small change — and the lite build, which leaves the calculator out, is
5
+ * everything that doesn't import calculator.js.
6
+ *
7
+ * MathZetta is a peer dependency; a page without a bundler maps the names with an import map (as
8
+ * the README shows).
9
+ */
10
+
11
+ export {
12
+ parse, parseTree, fromPostfix, toPostfix, asTree, walk, firstVariable,
13
+ operator, precedenceOf, isCustomFunction, VARIADIC,
14
+ GREEK, needsBrackets, unaryNeedsBrackets, isMinusOneTimes, isImplicitProduct, isVariable,
15
+ firstLeaf, customName, derivativeOrder, bracketedSides, bracketOp,
16
+ MATRICES, matrixRows,
17
+ } from 'mathzetta';
18
+ export { toSpeech, toSpeechSegments, toAudioSpeech } from 'mathzetta/speech';
package/src/select.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Picking out parts of an expression.
3
+ *
4
+ * A target can be written several ways; select() turns any of them into
5
+ * the matching tree nodes:
6
+ *
7
+ * 4 the node with id 4 (MathZetta's postfix index)
8
+ * 'x^2' every part that is x² — written like any other input
9
+ * { path: [0, 1] } child 1 of child 0 of the root — the same paths @mathzetta/cas reports
10
+ * (node) => bool any test
11
+ * [ …targets ] all of them
12
+ *
13
+ * Matching by expression compares postfix, so 'x^2 + 1' finds x² + 1 only
14
+ * where it is a part of the tree on its own: in 3 + x² + 1, which reads
15
+ * (3 + x²) + 1, it is not.
16
+ */
17
+
18
+ import { parseTree, toPostfix, walk, operator, VARIADIC } from './mathzetta.js';
19
+
20
+ /** A subtree's postfix as one string: two parts look the same exactly when these match. */
21
+ export const signature = (node) => toPostfix(node).join(' ');
22
+
23
+ /**
24
+ * @param {object} tree
25
+ * @param {*} target see above
26
+ * @returns {object[]} nodes, in tree order, without repeats
27
+ */
28
+ export function select(tree, target) {
29
+ const found = new Set();
30
+ collect(tree, target, found);
31
+ const out = [];
32
+ walk(tree, (n) => { if (found.has(n)) out.push(n); });
33
+ return out;
34
+ }
35
+
36
+ function collect(tree, target, found) {
37
+ if (target === null || target === undefined) return;
38
+ if (Array.isArray(target)) {
39
+ target.forEach((t) => collect(tree, t, found));
40
+ return;
41
+ }
42
+ if (typeof target === 'number') {
43
+ walk(tree, (n) => { if (n.id === target) found.add(n); });
44
+ return;
45
+ }
46
+ if (typeof target === 'function') {
47
+ walk(tree, (n, parent) => { if (target(n, parent)) found.add(n); });
48
+ return;
49
+ }
50
+ if (typeof target === 'string') {
51
+ const sig = signature(parseTree(target));
52
+ walk(tree, (n) => { if (signature(n) === sig) found.add(n); });
53
+ return;
54
+ }
55
+ if (target.path) {
56
+ const n = nodeAt(tree, target.path);
57
+ if (n) found.add(n);
58
+ return;
59
+ }
60
+ if (target.type) {
61
+ const sig = signature(target);
62
+ walk(tree, (n) => { if (n === target || signature(n) === sig) found.add(n); });
63
+ }
64
+ }
65
+
66
+ /** The node at a path of child indices, or null. */
67
+ export function nodeAt(tree, path) {
68
+ let n = tree;
69
+ for (const i of path) {
70
+ if (n.type !== 'op' || !n.args[i]) return null;
71
+ n = n.args[i];
72
+ }
73
+ return n;
74
+ }
75
+
76
+ /** The path of child indices to the node with this id, or null. */
77
+ export function pathOf(tree, id) {
78
+ const path = [];
79
+ const visit = (n) => {
80
+ if (n.id === id) return true;
81
+ if (n.type !== 'op') return false;
82
+ for (let i = 0; i < n.args.length; i++) {
83
+ path.push(i);
84
+ if (visit(n.args[i])) return true;
85
+ path.pop();
86
+ }
87
+ return false;
88
+ };
89
+ return visit(tree) ? path : null;
90
+ }
91
+
92
+ /**
93
+ * Give every node of a hand-built tree the id MathZetta would: its index
94
+ * in the postfix (variadic operators count their argument-count token).
95
+ * Changes the tree in place and returns it.
96
+ */
97
+ export function numberTree(tree) {
98
+ let i = 0;
99
+ const visit = (n) => {
100
+ if (n.type === 'op') {
101
+ n.args.forEach(visit);
102
+ if (operator(n.op)?.arity === VARIADIC) i++;
103
+ }
104
+ n.id = i++;
105
+ };
106
+ visit(tree);
107
+ return tree;
108
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,204 @@
1
+ // Inline Math's declarations. Each entry's .d.ts (index, lite, lite-calc, markdown, cas) picks from here.
2
+ import type { Input, Node, BlockHandler } from 'mathzetta';
3
+ import type { SpeechOptions } from 'mathzetta/speech';
4
+
5
+ export type { Input, Node, BlockHandler };
6
+
7
+ /**
8
+ * A part of an expression: a node id (MathZetta's postfix index), an expression written like any
9
+ * input ('x^2': every part that is x²), a path ({ path: [0, 1] }, as @mathzetta/cas reports it), a test,
10
+ * or a list of any of these.
11
+ */
12
+ export type Target = number | string | { path: number[] } | ((node: Node) => boolean) | Target[];
13
+
14
+ export interface LayoutOptions {
15
+ /** CSS class prefix (default 'mv'). */
16
+ prefix?: string;
17
+ /** Big operators with limits above and below, full-size fractions (default true). */
18
+ display?: boolean;
19
+ /** 'auto': sin x, ln|x|, sin(x + 1); 'always': sin(x). */
20
+ fnParens?: 'auto' | 'always';
21
+ /** sin(x)^2 written sin²x (default true). */
22
+ trigPowers?: boolean;
23
+ /** 'arc': arcsin x; 'power': sin⁻¹x. */
24
+ inverseTrig?: 'arc' | 'power';
25
+ /** d/dx [x² + 1] ('[') or d/dx (x² + 1) ('('). */
26
+ derivativeBrackets?: '[' | '(';
27
+ /** x^{1/2} with a slash ('slash') or stacked ('stack'). */
28
+ scriptFractions?: 'slash' | 'stack';
29
+ /** c ? a : b drawn as a piecewise brace (default true). */
30
+ cases?: boolean;
31
+ /** The accessible name: true = the spoken form, a string, a function of the tree, or false for none. */
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;
36
+ /** Extra attributes for a node's element. */
37
+ attributes?: ((node: Node) => Record<string, string>) | null;
38
+ /** Inline style for a node's element. */
39
+ style?: ((node: Node) => string) | null;
40
+ }
41
+
42
+ export interface DrawOptions extends LayoutOptions {
43
+ /** Colours by target: { 'x^2': 'red', 3: '#06c' } or [[target, colour], …]. */
44
+ colors?: Record<string | number, string> | Array<[Target, string]>;
45
+ /** Parts to mark with the `<prefix>-hl` class. */
46
+ highlight?: Target;
47
+ /** Variable values for `{{…}}` blocks. */
48
+ scope?: Record<string, unknown>;
49
+ /** Another `{{…}}` handler (casBlocks() from '@mathzetta/inlinemath/cas'). */
50
+ blocks?: BlockHandler;
51
+ }
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
+
60
+ export const DEFAULTS: Required<LayoutOptions>;
61
+
62
+ /** Anything MathZetta reads, as HTML (a string; works in Node). Needs inlinemath.css on the page. */
63
+ export function toHTML(input: Input, options?: DrawOptions): string;
64
+ /** Anything MathZetta reads → a tree with node ids (`{{…}}` filled as toHTML would). */
65
+ export function treeOf(input: Input, options?: DrawOptions): Node;
66
+ /** A tree → HTML, with its height and depth in em. */
67
+ export function layout(tree: Node, options?: LayoutOptions): { html: string; h: number; d: number };
68
+
69
+ /** The nodes a target picks out, in tree order. */
70
+ export function select(tree: Node, target: Target): Node[];
71
+ /** A subtree's postfix as one string: two parts look the same exactly when these match. */
72
+ export function signature(node: Node): string;
73
+ export function nodeAt(tree: Node, path: number[]): Node | null;
74
+ export function pathOf(tree: Node, id: number): number[] | null;
75
+ /** Gives a tree built by hand the ids MathZetta would. */
76
+ export function numberTree<T extends Node>(tree: T): T;
77
+ /** SVG paths of the glyphs Inline Math draws, ( ) { } √ ∫ ∑ ∏: each filled or stroked, in a unit box. */
78
+ export const PATHS: Record<string, { fill?: string; stroke?: string }>;
79
+
80
+ export interface ClickInfo { node: Node; id: number; path: number[] | null; element: Element; event: Event }
81
+
82
+ /** A live drawing: highlight, colour, listen, step with animation. */
83
+ export class View {
84
+ constructor(element: Element, input: Input, options?: ViewOptions);
85
+ readonly element: Element;
86
+ readonly tree: Node;
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;
94
+ nodes(target: Target): Node[];
95
+ elements(target: Target): Element[];
96
+ elementOf(node: Node): Element | null;
97
+ highlight(target: Target, className?: string): this;
98
+ unhighlight(target?: Target | null, className?: string): this;
99
+ color(target: Target, colour: string): this;
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. */
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;
117
+ /** The expression read aloud in pieces, each tagged with the node it describes. */
118
+ segments(options?: SpeechOptions): { text: string; id?: number }[];
119
+ showSegment(segment: { id?: number } | null, className?: string): this;
120
+ }
121
+ /** Draw into an element and return the live view. */
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;
125
+
126
+ // ---- inlinemath/markdown --------------------------------------------------------------------
127
+
128
+ export interface MarkdownOptions {
129
+ /** Options for each drawing (toHTML's). */
130
+ math?: DrawOptions;
131
+ /** The `{{…}}` handler (default the calculator; casBlocks() for the CAS). */
132
+ blocks?: BlockHandler;
133
+ /** Values for `{{…}}`. */
134
+ scope?: Record<string, unknown>;
135
+ /** Remember $r = 3$ for later `{{…}}` (default true). */
136
+ define?: boolean;
137
+ /** Let raw HTML through (default false: it is shown as text). */
138
+ html?: boolean;
139
+ /** Another Markdown renderer; the maths is taken out first and put back into its output. */
140
+ render?: (markdown: string) => string;
141
+ /** Which of '$$', '$', '\\[', '\\(' mark maths (default all). */
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;
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;
182
+ export function markdownToHTML(markdown: string, options?: MarkdownOptions): string;
183
+ export function renderMarkdownInto<E extends Element>(element: E, markdown: string, options?: MarkdownOptions): E;
184
+ export function extractMath(markdown: string, options?: { delimiters?: string[] }): { text: string; math: Array<{ source: string; display: boolean; raw: string; start: number; end: number }> };
185
+ export function renderMarkdown(markdown: string, options?: MarkdownOptions): string;
186
+
187
+ // ---- inlinemath/cas (needs @mathzetta/cas) --------------------------------------------------------
188
+
189
+ /** A step from the CAS (@mathzetta/cas's CASStep fits): path null for a step standing for hidden ones. */
190
+ export interface CASStep { id: string; name: string; from: string; to: string; path?: number[] | null; explanation?: string }
191
+ /** @mathzetta/cas reading and writing MathZetta text (its full API: @mathzetta/cas/docs/cas.md). */
192
+ export type CAS = Record<string, any>;
193
+ export function mathCAS(options?: { rules?: unknown[]; names?: string[]; complex?: boolean; [option: string]: unknown }): CAS;
194
+ /** A `{{…}}` handler: toHTML(input, { blocks: casBlocks() }). `last` holds the latest CAS result. */
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 } };
197
+ /** Steps as an ordered list, each rewrite highlighted, with its explanation. */
198
+ export function stepsHTML(steps: CASStep[], options?: DrawOptions): string;
199
+ /** A CAS node → MathZetta postfix tokens. */
200
+ export function tokensOf(node: unknown): string[];
201
+
202
+ // ---- the calculator (inlinemath/lite-calc re-exports it) -------------------------------------
203
+
204
+ export { calculateBlock, calculate } from 'mathzetta/calculate';
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Saved variables for '@mathzetta/inlinemath/markdown': `$A := 2+2$` saves A, and A in any maths
3
+ * after it is drawn as 2 + 2 (and `{{A}}` is 4). A store holds them for a whole page, so every
4
+ * part drawn with renderMarkdownInto sees the others' variables, and an input bound to a variable
5
+ * (`$A := [[2+2]]$`, a textbox) redraws everything that uses it.
6
+ *
7
+ * import { createVariables, variables } from '@mathzetta/inlinemath/markdown';
8
+ * variables.set('A', 'x^2'); // the page's store: A is x² everywhere
9
+ * variables.get('A'); // 'x^2'
10
+ */
11
+
12
+ /**
13
+ * Where a scope carries the saved variables' expressions, for {{…}} handlers that work with
14
+ * expressions rather than values (casBlocks: {{diff(f, x)}} with f saved): name → MathZetta
15
+ * postfix, every saved name inside already put in. A symbol, so the calculator never sees it.
16
+ */
17
+ export const EXPRESSIONS = Symbol.for('inlinemath.expressions');
18
+
19
+ // ---- names ----------------------------------------------------------------------------------
20
+
21
+ const LEAF = new Set(['symbol', 'number']);
22
+
23
+ /** The variable a node names: A, α, A_1 (a symbol with a plain subscript) — else null. */
24
+ export function nameOf(node) {
25
+ if (!node) return null;
26
+ if (node.type === 'symbol') return node.value;
27
+ if (node.type === 'op' && node.op === '_' && node.args.length === 2 && node.args[0].type === 'symbol' && LEAF.has(node.args[1].type)) {
28
+ return `${node.args[0].value}_${node.args[1].value}`;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /** A tree with its ids (and where-it-came-from marks) dropped, so it can be numbered again inside another. */
34
+ export function bare(node) {
35
+ const { id, from, ...rest } = node;
36
+ if (rest.args) rest.args = rest.args.map(bare);
37
+ return rest;
38
+ }
39
+
40
+ /** Ids dropped (to be numbered again), `from` marks kept. */
41
+ function renumber(node) {
42
+ const { id, ...rest } = node;
43
+ if (rest.args) rest.args = rest.args.map(renumber);
44
+ return rest;
45
+ }
46
+
47
+ /**
48
+ * The part of a substituted tree a variable stands for, written back in its own terms: the
49
+ * variables inside it put back as their names. `named(name)` gives a name's own tree.
50
+ */
51
+ export function unexpand(node, named) {
52
+ const visit = (n, top) => {
53
+ if (!top && n.from !== undefined) return named(n.from);
54
+ const { id, from, ...rest } = n;
55
+ if (rest.args) rest.args = rest.args.map((a) => visit(a, false));
56
+ return rest;
57
+ };
58
+ return visit(node, true);
59
+ }
60
+
61
+ /**
62
+ * Every saved name in a tree replaced by its own tree (which may use other saved names).
63
+ * `lookup(name)` gives a saved name's tree, or null. A name inside its own definition is left as it is.
64
+ * @returns {{ tree: object, changed: boolean }}
65
+ */
66
+ export function substitute(tree, lookup) {
67
+ let changed = false;
68
+ const seen = new Set();
69
+ const visit = (node) => {
70
+ const name = nameOf(node);
71
+ if (name !== null) {
72
+ if (!seen.has(name)) {
73
+ const value = lookup(name);
74
+ if (value) {
75
+ changed = true;
76
+ seen.add(name);
77
+ const out = { ...visit(bare(value)), from: name }; // where it came from, for editing
78
+ seen.delete(name);
79
+ return out;
80
+ }
81
+ }
82
+ return node; // A_1 is one name: its A is not substituted
83
+ }
84
+ if (node.args) return { ...node, args: node.args.map(visit) };
85
+ return node;
86
+ };
87
+ const out = visit(tree);
88
+ return { tree: changed ? renumber(out) : tree, changed };
89
+ }
90
+
91
+ // ---- inputs ---------------------------------------------------------------------------------
92
+
93
+ const NUMBER = '[-+]?(?:\\d+\\.?\\d*|\\.\\d+)';
94
+ const SLIDER = new RegExp(`^(${NUMBER})\\s*\\.\\.\\s*(${NUMBER})(?:\\s+step\\s+(${NUMBER}))?(?:\\s*=\\s*(${NUMBER}))?$`);
95
+
96
+ /**
97
+ * What `[[…]]` asks for: `[[2+2]]` a textbox, `[[2+2 | x^2]]` a dropdown,
98
+ * `[[1..10]]` a slider (`[[0..1 step 0.1 = 0.5]]` with a step and a starting value). Else null.
99
+ */
100
+ export function inputOf(text) {
101
+ const m = /^\s*\[\[([\s\S]*)\]\]\s*$/.exec(text);
102
+ // [[2, 1], [1, 3]] is a matrix (rows of a list), not an input
103
+ if (!m || /\]\s*,\s*\[/.test(m[1])) return null;
104
+ const inner = m[1].trim();
105
+ const range = SLIDER.exec(inner);
106
+ if (range) {
107
+ const min = Number(range[1]);
108
+ const max = Number(range[2]);
109
+ if (!(max > min)) return null;
110
+ const step = range[3] !== undefined ? Number(range[3]) : Number.isInteger(min) && Number.isInteger(max) ? 1 : (max - min) / 100;
111
+ if (!(step > 0)) return null;
112
+ const start = range[4] !== undefined ? Math.min(max, Math.max(min, Number(range[4]))) : min;
113
+ return { kind: 'slider', min, max, step, default: String(start) };
114
+ }
115
+ if (inner.includes('|')) {
116
+ const options = inner.split('|').map((o) => o.trim()).filter(Boolean);
117
+ if (options.length) return { kind: 'dropdown', options, default: options[0] };
118
+ }
119
+ return { kind: 'textbox', default: inner };
120
+ }
121
+
122
+ // ---- the store ------------------------------------------------------------------------------
123
+
124
+ const PAGE = Symbol('set on the page');
125
+
126
+ /**
127
+ * Where a page's variables live. A part of the page owns the definitions it makes (drawing it
128
+ * again replaces them); `set` is how an input — or the page's own code — changes one.
129
+ */
130
+ export class Variables {
131
+ constructor() {
132
+ this.owners = new Map(); // owner → Map(name → definition)
133
+ this.chosen = new Map(); // name → { key, text }: what an input was set to
134
+ this.listeners = new Set();
135
+ }
136
+
137
+ /** Every definition, in the order the parts were drawn; `except` leaves one part out. */
138
+ definitions(except) {
139
+ const all = new Map();
140
+ for (const [owner, defs] of this.owners) {
141
+ if (owner === except) continue;
142
+ for (const [name, def] of defs) {
143
+ all.delete(name); // a later definition goes after the earlier ones
144
+ all.set(name, def);
145
+ }
146
+ }
147
+ return all;
148
+ }
149
+
150
+ /** The text a definition stands for now: its expression, or what its input is set to. */
151
+ textOf(def) {
152
+ const chosen = this.chosen.get(def.name);
153
+ return chosen && chosen.key === def.key ? chosen.text : def.text;
154
+ }
155
+
156
+ /** A variable's expression now, or undefined. */
157
+ get(name) {
158
+ const def = this.definitions().get(name);
159
+ return def ? this.textOf(def) : undefined;
160
+ }
161
+
162
+ /** The saved names. */
163
+ names() {
164
+ return [...this.definitions().keys()];
165
+ }
166
+
167
+ /** Set a variable: what its input holds, or (with no input) a definition for the whole page. */
168
+ set(name, text) {
169
+ text = String(text);
170
+ const def = this.definitions().get(name);
171
+ if (def && def.input) {
172
+ const chosen = this.chosen.get(name);
173
+ if (chosen && chosen.key === def.key && chosen.text === text) return this;
174
+ this.chosen.set(name, { key: def.key, text });
175
+ } else {
176
+ const defs = new Map(this.owners.get(PAGE));
177
+ defs.set(name, { name, text, key: `page:${text}`, input: null });
178
+ this.owners.delete(PAGE);
179
+ this.owners.set(PAGE, defs);
180
+ }
181
+ this.notify(null, name);
182
+ return this;
183
+ }
184
+
185
+ /** Forget a variable set with `set`, or everything (no name). */
186
+ clear(name) {
187
+ if (name === undefined) {
188
+ this.owners.clear();
189
+ this.chosen.clear();
190
+ } else {
191
+ this.owners.get(PAGE)?.delete(name);
192
+ this.chosen.delete(name);
193
+ }
194
+ this.notify(null, name);
195
+ return this;
196
+ }
197
+
198
+ /** Called with (owner, name) when something changes; returns a function that stops it. */
199
+ subscribe(fn) {
200
+ this.listeners.add(fn);
201
+ return () => this.listeners.delete(fn);
202
+ }
203
+
204
+ /** A part's definitions (null: it has none any more). Tells the others only when they change. */
205
+ own(owner, defs) {
206
+ const before = this.owners.get(owner);
207
+ const same = (a, b) => (a?.size ?? 0) === (b?.size ?? 0) && [...(a ?? [])].every(([k, d]) => b?.get(k)?.key === d.key);
208
+ if (same(before, defs)) {
209
+ if (defs) this.owners.set(owner, defs);
210
+ return;
211
+ }
212
+ if (defs && defs.size) this.owners.set(owner, defs);
213
+ else this.owners.delete(owner);
214
+ this.notify(owner);
215
+ }
216
+
217
+ notify(owner, name) {
218
+ for (const fn of [...this.listeners]) fn(owner, name);
219
+ }
220
+ }
221
+
222
+ /** A store of its own, for parts of a page that shouldn't share variables. */
223
+ export function createVariables() {
224
+ return new Variables();
225
+ }
226
+
227
+ /** The page's store: renderMarkdownInto uses it unless told otherwise. */
228
+ export const variables = createVariables();