@oh-my-pi/pi-tui 18.0.7 → 18.0.9
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/CHANGELOG.md +29 -0
- package/dist/types/components/box.d.ts +2 -0
- package/dist/types/components/cancellable-loader.d.ts +2 -0
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +2 -0
- package/dist/types/components/scroll-view.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/components/settings-list.d.ts +4 -0
- package/dist/types/components/spacer.d.ts +2 -0
- package/dist/types/components/tab-bar.d.ts +2 -0
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/components/truncated-text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/latex-to-unicode.d.ts +4 -4
- package/dist/types/terminal.d.ts +11 -0
- package/dist/types/tui.d.ts +30 -0
- package/package.json +3 -3
- package/src/components/box.ts +10 -0
- package/src/components/cancellable-loader.ts +7 -0
- package/src/components/editor.ts +28 -0
- package/src/components/image.ts +11 -0
- package/src/components/input.ts +13 -0
- package/src/components/loader.ts +21 -5
- package/src/components/markdown.ts +39 -10
- package/src/components/scroll-view.ts +12 -0
- package/src/components/select-list.ts +13 -0
- package/src/components/settings-list.ts +19 -0
- package/src/components/spacer.ts +4 -0
- package/src/components/tab-bar.ts +11 -0
- package/src/components/text.ts +11 -0
- package/src/components/truncated-text.ts +15 -1
- package/src/debug-server.ts +422 -0
- package/src/index.ts +2 -0
- package/src/latex-to-unicode.ts +401 -172
- package/src/terminal.ts +22 -2
- package/src/tui.ts +115 -3
- package/src/utils.ts +10 -1
package/src/latex-to-unicode.ts
CHANGED
|
@@ -313,29 +313,66 @@ const FONTS: Record<string, FontStyle> = {
|
|
|
313
313
|
mathtt: "mono",
|
|
314
314
|
mathit: "italic",
|
|
315
315
|
mathbfit: "bolditalic",
|
|
316
|
-
textbf: "bold",
|
|
317
|
-
textit: "italic",
|
|
318
|
-
texttt: "mono",
|
|
319
|
-
textsf: "sans",
|
|
320
316
|
};
|
|
317
|
+
|
|
318
|
+
interface ParseStyle {
|
|
319
|
+
font: FontStyle | null;
|
|
320
|
+
bold: boolean;
|
|
321
|
+
italic: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface RenderStyle {
|
|
325
|
+
bold: boolean;
|
|
326
|
+
italic: boolean;
|
|
327
|
+
foreground: string | null;
|
|
328
|
+
background: string | null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type StyleAttribute = "bold" | "italic";
|
|
332
|
+
|
|
333
|
+
type RenderEvent =
|
|
334
|
+
| { kind: "scope-start"; attribute: StyleAttribute }
|
|
335
|
+
| { kind: "scope-end"; attribute: StyleAttribute; restore: boolean };
|
|
336
|
+
|
|
337
|
+
interface RenderChunk {
|
|
338
|
+
text: string;
|
|
339
|
+
style: RenderStyle;
|
|
340
|
+
before?: RenderEvent[];
|
|
341
|
+
after?: RenderEvent[];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
type Rendered = RenderChunk[];
|
|
345
|
+
|
|
346
|
+
const ROOT_STYLE: ParseStyle = { font: null, bold: false, italic: false };
|
|
347
|
+
|
|
348
|
+
const TEXT_STYLE_COMMANDS: Readonly<Record<string, Partial<Pick<ParseStyle, "bold" | "italic">>>> = {
|
|
349
|
+
textbf: { bold: true },
|
|
350
|
+
textit: { italic: true },
|
|
351
|
+
textsl: { italic: true },
|
|
352
|
+
emph: { italic: true },
|
|
353
|
+
textmd: { bold: false },
|
|
354
|
+
textup: { italic: false },
|
|
355
|
+
texttt: {},
|
|
356
|
+
textsf: {},
|
|
357
|
+
};
|
|
358
|
+
|
|
321
359
|
/**
|
|
322
|
-
* Math font command names
|
|
323
|
-
*
|
|
324
|
-
* re-wraps inline runs inside these commands when their argument
|
|
325
|
-
*
|
|
360
|
+
* Math font and text-style command names whose single brace argument
|
|
361
|
+
* is rendered with the corresponding style. Exported for the display block
|
|
362
|
+
* engine, which re-wraps inline runs inside these commands when their argument
|
|
363
|
+
* contains 2-D layout so styling survives box boundaries.
|
|
326
364
|
*/
|
|
327
|
-
export const MATH_FONT_COMMANDS: ReadonlySet<string> = new Set(
|
|
365
|
+
export const MATH_FONT_COMMANDS: ReadonlySet<string> = new Set([
|
|
366
|
+
...Object.keys(FONTS),
|
|
367
|
+
...Object.keys(TEXT_STYLE_COMMANDS),
|
|
368
|
+
]);
|
|
328
369
|
|
|
329
370
|
// Text-mode commands whose argument is passed through literally (no math).
|
|
330
371
|
const TEXT_COMMANDS: Record<string, true> = {
|
|
331
372
|
text: true,
|
|
332
373
|
textrm: true,
|
|
333
374
|
textnormal: true,
|
|
334
|
-
textup: true,
|
|
335
|
-
textmd: true,
|
|
336
375
|
textsc: true,
|
|
337
|
-
textsl: true,
|
|
338
|
-
emph: true,
|
|
339
376
|
mathrm: true,
|
|
340
377
|
mathnormal: true,
|
|
341
378
|
mbox: true,
|
|
@@ -842,22 +879,84 @@ const SYMBOLS: Record<string, string> = {
|
|
|
842
879
|
// Helpers
|
|
843
880
|
// ---------------------------------------------------------------------------
|
|
844
881
|
|
|
882
|
+
const ANSI_FG_RESET = "\x1b[39m";
|
|
883
|
+
const ANSI_BG_RESET = "\x1b[49m";
|
|
884
|
+
const ANSI_BOLD_ON = "\x1b[1m";
|
|
885
|
+
const ANSI_BOLD_OFF = "\x1b[22m";
|
|
886
|
+
const ANSI_ITALIC_ON = "\x1b[3m";
|
|
887
|
+
const ANSI_ITALIC_OFF = "\x1b[23m";
|
|
888
|
+
|
|
889
|
+
const DEFAULT_RENDER_STYLE: RenderStyle = {
|
|
890
|
+
bold: false,
|
|
891
|
+
italic: false,
|
|
892
|
+
foreground: null,
|
|
893
|
+
background: null,
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
function renderStyle(style: ParseStyle, foreground: string | null, background: string | null): RenderStyle {
|
|
897
|
+
return { bold: style.bold, italic: style.italic, foreground, background };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function sameRenderStyle(a: RenderStyle, b: RenderStyle): boolean {
|
|
901
|
+
return a.bold === b.bold && a.italic === b.italic && a.foreground === b.foreground && a.background === b.background;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function appendRendered(target: Rendered, source: Rendered): void {
|
|
905
|
+
for (const chunk of source) {
|
|
906
|
+
if (chunk.text === "") continue;
|
|
907
|
+
const previous = target[target.length - 1];
|
|
908
|
+
if (
|
|
909
|
+
previous !== undefined &&
|
|
910
|
+
previous.after === undefined &&
|
|
911
|
+
chunk.before === undefined &&
|
|
912
|
+
chunk.after === undefined &&
|
|
913
|
+
sameRenderStyle(previous.style, chunk.style)
|
|
914
|
+
) {
|
|
915
|
+
previous.text += chunk.text;
|
|
916
|
+
} else {
|
|
917
|
+
target.push({ ...chunk });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function concatRendered(...parts: Rendered[]): Rendered {
|
|
923
|
+
const out: Rendered = [];
|
|
924
|
+
for (const part of parts) appendRendered(out, part);
|
|
925
|
+
return out;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function styledText(text: string, style: RenderStyle): Rendered {
|
|
929
|
+
return text === "" ? [] : [{ text, style }];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function plainText(rendered: Rendered): string {
|
|
933
|
+
let text = "";
|
|
934
|
+
for (const chunk of rendered) text += chunk.text;
|
|
935
|
+
return text;
|
|
936
|
+
}
|
|
937
|
+
|
|
845
938
|
/** Map every code point of `text` through `table`; null if any is unmappable. */
|
|
846
|
-
function mapAll(text:
|
|
847
|
-
|
|
848
|
-
for (const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
939
|
+
function mapAll(text: Rendered, table: Record<string, string>): Rendered | null {
|
|
940
|
+
const out: Rendered = [];
|
|
941
|
+
for (const chunk of text) {
|
|
942
|
+
let mapped = "";
|
|
943
|
+
for (const ch of chunk.text) {
|
|
944
|
+
const replacement = table[ch];
|
|
945
|
+
if (replacement === undefined) return null;
|
|
946
|
+
mapped += replacement;
|
|
947
|
+
}
|
|
948
|
+
appendRendered(out, [{ ...chunk, text: mapped }]);
|
|
852
949
|
}
|
|
853
950
|
return out;
|
|
854
951
|
}
|
|
855
952
|
|
|
856
|
-
/** Number of Unicode code points (not UTF-16 units) in
|
|
857
|
-
function codePointLength(
|
|
858
|
-
let
|
|
859
|
-
for (const
|
|
860
|
-
|
|
953
|
+
/** Number of Unicode code points (not UTF-16 units) in rendered glyph text. */
|
|
954
|
+
function codePointLength(text: Rendered): number {
|
|
955
|
+
let length = 0;
|
|
956
|
+
for (const chunk of text) {
|
|
957
|
+
for (const _ of chunk.text) length++;
|
|
958
|
+
}
|
|
959
|
+
return length;
|
|
861
960
|
}
|
|
862
961
|
|
|
863
962
|
/** Style a single ASCII letter/digit via the math alphanumeric block. */
|
|
@@ -873,28 +972,121 @@ function styleAlnum(ch: string, style: FontStyle): string {
|
|
|
873
972
|
}
|
|
874
973
|
|
|
875
974
|
/** Identity, or math-alphanumeric styling when a font style is active. */
|
|
876
|
-
function styleChar(ch: string, style:
|
|
877
|
-
if (style === null) return ch;
|
|
975
|
+
function styleChar(ch: string, style: ParseStyle): string {
|
|
976
|
+
if (style.font === null) return ch;
|
|
878
977
|
const code = ch.charCodeAt(0);
|
|
879
978
|
const isAlnum = (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57);
|
|
880
|
-
return isAlnum ? styleAlnum(ch, style) : ch;
|
|
979
|
+
return isAlnum ? styleAlnum(ch, style.font) : ch;
|
|
980
|
+
}
|
|
981
|
+
function surroundRendered(text: Rendered, left: string, right: string, fallback: RenderStyle): Rendered {
|
|
982
|
+
return concatRendered(styledText(left, fallback), text, styledText(right, fallback));
|
|
983
|
+
}
|
|
984
|
+
function trimRendered(text: Rendered): Rendered {
|
|
985
|
+
const out = text.map(chunk => ({ ...chunk }));
|
|
986
|
+
while (out.length > 0) {
|
|
987
|
+
const first = out[0];
|
|
988
|
+
if (first === undefined) break;
|
|
989
|
+
first.text = first.text.trimStart();
|
|
990
|
+
if (first.text !== "") break;
|
|
991
|
+
out.shift();
|
|
992
|
+
}
|
|
993
|
+
while (out.length > 0) {
|
|
994
|
+
const lastIndex = out.length - 1;
|
|
995
|
+
const last = out[lastIndex];
|
|
996
|
+
if (last === undefined) break;
|
|
997
|
+
last.text = last.text.trimEnd();
|
|
998
|
+
if (last.text !== "") break;
|
|
999
|
+
out.pop();
|
|
1000
|
+
}
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
function scopeRendered(text: Rendered, attribute: StyleAttribute, parentActive: boolean): Rendered {
|
|
1004
|
+
if (!parentActive || text.length === 0) return text;
|
|
1005
|
+
const out = text.map(chunk => ({ ...chunk }));
|
|
1006
|
+
const first = out[0];
|
|
1007
|
+
const last = out[out.length - 1];
|
|
1008
|
+
if (first?.style[attribute]) {
|
|
1009
|
+
first.before = [...(first.before ?? []), { kind: "scope-start", attribute }];
|
|
1010
|
+
}
|
|
1011
|
+
if (last?.style[attribute]) {
|
|
1012
|
+
last.after = [...(last.after ?? []), { kind: "scope-end", attribute, restore: true }];
|
|
1013
|
+
}
|
|
1014
|
+
return out;
|
|
881
1015
|
}
|
|
882
1016
|
|
|
883
|
-
/** Append a combining mark after each non-space base glyph
|
|
884
|
-
function applyCombining(text:
|
|
885
|
-
|
|
886
|
-
for (const
|
|
1017
|
+
/** Append a combining mark after each non-space base glyph. */
|
|
1018
|
+
function applyCombining(text: Rendered, mark: string): Rendered {
|
|
1019
|
+
const out: Rendered = [];
|
|
1020
|
+
for (const chunk of text) {
|
|
1021
|
+
let mapped = "";
|
|
1022
|
+
for (const ch of chunk.text) mapped += ch === " " ? ch : ch + mark;
|
|
1023
|
+
appendRendered(out, [{ ...chunk, text: mapped }]);
|
|
1024
|
+
}
|
|
887
1025
|
return out;
|
|
888
1026
|
}
|
|
889
1027
|
|
|
1028
|
+
function emitStyleTransition(out: string[], from: RenderStyle, to: RenderStyle): void {
|
|
1029
|
+
if (from.bold && !to.bold) out.push(ANSI_BOLD_OFF);
|
|
1030
|
+
if (from.italic && !to.italic) out.push(ANSI_ITALIC_OFF);
|
|
1031
|
+
const foregroundChanged = from.foreground !== to.foreground;
|
|
1032
|
+
const backgroundChanged = from.background !== to.background;
|
|
1033
|
+
if (foregroundChanged && backgroundChanged) {
|
|
1034
|
+
if (to.background === null) {
|
|
1035
|
+
out.push(to.background ?? ANSI_BG_RESET);
|
|
1036
|
+
out.push(to.foreground ?? ANSI_FG_RESET);
|
|
1037
|
+
} else {
|
|
1038
|
+
out.push(to.foreground ?? ANSI_FG_RESET);
|
|
1039
|
+
out.push(to.background ?? ANSI_BG_RESET);
|
|
1040
|
+
}
|
|
1041
|
+
} else {
|
|
1042
|
+
if (foregroundChanged) out.push(to.foreground ?? ANSI_FG_RESET);
|
|
1043
|
+
if (backgroundChanged) out.push(to.background ?? ANSI_BG_RESET);
|
|
1044
|
+
}
|
|
1045
|
+
if (!from.bold && to.bold) out.push(ANSI_BOLD_ON);
|
|
1046
|
+
if (!from.italic && to.italic) out.push(ANSI_ITALIC_ON);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function emitStyleEvent(out: string[], event: RenderEvent, active: RenderStyle): RenderStyle {
|
|
1050
|
+
if (event.kind === "scope-start") {
|
|
1051
|
+
out.push(event.attribute === "bold" ? ANSI_BOLD_ON : ANSI_ITALIC_ON);
|
|
1052
|
+
return { ...active, [event.attribute]: true };
|
|
1053
|
+
}
|
|
1054
|
+
if (active[event.attribute]) out.push(event.attribute === "bold" ? ANSI_BOLD_OFF : ANSI_ITALIC_OFF);
|
|
1055
|
+
if (event.restore) out.push(event.attribute === "bold" ? ANSI_BOLD_ON : ANSI_ITALIC_ON);
|
|
1056
|
+
return { ...active, [event.attribute]: event.restore };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Materialize terminal attributes only after all glyph transforms are complete. */
|
|
1060
|
+
function renderAnsi(text: Rendered): string {
|
|
1061
|
+
let out = "";
|
|
1062
|
+
let active = DEFAULT_RENDER_STYLE;
|
|
1063
|
+
for (const chunk of text) {
|
|
1064
|
+
const transitions: string[] = [];
|
|
1065
|
+
emitStyleTransition(transitions, active, chunk.style);
|
|
1066
|
+
out += transitions.join("");
|
|
1067
|
+
active = chunk.style;
|
|
1068
|
+
for (const event of chunk.before ?? []) {
|
|
1069
|
+
const events: string[] = [];
|
|
1070
|
+
active = emitStyleEvent(events, event, active);
|
|
1071
|
+
out += events.join("");
|
|
1072
|
+
}
|
|
1073
|
+
out += chunk.text;
|
|
1074
|
+
for (const event of chunk.after ?? []) {
|
|
1075
|
+
const events: string[] = [];
|
|
1076
|
+
active = emitStyleEvent(events, event, active);
|
|
1077
|
+
out += events.join("");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const transitions: string[] = [];
|
|
1081
|
+
emitStyleTransition(transitions, active, DEFAULT_RENDER_STYLE);
|
|
1082
|
+
return out + transitions.join("");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
890
1085
|
/** Light unescape for text-mode content (`\&` → `&`, `~` → space). */
|
|
891
1086
|
function unescapeText(s: string): string {
|
|
892
1087
|
return s.replace(/\\([&%$#_{}\s])/g, "$1").replace(/~/g, " ");
|
|
893
1088
|
}
|
|
894
1089
|
|
|
895
|
-
const ANSI_FG_RESET = "\x1b[39m";
|
|
896
|
-
const ANSI_BG_RESET = "\x1b[49m";
|
|
897
|
-
|
|
898
1090
|
type AnsiColorFormat = "ansi-16m" | "ansi-256";
|
|
899
1091
|
|
|
900
1092
|
interface AnsiColor {
|
|
@@ -1160,30 +1352,18 @@ export function latexColorScope(model: string | null, spec: string): ((text: str
|
|
|
1160
1352
|
return text => foreground + text.replaceAll(ANSI_FG_RESET, foreground) + ANSI_FG_RESET;
|
|
1161
1353
|
}
|
|
1162
1354
|
|
|
1163
|
-
function
|
|
1164
|
-
text
|
|
1165
|
-
fromForeground: string | null,
|
|
1166
|
-
toForeground: string | null,
|
|
1167
|
-
fromBackground: string | null,
|
|
1168
|
-
toBackground: string | null,
|
|
1169
|
-
): string {
|
|
1170
|
-
if (fromForeground !== toForeground && fromForeground !== null) text += toForeground ?? ANSI_FG_RESET;
|
|
1171
|
-
if (fromBackground !== toBackground && fromBackground !== null) text += toBackground ?? ANSI_BG_RESET;
|
|
1172
|
-
return text;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function toSuperscript(text: string, group: boolean): string {
|
|
1176
|
-
if (text === "") return "";
|
|
1355
|
+
function toSuperscript(text: Rendered, group: boolean, fallback: RenderStyle): Rendered {
|
|
1356
|
+
if (text.length === 0) return [];
|
|
1177
1357
|
const mapped = mapAll(text, SUPERSCRIPT);
|
|
1178
1358
|
if (mapped !== null) return mapped;
|
|
1179
|
-
return group ?
|
|
1359
|
+
return concatRendered(styledText(group ? "^(" : "^", fallback), text, styledText(group ? ")" : "", fallback));
|
|
1180
1360
|
}
|
|
1181
1361
|
|
|
1182
|
-
function toSubscript(text:
|
|
1183
|
-
if (text ===
|
|
1362
|
+
function toSubscript(text: Rendered, group: boolean, fallback: RenderStyle): Rendered {
|
|
1363
|
+
if (text.length === 0) return [];
|
|
1184
1364
|
const mapped = mapAll(text, SUBSCRIPT);
|
|
1185
1365
|
if (mapped !== null) return mapped;
|
|
1186
|
-
return group ?
|
|
1366
|
+
return concatRendered(styledText(group ? "_(" : "_", fallback), text, styledText(group ? ")" : "", fallback));
|
|
1187
1367
|
}
|
|
1188
1368
|
|
|
1189
1369
|
// ---------------------------------------------------------------------------
|
|
@@ -1191,7 +1371,7 @@ function toSubscript(text: string, group: boolean): string {
|
|
|
1191
1371
|
// ---------------------------------------------------------------------------
|
|
1192
1372
|
|
|
1193
1373
|
interface Argument {
|
|
1194
|
-
text:
|
|
1374
|
+
text: Rendered;
|
|
1195
1375
|
/** True when the argument came from a `{…}` group (affects fraction/script parens). */
|
|
1196
1376
|
group: boolean;
|
|
1197
1377
|
}
|
|
@@ -1229,12 +1409,12 @@ class LatexParser {
|
|
|
1229
1409
|
}
|
|
1230
1410
|
|
|
1231
1411
|
render(): string {
|
|
1232
|
-
return
|
|
1412
|
+
return renderAnsi(this.parse(ROOT_STYLE, false));
|
|
1233
1413
|
}
|
|
1234
1414
|
|
|
1235
1415
|
/** Parse a run until end-of-input, or until `}` when `stopAtBrace`. */
|
|
1236
|
-
parse(style:
|
|
1237
|
-
|
|
1416
|
+
parse(style: ParseStyle, stopAtBrace: boolean): Rendered {
|
|
1417
|
+
const out: Rendered = [];
|
|
1238
1418
|
while (this.#i < this.#s.length) {
|
|
1239
1419
|
const c = this.#s[this.#i];
|
|
1240
1420
|
if (c === "}") {
|
|
@@ -1242,12 +1422,16 @@ class LatexParser {
|
|
|
1242
1422
|
this.#i++; // stray close brace
|
|
1243
1423
|
continue;
|
|
1244
1424
|
}
|
|
1245
|
-
out
|
|
1425
|
+
appendRendered(out, this.#node(style));
|
|
1246
1426
|
}
|
|
1247
1427
|
return out;
|
|
1248
1428
|
}
|
|
1249
1429
|
|
|
1250
|
-
#
|
|
1430
|
+
#renderStyle(style: ParseStyle): RenderStyle {
|
|
1431
|
+
return renderStyle(style, this.#foreground, this.#background);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
#node(style: ParseStyle): Rendered {
|
|
1251
1435
|
const c = this.#s[this.#i];
|
|
1252
1436
|
switch (c) {
|
|
1253
1437
|
case "\\":
|
|
@@ -1262,41 +1446,41 @@ class LatexParser {
|
|
|
1262
1446
|
return this.#script(style, false);
|
|
1263
1447
|
case "$":
|
|
1264
1448
|
this.#i++;
|
|
1265
|
-
return
|
|
1449
|
+
return []; // stray delimiter
|
|
1266
1450
|
case "~":
|
|
1267
1451
|
this.#i++;
|
|
1268
|
-
return " "; // non-breaking space
|
|
1452
|
+
return styledText(" ", this.#renderStyle(style)); // non-breaking space
|
|
1269
1453
|
case "&":
|
|
1270
1454
|
this.#i++;
|
|
1271
|
-
return " "; // column separator
|
|
1455
|
+
return styledText(" ", this.#renderStyle(style)); // column separator
|
|
1272
1456
|
case "'": {
|
|
1273
1457
|
let k = 0;
|
|
1274
1458
|
while (this.#s[this.#i] === "'") {
|
|
1275
1459
|
k++;
|
|
1276
1460
|
this.#i++;
|
|
1277
1461
|
}
|
|
1278
|
-
return k <= 4 ? PRIMES[k] : PRIMES[1].repeat(k);
|
|
1462
|
+
return styledText(k <= 4 ? PRIMES[k] : PRIMES[1].repeat(k), this.#renderStyle(style));
|
|
1279
1463
|
}
|
|
1280
1464
|
case "%": {
|
|
1281
1465
|
const nl = this.#s.indexOf("\n", this.#i);
|
|
1282
1466
|
this.#i = nl === -1 ? this.#s.length : nl + 1;
|
|
1283
|
-
return
|
|
1467
|
+
return [];
|
|
1284
1468
|
}
|
|
1285
1469
|
default:
|
|
1286
1470
|
this.#i++;
|
|
1287
|
-
return styleChar(c, style);
|
|
1471
|
+
return styledText(styleChar(c, style), this.#renderStyle(style));
|
|
1288
1472
|
}
|
|
1289
1473
|
}
|
|
1290
1474
|
|
|
1291
|
-
#command(style:
|
|
1475
|
+
#command(style: ParseStyle): Rendered {
|
|
1292
1476
|
this.#i++; // past backslash
|
|
1293
|
-
if (this.#i >= this.#s.length) return
|
|
1477
|
+
if (this.#i >= this.#s.length) return [];
|
|
1294
1478
|
const c = this.#s[this.#i];
|
|
1295
1479
|
if (!/[A-Za-z]/.test(c)) {
|
|
1296
1480
|
this.#i++;
|
|
1297
1481
|
switch (c) {
|
|
1298
1482
|
case "\\":
|
|
1299
|
-
return "\n"; // row break
|
|
1483
|
+
return styledText("\n", this.#renderStyle(style)); // row break
|
|
1300
1484
|
case "{":
|
|
1301
1485
|
case "}":
|
|
1302
1486
|
case "$":
|
|
@@ -1306,25 +1490,25 @@ class LatexParser {
|
|
|
1306
1490
|
case "_":
|
|
1307
1491
|
case " ":
|
|
1308
1492
|
case ".":
|
|
1309
|
-
return c;
|
|
1493
|
+
return styledText(c, this.#renderStyle(style));
|
|
1310
1494
|
case ",":
|
|
1311
1495
|
case ":":
|
|
1312
1496
|
case ";":
|
|
1313
1497
|
case ">":
|
|
1314
|
-
return " "; // spacing
|
|
1498
|
+
return styledText(" ", this.#renderStyle(style)); // spacing
|
|
1315
1499
|
case "!":
|
|
1316
|
-
return
|
|
1500
|
+
return []; // negative thin space
|
|
1317
1501
|
case "/":
|
|
1318
|
-
return
|
|
1502
|
+
return []; // italic correction
|
|
1319
1503
|
case "|":
|
|
1320
|
-
return "‖";
|
|
1504
|
+
return styledText("‖", this.#renderStyle(style));
|
|
1321
1505
|
case "(":
|
|
1322
1506
|
case ")":
|
|
1323
1507
|
case "[":
|
|
1324
1508
|
case "]":
|
|
1325
|
-
return
|
|
1509
|
+
return []; // bare math delimiters that slipped through
|
|
1326
1510
|
default:
|
|
1327
|
-
return c;
|
|
1511
|
+
return styledText(c, this.#renderStyle(style));
|
|
1328
1512
|
}
|
|
1329
1513
|
}
|
|
1330
1514
|
let name = "";
|
|
@@ -1336,26 +1520,32 @@ class LatexParser {
|
|
|
1336
1520
|
return this.#applyCommand(name, style);
|
|
1337
1521
|
}
|
|
1338
1522
|
|
|
1339
|
-
#applyCommand(name: string, style:
|
|
1340
|
-
|
|
1523
|
+
#applyCommand(name: string, style: ParseStyle): Rendered {
|
|
1524
|
+
const current = this.#renderStyle(style);
|
|
1341
1525
|
const font = FONTS[name];
|
|
1342
|
-
if (font) return this.#argument(font).text;
|
|
1526
|
+
if (font) return this.#argument({ ...style, font }).text;
|
|
1343
1527
|
|
|
1344
|
-
|
|
1528
|
+
const textStyle = TEXT_STYLE_COMMANDS[name];
|
|
1529
|
+
if (textStyle !== undefined) {
|
|
1530
|
+
const text = this.#argument({ ...style, ...textStyle }).text;
|
|
1531
|
+
const attribute = textStyle.bold === true ? "bold" : textStyle.italic === true ? "italic" : null;
|
|
1532
|
+
return attribute === null ? text : scopeRendered(text, attribute, style[attribute]);
|
|
1533
|
+
}
|
|
1345
1534
|
|
|
1535
|
+
if (TEXT_COMMANDS[name]) return styledText(unescapeText(this.#rawArgument()), current);
|
|
1346
1536
|
if (name === "operatorname") {
|
|
1347
1537
|
const fn = unescapeText(this.#rawArgument());
|
|
1348
|
-
return fn + this.#spaceBeforeArg();
|
|
1538
|
+
return styledText(fn + this.#spaceBeforeArg(), current);
|
|
1349
1539
|
}
|
|
1350
1540
|
|
|
1351
|
-
// Accents
|
|
1541
|
+
// Accents operate on glyph chunks, never on terminal escape sequences.
|
|
1352
1542
|
const accent = ACCENTS[name];
|
|
1353
1543
|
if (accent) return applyCombining(this.#argument(style).text, accent);
|
|
1354
1544
|
|
|
1355
1545
|
if (name === "frac" || name === "dfrac" || name === "tfrac" || name === "cfrac") {
|
|
1356
1546
|
const num = this.#argument(style);
|
|
1357
1547
|
const den = this.#argument(style);
|
|
1358
|
-
return this.#fraction(num, den);
|
|
1548
|
+
return this.#fraction(num, den, current);
|
|
1359
1549
|
}
|
|
1360
1550
|
|
|
1361
1551
|
if (name === "genfrac") {
|
|
@@ -1365,20 +1555,29 @@ class LatexParser {
|
|
|
1365
1555
|
this.#rawArgument(); // math style
|
|
1366
1556
|
const num = this.#argument(style);
|
|
1367
1557
|
const den = this.#argument(style);
|
|
1368
|
-
return left
|
|
1558
|
+
return concatRendered(left, this.#fraction(num, den, current), right);
|
|
1369
1559
|
}
|
|
1370
1560
|
|
|
1371
1561
|
if (name === "binom" || name === "dbinom" || name === "tbinom") {
|
|
1372
1562
|
const n = this.#argument(style);
|
|
1373
1563
|
const k = this.#argument(style);
|
|
1374
|
-
return
|
|
1564
|
+
return concatRendered(
|
|
1565
|
+
styledText("C(", current),
|
|
1566
|
+
n.text,
|
|
1567
|
+
styledText(", ", current),
|
|
1568
|
+
k.text,
|
|
1569
|
+
styledText(")", current),
|
|
1570
|
+
);
|
|
1375
1571
|
}
|
|
1376
1572
|
|
|
1377
1573
|
if (name === "sqrt") return this.#sqrt(style);
|
|
1378
1574
|
|
|
1379
1575
|
if (name === "not") {
|
|
1380
1576
|
const arg = this.#argument(style);
|
|
1381
|
-
|
|
1577
|
+
const mapped = NOT_MAP[plainText(arg.text)];
|
|
1578
|
+
return mapped === undefined
|
|
1579
|
+
? applyCombining(arg.text, "\u0338")
|
|
1580
|
+
: styledText(mapped, arg.text[0]?.style ?? current);
|
|
1382
1581
|
}
|
|
1383
1582
|
|
|
1384
1583
|
if (name === "overset" || name === "stackrel") return this.#scriptedAbove(style);
|
|
@@ -1388,18 +1587,21 @@ class LatexParser {
|
|
|
1388
1587
|
const arrow = EXTENSIBLE_ARROWS[name];
|
|
1389
1588
|
if (arrow !== undefined) return this.#extensibleArrow(style, arrow);
|
|
1390
1589
|
|
|
1391
|
-
if (name === "boxed" || name === "fbox") return
|
|
1392
|
-
if (name === "overbrace") return
|
|
1393
|
-
if (name === "underbrace") return
|
|
1394
|
-
if (name === "overbracket") return
|
|
1395
|
-
if (name === "underbracket") return
|
|
1396
|
-
if (name === "overparen") return
|
|
1397
|
-
if (name === "underparen") return
|
|
1590
|
+
if (name === "boxed" || name === "fbox") return surroundRendered(this.#argument(style).text, "[", "]", current);
|
|
1591
|
+
if (name === "overbrace") return surroundRendered(this.#argument(style).text, "⏞(", ")", current);
|
|
1592
|
+
if (name === "underbrace") return surroundRendered(this.#argument(style).text, "⏟(", ")", current);
|
|
1593
|
+
if (name === "overbracket") return surroundRendered(this.#argument(style).text, "⎴(", ")", current);
|
|
1594
|
+
if (name === "underbracket") return surroundRendered(this.#argument(style).text, "⎵(", ")", current);
|
|
1595
|
+
if (name === "overparen") return surroundRendered(this.#argument(style).text, "⏜(", ")", current);
|
|
1596
|
+
if (name === "underparen") return surroundRendered(this.#argument(style).text, "⏝(", ")", current);
|
|
1398
1597
|
if (name === "cancel") return applyCombining(this.#argument(style).text, "\u0338");
|
|
1399
1598
|
if (name === "bcancel") return applyCombining(this.#argument(style).text, "\u20E5");
|
|
1400
1599
|
if (name === "xcancel") return applyCombining(applyCombining(this.#argument(style).text, "\u0338"), "\u20E5");
|
|
1401
1600
|
if (name === "sout") return applyCombining(this.#argument(style).text, "\u0336");
|
|
1402
|
-
if (name === "substack")
|
|
1601
|
+
if (name === "substack") {
|
|
1602
|
+
const arg = this.#argument(style).text;
|
|
1603
|
+
return arg.map(chunk => ({ ...chunk, text: chunk.text.replace(NEWLINES, ",") }));
|
|
1604
|
+
}
|
|
1403
1605
|
|
|
1404
1606
|
if (name === "left" || name === "right" || name === "middle") return this.#delimiter(style);
|
|
1405
1607
|
|
|
@@ -1408,19 +1610,19 @@ class LatexParser {
|
|
|
1408
1610
|
if (name === "begin") return this.#environment(style);
|
|
1409
1611
|
if (name === "end") {
|
|
1410
1612
|
this.#rawArgument();
|
|
1411
|
-
return
|
|
1613
|
+
return [];
|
|
1412
1614
|
}
|
|
1413
1615
|
|
|
1414
|
-
if (name === "bmod") return " mod ";
|
|
1415
|
-
if (name === "pmod") return
|
|
1416
|
-
if (name === "pod") return
|
|
1417
|
-
if (name === "tag") return
|
|
1616
|
+
if (name === "bmod") return styledText(" mod ", current);
|
|
1617
|
+
if (name === "pmod") return surroundRendered(this.#argument(style).text, "(mod ", ")", current);
|
|
1618
|
+
if (name === "pod") return surroundRendered(this.#argument(style).text, "(", ")", current);
|
|
1619
|
+
if (name === "tag") return surroundRendered(this.#argument(style).text, "(", ")", current);
|
|
1418
1620
|
if (name === "label") {
|
|
1419
1621
|
this.#rawArgument();
|
|
1420
|
-
return
|
|
1622
|
+
return [];
|
|
1421
1623
|
}
|
|
1422
|
-
if (name === "ref" || name === "eqref") return `(${unescapeText(this.#rawArgument())})
|
|
1423
|
-
if (name === "url") return unescapeText(this.#rawArgument());
|
|
1624
|
+
if (name === "ref" || name === "eqref") return styledText(`(${unescapeText(this.#rawArgument())})`, current);
|
|
1625
|
+
if (name === "url") return styledText(unescapeText(this.#rawArgument()), current);
|
|
1424
1626
|
if (name === "href") {
|
|
1425
1627
|
this.#rawArgument();
|
|
1426
1628
|
return this.#argument(style).text;
|
|
@@ -1430,22 +1632,22 @@ class LatexParser {
|
|
|
1430
1632
|
if (name === "fcolorbox") return this.#fcolorbox(style);
|
|
1431
1633
|
if (name === "color") return this.#setForeground();
|
|
1432
1634
|
if (name === "normalcolor") {
|
|
1433
|
-
const previous = this.#foreground;
|
|
1434
1635
|
this.#foreground = null;
|
|
1435
|
-
return
|
|
1636
|
+
return [];
|
|
1436
1637
|
}
|
|
1437
1638
|
if (name === "phantom" || name === "hphantom") {
|
|
1438
|
-
|
|
1639
|
+
const arg = this.#argument(style).text;
|
|
1640
|
+
return styledText(" ".repeat(codePointLength(arg)), current);
|
|
1439
1641
|
}
|
|
1440
1642
|
if (name === "vphantom") {
|
|
1441
1643
|
this.#argument(style);
|
|
1442
|
-
return
|
|
1644
|
+
return [];
|
|
1443
1645
|
}
|
|
1444
1646
|
|
|
1445
|
-
if (FUNCTIONS[name]) return name + this.#spaceBeforeArg();
|
|
1647
|
+
if (FUNCTIONS[name]) return styledText(name + this.#spaceBeforeArg(), current);
|
|
1446
1648
|
|
|
1447
1649
|
const symbol = SYMBOLS[name];
|
|
1448
|
-
if (symbol !== undefined) return symbol;
|
|
1650
|
+
if (symbol !== undefined) return styledText(symbol, current);
|
|
1449
1651
|
|
|
1450
1652
|
// Layout-only commands that carry no visible glyph.
|
|
1451
1653
|
switch (name) {
|
|
@@ -1458,36 +1660,34 @@ class LatexParser {
|
|
|
1458
1660
|
case "nonumber":
|
|
1459
1661
|
case "notag":
|
|
1460
1662
|
case "quad":
|
|
1461
|
-
return name === "quad" ? " " : "";
|
|
1663
|
+
return styledText(name === "quad" ? " " : "", current);
|
|
1462
1664
|
case "qquad":
|
|
1463
|
-
return " ";
|
|
1665
|
+
return styledText(" ", current);
|
|
1464
1666
|
case "thinspace":
|
|
1465
1667
|
case "enspace":
|
|
1466
1668
|
case "medspace":
|
|
1467
1669
|
case "thickspace":
|
|
1468
1670
|
case "space":
|
|
1469
|
-
return " ";
|
|
1671
|
+
return styledText(" ", current);
|
|
1470
1672
|
case "negthinspace":
|
|
1471
1673
|
case "negmedspace":
|
|
1472
1674
|
case "negthickspace":
|
|
1473
|
-
return
|
|
1675
|
+
return [];
|
|
1474
1676
|
}
|
|
1475
1677
|
|
|
1476
1678
|
// Unknown command: surface the bare name rather than dropping it silently.
|
|
1477
|
-
return name;
|
|
1679
|
+
return styledText(name, current);
|
|
1478
1680
|
}
|
|
1479
1681
|
|
|
1480
|
-
#group(style:
|
|
1682
|
+
#group(style: ParseStyle): Rendered {
|
|
1481
1683
|
this.#i++;
|
|
1482
1684
|
const outerForeground = this.#foreground;
|
|
1483
1685
|
const outerBackground = this.#background;
|
|
1484
1686
|
const inner = this.parse(style, true);
|
|
1485
|
-
const innerForeground = this.#foreground;
|
|
1486
|
-
const innerBackground = this.#background;
|
|
1487
1687
|
if (this.#s[this.#i] === "}") this.#i++;
|
|
1488
1688
|
this.#foreground = outerForeground;
|
|
1489
1689
|
this.#background = outerBackground;
|
|
1490
|
-
return
|
|
1690
|
+
return inner;
|
|
1491
1691
|
}
|
|
1492
1692
|
|
|
1493
1693
|
#readAnsiColor(): AnsiColor | null {
|
|
@@ -1495,48 +1695,45 @@ class LatexParser {
|
|
|
1495
1695
|
return ansiColor(model, this.#rawArgument());
|
|
1496
1696
|
}
|
|
1497
1697
|
|
|
1498
|
-
#setForeground():
|
|
1698
|
+
#setForeground(): Rendered {
|
|
1499
1699
|
const color = this.#readAnsiColor();
|
|
1500
|
-
if (color
|
|
1501
|
-
|
|
1502
|
-
return color.foreground;
|
|
1700
|
+
if (color !== null) this.#foreground = color.foreground;
|
|
1701
|
+
return [];
|
|
1503
1702
|
}
|
|
1504
1703
|
|
|
1505
|
-
#scopedForeground(color: AnsiColor | null, style:
|
|
1704
|
+
#scopedForeground(color: AnsiColor | null, style: ParseStyle): Rendered {
|
|
1506
1705
|
const outerForeground = this.#foreground;
|
|
1507
1706
|
if (color === null) return this.#argument(style).text;
|
|
1508
1707
|
this.#foreground = color.foreground;
|
|
1509
1708
|
const arg = this.#argument(style).text;
|
|
1510
|
-
const innerForeground = this.#foreground;
|
|
1511
1709
|
this.#foreground = outerForeground;
|
|
1512
|
-
return
|
|
1710
|
+
return arg;
|
|
1513
1711
|
}
|
|
1514
1712
|
|
|
1515
|
-
#scopedBackground(color: AnsiColor | null, style:
|
|
1713
|
+
#scopedBackground(color: AnsiColor | null, style: ParseStyle): Rendered {
|
|
1516
1714
|
const outerBackground = this.#background;
|
|
1517
1715
|
if (color === null) return this.#argument(style).text;
|
|
1518
1716
|
this.#background = color.background;
|
|
1519
1717
|
const arg = this.#argument(style).text;
|
|
1520
|
-
const innerBackground = this.#background;
|
|
1521
1718
|
this.#background = outerBackground;
|
|
1522
|
-
return
|
|
1719
|
+
return arg;
|
|
1523
1720
|
}
|
|
1524
1721
|
|
|
1525
|
-
#fcolorbox(style:
|
|
1722
|
+
#fcolorbox(style: ParseStyle): Rendered {
|
|
1526
1723
|
const frameModel = this.#optionalRawArgument();
|
|
1527
1724
|
const frame = ansiColor(frameModel, this.#rawArgument());
|
|
1528
1725
|
const backgroundModel = this.#optionalRawArgument() ?? frameModel;
|
|
1529
1726
|
const background = ansiColor(backgroundModel, this.#rawArgument());
|
|
1530
1727
|
const body = this.#scopedBackground(background, style);
|
|
1531
|
-
|
|
1532
|
-
return
|
|
1728
|
+
const frameStyle = renderStyle(style, frame?.foreground ?? this.#foreground, this.#background);
|
|
1729
|
+
return concatRendered(styledText("[", frameStyle), body, styledText("]", frameStyle));
|
|
1533
1730
|
}
|
|
1534
1731
|
|
|
1535
1732
|
/** Read one argument: a `{…}` group, a single command, or a single char. */
|
|
1536
|
-
#argument(style:
|
|
1733
|
+
#argument(style: ParseStyle): Argument {
|
|
1537
1734
|
while (this.#s[this.#i] === " ") this.#i++;
|
|
1538
1735
|
const c = this.#s[this.#i];
|
|
1539
|
-
if (c === undefined) return { text:
|
|
1736
|
+
if (c === undefined) return { text: [], group: false };
|
|
1540
1737
|
if (c === "{") {
|
|
1541
1738
|
this.#i++;
|
|
1542
1739
|
const inner = this.parse(style, true);
|
|
@@ -1550,7 +1747,7 @@ class LatexParser {
|
|
|
1550
1747
|
return { text: this.#script(style, c === "^"), group: false };
|
|
1551
1748
|
}
|
|
1552
1749
|
this.#i++;
|
|
1553
|
-
return { text: styleChar(c, style), group: false };
|
|
1750
|
+
return { text: styledText(styleChar(c, style), this.#renderStyle(style)), group: false };
|
|
1554
1751
|
}
|
|
1555
1752
|
|
|
1556
1753
|
/** Read a raw (unparsed) argument, returning its literal source text. */
|
|
@@ -1600,74 +1797,89 @@ class LatexParser {
|
|
|
1600
1797
|
return out;
|
|
1601
1798
|
}
|
|
1602
1799
|
|
|
1603
|
-
#script(style:
|
|
1800
|
+
#script(style: ParseStyle, sup: boolean): Rendered {
|
|
1801
|
+
const current = this.#renderStyle(style);
|
|
1604
1802
|
const arg = this.#argument(style);
|
|
1605
|
-
return sup ? toSuperscript(arg.text, arg.group) : toSubscript(arg.text, arg.group);
|
|
1803
|
+
return sup ? toSuperscript(arg.text, arg.group, current) : toSubscript(arg.text, arg.group, current);
|
|
1606
1804
|
}
|
|
1607
1805
|
|
|
1608
|
-
#wrapFrac(arg: Argument):
|
|
1609
|
-
|
|
1806
|
+
#wrapFrac(arg: Argument, fallback: RenderStyle): Rendered {
|
|
1807
|
+
if (!arg.group || codePointLength(arg.text) <= 1) return arg.text;
|
|
1808
|
+
return surroundRendered(arg.text, "(", ")", fallback);
|
|
1610
1809
|
}
|
|
1611
1810
|
|
|
1612
|
-
#fraction(num: Argument, den: Argument):
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1811
|
+
#fraction(num: Argument, den: Argument, fallback: RenderStyle): Rendered {
|
|
1812
|
+
const numText = plainText(num.text);
|
|
1813
|
+
const denText = plainText(den.text);
|
|
1814
|
+
const first = num.text[0]?.style ?? fallback;
|
|
1815
|
+
const same = [...num.text, ...den.text].every(chunk => sameRenderStyle(chunk.style, first));
|
|
1816
|
+
const vulgar = VULGAR[`${numText}/${denText}`];
|
|
1817
|
+
if (vulgar && same) return styledText(vulgar, first);
|
|
1818
|
+
return concatRendered(this.#wrapFrac(num, fallback), styledText("/", fallback), this.#wrapFrac(den, fallback));
|
|
1616
1819
|
}
|
|
1617
1820
|
|
|
1618
|
-
#scriptedAbove(style:
|
|
1821
|
+
#scriptedAbove(style: ParseStyle): Rendered {
|
|
1822
|
+
const current = this.#renderStyle(style);
|
|
1619
1823
|
const above = this.#argument(style);
|
|
1620
1824
|
const base = this.#argument(style);
|
|
1621
|
-
return base.text
|
|
1825
|
+
return concatRendered(base.text, toSuperscript(above.text, true, current));
|
|
1622
1826
|
}
|
|
1623
1827
|
|
|
1624
|
-
#scriptedBelow(style:
|
|
1828
|
+
#scriptedBelow(style: ParseStyle): Rendered {
|
|
1829
|
+
const current = this.#renderStyle(style);
|
|
1625
1830
|
const below = this.#argument(style);
|
|
1626
1831
|
const base = this.#argument(style);
|
|
1627
|
-
return base.text
|
|
1832
|
+
return concatRendered(base.text, toSubscript(below.text, true, current));
|
|
1628
1833
|
}
|
|
1629
1834
|
|
|
1630
|
-
#prescript(style:
|
|
1835
|
+
#prescript(style: ParseStyle): Rendered {
|
|
1836
|
+
const current = this.#renderStyle(style);
|
|
1631
1837
|
const sup = this.#argument(style);
|
|
1632
1838
|
const sub = this.#argument(style);
|
|
1633
1839
|
const base = this.#argument(style);
|
|
1634
|
-
return toSuperscript(sup.text, true)
|
|
1840
|
+
return concatRendered(toSuperscript(sup.text, true, current), toSubscript(sub.text, true, current), base.text);
|
|
1635
1841
|
}
|
|
1636
1842
|
|
|
1637
|
-
#extensibleArrow(style:
|
|
1843
|
+
#extensibleArrow(style: ParseStyle, arrow: string): Rendered {
|
|
1844
|
+
const current = this.#renderStyle(style);
|
|
1638
1845
|
const below = this.#optionalArgument(style);
|
|
1639
1846
|
const above = this.#argument(style);
|
|
1640
|
-
return
|
|
1847
|
+
return concatRendered(
|
|
1848
|
+
styledText(arrow, current),
|
|
1849
|
+
toSuperscript(above.text, true, current),
|
|
1850
|
+
below === null ? [] : toSubscript(below.text, true, current),
|
|
1851
|
+
);
|
|
1641
1852
|
}
|
|
1642
1853
|
|
|
1643
|
-
#delimiter(style:
|
|
1854
|
+
#delimiter(style: ParseStyle): Rendered {
|
|
1644
1855
|
while (this.#s[this.#i] === " ") this.#i++;
|
|
1645
1856
|
const c = this.#s[this.#i];
|
|
1646
|
-
|
|
1857
|
+
const current = this.#renderStyle(style);
|
|
1858
|
+
if (c === undefined) return [];
|
|
1647
1859
|
if (c === ".") {
|
|
1648
1860
|
this.#i++;
|
|
1649
|
-
return
|
|
1861
|
+
return [];
|
|
1650
1862
|
}
|
|
1651
1863
|
if (c !== "\\") {
|
|
1652
1864
|
this.#i++;
|
|
1653
|
-
return styleChar(c, style);
|
|
1865
|
+
return styledText(styleChar(c, style), current);
|
|
1654
1866
|
}
|
|
1655
1867
|
this.#i++;
|
|
1656
|
-
if (this.#i >= this.#s.length) return
|
|
1868
|
+
if (this.#i >= this.#s.length) return [];
|
|
1657
1869
|
const d = this.#s[this.#i];
|
|
1658
1870
|
if (!/[A-Za-z]/.test(d)) {
|
|
1659
1871
|
this.#i++;
|
|
1660
1872
|
switch (d) {
|
|
1661
1873
|
case ".":
|
|
1662
|
-
return
|
|
1874
|
+
return [];
|
|
1663
1875
|
case "{":
|
|
1664
|
-
return "{";
|
|
1876
|
+
return styledText("{", current);
|
|
1665
1877
|
case "}":
|
|
1666
|
-
return "}";
|
|
1878
|
+
return styledText("}", current);
|
|
1667
1879
|
case "|":
|
|
1668
|
-
return "‖";
|
|
1880
|
+
return styledText("‖", current);
|
|
1669
1881
|
default:
|
|
1670
|
-
return d;
|
|
1882
|
+
return styledText(d, current);
|
|
1671
1883
|
}
|
|
1672
1884
|
}
|
|
1673
1885
|
let name = "";
|
|
@@ -1675,10 +1887,10 @@ class LatexParser {
|
|
|
1675
1887
|
name += this.#s[this.#i];
|
|
1676
1888
|
this.#i++;
|
|
1677
1889
|
}
|
|
1678
|
-
return SYMBOLS[name] ?? name;
|
|
1890
|
+
return styledText(SYMBOLS[name] ?? name, current);
|
|
1679
1891
|
}
|
|
1680
1892
|
|
|
1681
|
-
#optionalArgument(style:
|
|
1893
|
+
#optionalArgument(style: ParseStyle): Argument | null {
|
|
1682
1894
|
const source = this.#optionalRawArgument();
|
|
1683
1895
|
if (source === null) return null;
|
|
1684
1896
|
return { text: new LatexParser(source).parse(style, false), group: true };
|
|
@@ -1714,18 +1926,30 @@ class LatexParser {
|
|
|
1714
1926
|
return out;
|
|
1715
1927
|
}
|
|
1716
1928
|
|
|
1717
|
-
#sqrt(style:
|
|
1929
|
+
#sqrt(style: ParseStyle): Rendered {
|
|
1718
1930
|
while (this.#s[this.#i] === " ") this.#i++;
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1931
|
+
const current = this.#renderStyle(style);
|
|
1932
|
+
let radical = styledText("√", current);
|
|
1933
|
+
const index = this.#optionalArgument(style);
|
|
1934
|
+
if (index !== null) {
|
|
1935
|
+
const indexText = plainText(index.text);
|
|
1936
|
+
radical =
|
|
1937
|
+
indexText === "2"
|
|
1938
|
+
? styledText("√", current)
|
|
1939
|
+
: indexText === "3"
|
|
1940
|
+
? styledText("∛", current)
|
|
1941
|
+
: indexText === "4"
|
|
1942
|
+
? styledText("∜", current)
|
|
1943
|
+
: concatRendered(toSuperscript(index.text, true, current), styledText("√", current));
|
|
1723
1944
|
}
|
|
1724
1945
|
const radicand = this.#argument(style).text;
|
|
1725
|
-
return
|
|
1946
|
+
return concatRendered(
|
|
1947
|
+
radical,
|
|
1948
|
+
codePointLength(radicand) > 1 ? surroundRendered(radicand, "(", ")", current) : radicand,
|
|
1949
|
+
);
|
|
1726
1950
|
}
|
|
1727
1951
|
|
|
1728
|
-
#environment(style:
|
|
1952
|
+
#environment(style: ParseStyle): Rendered {
|
|
1729
1953
|
const env = this.#rawArgument().trim();
|
|
1730
1954
|
if (env === "array" || env === "tabular" || env === "array*" || env === "tabular*") {
|
|
1731
1955
|
this.#optionalRawArgument();
|
|
@@ -1740,16 +1964,16 @@ class LatexParser {
|
|
|
1740
1964
|
this.#optionalRawArgument();
|
|
1741
1965
|
if (this.#s[this.#i] === "{") this.#rawArgument(); // column count
|
|
1742
1966
|
}
|
|
1743
|
-
|
|
1967
|
+
const body: Rendered = [];
|
|
1744
1968
|
while (this.#i < this.#s.length) {
|
|
1745
1969
|
if (this.#s.startsWith("\\end", this.#i)) {
|
|
1746
1970
|
this.#i += 4;
|
|
1747
1971
|
this.#rawArgument();
|
|
1748
1972
|
break;
|
|
1749
1973
|
}
|
|
1750
|
-
body
|
|
1974
|
+
appendRendered(body, this.#node(style));
|
|
1751
1975
|
}
|
|
1752
|
-
|
|
1976
|
+
let renderedBody = trimRendered(body);
|
|
1753
1977
|
if (
|
|
1754
1978
|
env === "cases" ||
|
|
1755
1979
|
env === "cases*" ||
|
|
@@ -1758,10 +1982,15 @@ class LatexParser {
|
|
|
1758
1982
|
env === "rcases" ||
|
|
1759
1983
|
env === "drcases"
|
|
1760
1984
|
) {
|
|
1761
|
-
|
|
1985
|
+
renderedBody = renderedBody.map(chunk => ({
|
|
1986
|
+
...chunk,
|
|
1987
|
+
text: chunk.text.replace(/[ \t]*\n+[ \t]*/g, "; ").replace(/ {3,}/g, " "),
|
|
1988
|
+
}));
|
|
1762
1989
|
}
|
|
1763
1990
|
const delims = ENV_DELIMS[env];
|
|
1764
|
-
|
|
1991
|
+
if (!delims) return renderedBody;
|
|
1992
|
+
const current = this.#renderStyle(style);
|
|
1993
|
+
return concatRendered(styledText(delims[0], current), renderedBody, styledText(delims[1], current));
|
|
1765
1994
|
}
|
|
1766
1995
|
|
|
1767
1996
|
/** A separator space when the next glyph is alphanumeric or a command. */
|
|
@@ -1788,7 +2017,7 @@ export function latexToUnicode(src: string): string {
|
|
|
1788
2017
|
|
|
1789
2018
|
const NEWLINES = /\n+/g;
|
|
1790
2019
|
const BARE_MATH_LINE_COMMAND =
|
|
1791
|
-
/\\(?:operatorname|frac|dfrac|tfrac|cfrac|genfrac|sqrt|sum|prod|coprod|int|iint|iiint|lim|alpha|beta|gamma|delta|epsilon|varepsilon|theta|lambda|mu|sigma|phi|varphi|pi|omega|infty|partial|nabla|forall|exists|mathbb|mathcal|mathscr|mathbf|mathrm|left|right|begin|phantom|hphantom|vphantom|cdots|ldots|dots|to|rightarrow|leftarrow|leq|geq|neq|times|cdot|overline|underline|vec|hat|bar|textcolor|color|normalcolor|colorbox|fcolorbox)\b/;
|
|
2020
|
+
/\\(?:operatorname|frac|dfrac|tfrac|cfrac|genfrac|sqrt|sum|prod|coprod|int|iint|iiint|lim|alpha|beta|gamma|delta|epsilon|varepsilon|theta|lambda|mu|sigma|phi|varphi|pi|omega|infty|partial|nabla|forall|exists|mathbb|mathcal|mathscr|mathbf|mathrm|left|right|begin|phantom|hphantom|vphantom|cdots|ldots|dots|to|rightarrow|leftarrow|leq|geq|neq|times|cdot|overline|underline|vec|hat|bar|textbf|textit|textsl|emph|textmd|textup|texttt|textsf|textcolor|color|normalcolor|colorbox|fcolorbox)\b/;
|
|
1792
2021
|
|
|
1793
2022
|
// Display-math environments eligible for delimiter-less ("bare") rendering in
|
|
1794
2023
|
// prose. Deliberately excludes text-mode table/list/float environments
|