@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/src/markdown.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* '@mathzetta/inlinemath/markdown' — Markdown with maths in it.
|
|
3
|
+
*
|
|
4
|
+
* import { markdownToHTML } from '@mathzetta/inlinemath/markdown';
|
|
5
|
+
* el.innerHTML = markdownToHTML('The area is $\\pi r^2$, so for $r = 3$ it is ${{pi r^2}}$.');
|
|
6
|
+
*
|
|
7
|
+
* Maths: $…$ inline, $$…$$ (or a ```math fence) on its own, \(…\) and \[…\] too.
|
|
8
|
+
* Each is read by MathZetta (LaTeX, plain text or code all work) and drawn by
|
|
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$ or
|
|
11
|
+
* $g := 9.81$ is remembered for the {{…}} after it ({ define: false } turns that off).
|
|
12
|
+
*
|
|
13
|
+
* A $ is maths only when it looks like it (Pandoc's rule): the opening $ is not
|
|
14
|
+
* followed by a space, the closing $ is not after a space or before a digit, so
|
|
15
|
+
* "$5 and $10" stays text. \$ is a dollar sign. Code spans and fences are left alone.
|
|
16
|
+
* Maths that doesn't read is shown as written, marked, with the reason as its title.
|
|
17
|
+
*
|
|
18
|
+
* The Markdown is a small built-in renderer (headings, emphasis, links, images,
|
|
19
|
+
* lists, quotes, code, tables, rules). Raw HTML is shown as text unless { html: true }.
|
|
20
|
+
* { render: (markdown) => html } uses another renderer (marked, markdown-it …):
|
|
21
|
+
* the maths is taken out first and put back into its output.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { toHTML } from './html.js';
|
|
25
|
+
import { calculate } from './calculator.js';
|
|
26
|
+
|
|
27
|
+
// ---- finding the maths --------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const OPEN = '';
|
|
30
|
+
const CLOSE = '';
|
|
31
|
+
const placeholder = (n) => `${OPEN}${n}${CLOSE}`;
|
|
32
|
+
const PLACEHOLDER = new RegExp(`${OPEN}(\\d+)${CLOSE}`, 'g');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The maths in a Markdown text, and the text with each piece replaced by a
|
|
36
|
+
* placeholder (private-use characters, which no Markdown renderer touches).
|
|
37
|
+
* @returns {{ text: string, math: Array<{ source, display, raw }> }}
|
|
38
|
+
*/
|
|
39
|
+
export function extractMath(markdown, { delimiters = ['$$', '$', '\\[', '\\('] } = {}) {
|
|
40
|
+
const math = [];
|
|
41
|
+
let out = '';
|
|
42
|
+
let i = 0;
|
|
43
|
+
const s = String(markdown);
|
|
44
|
+
const atLineStart = (k) => k === 0 || s[k - 1] === '\n';
|
|
45
|
+
const keep = (source, display, raw) => {
|
|
46
|
+
math.push({ source, display, raw });
|
|
47
|
+
return placeholder(math.length - 1);
|
|
48
|
+
};
|
|
49
|
+
while (i < s.length) {
|
|
50
|
+
// a fenced code block: copied as is, except ```math, which is display maths
|
|
51
|
+
if (atLineStart(i)) {
|
|
52
|
+
const fence = /^( {0,3})(`{3,}|~{3,})([^\n]*)\n?/.exec(s.slice(i));
|
|
53
|
+
if (fence) {
|
|
54
|
+
const marker = fence[2];
|
|
55
|
+
const closeRe = new RegExp(`\\n {0,3}${marker[0] === '`' ? '`' : '~'}{${marker.length},}[ \\t]*(?=\\n|$)`);
|
|
56
|
+
const rest = s.slice(i + fence[0].length);
|
|
57
|
+
const end = closeRe.exec(`\n${rest}`);
|
|
58
|
+
const body = end ? rest.slice(0, Math.max(0, end.index - 1)) : rest;
|
|
59
|
+
const length = fence[0].length + (end ? end.index - 1 + end[0].length : rest.length);
|
|
60
|
+
const raw = s.slice(i, i + length);
|
|
61
|
+
if (fence[3].trim() === 'math') out += keep(body, true, raw);
|
|
62
|
+
else out += raw;
|
|
63
|
+
i += length;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const c = s[i];
|
|
68
|
+
// a code span: as many backticks to close it as opened it
|
|
69
|
+
if (c === '`') {
|
|
70
|
+
const run = /^`+/.exec(s.slice(i))[0];
|
|
71
|
+
const close = s.indexOf(run, i + run.length);
|
|
72
|
+
if (close > -1) {
|
|
73
|
+
out += s.slice(i, close + run.length);
|
|
74
|
+
i = close + run.length;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out += run;
|
|
78
|
+
i += run.length;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c === '\\') {
|
|
82
|
+
const two = s.slice(i, i + 2);
|
|
83
|
+
const closer = two === '\\[' ? '\\]' : two === '\\(' ? '\\)' : null;
|
|
84
|
+
if (closer && delimiters.includes(two)) {
|
|
85
|
+
const end = s.indexOf(closer, i + 2);
|
|
86
|
+
if (end > -1) {
|
|
87
|
+
out += keep(s.slice(i + 2, end), two === '\\[', s.slice(i, end + 2));
|
|
88
|
+
i = end + 2;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
out += two; // \$ and other escapes stay for the Markdown renderer
|
|
93
|
+
i += 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (c === '$') {
|
|
97
|
+
if (s[i + 1] === '$' && delimiters.includes('$$')) {
|
|
98
|
+
const end = s.indexOf('$$', i + 2);
|
|
99
|
+
if (end > -1) {
|
|
100
|
+
out += keep(s.slice(i + 2, end), true, s.slice(i, end + 2));
|
|
101
|
+
i = end + 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
} else if (delimiters.includes('$') && s[i + 1] && !/\s/.test(s[i + 1])) {
|
|
105
|
+
const end = closingDollar(s, i + 1);
|
|
106
|
+
if (end > -1) {
|
|
107
|
+
out += keep(s.slice(i + 1, end), false, s.slice(i, end + 1));
|
|
108
|
+
i = end + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
out += c;
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
return { text: out, math };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The $ that closes inline maths opened before `from`: not after a space, not before a digit, within the paragraph. */
|
|
120
|
+
function closingDollar(s, from) {
|
|
121
|
+
for (let j = from; j < s.length; j++) {
|
|
122
|
+
const c = s[j];
|
|
123
|
+
if (c === '\\') { j++; continue; }
|
|
124
|
+
if (c === '`') return -1; // maths doesn't run into code
|
|
125
|
+
if (c === '\n' && /^\n[ \t]*\n/.test(s.slice(j))) return -1;
|
|
126
|
+
if (c === '$' && !/\s/.test(s[j - 1]) && !/\d/.test(s[j + 1] ?? '') && s[j + 1] !== '$') return j;
|
|
127
|
+
}
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---- drawing the maths --------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const escape = (t) => String(t).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
134
|
+
|
|
135
|
+
/** A definition: r = 3, g := 9.81 (a single name on the left). */
|
|
136
|
+
const DEFINITION = /^\s*([A-Za-zͰ-Ͽ](?:_\{?\w+\}?)?)\s*(?::=|=)\s*([^=<>≤≥]+)$/;
|
|
137
|
+
|
|
138
|
+
function drawMath(item, options, scope) {
|
|
139
|
+
const { source, display } = item;
|
|
140
|
+
try {
|
|
141
|
+
const html = toHTML(source, { ...options.math, display, blocks: options.blocks, scope });
|
|
142
|
+
if (options.define !== false) define(source, scope);
|
|
143
|
+
return display ? `<span class="mv-md-display">${html}</span>` : `<span class="mv-md-math">${html}</span>`;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return `<span class="mv-md-error" title="${escape(error.message)}"><code>${escape(item.raw)}</code></span>`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** $r = 3$: remember r (exactly, as the calculator's text) for later {{…}}. */
|
|
150
|
+
function define(source, scope) {
|
|
151
|
+
const m = DEFINITION.exec(source);
|
|
152
|
+
if (!m || /\{\{/.test(m[2])) return;
|
|
153
|
+
try {
|
|
154
|
+
const r = calculate(m[2], scope);
|
|
155
|
+
if (typeof r.value === 'number' || typeof r.value === 'bigint') scope[m[1].replace(/[{}]/g, '')] = r.exact ? r.text : r.value;
|
|
156
|
+
} catch {
|
|
157
|
+
// not a number (x = y + 1): nothing to remember
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- the Markdown renderer ----------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
const SAFE_URL = /^(https?:|mailto:|#|\/|\.{0,2}\/|[^:]*$)/i;
|
|
164
|
+
const safeUrl = (u) => (SAFE_URL.test(u.trim()) ? escape(u.trim()) : '#');
|
|
165
|
+
|
|
166
|
+
/** Inline Markdown: code, links, images, emphasis, breaks. Placeholders pass through. */
|
|
167
|
+
function inline(text, options, sourceOf) {
|
|
168
|
+
const parts = [];
|
|
169
|
+
const hold = (html) => {
|
|
170
|
+
parts.push(html);
|
|
171
|
+
return `${parts.length - 1}`;
|
|
172
|
+
};
|
|
173
|
+
let t = text;
|
|
174
|
+
// code spans (maths placeholders inside are written back as their source)
|
|
175
|
+
t = t.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_, run, code) => hold(`<code>${escape(sourceOf(code.trim()))}</code>`));
|
|
176
|
+
// backslash escapes
|
|
177
|
+
t = t.replace(/\\([\\`*_{}[\]()#+\-.!|~$<>"'])/g, (_, ch) => hold(escape(ch)));
|
|
178
|
+
// autolinks
|
|
179
|
+
t = t.replace(/<((?:https?:\/\/|mailto:)[^\s>]+)>/g, (_, url) => hold(`<a href="${safeUrl(url)}">${escape(url)}</a>`));
|
|
180
|
+
// raw HTML (only with { html: true })
|
|
181
|
+
if (options.html) t = t.replace(/<\/?[A-Za-z][^>]*>/g, (tag) => hold(tag));
|
|
182
|
+
// images and links
|
|
183
|
+
// an address may hold balanced brackets: wiki/Function_(mathematics)
|
|
184
|
+
t = t.replace(/!\[([^\]]*)\]\(\s*((?:[^\s()]|\([^\s()]*\))+)(?:\s+"([^"]*)")?\s*\)/g, (_, alt, url, title) =>
|
|
185
|
+
hold(`<img src="${safeUrl(url)}" alt="${escape(alt)}"${title ? ` title="${escape(title)}"` : ''}>`));
|
|
186
|
+
t = t.replace(/\[([^\]]+)\]\(\s*((?:[^\s()]|\([^\s()]*\))+)(?:\s+"([^"]*)")?\s*\)/g, (_, label, url, title) =>
|
|
187
|
+
hold(`<a href="${safeUrl(url)}"${title ? ` title="${escape(title)}"` : ''}>${inline(label, options, sourceOf)}</a>`));
|
|
188
|
+
t = escape(t);
|
|
189
|
+
t = t.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '<strong>$2</strong>');
|
|
190
|
+
t = t.replace(/(^|[^*\w])\*(?=\S)([\s\S]*?\S)\*(?!\*)/g, '$1<em>$2</em>');
|
|
191
|
+
t = t.replace(/(^|[^_\w])_(?=\S)([\s\S]*?\S)_(?![_\w])/g, '$1<em>$2</em>');
|
|
192
|
+
t = t.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
|
|
193
|
+
t = t.replace(/( {2,}|\\)\n/g, '<br>\n');
|
|
194
|
+
return t.replace(/(\d+)/g, (_, n) => parts[Number(n)]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const BLOCK_START = /^( {0,3})(#{1,6}\s|>|```|~~~|([-*+]|\d{1,9}[.)])\s|(\*[ \t]*){3,}$|(-[ \t]*){3,}$|(_[ \t]*){3,}$)/;
|
|
198
|
+
const LIST_ITEM = /^( {0,3})([-*+]|\d{1,9}[.)])(\s+|$)/;
|
|
199
|
+
const TABLE_RULE = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
|
|
200
|
+
|
|
201
|
+
/** Block Markdown → HTML. */
|
|
202
|
+
function blocks(lines, options, sourceOf) {
|
|
203
|
+
const out = [];
|
|
204
|
+
let i = 0;
|
|
205
|
+
const para = (text) => inline(text, options, sourceOf);
|
|
206
|
+
while (i < lines.length) {
|
|
207
|
+
const line = lines[i];
|
|
208
|
+
if (!line.trim()) { i++; continue; }
|
|
209
|
+
// fenced code
|
|
210
|
+
const fence = /^ {0,3}(`{3,}|~{3,})\s*([^\s`]*)/.exec(line);
|
|
211
|
+
if (fence) {
|
|
212
|
+
const body = [];
|
|
213
|
+
i++;
|
|
214
|
+
while (i < lines.length && !new RegExp(`^ {0,3}${fence[1][0] === '`' ? '`' : '~'}{${fence[1].length},}\\s*$`).test(lines[i])) body.push(lines[i++]);
|
|
215
|
+
i++;
|
|
216
|
+
const lang = fence[2] ? ` class="language-${escape(fence[2])}"` : '';
|
|
217
|
+
out.push(`<pre><code${lang}>${escape(sourceOf(body.join('\n')))}</code></pre>`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// ATX heading
|
|
221
|
+
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
|
|
222
|
+
if (h) {
|
|
223
|
+
out.push(`<h${h[1].length}>${para(h[2])}</h${h[1].length}>`);
|
|
224
|
+
i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// thematic break
|
|
228
|
+
if (/^ {0,3}((\*[ \t]*){3,}|(-[ \t]*){3,}|(_[ \t]*){3,})$/.test(line)) {
|
|
229
|
+
out.push('<hr>');
|
|
230
|
+
i++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
// block quote
|
|
234
|
+
if (/^ {0,3}>/.test(line)) {
|
|
235
|
+
const body = [];
|
|
236
|
+
while (i < lines.length && lines[i].trim() && (/^ {0,3}>/.test(lines[i]) || body.length)) {
|
|
237
|
+
body.push(lines[i].replace(/^ {0,3}> ?/, ''));
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
out.push(`<blockquote>\n${blocks(body, options, sourceOf)}\n</blockquote>`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// list
|
|
244
|
+
const item = LIST_ITEM.exec(line);
|
|
245
|
+
if (item) {
|
|
246
|
+
const ordered = /\d/.test(item[2]);
|
|
247
|
+
const start = ordered ? parseInt(item[2], 10) : 1;
|
|
248
|
+
const items = [];
|
|
249
|
+
let loose = false;
|
|
250
|
+
while (i < lines.length) {
|
|
251
|
+
const m = LIST_ITEM.exec(lines[i]);
|
|
252
|
+
if (!m || /\d/.test(m[2]) !== ordered) break;
|
|
253
|
+
const indent = m[0].length;
|
|
254
|
+
const body = [lines[i].slice(indent)];
|
|
255
|
+
i++;
|
|
256
|
+
while (i < lines.length) {
|
|
257
|
+
const next = lines[i];
|
|
258
|
+
if (!next.trim()) {
|
|
259
|
+
// a blank line inside an item, or between items (then the list is loose)
|
|
260
|
+
const following = lines[i + 1];
|
|
261
|
+
if (following !== undefined && (following.startsWith(' '.repeat(Math.min(indent, 4))) && following.trim())) {
|
|
262
|
+
body.push('');
|
|
263
|
+
loose = true;
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (following !== undefined && LIST_ITEM.test(following)) loose = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
if (next.startsWith(' '.repeat(Math.min(indent, 4)))) body.push(next.slice(Math.min(indent, 4)));
|
|
271
|
+
else if (LIST_ITEM.test(next) || BLOCK_START.test(next)) break;
|
|
272
|
+
else body.push(next); // a lazy continuation line
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
items.push(body);
|
|
276
|
+
while (i < lines.length && !lines[i].trim() && lines[i + 1] !== undefined && LIST_ITEM.test(lines[i + 1])) i++;
|
|
277
|
+
}
|
|
278
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
279
|
+
const html = items.map((body) => {
|
|
280
|
+
let inner = blocks(body, options, sourceOf);
|
|
281
|
+
if (!loose) inner = inner.replace(/^<p>([\s\S]*?)<\/p>/, '$1').replace(/\n<p>([\s\S]*?)<\/p>/g, '\n$1');
|
|
282
|
+
return `<li>${inner}</li>`;
|
|
283
|
+
});
|
|
284
|
+
out.push(`<${tag}${ordered && start !== 1 ? ` start="${start}"` : ''}>\n${html.join('\n')}\n</${tag}>`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// table
|
|
288
|
+
if (line.includes('|') && lines[i + 1] !== undefined && TABLE_RULE.test(lines[i + 1]) && lines[i + 1].includes('-')) {
|
|
289
|
+
const cells = (row) => row.trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim());
|
|
290
|
+
const align = cells(lines[i + 1]).map((c) => (c.startsWith(':') && c.endsWith(':') ? 'center' : c.endsWith(':') ? 'right' : c.startsWith(':') ? 'left' : ''));
|
|
291
|
+
const cell = (tag, c, k) => `<${tag}${align[k] ? ` style="text-align:${align[k]}"` : ''}>${para(c)}</${tag}>`;
|
|
292
|
+
const head = cells(line);
|
|
293
|
+
i += 2;
|
|
294
|
+
const rows = [];
|
|
295
|
+
while (i < lines.length && lines[i].trim() && lines[i].includes('|')) rows.push(cells(lines[i++]));
|
|
296
|
+
out.push(`<table>\n<thead><tr>${head.map((c, k) => cell('th', c, k)).join('')}</tr></thead>\n<tbody>\n${rows.map((r) => `<tr>${head.map((_, k) => cell('td', r[k] ?? '', k)).join('')}</tr>`).join('\n')}\n</tbody>\n</table>`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// indented code
|
|
300
|
+
if (/^( {4}|\t)/.test(line)) {
|
|
301
|
+
const body = [];
|
|
302
|
+
while (i < lines.length && (/^( {4}|\t)/.test(lines[i]) || !lines[i].trim())) body.push(lines[i++].replace(/^( {4}|\t)/, ''));
|
|
303
|
+
while (body.length && !body[body.length - 1].trim()) body.pop();
|
|
304
|
+
out.push(`<pre><code>${escape(sourceOf(body.join('\n')))}</code></pre>`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// paragraph (or a setext heading)
|
|
308
|
+
const text = [];
|
|
309
|
+
while (i < lines.length && lines[i].trim() && !(text.length && BLOCK_START.test(lines[i]) && !/^ {0,3}[-=]+\s*$/.test(lines[i]))) {
|
|
310
|
+
if (text.length && /^ {0,3}(=+|-+)\s*$/.test(lines[i])) {
|
|
311
|
+
const level = lines[i].trim()[0] === '=' ? 1 : 2;
|
|
312
|
+
out.push(`<h${level}>${para(text.join('\n'))}</h${level}>`);
|
|
313
|
+
text.length = 0;
|
|
314
|
+
i++;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
text.push(lines[i].replace(/^ {0,3}/, ''));
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
if (text.length) out.push(`<p>${para(text.join('\n'))}</p>`);
|
|
321
|
+
}
|
|
322
|
+
return out.join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The built-in renderer: Markdown → HTML, raw HTML escaped unless options.html. */
|
|
326
|
+
export function renderMarkdown(markdown, options = {}, sourceOf = (t) => t) {
|
|
327
|
+
return blocks(String(markdown).replace(/\r\n?/g, '\n').split('\n'), options, sourceOf);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---- together ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Markdown with maths → HTML.
|
|
334
|
+
* @param {string} markdown
|
|
335
|
+
* @param {object} [options]
|
|
336
|
+
* @param {object} [options.math] options for each drawing (toHTML's: fnParens, colors …)
|
|
337
|
+
* @param {Function} [options.blocks] the {{…}} handler (default the calculator; casBlocks() for the CAS)
|
|
338
|
+
* @param {object} [options.scope] values for {{…}}: { r: 3 }
|
|
339
|
+
* @param {boolean} [options.define] remember $r = 3$ for later {{…}} (default true)
|
|
340
|
+
* @param {boolean} [options.html] let raw HTML through (default false: it is shown as text)
|
|
341
|
+
* @param {(markdown: string) => string} [options.render] another Markdown renderer
|
|
342
|
+
* @param {string[]} [options.delimiters] which of '$$', '$', '\\[', '\\(' mark maths (default all)
|
|
343
|
+
*/
|
|
344
|
+
export function markdownToHTML(markdown, options = {}) {
|
|
345
|
+
const { text, math } = extractMath(markdown, options);
|
|
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)]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Draw Markdown with maths into an element. */
|
|
358
|
+
export function renderMarkdownInto(element, markdown, options = {}) {
|
|
359
|
+
element.innerHTML = markdownToHTML(markdown, options);
|
|
360
|
+
return element;
|
|
361
|
+
}
|
package/src/mathzetta.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
} from 'mathzetta';
|
|
17
|
+
export { toSpeech, toSpeechSegments } 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,133 @@
|
|
|
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
|
+
/** Extra attributes for a node's element. */
|
|
34
|
+
attributes?: ((node: Node) => Record<string, string>) | null;
|
|
35
|
+
/** Inline style for a node's element. */
|
|
36
|
+
style?: ((node: Node) => string) | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DrawOptions extends LayoutOptions {
|
|
40
|
+
/** Colours by target: { 'x^2': 'red', 3: '#06c' } or [[target, colour], …]. */
|
|
41
|
+
colors?: Record<string | number, string> | Array<[Target, string]>;
|
|
42
|
+
/** Parts to mark with the `<prefix>-hl` class. */
|
|
43
|
+
highlight?: Target;
|
|
44
|
+
/** Variable values for `{{…}}` blocks. */
|
|
45
|
+
scope?: Record<string, unknown>;
|
|
46
|
+
/** Another `{{…}}` handler (casBlocks() from '@mathzetta/inlinemath/cas'). */
|
|
47
|
+
blocks?: BlockHandler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DEFAULTS: Required<LayoutOptions>;
|
|
51
|
+
|
|
52
|
+
/** Anything MathZetta reads, as HTML (a string; works in Node). Needs inlinemath.css on the page. */
|
|
53
|
+
export function toHTML(input: Input, options?: DrawOptions): string;
|
|
54
|
+
/** Anything MathZetta reads → a tree with node ids (`{{…}}` filled as toHTML would). */
|
|
55
|
+
export function treeOf(input: Input, options?: DrawOptions): Node;
|
|
56
|
+
/** A tree → HTML, with its height and depth in em. */
|
|
57
|
+
export function layout(tree: Node, options?: LayoutOptions): { html: string; h: number; d: number };
|
|
58
|
+
|
|
59
|
+
/** The nodes a target picks out, in tree order. */
|
|
60
|
+
export function select(tree: Node, target: Target): Node[];
|
|
61
|
+
/** A subtree's postfix as one string: two parts look the same exactly when these match. */
|
|
62
|
+
export function signature(node: Node): string;
|
|
63
|
+
export function nodeAt(tree: Node, path: number[]): Node | null;
|
|
64
|
+
export function pathOf(tree: Node, id: number): number[] | null;
|
|
65
|
+
/** Gives a tree built by hand the ids MathZetta would. */
|
|
66
|
+
export function numberTree<T extends Node>(tree: T): T;
|
|
67
|
+
/** SVG paths of the glyphs Inline Math draws, ( ) { } √ ∫ ∑ ∏: each filled or stroked, in a unit box. */
|
|
68
|
+
export const PATHS: Record<string, { fill?: string; stroke?: string }>;
|
|
69
|
+
|
|
70
|
+
export interface ClickInfo { node: Node; id: number; path: number[] | null; element: Element; event: Event }
|
|
71
|
+
|
|
72
|
+
/** A live drawing: highlight, colour, listen, step with animation. */
|
|
73
|
+
export class View {
|
|
74
|
+
constructor(element: Element, input: Input, options?: DrawOptions);
|
|
75
|
+
readonly element: Element;
|
|
76
|
+
readonly tree: Node;
|
|
77
|
+
/** Show a new expression; with animate, matching parts glide over and new ones fade in. */
|
|
78
|
+
set(input: Input, options?: { animate?: boolean; duration?: number; easing?: string }): this;
|
|
79
|
+
nodes(target: Target): Node[];
|
|
80
|
+
elements(target: Target): Element[];
|
|
81
|
+
elementOf(node: Node): Element | null;
|
|
82
|
+
highlight(target: Target, className?: string): this;
|
|
83
|
+
unhighlight(target?: Target | null, className?: string): this;
|
|
84
|
+
color(target: Target, colour: string): this;
|
|
85
|
+
/** Listen for an event on a part; returns a function that stops listening. */
|
|
86
|
+
on(type: string, fn: (info: ClickInfo) => void): () => void;
|
|
87
|
+
/** The expression read aloud in pieces, each tagged with the node it describes. */
|
|
88
|
+
segments(options?: SpeechOptions): { text: string; id?: number }[];
|
|
89
|
+
showSegment(segment: { id?: number } | null, className?: string): this;
|
|
90
|
+
}
|
|
91
|
+
/** Draw into an element and return the live view. */
|
|
92
|
+
export function render(element: Element, input: Input, options?: DrawOptions): View;
|
|
93
|
+
|
|
94
|
+
// ---- inlinemath/markdown --------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
export interface MarkdownOptions {
|
|
97
|
+
/** Options for each drawing (toHTML's). */
|
|
98
|
+
math?: DrawOptions;
|
|
99
|
+
/** The `{{…}}` handler (default the calculator; casBlocks() for the CAS). */
|
|
100
|
+
blocks?: BlockHandler;
|
|
101
|
+
/** Values for `{{…}}`. */
|
|
102
|
+
scope?: Record<string, unknown>;
|
|
103
|
+
/** Remember $r = 3$ for later `{{…}}` (default true). */
|
|
104
|
+
define?: boolean;
|
|
105
|
+
/** Let raw HTML through (default false: it is shown as text). */
|
|
106
|
+
html?: boolean;
|
|
107
|
+
/** Another Markdown renderer; the maths is taken out first and put back into its output. */
|
|
108
|
+
render?: (markdown: string) => string;
|
|
109
|
+
/** Which of '$$', '$', '\\[', '\\(' mark maths (default all). */
|
|
110
|
+
delimiters?: string[];
|
|
111
|
+
}
|
|
112
|
+
export function markdownToHTML(markdown: string, options?: MarkdownOptions): string;
|
|
113
|
+
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 }> };
|
|
115
|
+
export function renderMarkdown(markdown: string, options?: MarkdownOptions): string;
|
|
116
|
+
|
|
117
|
+
// ---- inlinemath/cas (needs @mathzetta/cas) --------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** A step from the CAS (@mathzetta/cas's CASStep fits): path null for a step standing for hidden ones. */
|
|
120
|
+
export interface CASStep { id: string; name: string; from: string; to: string; path?: number[] | null; explanation?: string }
|
|
121
|
+
/** @mathzetta/cas reading and writing MathZetta text (its full API: @mathzetta/cas/docs/cas.md). */
|
|
122
|
+
export type CAS = Record<string, any>;
|
|
123
|
+
export function mathCAS(options?: { rules?: unknown[]; names?: string[]; complex?: boolean; [option: string]: unknown }): CAS;
|
|
124
|
+
/** A `{{…}}` handler: toHTML(input, { blocks: casBlocks() }). `last` holds the latest CAS result. */
|
|
125
|
+
export function casBlocks(options?: { cas?: CAS }): BlockHandler & { last?: unknown };
|
|
126
|
+
/** Steps as an ordered list, each rewrite highlighted, with its explanation. */
|
|
127
|
+
export function stepsHTML(steps: CASStep[], options?: DrawOptions): string;
|
|
128
|
+
/** A CAS node → MathZetta postfix tokens. */
|
|
129
|
+
export function tokensOf(node: unknown): string[];
|
|
130
|
+
|
|
131
|
+
// ---- the calculator (inlinemath/lite-calc re-exports it) -------------------------------------
|
|
132
|
+
|
|
133
|
+
export { calculateBlock, calculate } from 'mathzetta/calculate';
|