@zenodinh/pi-render 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/core/paint.ts +35 -4
- package/src/core/types/host.ts +4 -0
- package/src/core/types/paint.ts +14 -0
- package/src/renderers/content/table.ts +37 -13
package/package.json
CHANGED
package/src/core/paint.ts
CHANGED
|
@@ -48,7 +48,6 @@ function tryStyle(fn: ((text: string) => string) | undefined, text: string): str
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
52
51
|
/** Region 1 — the tool-row factory. Reads `theme` live, so the next call paints the current theme. */
|
|
53
52
|
// shape: closure returning an object literal — trigger #4, eight stateless row roles over the captured theme.
|
|
54
53
|
export function createRowPaint(theme: HostTheme): RowPaint {
|
|
@@ -67,13 +66,45 @@ export function createRowPaint(theme: HostTheme): RowPaint {
|
|
|
67
66
|
};
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/**
|
|
70
|
+
* How the header band fills its cell: `"reverse"` swaps the cell's ink and background (SGR reverse
|
|
71
|
+
* video), `"none"` leaves the bare header. Picked once at construction, so a side-by-side can sweep it.
|
|
72
|
+
*/
|
|
73
|
+
export type BandStyle = "reverse" | "none";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* SGR reverse video, on and off. The band is the ONE escape pair that originates here rather than in a
|
|
77
|
+
* theme: no background token is reachable on the content seam, so the band is built from reverse video,
|
|
78
|
+
* which needs none and therefore survives a restore that happens before any theme is known.
|
|
79
|
+
*/
|
|
80
|
+
const REVERSE_ON = "\x1b[7m";
|
|
81
|
+
const REVERSE_OFF = "\x1b[27m";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Region 2 — the content factory. `rule` degrades hr → quoteBorder → plain; every role ends in plain.
|
|
85
|
+
*
|
|
86
|
+
* The markdown theme's own closures supply the emphasis ink: `bold`/`italic` for inline spans, and its
|
|
87
|
+
* `code` closure (the `mdCode` token, identical to `accent` in both stock themes) for the header ink.
|
|
88
|
+
* `bandStyle` picks the header band — `"reverse"` (the default) composes reverse video over the header,
|
|
89
|
+
* `"none"` leaves it bare — so the one band reachable on this seam is a construction-time choice.
|
|
90
|
+
*/
|
|
91
|
+
// shape: closure returning an object literal — trigger #4, eight stateless content roles over the
|
|
92
|
+
// captured markdown theme and the band style.
|
|
93
|
+
export function createContentPaint(mdTheme: MarkdownTheme, bandStyle: BandStyle = "reverse"): ContentPaint {
|
|
94
|
+
// The header ink: the theme's `code` closure under its `bold` closure. Each link is optional and
|
|
95
|
+
// degrades on its own, so a theme missing one still stamps whatever the other gives.
|
|
96
|
+
const header = (text: string): string => {
|
|
97
|
+
const inked = tryStyle(mdTheme.code, text) ?? text;
|
|
98
|
+
return tryStyle(mdTheme.bold, inked) ?? inked;
|
|
99
|
+
};
|
|
73
100
|
return {
|
|
74
101
|
rule: (text) => tryStyle(mdTheme.hr, text) ?? tryStyle(mdTheme.quoteBorder, text) ?? text,
|
|
75
102
|
quote: (text) => tryStyle(mdTheme.quote, text) ?? text,
|
|
76
103
|
code: (text) => tryStyle(mdTheme.code, text) ?? text,
|
|
77
104
|
codeBlockBorder: (text) => tryStyle(mdTheme.codeBlockBorder, text) ?? text,
|
|
105
|
+
strong: (text) => tryStyle(mdTheme.bold, text) ?? text,
|
|
106
|
+
em: (text) => tryStyle(mdTheme.italic, text) ?? text,
|
|
107
|
+
header,
|
|
108
|
+
headerBand: (text) => (bandStyle === "reverse" ? `${REVERSE_ON}${header(text)}${REVERSE_OFF}` : header(text)),
|
|
78
109
|
};
|
|
79
110
|
}
|
package/src/core/types/host.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface MarkdownTheme {
|
|
|
32
32
|
code?(text: string): string;
|
|
33
33
|
/** Fenced-code-block border wrapper. Optional. */
|
|
34
34
|
codeBlockBorder?(text: string): string;
|
|
35
|
+
/** Bold wrapper for `**strong**` spans — the host markdown theme's own `bold` closure. Optional. */
|
|
36
|
+
bold?(text: string): string;
|
|
37
|
+
/** Italic wrapper for `*em*` spans — the host markdown theme's own `italic` closure. Optional. */
|
|
38
|
+
italic?(text: string): string;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** The pi-tui component slice renderers return and receive. Structural host shape — no host import. */
|
package/src/core/types/paint.ts
CHANGED
|
@@ -51,4 +51,18 @@ export interface ContentPaint {
|
|
|
51
51
|
code(text: string): string;
|
|
52
52
|
/** Fenced-code-block border. Required. */
|
|
53
53
|
codeBlockBorder(text: string): string;
|
|
54
|
+
/** A `**strong**` inline span, drawn in the theme's bold. Required. */
|
|
55
|
+
strong(text: string): string;
|
|
56
|
+
/** An `*em*` inline span, drawn in the theme's italic. Required. */
|
|
57
|
+
em(text: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* One stamped header cell: the accent ink under bold. The ink is the markdown theme's `code` closure
|
|
60
|
+
* (`mdCode`, identical to `accent` in both stock themes) and the weight its `bold` closure. Required.
|
|
61
|
+
*/
|
|
62
|
+
header(text: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* One header-band cell: {@link header} carrying the band style — reverse video by default, so the
|
|
65
|
+
* cell's own ink becomes its background. Required.
|
|
66
|
+
*/
|
|
67
|
+
headerBand(text: string): string;
|
|
54
68
|
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* table.ts — the table surface: a markdown table becomes a banded box.
|
|
9
9
|
*
|
|
10
|
+
* The header row is stamped through the header-band role — bold over the accent ink inside a band — or,
|
|
11
|
+
* when the header row is empty (`| | |`), that stamp moves onto the first column so no blank band is
|
|
12
|
+
* spent. `**strong**` and `*em*` cells route through their own roles.
|
|
13
|
+
*
|
|
10
14
|
* Boundary: lane-local. The transformer passes markdown in and splices the returned markdown back
|
|
11
15
|
* out, so every escape in the output originates in the injected ContentPaint (T-08) — the #22 fix.
|
|
12
16
|
*
|
|
@@ -60,15 +64,22 @@ function isTableToken(token: Token): token is Token & TableToken {
|
|
|
60
64
|
return Array.isArray(token.rows) && token.rows.every((row) => Array.isArray(row) && row.every(isCell));
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/** The inline text a token wraps, already styled by its own children; `token.text` is the fallback. */
|
|
68
|
+
function innerText(token: InlineToken, paint: ContentPaint): string {
|
|
69
|
+
return (token.tokens ?? []).map((child) => inlineText(child, paint)).join("") || (token.text ?? "");
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
/** Inline styling is a ContentPaint role or nothing — the host tokenizer's own `codespan` is code. */
|
|
64
73
|
function inlineText(token: InlineToken, paint: ContentPaint): string {
|
|
65
74
|
switch (token.type) {
|
|
66
75
|
case "codespan":
|
|
67
76
|
return paint.code(token.text ?? "");
|
|
68
77
|
case "strong":
|
|
78
|
+
return paint.strong(innerText(token, paint));
|
|
69
79
|
case "em":
|
|
80
|
+
return paint.em(innerText(token, paint));
|
|
70
81
|
case "link":
|
|
71
|
-
return (token
|
|
82
|
+
return innerText(token, paint);
|
|
72
83
|
default:
|
|
73
84
|
return token.text ?? token.raw ?? "";
|
|
74
85
|
}
|
|
@@ -108,6 +119,10 @@ function renderTable(token: TableToken, width: number, paint: ContentPaint): str
|
|
|
108
119
|
widths[widest] = (widths[widest] ?? 0) - 1;
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
// An all-empty header (`| | |`) means the table carries its labels in the first column: the blank
|
|
123
|
+
// header row and its separator are dropped, and the band moves onto that first column instead.
|
|
124
|
+
const keyColumn = header.every((cell) => cell === "");
|
|
125
|
+
|
|
111
126
|
// A light box around and between everything: the frame is what keeps a wrapped cell visually
|
|
112
127
|
// inside the table instead of trailing off its right edge.
|
|
113
128
|
const ruleLine = (left: string, middle: string, right: string): string =>
|
|
@@ -115,24 +130,33 @@ function renderTable(token: TableToken, width: number, paint: ContentPaint): str
|
|
|
115
130
|
const bar = paint.rule("│");
|
|
116
131
|
const physical: string[] = [];
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
/** One logical row, wrapped into physical lines; `banded` picks the cells drawn through headerBand. */
|
|
134
|
+
const renderRow = (cells: string[], banded: (index: number) => boolean): void => {
|
|
135
|
+
const wrapped = cells.map((cell, index) => wrapTextWithAnsi(cell, widths[index] ?? MIN_COLUMN));
|
|
136
|
+
const height = Math.max(1, ...wrapped.map((lines) => lines.length));
|
|
120
137
|
for (let line = 0; line < height; line++) {
|
|
121
|
-
const parts =
|
|
138
|
+
const parts = wrapped.map((lines, index) => {
|
|
139
|
+
const padded = padCell(lines[line] ?? "", widths[index] ?? MIN_COLUMN);
|
|
140
|
+
return banded(index) ? ` ${paint.headerBand(padded)} ` : ` ${padded} `;
|
|
141
|
+
});
|
|
122
142
|
physical.push(`${bar}${parts.join(bar)}${bar}`);
|
|
123
143
|
}
|
|
124
144
|
};
|
|
125
145
|
|
|
126
|
-
const wrapBand = (cells: string[]): string[][] =>
|
|
127
|
-
cells.map((cell, index) => wrapTextWithAnsi(cell, widths[index] ?? MIN_COLUMN));
|
|
128
|
-
|
|
129
146
|
physical.push(ruleLine("┌", "┬", "┐"));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
147
|
+
if (keyColumn) {
|
|
148
|
+
rows.forEach((row, index) => {
|
|
149
|
+
if (index > 0) physical.push(ruleLine("├", "┼", "┤"));
|
|
150
|
+
renderRow(row, (index) => index === 0);
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
renderRow(header, () => true);
|
|
154
|
+
physical.push(ruleLine("├", "┼", "┤"));
|
|
155
|
+
rows.forEach((row, index) => {
|
|
156
|
+
if (index > 0) physical.push(ruleLine("├", "┼", "┤"));
|
|
157
|
+
renderRow(row, () => false);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
136
160
|
physical.push(ruleLine("└", "┴", "┘"));
|
|
137
161
|
return physical.map((line) => (visibleWidth(line) > width ? truncateToWidth(line, width) : line));
|
|
138
162
|
}
|