@texuli/deck 0.0.0-stage → 0.4.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 +182 -2
- package/dist/authoring/builder.d.ts +351 -0
- package/dist/authoring/builder.js +64 -0
- package/dist/authoring/index.d.ts +6 -0
- package/dist/authoring/index.js +6 -0
- package/dist/authoring/lint.d.ts +36 -0
- package/dist/authoring/lint.js +325 -0
- package/dist/authoring/outlines.d.ts +34 -0
- package/dist/authoring/outlines.js +255 -0
- package/dist/authoring/patch.d.ts +23 -0
- package/dist/authoring/patch.js +166 -0
- package/dist/authoring/select-layouts.d.ts +19 -0
- package/dist/authoring/select-layouts.js +90 -0
- package/dist/engine/auto-slide.d.ts +32 -0
- package/dist/engine/auto-slide.js +67 -0
- package/dist/engine/canvas.d.ts +56 -0
- package/dist/engine/canvas.js +48 -0
- package/dist/engine/controller.d.ts +126 -0
- package/dist/engine/controller.js +248 -0
- package/dist/engine/controls.d.ts +128 -0
- package/dist/engine/controls.js +460 -0
- package/dist/engine/hash.d.ts +42 -0
- package/dist/engine/hash.js +86 -0
- package/dist/engine/index.d.ts +88 -0
- package/dist/engine/index.js +88 -0
- package/dist/engine/messages.d.ts +138 -0
- package/dist/engine/messages.js +140 -0
- package/dist/engine/motion.d.ts +64 -0
- package/dist/engine/motion.js +112 -0
- package/dist/engine/navigation.d.ts +87 -0
- package/dist/engine/navigation.js +156 -0
- package/dist/engine/presenter.d.ts +58 -0
- package/dist/engine/presenter.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/markdown/attributes.d.ts +24 -0
- package/dist/markdown/attributes.js +94 -0
- package/dist/markdown/attrs-closed.d.ts +52 -0
- package/dist/markdown/attrs-closed.js +520 -0
- package/dist/markdown/blocks.d.ts +112 -0
- package/dist/markdown/blocks.js +766 -0
- package/dist/markdown/frontmatter.d.ts +30 -0
- package/dist/markdown/frontmatter.js +165 -0
- package/dist/markdown/index.d.ts +29 -0
- package/dist/markdown/index.js +29 -0
- package/dist/markdown/issues.d.ts +68 -0
- package/dist/markdown/issues.js +83 -0
- package/dist/markdown/json.d.ts +21 -0
- package/dist/markdown/json.js +236 -0
- package/dist/markdown/limits.d.ts +32 -0
- package/dist/markdown/limits.js +32 -0
- package/dist/markdown/parse.d.ts +31 -0
- package/dist/markdown/parse.js +283 -0
- package/dist/markdown/scan.d.ts +26 -0
- package/dist/markdown/scan.js +138 -0
- package/dist/markdown/slides.d.ts +32 -0
- package/dist/markdown/slides.js +854 -0
- package/dist/markdown/text.d.ts +76 -0
- package/dist/markdown/text.js +447 -0
- package/dist/markdown/twin.d.ts +41 -0
- package/dist/markdown/twin.js +374 -0
- package/dist/markdown/write.d.ts +17 -0
- package/dist/markdown/write.js +688 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +14 -0
- package/dist/math/parse.d.ts +9 -0
- package/dist/math/parse.js +648 -0
- package/dist/math/symbols.d.ts +44 -0
- package/dist/math/symbols.js +333 -0
- package/dist/math/types.d.ts +59 -0
- package/dist/math/types.js +69 -0
- package/dist/model/backgrounds/contrast.d.ts +77 -0
- package/dist/model/backgrounds/contrast.js +186 -0
- package/dist/model/backgrounds/index.d.ts +7 -0
- package/dist/model/backgrounds/index.js +7 -0
- package/dist/model/backgrounds/resolve.d.ts +86 -0
- package/dist/model/backgrounds/resolve.js +215 -0
- package/dist/model/backgrounds/schema.d.ts +178 -0
- package/dist/model/backgrounds/schema.js +203 -0
- package/dist/model/codes.d.ts +73 -0
- package/dist/model/codes.js +67 -0
- package/dist/model/deck.d.ts +392 -0
- package/dist/model/deck.js +338 -0
- package/dist/model/index.d.ts +19 -0
- package/dist/model/index.js +19 -0
- package/dist/model/layouts/catalog.d.ts +433 -0
- package/dist/model/layouts/catalog.js +495 -0
- package/dist/model/layouts/index.d.ts +5 -0
- package/dist/model/layouts/index.js +5 -0
- package/dist/model/layouts/props.d.ts +302 -0
- package/dist/model/layouts/props.js +272 -0
- package/dist/model/loader.d.ts +37 -0
- package/dist/model/loader.js +317 -0
- package/dist/model/migrate.d.ts +34 -0
- package/dist/model/migrate.js +453 -0
- package/dist/model/motion/index.d.ts +6 -0
- package/dist/model/motion/index.js +6 -0
- package/dist/model/motion/transition.d.ts +96 -0
- package/dist/model/motion/transition.js +127 -0
- package/dist/model/parse.d.ts +57 -0
- package/dist/model/parse.js +307 -0
- package/dist/model/plain.d.ts +40 -0
- package/dist/model/plain.js +275 -0
- package/dist/model/shared.d.ts +63 -0
- package/dist/model/shared.js +86 -0
- package/dist/model/steps/index.d.ts +7 -0
- package/dist/model/steps/index.js +7 -0
- package/dist/model/steps/phase.d.ts +34 -0
- package/dist/model/steps/phase.js +50 -0
- package/dist/model/steps/slide-steps.d.ts +129 -0
- package/dist/model/steps/slide-steps.js +256 -0
- package/dist/model/style/index.d.ts +8 -0
- package/dist/model/style/index.js +8 -0
- package/dist/model/style/resolve.d.ts +87 -0
- package/dist/model/style/resolve.js +174 -0
- package/dist/model/style/schema.d.ts +85 -0
- package/dist/model/style/schema.js +53 -0
- package/dist/model/style/theme.d.ts +69 -0
- package/dist/model/style/theme.js +84 -0
- package/dist/schema/builders.d.ts +146 -0
- package/dist/schema/builders.js +285 -0
- package/dist/schema/diagnostics.d.ts +78 -0
- package/dist/schema/diagnostics.js +81 -0
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.js +14 -0
- package/dist/schema/internal.d.ts +40 -0
- package/dist/schema/internal.js +209 -0
- package/dist/schema/json-schema.d.ts +12 -0
- package/dist/schema/json-schema.js +213 -0
- package/dist/schema/types.d.ts +166 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validate.d.ts +51 -0
- package/dist/schema/validate.js +765 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/templates.d.ts +11 -0
- package/dist/templates/templates.js +47 -0
- package/package.json +60 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/deck/math: the owned TeX subset → MathML node tree (ADR-009, math
|
|
3
|
+
* policy). No third-party code and no markup strings: `texToMath` returns a
|
|
4
|
+
* tree of MathML Core elements from a closed allowlist (`MATH_TAGS`,
|
|
5
|
+
* `MATH_ATTRIBUTES`), which a renderer walks. Commands that would link,
|
|
6
|
+
* color, load, label or define macros (`\href`, `\color`, `\html*`, `\def`…)
|
|
7
|
+
* are refused, and bounds (`MATH_LIMITS`) keep hostile input cheap.
|
|
8
|
+
*
|
|
9
|
+
* Block math (`$$…$$`) is converted by the deck Markdown dialect; inline
|
|
10
|
+
* `$…$` stays inside text blocks as written (an `@texuli/blocks` opt-in is a
|
|
11
|
+
* follow-up).
|
|
12
|
+
*/
|
|
13
|
+
export { mathText, texToMath } from './parse.js';
|
|
14
|
+
export { MATH_ATTRIBUTES, MATH_CODES, MATH_LIMITS, MATH_TAGS, type MathAttribute, type MathCode, type MathIssue, type MathNode, type MathResult, type MathTag, type TexToMathOptions, } from './types.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/deck/math: the owned TeX subset → MathML node tree (ADR-009, math
|
|
3
|
+
* policy). No third-party code and no markup strings: `texToMath` returns a
|
|
4
|
+
* tree of MathML Core elements from a closed allowlist (`MATH_TAGS`,
|
|
5
|
+
* `MATH_ATTRIBUTES`), which a renderer walks. Commands that would link,
|
|
6
|
+
* color, load, label or define macros (`\href`, `\color`, `\html*`, `\def`…)
|
|
7
|
+
* are refused, and bounds (`MATH_LIMITS`) keep hostile input cheap.
|
|
8
|
+
*
|
|
9
|
+
* Block math (`$$…$$`) is converted by the deck Markdown dialect; inline
|
|
10
|
+
* `$…$` stays inside text blocks as written (an `@texuli/blocks` opt-in is a
|
|
11
|
+
* follow-up).
|
|
12
|
+
*/
|
|
13
|
+
export { mathText, texToMath } from "./parse.js";
|
|
14
|
+
export { MATH_ATTRIBUTES, MATH_CODES, MATH_LIMITS, MATH_TAGS, } from "./types.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MathNode, type MathResult, type TexToMathOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a TeX formula (without its `$`/`$$` delimiters) to a MathML node
|
|
4
|
+
* tree. Never throws: an unsupported command, a syntax error or a bound
|
|
5
|
+
* gives `ok: false`, one issue and a node that shows the source as text.
|
|
6
|
+
*/
|
|
7
|
+
export declare function texToMath(tex: string, options?: TexToMathOptions): MathResult;
|
|
8
|
+
/** The plain text of a math tree (for accessible names, text twins and PPTX). */
|
|
9
|
+
export declare function mathText(node: MathNode): string;
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owned TeX subset → MathML node tree. A small recursive-descent reader
|
|
3
|
+
* over the source with explicit bounds (`MATH_LIMITS`): source length,
|
|
4
|
+
* nesting depth and output nodes. Unknown and refused commands fail the
|
|
5
|
+
* whole formula, which then shows its source as text.
|
|
6
|
+
*/
|
|
7
|
+
import { ACCENTS, BIG_OPERATORS, DELIMITERS, ENVIRONMENTS, ESCAPED, FONTS, FUNCTIONS, GREEK, IDENTIFIERS, OPERATORS, REFUSED, SIZING, SPACES, UNDER_ACCENTS, } from "./symbols.js";
|
|
8
|
+
import { MATH_CODES, MATH_LIMITS, } from "./types.js";
|
|
9
|
+
class TexError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
offset;
|
|
12
|
+
constructor(code, message, offset) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.offset = offset;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Characters read as operators (`-` becomes a minus sign). */
|
|
19
|
+
const OPERATOR_CHARS = Object.freeze({
|
|
20
|
+
'+': '+',
|
|
21
|
+
'-': '−',
|
|
22
|
+
'*': '∗',
|
|
23
|
+
'=': '=',
|
|
24
|
+
'<': '<',
|
|
25
|
+
'>': '>',
|
|
26
|
+
'/': '/',
|
|
27
|
+
'!': '!',
|
|
28
|
+
'(': '(',
|
|
29
|
+
')': ')',
|
|
30
|
+
'[': '[',
|
|
31
|
+
']': ']',
|
|
32
|
+
'|': '|',
|
|
33
|
+
',': ',',
|
|
34
|
+
';': ';',
|
|
35
|
+
':': ':',
|
|
36
|
+
'.': '.',
|
|
37
|
+
'?': '?',
|
|
38
|
+
'@': '@',
|
|
39
|
+
});
|
|
40
|
+
/** `\not` before these reads as the negated relation. */
|
|
41
|
+
const NEGATED = Object.freeze({
|
|
42
|
+
'=': '≠',
|
|
43
|
+
'<': '≮',
|
|
44
|
+
'>': '≯',
|
|
45
|
+
in: '∉',
|
|
46
|
+
subset: '⊄',
|
|
47
|
+
supset: '⊅',
|
|
48
|
+
equiv: '≢',
|
|
49
|
+
sim: '≁',
|
|
50
|
+
approx: '≉',
|
|
51
|
+
le: '≰',
|
|
52
|
+
ge: '≱',
|
|
53
|
+
});
|
|
54
|
+
const TEXT_COMMANDS = Object.freeze({
|
|
55
|
+
text: undefined,
|
|
56
|
+
textrm: undefined,
|
|
57
|
+
textnormal: undefined,
|
|
58
|
+
mbox: undefined,
|
|
59
|
+
textup: undefined,
|
|
60
|
+
textbf: 'bold',
|
|
61
|
+
textit: 'italic',
|
|
62
|
+
textsf: 'sans-serif',
|
|
63
|
+
texttt: 'monospace',
|
|
64
|
+
});
|
|
65
|
+
class TexParser {
|
|
66
|
+
src;
|
|
67
|
+
display;
|
|
68
|
+
i = 0;
|
|
69
|
+
nodes = 0;
|
|
70
|
+
depth = 0;
|
|
71
|
+
constructor(src, display) {
|
|
72
|
+
this.src = src;
|
|
73
|
+
this.display = display;
|
|
74
|
+
}
|
|
75
|
+
fail(code, message, offset = this.i) {
|
|
76
|
+
throw new TexError(code, message, offset);
|
|
77
|
+
}
|
|
78
|
+
make(node) {
|
|
79
|
+
this.nodes++;
|
|
80
|
+
if (this.nodes > MATH_LIMITS.nodes) {
|
|
81
|
+
this.fail(MATH_CODES.limit, `A formula produces at most ${MATH_LIMITS.nodes} nodes.`);
|
|
82
|
+
}
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
token(tag, text, attrs) {
|
|
86
|
+
return this.make(attrs ? { tag, text, attrs } : { tag, text });
|
|
87
|
+
}
|
|
88
|
+
enter() {
|
|
89
|
+
this.depth++;
|
|
90
|
+
if (this.depth > MATH_LIMITS.depth) {
|
|
91
|
+
this.fail(MATH_CODES.limit, `Groups nest at most ${MATH_LIMITS.depth} deep.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
skipSpace() {
|
|
95
|
+
while (this.i < this.src.length) {
|
|
96
|
+
const c = this.src[this.i];
|
|
97
|
+
if (c === ' ' || c === '\t' || c === '\n')
|
|
98
|
+
this.i++;
|
|
99
|
+
else if (c === '%') {
|
|
100
|
+
while (this.i < this.src.length && this.src[this.i] !== '\n')
|
|
101
|
+
this.i++;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The command name at `this.i` (just after a backslash), without consuming it. */
|
|
108
|
+
peekCommand(at = this.i) {
|
|
109
|
+
if (this.src[at] !== '\\')
|
|
110
|
+
return '';
|
|
111
|
+
let end = at + 1;
|
|
112
|
+
while (end < this.src.length && /[A-Za-z]/.test(this.src[end] ?? ''))
|
|
113
|
+
end++;
|
|
114
|
+
if (end === at + 1)
|
|
115
|
+
return this.src[at + 1] ?? '';
|
|
116
|
+
return this.src.slice(at + 1, end);
|
|
117
|
+
}
|
|
118
|
+
readCommand() {
|
|
119
|
+
const name = this.peekCommand();
|
|
120
|
+
this.i += 1 + Math.max(1, name.length);
|
|
121
|
+
return name;
|
|
122
|
+
}
|
|
123
|
+
atStop(stop) {
|
|
124
|
+
this.skipSpace();
|
|
125
|
+
if (this.i >= this.src.length)
|
|
126
|
+
return true;
|
|
127
|
+
const c = this.src[this.i];
|
|
128
|
+
if (c === '}')
|
|
129
|
+
return true;
|
|
130
|
+
if (stop === ']' && c === ']')
|
|
131
|
+
return true;
|
|
132
|
+
if (c === '&')
|
|
133
|
+
return stop === 'cell' ? true : this.fail(MATH_CODES.syntax, '`&` is only for matrix cells.');
|
|
134
|
+
if (c === '\\') {
|
|
135
|
+
const name = this.peekCommand();
|
|
136
|
+
if (name === 'right')
|
|
137
|
+
return true;
|
|
138
|
+
if (name === 'end')
|
|
139
|
+
return true;
|
|
140
|
+
if (name === '\\' && stop === 'cell')
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
list(stop) {
|
|
146
|
+
const out = [];
|
|
147
|
+
while (!this.atStop(stop)) {
|
|
148
|
+
if (this.src[this.i] === '\\' && this.peekCommand() === '\\') {
|
|
149
|
+
// A line break outside a matrix: no visible effect in inline MathML.
|
|
150
|
+
this.i += 2;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const atom = this.atom();
|
|
154
|
+
if (atom === null)
|
|
155
|
+
continue;
|
|
156
|
+
out.push(this.scripts(atom));
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
row(children) {
|
|
161
|
+
if (children.length === 1 && children[0])
|
|
162
|
+
return children[0];
|
|
163
|
+
return this.make({ tag: 'mrow', children });
|
|
164
|
+
}
|
|
165
|
+
group() {
|
|
166
|
+
const start = this.i;
|
|
167
|
+
this.i++; // {
|
|
168
|
+
this.enter();
|
|
169
|
+
const children = this.list('}');
|
|
170
|
+
this.depth--;
|
|
171
|
+
if (this.src[this.i] !== '}')
|
|
172
|
+
this.fail(MATH_CODES.syntax, 'A `{` is never closed.', start);
|
|
173
|
+
this.i++;
|
|
174
|
+
return this.row(children);
|
|
175
|
+
}
|
|
176
|
+
/** One argument: a braced group or a single token. */
|
|
177
|
+
argument(name) {
|
|
178
|
+
this.skipSpace();
|
|
179
|
+
if (this.i >= this.src.length || this.src[this.i] === '}') {
|
|
180
|
+
this.fail(MATH_CODES.syntax, `\\${name} needs an argument.`);
|
|
181
|
+
}
|
|
182
|
+
if (this.src[this.i] === '{')
|
|
183
|
+
return this.group();
|
|
184
|
+
const c = this.src[this.i] ?? '';
|
|
185
|
+
if (/[0-9]/.test(c)) {
|
|
186
|
+
this.i++;
|
|
187
|
+
return this.token('mn', c);
|
|
188
|
+
}
|
|
189
|
+
const atom = this.atom();
|
|
190
|
+
if (atom === null)
|
|
191
|
+
this.fail(MATH_CODES.syntax, `\\${name} needs an argument.`);
|
|
192
|
+
return atom.node;
|
|
193
|
+
}
|
|
194
|
+
/** The raw text of a braced argument (for `\text`, `\operatorname`, environment names). */
|
|
195
|
+
rawGroup(name) {
|
|
196
|
+
this.skipSpace();
|
|
197
|
+
if (this.src[this.i] !== '{')
|
|
198
|
+
this.fail(MATH_CODES.syntax, `\\${name} needs {…}.`);
|
|
199
|
+
const start = this.i;
|
|
200
|
+
this.i++;
|
|
201
|
+
let level = 1;
|
|
202
|
+
let out = '';
|
|
203
|
+
while (this.i < this.src.length) {
|
|
204
|
+
const c = this.src[this.i] ?? '';
|
|
205
|
+
if (c === '\\') {
|
|
206
|
+
const next = this.src[this.i + 1] ?? '';
|
|
207
|
+
if (Object.hasOwn(ESCAPED, next) && next !== '|') {
|
|
208
|
+
out += next;
|
|
209
|
+
this.i += 2;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (next === ' ') {
|
|
213
|
+
out += ' ';
|
|
214
|
+
this.i += 2;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
this.fail(MATH_CODES.unsupported, `Commands are not read inside \\${name}.`);
|
|
218
|
+
}
|
|
219
|
+
if (c === '{')
|
|
220
|
+
level++;
|
|
221
|
+
else if (c === '}') {
|
|
222
|
+
level--;
|
|
223
|
+
if (level === 0) {
|
|
224
|
+
this.i++;
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
out += c;
|
|
229
|
+
this.i++;
|
|
230
|
+
}
|
|
231
|
+
return this.fail(MATH_CODES.syntax, 'A `{` is never closed.', start);
|
|
232
|
+
}
|
|
233
|
+
delimiter(after) {
|
|
234
|
+
this.skipSpace();
|
|
235
|
+
const c = this.src[this.i] ?? '';
|
|
236
|
+
if (c === '\\') {
|
|
237
|
+
const name = this.peekCommand();
|
|
238
|
+
const key = `\\${name}`;
|
|
239
|
+
if (Object.hasOwn(DELIMITERS, key)) {
|
|
240
|
+
this.readCommand();
|
|
241
|
+
return DELIMITERS[key] ?? '';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (Object.hasOwn(DELIMITERS, c)) {
|
|
245
|
+
this.i++;
|
|
246
|
+
return DELIMITERS[c] ?? '';
|
|
247
|
+
}
|
|
248
|
+
return this.fail(MATH_CODES.syntax, `\\${after} needs a delimiter such as ( [ \\{ | or .`);
|
|
249
|
+
}
|
|
250
|
+
fence(text) {
|
|
251
|
+
if (text === '')
|
|
252
|
+
return null;
|
|
253
|
+
return this.token('mo', text, { fence: 'true', stretchy: 'true' });
|
|
254
|
+
}
|
|
255
|
+
atom() {
|
|
256
|
+
const start = this.i;
|
|
257
|
+
const c = this.src[this.i] ?? '';
|
|
258
|
+
if (c === '{')
|
|
259
|
+
return { node: this.group(), limits: false };
|
|
260
|
+
if (c === '^' || c === '_') {
|
|
261
|
+
// A script with no base: an empty base row.
|
|
262
|
+
return { node: this.make({ tag: 'mrow', children: [] }), limits: false };
|
|
263
|
+
}
|
|
264
|
+
if (c === "'") {
|
|
265
|
+
this.i++;
|
|
266
|
+
return { node: this.token('mo', '′'), limits: false };
|
|
267
|
+
}
|
|
268
|
+
if (/[0-9]/.test(c)) {
|
|
269
|
+
const match = /^[0-9]+(?:\.[0-9]+)?/.exec(this.src.slice(this.i, this.i + 64));
|
|
270
|
+
const digits = match?.[0] ?? c;
|
|
271
|
+
this.i += digits.length;
|
|
272
|
+
return { node: this.token('mn', digits), limits: false };
|
|
273
|
+
}
|
|
274
|
+
if (c === '\\')
|
|
275
|
+
return this.command(start);
|
|
276
|
+
if (c === '~') {
|
|
277
|
+
this.i++;
|
|
278
|
+
return { node: this.make({ tag: 'mspace', attrs: { width: '0.3333em' } }), limits: false };
|
|
279
|
+
}
|
|
280
|
+
if (c === '#' || c === '$') {
|
|
281
|
+
this.fail(MATH_CODES.syntax, `\`${c}\` is not allowed in a formula.`);
|
|
282
|
+
}
|
|
283
|
+
this.i += c.length;
|
|
284
|
+
if (Object.hasOwn(OPERATOR_CHARS, c)) {
|
|
285
|
+
return { node: this.token('mo', OPERATOR_CHARS[c] ?? c), limits: false };
|
|
286
|
+
}
|
|
287
|
+
const code = this.src.codePointAt(start) ?? 0;
|
|
288
|
+
const char = String.fromCodePoint(code);
|
|
289
|
+
this.i = start + char.length;
|
|
290
|
+
if (/\p{L}/u.test(char))
|
|
291
|
+
return { node: this.token('mi', char), limits: false };
|
|
292
|
+
return { node: this.token('mo', char), limits: false };
|
|
293
|
+
}
|
|
294
|
+
command(start) {
|
|
295
|
+
const name = this.readCommand();
|
|
296
|
+
if (name === '')
|
|
297
|
+
this.fail(MATH_CODES.syntax, 'A formula cannot end with `\\`.', start);
|
|
298
|
+
if (REFUSED.has(name)) {
|
|
299
|
+
this.fail(MATH_CODES.unsupported, `\\${name} is refused: math cannot link, color, load, label or define macros.`, start);
|
|
300
|
+
}
|
|
301
|
+
if (Object.hasOwn(GREEK, name)) {
|
|
302
|
+
const upper = name[0] !== undefined && name[0] === name[0].toUpperCase();
|
|
303
|
+
return {
|
|
304
|
+
node: this.token('mi', GREEK[name] ?? '', upper ? { mathvariant: 'normal' } : undefined),
|
|
305
|
+
limits: false,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (Object.hasOwn(IDENTIFIERS, name)) {
|
|
309
|
+
return { node: this.token('mi', IDENTIFIERS[name] ?? ''), limits: false };
|
|
310
|
+
}
|
|
311
|
+
if (Object.hasOwn(OPERATORS, name)) {
|
|
312
|
+
return { node: this.token('mo', OPERATORS[name] ?? ''), limits: false };
|
|
313
|
+
}
|
|
314
|
+
if (Object.hasOwn(ESCAPED, name)) {
|
|
315
|
+
return { node: this.token('mo', ESCAPED[name] ?? ''), limits: false };
|
|
316
|
+
}
|
|
317
|
+
if (Object.hasOwn(SPACES, name)) {
|
|
318
|
+
return {
|
|
319
|
+
node: this.make({ tag: 'mspace', attrs: { width: SPACES[name] ?? '0' } }),
|
|
320
|
+
limits: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (Object.hasOwn(BIG_OPERATORS, name)) {
|
|
324
|
+
const op = BIG_OPERATORS[name];
|
|
325
|
+
return {
|
|
326
|
+
node: this.token('mo', op?.text ?? '', { largeop: 'true', movablelimits: 'false' }),
|
|
327
|
+
limits: op?.integral !== true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (Object.hasOwn(FUNCTIONS, name)) {
|
|
331
|
+
return { node: this.token('mi', name), limits: FUNCTIONS[name]?.limits === true };
|
|
332
|
+
}
|
|
333
|
+
if (name === 'operatorname') {
|
|
334
|
+
const text = this.rawGroup(name).trim();
|
|
335
|
+
if (!/^[A-Za-z][A-Za-z0-9 ]{0,31}$/.test(text)) {
|
|
336
|
+
this.fail(MATH_CODES.syntax, '\\operatorname takes letters only.', start);
|
|
337
|
+
}
|
|
338
|
+
return { node: this.token('mi', text, { mathvariant: 'normal' }), limits: false };
|
|
339
|
+
}
|
|
340
|
+
if (Object.hasOwn(TEXT_COMMANDS, name)) {
|
|
341
|
+
const text = this.rawGroup(name);
|
|
342
|
+
const variant = TEXT_COMMANDS[name];
|
|
343
|
+
return {
|
|
344
|
+
node: this.token('mtext', text, variant ? { mathvariant: variant } : undefined),
|
|
345
|
+
limits: false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (name === 'frac' || name === 'dfrac' || name === 'tfrac' || name === 'cfrac') {
|
|
349
|
+
this.enter();
|
|
350
|
+
const top = this.argument(name);
|
|
351
|
+
const bottom = this.argument(name);
|
|
352
|
+
this.depth--;
|
|
353
|
+
const frac = this.make({ tag: 'mfrac', children: [top, bottom] });
|
|
354
|
+
if (name === 'frac')
|
|
355
|
+
return { node: frac, limits: false };
|
|
356
|
+
return {
|
|
357
|
+
node: this.make({
|
|
358
|
+
tag: 'mstyle',
|
|
359
|
+
attrs: { displaystyle: name === 'tfrac' ? 'false' : 'true' },
|
|
360
|
+
children: [frac],
|
|
361
|
+
}),
|
|
362
|
+
limits: false,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (name === 'binom' || name === 'dbinom' || name === 'tbinom') {
|
|
366
|
+
this.enter();
|
|
367
|
+
const top = this.argument(name);
|
|
368
|
+
const bottom = this.argument(name);
|
|
369
|
+
this.depth--;
|
|
370
|
+
const frac = this.make({
|
|
371
|
+
tag: 'mfrac',
|
|
372
|
+
attrs: { linethickness: '0' },
|
|
373
|
+
children: [top, bottom],
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
node: this.make({
|
|
377
|
+
tag: 'mrow',
|
|
378
|
+
children: [this.token('mo', '('), frac, this.token('mo', ')')],
|
|
379
|
+
}),
|
|
380
|
+
limits: false,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (name === 'sqrt') {
|
|
384
|
+
this.enter();
|
|
385
|
+
this.skipSpace();
|
|
386
|
+
let index = null;
|
|
387
|
+
if (this.src[this.i] === '[') {
|
|
388
|
+
const open = this.i;
|
|
389
|
+
this.i++;
|
|
390
|
+
const children = this.list(']');
|
|
391
|
+
if (this.src[this.i] !== ']')
|
|
392
|
+
this.fail(MATH_CODES.syntax, 'A `[` is never closed.', open);
|
|
393
|
+
this.i++;
|
|
394
|
+
index = this.row(children);
|
|
395
|
+
}
|
|
396
|
+
const base = this.argument(name);
|
|
397
|
+
this.depth--;
|
|
398
|
+
return {
|
|
399
|
+
node: index
|
|
400
|
+
? this.make({ tag: 'mroot', children: [base, index] })
|
|
401
|
+
: this.make({ tag: 'msqrt', children: [base] }),
|
|
402
|
+
limits: false,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (Object.hasOwn(ACCENTS, name)) {
|
|
406
|
+
this.enter();
|
|
407
|
+
const base = this.argument(name);
|
|
408
|
+
this.depth--;
|
|
409
|
+
const stretchy = name.startsWith('over') || name.startsWith('wide') ? 'true' : 'false';
|
|
410
|
+
return {
|
|
411
|
+
node: this.make({
|
|
412
|
+
tag: 'mover',
|
|
413
|
+
attrs: { accent: 'true' },
|
|
414
|
+
children: [base, this.token('mo', ACCENTS[name] ?? '', { stretchy })],
|
|
415
|
+
}),
|
|
416
|
+
limits: false,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (Object.hasOwn(UNDER_ACCENTS, name)) {
|
|
420
|
+
this.enter();
|
|
421
|
+
const base = this.argument(name);
|
|
422
|
+
this.depth--;
|
|
423
|
+
return {
|
|
424
|
+
node: this.make({
|
|
425
|
+
tag: 'munder',
|
|
426
|
+
attrs: { accentunder: 'true' },
|
|
427
|
+
children: [base, this.token('mo', UNDER_ACCENTS[name] ?? '', { stretchy: 'true' })],
|
|
428
|
+
}),
|
|
429
|
+
limits: false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (Object.hasOwn(FONTS, name)) {
|
|
433
|
+
this.enter();
|
|
434
|
+
const base = this.argument(name);
|
|
435
|
+
this.depth--;
|
|
436
|
+
applyVariant(base, FONTS[name] ?? 'normal');
|
|
437
|
+
return { node: base, limits: false };
|
|
438
|
+
}
|
|
439
|
+
if (name === 'left') {
|
|
440
|
+
const open = this.delimiter(name);
|
|
441
|
+
this.enter();
|
|
442
|
+
const children = this.list('right');
|
|
443
|
+
this.depth--;
|
|
444
|
+
if (this.peekCommand() !== 'right')
|
|
445
|
+
this.fail(MATH_CODES.syntax, '\\left has no \\right.', start);
|
|
446
|
+
this.readCommand();
|
|
447
|
+
const close = this.delimiter('right');
|
|
448
|
+
const parts = [this.fence(open), ...children, this.fence(close)].filter((node) => node !== null);
|
|
449
|
+
return { node: this.make({ tag: 'mrow', children: parts }), limits: false };
|
|
450
|
+
}
|
|
451
|
+
if (name === 'right')
|
|
452
|
+
this.fail(MATH_CODES.syntax, '\\right has no \\left.', start);
|
|
453
|
+
if (SIZING.has(name)) {
|
|
454
|
+
const text = this.delimiter(name);
|
|
455
|
+
return { node: this.token('mo', text, { stretchy: 'false' }), limits: false };
|
|
456
|
+
}
|
|
457
|
+
if (name === 'not') {
|
|
458
|
+
this.skipSpace();
|
|
459
|
+
const next = this.src[this.i] === '\\' ? this.peekCommand() : (this.src[this.i] ?? '');
|
|
460
|
+
if (!Object.hasOwn(NEGATED, next)) {
|
|
461
|
+
this.fail(MATH_CODES.unsupported, '\\not works before =, <, >, \\in, \\subset, \\equiv…', start);
|
|
462
|
+
}
|
|
463
|
+
this.i += this.src[this.i] === '\\' ? 1 + next.length : 1;
|
|
464
|
+
return { node: this.token('mo', NEGATED[next] ?? ''), limits: false };
|
|
465
|
+
}
|
|
466
|
+
if (name === 'begin')
|
|
467
|
+
return { node: this.environment(start), limits: false };
|
|
468
|
+
if (name === 'end')
|
|
469
|
+
this.fail(MATH_CODES.syntax, '\\end has no \\begin.', start);
|
|
470
|
+
if (name === 'limits' ||
|
|
471
|
+
name === 'nolimits' ||
|
|
472
|
+
name === 'displaystyle' ||
|
|
473
|
+
name === 'textstyle' ||
|
|
474
|
+
name === 'scriptstyle' ||
|
|
475
|
+
name === 'nonumber' ||
|
|
476
|
+
name === 'notag') {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return this.fail(MATH_CODES.unsupported, `\\${name} is not in the supported TeX subset.`, start);
|
|
480
|
+
}
|
|
481
|
+
environment(start) {
|
|
482
|
+
const name = this.rawGroup('begin').trim();
|
|
483
|
+
const env = Object.hasOwn(ENVIRONMENTS, name) ? ENVIRONMENTS[name] : undefined;
|
|
484
|
+
if (!env) {
|
|
485
|
+
this.fail(MATH_CODES.unsupported, `The environment "${name.slice(0, 32)}" is not supported.`, start);
|
|
486
|
+
}
|
|
487
|
+
if (name === 'array')
|
|
488
|
+
this.rawGroup('array'); // column spec: alignment only
|
|
489
|
+
this.enter();
|
|
490
|
+
const rows = [];
|
|
491
|
+
let cells = 0;
|
|
492
|
+
let row = [];
|
|
493
|
+
for (;;) {
|
|
494
|
+
const children = this.list('cell');
|
|
495
|
+
cells++;
|
|
496
|
+
if (cells > MATH_LIMITS.cells) {
|
|
497
|
+
this.fail(MATH_CODES.limit, `A matrix has at most ${MATH_LIMITS.cells} cells.`);
|
|
498
|
+
}
|
|
499
|
+
row.push(this.make({ tag: 'mtd', children }));
|
|
500
|
+
if (this.i >= this.src.length)
|
|
501
|
+
this.fail(MATH_CODES.syntax, `\\begin{${name}} has no \\end.`, start);
|
|
502
|
+
const c = this.src[this.i];
|
|
503
|
+
if (c === '&') {
|
|
504
|
+
this.i++;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const command = this.peekCommand();
|
|
508
|
+
if (command === '\\') {
|
|
509
|
+
this.i += 2;
|
|
510
|
+
rows.push(this.make({ tag: 'mtr', children: row }));
|
|
511
|
+
row = [];
|
|
512
|
+
if (rows.length > MATH_LIMITS.rows) {
|
|
513
|
+
this.fail(MATH_CODES.limit, `A matrix has at most ${MATH_LIMITS.rows} rows.`);
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (command === 'end') {
|
|
518
|
+
this.readCommand();
|
|
519
|
+
const closing = this.rawGroup('end').trim();
|
|
520
|
+
if (closing !== name) {
|
|
521
|
+
this.fail(MATH_CODES.syntax, `\\begin{${name}} ends with \\end{${closing.slice(0, 32)}}.`, start);
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
this.fail(MATH_CODES.syntax, `Unexpected \`${c ?? ''}\` in \\begin{${name}}.`);
|
|
526
|
+
}
|
|
527
|
+
// A trailing `\\` leaves an empty last row: drop it.
|
|
528
|
+
const last = row.length === 1 ? row[0] : undefined;
|
|
529
|
+
if (!(last && (last.children?.length ?? 0) === 0 && rows.length > 0)) {
|
|
530
|
+
rows.push(this.make({ tag: 'mtr', children: row }));
|
|
531
|
+
}
|
|
532
|
+
this.depth--;
|
|
533
|
+
const table = this.make({
|
|
534
|
+
tag: 'mtable',
|
|
535
|
+
attrs: env.align ? { columnalign: env.align } : {},
|
|
536
|
+
children: rows,
|
|
537
|
+
});
|
|
538
|
+
if (table.attrs && Object.keys(table.attrs).length === 0)
|
|
539
|
+
delete table.attrs;
|
|
540
|
+
const parts = [this.fence(env.open), table, this.fence(env.close)].filter((node) => node !== null);
|
|
541
|
+
return parts.length === 1 && parts[0] ? parts[0] : this.make({ tag: 'mrow', children: parts });
|
|
542
|
+
}
|
|
543
|
+
scripts(base) {
|
|
544
|
+
let sub = null;
|
|
545
|
+
let sup = null;
|
|
546
|
+
let primes = '';
|
|
547
|
+
for (;;) {
|
|
548
|
+
this.skipSpace();
|
|
549
|
+
const c = this.src[this.i];
|
|
550
|
+
if (c === "'") {
|
|
551
|
+
primes += '′';
|
|
552
|
+
this.i++;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (c !== '^' && c !== '_')
|
|
556
|
+
break;
|
|
557
|
+
const at = this.i;
|
|
558
|
+
this.i++;
|
|
559
|
+
this.enter();
|
|
560
|
+
const arg = this.argument(c === '^' ? 'superscript' : 'subscript');
|
|
561
|
+
this.depth--;
|
|
562
|
+
if (c === '^') {
|
|
563
|
+
if (sup !== null)
|
|
564
|
+
this.fail(MATH_CODES.syntax, 'Double superscript: use braces.', at);
|
|
565
|
+
sup = arg;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
if (sub !== null)
|
|
569
|
+
this.fail(MATH_CODES.syntax, 'Double subscript: use braces.', at);
|
|
570
|
+
sub = arg;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (primes) {
|
|
574
|
+
const prime = this.token('mo', primes);
|
|
575
|
+
sup = sup ? this.make({ tag: 'mrow', children: [prime, sup] }) : prime;
|
|
576
|
+
}
|
|
577
|
+
if (sub === null && sup === null)
|
|
578
|
+
return base.node;
|
|
579
|
+
const under = base.limits && this.display;
|
|
580
|
+
if (sub && sup) {
|
|
581
|
+
return this.make({ tag: under ? 'munderover' : 'msubsup', children: [base.node, sub, sup] });
|
|
582
|
+
}
|
|
583
|
+
if (sub)
|
|
584
|
+
return this.make({ tag: under ? 'munder' : 'msub', children: [base.node, sub] });
|
|
585
|
+
return this.make({ tag: under ? 'mover' : 'msup', children: [base.node, sup] });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function applyVariant(node, variant) {
|
|
589
|
+
if (node.tag === 'mi' || node.tag === 'mn' || node.tag === 'mtext') {
|
|
590
|
+
node.attrs = { ...node.attrs, mathvariant: variant };
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
for (const child of node.children ?? [])
|
|
594
|
+
applyVariant(child, variant);
|
|
595
|
+
}
|
|
596
|
+
function fallback(source, display) {
|
|
597
|
+
return {
|
|
598
|
+
tag: 'math',
|
|
599
|
+
attrs: { display: display ? 'block' : 'inline' },
|
|
600
|
+
children: [{ tag: 'mtext', text: source }],
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Convert a TeX formula (without its `$`/`$$` delimiters) to a MathML node
|
|
605
|
+
* tree. Never throws: an unsupported command, a syntax error or a bound
|
|
606
|
+
* gives `ok: false`, one issue and a node that shows the source as text.
|
|
607
|
+
*/
|
|
608
|
+
export function texToMath(tex, options = {}) {
|
|
609
|
+
const display = options.display === true;
|
|
610
|
+
const source = String(tex);
|
|
611
|
+
if (source.length > MATH_LIMITS.source) {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
node: fallback(source.slice(0, MATH_LIMITS.source), display),
|
|
615
|
+
issues: [
|
|
616
|
+
{
|
|
617
|
+
code: MATH_CODES.limit,
|
|
618
|
+
message: `A formula is at most ${MATH_LIMITS.source} characters.`,
|
|
619
|
+
offset: MATH_LIMITS.source,
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const parser = new TexParser(source, display);
|
|
625
|
+
try {
|
|
626
|
+
const children = parser.list('eof');
|
|
627
|
+
if (parser.i < source.length) {
|
|
628
|
+
parser.fail(MATH_CODES.syntax, 'A `}` has no matching `{`.');
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
ok: true,
|
|
632
|
+
node: { tag: 'math', attrs: { display: display ? 'block' : 'inline' }, children },
|
|
633
|
+
issues: [],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
const issue = error instanceof TexError
|
|
638
|
+
? { code: error.code, message: error.message, offset: error.offset }
|
|
639
|
+
: { code: MATH_CODES.syntax, message: 'The formula could not be read.', offset: 0 };
|
|
640
|
+
return { ok: false, node: fallback(source, display), issues: [issue] };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** The plain text of a math tree (for accessible names, text twins and PPTX). */
|
|
644
|
+
export function mathText(node) {
|
|
645
|
+
if (node.text !== undefined)
|
|
646
|
+
return node.text;
|
|
647
|
+
return (node.children ?? []).map(mathText).join('');
|
|
648
|
+
}
|