@mathzetta/inlinemath 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -2
  3. package/dist/cas.external.js +2 -0
  4. package/dist/cas.js +2 -0
  5. package/dist/chunks/calculate-BX9lJ7Ff.js +2 -0
  6. package/dist/chunks/draw-Cm4exbg7.js +2 -0
  7. package/dist/chunks/html-CvuSyhzk.js +2 -0
  8. package/dist/chunks/html-DWtfytjA.js +2 -0
  9. package/dist/chunks/lite-Bj_L6w2g.js +2 -0
  10. package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
  11. package/dist/fonts/fonts.css +17 -0
  12. package/dist/fonts/lmroman12-italic.woff2 +0 -0
  13. package/dist/fonts/lmroman12-regular.woff2 +0 -0
  14. package/dist/inlinemath-cas.external.global.min.js +2 -0
  15. package/dist/inlinemath-cas.global.min.js +2 -0
  16. package/dist/inlinemath-lite-calc.global.min.js +2 -0
  17. package/dist/inlinemath-lite.external.global.min.js +2 -0
  18. package/dist/inlinemath-lite.global.min.js +2 -0
  19. package/dist/inlinemath.css +127 -0
  20. package/dist/inlinemath.external.global.min.js +2 -0
  21. package/dist/inlinemath.external.js +2 -0
  22. package/dist/inlinemath.global.min.js +2 -0
  23. package/dist/inlinemath.js +2 -0
  24. package/dist/lite-calc.js +2 -0
  25. package/dist/lite.external.js +2 -0
  26. package/dist/lite.js +2 -0
  27. package/dist/markdown.external.js +2 -0
  28. package/dist/markdown.js +2 -0
  29. package/fonts/GUST-FONT-LICENSE.txt +30 -0
  30. package/fonts/fonts.css +17 -0
  31. package/fonts/lmroman12-italic.woff2 +0 -0
  32. package/fonts/lmroman12-regular.woff2 +0 -0
  33. package/inlinemath.css +127 -0
  34. package/package.json +95 -4
  35. package/src/calculator.js +6 -0
  36. package/src/cas.d.ts +3 -0
  37. package/src/cas.js +139 -0
  38. package/src/draw.js +72 -0
  39. package/src/glyphs.js +38 -0
  40. package/src/html.js +32 -0
  41. package/src/index.d.ts +3 -0
  42. package/src/index.js +17 -0
  43. package/src/layout.js +605 -0
  44. package/src/lite-calc.d.ts +3 -0
  45. package/src/lite-calc.js +15 -0
  46. package/src/lite.d.ts +3 -0
  47. package/src/lite.js +18 -0
  48. package/src/markdown.d.ts +3 -0
  49. package/src/markdown.js +361 -0
  50. package/src/mathzetta.js +17 -0
  51. package/src/select.js +108 -0
  52. package/src/types.d.ts +133 -0
  53. package/src/view.js +162 -0
