tinky 1.5.0 → 1.7.0
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/README.ja-JP.md +40 -0
- package/README.md +39 -0
- package/README.zh-CN.md +38 -0
- package/lib/components/Text.js +10 -3
- package/lib/core/cell-buffer.d.ts +91 -0
- package/lib/core/cell-buffer.js +346 -0
- package/lib/core/cell-log-update-run.d.ts +40 -0
- package/lib/core/cell-log-update-run.js +526 -0
- package/lib/core/cell-output.d.ts +20 -0
- package/lib/core/cell-output.js +231 -0
- package/lib/core/cell-renderer.d.ts +25 -0
- package/lib/core/cell-renderer.js +60 -0
- package/lib/core/incremental-rendering.d.ts +43 -0
- package/lib/core/incremental-rendering.js +22 -0
- package/lib/core/output.d.ts +21 -6
- package/lib/core/output.js +73 -8
- package/lib/core/render-border.d.ts +2 -12
- package/lib/core/render-border.js +78 -69
- package/lib/core/render-node-to-output.d.ts +2 -2
- package/lib/core/render.d.ts +17 -4
- package/lib/core/renderer.js +9 -6
- package/lib/core/tinky.d.ts +27 -3
- package/lib/core/tinky.js +125 -5
- package/lib/index.d.ts +1 -0
- package/lib/types/ansi.d.ts +5 -0
- package/lib/types/ansi.js +1 -0
- package/lib/utils/ansi-escapes.d.ts +2 -0
- package/lib/utils/ansi-escapes.js +2 -0
- package/lib/utils/colorize.d.ts +5 -0
- package/lib/utils/colorize.js +42 -20
- package/lib/utils/render-background.d.ts +2 -2
- package/lib/utils/render-background.js +4 -3
- package/lib/utils/squash-text-nodes.d.ts +5 -0
- package/lib/utils/squash-text-nodes.js +18 -2
- package/package.json +3 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output implementation that writes directly into CellBuffer.
|
|
3
|
+
*/
|
|
4
|
+
import sliceAnsi from "slice-ansi";
|
|
5
|
+
import stringWidth from "string-width";
|
|
6
|
+
import widestLine from "widest-line";
|
|
7
|
+
import { styledCharsFromTokens, tokenize } from "@alcalzone/ansi-tokenize";
|
|
8
|
+
const ESC = "\u001b";
|
|
9
|
+
const isPrintableAsciiText = (value) => {
|
|
10
|
+
for (let index = 0; index < value.length; index++) {
|
|
11
|
+
const code = value.charCodeAt(index);
|
|
12
|
+
if (code === 10) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (code < 0x20 || code > 0x7e) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
const getCharWidth = (value, widthCache) => {
|
|
22
|
+
if (value.length === 1 &&
|
|
23
|
+
value.charCodeAt(0) >= 0x20 &&
|
|
24
|
+
value.charCodeAt(0) <= 0x7e) {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const cached = widthCache.get(value);
|
|
28
|
+
if (cached !== undefined) {
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
const width = Math.max(1, stringWidth(value));
|
|
32
|
+
widthCache.set(value, width);
|
|
33
|
+
return width;
|
|
34
|
+
};
|
|
35
|
+
export class CellOutput {
|
|
36
|
+
clips = [];
|
|
37
|
+
styleIdByRef = new WeakMap();
|
|
38
|
+
lastStyleRef;
|
|
39
|
+
lastStyleId = 0;
|
|
40
|
+
buffer;
|
|
41
|
+
constructor(buffer) {
|
|
42
|
+
this.buffer = buffer;
|
|
43
|
+
}
|
|
44
|
+
clip(clip) {
|
|
45
|
+
this.clips.push(clip);
|
|
46
|
+
}
|
|
47
|
+
unclip() {
|
|
48
|
+
this.clips.pop();
|
|
49
|
+
}
|
|
50
|
+
fill(x, y, length, char, charWidth, styles) {
|
|
51
|
+
if (length <= 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const visualLength = length * charWidth;
|
|
55
|
+
let startX = x;
|
|
56
|
+
let endX = x + visualLength;
|
|
57
|
+
const row = y;
|
|
58
|
+
const clip = this.clips.at(-1);
|
|
59
|
+
if (clip) {
|
|
60
|
+
const clipY1 = clip.y1 ?? 0;
|
|
61
|
+
// Clipping logic: y2 is exclusive boundary
|
|
62
|
+
if (clip.y2 !== undefined && row >= clip.y2) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (row < clipY1) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const clipX1 = clip.x1 ?? 0;
|
|
69
|
+
startX = Math.max(startX, clipX1);
|
|
70
|
+
if (clip.x2 !== undefined) {
|
|
71
|
+
endX = Math.min(endX, clip.x2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const fillVisualLength = endX - startX;
|
|
75
|
+
if (fillVisualLength <= 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const styleId = this.buffer.styleRegistry.getId(styles);
|
|
79
|
+
this.buffer.fill(startX, row, fillVisualLength, char, charWidth, styleId);
|
|
80
|
+
}
|
|
81
|
+
resolveStyleId(styles) {
|
|
82
|
+
if (styles.length === 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (styles === this.lastStyleRef) {
|
|
86
|
+
return this.lastStyleId;
|
|
87
|
+
}
|
|
88
|
+
const byRef = this.styleIdByRef.get(styles);
|
|
89
|
+
if (byRef !== undefined) {
|
|
90
|
+
this.lastStyleRef = styles;
|
|
91
|
+
this.lastStyleId = byRef;
|
|
92
|
+
return byRef;
|
|
93
|
+
}
|
|
94
|
+
const styleId = this.buffer.styleRegistry.getId(styles);
|
|
95
|
+
this.styleIdByRef.set(styles, styleId);
|
|
96
|
+
this.lastStyleRef = styles;
|
|
97
|
+
this.lastStyleId = styleId;
|
|
98
|
+
return styleId;
|
|
99
|
+
}
|
|
100
|
+
writePlainLine(line, writeX, row, preserveLineTrailingSpaces, widthCache) {
|
|
101
|
+
let offsetX = writeX;
|
|
102
|
+
for (const value of line) {
|
|
103
|
+
const charWidth = getCharWidth(value, widthCache);
|
|
104
|
+
if (offsetX < 0) {
|
|
105
|
+
offsetX += charWidth;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (offsetX >= this.buffer.width) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (offsetX + charWidth > this.buffer.width) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
this.buffer.setCell(offsetX, row, value, charWidth, 0, preserveLineTrailingSpaces);
|
|
115
|
+
offsetX += charWidth;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
write(x, y, text, options) {
|
|
119
|
+
const { transformers } = options;
|
|
120
|
+
if (!text) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let writeX = x;
|
|
124
|
+
let writeY = y;
|
|
125
|
+
let lines = text.split("\n");
|
|
126
|
+
// Match Output.get() semantics: transformed lines may intentionally keep
|
|
127
|
+
// trailing spaces (for alignment or visual effects), so mark them.
|
|
128
|
+
const preserveLineTrailingSpaces = transformers.length > 0;
|
|
129
|
+
const widthCache = new Map();
|
|
130
|
+
const clip = this.clips.at(-1);
|
|
131
|
+
if (!clip &&
|
|
132
|
+
transformers.length === 0 &&
|
|
133
|
+
!text.includes(ESC) &&
|
|
134
|
+
isPrintableAsciiText(text)) {
|
|
135
|
+
let offsetY = 0;
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const row = writeY + offsetY;
|
|
138
|
+
if (row < 0) {
|
|
139
|
+
offsetY++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (row >= this.buffer.height) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
this.writePlainLine(line, writeX, row, preserveLineTrailingSpaces, widthCache);
|
|
146
|
+
offsetY++;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (clip) {
|
|
151
|
+
const clipHorizontally = typeof clip.x1 === "number" && typeof clip.x2 === "number";
|
|
152
|
+
const clipVertically = typeof clip.y1 === "number" && typeof clip.y2 === "number";
|
|
153
|
+
const clipX1 = clip.x1 ?? 0;
|
|
154
|
+
const clipX2 = clip.x2 ?? 0;
|
|
155
|
+
const clipY1 = clip.y1 ?? 0;
|
|
156
|
+
const clipY2 = clip.y2 ?? 0;
|
|
157
|
+
if (clipHorizontally) {
|
|
158
|
+
const width = widestLine(text);
|
|
159
|
+
if (writeX + width < clipX1 || writeX > clipX2) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (clipVertically) {
|
|
164
|
+
const height = lines.length;
|
|
165
|
+
if (writeY + height < clipY1 || writeY > clipY2) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (clipHorizontally) {
|
|
170
|
+
lines = lines.map((line) => {
|
|
171
|
+
const from = writeX < clipX1 ? clipX1 - writeX : 0;
|
|
172
|
+
const width = stringWidth(line);
|
|
173
|
+
const to = writeX + width > clipX2 ? clipX2 - writeX : width;
|
|
174
|
+
return sliceAnsi(line, from, to);
|
|
175
|
+
});
|
|
176
|
+
if (writeX < clipX1) {
|
|
177
|
+
writeX = clipX1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (clipVertically) {
|
|
181
|
+
const from = writeY < clipY1 ? clipY1 - writeY : 0;
|
|
182
|
+
const height = lines.length;
|
|
183
|
+
const to = writeY + height > clipY2 ? clipY2 - writeY : height;
|
|
184
|
+
lines = lines.slice(from, to);
|
|
185
|
+
if (writeY < clipY1) {
|
|
186
|
+
writeY = clipY1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let offsetY = 0;
|
|
191
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
192
|
+
const row = writeY + offsetY;
|
|
193
|
+
if (row < 0) {
|
|
194
|
+
offsetY++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (row >= this.buffer.height) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
let transformedLine = line;
|
|
201
|
+
for (const transformer of transformers) {
|
|
202
|
+
transformedLine = transformer(transformedLine, lineIndex);
|
|
203
|
+
}
|
|
204
|
+
if (!transformedLine.includes(ESC) &&
|
|
205
|
+
isPrintableAsciiText(transformedLine)) {
|
|
206
|
+
this.writePlainLine(transformedLine, writeX, row, preserveLineTrailingSpaces, widthCache);
|
|
207
|
+
offsetY++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const styledChars = styledCharsFromTokens(tokenize(transformedLine));
|
|
211
|
+
let offsetX = writeX;
|
|
212
|
+
for (const char of styledChars) {
|
|
213
|
+
const charWidth = getCharWidth(char.value, widthCache);
|
|
214
|
+
if (offsetX < 0) {
|
|
215
|
+
offsetX += charWidth;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (offsetX >= this.buffer.width) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (offsetX + charWidth > this.buffer.width) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
const styleId = this.resolveStyleId(char.styles);
|
|
225
|
+
this.buffer.setCell(offsetX, row, char.value, charWidth, styleId, preserveLineTrailingSpaces);
|
|
226
|
+
offsetX += charWidth;
|
|
227
|
+
}
|
|
228
|
+
offsetY++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DOMElement } from "./dom.js";
|
|
2
|
+
import { type CellBuffer } from "./cell-buffer.js";
|
|
3
|
+
interface CellRendererResult {
|
|
4
|
+
/** Interactive frame rendered into `buffer`. */
|
|
5
|
+
buffer: CellBuffer;
|
|
6
|
+
/** Height (rows) of the interactive frame. */
|
|
7
|
+
outputHeight: number;
|
|
8
|
+
/** Newly appended `<Static>` output, with trailing newline when present. */
|
|
9
|
+
staticOutput: string;
|
|
10
|
+
/** Screen-reader text when screen-reader mode is enabled. */
|
|
11
|
+
screenReaderOutput?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Renderer that writes interactive output into a CellBuffer.
|
|
15
|
+
*
|
|
16
|
+
* Static output is still rendered as string output because it is emitted once
|
|
17
|
+
* and not updated incrementally.
|
|
18
|
+
*
|
|
19
|
+
* @param node - Root DOM node to render.
|
|
20
|
+
* @param buffer - Reusable destination buffer for the interactive frame.
|
|
21
|
+
* @param isScreenReaderEnabled - Enables screen-reader rendering path.
|
|
22
|
+
* @returns The rendered interactive frame and any static output emitted this pass.
|
|
23
|
+
*/
|
|
24
|
+
export declare const cellRenderer: (node: DOMElement, buffer: CellBuffer, isScreenReaderEnabled: boolean) => CellRendererResult;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { renderNodeToOutput, renderNodeToScreenReaderOutput, } from "./render-node-to-output.js";
|
|
2
|
+
import { Output } from "./output.js";
|
|
3
|
+
import { CellOutput } from "./cell-output.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renderer that writes interactive output into a CellBuffer.
|
|
6
|
+
*
|
|
7
|
+
* Static output is still rendered as string output because it is emitted once
|
|
8
|
+
* and not updated incrementally.
|
|
9
|
+
*
|
|
10
|
+
* @param node - Root DOM node to render.
|
|
11
|
+
* @param buffer - Reusable destination buffer for the interactive frame.
|
|
12
|
+
* @param isScreenReaderEnabled - Enables screen-reader rendering path.
|
|
13
|
+
* @returns The rendered interactive frame and any static output emitted this pass.
|
|
14
|
+
*/
|
|
15
|
+
export const cellRenderer = (node, buffer, isScreenReaderEnabled) => {
|
|
16
|
+
if (!node.taffyNode) {
|
|
17
|
+
buffer.resize(0, 0);
|
|
18
|
+
buffer.clear();
|
|
19
|
+
return { buffer, outputHeight: 0, staticOutput: "" };
|
|
20
|
+
}
|
|
21
|
+
if (isScreenReaderEnabled) {
|
|
22
|
+
const output = renderNodeToScreenReaderOutput(node, {
|
|
23
|
+
skipStaticElements: true,
|
|
24
|
+
});
|
|
25
|
+
const outputHeight = output === "" ? 0 : output.split("\n").length;
|
|
26
|
+
let staticOutput = "";
|
|
27
|
+
if (node.staticNode) {
|
|
28
|
+
staticOutput = renderNodeToScreenReaderOutput(node.staticNode, {
|
|
29
|
+
skipStaticElements: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
buffer,
|
|
34
|
+
outputHeight,
|
|
35
|
+
staticOutput: staticOutput ? `${staticOutput}\n` : "",
|
|
36
|
+
screenReaderOutput: output,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
40
|
+
buffer.resize(layout.width, layout.height);
|
|
41
|
+
buffer.clear();
|
|
42
|
+
const output = new CellOutput(buffer);
|
|
43
|
+
renderNodeToOutput(node, output, { skipStaticElements: true });
|
|
44
|
+
let staticOutput;
|
|
45
|
+
if (node.staticNode?.taffyNode) {
|
|
46
|
+
const staticLayout = node.staticNode.taffyNode.tree.getLayout(node.staticNode.taffyNode.id);
|
|
47
|
+
staticOutput = new Output({
|
|
48
|
+
width: staticLayout.width,
|
|
49
|
+
height: staticLayout.height,
|
|
50
|
+
});
|
|
51
|
+
renderNodeToOutput(node.staticNode, staticOutput, {
|
|
52
|
+
skipStaticElements: false,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
buffer,
|
|
57
|
+
outputHeight: buffer.height,
|
|
58
|
+
staticOutput: staticOutput ? `${staticOutput.get().output}\n` : "",
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing configuration for incremental rendering.
|
|
3
|
+
*
|
|
4
|
+
* You can use this object form when you need to choose a strategy explicitly
|
|
5
|
+
* or disable incremental rendering without changing existing option shapes.
|
|
6
|
+
*/
|
|
7
|
+
export interface IncrementalRenderingConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Master on/off switch for object mode.
|
|
10
|
+
*
|
|
11
|
+
* - `false`: disables incremental rendering, regardless of `strategy`.
|
|
12
|
+
* - `true` or omitted: enables incremental rendering and uses `strategy`.
|
|
13
|
+
*/
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Diff strategy used for interactive output updates.
|
|
17
|
+
*
|
|
18
|
+
* - `"line"`: compares line strings and redraws changed lines.
|
|
19
|
+
* - `"run"`: compares terminal cells and rewrites minimal changed runs.
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue "run"
|
|
22
|
+
*/
|
|
23
|
+
strategy?: "line" | "run";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Public option accepted by `render(..., { incrementalRendering })`.
|
|
27
|
+
*/
|
|
28
|
+
export type IncrementalRenderingOption = boolean | IncrementalRenderingConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Internal normalized strategy used inside the renderer.
|
|
31
|
+
*/
|
|
32
|
+
export type IncrementalRenderStrategy = "none" | "line" | "run";
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes user-facing incremental rendering options to an internal strategy.
|
|
35
|
+
*
|
|
36
|
+
* Resolution order:
|
|
37
|
+
* - `undefined` or `false` -> `"none"`
|
|
38
|
+
* - `true` -> `"run"`
|
|
39
|
+
* - `{ enabled: false }` -> `"none"`
|
|
40
|
+
* - `{ strategy: "line" }` -> `"line"`
|
|
41
|
+
* - `{ strategy: "run" }` or omitted strategy -> `"run"`
|
|
42
|
+
*/
|
|
43
|
+
export declare const normalizeIncrementalRendering: (value: IncrementalRenderingOption | undefined) => IncrementalRenderStrategy;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes user-facing incremental rendering options to an internal strategy.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* - `undefined` or `false` -> `"none"`
|
|
6
|
+
* - `true` -> `"run"`
|
|
7
|
+
* - `{ enabled: false }` -> `"none"`
|
|
8
|
+
* - `{ strategy: "line" }` -> `"line"`
|
|
9
|
+
* - `{ strategy: "run" }` or omitted strategy -> `"run"`
|
|
10
|
+
*/
|
|
11
|
+
export const normalizeIncrementalRendering = (value) => {
|
|
12
|
+
if (value === undefined || value === false) {
|
|
13
|
+
return "none";
|
|
14
|
+
}
|
|
15
|
+
if (value === true) {
|
|
16
|
+
return "run";
|
|
17
|
+
}
|
|
18
|
+
if (value.enabled === false) {
|
|
19
|
+
return "none";
|
|
20
|
+
}
|
|
21
|
+
return value.strategy ?? "run";
|
|
22
|
+
};
|
package/lib/core/output.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
2
2
|
import { type Dimension } from "../utils/dimension.js";
|
|
3
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
4
|
+
/**
|
|
5
|
+
* Options for writing text to an output target.
|
|
6
|
+
*/
|
|
7
|
+
export interface OutputWriteOptions {
|
|
8
|
+
/** Array of transformers to apply to each rendered line. */
|
|
9
|
+
transformers: OutputTransformer[];
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Represents a clipping rectangle.
|
|
5
13
|
*/
|
|
6
|
-
interface Clip {
|
|
14
|
+
export interface Clip {
|
|
7
15
|
/** Left boundary (undefined for no limit). */
|
|
8
16
|
x1: number | undefined;
|
|
9
17
|
/** Right boundary (undefined for no limit). */
|
|
@@ -13,6 +21,15 @@ interface Clip {
|
|
|
13
21
|
/** Bottom boundary (undefined for no limit). */
|
|
14
22
|
y2: number | undefined;
|
|
15
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Shared write/clip/unclip surface used by render traversal.
|
|
26
|
+
*/
|
|
27
|
+
export interface OutputLike {
|
|
28
|
+
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
29
|
+
clip(clip: Clip): void;
|
|
30
|
+
unclip(): void;
|
|
31
|
+
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
32
|
+
}
|
|
16
33
|
/**
|
|
17
34
|
* "Virtual" output class
|
|
18
35
|
*
|
|
@@ -21,7 +38,7 @@ interface Clip {
|
|
|
21
38
|
*
|
|
22
39
|
* Used to generate the final output of all nodes before writing to stdout.
|
|
23
40
|
*/
|
|
24
|
-
export declare class Output {
|
|
41
|
+
export declare class Output implements OutputLike {
|
|
25
42
|
width: number;
|
|
26
43
|
height: number;
|
|
27
44
|
private readonly operations;
|
|
@@ -39,9 +56,7 @@ export declare class Output {
|
|
|
39
56
|
* @param text - The text to write.
|
|
40
57
|
* @param options - Options containing transformers.
|
|
41
58
|
*/
|
|
42
|
-
write(x: number, y: number, text: string, options:
|
|
43
|
-
transformers: OutputTransformer[];
|
|
44
|
-
}): void;
|
|
59
|
+
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
45
60
|
/**
|
|
46
61
|
* Sets a clipping area for subsequent operations.
|
|
47
62
|
*
|
|
@@ -52,6 +67,7 @@ export declare class Output {
|
|
|
52
67
|
* Removes the last clipping area, restoring the previous one.
|
|
53
68
|
*/
|
|
54
69
|
unclip(): void;
|
|
70
|
+
fill(x: number, y: number, length: number, char: string, _charWidth: number, styles: AnsiCode[]): void;
|
|
55
71
|
/**
|
|
56
72
|
* Generates the final output string and its height.
|
|
57
73
|
*
|
|
@@ -62,4 +78,3 @@ export declare class Output {
|
|
|
62
78
|
height: number;
|
|
63
79
|
};
|
|
64
80
|
}
|
|
65
|
-
export {};
|
package/lib/core/output.js
CHANGED
|
@@ -33,7 +33,6 @@ export class Output {
|
|
|
33
33
|
* @param options - Options containing transformers.
|
|
34
34
|
*/
|
|
35
35
|
write(x, y, text, options) {
|
|
36
|
-
const { transformers } = options;
|
|
37
36
|
if (!text) {
|
|
38
37
|
return;
|
|
39
38
|
}
|
|
@@ -42,7 +41,7 @@ export class Output {
|
|
|
42
41
|
x,
|
|
43
42
|
y,
|
|
44
43
|
text,
|
|
45
|
-
|
|
44
|
+
options,
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
@@ -64,6 +63,18 @@ export class Output {
|
|
|
64
63
|
type: "unclip",
|
|
65
64
|
});
|
|
66
65
|
}
|
|
66
|
+
fill(x, y, length, char, _charWidth, styles) {
|
|
67
|
+
if (length <= 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const prefix = styles.map((style) => style.code).join("");
|
|
71
|
+
const suffix = [...styles]
|
|
72
|
+
.reverse()
|
|
73
|
+
.map((style) => style.endCode)
|
|
74
|
+
.join("");
|
|
75
|
+
const text = prefix + char.repeat(length) + suffix;
|
|
76
|
+
this.write(x, y, text, { transformers: [] });
|
|
77
|
+
}
|
|
67
78
|
/**
|
|
68
79
|
* Generates the final output string and its height.
|
|
69
80
|
*
|
|
@@ -73,6 +84,10 @@ export class Output {
|
|
|
73
84
|
// Initialize output array with a specific set of rows, so that
|
|
74
85
|
// margin/padding at the bottom is preserved
|
|
75
86
|
const output = [];
|
|
87
|
+
// Tracks whether a cell was explicitly written this frame.
|
|
88
|
+
const touched = [];
|
|
89
|
+
// Preserves intentional trailing spaces produced by transformers.
|
|
90
|
+
const preserveTrailingSpace = [];
|
|
76
91
|
for (let y = 0; y < this.height; y++) {
|
|
77
92
|
const row = [];
|
|
78
93
|
for (let x = 0; x < this.width; x++) {
|
|
@@ -83,7 +98,11 @@ export class Output {
|
|
|
83
98
|
styles: [],
|
|
84
99
|
});
|
|
85
100
|
}
|
|
101
|
+
const rowTouched = new Array(this.width).fill(false);
|
|
102
|
+
const rowPreserve = new Array(this.width).fill(false);
|
|
86
103
|
output.push(row);
|
|
104
|
+
touched.push(rowTouched);
|
|
105
|
+
preserveTrailingSpace.push(rowPreserve);
|
|
87
106
|
}
|
|
88
107
|
const clips = [];
|
|
89
108
|
for (const operation of this.operations) {
|
|
@@ -94,9 +113,11 @@ export class Output {
|
|
|
94
113
|
clips.pop();
|
|
95
114
|
}
|
|
96
115
|
if (operation.type === "write") {
|
|
97
|
-
const { text
|
|
116
|
+
const { text } = operation;
|
|
117
|
+
const { transformers } = operation.options;
|
|
98
118
|
let { x, y } = operation;
|
|
99
119
|
let lines = text.split("\n");
|
|
120
|
+
const preserveLineTrailingSpaces = transformers.length > 0;
|
|
100
121
|
const clip = clips.at(-1);
|
|
101
122
|
if (clip) {
|
|
102
123
|
const clipHorizontally = typeof clip.x1 === "number" && typeof clip.x2 === "number";
|
|
@@ -142,10 +163,13 @@ export class Output {
|
|
|
142
163
|
}
|
|
143
164
|
let offsetY = 0;
|
|
144
165
|
for (const [index, line] of lines.entries()) {
|
|
145
|
-
const
|
|
166
|
+
const rowIndex = y + offsetY;
|
|
167
|
+
const currentLine = output[rowIndex];
|
|
168
|
+
const touchedRow = touched[rowIndex];
|
|
169
|
+
const preserveRow = preserveTrailingSpace[rowIndex];
|
|
146
170
|
// Line can be missing if `text` is taller than height of
|
|
147
171
|
// pre-initialized `this.output`
|
|
148
|
-
if (!currentLine) {
|
|
172
|
+
if (!currentLine || !touchedRow || !preserveRow) {
|
|
149
173
|
continue;
|
|
150
174
|
}
|
|
151
175
|
let transformedLine = line;
|
|
@@ -156,6 +180,8 @@ export class Output {
|
|
|
156
180
|
let offsetX = x;
|
|
157
181
|
for (const character of characters) {
|
|
158
182
|
currentLine[offsetX] = character;
|
|
183
|
+
touchedRow[offsetX] = true;
|
|
184
|
+
preserveRow[offsetX] = preserveLineTrailingSpaces;
|
|
159
185
|
// Determine printed width using string-width to align with measurement
|
|
160
186
|
const characterWidth = Math.max(1, stringWidth(character.value));
|
|
161
187
|
// For multi-column characters, clear following cells to avoid stray
|
|
@@ -168,6 +194,8 @@ export class Output {
|
|
|
168
194
|
fullWidth: false,
|
|
169
195
|
styles: character.styles,
|
|
170
196
|
};
|
|
197
|
+
touchedRow[offsetX + index] = true;
|
|
198
|
+
preserveRow[offsetX + index] = preserveLineTrailingSpaces;
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
offsetX += characterWidth;
|
|
@@ -177,9 +205,46 @@ export class Output {
|
|
|
177
205
|
}
|
|
178
206
|
}
|
|
179
207
|
const generatedOutput = output
|
|
180
|
-
.map((line) => {
|
|
181
|
-
const
|
|
182
|
-
|
|
208
|
+
.map((line, rowIndex) => {
|
|
209
|
+
const rowTouched = touched[rowIndex] ?? [];
|
|
210
|
+
const rowPreserve = preserveTrailingSpace[rowIndex] ?? [];
|
|
211
|
+
const compactLine = [];
|
|
212
|
+
const compactTouched = [];
|
|
213
|
+
const compactPreserve = [];
|
|
214
|
+
for (let index = 0; index < line.length; index++) {
|
|
215
|
+
const item = line[index];
|
|
216
|
+
if (item === undefined) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
compactLine.push(item);
|
|
220
|
+
compactTouched.push(rowTouched[index] === true);
|
|
221
|
+
compactPreserve.push(rowPreserve[index] === true);
|
|
222
|
+
}
|
|
223
|
+
let end = compactLine.length - 1;
|
|
224
|
+
while (end >= 0) {
|
|
225
|
+
const item = compactLine[end];
|
|
226
|
+
if (!item) {
|
|
227
|
+
end--;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!compactTouched[end]) {
|
|
231
|
+
end--;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const canTrimUnstyledSpace = item.type === "char" &&
|
|
235
|
+
item.value === " " &&
|
|
236
|
+
item.styles.length === 0 &&
|
|
237
|
+
!compactPreserve[end];
|
|
238
|
+
if (canTrimUnstyledSpace) {
|
|
239
|
+
end--;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (end < 0) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
return styledCharsToString(compactLine.slice(0, end + 1));
|
|
183
248
|
})
|
|
184
249
|
.join("\n");
|
|
185
250
|
return {
|
|
@@ -1,13 +1,3 @@
|
|
|
1
1
|
import { type DOMNode } from "./dom.js";
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
* Renders the border for a DOM node.
|
|
5
|
-
* Calculates border dimensions and draws border characters with specified
|
|
6
|
-
* styles.
|
|
7
|
-
*
|
|
8
|
-
* @param x - The x-coordinate (column) where the border starts.
|
|
9
|
-
* @param y - The y-coordinate (row) where the border starts.
|
|
10
|
-
* @param node - The DOM node for which to render the border.
|
|
11
|
-
* @param output - The output instance to write the border to.
|
|
12
|
-
*/
|
|
13
|
-
export declare const renderBorder: (x: number, y: number, node: DOMNode, output: Output) => void;
|
|
2
|
+
import { type OutputLike } from "./output.js";
|
|
3
|
+
export declare const renderBorder: (x: number, y: number, node: DOMNode, output: OutputLike) => void;
|