@oh-my-pi/pi-tui 16.0.2 → 16.0.3
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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.3] - 2026-06-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `\tfrac` support to stacked display-math rendering so it now displays as a vertical fraction in `latexToBlock` output
|
|
10
|
+
- Added markdown parsing for own-line display-math blocks (`$$...$$` and `\[...\]`) and delimiter-free `\begin{...}...\end{...}` math environments so block equations render via LaTeX-to-Unicode
|
|
11
|
+
- Added stacked rendering of display-math fractions (`\frac`, `\dfrac`, `\cfrac`): the numerator is drawn over a horizontal bar over the denominator, with surrounding terms and `align`/`equation`-style environment rows aligned to the bar. Triggered for own-line `$$`/`\[` blocks, bare `\begin{...}` environments, and a paragraph whose sole content is a single display-math span; inline `$...$` fractions stay single-line (`½`, `(a+b)/c`)
|
|
12
|
+
- Added bare math auto-rendering in `renderMathInText` for math-shaped lines and math environment blocks that omit `$`/`\(` delimiters
|
|
13
|
+
- Added LaTeX-to-Unicode rendering for markdown math spans, converting `$$...$$`, `$...$`, `\(...\)`, and `\[...\]` into readable Unicode in Markdown output
|
|
14
|
+
- Exported LaTeX conversion helpers from the package entrypoint so consumers can call `latexToUnicode`, `latexToBlock`, `renderMathInText`, `inlineMathSpanEnd`, and `isBareMathEnvironment` directly
|
|
15
|
+
- Expanded LaTeX-to-Unicode conversion coverage for additional math fonts, delimiters, extensible arrows, layout environments, cancel/brace annotations, references, and AMS symbols
|
|
16
|
+
- Added ANSI color rendering for LaTeX `\textcolor`, scoped `\color`, `\colorbox`, and `\fcolorbox`, including xcolor/CSS color parsing and truecolor/256-color terminal output
|
|
17
|
+
- Added an optional `maxWidth` parameter to `MarkdownTheme.resolveMermaidAscii` to allow diagram resolvers to fit ASCII output to the available content width
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Changed markdown math rendering to preserve multiline layout for display equations, keeping `\\` row breaks as separate output lines (including inside list items)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed `alignat`/`alignedat`/`gatheredat` rendering in `latexToBlock` so the required `{n}` preamble is not rendered as visible math content
|
|
26
|
+
- Fixed math parsing to leave non-math LaTeX snippets (for example `\begin{itemize}`) and fenced code blocks as literal text instead of rendering them as math
|
|
27
|
+
- Fixed `renderInlineMarkdown` to handle top-level display-math tokens so raw `$$...$$` delimiters are no longer leaked
|
|
28
|
+
- Fixed inline math span detection so escaped dollars and currency-like patterns (such as `$5` and `$10`) are not converted as math
|
|
29
|
+
- Fixed Mermaid diagram rendering in Markdown code blocks to clip each ASCII line to content width before wrapping, preventing preformatted diagram rows from fragmenting
|
|
30
|
+
- Fixed fullscreen overlays losing keyboard focus to hidden prompt surfaces, which could make settings unresponsive while a background approval request was pending ([#2789](https://github.com/can1357/oh-my-pi/issues/2789)).
|
|
31
|
+
- Fixed `bun test` runs inside a real terminal leaking TUI output: `ProcessTerminal` now honors a headless test-runtime default, so frame paints, `start()` capability probes (OSC 11 / DA1 / kitty), the progress keepalive, notifications, and teardown escapes no longer reach the developer's terminal, and stdin raw mode is never engaged. Previously `#safeWrite` only skipped on `!process.stdout.isTTY`, so a developer running the suite in an interactive terminal saw stray status/editor boxes and probe queries. Terminal-contract suites opt back into real I/O via `setTerminalHeadless(false)`
|
|
32
|
+
|
|
5
33
|
## [16.0.2] - 2026-06-16
|
|
6
34
|
|
|
7
35
|
### Fixed
|
|
@@ -44,7 +44,7 @@ export interface MarkdownTheme {
|
|
|
44
44
|
* Resolve a mermaid ASCII rendering by fenced block source text.
|
|
45
45
|
* Return null to fall back to fenced code rendering.
|
|
46
46
|
*/
|
|
47
|
-
resolveMermaidAscii?: (source: string) => string | null;
|
|
47
|
+
resolveMermaidAscii?: (source: string, maxWidth?: number) => string | null;
|
|
48
48
|
symbols: SymbolTheme;
|
|
49
49
|
}
|
|
50
50
|
export declare class Markdown implements Component {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export * from "./fuzzy";
|
|
|
19
19
|
export * from "./keybindings";
|
|
20
20
|
export * from "./keys";
|
|
21
21
|
export * from "./kitty-graphics";
|
|
22
|
+
export * from "./latex-block";
|
|
23
|
+
export * from "./latex-to-unicode";
|
|
22
24
|
export * from "./mouse";
|
|
23
25
|
export * from "./stdin-buffer";
|
|
24
26
|
export type * from "./symbols";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a display LaTeX math fragment to lines, stacking `\frac` vertically.
|
|
3
|
+
* Top-level source newlines become vertical rows (so a `lhs =` line stays above
|
|
4
|
+
* its block); each row stacks fractions via `parseExpr`. Inline math should use
|
|
5
|
+
* `latexToUnicode` instead — fractions there stay single-line.
|
|
6
|
+
*/
|
|
7
|
+
export declare function latexToBlock(src: string): string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a bare LaTeX math fragment (no surrounding `$`/`\(` delimiters) to its
|
|
3
|
+
* best-effort Unicode rendering. Unknown commands degrade to their bare name;
|
|
4
|
+
* `\\` becomes a newline. Always returns a string (never throws).
|
|
5
|
+
*/
|
|
6
|
+
export declare function latexToUnicode(src: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* True when `env` is a math environment safe to auto-render without `$`/`\[`
|
|
9
|
+
* delimiters. The trailing `*` of starred variants (`align*`, `equation*`) is
|
|
10
|
+
* ignored; text-mode environments (`tabular`, `itemize`, …) return false.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBareMathEnvironment(env: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Scan prose for math spans — `$$…$$`, `\[…\]` (display) and `$…$`, `\(…\)`
|
|
15
|
+
* (inline) — and replace each with its Unicode rendering, leaving everything
|
|
16
|
+
* else verbatim. Newlines inside a span collapse to spaces so the result stays
|
|
17
|
+
* single-line-safe.
|
|
18
|
+
*
|
|
19
|
+
* Inline `$…$` uses pandoc's anti-currency heuristics: the opener must not be
|
|
20
|
+
* followed by whitespace, the closer must not be preceded by whitespace nor
|
|
21
|
+
* followed by a digit, and `\$` is treated as a literal dollar — so "$5 and
|
|
22
|
+
* $10" is left untouched.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderMathInText(text: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Index of the `$` that closes an inline math span opened at `open` (the index
|
|
27
|
+
* of the opening `$`), or -1 when the run is not inline math. Applies pandoc's
|
|
28
|
+
* anti-currency heuristics: the opener must not be followed by whitespace, the
|
|
29
|
+
* closer must not be preceded by whitespace nor followed by a digit, `\$` is a
|
|
30
|
+
* literal dollar, and the span may not span a newline. Shared by
|
|
31
|
+
* `renderMathInText` and the markdown math tokenizer so the rule has one home.
|
|
32
|
+
*/
|
|
33
|
+
export declare function inlineMathSpanEnd(text: string, open: number): number;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -66,6 +66,11 @@ export interface Component {
|
|
|
66
66
|
*/
|
|
67
67
|
dispose?(): void;
|
|
68
68
|
}
|
|
69
|
+
/** Lets an overlay root delegate keyboard focus to components it owns. */
|
|
70
|
+
export interface OverlayFocusOwner {
|
|
71
|
+
/** Returns true when `component` is a focus target inside this overlay. */
|
|
72
|
+
ownsOverlayFocusTarget(component: Component): boolean;
|
|
73
|
+
}
|
|
69
74
|
/**
|
|
70
75
|
* Component seam for append-only native-scrollback commits. A component that
|
|
71
76
|
* renders a finalized prefix followed by a live/mutating suffix reports the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.3",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.0.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.0.3",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.5"
|
|
44
44
|
},
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { LRUCache } from "lru-cache/raw";
|
|
2
|
-
import { Marked,
|
|
2
|
+
import { Marked, type Token, Tokenizer, type TokenizerAndRendererExtension, type Tokens } from "marked";
|
|
3
|
+
import { latexToBlock } from "../latex-block";
|
|
4
|
+
import { inlineMathSpanEnd, isBareMathEnvironment, latexToUnicode } from "../latex-to-unicode";
|
|
3
5
|
import type { SymbolTheme } from "../symbols";
|
|
4
6
|
import { TERMINAL } from "../terminal-capabilities";
|
|
5
7
|
import type { Component } from "../tui";
|
|
6
8
|
import {
|
|
7
9
|
applyBackgroundToLine,
|
|
10
|
+
Ellipsis,
|
|
8
11
|
encodeTextSized,
|
|
9
12
|
getSegmenter,
|
|
10
13
|
padding,
|
|
11
14
|
replaceTabs,
|
|
15
|
+
truncateToWidth,
|
|
12
16
|
visibleWidth,
|
|
13
17
|
wrapTextWithAnsi,
|
|
14
18
|
} from "../utils";
|
|
@@ -48,6 +52,126 @@ markdownParser.setOptions({
|
|
|
48
52
|
tokenizer: new StrictStrikethroughTokenizer(),
|
|
49
53
|
});
|
|
50
54
|
|
|
55
|
+
// Math spans (`$$…$$`, `\[…\]`, `$…$`, `\(…\)`) are tokenized as a dedicated
|
|
56
|
+
// `math` inline token before markdown's escape/emphasis/link rules run, so
|
|
57
|
+
// backslash commands (`\frac`, `\alpha`) and intraword underscores (`x_i`)
|
|
58
|
+
// survive intact instead of being mangled or split. The `$…$` form uses
|
|
59
|
+
// pandoc's anti-currency heuristic (`inlineMathSpanEnd`) so "$5 and $10" is
|
|
60
|
+
// never math. Inline extensions run before marked's escape tokenizer, so
|
|
61
|
+
// `\(…\)` becomes math while a genuinely escaped `\$` is left to `escape` and
|
|
62
|
+
// renders as a literal dollar.
|
|
63
|
+
const mathExtension: TokenizerAndRendererExtension = {
|
|
64
|
+
name: "math",
|
|
65
|
+
level: "inline",
|
|
66
|
+
start(src) {
|
|
67
|
+
const m = /\$|\\\(|\\\[/.exec(src);
|
|
68
|
+
return m ? m.index : undefined;
|
|
69
|
+
},
|
|
70
|
+
tokenizer(src) {
|
|
71
|
+
if (src.startsWith("$$")) {
|
|
72
|
+
const end = src.indexOf("$$", 2);
|
|
73
|
+
if (end !== -1 && src.slice(2, end).trim().length > 0) {
|
|
74
|
+
return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: true };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (src.startsWith("\\[")) {
|
|
79
|
+
const end = src.indexOf("\\]", 2);
|
|
80
|
+
if (end !== -1) return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: true };
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (src.startsWith("\\(")) {
|
|
84
|
+
const end = src.indexOf("\\)", 2);
|
|
85
|
+
if (end !== -1) return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: false };
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (src.charCodeAt(0) === 0x24 /* $ */) {
|
|
89
|
+
const end = inlineMathSpanEnd(src, 0);
|
|
90
|
+
if (end !== -1) return { type: "math", raw: src.slice(0, end + 1), text: src.slice(1, end), display: false };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
renderer(token) {
|
|
95
|
+
return (token as { text?: string }).text ?? "";
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Display math blocks: opening `$$` / `\[` and closing `$$` / `\]` each alone on
|
|
100
|
+
// their own line (≤3 leading spaces). Matched at the block level — before
|
|
101
|
+
// paragraph/list parsing — so a multi-line equation (e.g. a matrix with `\\`
|
|
102
|
+
// row breaks) renders across several lines instead of being collapsed onto one,
|
|
103
|
+
// and blank lines inside the block don't split it. The own-line requirement
|
|
104
|
+
// keeps inline `$$…$$` inside prose for the inline tokenizer above.
|
|
105
|
+
const MATH_BLOCK_DOLLAR = /^ {0,3}\$\$[ \t]*\n([\s\S]+?)\n {0,3}\$\$[ \t]*(?:\n|$)/;
|
|
106
|
+
const MATH_BLOCK_BRACKET = /^ {0,3}\\\[[ \t]*\n([\s\S]+?)\n {0,3}\\\][ \t]*(?:\n|$)/;
|
|
107
|
+
const MATH_BLOCK_START = /(?:^|\n) {0,3}(?:\$\$|\\\[)[ \t]*\n/;
|
|
108
|
+
const mathBlockExtension: TokenizerAndRendererExtension = {
|
|
109
|
+
name: "mathBlock",
|
|
110
|
+
level: "block",
|
|
111
|
+
start(src) {
|
|
112
|
+
const m = MATH_BLOCK_START.exec(src);
|
|
113
|
+
return m ? m.index : undefined;
|
|
114
|
+
},
|
|
115
|
+
tokenizer(src) {
|
|
116
|
+
const m = MATH_BLOCK_DOLLAR.exec(src) ?? MATH_BLOCK_BRACKET.exec(src);
|
|
117
|
+
if (!m || m[1].trim().length === 0) return undefined;
|
|
118
|
+
return { type: "math", raw: m[0], text: m[1], display: true };
|
|
119
|
+
},
|
|
120
|
+
renderer(token) {
|
|
121
|
+
return (token as { text?: string }).text ?? "";
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Bare (delimiter-less) display-math environments: `\begin{<mathenv>}…\end{…}`
|
|
126
|
+
// written without `$$`/`\[` fences (common in raw model output). Captured at the
|
|
127
|
+
// block level as a whole unit — including any immediately preceding `lhs =`
|
|
128
|
+
// line — so marked never splits it on inline `\\` row breaks. Restricted to math
|
|
129
|
+
// environments (isBareMathEnvironment), and the `≤3 leading spaces` + "block
|
|
130
|
+
// starts at offset 0" guards keep fenced/indented `\begin{cases}` code blocks
|
|
131
|
+
// for marked's own code rules.
|
|
132
|
+
const BARE_ENV_BEGIN = /(?:^|\n)[ \t]{0,3}\\begin\{([A-Za-z]+\*?)\}/;
|
|
133
|
+
function bareMathEnvBlock(src: string): readonly [number, number] | null {
|
|
134
|
+
const bm = BARE_ENV_BEGIN.exec(src);
|
|
135
|
+
if (!bm || !isBareMathEnvironment(bm[1])) return null;
|
|
136
|
+
const beginLineStart = bm.index === 0 ? 0 : bm.index + 1; // skip the matched leading `\n`
|
|
137
|
+
const endToken = `\\end{${bm[1]}}`;
|
|
138
|
+
const endAt = src.indexOf(endToken, bm.index);
|
|
139
|
+
if (endAt === -1) return null;
|
|
140
|
+
// The `\end` must close before any blank line (i.e. within the same block).
|
|
141
|
+
if (/\n[ \t]*\n/.test(src.slice(beginLineStart, endAt))) return null;
|
|
142
|
+
let blockEnd = endAt + endToken.length;
|
|
143
|
+
while (src[blockEnd] === " " || src[blockEnd] === "\t") blockEnd++;
|
|
144
|
+
if (src[blockEnd] === "\n") blockEnd++;
|
|
145
|
+
// Pull in one immediately-preceding `lhs =`/open-delimiter line (e.g. `f(x) =`).
|
|
146
|
+
let start = beginLineStart;
|
|
147
|
+
if (start > 0 && src[start - 1] === "\n") {
|
|
148
|
+
const prevStart = src.lastIndexOf("\n", start - 2) + 1;
|
|
149
|
+
const prevLine = src.slice(prevStart, start - 1);
|
|
150
|
+
if (/[=([{]\s*$/.test(prevLine)) start = prevStart;
|
|
151
|
+
}
|
|
152
|
+
return [start, blockEnd];
|
|
153
|
+
}
|
|
154
|
+
const mathEnvBlockExtension: TokenizerAndRendererExtension = {
|
|
155
|
+
name: "mathEnvBlock",
|
|
156
|
+
level: "block",
|
|
157
|
+
start(src) {
|
|
158
|
+
const r = bareMathEnvBlock(src);
|
|
159
|
+
return r ? r[0] : undefined;
|
|
160
|
+
},
|
|
161
|
+
tokenizer(src) {
|
|
162
|
+
const r = bareMathEnvBlock(src);
|
|
163
|
+
if (r?.[0] !== 0) return undefined; // only consume when the block starts at offset 0
|
|
164
|
+
const raw = src.slice(0, r[1]);
|
|
165
|
+
const text = raw.replace(/\n[ \t]*$/, "");
|
|
166
|
+
if (text.trim().length === 0) return undefined;
|
|
167
|
+
return { type: "math", raw, text, display: true };
|
|
168
|
+
},
|
|
169
|
+
renderer(token) {
|
|
170
|
+
return (token as { text?: string }).text ?? "";
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
markdownParser.use({ extensions: [mathBlockExtension, mathEnvBlockExtension, mathExtension] });
|
|
174
|
+
|
|
51
175
|
// ---------------------------------------------------------------------------
|
|
52
176
|
// Module-level LRU render cache
|
|
53
177
|
// ---------------------------------------------------------------------------
|
|
@@ -131,7 +255,7 @@ export interface MarkdownTheme {
|
|
|
131
255
|
* Resolve a mermaid ASCII rendering by fenced block source text.
|
|
132
256
|
* Return null to fall back to fenced code rendering.
|
|
133
257
|
*/
|
|
134
|
-
resolveMermaidAscii?: (source: string) => string | null;
|
|
258
|
+
resolveMermaidAscii?: (source: string, maxWidth?: number) => string | null;
|
|
135
259
|
symbols: SymbolTheme;
|
|
136
260
|
}
|
|
137
261
|
|
|
@@ -186,9 +310,46 @@ function encodeTextSizedHeading(text: string, scale: 1 | 2 | 3): string {
|
|
|
186
310
|
return out;
|
|
187
311
|
}
|
|
188
312
|
|
|
313
|
+
const MATH_NEWLINES = /\n+/g;
|
|
314
|
+
|
|
315
|
+
/** True for the custom inline `math` token produced by the math extension. */
|
|
316
|
+
function isMathToken(token: Token): token is Token & { text: string; display: boolean } {
|
|
317
|
+
return (token as { type: string }).type === "math";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Convert a `math` token's LaTeX to single-line Unicode for inline rendering. */
|
|
321
|
+
function renderMathToken(text: string): string {
|
|
322
|
+
return latexToUnicode(text).replace(MATH_NEWLINES, " ");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* When a paragraph's only meaningful content is a single display math token
|
|
327
|
+
* (`$$…$$` / `\[…\]`), return it so the paragraph can be stacked multi-line
|
|
328
|
+
* instead of flattened inline. Models routinely write display math on one line,
|
|
329
|
+
* which marked captures as an inline `display:true` math token inside a
|
|
330
|
+
* paragraph; without this it would flatten through `renderMathToken`.
|
|
331
|
+
*/
|
|
332
|
+
function soleDisplayMath(tokens?: Token[]): (Token & { text: string }) | null {
|
|
333
|
+
if (!tokens) return null;
|
|
334
|
+
let math: (Token & { text: string; display: boolean }) | null = null;
|
|
335
|
+
for (const token of tokens) {
|
|
336
|
+
if (isMathToken(token) && token.display) {
|
|
337
|
+
if (math) return null;
|
|
338
|
+
math = token;
|
|
339
|
+
} else if (!(token.type === "text" && typeof token.text === "string" && token.text.trim() === "")) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return math;
|
|
344
|
+
}
|
|
345
|
+
|
|
189
346
|
function plainInlineTokens(tokens: Token[]): string {
|
|
190
347
|
let result = "";
|
|
191
348
|
for (const token of tokens) {
|
|
349
|
+
if (isMathToken(token)) {
|
|
350
|
+
result += renderMathToken(token.text);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
192
353
|
switch (token.type) {
|
|
193
354
|
case "text":
|
|
194
355
|
result += token.tokens && token.tokens.length > 0 ? plainInlineTokens(token.tokens) : token.text;
|
|
@@ -658,6 +819,14 @@ export class Markdown implements Component {
|
|
|
658
819
|
#renderToken(token: Token, width: number, nextTokenType?: string, styleContext?: InlineStyleContext): string[] {
|
|
659
820
|
const lines: string[] = [];
|
|
660
821
|
|
|
822
|
+
// Display math block (own-line `$$…$$` / `\[…\]`): stack `\frac` vertically
|
|
823
|
+
// and keep `\\` row breaks, so fractions and matrices span multiple lines.
|
|
824
|
+
if (isMathToken(token)) {
|
|
825
|
+
for (const mathLine of latexToBlock(token.text)) lines.push(this.#applyDefaultStyle(mathLine));
|
|
826
|
+
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
827
|
+
return lines;
|
|
828
|
+
}
|
|
829
|
+
|
|
661
830
|
switch (token.type) {
|
|
662
831
|
case "heading": {
|
|
663
832
|
const headingLevel = token.depth;
|
|
@@ -692,6 +861,12 @@ export class Markdown implements Component {
|
|
|
692
861
|
}
|
|
693
862
|
|
|
694
863
|
case "paragraph": {
|
|
864
|
+
const displayMath = soleDisplayMath(token.tokens);
|
|
865
|
+
if (displayMath) {
|
|
866
|
+
for (const mathLine of latexToBlock(displayMath.text)) lines.push(this.#applyDefaultStyle(mathLine));
|
|
867
|
+
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") lines.push("");
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
695
870
|
const paragraphText = this.#renderInlineTokens(token.tokens || [], styleContext);
|
|
696
871
|
lines.push(paragraphText);
|
|
697
872
|
// Don't add spacing if next token is space or list
|
|
@@ -702,13 +877,18 @@ export class Markdown implements Component {
|
|
|
702
877
|
}
|
|
703
878
|
|
|
704
879
|
case "code": {
|
|
705
|
-
//
|
|
880
|
+
// Mermaid diagrams render as ASCII art when the theme supplies a
|
|
881
|
+
// resolver. The art is preformatted, so clip each row to the content
|
|
882
|
+
// width: the later wrap pass would otherwise fragment the box-drawing
|
|
883
|
+
// canvas. truncateToWidth is ANSI- and wide-char-aware, and the
|
|
884
|
+
// resolver already re-fits over-wide horizontal graphs top-down.
|
|
706
885
|
if (token.lang === "mermaid" && this.#theme.resolveMermaidAscii) {
|
|
707
|
-
const ascii = this.#theme.resolveMermaidAscii(token.text);
|
|
708
|
-
|
|
886
|
+
const ascii = this.#theme.resolveMermaidAscii(token.text, width);
|
|
709
887
|
if (ascii) {
|
|
710
|
-
for (const asciiLine of
|
|
711
|
-
lines.push(
|
|
888
|
+
for (const asciiLine of ascii.split("\n")) {
|
|
889
|
+
lines.push(
|
|
890
|
+
visibleWidth(asciiLine) > width ? truncateToWidth(asciiLine, width, Ellipsis.Omit) : asciiLine,
|
|
891
|
+
);
|
|
712
892
|
}
|
|
713
893
|
if (nextTokenType && nextTokenType !== "space") {
|
|
714
894
|
lines.push("");
|
|
@@ -839,6 +1019,10 @@ export class Markdown implements Component {
|
|
|
839
1019
|
const swatchGlyph = this.#theme.symbols.colorSwatch || DEFAULT_COLOR_SWATCH_GLYPH;
|
|
840
1020
|
|
|
841
1021
|
for (const token of tokens) {
|
|
1022
|
+
if (isMathToken(token)) {
|
|
1023
|
+
result += applyTextWithNewlines(renderMathToken(token.text));
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
842
1026
|
switch (token.type) {
|
|
843
1027
|
case "text":
|
|
844
1028
|
// Text tokens in list items can have nested tokens for inline formatting
|
|
@@ -996,16 +1180,29 @@ export class Markdown implements Component {
|
|
|
996
1180
|
lines.push({ text: nestedLine, nested: true });
|
|
997
1181
|
}
|
|
998
1182
|
} else if (token.type === "text") {
|
|
999
|
-
// Text content (may have inline tokens)
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1183
|
+
// Text content (may have inline tokens, or a sole display-math token)
|
|
1184
|
+
const displayMath = soleDisplayMath(token.tokens);
|
|
1185
|
+
if (displayMath) {
|
|
1186
|
+
const apply = styleContext?.applyText ?? ((t: string) => this.#applyDefaultStyle(t));
|
|
1187
|
+
for (const mathLine of latexToBlock(displayMath.text))
|
|
1188
|
+
lines.push({ text: apply(mathLine), nested: false });
|
|
1189
|
+
} else {
|
|
1190
|
+
const text =
|
|
1191
|
+
token.tokens && token.tokens.length > 0
|
|
1192
|
+
? this.#renderInlineTokens(token.tokens, styleContext)
|
|
1193
|
+
: token.text || "";
|
|
1194
|
+
lines.push({ text, nested: false });
|
|
1195
|
+
}
|
|
1005
1196
|
} else if (token.type === "paragraph") {
|
|
1006
1197
|
// Paragraph in list item
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1198
|
+
const apply = styleContext?.applyText ?? ((t: string) => this.#applyDefaultStyle(t));
|
|
1199
|
+
const displayMath = soleDisplayMath(token.tokens);
|
|
1200
|
+
if (displayMath) {
|
|
1201
|
+
for (const mathLine of latexToBlock(displayMath.text))
|
|
1202
|
+
lines.push({ text: apply(mathLine), nested: false });
|
|
1203
|
+
} else {
|
|
1204
|
+
lines.push({ text: this.#renderInlineTokens(token.tokens || [], styleContext), nested: false });
|
|
1205
|
+
}
|
|
1009
1206
|
} else if (token.type === "code") {
|
|
1010
1207
|
// Code block in list item
|
|
1011
1208
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
@@ -1022,6 +1219,10 @@ export class Markdown implements Component {
|
|
|
1022
1219
|
}
|
|
1023
1220
|
}
|
|
1024
1221
|
lines.push({ text: this.#theme.codeBlockBorder("```"), nested: false });
|
|
1222
|
+
} else if (isMathToken(token)) {
|
|
1223
|
+
// Display math block inside a list item: stack fractions / matrix rows.
|
|
1224
|
+
const apply = styleContext?.applyText ?? ((t: string) => this.#applyDefaultStyle(t));
|
|
1225
|
+
for (const mathLine of latexToBlock(token.text)) lines.push({ text: apply(mathLine), nested: false });
|
|
1025
1226
|
} else {
|
|
1026
1227
|
// Other token types - try to render as inline
|
|
1027
1228
|
const text = this.#renderInlineTokens([token], styleContext);
|
|
@@ -1249,10 +1450,14 @@ export class Markdown implements Component {
|
|
|
1249
1450
|
export function renderInlineMarkdown(text: string, mdTheme: MarkdownTheme, baseColor?: (t: string) => string): string {
|
|
1250
1451
|
// Guard against undefined/null during streaming — partial JSON can leave fields unpopulated.
|
|
1251
1452
|
if (typeof text !== "string") return (baseColor ?? (t => t))(text != null ? String(text) : "");
|
|
1252
|
-
const tokens =
|
|
1453
|
+
const tokens = markdownParser.lexer(text);
|
|
1253
1454
|
const applyText = baseColor ?? ((t: string) => t);
|
|
1254
1455
|
let result = "";
|
|
1255
1456
|
for (const token of tokens) {
|
|
1457
|
+
if (isMathToken(token)) {
|
|
1458
|
+
result += applyText(renderMathToken(token.text));
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1256
1461
|
if (token.type === "paragraph" && token.tokens) {
|
|
1257
1462
|
result += renderInlineTokens(token.tokens, mdTheme, applyText);
|
|
1258
1463
|
} else if (token.type === "list") {
|
|
@@ -1274,6 +1479,10 @@ function renderInlineTokens(tokens: Token[], mdTheme: MarkdownTheme, applyText:
|
|
|
1274
1479
|
let result = "";
|
|
1275
1480
|
const styleReset = applyText("");
|
|
1276
1481
|
for (const token of tokens) {
|
|
1482
|
+
if (isMathToken(token)) {
|
|
1483
|
+
result += applyText(renderMathToken(token.text));
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1277
1486
|
switch (token.type) {
|
|
1278
1487
|
case "text":
|
|
1279
1488
|
if (token.tokens && token.tokens.length > 0) {
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,9 @@ export * from "./keybindings";
|
|
|
29
29
|
export * from "./keys";
|
|
30
30
|
// Kitty graphics: Unicode placeholders
|
|
31
31
|
export * from "./kitty-graphics";
|
|
32
|
+
// LaTeX → Unicode/ANSI math rendering
|
|
33
|
+
export * from "./latex-block";
|
|
34
|
+
export * from "./latex-to-unicode";
|
|
32
35
|
// SGR mouse report parsing
|
|
33
36
|
export * from "./mouse";
|
|
34
37
|
// Mermaid diagram support
|