package/inlinemath.css ADDED
@@ -0,0 +1,127 @@
1
+ /*
2
+ * Inline Math stylesheet. Everything is under .mv; colours are left to the page.
3
+ *
4
+ * The numbers marked "metric" must match the constants at the top of
5
+ * src/layout.js (line-height 1, bar 0.06em, gap 0.1em); everything else can
6
+ * be changed freely.
7
+ *
8
+ * Handy hooks:
9
+ * .mv-n[data-node="4"] one node (ids from MathZetta)
10
+ * .mv-var .mv-num .mv-word variables, numbers, function names
11
+ * .mv-op-frac .mv-op-pow … a kind of operation
12
+ * .mv-hl the class highlight() adds
13
+ * --mv-hl highlight colour
14
+ */
15
+
16
+ .mv {
17
+ display: inline-block;
18
+ font-family: "Inline Math Roman", "Latin Modern Roman", "STIX Two Text", "Cambria", "Times New Roman", serif;
19
+ font-size: 1.2em;
20
+ font-style: normal;
21
+ font-weight: normal;
22
+ line-height: 1; /* metric */
23
+ white-space: nowrap;
24
+ text-indent: 0;
25
+ text-align: left;
26
+ letter-spacing: 0;
27
+ }
28
+ .mv-display { display: block; text-align: center; margin: 0.6em 0; }
29
+ .mv span, .mv svg { line-height: 1; }
30
+ .mv-n { display: inline-block; border-radius: 0.12em; }
31
+
32
+ /* ---- letters and signs -------------------------------------------------- */
33
+ .mv-var { font-style: italic; padding-right: 0.03em; }
34
+ .mv-num, .mv-word, .mv-name, .mv-ord, .mv-text, .mv-d { font-style: normal; }
35
+ .mv-prime { margin-left: 0.04em; }
36
+ .mv-bin { margin: 0 0.22em; }
37
+ .mv-rel { margin: 0 0.28em; }
38
+ .mv-punct { margin-right: 0.17em; }
39
+ .mv-sign { margin-right: 0.04em; }
40
+ .mv-times { display: inline-block; width: 0.06em; }
41
+ .mv-callgap { display: inline-block; width: 0.05em; }
42
+ .mv-fnspace { display: inline-block; width: 0.17em; }
43
+ .mv-space { display: inline-block; width: 0.33em; }
44
+ .mv-dx { display: inline-block; margin-left: 0.17em; }
45
+ .mv-text { margin: 0 0.25em; }
46
+ /* Tighter spacing in limits and exponents: n=1, x→0. */
47
+ .mv-script .mv-rel, .mv-sup .mv-rel, .mv-sub .mv-rel { margin: 0 0.12em; }
48
+ .mv-script .mv-bin, .mv-sup .mv-bin, .mv-sub .mv-bin { margin: 0 0.1em; }
49
+
50
+ /* ---- fractions (metric: padding and border) ------------------------------ */
51
+ .mv-frac { display: inline-flex; flex-direction: column; align-items: stretch; margin: 0 0.12em; }
52
+ .mv-numer, .mv-denom { display: block; text-align: center; padding: 0 0.1em; }
53
+ .mv-numer { padding-bottom: 0.1em; }
54
+ .mv-denom { padding-top: 0.1em; border-top: 0.06em solid currentColor; }
55
+ .mv-frac-nobar > .mv-denom { border-top: 0; }
56
+ .mv-scaled { display: inline-block; }
57
+
58
+ /* ---- scripts ----------------------------------------------------------------- */
59
+ .mv-sup, .mv-sub { display: inline-block; margin-left: 0.04em; }
60
+ .mv-subsup, .mv-limits { display: inline-flex; flex-direction: column; justify-content: space-between; align-items: flex-start; margin-left: 0.04em; }
61
+ .mv-limits { margin-left: 0.02em; margin-right: 0.12em; }
62
+ .mv-limits > .mv-lower { margin-left: -0.3em; }
63
+ .mv-script { display: block; }
64
+ .mv-under { display: inline-flex; flex-direction: column; align-items: center; margin-right: 0.15em; }
65
+ .mv-under > .mv-script { margin-top: 0.05em; }
66
+ .mv-bigop { display: inline-flex; flex-direction: column; align-items: center; margin: 0 0.12em; }
67
+ .mv-bigop > .mv-script { display: block; margin: 0.08em 0; }
68
+ .mv-bigop > svg { display: block; }
69
+
70
+ /* ---- radicals (metric: border and padding-top) ----------------------------- */
71
+ .mv-sqrt { display: inline-block; white-space: nowrap; }
72
+ .mv-hook { display: inline-block; overflow: visible; margin-left: 0.05em; }
73
+ .mv-radicand { display: inline-block; border-top: 0.06em solid currentColor; padding: 0.1em 0.1em 0 0.06em; }
74
+ .mv-rootindex { display: inline-block; margin: 0 -0.32em 0 0.12em; }
75
+
76
+ /* ---- delimiters: drawn at the full height of what they enclose -------------- */
77
+ .mv-fence { display: inline-block; position: relative; }
78
+ .mv-delim { position: absolute; top: 0; bottom: 0; height: 100%; }
79
+ .mv-delim-l { left: 0.03em; }
80
+ .mv-delim-r { right: 0.03em; }
81
+ span.mv-delim { width: 0.16em; border: 0 solid currentColor; box-sizing: border-box; }
82
+ .mv-delim.mv-lbrack { border-width: 0.06em 0 0.06em 0.06em; left: 0.06em; }
83
+ .mv-delim.mv-rbrack { border-width: 0.06em 0.06em 0.06em 0; right: 0.06em; }
84
+ .mv-delim.mv-vert { border-left-width: 0.06em; width: 0; left: 0.07em; }
85
+ .mv-delim.mv-vert.mv-delim-r { left: auto; right: 0.07em; }
86
+ .mv-delim.mv-lfloor { border-width: 0 0 0.06em 0.06em; }
87
+ .mv-delim.mv-rfloor { border-width: 0 0.06em 0.06em 0; }
88
+ .mv-delim.mv-lceil { border-width: 0.06em 0 0 0.06em; }
89
+ .mv-delim.mv-rceil { border-width: 0.06em 0.06em 0 0; }
90
+
91
+ /* ---- shapes ----------------------------------------------------------------- */
92
+ .mv svg { overflow: visible; }
93
+ .mv svg path[stroke] { stroke-width: 0.065em; stroke-linecap: round; stroke-linejoin: round; }
94
+ .mv-intsign path[stroke] { stroke-width: 0.075em; }
95
+ .mv-bigsign { display: inline-block; margin: 0 0.06em; }
96
+ .mv-intsign { margin-right: 0.12em; }
97
+
98
+ /* ---- piecewise ------------------------------------------------------------- */
99
+ .mv-cases { display: inline-grid; grid-template-columns: auto auto; column-gap: 1em; row-gap: 0.25em; align-items: baseline; text-align: left; margin: 0 0.1em; }
100
+ .mv-case-value, .mv-case-cond { display: block; }
101
+
102
+ /* ---- decorations: \boxed \cancel \overline \vec … --------------------------- */
103
+ .mv-boxed { display: inline-block; border: 0.06em solid currentColor; padding: 0.16em 0.2em; }
104
+ .mv-over { display: inline-block; border-top: 0.06em solid currentColor; padding-top: 0.08em; }
105
+ .mv-underline { display: inline-block; border-bottom: 0.06em solid currentColor; padding-bottom: 0.06em; }
106
+ .mv-cancel { display: inline-block; background: linear-gradient(to top right, transparent calc(50% - 0.04em), currentColor calc(50% - 0.03em), currentColor calc(50% + 0.03em), transparent calc(50% + 0.04em)); }
107
+ .mv-accent { display: inline-block; position: relative; padding-top: 0.3em; }
108
+ .mv-accent-mark { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-size: 0.7em; line-height: 0.6; }
109
+
110
+ /* ---- interaction -------------------------------------------------------------- */
111
+ .mv-hl { background: var(--mv-hl, rgba(255, 200, 0, 0.35)); box-shadow: 0 0 0 0.08em var(--mv-hl, rgba(255, 200, 0, 0.35)); }
112
+ .mv-clickable .mv-n { cursor: pointer; }
113
+ .mv-clickable .mv-n:hover { outline: 1px dashed rgba(0, 0, 0, 0.25); }
114
+
115
+ /* ---- steps (stepsHTML from '@mathzetta/inlinemath/cas') ---------------------------------------- */
116
+ .mv-steps { margin: 0; padding-left: 1.6em; }
117
+ .mv-step { margin: 0.5em 0 0.9em; }
118
+ .mv-step-name { display: block; font-size: 0.85em; font-weight: 600; color: #555; }
119
+ .mv-step-maths { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4em 0.8em; margin: 0.15em 0; }
120
+ .mv-step-arrow { color: #888; }
121
+ .mv-step-why { margin: 0.1em 0 0; font-size: 0.9em; color: #444; }
122
+
123
+ /* ---- Markdown with maths (markdownToHTML from '@mathzetta/inlinemath/markdown') ------------------- */
124
+ .mv-md-math { display: inline-block; vertical-align: middle; }
125
+ .mv-md-display { display: block; text-align: center; margin: 0.6em 0; overflow-x: auto; }
126
+ .mv-md-block { margin: 0.8em 0; }
127
+ .mv-md-error code { background: rgba(200, 60, 0, 0.08); color: #a33; border-bottom: 1px dotted #a33; }
package/package.json CHANGED
@@ -1,6 +1,97 @@
1
1
  {
2
2
  "name": "@mathzetta/inlinemath",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Draw calculus-level maths as HTML and CSS from anything MathZetta reads, with every part addressable.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./lite": {
14
+ "types": "./src/lite.d.ts",
15
+ "default": "./src/lite.js"
16
+ },
17
+ "./lite-calc": {
18
+ "types": "./src/lite-calc.d.ts",
19
+ "default": "./src/lite-calc.js"
20
+ },
21
+ "./cas": {
22
+ "types": "./src/cas.d.ts",
23
+ "default": "./src/cas.js"
24
+ },
25
+ "./markdown": {
26
+ "types": "./src/markdown.d.ts",
27
+ "default": "./src/markdown.js"
28
+ },
29
+ "./inlinemath.css": "./inlinemath.css",
30
+ "./fonts/fonts.css": "./fonts/fonts.css",
31
+ "./fonts/*": "./fonts/*",
32
+ "./dist/*": "./dist/*",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "src/",
37
+ "dist/",
38
+ "fonts/",
39
+ "inlinemath.css",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "scripts": {
44
+ "test": "node test/run.js",
45
+ "build": "node tools/build.mjs",
46
+ "prepublishOnly": "npm test && npm run build"
47
+ },
48
+ "peerDependencies": {
49
+ "@mathzetta/cas": "^0.1.0",
50
+ "mathzetta": "^1.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@rollup/plugin-terser": "^0.4.4",
54
+ "@mathzetta/cas": "^0.1.0",
55
+ "mathzetta": "^1.0.0",
56
+ "rollup": "^4.65.0"
57
+ },
58
+ "license": "MIT",
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@mathzetta/cas": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "homepage": "https://inlinemath.com",
68
+ "unpkg": "dist/inlinemath.global.min.js",
69
+ "jsdelivr": "dist/inlinemath.global.min.js",
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "keywords": [
74
+ "math",
75
+ "mathml",
76
+ "html",
77
+ "calculus",
78
+ "render",
79
+ "mathzetta",
80
+ "inline",
81
+ "markdown"
82
+ ],
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "git+https://github.com/triplelog/mathzetta.git",
86
+ "directory": "packages/inlinemath"
87
+ },
88
+ "author": {
89
+ "name": "Robert Wilcox",
90
+ "email": "rwilcox@mathzetta.com",
91
+ "url": "https://mathzetta.com"
92
+ },
93
+ "bugs": {
94
+ "url": "https://github.com/triplelog/mathzetta/issues",
95
+ "email": "rwilcox@mathzetta.com"
96
+ }
97
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MathZetta's calculator. Loading 'mathzetta/calculate' is what makes `{{3+4}}` in an input draw as
3
+ * 7: it registers the calculator with the parser. The full Inline Math (index.js, markdown, cas)
4
+ * imports this; the lite build (lite.js) doesn't.
5
+ */
6
+ export { calculateBlock, calculate } from 'mathzetta/calculate';
package/src/cas.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // '@mathzetta/inlinemath/cas' — @mathzetta/cas in drawn maths (needs @mathzetta/cas). See types.d.ts.
2
+ export type { CAS, CASStep } from './types.js';
3
+ export { mathCAS, casBlocks, stepsHTML, tokensOf } from './types.js';
package/src/cas.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * '@mathzetta/inlinemath/cas' — @mathzetta/cas in Inline Math: `{{…}}` blocks that simplify,
3
+ * solve, differentiate …, and step lists drawn as maths.
4
+ *
5
+ * import { toHTML } from '@mathzetta/inlinemath';
6
+ * import { casBlocks, stepsHTML, mathCAS } from '@mathzetta/inlinemath/cas';
7
+ *
8
+ * toHTML('f′(x) = {{diff(x^3 sin x, x)}}', { blocks: casBlocks() })
9
+ * toHTML('{{solve(x^2 - 5x + 6 = 0, x)}}', { blocks: casBlocks() }) // x = 2 or x = 3
10
+ * stepsHTML(mathCAS().diff('x^2 sin x', 'x', { steps: 'brief' }).steps)
11
+ *
12
+ * Block functions: simplify, expand, factor, cancel, diff(f, x[, n]), integrate(f, x[, a, b]),
13
+ * solve(equation, x), approx(expr[, digits]); d/dx … and ∫ … without limits go to the CAS
14
+ * too. Everything else is MathZetta's calculator (calculateBlock), as without this.
15
+ *
16
+ * Needs @mathzetta/cas (a peer dependency, like mathzetta).
17
+ */
18
+
19
+ import { createCAS, toText as plainText } from '@mathzetta/cas';
20
+ import { parse as parsePostfix } from '@mathzetta/cas/engine';
21
+ import { parse, operator } from './mathzetta.js';
22
+ import { calculateBlock } from './calculator.js';
23
+ // Only this entry needs MathZetta's text writer (so only pages using inlinemath/cas map 'mathzetta/text').
24
+ import { toText } from 'mathzetta/text';
25
+ import { toHTML } from './html.js';
26
+
27
+ const shared = new Map();
28
+
29
+ /** How the complex functions are written for MathZetta: Re z, Im z, z̄ (an overline), arg z. */
30
+ const DRAWN = { re: 'Re', im: 'Im', conj: 'overline' };
31
+ const COMPLEX_NAMES = ['Re', 'Im', 're', 'im', 'conj', 'arg'];
32
+
33
+ /**
34
+ * A CAS node → MathZetta postfix tokens. Functions declared in a rule file
35
+ * (sinc) are MathZetta's user functions: x sinc → x 1 \:sinc.
36
+ */
37
+ export function tokensOf(node) {
38
+ const out = [];
39
+ const visit = (n) => {
40
+ if (n.type !== 'op') { out.push(n.token); return; }
41
+ n.args.forEach(visit);
42
+ if (operator(n.op)) {
43
+ if (operator(n.op).arity === -1) out.push(String(n.args.length));
44
+ out.push(n.op);
45
+ } else out.push(String(n.args.length), `\\:${DRAWN[n.op] ?? n.op}`);
46
+ };
47
+ visit(node);
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * A CAS that reads and writes MathZetta text. Options go to @mathzetta/cas's createCAS
53
+ * (rules: your rule files; complex: true for i = √-1); `names` are functions of your
54
+ * own, so `sinc(x)` is read as sinc and not as s·i·n·c(x). With complex, Re, Im,
55
+ * conj and arg are read as functions too.
56
+ */
57
+ export function mathCAS(options = {}) {
58
+ const key = options.rules || options.names ? null : options.complex ? 'complex' : 'default';
59
+ if (key && shared.has(key)) return shared.get(key);
60
+ const names = [...(options.names ?? []), ...(options.complex ? COMPLEX_NAMES : [])];
61
+ const cas = createCAS({
62
+ ...options,
63
+ read: (text) => parse(text, names.length ? { names } : {}),
64
+ // explanations read "3x^2", "sin(3x)"; a block's answer is MathZetta text, so it is read back exactly
65
+ format: plainText,
66
+ });
67
+ if (key) shared.set(key, cas);
68
+ return cas;
69
+ }
70
+
71
+ const CALLS = new Set(['simplify', 'expand', 'factor', 'cancel', 'diff', 'integrate', 'solve', 'approx']);
72
+
73
+ /**
74
+ * A `{{…}}` handler: pass it as toHTML(input, { blocks: casBlocks() }).
75
+ * @param {object} [options] { cas } a CAS of your own (mathCAS({ rules })), { steps } kept on the handler's `last`
76
+ */
77
+ export function casBlocks(options = {}) {
78
+ const cas = options.cas ?? mathCAS();
79
+ const handler = (text, scope) => {
80
+ let tokens;
81
+ try {
82
+ tokens = cas.read(text);
83
+ } catch {
84
+ return calculateBlock(text, scope);
85
+ }
86
+ const head = tokens[tokens.length - 1];
87
+ const name = head?.startsWith('\\:') ? head.slice(2) : null;
88
+ const symbolic = name && CALLS.has(name);
89
+ // d/dx …, ∫ … dx and ∫ from a to b … dx (MathZetta writes the limits as int's 3rd and 4th operands)
90
+ const calculus = head === 'der' || head === 'int';
91
+ if (!symbolic && !calculus) return calculateBlock(text, scope);
92
+ const node = cas.node(tokens);
93
+ const args = symbolic ? node.args : [node];
94
+ const word = (n) => (n ? n.s : undefined);
95
+ let r;
96
+ try {
97
+ switch (symbolic ? name : head === 'der' ? 'derivative' : 'antiderivative') {
98
+ case 'simplify': r = cas.simplify(args[0]); break;
99
+ case 'expand': r = cas.expand(args[0]); break;
100
+ case 'factor': r = cas.factor(args[0]); break;
101
+ case 'cancel': r = cas.cancel(args[0]); break;
102
+ case 'diff': r = cas.diff(args[0], word(args[1]), { order: args[2] ? Number(args[2].s) : 1 }); break;
103
+ case 'integrate': r = cas.integrate(args[0], word(args[1]), args[3] ? { from: args[2].s, to: args[3].s } : {}); break;
104
+ case 'solve': r = cas.solve(args[0], word(args[1])); break;
105
+ case 'approx': r = cas.approximate(args[0], { digits: args[1] ? Number(args[1].s) : 15 }); break;
106
+ case 'derivative': r = cas.diff(node.args[0], node.args[1].s); break;
107
+ default: r = cas.integrate(node); break;
108
+ }
109
+ } catch (error) {
110
+ // no answer: leave it as written, as calculateBlock does
111
+ return `(${text})`;
112
+ }
113
+ handler.last = r;
114
+ if (!r.result) return `"${r.text}"`;
115
+ const out = toText(tokensOf(r.result));
116
+ return /^-?\d+(\.\d+)?$/.test(out) ? out : `(${out})`;
117
+ };
118
+ return handler;
119
+ }
120
+
121
+ const escape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
122
+
123
+ /**
124
+ * Steps (from any CAS call) as an ordered list: each step's expression before,
125
+ * with the part a rule rewrote highlighted, the expression after, and the
126
+ * explanation. Options go to toHTML for both drawings (size, colors …).
127
+ * @returns {string} <ol class="mv-steps">…</ol>
128
+ */
129
+ export function stepsHTML(steps, options = {}) {
130
+ const items = steps.map((s) => {
131
+ const from = tokensOf(parsePostfix(s.from));
132
+ const before = toHTML(from, { ...options, highlight: s.path && s.path.length ? { path: s.path } : undefined });
133
+ const after = s.to === s.from ? '' : `<span class="mv-step-arrow" aria-label="becomes">⟶</span>${toHTML(tokensOf(parsePostfix(s.to)), options)}`;
134
+ const why = s.explanation ? `<p class="mv-step-why">${escape(s.explanation)}</p>` : '';
135
+ return `<li class="mv-step" data-rule="${escape(s.id)}"><span class="mv-step-name">${escape(s.name)}</span>`
136
+ + `<div class="mv-step-maths">${before}${after}</div>${why}</li>`;
137
+ });
138
+ return `<ol class="mv-steps">${items.join('')}</ol>`;
139
+ }
package/src/draw.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Input → tree → HTML string, with no calculator: the drawing both builds share (html.js adds the
3
+ * calculator for the full build; lite.js uses this as it is). No DOM needed, so this runs in Node too.
4
+ */
5
+
6
+ import { parseTree, fromPostfix } from './mathzetta.js';
7
+ import { layout } from './layout.js';
8
+ import { select, numberTree } from './select.js';
9
+
10
+ /**
11
+ * Anything MathZetta reads → a tree with node ids. `{{...}}` blocks are filled by `options.blocks`
12
+ * (with `options.scope`), else by whatever handler MathZetta has (the calculator once
13
+ * 'mathzetta/calculate' is loaded), else drawn as a group.
14
+ * @param {string|string[]|object} input expression text, standard postfix, or a tree
15
+ * @param {object} [options] { blocks, scope }
16
+ */
17
+ export function treeOf(input, options = {}) {
18
+ const { blocks } = options;
19
+ if (typeof input === 'string') return parseTree(input, blocks ? { blocks: (text) => blocks(text, options.scope) } : {});
20
+ if (Array.isArray(input)) return fromPostfix(input);
21
+ if (input && typeof input === 'object' && input.type) {
22
+ return input.id === undefined ? numberTree(structuredClone(input)) : input;
23
+ }
24
+ throw new Error('Inline Math needs an expression, standard postfix or a tree');
25
+ }
26
+
27
+ /**
28
+ * @param {string|string[]|object} input
29
+ * @param {object} [options] layout options (see DEFAULTS in layout.js) plus:
30
+ * @param {object|Array} [options.colors] { 'x^2': 'red', 3: '#06c' } or [[target, colour], …]
31
+ * @param {*} [options.highlight] target(s) to mark with the mv-hl class
32
+ * @param {object} [options.scope] variable values for `{{...}}` blocks
33
+ * @returns {string}
34
+ */
35
+ export function toHTML(input, options = {}) {
36
+ const tree = treeOf(input, options);
37
+ return layout(tree, layoutOptions(tree, options)).html;
38
+ }
39
+
40
+ /** Turn the friendly options (colors, highlight) into layout's attributes and style hooks. */
41
+ export function layoutOptions(tree, options) {
42
+ const { colors, highlight, scope, blocks, ...rest } = options;
43
+ const styles = new Map();
44
+ for (const [target, colour] of pairs(colors)) {
45
+ if (!isColour(colour)) continue; // one CSS colour value, nothing more: no `;`, no url(…)
46
+ for (const n of select(tree, target)) styles.set(n, `color:${colour}`);
47
+ }
48
+ const marked = new Set(highlight === undefined ? [] : select(tree, highlight));
49
+ if (!styles.size && !marked.size) return rest;
50
+ const prefix = rest.prefix || 'mv';
51
+ return {
52
+ ...rest,
53
+ style: (n) => [styles.get(n), rest.style && rest.style(n)].filter(Boolean).join(';'),
54
+ attributes: (n) => {
55
+ const extra = { ...(rest.attributes ? rest.attributes(n) : {}) };
56
+ if (marked.has(n)) extra.class = [extra.class, `${prefix}-hl`].filter(Boolean).join(' ');
57
+ return extra;
58
+ },
59
+ };
60
+ }
61
+
62
+ /** { 'x^2': 'red', 3: 'blue' } or [[target, colour]] → [[target, colour]]; numeric keys become ids. */
63
+ /** A colour a style attribute can hold on its own: red, #06c, rgb(0 102 204 / 50%), var(--accent). */
64
+ function isColour(value) {
65
+ return typeof value === 'string' && /^[#\w\s.,%()/+-]+$/.test(value) && !/url\s*\(|expression/i.test(value);
66
+ }
67
+
68
+ function pairs(colors) {
69
+ if (!colors) return [];
70
+ if (Array.isArray(colors)) return colors;
71
+ return Object.entries(colors).map(([k, v]) => [/^\d+$/.test(k) ? Number(k) : k, v]);
72
+ }
package/src/glyphs.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shapes drawn as inline SVG: parentheses, braces, the radical sign and the
3
+ * big operators. Each is drawn in a 0..1 box with preserveAspectRatio="none"
4
+ * and sized by CSS, so the same path serves every height.
5
+ *
6
+ * Filled shapes (parens, braces) keep their thickness because their width
7
+ * stays fixed while their height stretches. Stroked shapes (√, ∫) use
8
+ * non-scaling strokes so the line weight never changes with size.
9
+ *
10
+ * To restyle a shape, change its path here; nothing else depends on it.
11
+ */
12
+
13
+ export const PATHS = {
14
+ // Crescents: outer curve down, inner curve back up.
15
+ '(': { fill: 'M0.92,0 C0.1,0.2 0.1,0.8 0.92,1 L1,0.975 C0.48,0.75 0.48,0.25 1,0.025 Z' },
16
+ ')': { fill: 'M0.08,0 C0.9,0.2 0.9,0.8 0.08,1 L0,0.975 C0.52,0.75 0.52,0.25 0,0.025 Z' },
17
+ '{': { fill: 'M1,0 C0.45,0 0.5,0.05 0.5,0.2 L0.5,0.38 C0.5,0.46 0.35,0.49 0.05,0.5 C0.35,0.51 0.5,0.54 0.5,0.62 L0.5,0.8 C0.5,0.95 0.45,1 1,1 L1,0.985 C0.68,0.985 0.64,0.95 0.64,0.8 L0.64,0.62 C0.64,0.53 0.55,0.505 0.3,0.5 C0.55,0.495 0.64,0.47 0.64,0.38 L0.64,0.2 C0.64,0.05 0.68,0.015 1,0.015 Z' },
18
+ '}': { fill: 'M0,0 C0.55,0 0.5,0.05 0.5,0.2 L0.5,0.38 C0.5,0.46 0.65,0.49 0.95,0.5 C0.65,0.51 0.5,0.54 0.5,0.62 L0.5,0.8 C0.5,0.95 0.55,1 0,1 L0,0.985 C0.32,0.985 0.36,0.95 0.36,0.8 L0.36,0.62 C0.36,0.53 0.45,0.505 0.7,0.5 C0.45,0.495 0.36,0.47 0.36,0.38 L0.36,0.2 C0.36,0.05 0.32,0.015 0,0.015 Z' },
19
+ // Radical: short tick, long down-stroke, long up-stroke to the top right.
20
+ 'sqrt': { stroke: 'M0,0.62 L0.18,0.55 L0.45,0.97 L0.98,0.015' },
21
+ // Integral: an S slanted to the right with small hooks at each end.
22
+ 'int': { stroke: 'M0.95,0.09 C0.9,0.01 0.72,0 0.66,0.12 C0.6,0.25 0.42,0.75 0.34,0.88 C0.28,0.99 0.1,0.99 0.05,0.91' },
23
+ 'sum': { fill: 'M0.04,0 L0.94,0 L0.98,0.24 L0.94,0.24 C0.9,0.1 0.8,0.07 0.62,0.07 L0.24,0.07 L0.58,0.5 L0.2,0.93 L0.64,0.93 C0.82,0.93 0.92,0.9 0.95,0.76 L0.99,0.76 L0.94,1 L0.02,1 L0.47,0.47 Z' },
24
+ 'prod': { fill: 'M0,0 L1,0 L1,0.04 L0.88,0.05 L0.88,0.95 L1,0.96 L1,1 L0.62,1 L0.62,0.96 L0.74,0.95 L0.74,0.06 L0.26,0.06 L0.26,0.95 L0.38,0.96 L0.38,1 L0,1 L0,0.96 L0.12,0.95 L0.12,0.05 L0,0.04 Z' },
25
+ };
26
+
27
+ /**
28
+ * An SVG element for a shape. `style` sizes and places it (width, height,
29
+ * vertical-align or absolute position).
30
+ */
31
+ export function svg(name, cls, style = '') {
32
+ const shape = PATHS[name];
33
+ const path = shape.fill
34
+ ? `<path d="${shape.fill}" fill="currentColor"/>`
35
+ : `<path d="${shape.stroke}" fill="none" stroke="currentColor" vector-effect="non-scaling-stroke"/>`;
36
+ const attr = style ? ` style="${style}"` : '';
37
+ return `<svg class="${cls}" viewBox="0 0 1 1" preserveAspectRatio="none" aria-hidden="true"${attr}>${path}</svg>`;
38
+ }
package/src/html.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Input → tree → HTML string. No DOM needed, so this runs in Node too. The full build: `{{...}}`
3
+ * blocks are calculated (draw.js is the same without the calculator).
4
+ */
5
+
6
+ import { calculateBlock } from './calculator.js';
7
+ import * as draw from './draw.js';
8
+
9
+ export { layoutOptions } from './draw.js';
10
+
11
+ /**
12
+ * Anything MathZetta reads → a tree with node ids. `{{...}}` blocks in text
13
+ * are calculated first: '3+4={{3+4}}' draws 3 + 4 = 7.
14
+ * @param {string|string[]|object} input expression text, standard postfix, or a tree
15
+ * @param {object} [options] { scope } variable values for `{{...}}` blocks;
16
+ * { blocks } another handler for them (casBlocks() from '@mathzetta/inlinemath/cas' simplifies, solves …)
17
+ */
18
+ export function treeOf(input, options = {}) {
19
+ return draw.treeOf(input, { ...options, blocks: options.blocks ?? calculateBlock });
20
+ }
21
+
22
+ /**
23
+ * @param {string|string[]|object} input
24
+ * @param {object} [options] layout options (see DEFAULTS in layout.js) plus:
25
+ * @param {object|Array} [options.colors] { 'x^2': 'red', 3: '#06c' } or [[target, colour], …]
26
+ * @param {*} [options.highlight] target(s) to mark with the mv-hl class
27
+ * @param {object} [options.scope] variable values for `{{...}}` blocks
28
+ * @returns {string}
29
+ */
30
+ export function toHTML(input, options = {}) {
31
+ return draw.toHTML(input, { ...options, blocks: options.blocks ?? calculateBlock });
32
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // '@mathzetta/inlinemath' — drawing, the live view, selection. See types.d.ts.
2
+ export type { Input, Node, Target, LayoutOptions, DrawOptions, ClickInfo } from './types.js';
3
+ export { toHTML, treeOf, layout, DEFAULTS, View, render, select, signature, nodeAt, pathOf, numberTree, PATHS } from './types.js';
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Inline Math — draws calculus-level maths as HTML and CSS, from anything
3
+ * MathZetta can read, with every part addressable.
4
+ *
5
+ * import { toHTML, render } from '@mathzetta/inlinemath';
6
+ *
7
+ * toHTML('\\int_0^1 x^2 dx') // a string, works in Node
8
+ * const view = render(div, 'd/dx sin(x^2)') // live: highlight, click, animate
9
+ *
10
+ * Needs inlinemath.css on the page (and optionally fonts/fonts.css).
11
+ */
12
+
13
+ export { toHTML, treeOf } from './html.js';
14
+ export { layout, DEFAULTS } from './layout.js';
15
+ export { View, render } from './view.js';
16
+ export { select, signature, nodeAt, pathOf, numberTree } from './select.js';
17
+ export { PATHS } from './glyphs.js';