@simonklee/opentui-tex 0.3.0 → 0.3.1
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/README.md +87 -46
- package/dist/backend.d.ts +6 -0
- package/dist/{chunk-pgj24z8a.js → chunk-gbwc618x.js} +520 -157
- package/dist/{chunk-k9sn9j1p.js → chunk-qz8qag2e.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/math-layout.d.ts +5 -1
- package/dist/math-parser.d.ts +5 -2
- package/dist/math-types.d.ts +25 -1
- package/dist/react.js +2 -2
- package/dist/solid.js +2 -2
- package/dist/tex-renderable.d.ts +2 -0
- package/package.json +10 -10
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
BoxRenderable,
|
|
4
4
|
ImageRenderable,
|
|
5
5
|
TextRenderable,
|
|
6
|
+
StyledText,
|
|
7
|
+
createTextAttributes,
|
|
8
|
+
parseColor,
|
|
6
9
|
Yoga
|
|
7
10
|
} from "@opentui/core";
|
|
8
11
|
import stringWidth3 from "string-width";
|
|
@@ -67,18 +70,18 @@ var subscript = {
|
|
|
67
70
|
x: "ₓ"
|
|
68
71
|
};
|
|
69
72
|
function layoutMath(node, displayMode) {
|
|
70
|
-
return layout(node, displayMode);
|
|
73
|
+
return layout(node, { display: displayMode });
|
|
71
74
|
}
|
|
72
|
-
function
|
|
73
|
-
const
|
|
75
|
+
function boxToOutput(box, widthMax, heightMax) {
|
|
76
|
+
const spans = [];
|
|
74
77
|
for (let y = 0;y < Math.min(box.height, heightMax); y++) {
|
|
75
|
-
|
|
78
|
+
const line = [];
|
|
76
79
|
let width = 0;
|
|
77
80
|
for (let x = 0;x < box.width && width < widthMax; x++) {
|
|
78
81
|
const value = box.cells[y][x];
|
|
79
|
-
const cellWidth = value ? stringWidth(value) : 0;
|
|
82
|
+
const cellWidth = value ? stringWidth(value.char) : 0;
|
|
80
83
|
if (cellWidth === 0) {
|
|
81
|
-
line
|
|
84
|
+
appendSpan(line, " ");
|
|
82
85
|
width++;
|
|
83
86
|
continue;
|
|
84
87
|
}
|
|
@@ -86,174 +89,238 @@ function boxToString(box, widthMax, heightMax) {
|
|
|
86
89
|
throw new Error(`Unicode glyph exceeds the ${widthMax}-column TeX width`);
|
|
87
90
|
if (width + cellWidth > widthMax)
|
|
88
91
|
break;
|
|
89
|
-
line
|
|
92
|
+
appendSpan(line, value.char, value.style);
|
|
90
93
|
width += cellWidth;
|
|
91
94
|
x += cellWidth - 1;
|
|
92
95
|
}
|
|
93
|
-
|
|
96
|
+
trimSpans(line);
|
|
97
|
+
if (y)
|
|
98
|
+
appendSpan(spans, `
|
|
99
|
+
`);
|
|
100
|
+
for (const span of line)
|
|
101
|
+
appendSpan(spans, span.text, span);
|
|
94
102
|
}
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
trimSpans(spans);
|
|
104
|
+
const text = spans.map((span) => span.text).join("");
|
|
105
|
+
return spans.some((span) => span.color !== undefined || span.bold !== undefined || span.italic !== undefined) ? { text, spans } : { text };
|
|
97
106
|
}
|
|
98
|
-
function
|
|
107
|
+
function appendSpan(spans, text, style) {
|
|
108
|
+
const last = spans.at(-1);
|
|
109
|
+
if (last && last.color === style?.color && last.bold === style?.bold && last.italic === style?.italic) {
|
|
110
|
+
last.text += text;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
spans.push({ ...style, text });
|
|
114
|
+
}
|
|
115
|
+
function trimSpans(spans) {
|
|
116
|
+
while (spans.length) {
|
|
117
|
+
const last = spans.at(-1);
|
|
118
|
+
last.text = last.text.trimEnd();
|
|
119
|
+
if (last.text)
|
|
120
|
+
return;
|
|
121
|
+
spans.pop();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function layout(node, context) {
|
|
99
125
|
switch (node.type) {
|
|
100
126
|
case "row":
|
|
101
|
-
return layoutRow(node.body,
|
|
127
|
+
return layoutRow(node.body, context);
|
|
102
128
|
case "symbol":
|
|
103
129
|
case "text":
|
|
104
130
|
case "operator":
|
|
105
|
-
return textBox(node.value);
|
|
131
|
+
return textBox(applyVariant(node.value, context.variant), context.style);
|
|
106
132
|
case "space":
|
|
107
133
|
return blank(node.width, 1, 0);
|
|
108
134
|
case "fraction":
|
|
109
|
-
return fraction(layout(node.numerator,
|
|
135
|
+
return fraction(layout(node.numerator, context), layout(node.denominator, context), node.bar, node.numeratorAlign, context.style);
|
|
110
136
|
case "root":
|
|
111
|
-
return root(layout(node.body,
|
|
137
|
+
return root(layout(node.body, context), node.index ? layout(node.index, context) : undefined, context.style);
|
|
112
138
|
case "scripts":
|
|
113
|
-
return scripts(node,
|
|
139
|
+
return scripts(node, context);
|
|
114
140
|
case "delimited":
|
|
115
|
-
return delimited(node.left, layout(node.body,
|
|
141
|
+
return delimited(node.left, layout(node.body, context), node.right, context.style);
|
|
116
142
|
case "matrix":
|
|
117
|
-
return matrix(node
|
|
143
|
+
return matrix(node, context);
|
|
118
144
|
case "accent":
|
|
119
|
-
return accent(node.accent, layout(node.body,
|
|
145
|
+
return accent(node.accent, layout(node.body, context), context.style);
|
|
146
|
+
case "brace":
|
|
147
|
+
return brace(layout(node.body, context), node.position, context.style);
|
|
148
|
+
case "variant":
|
|
149
|
+
return layout(node.body, { ...context, variant: node.variant, style: node.variant === "bold" ? { ...context.style, bold: true } : node.variant === "italic" ? { ...context.style, italic: true } : context.style });
|
|
150
|
+
case "color":
|
|
151
|
+
return layout(node.body, { ...context, style: { ...context.style, color: node.color } });
|
|
120
152
|
case "overunder":
|
|
121
|
-
return overUnder(layout(node.base,
|
|
153
|
+
return overUnder(layout(node.base, context), node.over ? layout(node.over, context) : undefined, node.under ? layout(node.under, context) : undefined);
|
|
122
154
|
}
|
|
123
155
|
}
|
|
124
|
-
function layoutRow(nodes,
|
|
156
|
+
function layoutRow(nodes, context) {
|
|
125
157
|
const boxes = [];
|
|
126
158
|
let previous;
|
|
127
159
|
for (let index = 0;index < nodes.length; index++) {
|
|
128
160
|
const role = normalizedRole(roleOf(nodes[index]), previous, nextRole(nodes, index + 1));
|
|
129
161
|
if (needsSpace(previous, role, boxes.length))
|
|
130
162
|
boxes.push(blank(1, 1, 0));
|
|
131
|
-
boxes.push(layout(nodes[index],
|
|
163
|
+
boxes.push(layout(nodes[index], context));
|
|
132
164
|
if (nodes[index].type !== "space")
|
|
133
165
|
previous = role ?? "ordinary";
|
|
134
166
|
}
|
|
135
167
|
return hpack(boxes);
|
|
136
168
|
}
|
|
137
|
-
function fraction(top, bottom, bar) {
|
|
169
|
+
function fraction(top, bottom, bar, align, style) {
|
|
138
170
|
const width = Math.max(top.width, bottom.width) + 2;
|
|
139
171
|
const result = blank(width, top.height + bottom.height + 1, top.height);
|
|
140
|
-
overlay(result, top, Math.floor((width - top.width) / 2), 0);
|
|
172
|
+
overlay(result, top, align === "left" ? 1 : align === "right" ? width - top.width - 1 : Math.floor((width - top.width) / 2), 0);
|
|
141
173
|
if (bar)
|
|
142
|
-
horizontal(result, top.height, width, "─");
|
|
174
|
+
horizontal(result, top.height, width, "─", style);
|
|
143
175
|
overlay(result, bottom, Math.floor((width - bottom.width) / 2), top.height + 1);
|
|
144
176
|
return result;
|
|
145
177
|
}
|
|
146
|
-
function root(body, index) {
|
|
178
|
+
function root(body, index, style) {
|
|
147
179
|
const indexWidth = index ? Math.max(0, index.width - 1) : 0;
|
|
148
180
|
const bodyX = indexWidth + 2;
|
|
149
181
|
const bodyY = index?.height ?? 1;
|
|
150
182
|
const result = blank(bodyX + body.width, body.height + bodyY, body.baseline + bodyY);
|
|
151
|
-
set(result, bodyX - 1, bodyY - 1, "╭");
|
|
183
|
+
set(result, bodyX - 1, bodyY - 1, "╭", style);
|
|
152
184
|
for (let x = bodyX;x < result.width; x++)
|
|
153
|
-
set(result, x, bodyY - 1, "─");
|
|
154
|
-
set(result, bodyX - 2, result.baseline, "√");
|
|
185
|
+
set(result, x, bodyY - 1, "─", style);
|
|
186
|
+
set(result, bodyX - 2, result.baseline, "√", style);
|
|
155
187
|
overlay(result, body, bodyX, bodyY);
|
|
156
188
|
if (index)
|
|
157
189
|
overlay(result, index, 0, 0);
|
|
158
190
|
return result;
|
|
159
191
|
}
|
|
160
|
-
function scripts(node,
|
|
161
|
-
const base = layout(node.base,
|
|
192
|
+
function scripts(node, context) {
|
|
193
|
+
const base = layout(node.base, context);
|
|
194
|
+
const sup = node.superscript ? layout(node.superscript, context) : undefined;
|
|
195
|
+
const sub = node.subscript ? layout(node.subscript, context) : undefined;
|
|
162
196
|
const supText = node.superscript ? simpleText(node.superscript) : undefined;
|
|
163
197
|
const subText = node.subscript ? simpleText(node.subscript) : undefined;
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
let target = node.base;
|
|
199
|
+
while (target.type === "variant" || target.type === "color" || target.type === "scripts") {
|
|
200
|
+
target = target.type === "scripts" ? target.base : target.body;
|
|
201
|
+
}
|
|
202
|
+
const limits = target.type === "operator" && (target.limits === true || target.limits === "display" && context.display);
|
|
203
|
+
if (limits || target.type === "brace")
|
|
204
|
+
return overUnder(base, sup, sub);
|
|
205
|
+
if (base.height === 1) {
|
|
166
206
|
const mappedSup = supText === undefined ? undefined : mapScript(supText, superscript);
|
|
167
207
|
const mappedSub = subText === undefined ? undefined : mapScript(subText, subscript);
|
|
168
|
-
if ((!
|
|
169
|
-
return hpack([base, textBox((mappedSup ?? "") + (mappedSub ?? ""))]);
|
|
208
|
+
if ((!sup?.width || mappedSup !== undefined) && (!sub?.width || mappedSub !== undefined)) {
|
|
209
|
+
return hpack([base, textBox((mappedSup ?? "") + (mappedSub ?? ""), context.style)]);
|
|
170
210
|
}
|
|
171
211
|
}
|
|
172
|
-
if (limits)
|
|
173
|
-
return overUnder(base, node.superscript ? layout(node.superscript, display) : undefined, node.subscript ? layout(node.subscript, display) : undefined);
|
|
174
|
-
const sup = node.superscript ? layout(node.superscript, display) : undefined;
|
|
175
|
-
const sub = node.subscript ? layout(node.subscript, display) : undefined;
|
|
176
212
|
const sideWidth = Math.max(sup?.width ?? 0, sub?.width ?? 0);
|
|
177
|
-
const
|
|
178
|
-
|
|
213
|
+
const topHeight = sup?.width ? sup.height : 0;
|
|
214
|
+
const bottomHeight = sub?.width ? sub.height : 0;
|
|
215
|
+
const result = blank(base.width + sideWidth, topHeight + base.height + bottomHeight, topHeight + base.baseline);
|
|
216
|
+
overlay(result, base, 0, topHeight);
|
|
179
217
|
if (sup)
|
|
180
218
|
overlay(result, sup, base.width, 0);
|
|
181
219
|
if (sub)
|
|
182
|
-
overlay(result, sub, base.width,
|
|
220
|
+
overlay(result, sub, base.width, topHeight + base.height);
|
|
183
221
|
return result;
|
|
184
222
|
}
|
|
185
223
|
function overUnder(base, over, under) {
|
|
186
224
|
const width = Math.max(base.width, over?.width ?? 0, under?.width ?? 0);
|
|
187
|
-
const
|
|
225
|
+
const overHeight = over?.width ? over.height : 0;
|
|
226
|
+
const underHeight = under?.width ? under.height : 0;
|
|
227
|
+
const result = blank(width, overHeight + base.height + underHeight, overHeight + base.baseline);
|
|
188
228
|
if (over)
|
|
189
229
|
overlay(result, over, Math.floor((width - over.width) / 2), 0);
|
|
190
|
-
overlay(result, base, Math.floor((width - base.width) / 2),
|
|
230
|
+
overlay(result, base, Math.floor((width - base.width) / 2), overHeight);
|
|
191
231
|
if (under)
|
|
192
|
-
overlay(result, under, Math.floor((width - under.width) / 2),
|
|
232
|
+
overlay(result, under, Math.floor((width - under.width) / 2), overHeight + base.height);
|
|
193
233
|
return result;
|
|
194
234
|
}
|
|
195
|
-
function delimited(left, body, right) {
|
|
196
|
-
return hpack([delimiter(left, body.height, body.baseline, true), body, delimiter(right, body.height, body.baseline, false)]);
|
|
235
|
+
function delimited(left, body, right, style) {
|
|
236
|
+
return hpack([delimiter(left, body.height, body.baseline, true, style), body, delimiter(right, body.height, body.baseline, false, style)]);
|
|
197
237
|
}
|
|
198
|
-
function matrix(
|
|
199
|
-
const cells = rows.map((row) => row.map((
|
|
200
|
-
const
|
|
238
|
+
function matrix(node, context) {
|
|
239
|
+
const cells = node.rows.map((row) => row.map((cell) => layout(cell, context)));
|
|
240
|
+
const alignments = node.columns?.match(/[lcr]/g);
|
|
241
|
+
const rules = node.columns?.split(/[lcr]/).map((rule) => rule.length) ?? [];
|
|
242
|
+
const columns = Math.max(alignments?.length ?? 0, ...cells.map((row) => row.length));
|
|
201
243
|
const widths = Array.from({ length: columns }, (_, x) => Math.max(0, ...cells.map((row) => row[x]?.width ?? 0)));
|
|
202
244
|
const ascents = cells.map((row) => Math.max(0, ...row.map((cell) => cell.baseline)));
|
|
203
245
|
const descents = cells.map((row) => Math.max(0, ...row.map((cell) => cell.height - cell.baseline - 1)));
|
|
204
246
|
const heights = ascents.map((value, y2) => value + descents[y2] + 1);
|
|
205
|
-
const
|
|
206
|
-
const
|
|
247
|
+
const aligned = node.environment === "aligned" || node.environment === "align";
|
|
248
|
+
const gap = node.environment === "cases" || aligned ? 2 : 1;
|
|
249
|
+
const gaps = Array.from({ length: columns + 1 }, (_, boundary) => {
|
|
250
|
+
const edge = boundary === 0 || boundary === columns;
|
|
251
|
+
return rules[boundary] ? rules[boundary] + (edge ? 1 : 2) : edge ? 0 : gap;
|
|
252
|
+
});
|
|
253
|
+
const width = widths.reduce((sum, value) => sum + value, 0) + gaps.reduce((sum, value) => sum + value, 0);
|
|
207
254
|
const height = Math.max(1, heights.reduce((sum, value) => sum + value, 0));
|
|
208
255
|
const result = blank(width, height, Math.floor(height / 2));
|
|
209
256
|
let y = 0;
|
|
210
257
|
for (let rowIndex = 0;rowIndex < cells.length; rowIndex++) {
|
|
211
|
-
let x = 0;
|
|
258
|
+
let x = gaps[0];
|
|
212
259
|
for (let column = 0;column < columns; column++) {
|
|
213
260
|
const cell = cells[rowIndex][column];
|
|
214
261
|
if (cell) {
|
|
215
|
-
const
|
|
216
|
-
const offset =
|
|
262
|
+
const alignment = alignments?.[column] ?? (node.environment === "cases" ? "l" : aligned ? column % 2 === 0 ? "r" : "l" : "c");
|
|
263
|
+
const offset = alignment === "l" ? 0 : alignment === "r" ? widths[column] - cell.width : Math.floor((widths[column] - cell.width) / 2);
|
|
217
264
|
overlay(result, cell, x + offset, y + ascents[rowIndex] - cell.baseline);
|
|
218
265
|
}
|
|
219
|
-
x += widths[column] +
|
|
266
|
+
x += widths[column] + gaps[column + 1];
|
|
220
267
|
}
|
|
221
268
|
y += heights[rowIndex];
|
|
222
269
|
}
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
let boundaryX = 0;
|
|
271
|
+
for (let boundary = 0;boundary <= columns; boundary++) {
|
|
272
|
+
for (let rule = 0;rule < (rules[boundary] ?? 0); rule++) {
|
|
273
|
+
for (let row = 0;row < height; row++)
|
|
274
|
+
set(result, boundaryX + (boundary === 0 ? 0 : 1) + rule, row, "│", context.style);
|
|
275
|
+
}
|
|
276
|
+
boundaryX += gaps[boundary] + (widths[boundary] ?? 0);
|
|
277
|
+
}
|
|
278
|
+
const pair = matrixDelimiters(node.environment);
|
|
279
|
+
return pair ? delimited(pair[0], result, pair[1], context.style) : result;
|
|
225
280
|
}
|
|
226
|
-
function
|
|
281
|
+
function brace(body, position, style) {
|
|
282
|
+
const over = position === "over";
|
|
283
|
+
const width = Math.max(3, body.width);
|
|
284
|
+
const result = blank(width, body.height + 1, body.baseline + (over ? 1 : 0));
|
|
285
|
+
const y = over ? 0 : body.height;
|
|
286
|
+
overlay(result, body, Math.floor((width - body.width) / 2), over ? 1 : 0);
|
|
287
|
+
horizontal(result, y, width, "─", style);
|
|
288
|
+
set(result, 0, y, over ? "╭" : "╰", style);
|
|
289
|
+
set(result, width - 1, y, over ? "╮" : "╯", style);
|
|
290
|
+
set(result, Math.floor((width - 1) / 2), y, over ? "┴" : "┬", style);
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
function accent(kind, body, style) {
|
|
227
294
|
if (kind === "underline") {
|
|
228
295
|
const result2 = blank(body.width, body.height + 1, body.baseline);
|
|
229
296
|
overlay(result2, body, 0, 0);
|
|
230
|
-
horizontal(result2, body.height, body.width, "─");
|
|
297
|
+
horizontal(result2, body.height, body.width, "─", style);
|
|
231
298
|
return result2;
|
|
232
299
|
}
|
|
233
300
|
const result = blank(body.width, body.height + 1, body.baseline + 1);
|
|
234
301
|
overlay(result, body, 0, 1);
|
|
235
302
|
const mark = kind === "hat" || kind === "widehat" ? body.width === 1 ? "^" : "⌢" : kind === "bar" || kind === "overline" ? "─" : kind === "vec" ? "→" : kind === "tilde" ? "~" : kind === "dot" ? "·" : "¨";
|
|
236
303
|
if (kind === "bar" || kind === "overline")
|
|
237
|
-
horizontal(result, 0, body.width, mark);
|
|
304
|
+
horizontal(result, 0, body.width, mark, style);
|
|
238
305
|
else
|
|
239
|
-
set(result, Math.max(0, Math.floor((body.width - stringWidth(mark)) / 2)), 0, mark);
|
|
306
|
+
set(result, Math.max(0, Math.floor((body.width - stringWidth(mark)) / 2)), 0, mark, style);
|
|
240
307
|
return result;
|
|
241
308
|
}
|
|
242
|
-
function delimiter(value, height, baseline, left) {
|
|
309
|
+
function delimiter(value, height, baseline, left, style) {
|
|
243
310
|
const base = value.replace(/\p{Mark}+$/u, "");
|
|
244
311
|
if (!base)
|
|
245
312
|
return blank(0, height, baseline);
|
|
246
313
|
if (height === 1)
|
|
247
|
-
return textBox(value);
|
|
314
|
+
return textBox(value, style);
|
|
248
315
|
const glyphs = delimiterGlyphs(base, left);
|
|
249
316
|
const suffix = value.slice(base.length);
|
|
250
317
|
const result = blank(Math.max(...glyphs.map((glyph) => stringWidth(glyph + suffix))), height, baseline);
|
|
251
318
|
for (let y = 0;y < height; y++)
|
|
252
|
-
set(result, 0, y, y === 0 ? glyphs[0] : y === height - 1 ? glyphs[2] : glyphs[1]);
|
|
319
|
+
set(result, 0, y, y === 0 ? glyphs[0] : y === height - 1 ? glyphs[2] : glyphs[1], style);
|
|
253
320
|
if ((base === "{" || base === "}") && height >= 3)
|
|
254
|
-
set(result, 0, Math.floor(height / 2), left ? "⎨" : "⎬");
|
|
321
|
+
set(result, 0, Math.floor(height / 2), left ? "⎨" : "⎬", style);
|
|
255
322
|
if (suffix)
|
|
256
|
-
set(result, 0, baseline, result.cells[baseline][0] + suffix);
|
|
323
|
+
set(result, 0, baseline, result.cells[baseline][0].char + suffix, style);
|
|
257
324
|
return result;
|
|
258
325
|
}
|
|
259
326
|
function delimiterGlyphs(value, left) {
|
|
@@ -282,12 +349,12 @@ function delimiterGlyphs(value, left) {
|
|
|
282
349
|
function matrixDelimiters(value) {
|
|
283
350
|
return value === "pmatrix" ? ["(", ")"] : value === "bmatrix" ? ["[", "]"] : value === "Bmatrix" ? ["{", "}"] : value === "vmatrix" ? ["│", "│"] : value === "Vmatrix" ? ["║", "║"] : value === "cases" ? ["{", ""] : undefined;
|
|
284
351
|
}
|
|
285
|
-
function textBox(text) {
|
|
352
|
+
function textBox(text, style) {
|
|
286
353
|
const parts = Array.from(graphemeSegmenter.segment(text), (part) => part.segment);
|
|
287
354
|
const result = blank(parts.reduce((sum, part) => sum + stringWidth(part), 0), 1, 0);
|
|
288
355
|
let x = 0;
|
|
289
356
|
for (const part of parts) {
|
|
290
|
-
set(result, x, 0, part);
|
|
357
|
+
set(result, x, 0, part, style);
|
|
291
358
|
x += stringWidth(part);
|
|
292
359
|
}
|
|
293
360
|
return result;
|
|
@@ -318,13 +385,13 @@ function overlay(target, source, x, y) {
|
|
|
318
385
|
if (source.cells[sy][sx])
|
|
319
386
|
target.cells[y + sy][x + sx] = source.cells[sy][sx];
|
|
320
387
|
}
|
|
321
|
-
function set(box, x, y,
|
|
388
|
+
function set(box, x, y, char, style) {
|
|
322
389
|
if (x >= 0 && y >= 0 && x < box.width && y < box.height)
|
|
323
|
-
box.cells[y][x] =
|
|
390
|
+
box.cells[y][x] = style ? { char, style } : { char };
|
|
324
391
|
}
|
|
325
|
-
function horizontal(box, y, width, value) {
|
|
392
|
+
function horizontal(box, y, width, value, style) {
|
|
326
393
|
for (let x = 0;x < width; x++)
|
|
327
|
-
set(box, x, y, value);
|
|
394
|
+
set(box, x, y, value, style);
|
|
328
395
|
}
|
|
329
396
|
function simpleText(node) {
|
|
330
397
|
if (node.type === "symbol" || node.type === "text" || node.type === "operator")
|
|
@@ -352,6 +419,8 @@ function roleOf(node) {
|
|
|
352
419
|
return "operator";
|
|
353
420
|
if (node.type === "scripts")
|
|
354
421
|
return roleOf(node.base);
|
|
422
|
+
if (node.type === "variant" || node.type === "color")
|
|
423
|
+
return roleOf(node.body);
|
|
355
424
|
return;
|
|
356
425
|
}
|
|
357
426
|
function nextRole(nodes, start) {
|
|
@@ -366,6 +435,36 @@ function needsSpace(previous, current, count) {
|
|
|
366
435
|
function normalizedRole(role, previous, next) {
|
|
367
436
|
return role === "binary" && (!previous || ["binary", "relation", "operator", "punctuation", "opening"].includes(previous) || !next || ["binary", "relation", "punctuation", "closing"].includes(next)) ? "ordinary" : role;
|
|
368
437
|
}
|
|
438
|
+
function applyVariant(value, variant) {
|
|
439
|
+
if (!variant || variant === "normal" || variant === "bold" || variant === "italic")
|
|
440
|
+
return value;
|
|
441
|
+
const exceptions = {
|
|
442
|
+
"double-struck": { C: "ℂ", H: "ℍ", N: "ℕ", P: "ℙ", Q: "ℚ", R: "ℝ", Z: "ℤ" },
|
|
443
|
+
script: { B: "ℬ", E: "ℰ", F: "ℱ", H: "ℋ", I: "ℐ", L: "ℒ", M: "ℳ", R: "ℛ", e: "ℯ", g: "ℊ", o: "ℴ" },
|
|
444
|
+
fraktur: { C: "ℭ", H: "ℌ", I: "ℑ", R: "ℜ", Z: "ℨ" }
|
|
445
|
+
};
|
|
446
|
+
const ranges = {
|
|
447
|
+
"double-struck": [120120, 120146, 120792],
|
|
448
|
+
script: [119964, 119990],
|
|
449
|
+
fraktur: [120068, 120094],
|
|
450
|
+
sans: [120224, 120250, 120802],
|
|
451
|
+
monospace: [120432, 120458, 120822]
|
|
452
|
+
};
|
|
453
|
+
const range = ranges[variant];
|
|
454
|
+
return Array.from(value).map((char) => {
|
|
455
|
+
const exception = exceptions[variant]?.[char];
|
|
456
|
+
if (exception)
|
|
457
|
+
return exception;
|
|
458
|
+
const code = char.codePointAt(0);
|
|
459
|
+
if (code >= 65 && code <= 90)
|
|
460
|
+
return String.fromCodePoint(range[0] + code - 65);
|
|
461
|
+
if (code >= 97 && code <= 122)
|
|
462
|
+
return String.fromCodePoint(range[1] + code - 97);
|
|
463
|
+
if (range[2] !== undefined && code >= 48 && code <= 57)
|
|
464
|
+
return String.fromCodePoint(range[2] + code - 48);
|
|
465
|
+
return char;
|
|
466
|
+
}).join("");
|
|
467
|
+
}
|
|
369
468
|
|
|
370
469
|
// src/math-symbols.ts
|
|
371
470
|
var ordinary = {
|
|
@@ -428,7 +527,23 @@ var ordinary = {
|
|
|
428
527
|
aleph: "ℵ",
|
|
429
528
|
top: "⊤",
|
|
430
529
|
bot: "⊥",
|
|
431
|
-
checkmark: "✓"
|
|
530
|
+
checkmark: "✓",
|
|
531
|
+
imath: "ı",
|
|
532
|
+
jmath: "ȷ",
|
|
533
|
+
beth: "ℶ",
|
|
534
|
+
gimel: "ℷ",
|
|
535
|
+
daleth: "ℸ",
|
|
536
|
+
measuredangle: "∡",
|
|
537
|
+
triangle: "△",
|
|
538
|
+
square: "□",
|
|
539
|
+
lozenge: "◊",
|
|
540
|
+
nexists: "∄",
|
|
541
|
+
lnot: "¬",
|
|
542
|
+
backprime: "‵",
|
|
543
|
+
clubsuit: "♣",
|
|
544
|
+
diamondsuit: "♢",
|
|
545
|
+
heartsuit: "♡",
|
|
546
|
+
spadesuit: "♠"
|
|
432
547
|
};
|
|
433
548
|
var binary = {
|
|
434
549
|
pm: "±",
|
|
@@ -451,7 +566,16 @@ var binary = {
|
|
|
451
566
|
wedge: "∧",
|
|
452
567
|
lor: "∨",
|
|
453
568
|
vee: "∨",
|
|
454
|
-
setminus: "∖"
|
|
569
|
+
setminus: "∖",
|
|
570
|
+
uplus: "⊎",
|
|
571
|
+
sqcap: "⊓",
|
|
572
|
+
sqcup: "⊔",
|
|
573
|
+
wr: "≀",
|
|
574
|
+
diamond: "⋄",
|
|
575
|
+
bigtriangleup: "△",
|
|
576
|
+
bigtriangledown: "▽",
|
|
577
|
+
triangleleft: "◁",
|
|
578
|
+
triangleright: "▷"
|
|
455
579
|
};
|
|
456
580
|
var relation = {
|
|
457
581
|
ne: "≠",
|
|
@@ -493,7 +617,42 @@ var relation = {
|
|
|
493
617
|
longleftrightarrow: "⟷",
|
|
494
618
|
uparrow: "↑",
|
|
495
619
|
downarrow: "↓",
|
|
496
|
-
updownarrow: "↕"
|
|
620
|
+
updownarrow: "↕",
|
|
621
|
+
equals: "=",
|
|
622
|
+
asymp: "≍",
|
|
623
|
+
lt: "<",
|
|
624
|
+
gt: ">",
|
|
625
|
+
prec: "≺",
|
|
626
|
+
succ: "≻",
|
|
627
|
+
preceq: "⪯",
|
|
628
|
+
succeq: "⪰",
|
|
629
|
+
sqsubset: "⊏",
|
|
630
|
+
sqsupset: "⊐",
|
|
631
|
+
sqsubseteq: "⊑",
|
|
632
|
+
sqsupseteq: "⊒",
|
|
633
|
+
owns: "∋",
|
|
634
|
+
dashv: "⊣",
|
|
635
|
+
mid: "∣",
|
|
636
|
+
smile: "⌣",
|
|
637
|
+
frown: "⌢",
|
|
638
|
+
hookleftarrow: "↩",
|
|
639
|
+
hookrightarrow: "↪",
|
|
640
|
+
leftharpoonup: "↼",
|
|
641
|
+
leftharpoondown: "↽",
|
|
642
|
+
rightharpoonup: "⇀",
|
|
643
|
+
rightharpoondown: "⇁",
|
|
644
|
+
rightleftharpoons: "⇌",
|
|
645
|
+
Longleftarrow: "⟸",
|
|
646
|
+
Longrightarrow: "⟹",
|
|
647
|
+
Longleftrightarrow: "⟺",
|
|
648
|
+
longmapsto: "⟼",
|
|
649
|
+
Uparrow: "⇑",
|
|
650
|
+
Downarrow: "⇓",
|
|
651
|
+
Updownarrow: "⇕",
|
|
652
|
+
nearrow: "↗",
|
|
653
|
+
searrow: "↘",
|
|
654
|
+
swarrow: "↙",
|
|
655
|
+
nwarrow: "↖"
|
|
497
656
|
};
|
|
498
657
|
var punctuation = { ldots: "…", dots: "…", cdots: "⋯", vdots: "⋮", ddots: "⋱", colon: ":" };
|
|
499
658
|
function definitions(values, role) {
|
|
@@ -516,9 +675,46 @@ var operators = {
|
|
|
516
675
|
bigcap: "⋂",
|
|
517
676
|
bigcup: "⋃",
|
|
518
677
|
bigvee: "⋁",
|
|
519
|
-
bigwedge: "⋀"
|
|
678
|
+
bigwedge: "⋀",
|
|
679
|
+
bigoplus: "⨁",
|
|
680
|
+
bigotimes: "⨂",
|
|
681
|
+
bigodot: "⨀"
|
|
520
682
|
};
|
|
521
|
-
var namedOperators = new Set([
|
|
683
|
+
var namedOperators = new Set([
|
|
684
|
+
"arccos",
|
|
685
|
+
"arcsin",
|
|
686
|
+
"arctan",
|
|
687
|
+
"arg",
|
|
688
|
+
"cos",
|
|
689
|
+
"cosh",
|
|
690
|
+
"cot",
|
|
691
|
+
"coth",
|
|
692
|
+
"csc",
|
|
693
|
+
"deg",
|
|
694
|
+
"det",
|
|
695
|
+
"dim",
|
|
696
|
+
"exp",
|
|
697
|
+
"gcd",
|
|
698
|
+
"hom",
|
|
699
|
+
"inf",
|
|
700
|
+
"ker",
|
|
701
|
+
"lg",
|
|
702
|
+
"lim",
|
|
703
|
+
"liminf",
|
|
704
|
+
"limsup",
|
|
705
|
+
"ln",
|
|
706
|
+
"log",
|
|
707
|
+
"max",
|
|
708
|
+
"min",
|
|
709
|
+
"mod",
|
|
710
|
+
"Pr",
|
|
711
|
+
"sec",
|
|
712
|
+
"sin",
|
|
713
|
+
"sinh",
|
|
714
|
+
"sup",
|
|
715
|
+
"tan",
|
|
716
|
+
"tanh"
|
|
717
|
+
]);
|
|
522
718
|
var delimiters = {
|
|
523
719
|
"(": "(",
|
|
524
720
|
")": ")",
|
|
@@ -538,6 +734,16 @@ var delimiters = {
|
|
|
538
734
|
rfloor: "⌋",
|
|
539
735
|
lceil: "⌈",
|
|
540
736
|
rceil: "⌉",
|
|
737
|
+
lvert: "│",
|
|
738
|
+
rvert: "│",
|
|
739
|
+
lVert: "║",
|
|
740
|
+
rVert: "║",
|
|
741
|
+
"\\{": "{",
|
|
742
|
+
"\\}": "}",
|
|
743
|
+
"/": "/",
|
|
744
|
+
"<": "<",
|
|
745
|
+
">": ">",
|
|
746
|
+
backslash: "\\",
|
|
541
747
|
".": ""
|
|
542
748
|
};
|
|
543
749
|
var accents = {
|
|
@@ -571,22 +777,39 @@ var environments = new Set([
|
|
|
571
777
|
"smallmatrix",
|
|
572
778
|
"array"
|
|
573
779
|
]);
|
|
574
|
-
|
|
575
|
-
|
|
780
|
+
var variants = {
|
|
781
|
+
mathrm: "normal",
|
|
782
|
+
textrm: "normal",
|
|
783
|
+
mathnormal: "normal",
|
|
784
|
+
mathbf: "bold",
|
|
785
|
+
boldsymbol: "bold",
|
|
786
|
+
bm: "bold",
|
|
787
|
+
mathit: "italic",
|
|
788
|
+
mathsf: "sans",
|
|
789
|
+
mathtt: "monospace",
|
|
790
|
+
mathbb: "double-struck",
|
|
791
|
+
mathcal: "script",
|
|
792
|
+
mathscr: "script",
|
|
793
|
+
mathfrak: "fraktur"
|
|
794
|
+
};
|
|
795
|
+
function parseMath(source, options = {}) {
|
|
796
|
+
return new Parser(source, false, options.strict ?? false).parse();
|
|
576
797
|
}
|
|
577
|
-
function parseMathIncomplete(source) {
|
|
578
|
-
return new Parser(source, true).parse();
|
|
798
|
+
function parseMathIncomplete(source, options = {}) {
|
|
799
|
+
return new Parser(source, true, options.strict ?? false).parse();
|
|
579
800
|
}
|
|
580
801
|
|
|
581
802
|
class Parser {
|
|
582
803
|
source;
|
|
583
804
|
incomplete;
|
|
805
|
+
strict;
|
|
584
806
|
offset = 0;
|
|
585
807
|
depth = 0;
|
|
586
808
|
graphemes;
|
|
587
|
-
constructor(source, incomplete
|
|
809
|
+
constructor(source, incomplete, strict) {
|
|
588
810
|
this.source = source;
|
|
589
811
|
this.incomplete = incomplete;
|
|
812
|
+
this.strict = strict;
|
|
590
813
|
this.graphemes = graphemeSegmenter.segment(source);
|
|
591
814
|
}
|
|
592
815
|
parse() {
|
|
@@ -614,14 +837,14 @@ class Parser {
|
|
|
614
837
|
scripts2.subscript = argument;
|
|
615
838
|
body.push(scripts2);
|
|
616
839
|
} else {
|
|
617
|
-
const atom = this.parseAtom(body.at(-1));
|
|
840
|
+
const atom = this.parseAtom(body.at(-1), stop);
|
|
618
841
|
if (atom)
|
|
619
842
|
body.push(atom);
|
|
620
843
|
}
|
|
621
844
|
}
|
|
622
845
|
return body;
|
|
623
846
|
}
|
|
624
|
-
parseAtom(previous) {
|
|
847
|
+
parseAtom(previous, stop) {
|
|
625
848
|
this.depth++;
|
|
626
849
|
if (this.depth > MAX_NESTING_DEPTH)
|
|
627
850
|
this.fail(`TeX nesting exceeds the ${MAX_NESTING_DEPTH}-level limit`);
|
|
@@ -629,26 +852,23 @@ class Parser {
|
|
|
629
852
|
if (this.peek() === "{")
|
|
630
853
|
return this.parseGroup();
|
|
631
854
|
if (this.peek() === "\\") {
|
|
632
|
-
const atom = this.parseCommand(previous);
|
|
633
|
-
|
|
634
|
-
|
|
855
|
+
const atom = this.parseCommand(previous, stop);
|
|
856
|
+
const value2 = atom && unwrapStyle(atom);
|
|
857
|
+
if (value2 && "value" in value2)
|
|
858
|
+
value2.value += this.readCombiningSuffix();
|
|
635
859
|
return atom;
|
|
636
860
|
}
|
|
637
861
|
if (this.peek() === "~") {
|
|
638
862
|
this.offset++;
|
|
639
863
|
return { type: "space", width: 1 };
|
|
640
864
|
}
|
|
641
|
-
const
|
|
642
|
-
const literal = part.segment.slice(this.offset - part.index);
|
|
643
|
-
const boundary = literal.search(/[\\{}[\]^_~&\s]/u);
|
|
644
|
-
const value = boundary > 0 ? literal.slice(0, boundary) : literal;
|
|
645
|
-
this.offset += value.length;
|
|
865
|
+
const value = this.readLiteral();
|
|
646
866
|
return { type: "symbol", value, role: inferRole(value[0]) };
|
|
647
867
|
} finally {
|
|
648
868
|
this.depth--;
|
|
649
869
|
}
|
|
650
870
|
}
|
|
651
|
-
parseCommand(previous) {
|
|
871
|
+
parseCommand(previous, stop) {
|
|
652
872
|
const start = this.offset;
|
|
653
873
|
const command = this.readCommand();
|
|
654
874
|
if (command === "\\")
|
|
@@ -658,7 +878,20 @@ class Parser {
|
|
|
658
878
|
if (command === "end")
|
|
659
879
|
this.fail("Unexpected \\end", start);
|
|
660
880
|
if (["frac", "dfrac", "tfrac", "cfrac"].includes(command)) {
|
|
661
|
-
|
|
881
|
+
this.skipWhitespace();
|
|
882
|
+
const alignment = command === "cfrac" && this.peek() === "[" ? /^\[([lr]?)(\]|$)/.exec(this.source.slice(this.offset)) : undefined;
|
|
883
|
+
if (alignment === null || alignment && !alignment[2] && (!this.incomplete || this.offset + alignment[0].length !== this.source.length)) {
|
|
884
|
+
this.fail("Unsupported \\cfrac alignment; expected [l], [r], or []");
|
|
885
|
+
}
|
|
886
|
+
if (alignment)
|
|
887
|
+
this.offset += alignment[0].length;
|
|
888
|
+
return {
|
|
889
|
+
type: "fraction",
|
|
890
|
+
numerator: this.parseArgument(),
|
|
891
|
+
denominator: this.parseArgument(),
|
|
892
|
+
bar: true,
|
|
893
|
+
...alignment?.[1] ? { numeratorAlign: alignment[1] === "l" ? "left" : "right" } : {}
|
|
894
|
+
};
|
|
662
895
|
}
|
|
663
896
|
if (["binom", "dbinom", "tbinom"].includes(command)) {
|
|
664
897
|
return { type: "delimited", left: "(", body: { type: "fraction", numerator: this.parseArgument(), denominator: this.parseArgument(), bar: false }, right: ")" };
|
|
@@ -673,16 +906,19 @@ class Parser {
|
|
|
673
906
|
this.fail("Unexpected \\right", start);
|
|
674
907
|
if (command === "middle")
|
|
675
908
|
return { type: "symbol", value: this.readDelimiter() };
|
|
676
|
-
if (command
|
|
909
|
+
if (Object.hasOwn(accents, command))
|
|
677
910
|
return { type: "accent", accent: accents[command], body: this.parseArgument() };
|
|
678
|
-
if (
|
|
679
|
-
return this.parseArgument();
|
|
911
|
+
if (Object.hasOwn(variants, command)) {
|
|
912
|
+
return { type: "variant", variant: variants[command], body: command === "textrm" ? { type: "text", value: this.readTextGroup() } : this.parseArgument() };
|
|
680
913
|
}
|
|
681
|
-
if (
|
|
682
|
-
return { type: "text", value: this.
|
|
914
|
+
if (command === "text" || command === "mbox")
|
|
915
|
+
return { type: "text", value: this.readTextGroup() };
|
|
916
|
+
if (command === "operatorname") {
|
|
917
|
+
const limits = this.peek() === "*";
|
|
918
|
+
if (limits)
|
|
919
|
+
this.offset++;
|
|
920
|
+
return { type: "operator", value: this.readTextGroup(), limits };
|
|
683
921
|
}
|
|
684
|
-
if (command === "operatorname")
|
|
685
|
-
return { type: "operator", value: this.readRawGroup(), limits: false };
|
|
686
922
|
if (command === "overset" || command === "stackrel") {
|
|
687
923
|
const over = this.parseArgument();
|
|
688
924
|
return { type: "overunder", over, base: this.parseArgument() };
|
|
@@ -691,14 +927,39 @@ class Parser {
|
|
|
691
927
|
const under = this.parseArgument();
|
|
692
928
|
return { type: "overunder", under, base: this.parseArgument() };
|
|
693
929
|
}
|
|
930
|
+
if (command === "overbrace" || command === "underbrace") {
|
|
931
|
+
return { type: "brace", body: this.parseArgument(), position: command === "overbrace" ? "over" : "under" };
|
|
932
|
+
}
|
|
933
|
+
if (command === "textcolor" || command === "color") {
|
|
934
|
+
const color = this.readRawGroup().value;
|
|
935
|
+
return { type: "color", color, body: command === "textcolor" ? this.parseArgument() : row(this.parseRow(stop)) };
|
|
936
|
+
}
|
|
694
937
|
if (command === "not") {
|
|
695
938
|
const target = this.parseArgument();
|
|
696
|
-
|
|
697
|
-
|
|
939
|
+
const symbol = unwrapStyle(target);
|
|
940
|
+
if (symbol.type === "symbol") {
|
|
941
|
+
symbol.value = negate(symbol.value);
|
|
942
|
+
return target;
|
|
943
|
+
}
|
|
698
944
|
return { type: "row", body: [{ type: "symbol", value: "¬" }, target] };
|
|
699
945
|
}
|
|
946
|
+
if (command === "pmod") {
|
|
947
|
+
return { type: "row", body: [{ type: "space", width: 1 }, { type: "text", value: "(mod " }, this.parseArgument(), { type: "text", value: ")" }] };
|
|
948
|
+
}
|
|
949
|
+
if (command === "mod" || command === "bmod")
|
|
950
|
+
return { type: "operator", value: "mod", limits: false };
|
|
951
|
+
if (command === "displaylines") {
|
|
952
|
+
this.skipWhitespace();
|
|
953
|
+
if (this.incomplete && this.done())
|
|
954
|
+
return { type: "matrix", environment: "gathered", rows: [[placeholder()]] };
|
|
955
|
+
this.expect("{");
|
|
956
|
+
return this.parseMatrix("gathered", "}");
|
|
957
|
+
}
|
|
700
958
|
if (command === "limits" || command === "nolimits") {
|
|
701
|
-
|
|
959
|
+
let base = previous;
|
|
960
|
+
while (base?.type === "variant" || base?.type === "color" || base?.type === "scripts") {
|
|
961
|
+
base = base.type === "scripts" ? base.base : base.body;
|
|
962
|
+
}
|
|
702
963
|
if (base?.type === "operator")
|
|
703
964
|
base.limits = command === "limits";
|
|
704
965
|
return;
|
|
@@ -707,46 +968,42 @@ class Parser {
|
|
|
707
968
|
return;
|
|
708
969
|
if (/^(?:big|Big|bigg|Bigg)[lrm]?$/.test(command))
|
|
709
970
|
return { type: "symbol", value: this.readDelimiter() };
|
|
710
|
-
if (command
|
|
971
|
+
if (Object.hasOwn(spacing, command))
|
|
711
972
|
return { type: "space", width: spacing[command] };
|
|
712
|
-
if (command
|
|
973
|
+
if (Object.hasOwn(symbols, command))
|
|
713
974
|
return { type: "symbol", ...symbols[command] };
|
|
714
|
-
if (command
|
|
975
|
+
if (Object.hasOwn(operators, command))
|
|
715
976
|
return { type: "operator", value: operators[command], limits: command.includes("int") ? false : "display" };
|
|
716
977
|
if (namedOperators.has(command))
|
|
717
|
-
return { type: "operator", value: command, limits:
|
|
718
|
-
if (command
|
|
978
|
+
return { type: "operator", value: command, limits: command.startsWith("lim") || ["min", "max"].includes(command) ? "display" : false };
|
|
979
|
+
if (Object.hasOwn(delimiters, command))
|
|
719
980
|
return { type: "symbol", value: delimiters[`\\${command}`] ?? delimiters[command] };
|
|
720
981
|
if (["{", "}", "%", "#", "$", "&", "_", "backslash"].includes(command))
|
|
721
982
|
return { type: "symbol", value: command === "backslash" ? "\\" : command };
|
|
983
|
+
if (command === " ")
|
|
984
|
+
return { type: "space", width: 1 };
|
|
985
|
+
if (this.strict)
|
|
986
|
+
this.fail(`Unsupported command \\${command}`, start);
|
|
722
987
|
return { type: "text", value: `\\${command}` };
|
|
723
988
|
}
|
|
724
989
|
parseEnvironment() {
|
|
725
|
-
const rawName = this.readRawGroup();
|
|
990
|
+
const rawName = this.readRawGroup().value;
|
|
726
991
|
const name = rawName.replace(/\*$/, "");
|
|
727
992
|
if (!environments.has(name))
|
|
728
993
|
this.fail(`Unsupported TeX environment: ${rawName}`);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
this.readRawGroup();
|
|
733
|
-
}
|
|
994
|
+
return this.parseMatrix(name, `\\end{${rawName}}`, name === "array" ? this.readArrayColumns() : undefined);
|
|
995
|
+
}
|
|
996
|
+
parseMatrix(environment, end, columns) {
|
|
734
997
|
const rows = [];
|
|
735
998
|
let cells = [];
|
|
736
999
|
while (!this.done()) {
|
|
737
1000
|
this.skipWhitespace();
|
|
738
1001
|
if (this.done())
|
|
739
1002
|
break;
|
|
740
|
-
if (this.
|
|
741
|
-
|
|
742
|
-
if (cells.length || !rows.length)
|
|
743
|
-
rows.push(cells);
|
|
744
|
-
return { type: "matrix", rows, environment: name };
|
|
745
|
-
}
|
|
1003
|
+
if (this.source.startsWith(end, this.offset) && !cells.length)
|
|
1004
|
+
break;
|
|
746
1005
|
const start = this.offset;
|
|
747
|
-
cells.push(row(this.parseRow(() => this.peek() === "&" || this.source.startsWith("\\\\", this.offset) || this.source.startsWith(
|
|
748
|
-
if (this.offset === start)
|
|
749
|
-
this.fail(`Unexpected token in ${rawName}`);
|
|
1006
|
+
cells.push(row(this.parseRow(() => this.peek() === "&" || this.source.startsWith("\\\\", this.offset) || this.source.startsWith(end, this.offset) || this.isCommand("end"))));
|
|
750
1007
|
this.skipWhitespace();
|
|
751
1008
|
if (this.peek() === "&") {
|
|
752
1009
|
this.offset++;
|
|
@@ -762,15 +1019,29 @@ class Parser {
|
|
|
762
1019
|
cells = [];
|
|
763
1020
|
continue;
|
|
764
1021
|
}
|
|
1022
|
+
if (this.source.startsWith(end, this.offset))
|
|
1023
|
+
break;
|
|
1024
|
+
if (this.offset === start)
|
|
1025
|
+
this.fail(`Unexpected token in ${environment}`);
|
|
765
1026
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1027
|
+
const closed = this.source.startsWith(end, this.offset);
|
|
1028
|
+
if (!closed && !(this.incomplete && this.done()))
|
|
1029
|
+
this.fail(`Unclosed TeX environment: ${environment}`);
|
|
1030
|
+
if (closed)
|
|
1031
|
+
this.offset += end.length;
|
|
1032
|
+
if (!closed && !rows.length && !cells.length)
|
|
1033
|
+
cells.push(placeholder());
|
|
1034
|
+
if (cells.length || !rows.length)
|
|
1035
|
+
rows.push(cells);
|
|
1036
|
+
return { type: "matrix", rows, environment, ...columns !== undefined ? { columns } : {} };
|
|
1037
|
+
}
|
|
1038
|
+
readArrayColumns() {
|
|
1039
|
+
const group = this.readRawGroup();
|
|
1040
|
+
const columns = group.value.replace(/\s/g, "");
|
|
1041
|
+
if (/[^lcr|]/.test(columns) || !/[lcr]/.test(columns) && group.closed) {
|
|
1042
|
+
this.fail("Unsupported array columns; expected l, c, r, and |");
|
|
772
1043
|
}
|
|
773
|
-
|
|
1044
|
+
return columns;
|
|
774
1045
|
}
|
|
775
1046
|
parseLeftRight() {
|
|
776
1047
|
const left = this.readDelimiter();
|
|
@@ -794,7 +1065,7 @@ class Parser {
|
|
|
794
1065
|
}
|
|
795
1066
|
if (this.peek() === "}")
|
|
796
1067
|
this.fail("Unexpected closing TeX group");
|
|
797
|
-
const argument = this.
|
|
1068
|
+
const argument = this.parseAtom();
|
|
798
1069
|
if (argument)
|
|
799
1070
|
return argument;
|
|
800
1071
|
}
|
|
@@ -826,22 +1097,51 @@ class Parser {
|
|
|
826
1097
|
this.offset++;
|
|
827
1098
|
return result;
|
|
828
1099
|
}
|
|
829
|
-
|
|
1100
|
+
readTextGroup() {
|
|
1101
|
+
const group = this.readRawGroup(true);
|
|
1102
|
+
return (group.value || (group.closed ? "" : "□")).replace(/\\([A-Za-z@]+|.)/g, (match, command) => {
|
|
1103
|
+
if ("{}%#$&_ ".includes(command))
|
|
1104
|
+
return command;
|
|
1105
|
+
if (command === "textbackslash")
|
|
1106
|
+
return "\\";
|
|
1107
|
+
if (command === "!")
|
|
1108
|
+
return "";
|
|
1109
|
+
if (Object.hasOwn(spacing, command))
|
|
1110
|
+
return " ".repeat(Math.max(1, spacing[command]));
|
|
1111
|
+
return match;
|
|
1112
|
+
}).replaceAll("~", " ");
|
|
1113
|
+
}
|
|
1114
|
+
readRawGroup(preserveComments = false) {
|
|
830
1115
|
this.skipWhitespace();
|
|
1116
|
+
if (this.incomplete && this.done())
|
|
1117
|
+
return { value: "", closed: false };
|
|
831
1118
|
this.expect("{");
|
|
832
|
-
|
|
1119
|
+
let start = this.offset;
|
|
1120
|
+
const parts = [];
|
|
833
1121
|
let depth = 1;
|
|
834
1122
|
while (!this.done()) {
|
|
835
1123
|
const char = this.source[this.offset++];
|
|
1124
|
+
if (char === "%" && !preserveComments && !this.escaped(this.offset - 1)) {
|
|
1125
|
+
parts.push(this.source.slice(start, this.offset - 1));
|
|
1126
|
+
while (!this.done() && !/[\r\n]/.test(this.peek()))
|
|
1127
|
+
this.offset++;
|
|
1128
|
+
if (this.peek() === "\r")
|
|
1129
|
+
this.offset++;
|
|
1130
|
+
if (this.peek() === `
|
|
1131
|
+
`)
|
|
1132
|
+
this.offset++;
|
|
1133
|
+
start = this.offset;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
836
1136
|
if (char === "{" && !this.escaped(this.offset - 1))
|
|
837
1137
|
depth++;
|
|
838
1138
|
else if (char === "}" && !this.escaped(this.offset - 1) && --depth === 0)
|
|
839
|
-
return this.source.slice(start, this.offset - 1);
|
|
1139
|
+
return { value: parts.join("") + this.source.slice(start, this.offset - 1), closed: true };
|
|
840
1140
|
if (depth > MAX_NESTING_DEPTH)
|
|
841
1141
|
this.fail(`TeX nesting exceeds the ${MAX_NESTING_DEPTH}-level limit`);
|
|
842
1142
|
}
|
|
843
1143
|
if (this.incomplete)
|
|
844
|
-
return this.source.slice(start)
|
|
1144
|
+
return { value: parts.join("") + this.source.slice(start), closed: false };
|
|
845
1145
|
this.fail("Unclosed TeX group", start);
|
|
846
1146
|
}
|
|
847
1147
|
readCommand() {
|
|
@@ -868,11 +1168,32 @@ class Parser {
|
|
|
868
1168
|
if (this.peek() === "}")
|
|
869
1169
|
this.fail("Unexpected closing TeX group");
|
|
870
1170
|
if (this.peek() === "\\") {
|
|
1171
|
+
const start = this.offset;
|
|
871
1172
|
const command = this.readCommand();
|
|
872
|
-
|
|
1173
|
+
if (Object.hasOwn(delimiters, command))
|
|
1174
|
+
return (delimiters[`\\${command}`] ?? delimiters[command]) + this.readCombiningSuffix();
|
|
1175
|
+
if (this.incomplete && this.done() && this.offset === start + 1)
|
|
1176
|
+
return "";
|
|
1177
|
+
if (this.strict)
|
|
1178
|
+
this.fail(`Unsupported delimiter \\${command}`, start);
|
|
1179
|
+
return command + this.readCombiningSuffix();
|
|
873
1180
|
}
|
|
874
|
-
const token = this.
|
|
875
|
-
|
|
1181
|
+
const token = this.peek();
|
|
1182
|
+
if (Object.hasOwn(delimiters, token)) {
|
|
1183
|
+
this.offset++;
|
|
1184
|
+
return delimiters[token] + this.readCombiningSuffix();
|
|
1185
|
+
}
|
|
1186
|
+
if (this.strict)
|
|
1187
|
+
this.fail(`Unsupported delimiter ${token}`);
|
|
1188
|
+
return this.readLiteral();
|
|
1189
|
+
}
|
|
1190
|
+
readLiteral() {
|
|
1191
|
+
const part = this.graphemes.containing(this.offset);
|
|
1192
|
+
const literal = part.segment.slice(this.offset - part.index);
|
|
1193
|
+
const boundary = literal.search(/[\\{}[\]^_~&%\s]/u);
|
|
1194
|
+
const value = boundary > 0 ? literal.slice(0, boundary) : literal;
|
|
1195
|
+
this.offset += value.length;
|
|
1196
|
+
return value;
|
|
876
1197
|
}
|
|
877
1198
|
readCombiningSuffix() {
|
|
878
1199
|
if (this.done())
|
|
@@ -888,17 +1209,22 @@ class Parser {
|
|
|
888
1209
|
return;
|
|
889
1210
|
while (!this.done() && this.source[this.offset++] !== "]") {}
|
|
890
1211
|
}
|
|
891
|
-
isEnd(name) {
|
|
892
|
-
return this.source.startsWith(`\\end{${name}}`, this.offset);
|
|
893
|
-
}
|
|
894
1212
|
isCommand(name) {
|
|
895
1213
|
if (!this.source.startsWith(`\\${name}`, this.offset))
|
|
896
1214
|
return false;
|
|
897
1215
|
return !/[A-Za-z@]/.test(this.source[this.offset + name.length + 1] ?? "");
|
|
898
1216
|
}
|
|
899
1217
|
skipWhitespace() {
|
|
900
|
-
while (
|
|
901
|
-
this.
|
|
1218
|
+
while (!this.done()) {
|
|
1219
|
+
if (/\s/.test(this.peek())) {
|
|
1220
|
+
this.offset++;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (this.peek() !== "%")
|
|
1224
|
+
return;
|
|
1225
|
+
while (!this.done() && !/[\r\n]/.test(this.peek()))
|
|
1226
|
+
this.offset++;
|
|
1227
|
+
}
|
|
902
1228
|
}
|
|
903
1229
|
done() {
|
|
904
1230
|
return this.offset >= this.source.length;
|
|
@@ -927,6 +1253,11 @@ function row(body) {
|
|
|
927
1253
|
function placeholder() {
|
|
928
1254
|
return { type: "symbol", value: "□" };
|
|
929
1255
|
}
|
|
1256
|
+
function unwrapStyle(node) {
|
|
1257
|
+
while (node.type === "variant" || node.type === "color")
|
|
1258
|
+
node = node.body;
|
|
1259
|
+
return node;
|
|
1260
|
+
}
|
|
930
1261
|
function inferRole(value) {
|
|
931
1262
|
if ("+-*/×÷±∓".includes(value))
|
|
932
1263
|
return "binary";
|
|
@@ -941,7 +1272,24 @@ function inferRole(value) {
|
|
|
941
1272
|
return "ordinary";
|
|
942
1273
|
}
|
|
943
1274
|
function negate(value) {
|
|
944
|
-
return {
|
|
1275
|
+
return {
|
|
1276
|
+
"=": "≠",
|
|
1277
|
+
"<": "≮",
|
|
1278
|
+
">": "≯",
|
|
1279
|
+
"≤": "≰",
|
|
1280
|
+
"≥": "≱",
|
|
1281
|
+
"∈": "∉",
|
|
1282
|
+
"∋": "∌",
|
|
1283
|
+
"⊂": "⊄",
|
|
1284
|
+
"⊃": "⊅",
|
|
1285
|
+
"≡": "≢",
|
|
1286
|
+
"≈": "≉",
|
|
1287
|
+
"∼": "≁",
|
|
1288
|
+
"⊆": "⊈",
|
|
1289
|
+
"⊇": "⊉",
|
|
1290
|
+
"∣": "∤",
|
|
1291
|
+
"∥": "∦"
|
|
1292
|
+
}[value] ?? `${value}̸`;
|
|
945
1293
|
}
|
|
946
1294
|
|
|
947
1295
|
// src/unicode-tex-backend.ts
|
|
@@ -971,18 +1319,19 @@ function renderUnicode(request, incomplete) {
|
|
|
971
1319
|
}
|
|
972
1320
|
const widthMax = Math.floor(request.widthMax);
|
|
973
1321
|
const heightMax = Math.floor(request.heightMax);
|
|
974
|
-
const
|
|
975
|
-
const
|
|
1322
|
+
const options = { strict: request.strict };
|
|
1323
|
+
const node = incomplete ? parseMathIncomplete(request.formula, options) : parseMath(request.formula, options);
|
|
1324
|
+
const output = boxToOutput(layoutMath(node, request.display), widthMax, heightMax);
|
|
976
1325
|
assertNotAborted(request.signal);
|
|
977
|
-
if (!text)
|
|
1326
|
+
if (!output.text)
|
|
978
1327
|
throw new Error("TeX formula produced no Unicode output");
|
|
979
|
-
if (text.length > OUTPUT_LENGTH_MAX)
|
|
1328
|
+
if (output.text.length > OUTPUT_LENGTH_MAX)
|
|
980
1329
|
throw new Error(`Unicode TeX output exceeds ${OUTPUT_LENGTH_MAX} characters`);
|
|
981
|
-
const lines = text.split(`
|
|
1330
|
+
const lines = output.text.split(`
|
|
982
1331
|
`);
|
|
983
1332
|
return {
|
|
984
1333
|
kind: "unicode",
|
|
985
|
-
|
|
1334
|
+
...output,
|
|
986
1335
|
columns: Math.max(1, ...lines.map((line) => stringWidth2(line))),
|
|
987
1336
|
rows: lines.length
|
|
988
1337
|
};
|
|
@@ -1031,6 +1380,7 @@ class TexRenderable extends BoxRenderable {
|
|
|
1031
1380
|
ready;
|
|
1032
1381
|
backend;
|
|
1033
1382
|
fallback;
|
|
1383
|
+
strict;
|
|
1034
1384
|
widthMax;
|
|
1035
1385
|
heightMax;
|
|
1036
1386
|
autoWidth;
|
|
@@ -1059,6 +1409,7 @@ class TexRenderable extends BoxRenderable {
|
|
|
1059
1409
|
imageOptions,
|
|
1060
1410
|
onError,
|
|
1061
1411
|
streaming = false,
|
|
1412
|
+
strict = false,
|
|
1062
1413
|
...boxOptions
|
|
1063
1414
|
} = options;
|
|
1064
1415
|
super(context, {
|
|
@@ -1075,6 +1426,7 @@ class TexRenderable extends BoxRenderable {
|
|
|
1075
1426
|
this._display = display;
|
|
1076
1427
|
this.backend = backend;
|
|
1077
1428
|
this.fallback = fallback;
|
|
1429
|
+
this.strict = strict;
|
|
1078
1430
|
this.widthMax = dimensionMax(widthMax);
|
|
1079
1431
|
this.heightMax = dimensionMax(heightMax);
|
|
1080
1432
|
this.autoWidth = options.width == null || options.width === "auto";
|
|
@@ -1212,18 +1564,24 @@ class TexRenderable extends BoxRenderable {
|
|
|
1212
1564
|
background,
|
|
1213
1565
|
widthMax: this.widthMax,
|
|
1214
1566
|
heightMax: this.heightMax,
|
|
1215
|
-
signal: controller.signal
|
|
1567
|
+
signal: controller.signal,
|
|
1568
|
+
strict: this.strict
|
|
1216
1569
|
};
|
|
1217
1570
|
if (this._streaming) {
|
|
1218
|
-
|
|
1571
|
+
try {
|
|
1572
|
+
this.applyOutput(this.previewOutput(request));
|
|
1573
|
+
} catch {
|
|
1574
|
+
this.applyOutput(rawSourceOutput(formula, this.widthMax, this.heightMax));
|
|
1575
|
+
}
|
|
1219
1576
|
return;
|
|
1220
1577
|
}
|
|
1221
1578
|
let unicodeOutput = null;
|
|
1222
1579
|
const synchronousBackend = this.backend instanceof UnicodeTexBackend && this.backend.render === UNICODE_RENDER && this.backend.renderSync === UNICODE_RENDER_SYNC ? this.backend : null;
|
|
1223
1580
|
if (!synchronousBackend) {
|
|
1224
1581
|
try {
|
|
1225
|
-
|
|
1226
|
-
this.applyOutput(
|
|
1582
|
+
const preview = DEFAULT_PREVIEW_BACKEND.renderSync(request);
|
|
1583
|
+
this.applyOutput(preview);
|
|
1584
|
+
unicodeOutput = preview;
|
|
1227
1585
|
} catch {}
|
|
1228
1586
|
}
|
|
1229
1587
|
try {
|
|
@@ -1285,7 +1643,12 @@ class TexRenderable extends BoxRenderable {
|
|
|
1285
1643
|
applyOutput(output) {
|
|
1286
1644
|
const dimensions = output.kind === "image" ? measureTex(output.image.width, output.image.height, this.display, this.widthMax, this.heightMax) : { columns: Math.min(this.widthMax, output.columns), rows: Math.min(this.heightMax, output.rows) };
|
|
1287
1645
|
const child = output.kind === "image" ? this.createImageChild(output.image, dimensions) : new TextRenderable(this._ctx, {
|
|
1288
|
-
content: output.
|
|
1646
|
+
content: output.spans ? new StyledText(output.spans.map((span) => ({
|
|
1647
|
+
__isChunk: true,
|
|
1648
|
+
text: span.text,
|
|
1649
|
+
fg: span.color === undefined ? undefined : parseColor(span.color),
|
|
1650
|
+
attributes: createTextAttributes({ bold: span.bold, italic: span.italic })
|
|
1651
|
+
}))) : output.text,
|
|
1289
1652
|
fg: this._foreground,
|
|
1290
1653
|
bg: this._background,
|
|
1291
1654
|
wrapMode: "none",
|