@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.
- package/LICENSE +21 -0
- package/README.md +282 -2
- package/dist/cas.external.js +2 -0
- package/dist/cas.js +2 -0
- package/dist/chunks/calculate-BX9lJ7Ff.js +2 -0
- package/dist/chunks/draw-Cm4exbg7.js +2 -0
- package/dist/chunks/html-CvuSyhzk.js +2 -0
- package/dist/chunks/html-DWtfytjA.js +2 -0
- package/dist/chunks/lite-Bj_L6w2g.js +2 -0
- package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/dist/fonts/fonts.css +17 -0
- package/dist/fonts/lmroman12-italic.woff2 +0 -0
- package/dist/fonts/lmroman12-regular.woff2 +0 -0
- package/dist/inlinemath-cas.external.global.min.js +2 -0
- package/dist/inlinemath-cas.global.min.js +2 -0
- package/dist/inlinemath-lite-calc.global.min.js +2 -0
- package/dist/inlinemath-lite.external.global.min.js +2 -0
- package/dist/inlinemath-lite.global.min.js +2 -0
- package/dist/inlinemath.css +127 -0
- package/dist/inlinemath.external.global.min.js +2 -0
- package/dist/inlinemath.external.js +2 -0
- package/dist/inlinemath.global.min.js +2 -0
- package/dist/inlinemath.js +2 -0
- package/dist/lite-calc.js +2 -0
- package/dist/lite.external.js +2 -0
- package/dist/lite.js +2 -0
- package/dist/markdown.external.js +2 -0
- package/dist/markdown.js +2 -0
- package/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/fonts/fonts.css +17 -0
- package/fonts/lmroman12-italic.woff2 +0 -0
- package/fonts/lmroman12-regular.woff2 +0 -0
- package/inlinemath.css +127 -0
- package/package.json +95 -4
- package/src/calculator.js +6 -0
- package/src/cas.d.ts +3 -0
- package/src/cas.js +139 -0
- package/src/draw.js +72 -0
- package/src/glyphs.js +38 -0
- package/src/html.js +32 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +17 -0
- package/src/layout.js +605 -0
- package/src/lite-calc.d.ts +3 -0
- package/src/lite-calc.js +15 -0
- package/src/lite.d.ts +3 -0
- package/src/lite.js +18 -0
- package/src/markdown.d.ts +3 -0
- package/src/markdown.js +361 -0
- package/src/mathzetta.js +17 -0
- package/src/select.js +108 -0
- package/src/types.d.ts +133 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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';
|