@oh-my-pi/pi-tui 17.4.2 → 18.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +1 -1
- package/dist/types/autocomplete.d.ts +17 -6
- package/dist/types/components/editor.d.ts +42 -5
- package/dist/types/components/image.d.ts +2 -5
- package/dist/types/components/markdown.d.ts +18 -24
- package/dist/types/components/select-list.d.ts +9 -0
- package/dist/types/components/text.d.ts +0 -1
- package/dist/types/keybindings.d.ts +5 -0
- package/dist/types/terminal-capabilities.d.ts +10 -3
- package/dist/types/terminal.d.ts +42 -2
- package/dist/types/tui.d.ts +63 -218
- package/dist/types/utils.d.ts +3 -2
- package/package.json +4 -4
- package/src/autocomplete.ts +147 -61
- package/src/components/editor.ts +407 -157
- package/src/components/image.ts +4 -6
- package/src/components/loader.ts +1 -9
- package/src/components/markdown.ts +133 -417
- package/src/components/select-list.ts +67 -15
- package/src/components/text.ts +8 -18
- package/src/fuzzy.ts +63 -6
- package/src/keybindings.ts +5 -0
- package/src/stdin-buffer.ts +130 -20
- package/src/terminal-capabilities.ts +20 -3
- package/src/terminal.ts +147 -22
- package/src/tui.ts +628 -3510
- package/src/utils.ts +12 -3
|
@@ -11,12 +11,7 @@ import { latexToBlock } from "../latex-block";
|
|
|
11
11
|
import { inlineMathSpanEnd, isBareMathEnvironment, latexToUnicode } from "../latex-to-unicode";
|
|
12
12
|
import type { SymbolTheme } from "../symbols";
|
|
13
13
|
import { TERMINAL } from "../terminal-capabilities";
|
|
14
|
-
import type {
|
|
15
|
-
Component,
|
|
16
|
-
NativeScrollbackCommittedRows,
|
|
17
|
-
NativeScrollbackReplay,
|
|
18
|
-
NativeScrollbackWidthEpoch,
|
|
19
|
-
} from "../tui";
|
|
14
|
+
import type { Component } from "../tui";
|
|
20
15
|
import {
|
|
21
16
|
applyBackgroundToLine,
|
|
22
17
|
Ellipsis,
|
|
@@ -924,16 +919,11 @@ function renderedLine(text: string, literalCode?: boolean): RenderedLine {
|
|
|
924
919
|
return literalCode ? { text, literalCode: true } : { text };
|
|
925
920
|
}
|
|
926
921
|
|
|
927
|
-
|
|
928
|
-
lines: readonly string[];
|
|
929
|
-
tables: readonly RenderedTableLayout[];
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const renderCache = new LRUCache<string, RenderCacheEntry>({
|
|
922
|
+
const renderCache = new LRUCache<string, readonly string[]>({
|
|
933
923
|
max: RENDER_CACHE_MAX,
|
|
934
924
|
maxSize: RENDER_CACHE_MAX_SIZE,
|
|
935
925
|
maxEntrySize: RENDER_CACHE_MAX_ENTRY_SIZE,
|
|
936
|
-
sizeCalculation:
|
|
926
|
+
sizeCalculation: renderedLinesCacheSize,
|
|
937
927
|
});
|
|
938
928
|
|
|
939
929
|
function renderedLinesCacheSize(lines: readonly string[]): number {
|
|
@@ -942,12 +932,6 @@ function renderedLinesCacheSize(lines: readonly string[]): number {
|
|
|
942
932
|
return Math.max(1, size);
|
|
943
933
|
}
|
|
944
934
|
|
|
945
|
-
function renderCacheEntrySize(entry: RenderCacheEntry): number {
|
|
946
|
-
let size = renderedLinesCacheSize(entry.lines);
|
|
947
|
-
for (const table of entry.tables) size += table.key.length + table.columnWidths.length + 4;
|
|
948
|
-
return size;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
935
|
// A reference-link definition (`[label]: dest`) resolves across the whole
|
|
952
936
|
// document, so a split lex cannot reproduce it — disable the streaming fast path
|
|
953
937
|
// when one is present (rare in streamed output). The label may contain
|
|
@@ -1165,6 +1149,15 @@ export interface DefaultTextStyle {
|
|
|
1165
1149
|
underline?: boolean;
|
|
1166
1150
|
}
|
|
1167
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* Stateful incremental code highlighter carrying parser state across pushes.
|
|
1154
|
+
* Produced per streaming fence by {@link MarkdownTheme.createHighlightStream}.
|
|
1155
|
+
*/
|
|
1156
|
+
export interface HighlightStreamSession {
|
|
1157
|
+
/** Highlight the next chunk and advance parser state. */
|
|
1158
|
+
push(chunk: string): string;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1168
1161
|
/**
|
|
1169
1162
|
* Theme functions for markdown elements.
|
|
1170
1163
|
* Each function takes text and returns styled text with ANSI codes.
|
|
@@ -1185,6 +1178,14 @@ export interface MarkdownTheme {
|
|
|
1185
1178
|
strikethrough: (text: string) => string;
|
|
1186
1179
|
underline: (text: string) => string;
|
|
1187
1180
|
highlightCode?: (code: string, lang?: string) => string[];
|
|
1181
|
+
/**
|
|
1182
|
+
* Create a stateful incremental highlighter for one streaming code fence.
|
|
1183
|
+
* `push` receives newline-terminated complete lines (only the final push
|
|
1184
|
+
* may omit the trailing newline) and must return highlighted ANSI text for
|
|
1185
|
+
* exactly the pushed chunk, byte-identical to highlighting the concatenated
|
|
1186
|
+
* text through `highlightCode`. Return null when `lang` is unsupported.
|
|
1187
|
+
*/
|
|
1188
|
+
createHighlightStream?: (lang?: string) => HighlightStreamSession | null;
|
|
1188
1189
|
/**
|
|
1189
1190
|
* Resolve a mermaid ASCII rendering by fenced block source text.
|
|
1190
1191
|
* Return null to fall back to fenced code rendering.
|
|
@@ -1453,35 +1454,25 @@ interface StreamPrefixLineCache extends RenderSignature {
|
|
|
1453
1454
|
text: string;
|
|
1454
1455
|
tokenCount: number;
|
|
1455
1456
|
lines: readonly string[];
|
|
1456
|
-
tables: readonly TableRenderSpec[];
|
|
1457
1457
|
}
|
|
1458
|
-
interface
|
|
1458
|
+
interface StreamingHighlightCache extends RenderSignature {
|
|
1459
1459
|
lang: string | undefined;
|
|
1460
1460
|
text: string;
|
|
1461
1461
|
lines: readonly string[];
|
|
1462
|
+
stream: HighlightStreamSession;
|
|
1462
1463
|
}
|
|
1463
1464
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
startRow: number;
|
|
1473
|
-
endRow: number;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
interface RenderedTableLayout extends TableLayoutLock {
|
|
1477
|
-
key: string;
|
|
1478
|
-
startRow: number;
|
|
1479
|
-
endRow: number;
|
|
1465
|
+
/**
|
|
1466
|
+
* Split a highlight-stream push result (newline-terminated lines) into
|
|
1467
|
+
* per-line strings, dropping the empty tail produced by the final newline.
|
|
1468
|
+
*/
|
|
1469
|
+
function splitPushedHighlightLines(pushed: string): string[] {
|
|
1470
|
+
const lines = pushed.split("\n");
|
|
1471
|
+
lines.pop();
|
|
1472
|
+
return lines;
|
|
1480
1473
|
}
|
|
1481
1474
|
|
|
1482
|
-
export class Markdown
|
|
1483
|
-
implements Component, NativeScrollbackCommittedRows, NativeScrollbackReplay, NativeScrollbackWidthEpoch
|
|
1484
|
-
{
|
|
1475
|
+
export class Markdown implements Component {
|
|
1485
1476
|
#text: string;
|
|
1486
1477
|
#paddingX: number; // Left/right padding
|
|
1487
1478
|
#paddingY: number; // Top/bottom padding
|
|
@@ -1509,48 +1500,19 @@ export class Markdown
|
|
|
1509
1500
|
#streamPrefixText?: string;
|
|
1510
1501
|
#streamPrefixTokens?: Token[];
|
|
1511
1502
|
#streamPrefixLineCache?: StreamPrefixLineCache;
|
|
1512
|
-
// Rows of the most recent render() that are settled — top padding plus the
|
|
1513
|
-
// rendered frozen token prefix — exposed via getLastRenderSettledRows()
|
|
1514
|
-
// for native-scrollback commit gating.
|
|
1515
|
-
#lastRenderSettledRows = 0;
|
|
1516
|
-
// Frozen-prefix text backing the last non-zero settled exposure. Settled
|
|
1517
|
-
// rows are declared final downstream, so a render whose frozen text no
|
|
1518
|
-
// longer extends this prefix (a rewind / wholesale rewrite) resets the
|
|
1519
|
-
// exposure to 0 and re-earns it — the exposure is hard-monotone within a
|
|
1520
|
-
// text lineage.
|
|
1521
|
-
#settledExposedText?: string;
|
|
1522
|
-
// Semantic source state that produced the most recent render. Unlike #text,
|
|
1523
|
-
// it does not advance when streaming updates arrive before the next paint.
|
|
1524
|
-
#lastRenderedText?: string;
|
|
1525
|
-
#lastRenderedTransientRenderCache = false;
|
|
1526
|
-
#lastRenderedHasMutableTrailingRow = false;
|
|
1527
|
-
#widthEpochBoundaries = new WeakMap<
|
|
1528
|
-
object,
|
|
1529
|
-
{ text: string; transientRenderCache: boolean; hasMutableTrailingRow: boolean }
|
|
1530
|
-
>();
|
|
1531
|
-
|
|
1532
1503
|
// True while #renderStreamingContentLines renders the frozen token range:
|
|
1533
1504
|
// frozen code blocks highlight even in transient mode so their bytes match
|
|
1534
1505
|
// the finalized render (they render once into the prefix line cache, so
|
|
1535
|
-
// the FFI cost is amortized).
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
|
|
1539
|
-
|
|
1506
|
+
// the FFI cost is amortized). In the volatile tail, an open fence
|
|
1507
|
+
// incrementally highlights its completed lines through a stateful
|
|
1508
|
+
// highlight stream (falling back to per-line highlighting for diff-family
|
|
1509
|
+
// fences when the theme lacks one) so completed rows are styled immediately;
|
|
1510
|
+
// only the trailing partial line stays unhighlighted.
|
|
1511
|
+
#renderingStablePrefix = false;
|
|
1512
|
+
#streamingHighlightCache?: StreamingHighlightCache;
|
|
1540
1513
|
#activeRenderSignature?: RenderSignature;
|
|
1541
|
-
// Streaming tables may grow naturally while wholly repaintable. Once any
|
|
1542
|
-
// physical row of a table enters native scrollback, its current column widths
|
|
1543
|
-
// are locked for the rest of this append-only text lineage: future wider cells
|
|
1544
|
-
// wrap inside those columns instead of reflowing immutable history above.
|
|
1545
|
-
#tableLayoutWidth?: number;
|
|
1546
|
-
#lockedTableLayouts = new Map<string, TableLayoutLock>();
|
|
1547
|
-
#lastRenderedTableLayouts: RenderedTableLayout[] = [];
|
|
1548
|
-
#activeTableRenderSpecs?: TableRenderSpec[];
|
|
1549
|
-
|
|
1550
1514
|
#ignoreTight = false;
|
|
1551
|
-
|
|
1552
1515
|
setIgnoreTight(ignore: boolean): this {
|
|
1553
|
-
if (this.#ignoreTight !== ignore) this.#clearTableLayouts();
|
|
1554
1516
|
this.#ignoreTight = ignore;
|
|
1555
1517
|
this.invalidate();
|
|
1556
1518
|
return this;
|
|
@@ -1580,7 +1542,6 @@ export class Markdown
|
|
|
1580
1542
|
// full lex + wrap runs per re-emit — one of the top CPU hotspots during
|
|
1581
1543
|
// streaming (issue #4353). Mirrors `Text.setText`'s guard.
|
|
1582
1544
|
if (text === this.#text) return false;
|
|
1583
|
-
if (!text.startsWith(this.#text)) this.#clearTableLayouts();
|
|
1584
1545
|
this.#text = text;
|
|
1585
1546
|
if (!text.trim()) {
|
|
1586
1547
|
// Blank replacement: render() early-returns before #lexTokens can see
|
|
@@ -1589,7 +1550,6 @@ export class Markdown
|
|
|
1589
1550
|
this.#streamPrefixText = undefined;
|
|
1590
1551
|
this.#streamPrefixTokens = undefined;
|
|
1591
1552
|
this.#streamPrefixLineCache = undefined;
|
|
1592
|
-
this.#settledExposedText = undefined;
|
|
1593
1553
|
}
|
|
1594
1554
|
this.invalidate();
|
|
1595
1555
|
return true;
|
|
@@ -1611,104 +1571,6 @@ export class Markdown
|
|
|
1611
1571
|
this.invalidate();
|
|
1612
1572
|
}
|
|
1613
1573
|
|
|
1614
|
-
/**
|
|
1615
|
-
* Rows at the top of the most recent render() (top padding + rendered
|
|
1616
|
-
* frozen-token prefix) whose bytes are settled: byte-stable at this
|
|
1617
|
-
* width/theme for as long as the text keeps growing append-only. Hosts
|
|
1618
|
-
* feed this to transcript commit gating (see the coding agent's
|
|
1619
|
-
* `FinalizableBlock.getTranscriptBlockSettledRows`). 0 outside streaming
|
|
1620
|
-
* (`transientRenderCache`) mode, after a text rewind (re-earned on the new
|
|
1621
|
-
* lineage), and on cache-served non-streaming renders.
|
|
1622
|
-
*/
|
|
1623
|
-
getLastRenderSettledRows(): number {
|
|
1624
|
-
return this.#lastRenderSettledRows;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
captureNativeScrollbackWidthEpoch(): unknown {
|
|
1628
|
-
if (this.#lastRenderedText === undefined) return undefined;
|
|
1629
|
-
const marker = {};
|
|
1630
|
-
this.#widthEpochBoundaries.set(marker, {
|
|
1631
|
-
text: this.#lastRenderedText,
|
|
1632
|
-
transientRenderCache: this.#lastRenderedTransientRenderCache,
|
|
1633
|
-
hasMutableTrailingRow: this.#lastRenderedHasMutableTrailingRow,
|
|
1634
|
-
});
|
|
1635
|
-
return marker;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined {
|
|
1639
|
-
if (typeof boundary !== "object" || boundary === null || this.#cachedWidth === undefined) return undefined;
|
|
1640
|
-
const captured = this.#widthEpochBoundaries.get(boundary);
|
|
1641
|
-
if (captured === undefined) return undefined;
|
|
1642
|
-
const snapshot = new Markdown(
|
|
1643
|
-
captured.text,
|
|
1644
|
-
this.#paddingX,
|
|
1645
|
-
this.#paddingY,
|
|
1646
|
-
this.#theme,
|
|
1647
|
-
this.#defaultTextStyle,
|
|
1648
|
-
this.#codeBlockIndent,
|
|
1649
|
-
);
|
|
1650
|
-
snapshot.#ignoreTight = this.#ignoreTight;
|
|
1651
|
-
snapshot.#transientRenderCache = captured.transientRenderCache;
|
|
1652
|
-
return Math.max(
|
|
1653
|
-
0,
|
|
1654
|
-
snapshot.render(this.#cachedWidth).length - this.#paddingY - (captured.hasMutableTrailingRow ? 1 : 0),
|
|
1655
|
-
);
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
getNativeScrollbackWidthEpochRows(): number | undefined {
|
|
1659
|
-
return this.#cachedLines === undefined ? undefined : this.#widthEpochRows(this.#cachedLines.length);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
isNativeScrollbackWidthEpochAppendOnly(boundary: unknown): boolean {
|
|
1663
|
-
if (typeof boundary !== "object" || boundary === null) return true;
|
|
1664
|
-
return this.#widthEpochBoundaries.get(boundary)?.hasMutableTrailingRow !== true;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
#widthEpochRows(renderedRows: number): number {
|
|
1668
|
-
return Math.max(0, renderedRows - this.#paddingY - (this.#transientRenderCache ? 1 : 0));
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
#recordLastRenderedState(hasContentRows: boolean): void {
|
|
1672
|
-
this.#lastRenderedText = this.#text;
|
|
1673
|
-
this.#lastRenderedTransientRenderCache = this.#transientRenderCache;
|
|
1674
|
-
this.#lastRenderedHasMutableTrailingRow = this.#transientRenderCache && hasContentRows;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
/**
|
|
1678
|
-
* Freeze every table whose first physical row is already part of the native
|
|
1679
|
-
* scrollback prefix. The recorded widths came from the exact frame that was
|
|
1680
|
-
* just emitted, so the next streamed delta cannot retroactively widen it.
|
|
1681
|
-
*/
|
|
1682
|
-
setNativeScrollbackCommittedRows(rows: number): void {
|
|
1683
|
-
const committed = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
|
|
1684
|
-
let changed = false;
|
|
1685
|
-
for (const table of this.#lastRenderedTableLayouts) {
|
|
1686
|
-
if (table.startRow >= committed || this.#lockedTableLayouts.has(table.key)) continue;
|
|
1687
|
-
this.#lockedTableLayouts.set(table.key, {
|
|
1688
|
-
availableWidth: table.availableWidth,
|
|
1689
|
-
columnWidths: table.columnWidths.slice(),
|
|
1690
|
-
});
|
|
1691
|
-
changed = true;
|
|
1692
|
-
}
|
|
1693
|
-
if (changed) this.invalidate();
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/** A destructive replay removes the immutable tape this layout was guarding. */
|
|
1697
|
-
prepareNativeScrollbackReplay(): void {
|
|
1698
|
-
this.#clearTableLayouts();
|
|
1699
|
-
this.#tableLayoutWidth = undefined;
|
|
1700
|
-
this.invalidate();
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
#clearTableLayouts(): void {
|
|
1704
|
-
this.#lockedTableLayouts.clear();
|
|
1705
|
-
this.#lastRenderedTableLayouts = [];
|
|
1706
|
-
this.#activeTableRenderSpecs = undefined;
|
|
1707
|
-
// Same-width replay/non-append rewrites could otherwise reuse physical
|
|
1708
|
-
// prefix lines rendered with the retired locked widths.
|
|
1709
|
-
this.#streamPrefixLineCache = undefined;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
1574
|
// Lex `text` into block tokens, reusing the frozen stable prefix when the text
|
|
1713
1575
|
// only grew (the streaming path). Falls back to a full lex whenever the prefix
|
|
1714
1576
|
// is no longer a prefix (non-append edit), the text carries reference-link
|
|
@@ -1716,17 +1578,19 @@ export class Markdown
|
|
|
1716
1578
|
// raw-span offsets). Every fallback is correctness-preserving — only speed
|
|
1717
1579
|
// differs; the render loop sees the identical token list either way.
|
|
1718
1580
|
#lexTokens(text: string): Token[] {
|
|
1719
|
-
|
|
1581
|
+
// When a frozen prefix exists, it was already verified ref-def-free when
|
|
1582
|
+
// frozen (#freezeStablePrefix only runs when canStream was true). The prefix
|
|
1583
|
+
// ends at a "\n\n" block boundary (stableBlockBoundary), so the tail starts
|
|
1584
|
+
// at a fresh line — scanning only the tail for ref defs is sufficient and
|
|
1585
|
+
// avoids re-scanning the growing prefix every frame (O(n²) → O(n) overall).
|
|
1720
1586
|
const prefix = this.#streamPrefixText;
|
|
1721
1587
|
const prefixTokens = this.#streamPrefixTokens;
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
) {
|
|
1729
|
-
const tailTokens = lexDocument(text.slice(prefix.length));
|
|
1588
|
+
const hasPrefix =
|
|
1589
|
+
prefix !== undefined && prefixTokens !== undefined && text.length > prefix.length && text.startsWith(prefix);
|
|
1590
|
+
const refDefText = hasPrefix ? text.slice(prefix.length) : text;
|
|
1591
|
+
const canStream = !HAS_REF_DEF.test(refDefText) && !refDefText.includes("\r");
|
|
1592
|
+
if (canStream && hasPrefix) {
|
|
1593
|
+
const tailTokens = lexDocument(refDefText);
|
|
1730
1594
|
const tokens = [...prefixTokens, ...tailTokens];
|
|
1731
1595
|
this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
1732
1596
|
return tokens;
|
|
@@ -1763,24 +1627,14 @@ export class Markdown
|
|
|
1763
1627
|
}
|
|
1764
1628
|
|
|
1765
1629
|
render(width: number): readonly string[] {
|
|
1766
|
-
if (this.#tableLayoutWidth !== undefined && this.#tableLayoutWidth !== width) {
|
|
1767
|
-
this.#clearTableLayouts();
|
|
1768
|
-
this.invalidate();
|
|
1769
|
-
}
|
|
1770
|
-
this.#tableLayoutWidth = width;
|
|
1771
1630
|
// L1: per-instance cache — fastest path for repeated renders of the same
|
|
1772
1631
|
// instance at the same width (e.g. resize debounce, repeated redraws).
|
|
1773
1632
|
// Returning the cached reference is load-bearing: parents memoize their
|
|
1774
1633
|
// concatenation on reference equality.
|
|
1775
1634
|
if (this.#cachedLines && this.#cachedText === this.#text && this.#cachedWidth === width) {
|
|
1776
|
-
this.#recordLastRenderedState(this.#cachedLines.length > 0);
|
|
1777
1635
|
return this.#cachedLines;
|
|
1778
1636
|
}
|
|
1779
1637
|
|
|
1780
|
-
// Recomputed below by the streaming path; every other path (cache-served,
|
|
1781
|
-
// empty text, non-streaming full render) exposes no settled rows.
|
|
1782
|
-
this.#lastRenderSettledRows = 0;
|
|
1783
|
-
|
|
1784
1638
|
// Calculate available width for content (subtract horizontal padding)
|
|
1785
1639
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
1786
1640
|
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
@@ -1790,7 +1644,6 @@ export class Markdown
|
|
|
1790
1644
|
this.#cachedText = this.#text;
|
|
1791
1645
|
this.#cachedWidth = width;
|
|
1792
1646
|
this.#cachedLines = EMPTY_RENDER_LINES;
|
|
1793
|
-
this.#recordLastRenderedState(false);
|
|
1794
1647
|
return EMPTY_RENDER_LINES;
|
|
1795
1648
|
}
|
|
1796
1649
|
|
|
@@ -1813,41 +1666,29 @@ export class Markdown
|
|
|
1813
1666
|
// theme.heading is used as the representative theme probe — it's required
|
|
1814
1667
|
// by MarkdownTheme and is one of the most styling-sensitive entries.
|
|
1815
1668
|
let cacheKey: string | undefined;
|
|
1816
|
-
if (!this.transientRenderCache
|
|
1669
|
+
if (!this.transientRenderCache) {
|
|
1817
1670
|
cacheKey = this.#renderCacheKey(normalizedText, signature);
|
|
1818
1671
|
const cached = renderCache.get(cacheKey);
|
|
1819
1672
|
if (cached !== undefined) {
|
|
1820
|
-
// Restore both the rendered rows and the geometry metadata that produced
|
|
1821
|
-
// them. A later scrollback publication must never lock widths from an
|
|
1822
|
-
// older transient frame against rows served from this cache entry.
|
|
1823
|
-
this.#lastRenderedTableLayouts = cached.tables.map(table => ({
|
|
1824
|
-
...table,
|
|
1825
|
-
columnWidths: table.columnWidths.slice(),
|
|
1826
|
-
}));
|
|
1827
1673
|
// Populate L1 so subsequent calls from this instance are O(1) map lookup.
|
|
1828
1674
|
this.#cachedText = this.#text;
|
|
1829
1675
|
this.#cachedWidth = width;
|
|
1830
|
-
this.#cachedLines = cached
|
|
1831
|
-
|
|
1832
|
-
return cached.lines;
|
|
1676
|
+
this.#cachedLines = cached;
|
|
1677
|
+
return cached;
|
|
1833
1678
|
}
|
|
1834
1679
|
}
|
|
1835
1680
|
|
|
1836
1681
|
// Parse markdown to HTML-like tokens
|
|
1837
1682
|
const tokens = this.#lexTokens(normalizedText);
|
|
1838
1683
|
let contentLines: string[];
|
|
1839
|
-
const tableRenderSpecs: TableRenderSpec[] = [];
|
|
1840
|
-
this.#activeTableRenderSpecs = tableRenderSpecs;
|
|
1841
1684
|
this.#activeRenderSignature = signature;
|
|
1842
1685
|
try {
|
|
1843
1686
|
contentLines = this.transientRenderCache
|
|
1844
1687
|
? this.#renderStreamingContentLines(tokens, normalizedText, signature, contentWidth)
|
|
1845
|
-
: this.#renderContentLines(tokens, 0, tokens.length, contentWidth, signature
|
|
1688
|
+
: this.#renderContentLines(tokens, 0, tokens.length, contentWidth, signature);
|
|
1846
1689
|
} finally {
|
|
1847
1690
|
this.#activeRenderSignature = undefined;
|
|
1848
|
-
this.#activeTableRenderSpecs = undefined;
|
|
1849
1691
|
}
|
|
1850
|
-
this.#lastRenderedTableLayouts = this.#resolveRenderedTableLayouts(tableRenderSpecs, signature.paddingY);
|
|
1851
1692
|
const emptyLines = this.#renderEmptyPaddingLines(signature);
|
|
1852
1693
|
|
|
1853
1694
|
// Combine top padding, content, and bottom padding
|
|
@@ -1864,16 +1705,8 @@ export class Markdown
|
|
|
1864
1705
|
// Update L2 module-level LRU so future instances with the same key skip
|
|
1865
1706
|
// the marked.lexer + highlightCode (Rust FFI) work entirely.
|
|
1866
1707
|
if (cacheKey !== undefined) {
|
|
1867
|
-
renderCache.set(cacheKey,
|
|
1868
|
-
lines: result,
|
|
1869
|
-
tables: this.#lastRenderedTableLayouts.map(table => ({
|
|
1870
|
-
...table,
|
|
1871
|
-
columnWidths: table.columnWidths.slice(),
|
|
1872
|
-
})),
|
|
1873
|
-
});
|
|
1708
|
+
renderCache.set(cacheKey, result);
|
|
1874
1709
|
}
|
|
1875
|
-
this.#recordLastRenderedState(contentLines.length > 0);
|
|
1876
|
-
|
|
1877
1710
|
return result;
|
|
1878
1711
|
}
|
|
1879
1712
|
|
|
@@ -1905,78 +1738,43 @@ export class Markdown
|
|
|
1905
1738
|
signature: RenderSignature,
|
|
1906
1739
|
contentWidth: number,
|
|
1907
1740
|
): string[] {
|
|
1908
|
-
const
|
|
1909
|
-
const
|
|
1910
|
-
if (
|
|
1911
|
-
return this.#renderContentLines(tokens, 0, tokens.length, contentWidth, signature
|
|
1741
|
+
const stableText = this.#streamPrefixText;
|
|
1742
|
+
const stableTokenCount = this.#streamPrefixTokens?.length ?? 0;
|
|
1743
|
+
if (stableText === undefined || stableTokenCount === 0 || !normalizedText.startsWith(stableText)) {
|
|
1744
|
+
return this.#renderContentLines(tokens, 0, tokens.length, contentWidth, signature);
|
|
1912
1745
|
}
|
|
1913
1746
|
|
|
1914
1747
|
const contentLines: string[] = [];
|
|
1915
|
-
const reusablePrefix = this.#matchingStreamPrefixLineCache(normalizedText,
|
|
1748
|
+
const reusablePrefix = this.#matchingStreamPrefixLineCache(normalizedText, stableText, signature);
|
|
1916
1749
|
let renderedUntil = 0;
|
|
1917
|
-
|
|
1918
|
-
if (reusablePrefix && reusablePrefix.tokenCount <= frozenTokenCount) {
|
|
1750
|
+
if (reusablePrefix && reusablePrefix.tokenCount <= stableTokenCount) {
|
|
1919
1751
|
contentLines.push(...reusablePrefix.lines);
|
|
1920
|
-
this.#activeTableRenderSpecs?.push(...reusablePrefix.tables);
|
|
1921
1752
|
renderedUntil = reusablePrefix.tokenCount;
|
|
1922
|
-
renderedSourceOffset = reusablePrefix.text.length;
|
|
1923
1753
|
}
|
|
1924
1754
|
|
|
1925
|
-
if (renderedUntil <
|
|
1926
|
-
//
|
|
1755
|
+
if (renderedUntil < stableTokenCount) {
|
|
1756
|
+
// Stable tokens render with full fidelity (syntax highlighting on)
|
|
1927
1757
|
// so these cached rows byte-match the finalized render.
|
|
1928
|
-
this.#
|
|
1758
|
+
this.#renderingStablePrefix = true;
|
|
1929
1759
|
try {
|
|
1930
1760
|
contentLines.push(
|
|
1931
|
-
...this.#renderContentLines(
|
|
1932
|
-
tokens,
|
|
1933
|
-
renderedUntil,
|
|
1934
|
-
frozenTokenCount,
|
|
1935
|
-
contentWidth,
|
|
1936
|
-
signature,
|
|
1937
|
-
contentLines.length,
|
|
1938
|
-
renderedSourceOffset,
|
|
1939
|
-
),
|
|
1761
|
+
...this.#renderContentLines(tokens, renderedUntil, stableTokenCount, contentWidth, signature),
|
|
1940
1762
|
);
|
|
1941
1763
|
} finally {
|
|
1942
|
-
this.#
|
|
1764
|
+
this.#renderingStablePrefix = false;
|
|
1943
1765
|
}
|
|
1944
|
-
renderedUntil =
|
|
1766
|
+
renderedUntil = stableTokenCount;
|
|
1945
1767
|
}
|
|
1946
1768
|
|
|
1947
1769
|
this.#streamPrefixLineCache = {
|
|
1948
1770
|
...signature,
|
|
1949
|
-
text:
|
|
1950
|
-
tokenCount:
|
|
1771
|
+
text: stableText,
|
|
1772
|
+
tokenCount: stableTokenCount,
|
|
1951
1773
|
lines: contentLines.slice(),
|
|
1952
|
-
tables: this.#activeTableRenderSpecs?.slice() ?? [],
|
|
1953
1774
|
};
|
|
1954
1775
|
|
|
1955
|
-
// Settled exposure (hard-monotone): these rows are declared final to
|
|
1956
|
-
// the host, so expose them only while the frozen text still extends
|
|
1957
|
-
// the previously exposed prefix; a rewind resets to 0 and re-earns on
|
|
1958
|
-
// the rewritten lineage.
|
|
1959
|
-
if (contentLines.length > 0) {
|
|
1960
|
-
if (this.#settledExposedText === undefined || frozenText.startsWith(this.#settledExposedText)) {
|
|
1961
|
-
this.#settledExposedText = frozenText;
|
|
1962
|
-
this.#lastRenderSettledRows = signature.paddingY + contentLines.length;
|
|
1963
|
-
} else {
|
|
1964
|
-
this.#settledExposedText = undefined;
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
1776
|
if (renderedUntil < tokens.length) {
|
|
1969
|
-
contentLines.push(
|
|
1970
|
-
...this.#renderContentLines(
|
|
1971
|
-
tokens,
|
|
1972
|
-
renderedUntil,
|
|
1973
|
-
tokens.length,
|
|
1974
|
-
contentWidth,
|
|
1975
|
-
signature,
|
|
1976
|
-
contentLines.length,
|
|
1977
|
-
frozenText.length,
|
|
1978
|
-
),
|
|
1979
|
-
);
|
|
1777
|
+
contentLines.push(...this.#renderContentLines(tokens, renderedUntil, tokens.length, contentWidth, signature));
|
|
1980
1778
|
}
|
|
1981
1779
|
|
|
1982
1780
|
return contentLines;
|
|
@@ -1984,12 +1782,12 @@ export class Markdown
|
|
|
1984
1782
|
|
|
1985
1783
|
#matchingStreamPrefixLineCache(
|
|
1986
1784
|
normalizedText: string,
|
|
1987
|
-
|
|
1785
|
+
stableText: string,
|
|
1988
1786
|
signature: RenderSignature,
|
|
1989
1787
|
): StreamPrefixLineCache | undefined {
|
|
1990
1788
|
const cache = this.#streamPrefixLineCache;
|
|
1991
1789
|
if (!cache) return undefined;
|
|
1992
|
-
if (!normalizedText.startsWith(cache.text) || !
|
|
1790
|
+
if (!normalizedText.startsWith(cache.text) || !stableText.startsWith(cache.text)) return undefined;
|
|
1993
1791
|
if (cache.width !== signature.width) return undefined;
|
|
1994
1792
|
if (cache.paddingX !== signature.paddingX) return undefined;
|
|
1995
1793
|
if (cache.paddingY !== signature.paddingY) return undefined;
|
|
@@ -2010,25 +1808,12 @@ export class Markdown
|
|
|
2010
1808
|
end: number,
|
|
2011
1809
|
contentWidth: number,
|
|
2012
1810
|
signature: RenderSignature,
|
|
2013
|
-
rowOffset: number,
|
|
2014
|
-
startingSourceOffset: number,
|
|
2015
1811
|
): string[] {
|
|
2016
1812
|
const wrappedLines: RenderedLine[] = [];
|
|
2017
|
-
let sourceOffset = startingSourceOffset;
|
|
2018
1813
|
for (let i = start; i < end; i++) {
|
|
2019
1814
|
const token = tokens[i];
|
|
2020
1815
|
const nextToken = tokens[i + 1];
|
|
2021
|
-
const
|
|
2022
|
-
const tokenWrappedRowStart = wrappedLines.length;
|
|
2023
|
-
const tokenRowStart = rowOffset + tokenWrappedRowStart;
|
|
2024
|
-
const renderedTokenLines = this.#renderToken(
|
|
2025
|
-
token,
|
|
2026
|
-
contentWidth,
|
|
2027
|
-
nextToken?.type,
|
|
2028
|
-
undefined,
|
|
2029
|
-
`offset:${sourceOffset}`,
|
|
2030
|
-
);
|
|
2031
|
-
const tokenLineOffsets = [0];
|
|
1816
|
+
const renderedTokenLines = this.#renderToken(token, contentWidth, nextToken?.type);
|
|
2032
1817
|
for (const renderedRow of renderedTokenLines) {
|
|
2033
1818
|
// Lists wrap while their structural prefixes are still available, so
|
|
2034
1819
|
// continuation rows retain the correct hanging indent. Re-wrapping the
|
|
@@ -2045,30 +1830,7 @@ export class Markdown
|
|
|
2045
1830
|
}
|
|
2046
1831
|
}
|
|
2047
1832
|
}
|
|
2048
|
-
tokenLineOffsets.push(wrappedLines.length - tokenWrappedRowStart);
|
|
2049
|
-
}
|
|
2050
|
-
const tableSpecs = this.#activeTableRenderSpecs;
|
|
2051
|
-
if (tableSpecs !== undefined) {
|
|
2052
|
-
for (let specIndex = tableSpecStart; specIndex < tableSpecs.length; specIndex++) {
|
|
2053
|
-
const spec = tableSpecs[specIndex]!;
|
|
2054
|
-
let relativeStart: number;
|
|
2055
|
-
let relativeEnd: number;
|
|
2056
|
-
if (token.type === "table") {
|
|
2057
|
-
// Exclude the optional inter-block blank from a top-level table's span.
|
|
2058
|
-
relativeStart = 0;
|
|
2059
|
-
relativeEnd = Math.min(renderedTokenLines.length, spec.lineCount);
|
|
2060
|
-
} else {
|
|
2061
|
-
// Container renderers express nested table spans relative to their
|
|
2062
|
-
// returned lines. Preserve that exact span through this final wrap.
|
|
2063
|
-
if (spec.startRow < 0 || spec.endRow <= spec.startRow) continue;
|
|
2064
|
-
relativeStart = Math.min(renderedTokenLines.length, spec.startRow);
|
|
2065
|
-
relativeEnd = Math.min(renderedTokenLines.length, spec.endRow);
|
|
2066
|
-
}
|
|
2067
|
-
spec.startRow = tokenRowStart + tokenLineOffsets[relativeStart]!;
|
|
2068
|
-
spec.endRow = tokenRowStart + tokenLineOffsets[relativeEnd]!;
|
|
2069
|
-
}
|
|
2070
1833
|
}
|
|
2071
|
-
sourceOffset += token.raw.length;
|
|
2072
1834
|
}
|
|
2073
1835
|
|
|
2074
1836
|
const leftMargin = padding(signature.paddingX);
|
|
@@ -2117,37 +1879,20 @@ export class Markdown
|
|
|
2117
1879
|
return contentLines;
|
|
2118
1880
|
}
|
|
2119
1881
|
|
|
2120
|
-
#resolveRenderedTableLayouts(specs: readonly TableRenderSpec[], topPadding: number): RenderedTableLayout[] {
|
|
2121
|
-
const layouts: RenderedTableLayout[] = [];
|
|
2122
|
-
for (const spec of specs) {
|
|
2123
|
-
if (spec.startRow < 0 || spec.endRow <= spec.startRow) continue;
|
|
2124
|
-
layouts.push({
|
|
2125
|
-
key: spec.key,
|
|
2126
|
-
availableWidth: spec.availableWidth,
|
|
2127
|
-
columnWidths: spec.columnWidths.slice(),
|
|
2128
|
-
startRow: topPadding + spec.startRow,
|
|
2129
|
-
endRow: topPadding + spec.endRow,
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
return layouts;
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
1882
|
#renderCodeBodyLines(token: Token, codeIndent: string): RenderedLine[] {
|
|
2136
1883
|
const literalCode = this.#codeBlockIndent === 0;
|
|
2137
1884
|
const bodyLines: RenderedLine[] = [];
|
|
2138
1885
|
const tokenText = "text" in token && typeof token.text === "string" ? token.text : "";
|
|
2139
1886
|
const lang = "lang" in token && typeof token.lang === "string" ? token.lang : undefined;
|
|
2140
|
-
const normalizedLang = lang?.toLowerCase();
|
|
2141
|
-
const canStreamDiff =
|
|
2142
|
-
this.transientRenderCache &&
|
|
2143
|
-
!this.#renderingFrozenPrefix &&
|
|
2144
|
-
this.#theme.highlightCode &&
|
|
2145
|
-
(normalizedLang === "diff" || normalizedLang === "patch" || normalizedLang === "udiff");
|
|
2146
1887
|
const addBodyLine = (line: string): void => {
|
|
2147
1888
|
bodyLines.push(renderedLine(literalCode ? line : codeIndent + line, literalCode));
|
|
2148
1889
|
};
|
|
2149
1890
|
|
|
2150
|
-
|
|
1891
|
+
const streaming = this.transientRenderCache && !this.#renderingStablePrefix;
|
|
1892
|
+
if (this.#theme.highlightCode && (!streaming || this.#codeTokenHasClosingFence(token))) {
|
|
1893
|
+
// Finalized content — or a fence that closed mid-stream, which
|
|
1894
|
+
// highlights through the same whole-block call the finalized render
|
|
1895
|
+
// uses so cached stable rows byte-match it.
|
|
2151
1896
|
const highlightedLines = this.#theme.highlightCode(tokenText, lang);
|
|
2152
1897
|
for (const hlLine of highlightedLines) {
|
|
2153
1898
|
addBodyLine(hlLine);
|
|
@@ -2155,18 +1900,18 @@ export class Markdown
|
|
|
2155
1900
|
return bodyLines;
|
|
2156
1901
|
}
|
|
2157
1902
|
|
|
2158
|
-
if (
|
|
2159
|
-
|
|
1903
|
+
if (streaming && this.#theme.highlightCode) {
|
|
1904
|
+
// Open fence: highlight completed lines incrementally so semantic
|
|
1905
|
+
// colors reach completed rows immediately; only the trailing partial
|
|
1906
|
+
// line stays unhighlighted.
|
|
2160
1907
|
const lineEnd = tokenText.lastIndexOf("\n");
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
for (const hlLine of
|
|
1908
|
+
const completedLines = lineEnd >= 0 ? this.#highlightStreamingLines(tokenText.slice(0, lineEnd), lang) : null;
|
|
1909
|
+
if (completedLines) {
|
|
1910
|
+
for (const hlLine of completedLines) {
|
|
2164
1911
|
addBodyLine(hlLine);
|
|
2165
1912
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
addBodyLine(this.#theme.codeBlock(codeLine));
|
|
2169
|
-
}
|
|
1913
|
+
for (const codeLine of tokenText.slice(lineEnd + 1).split("\n")) {
|
|
1914
|
+
addBodyLine(this.#theme.codeBlock(codeLine));
|
|
2170
1915
|
}
|
|
2171
1916
|
return bodyLines;
|
|
2172
1917
|
}
|
|
@@ -2209,11 +1954,17 @@ export class Markdown
|
|
|
2209
1954
|
return false;
|
|
2210
1955
|
}
|
|
2211
1956
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
1957
|
+
/**
|
|
1958
|
+
* Highlight the completed (newline-terminated) prefix of a streaming code
|
|
1959
|
+
* fence. Uses a stateful per-fence highlight stream so each render pushes
|
|
1960
|
+
* only the newly completed lines, with output byte-identical to the
|
|
1961
|
+
* whole-block `highlightCode` the finalized render performs. Returns null
|
|
1962
|
+
* when no stream is available for `lang` (caller falls back to plain
|
|
1963
|
+
* code-block styling).
|
|
1964
|
+
*/
|
|
1965
|
+
#highlightStreamingLines(completedText: string, lang: string | undefined): readonly string[] | null {
|
|
2215
1966
|
const signature = this.#activeRenderSignature;
|
|
2216
|
-
const cache = this.#
|
|
1967
|
+
const cache = this.#streamingHighlightCache;
|
|
2217
1968
|
if (
|
|
2218
1969
|
signature &&
|
|
2219
1970
|
cache &&
|
|
@@ -2233,25 +1984,49 @@ export class Markdown
|
|
|
2233
1984
|
cache.headingProbe === signature.headingProbe
|
|
2234
1985
|
) {
|
|
2235
1986
|
if (completedText.length === cache.text.length) return cache.lines;
|
|
2236
|
-
|
|
1987
|
+
// Invariant: the stream has consumed `cache.text + "\n"`, so pushing
|
|
1988
|
+
// the added lines with a trailing newline advances it to
|
|
1989
|
+
// `completedText + "\n"` — every fed line stays newline-terminated.
|
|
2237
1990
|
const addedText = completedText.slice(cache.text.length + 1);
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
}
|
|
2241
|
-
this.#streamingDiffLineCache = { ...signature, lang, text: completedText, lines };
|
|
1991
|
+
const lines = cache.lines.concat(splitPushedHighlightLines(cache.stream.push(`${addedText}\n`)));
|
|
1992
|
+
this.#streamingHighlightCache = { ...signature, lang, text: completedText, lines, stream: cache.stream };
|
|
2242
1993
|
return lines;
|
|
2243
1994
|
}
|
|
2244
1995
|
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}
|
|
1996
|
+
const stream = this.#createHighlightStream(lang);
|
|
1997
|
+
if (!stream) return null;
|
|
1998
|
+
const lines = splitPushedHighlightLines(stream.push(`${completedText}\n`));
|
|
2249
1999
|
if (signature) {
|
|
2250
|
-
this.#
|
|
2000
|
+
this.#streamingHighlightCache = { ...signature, lang, text: completedText, lines, stream };
|
|
2251
2001
|
}
|
|
2252
2002
|
return lines;
|
|
2253
2003
|
}
|
|
2254
2004
|
|
|
2005
|
+
/**
|
|
2006
|
+
* Resolve a highlight stream for `lang`. Prefers the theme's stateful
|
|
2007
|
+
* factory; without one, emulates a stream via per-line `highlightCode` for
|
|
2008
|
+
* diff-family fences only — that grammar is line-local, so per-line output
|
|
2009
|
+
* matches the whole-block render (other grammars carry cross-line state
|
|
2010
|
+
* and would diverge).
|
|
2011
|
+
*/
|
|
2012
|
+
#createHighlightStream(lang: string | undefined): HighlightStreamSession | null {
|
|
2013
|
+
const factory = this.#theme.createHighlightStream;
|
|
2014
|
+
if (factory) return factory(lang);
|
|
2015
|
+
const highlightCode = this.#theme.highlightCode;
|
|
2016
|
+
if (!highlightCode) return null;
|
|
2017
|
+
const normalizedLang = lang?.toLowerCase();
|
|
2018
|
+
if (normalizedLang !== "diff" && normalizedLang !== "patch" && normalizedLang !== "udiff") return null;
|
|
2019
|
+
return {
|
|
2020
|
+
push: chunk => {
|
|
2021
|
+
const lines = chunk.split("\n");
|
|
2022
|
+
const trailing = lines.pop() ?? "";
|
|
2023
|
+
let out = "";
|
|
2024
|
+
for (const line of lines) out += `${highlightCode(line, lang).join("\n")}\n`;
|
|
2025
|
+
return trailing ? out + highlightCode(trailing, lang).join("\n") : out;
|
|
2026
|
+
},
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2255
2030
|
#renderEmptyPaddingLines(signature: RenderSignature): string[] {
|
|
2256
2031
|
const emptyLine = padding(signature.width);
|
|
2257
2032
|
const emptyLines: string[] = [];
|
|
@@ -2351,7 +2126,6 @@ export class Markdown
|
|
|
2351
2126
|
width: number,
|
|
2352
2127
|
nextTokenType?: string,
|
|
2353
2128
|
styleContext?: InlineStyleContext,
|
|
2354
|
-
tokenKey = "root",
|
|
2355
2129
|
): RenderedLine[] {
|
|
2356
2130
|
const lines: RenderedLine[] = [];
|
|
2357
2131
|
|
|
@@ -2461,7 +2235,7 @@ export class Markdown
|
|
|
2461
2235
|
}
|
|
2462
2236
|
|
|
2463
2237
|
case "table": {
|
|
2464
|
-
const tableLines = this.#renderTable(token as TableToken, width, nextTokenType, styleContext
|
|
2238
|
+
const tableLines = this.#renderTable(token as TableToken, width, nextTokenType, styleContext);
|
|
2465
2239
|
for (const tableLine of tableLines) lines.push(renderedLine(tableLine));
|
|
2466
2240
|
break;
|
|
2467
2241
|
}
|
|
@@ -2474,58 +2248,23 @@ export class Markdown
|
|
|
2474
2248
|
const quoteContentWidth = Math.max(1, width - 2);
|
|
2475
2249
|
const quoteTokens = token.tokens || [];
|
|
2476
2250
|
const renderedQuoteLines: RenderedLine[] = [];
|
|
2477
|
-
const blockquoteSpecStart = this.#activeTableRenderSpecs?.length ?? 0;
|
|
2478
|
-
|
|
2479
2251
|
for (let i = 0; i < quoteTokens.length; i++) {
|
|
2480
2252
|
const quoteToken = quoteTokens[i];
|
|
2481
2253
|
const nextQuoteToken = quoteTokens[i + 1];
|
|
2482
|
-
const quoteTokenRowStart = renderedQuoteLines.length;
|
|
2483
|
-
const quoteSpecStart = this.#activeTableRenderSpecs?.length ?? 0;
|
|
2484
2254
|
const quoteTokenLines = this.#renderToken(
|
|
2485
2255
|
quoteToken,
|
|
2486
2256
|
quoteContentWidth,
|
|
2487
2257
|
nextQuoteToken?.type,
|
|
2488
2258
|
quoteInlineStyleContext,
|
|
2489
|
-
`${tokenKey}/quote:${i}`,
|
|
2490
2259
|
);
|
|
2491
2260
|
renderedQuoteLines.push(...quoteTokenLines);
|
|
2492
|
-
|
|
2493
|
-
const tableSpecs = this.#activeTableRenderSpecs;
|
|
2494
|
-
if (tableSpecs !== undefined) {
|
|
2495
|
-
for (let specIndex = quoteSpecStart; specIndex < tableSpecs.length; specIndex++) {
|
|
2496
|
-
const spec = tableSpecs[specIndex]!;
|
|
2497
|
-
if (spec.startRow < 0) {
|
|
2498
|
-
// Direct child tables initially have no row coordinates. Their
|
|
2499
|
-
// structural line count excludes any inter-block blank.
|
|
2500
|
-
spec.startRow = quoteTokenRowStart;
|
|
2501
|
-
spec.endRow = quoteTokenRowStart + Math.min(quoteTokenLines.length, spec.lineCount);
|
|
2502
|
-
} else {
|
|
2503
|
-
// A nested blockquote already mapped the table into its own
|
|
2504
|
-
// returned rows; translate those rows into this quote's input.
|
|
2505
|
-
spec.startRow += quoteTokenRowStart;
|
|
2506
|
-
spec.endRow += quoteTokenRowStart;
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
2261
|
}
|
|
2511
2262
|
|
|
2512
2263
|
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1]!.text === "") {
|
|
2513
2264
|
renderedQuoteLines.pop();
|
|
2514
2265
|
}
|
|
2515
2266
|
|
|
2516
|
-
const
|
|
2517
|
-
const borderedQuoteLines = this.#applyQuoteBorder(renderedQuoteLines, width, quoteRowOffsets);
|
|
2518
|
-
const tableSpecs = this.#activeTableRenderSpecs;
|
|
2519
|
-
if (tableSpecs !== undefined) {
|
|
2520
|
-
for (let specIndex = blockquoteSpecStart; specIndex < tableSpecs.length; specIndex++) {
|
|
2521
|
-
const spec = tableSpecs[specIndex]!;
|
|
2522
|
-
if (spec.startRow < 0 || spec.endRow <= spec.startRow) continue;
|
|
2523
|
-
const relativeStart = Math.min(renderedQuoteLines.length, spec.startRow);
|
|
2524
|
-
const relativeEnd = Math.min(renderedQuoteLines.length, spec.endRow);
|
|
2525
|
-
spec.startRow = quoteRowOffsets[relativeStart]!;
|
|
2526
|
-
spec.endRow = quoteRowOffsets[relativeEnd]!;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2267
|
+
const borderedQuoteLines = this.#applyQuoteBorder(renderedQuoteLines, width);
|
|
2529
2268
|
lines.push(...borderedQuoteLines);
|
|
2530
2269
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2531
2270
|
lines.push(renderedLine("")); // Add spacing after blockquotes (unless space token follows)
|
|
@@ -2573,7 +2312,7 @@ export class Markdown
|
|
|
2573
2312
|
* Wrap already-rendered lines in the blockquote border and quote styling.
|
|
2574
2313
|
* `width` is the full content width; the border reserves two cells.
|
|
2575
2314
|
*/
|
|
2576
|
-
#applyQuoteBorder(renderedLines: RenderedLine[], width: number
|
|
2315
|
+
#applyQuoteBorder(renderedLines: RenderedLine[], width: number): RenderedLine[] {
|
|
2577
2316
|
const quoteStyle = (text: string) => this.#theme.quote(this.#theme.italic(text));
|
|
2578
2317
|
const quoteStylePrefix = this.#getStylePrefix(quoteStyle);
|
|
2579
2318
|
const applyQuoteStyle = (line: string): string => {
|
|
@@ -2585,7 +2324,6 @@ export class Markdown
|
|
|
2585
2324
|
};
|
|
2586
2325
|
const quoteContentWidth = Math.max(1, width - 2);
|
|
2587
2326
|
const lines: RenderedLine[] = [];
|
|
2588
|
-
sourceRowOffsets?.push(0);
|
|
2589
2327
|
for (const quoteLine of renderedLines) {
|
|
2590
2328
|
if (quoteLine.literalCode) {
|
|
2591
2329
|
const wrappedLiteralRows = wrapTextWithAnsi(quoteLine.text, quoteContentWidth);
|
|
@@ -2602,7 +2340,6 @@ export class Markdown
|
|
|
2602
2340
|
lines.push(renderedLine(this.#theme.quoteBorder(`${this.#theme.symbols.quoteBorder} `) + wrappedLine));
|
|
2603
2341
|
}
|
|
2604
2342
|
}
|
|
2605
|
-
sourceRowOffsets?.push(lines.length);
|
|
2606
2343
|
}
|
|
2607
2344
|
return lines;
|
|
2608
2345
|
}
|
|
@@ -2976,7 +2713,6 @@ export class Markdown
|
|
|
2976
2713
|
availableWidth: number,
|
|
2977
2714
|
nextTokenType?: string,
|
|
2978
2715
|
styleContext?: InlineStyleContext,
|
|
2979
|
-
tableKey = "table",
|
|
2980
2716
|
): string[] {
|
|
2981
2717
|
const lines: string[] = [];
|
|
2982
2718
|
const numCols = token.header.length;
|
|
@@ -3091,17 +2827,6 @@ export class Markdown
|
|
|
3091
2827
|
}
|
|
3092
2828
|
}
|
|
3093
2829
|
|
|
3094
|
-
const lockedLayout = this.#lockedTableLayouts.get(tableKey);
|
|
3095
|
-
if (
|
|
3096
|
-
lockedLayout !== undefined &&
|
|
3097
|
-
lockedLayout.availableWidth === availableWidth &&
|
|
3098
|
-
lockedLayout.columnWidths.length === numCols &&
|
|
3099
|
-
lockedLayout.columnWidths.every(width => Number.isFinite(width) && width >= 1) &&
|
|
3100
|
-
lockedLayout.columnWidths.reduce((total, width) => total + width, borderOverhead) <= availableWidth
|
|
3101
|
-
) {
|
|
3102
|
-
columnWidths = lockedLayout.columnWidths.slice();
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
2830
|
const t = this.#theme.symbols.table;
|
|
3106
2831
|
const h = t.horizontal;
|
|
3107
2832
|
const v = t.vertical;
|
|
@@ -3157,15 +2882,6 @@ export class Markdown
|
|
|
3157
2882
|
const bottomBorderCells = columnWidths.map(w => h.repeat(w));
|
|
3158
2883
|
const bottomBorder = `${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`;
|
|
3159
2884
|
lines.push(bottomBorder);
|
|
3160
|
-
this.#activeTableRenderSpecs?.push({
|
|
3161
|
-
key: tableKey,
|
|
3162
|
-
availableWidth,
|
|
3163
|
-
columnWidths: columnWidths.slice(),
|
|
3164
|
-
lineCount: lines.length,
|
|
3165
|
-
startRow: -1,
|
|
3166
|
-
endRow: -1,
|
|
3167
|
-
});
|
|
3168
|
-
|
|
3169
2885
|
if (nextTokenType && nextTokenType !== "space") {
|
|
3170
2886
|
lines.push(""); // Add spacing after table
|
|
3171
2887
|
}
|