@oh-my-pi/pi-tui 16.0.2 → 16.0.4
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 +28 -0
- package/dist/types/components/markdown.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/latex-block.d.ts +7 -0
- package/dist/types/latex-to-unicode.d.ts +33 -0
- package/dist/types/tui.d.ts +5 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +225 -16
- package/src/index.ts +3 -0
- package/src/latex-block.ts +461 -0
- package/src/latex-to-unicode.ts +1994 -0
- package/src/terminal-capabilities.ts +2 -2
- package/src/terminal.ts +17 -2
- package/src/tui.ts +25 -4
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// Two-dimensional layout for *display* LaTeX math: stacks `\frac` numerator over
|
|
2
|
+
// denominator with a horizontal bar, aligning surrounding text to the bar's row.
|
|
3
|
+
//
|
|
4
|
+
// −b ± √(b² − 4ac)
|
|
5
|
+
// x = ────────────────
|
|
6
|
+
// 2a
|
|
7
|
+
//
|
|
8
|
+
// Only display blocks (`$$…$$`, `\[…\]`) use this; inline `$…$` stays single-line
|
|
9
|
+
// (`½`, `(a+b)/c`). Everything that is not a fraction — symbols, scripts, roots,
|
|
10
|
+
// matrices, environments — is delegated to `latexToUnicode`, so this engine only
|
|
11
|
+
// adds the vertical stacking the flat string form can't express.
|
|
12
|
+
|
|
13
|
+
import { latexToUnicode } from "./latex-to-unicode";
|
|
14
|
+
import { visibleWidth } from "./utils";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A rectangular block of rendered text. Every entry in `lines` is padded to
|
|
18
|
+
* exactly `width` visible columns; `baseline` is the row that aligns with the
|
|
19
|
+
* surrounding text when boxes are placed side by side (e.g. the fraction bar).
|
|
20
|
+
*/
|
|
21
|
+
interface Box {
|
|
22
|
+
lines: string[];
|
|
23
|
+
baseline: number;
|
|
24
|
+
width: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BAR = "─";
|
|
28
|
+
const FRAC_COMMANDS: Record<string, true> = { frac: true, dfrac: true, tfrac: true, cfrac: true };
|
|
29
|
+
|
|
30
|
+
// Display "wrapper" environments whose body is an expression (possibly with `\\`
|
|
31
|
+
// row breaks and `&` alignment). Their bodies are parsed so fractions inside
|
|
32
|
+
// stack; grid/structure environments (matrix/array/cases) stay opaque and are
|
|
33
|
+
// rendered flat by `latexToUnicode`.
|
|
34
|
+
const DISPLAY_ROW_ENVIRONMENTS: Record<string, true> = {
|
|
35
|
+
equation: true,
|
|
36
|
+
eqnarray: true,
|
|
37
|
+
align: true,
|
|
38
|
+
aligned: true,
|
|
39
|
+
alignat: true,
|
|
40
|
+
alignedat: true,
|
|
41
|
+
flalign: true,
|
|
42
|
+
split: true,
|
|
43
|
+
gather: true,
|
|
44
|
+
gathered: true,
|
|
45
|
+
gatheredat: true,
|
|
46
|
+
multline: true,
|
|
47
|
+
displaymath: true,
|
|
48
|
+
math: true,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function spaces(n: number): string {
|
|
52
|
+
return n > 0 ? " ".repeat(n) : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Pad `line` on the right to `width` visible columns. */
|
|
56
|
+
function padRight(line: string, width: number): string {
|
|
57
|
+
return line + spaces(width - visibleWidth(line));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pad `line` symmetrically (left-biased) to `width` visible columns. */
|
|
61
|
+
function center(line: string, width: number): string {
|
|
62
|
+
const extra = width - visibleWidth(line);
|
|
63
|
+
if (extra <= 0) return line;
|
|
64
|
+
const left = extra >> 1;
|
|
65
|
+
return spaces(left) + line + spaces(extra - left);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A single rendered string (possibly multi-line) as a baseline-centered box. */
|
|
69
|
+
function textBox(text: string): Box {
|
|
70
|
+
const raw = text.split("\n");
|
|
71
|
+
let width = 0;
|
|
72
|
+
for (const line of raw) width = Math.max(width, visibleWidth(line));
|
|
73
|
+
return { lines: raw.map(line => padRight(line, width)), baseline: (raw.length - 1) >> 1, width };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Place boxes side by side, aligning their baselines. */
|
|
77
|
+
function hconcat(boxes: Box[]): Box {
|
|
78
|
+
if (boxes.length === 1) return boxes[0];
|
|
79
|
+
let above = 0;
|
|
80
|
+
let below = 0;
|
|
81
|
+
for (const b of boxes) {
|
|
82
|
+
above = Math.max(above, b.baseline);
|
|
83
|
+
below = Math.max(below, b.lines.length - 1 - b.baseline);
|
|
84
|
+
}
|
|
85
|
+
const height = above + below + 1;
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
let width = 0;
|
|
88
|
+
for (const b of boxes) width += b.width;
|
|
89
|
+
for (let row = 0; row < height; row++) {
|
|
90
|
+
let line = "";
|
|
91
|
+
for (const b of boxes) {
|
|
92
|
+
const local = row - (above - b.baseline);
|
|
93
|
+
line += local >= 0 && local < b.lines.length ? b.lines[local] : spaces(b.width);
|
|
94
|
+
}
|
|
95
|
+
lines.push(line);
|
|
96
|
+
}
|
|
97
|
+
return { lines, baseline: above, width };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Stack `num` over `den`, separated by a bar; the bar becomes the baseline. */
|
|
101
|
+
function fracBox(num: Box, den: Box): Box {
|
|
102
|
+
const width = Math.max(num.width, den.width) + 2;
|
|
103
|
+
const lines = [
|
|
104
|
+
...num.lines.map(line => center(line, width)),
|
|
105
|
+
BAR.repeat(width),
|
|
106
|
+
...den.lines.map(line => center(line, width)),
|
|
107
|
+
];
|
|
108
|
+
return { lines, baseline: num.lines.length, width };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Stack boxes vertically (left-aligned), e.g. the rows of an aligned block. */
|
|
112
|
+
function vconcat(boxes: Box[]): Box {
|
|
113
|
+
if (boxes.length === 1) return boxes[0];
|
|
114
|
+
let width = 0;
|
|
115
|
+
for (const b of boxes) width = Math.max(width, b.width);
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
for (const b of boxes) for (const line of b.lines) lines.push(padRight(line, width));
|
|
118
|
+
return { lines, baseline: (lines.length - 1) >> 1, width };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface Span {
|
|
122
|
+
text: string;
|
|
123
|
+
end: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Read a balanced `{…}` beginning at `i` (which must point at `{`). */
|
|
127
|
+
function readBraceGroup(src: string, i: number): Span {
|
|
128
|
+
let depth = 0;
|
|
129
|
+
let out = "";
|
|
130
|
+
let j = i;
|
|
131
|
+
for (; j < src.length; j++) {
|
|
132
|
+
const c = src[j];
|
|
133
|
+
if (c === "\\") {
|
|
134
|
+
out += c + (src[j + 1] ?? "");
|
|
135
|
+
j++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (c === "{") {
|
|
139
|
+
depth++;
|
|
140
|
+
if (depth > 1) out += c;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (c === "}") {
|
|
144
|
+
depth--;
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
j++;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
out += c;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
out += c;
|
|
153
|
+
}
|
|
154
|
+
return { text: out, end: j };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read one fraction argument: a `{…}` group, a single char, or a `\command`
|
|
159
|
+
* together with its attached `[…]`/`{…}` arguments (or whole `\begin…\end`
|
|
160
|
+
* block), so e.g. `\frac\sqrt{a}{b}` reads `\sqrt{a}` as the numerator.
|
|
161
|
+
*/
|
|
162
|
+
function readArg(src: string, i: number): Span {
|
|
163
|
+
while (src[i] === " ") i++;
|
|
164
|
+
if (i >= src.length) return { text: "", end: i };
|
|
165
|
+
if (src[i] === "{") return readBraceGroup(src, i);
|
|
166
|
+
if (src[i] !== "\\") return { text: src[i], end: i + 1 };
|
|
167
|
+
let j = i + 1;
|
|
168
|
+
let name = "";
|
|
169
|
+
while (/[A-Za-z]/.test(src[j] ?? "")) {
|
|
170
|
+
name += src[j];
|
|
171
|
+
j++;
|
|
172
|
+
}
|
|
173
|
+
if (name === "begin") {
|
|
174
|
+
const env = consumeEnvironment(src, i);
|
|
175
|
+
if (env) return env;
|
|
176
|
+
}
|
|
177
|
+
if (!name) return { text: src.slice(i, i + 2), end: i + 2 }; // non-letter command (\,, \{, …)
|
|
178
|
+
let end = j;
|
|
179
|
+
while (src[end] === "[" || src[end] === "{") {
|
|
180
|
+
if (src[end] === "{") end = readBraceGroup(src, end).end;
|
|
181
|
+
else {
|
|
182
|
+
const close = src.indexOf("]", end);
|
|
183
|
+
end = close === -1 ? src.length : close + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { text: src.slice(i, end), end };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface EnvParts {
|
|
190
|
+
env: string;
|
|
191
|
+
bodyStart: number;
|
|
192
|
+
bodyEnd: number;
|
|
193
|
+
end: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Locate a `\begin{env}…\end{env}` block (balanced) starting at the backslash. */
|
|
197
|
+
function readEnvironment(src: string, start: number): EnvParts | null {
|
|
198
|
+
let i = start + 6; // past "\begin"
|
|
199
|
+
while (src[i] === " ") i++;
|
|
200
|
+
if (src[i] !== "{") return null;
|
|
201
|
+
const nameGroup = readBraceGroup(src, i);
|
|
202
|
+
let k = nameGroup.end;
|
|
203
|
+
let depth = 1;
|
|
204
|
+
let bodyEnd = src.length;
|
|
205
|
+
while (k < src.length && depth > 0) {
|
|
206
|
+
if (src.startsWith("\\begin", k)) {
|
|
207
|
+
depth++;
|
|
208
|
+
k += 6;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (src.startsWith("\\end", k)) {
|
|
212
|
+
depth--;
|
|
213
|
+
if (depth === 0) bodyEnd = k;
|
|
214
|
+
k += 4;
|
|
215
|
+
while (src[k] === " ") k++;
|
|
216
|
+
if (src[k] === "{") k = readBraceGroup(src, k).end;
|
|
217
|
+
if (depth === 0) break;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
k++;
|
|
221
|
+
}
|
|
222
|
+
return { env: nameGroup.text.trim(), bodyStart: nameGroup.end, bodyEnd, end: k };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The full `\begin{env}…\end{env}` substring as an inline run. */
|
|
226
|
+
function consumeEnvironment(src: string, start: number): Span | null {
|
|
227
|
+
const env = readEnvironment(src, start);
|
|
228
|
+
return env ? { text: src.slice(start, env.end), end: env.end } : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Split an environment body on top-level `\\` row breaks (depth-aware). */
|
|
232
|
+
function splitRows(body: string): string[] {
|
|
233
|
+
const rows: string[] = [];
|
|
234
|
+
let braceDepth = 0;
|
|
235
|
+
let envDepth = 0;
|
|
236
|
+
let last = 0;
|
|
237
|
+
let i = 0;
|
|
238
|
+
while (i < body.length) {
|
|
239
|
+
if (body.startsWith("\\begin", i)) {
|
|
240
|
+
envDepth++;
|
|
241
|
+
i += 6;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (body.startsWith("\\end", i)) {
|
|
245
|
+
envDepth--;
|
|
246
|
+
i += 4;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const c = body[i];
|
|
250
|
+
if (c === "\\") {
|
|
251
|
+
if (body[i + 1] === "\\" && braceDepth === 0 && envDepth === 0) {
|
|
252
|
+
rows.push(body.slice(last, i));
|
|
253
|
+
i += 2;
|
|
254
|
+
while (body[i] === " ") i++;
|
|
255
|
+
if (body[i] === "[") {
|
|
256
|
+
const close = body.indexOf("]", i);
|
|
257
|
+
i = close === -1 ? body.length : close + 1;
|
|
258
|
+
}
|
|
259
|
+
last = i;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
i += 2; // skip escaped char / second backslash so `\{`/`\\` never skew depth
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (c === "{") braceDepth++;
|
|
266
|
+
else if (c === "}") braceDepth--;
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
rows.push(body.slice(last));
|
|
270
|
+
return rows;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Render a `\begin{env}…\end{env}` block. Expression "wrapper" environments
|
|
275
|
+
* (`equation`, `align`, `gather`, …) have their rows parsed so fractions stack;
|
|
276
|
+
* grid/structure environments (matrix/array/cases) render flat via
|
|
277
|
+
* `latexToUnicode`.
|
|
278
|
+
*/
|
|
279
|
+
function parseEnvironment(src: string, start: number): { box: Box; end: number } | null {
|
|
280
|
+
const env = readEnvironment(src, start);
|
|
281
|
+
if (env === null) return null;
|
|
282
|
+
const base = env.env.endsWith("*") ? env.env.slice(0, -1) : env.env;
|
|
283
|
+
if (!DISPLAY_ROW_ENVIRONMENTS[base]) {
|
|
284
|
+
return { box: textBox(latexToUnicode(src.slice(start, env.end))), end: env.end };
|
|
285
|
+
}
|
|
286
|
+
let bodyStart = env.bodyStart;
|
|
287
|
+
if (base === "alignat" || base === "alignedat" || base === "gatheredat") {
|
|
288
|
+
// These carry a required column-count argument `{n}` before the body.
|
|
289
|
+
let p = bodyStart;
|
|
290
|
+
while (src[p] === " " || src[p] === "\n") p++;
|
|
291
|
+
if (src[p] === "{") bodyStart = readBraceGroup(src, p).end;
|
|
292
|
+
}
|
|
293
|
+
const rows = splitRows(src.slice(bodyStart, env.bodyEnd))
|
|
294
|
+
.map(row => row.trim())
|
|
295
|
+
.filter(row => row !== "")
|
|
296
|
+
.map(row => parseExpr(row));
|
|
297
|
+
return { box: rows.length > 0 ? vconcat(rows) : textBox(""), end: env.end };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Append a script (`^`/`_`) and its argument to the inline run verbatim. */
|
|
301
|
+
function readScript(src: string, i: number): Span {
|
|
302
|
+
let out = src[i];
|
|
303
|
+
i++;
|
|
304
|
+
while (src[i] === " ") {
|
|
305
|
+
out += src[i];
|
|
306
|
+
i++;
|
|
307
|
+
}
|
|
308
|
+
if (src[i] === "{") {
|
|
309
|
+
const group = readBraceGroup(src, i);
|
|
310
|
+
return { text: `${out}{${group.text}}`, end: group.end };
|
|
311
|
+
}
|
|
312
|
+
if (src[i] === "\\") {
|
|
313
|
+
let j = i + 1;
|
|
314
|
+
if (/[A-Za-z]/.test(src[j] ?? "")) while (/[A-Za-z]/.test(src[j] ?? "")) j++;
|
|
315
|
+
else j++;
|
|
316
|
+
return { text: out + src.slice(i, j), end: j };
|
|
317
|
+
}
|
|
318
|
+
if (i < src.length) return { text: out + src[i], end: i + 1 };
|
|
319
|
+
return { text: out, end: i };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parse a math fragment into a layout box, stacking top-level fractions (and
|
|
324
|
+
* fractions nested inside other fractions' arguments). Non-fraction runs —
|
|
325
|
+
* including scripts, roots, environments, and command arguments — are gathered
|
|
326
|
+
* into inline strings and rendered through `latexToUnicode`.
|
|
327
|
+
*/
|
|
328
|
+
function parseExpr(src: string): Box {
|
|
329
|
+
const boxes: Box[] = [];
|
|
330
|
+
let inline = "";
|
|
331
|
+
const flush = (): void => {
|
|
332
|
+
if (inline) {
|
|
333
|
+
boxes.push(textBox(latexToUnicode(inline)));
|
|
334
|
+
inline = "";
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
let i = 0;
|
|
338
|
+
while (i < src.length) {
|
|
339
|
+
const c = src[i];
|
|
340
|
+
if (c === "\\") {
|
|
341
|
+
let j = i + 1;
|
|
342
|
+
let name = "";
|
|
343
|
+
while (j < src.length && /[A-Za-z]/.test(src[j])) {
|
|
344
|
+
name += src[j];
|
|
345
|
+
j++;
|
|
346
|
+
}
|
|
347
|
+
if (name && FRAC_COMMANDS[name]) {
|
|
348
|
+
flush();
|
|
349
|
+
const num = readArg(src, j);
|
|
350
|
+
const den = readArg(src, num.end);
|
|
351
|
+
boxes.push(fracBox(parseExpr(num.text), parseExpr(den.text)));
|
|
352
|
+
i = den.end;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (name === "begin") {
|
|
356
|
+
const env = parseEnvironment(src, i);
|
|
357
|
+
if (env) {
|
|
358
|
+
flush();
|
|
359
|
+
boxes.push(env.box);
|
|
360
|
+
i = env.end;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!name) {
|
|
365
|
+
// Non-letter command (`\\`, `\,`, `\{`, …): keep the 2-char token inline.
|
|
366
|
+
inline += `\\${src[j] ?? ""}`;
|
|
367
|
+
i = j + 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
// Other command: keep it and its bracket/brace arguments inline so a
|
|
371
|
+
// `{…}` argument is never mistaken for a top-level stacking group.
|
|
372
|
+
inline += `\\${name}`;
|
|
373
|
+
i = j;
|
|
374
|
+
while (src[i] === "[" || src[i] === "{") {
|
|
375
|
+
if (src[i] === "{") {
|
|
376
|
+
const group = readBraceGroup(src, i);
|
|
377
|
+
inline += `{${group.text}}`;
|
|
378
|
+
i = group.end;
|
|
379
|
+
} else {
|
|
380
|
+
const close = src.indexOf("]", i);
|
|
381
|
+
const end = close === -1 ? src.length : close + 1;
|
|
382
|
+
inline += src.slice(i, end);
|
|
383
|
+
i = end;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (c === "^" || c === "_") {
|
|
389
|
+
const script = readScript(src, i);
|
|
390
|
+
inline += script.text;
|
|
391
|
+
i = script.end;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (c === "{") {
|
|
395
|
+
const group = readBraceGroup(src, i);
|
|
396
|
+
flush();
|
|
397
|
+
boxes.push(parseExpr(group.text));
|
|
398
|
+
i = group.end;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
inline += c;
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
flush();
|
|
405
|
+
if (boxes.length === 0) return textBox("");
|
|
406
|
+
return hconcat(boxes);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Split on top-level `\n` row separators (outside braces and environments). */
|
|
410
|
+
function splitLines(src: string): string[] {
|
|
411
|
+
const lines: string[] = [];
|
|
412
|
+
let braceDepth = 0;
|
|
413
|
+
let envDepth = 0;
|
|
414
|
+
let last = 0;
|
|
415
|
+
let i = 0;
|
|
416
|
+
while (i < src.length) {
|
|
417
|
+
if (src.startsWith("\\begin", i)) {
|
|
418
|
+
envDepth++;
|
|
419
|
+
i += 6;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (src.startsWith("\\end", i)) {
|
|
423
|
+
envDepth--;
|
|
424
|
+
i += 4;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const c = src[i];
|
|
428
|
+
if (c === "\\") {
|
|
429
|
+
i += 2; // escaped char / second backslash — never a logical-line break
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (c === "{") braceDepth++;
|
|
433
|
+
else if (c === "}") braceDepth--;
|
|
434
|
+
else if (c === "\n" && braceDepth === 0 && envDepth === 0) {
|
|
435
|
+
lines.push(src.slice(last, i));
|
|
436
|
+
last = i + 1;
|
|
437
|
+
}
|
|
438
|
+
i++;
|
|
439
|
+
}
|
|
440
|
+
lines.push(src.slice(last));
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Render a display LaTeX math fragment to lines, stacking `\frac` vertically.
|
|
446
|
+
* Top-level source newlines become vertical rows (so a `lhs =` line stays above
|
|
447
|
+
* its block); each row stacks fractions via `parseExpr`. Inline math should use
|
|
448
|
+
* `latexToUnicode` instead — fractions there stay single-line.
|
|
449
|
+
*/
|
|
450
|
+
export function latexToBlock(src: string): string[] {
|
|
451
|
+
if (typeof src !== "string" || src.trim() === "") return [];
|
|
452
|
+
const rows = splitLines(src.trim())
|
|
453
|
+
.map(line => line.trim())
|
|
454
|
+
.filter(line => line !== "")
|
|
455
|
+
.map(line => parseExpr(line));
|
|
456
|
+
if (rows.length === 0) return [];
|
|
457
|
+
let lines = vconcat(rows).lines;
|
|
458
|
+
while (lines.length > 1 && lines[lines.length - 1].trim() === "") lines = lines.slice(0, -1);
|
|
459
|
+
while (lines.length > 1 && lines[0].trim() === "") lines = lines.slice(1);
|
|
460
|
+
return lines;
|
|
461
|
+
}
|