@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.
@@ -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 boxToString(box, widthMax, heightMax) {
73
- const lines = [];
75
+ function boxToOutput(box, widthMax, heightMax) {
76
+ const spans = [];
74
77
  for (let y = 0;y < Math.min(box.height, heightMax); y++) {
75
- let line = "";
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 += value;
92
+ appendSpan(line, value.char, value.style);
90
93
  width += cellWidth;
91
94
  x += cellWidth - 1;
92
95
  }
93
- lines.push(line.trimEnd());
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
- return lines.join(`
96
- `).trimEnd();
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 layout(node, display) {
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, display);
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, display), layout(node.denominator, display), node.bar);
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, display), node.index ? layout(node.index, display) : undefined);
137
+ return root(layout(node.body, context), node.index ? layout(node.index, context) : undefined, context.style);
112
138
  case "scripts":
113
- return scripts(node, display);
139
+ return scripts(node, context);
114
140
  case "delimited":
115
- return delimited(node.left, layout(node.body, display), node.right);
141
+ return delimited(node.left, layout(node.body, context), node.right, context.style);
116
142
  case "matrix":
117
- return matrix(node.rows, node.environment, display);
143
+ return matrix(node, context);
118
144
  case "accent":
119
- return accent(node.accent, layout(node.body, display));
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, display), node.over ? layout(node.over, display) : undefined, node.under ? layout(node.under, display) : undefined);
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, display) {
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], display));
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, display) {
161
- const base = layout(node.base, display);
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
- const limits = node.base.type === "operator" && (node.base.limits === true || node.base.limits === "display" && display);
165
- if (!limits) {
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 ((!node.superscript || mappedSup !== undefined) && (!node.subscript || mappedSub !== undefined)) {
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 result = blank(base.width + sideWidth, (sup?.height ?? 0) + base.height + (sub?.height ?? 0), (sup?.height ?? 0) + base.baseline);
178
- overlay(result, base, 0, sup?.height ?? 0);
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, (sup?.height ?? 0) + base.height);
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 result = blank(width, (over?.height ?? 0) + base.height + (under?.height ?? 0), (over?.height ?? 0) + base.baseline);
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), over?.height ?? 0);
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), (over?.height ?? 0) + base.height);
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(rows, environment, display) {
199
- const cells = rows.map((row) => row.map((node) => layout(node, display)));
200
- const columns = Math.max(0, ...cells.map((row) => row.length));
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 gap = environment === "cases" || environment === "aligned" || environment === "align" ? 2 : 1;
206
- const width = widths.reduce((sum, value) => sum + value, 0) + Math.max(0, columns - 1) * gap;
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 aligned = environment === "aligned" || environment === "align" || environment === "cases";
216
- const offset = aligned ? column % 2 === 0 ? widths[column] - cell.width : 0 : Math.floor((widths[column] - cell.width) / 2);
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] + gap;
266
+ x += widths[column] + gaps[column + 1];
220
267
  }
221
268
  y += heights[rowIndex];
222
269
  }
223
- const pair = matrixDelimiters(environment);
224
- return pair ? delimited(pair[0], result, pair[1]) : result;
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 accent(kind, body) {
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, value) {
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] = value;
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(["sin", "cos", "tan", "log", "ln", "exp", "lim", "min", "max", "sup", "inf", "det", "gcd", "ker"]);
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
- function parseMath(source) {
575
- return new Parser(source).parse();
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 = false) {
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
- if (atom && "value" in atom)
634
- atom.value += this.readCombiningSuffix();
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 part = this.graphemes.containing(this.offset);
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
- return { type: "fraction", numerator: this.parseArgument(), denominator: this.parseArgument(), bar: true };
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 in accents)
909
+ if (Object.hasOwn(accents, command))
677
910
  return { type: "accent", accent: accents[command], body: this.parseArgument() };
678
- if (["mathrm", "mathbf", "mathit", "mathsf", "mathtt", "mathbb", "mathcal", "mathfrak"].includes(command)) {
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 (["text", "textrm", "mbox"].includes(command)) {
682
- return { type: "text", value: this.readRawGroup().replace(/\\([{}%#$&_])/g, "$1").replaceAll("~", " ") };
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
- if (target.type === "symbol")
697
- return { ...target, value: negate(target.value) };
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
- const base = previous?.type === "scripts" ? previous.base : previous;
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 in spacing)
971
+ if (Object.hasOwn(spacing, command))
711
972
  return { type: "space", width: spacing[command] };
712
- if (command in symbols)
973
+ if (Object.hasOwn(symbols, command))
713
974
  return { type: "symbol", ...symbols[command] };
714
- if (command in operators)
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: ["lim", "min", "max"].includes(command) ? "display" : false };
718
- if (command in delimiters)
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
- if (name === "array") {
730
- this.skipWhitespace();
731
- if (this.peek() === "{")
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.isEnd(rawName)) {
741
- this.offset += `\\end{${rawName}}`.length;
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("\\end{", this.offset))));
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
- if (this.incomplete) {
767
- if (cells.length)
768
- rows.push(cells);
769
- else if (!rows.length)
770
- rows.push([placeholder()]);
771
- return { type: "matrix", rows, environment: name };
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
- this.fail(`Unclosed TeX environment: ${rawName}`);
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.peek() === "{" ? this.parseGroup() : this.parseAtom();
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
- readRawGroup() {
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
- const start = this.offset;
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
- return (delimiters[`\\${command}`] ?? delimiters[command] ?? command) + this.readCombiningSuffix();
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.source[this.offset++] ?? "";
875
- return (delimiters[token] ?? token) + this.readCombiningSuffix();
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 (/\s/.test(this.peek()))
901
- this.offset++;
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 { "=": "≠", "<": "≮", ">": "≯", "≤": "≰", "≥": "≱", "∈": "∉", "∋": "∌", "⊂": "⊄", "⊃": "⊅" }[value] ?? `${value}̸`;
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 node = incomplete ? parseMathIncomplete(request.formula) : parseMath(request.formula);
975
- const text = boxToString(layoutMath(node, request.display), widthMax, heightMax);
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
- text,
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
- this.applyOutput(this.previewOutput(request));
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
- unicodeOutput = DEFAULT_PREVIEW_BACKEND.renderSync(request);
1226
- this.applyOutput(unicodeOutput);
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.text,
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",