@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6
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.md +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-aZr9O8h3.js +665 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
import { widestLine } from "#/ansi/string-width.ts";
|
|
2
1
|
import { type DOMElement } from "#/dom.ts";
|
|
3
2
|
import { getMaxWidth } from "#/get-max-width.ts";
|
|
4
|
-
import type { Output } from "#/output.ts";
|
|
5
3
|
import { renderBackground } from "#/render-background.ts";
|
|
6
4
|
import { renderBorder } from "#/render-border.ts";
|
|
5
|
+
import type { Canvas } from "#/screen/canvas.ts";
|
|
6
|
+
import { serializeLine } from "#/screen/serialize.ts";
|
|
7
7
|
import { squashTextNodes } from "#/squash-text-nodes.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
hasCompatibilityText,
|
|
10
|
+
structuredTextBaseStyle,
|
|
11
|
+
structuredTextLines,
|
|
12
|
+
wrapStructuredText,
|
|
13
|
+
sampleStructuredText,
|
|
14
|
+
} from "#/structured-text.ts";
|
|
15
|
+
import type { AnsiTransformer } from "#/transform-adapter.ts";
|
|
9
16
|
import { Yoga } from "#/yoga/index.ts";
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
// the tree and will have their own coordinates in the layout.
|
|
13
|
-
// To ensure text nodes are aligned correctly, take X and Y of the first text node
|
|
14
|
-
// and use it as offset for the rest of the nodes
|
|
15
|
-
// Only first node is taken into account, because other text nodes can't have margin or padding,
|
|
16
|
-
// so their coordinates will be relative to the first node anyway
|
|
17
|
-
const applyPaddingToText = (node: DOMElement, text: string): string => {
|
|
18
|
-
const yogaNode = node.childNodes[0]?.yogaNode;
|
|
19
|
-
|
|
20
|
-
if (yogaNode) {
|
|
21
|
-
const offsetX = yogaNode.getComputedLeft();
|
|
22
|
-
const offsetY = yogaNode.getComputedTop();
|
|
23
|
-
|
|
24
|
-
text =
|
|
25
|
-
"\n".repeat(offsetY) +
|
|
26
|
-
(offsetX > 0 ? text.replace(/^(?!\s*$)/gm, " ".repeat(offsetX)) : text);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return text;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type OutputTransformer = (s: string, index: number) => string;
|
|
33
|
-
|
|
34
|
-
export const renderNodeToScreenReaderOutput = (
|
|
18
|
+
export const renderAccessibleText = (
|
|
35
19
|
node: DOMElement,
|
|
36
20
|
options: {
|
|
37
21
|
parentRole?: string;
|
|
@@ -61,7 +45,7 @@ export const renderNodeToScreenReaderOutput = (
|
|
|
61
45
|
|
|
62
46
|
output = childNodes
|
|
63
47
|
.map((childNode) => {
|
|
64
|
-
const screenReaderOutput =
|
|
48
|
+
const screenReaderOutput = renderAccessibleText(childNode as DOMElement, {
|
|
65
49
|
parentRole: node.internal_accessibility?.role,
|
|
66
50
|
skipStaticElements: options.skipStaticElements,
|
|
67
51
|
});
|
|
@@ -92,13 +76,13 @@ export const renderNodeToScreenReaderOutput = (
|
|
|
92
76
|
};
|
|
93
77
|
|
|
94
78
|
// After nodes are laid out, render each to output object, which later gets rendered to terminal
|
|
95
|
-
export const
|
|
79
|
+
export const paintTree = (
|
|
96
80
|
node: DOMElement,
|
|
97
|
-
output:
|
|
81
|
+
output: Canvas,
|
|
98
82
|
options: {
|
|
99
83
|
offsetX?: number;
|
|
100
84
|
offsetY?: number;
|
|
101
|
-
transformers?:
|
|
85
|
+
transformers?: AnsiTransformer[];
|
|
102
86
|
skipStaticElements: boolean;
|
|
103
87
|
},
|
|
104
88
|
) => {
|
|
@@ -120,28 +104,58 @@ export const renderNodeToOutput = (
|
|
|
120
104
|
const y = offsetY + yogaNode.getComputedTop();
|
|
121
105
|
|
|
122
106
|
// Transformers are functions that transform final text output of each component
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
newTransformers = [node.internal_transform, ...transformers];
|
|
128
|
-
}
|
|
107
|
+
// The canvas applies explicit compatibility transformers at this boundary.
|
|
108
|
+
const newTransformers = node.internal_transform
|
|
109
|
+
? [node.internal_transform, ...transformers]
|
|
110
|
+
: transformers;
|
|
129
111
|
|
|
130
112
|
if (node.nodeName === "ink-text") {
|
|
131
|
-
|
|
113
|
+
const text = squashTextNodes(node);
|
|
132
114
|
|
|
133
115
|
if (text.length > 0) {
|
|
134
|
-
const currentWidth = widestLine(text);
|
|
135
116
|
const maxWidth = getMaxWidth(yogaNode);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
117
|
+
const firstChildYoga = node.childNodes[0]?.yogaNode;
|
|
118
|
+
const paddingX = firstChildYoga?.getComputedLeft() ?? 0;
|
|
119
|
+
const paddingY = firstChildYoga?.getComputedTop() ?? 0;
|
|
120
|
+
const textWrap = node.style.textWrap ?? "wrap";
|
|
121
|
+
// `none` text may paint past the screen's nominal width; the screen
|
|
122
|
+
// grows the touched rows on demand. Clip rects still apply.
|
|
123
|
+
const overflow = textWrap === "none";
|
|
124
|
+
const lines = wrapStructuredText(
|
|
125
|
+
structuredTextLines(node),
|
|
126
|
+
maxWidth,
|
|
127
|
+
textWrap,
|
|
128
|
+
structuredTextBaseStyle(node),
|
|
129
|
+
);
|
|
130
|
+
const paintBounds = {
|
|
131
|
+
x: x + paddingX,
|
|
132
|
+
y: y + paddingY,
|
|
133
|
+
width: Math.max(
|
|
134
|
+
1,
|
|
135
|
+
...lines.map((line) => line.reduce((width, cell) => width + cell.width, 0)),
|
|
136
|
+
),
|
|
137
|
+
height: lines.length,
|
|
138
|
+
};
|
|
139
|
+
const sampled = sampleStructuredText(lines, paintBounds, output.paintContext);
|
|
140
|
+
|
|
141
|
+
if (hasCompatibilityText(node)) {
|
|
142
|
+
const textTransformers = collectTransformers(node, transformers);
|
|
143
|
+
output.writeAnsi(
|
|
144
|
+
paintBounds.x,
|
|
145
|
+
paintBounds.y,
|
|
146
|
+
sampled
|
|
147
|
+
.map((line) =>
|
|
148
|
+
serializeLine(line, {
|
|
149
|
+
colorProfile: output.paintContext.profile ?? "truecolor",
|
|
150
|
+
trimEnd: false,
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
.join("\n"),
|
|
154
|
+
{ transformers: textTransformers, overflow },
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
output.writeCells(paintBounds.x, paintBounds.y, sampled, { overflow });
|
|
140
158
|
}
|
|
141
|
-
|
|
142
|
-
text = applyPaddingToText(node, text);
|
|
143
|
-
|
|
144
|
-
output.write(x, y, text, { transformers: newTransformers });
|
|
145
159
|
}
|
|
146
160
|
|
|
147
161
|
return;
|
|
@@ -177,7 +191,7 @@ export const renderNodeToOutput = (
|
|
|
177
191
|
|
|
178
192
|
if (node.nodeName === "ink-root" || node.nodeName === "ink-box") {
|
|
179
193
|
for (const childNode of node.childNodes) {
|
|
180
|
-
|
|
194
|
+
paintTree(childNode as DOMElement, output, {
|
|
181
195
|
offsetX: x,
|
|
182
196
|
offsetY: y,
|
|
183
197
|
transformers: newTransformers,
|
|
@@ -191,3 +205,16 @@ export const renderNodeToOutput = (
|
|
|
191
205
|
}
|
|
192
206
|
}
|
|
193
207
|
};
|
|
208
|
+
|
|
209
|
+
function collectTransformers(
|
|
210
|
+
node: DOMElement,
|
|
211
|
+
inherited: readonly AnsiTransformer[],
|
|
212
|
+
): AnsiTransformer[] {
|
|
213
|
+
return [
|
|
214
|
+
...node.childNodes.flatMap((child) =>
|
|
215
|
+
child.nodeName === "#text" ? [] : collectTransformers(child, []),
|
|
216
|
+
),
|
|
217
|
+
...(node.internal_transform ? [node.internal_transform] : []),
|
|
218
|
+
...inherited,
|
|
219
|
+
];
|
|
220
|
+
}
|
package/src/reconciler.ts
CHANGED
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
type DOMElement,
|
|
21
21
|
} from "#/dom.ts";
|
|
22
22
|
import { isSigilDev } from "#/env.ts";
|
|
23
|
-
import {
|
|
23
|
+
import type { SemanticTextStyle } from "#/semantic-text-style.ts";
|
|
24
24
|
import { styles as applyStyles, type Styles } from "#/styles.ts";
|
|
25
|
+
import { type AnsiTransformer } from "#/transform-adapter.ts";
|
|
25
26
|
import { Yoga } from "#/yoga/index.ts";
|
|
26
27
|
|
|
27
28
|
import pkg from "../package.json" with { type: "json" };
|
|
@@ -226,7 +227,17 @@ export const reconciler = createReconciler<
|
|
|
226
227
|
|
|
227
228
|
if (key === "internal_transform") {
|
|
228
229
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
229
|
-
node.internal_transform = value as
|
|
230
|
+
node.internal_transform = value as AnsiTransformer;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (key === "internal_ansi") {
|
|
235
|
+
node.internal_ansi = value === true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (key === "internal_textStyle") {
|
|
240
|
+
node.internal_textStyle = value as SemanticTextStyle;
|
|
230
241
|
continue;
|
|
231
242
|
}
|
|
232
243
|
|
|
@@ -331,7 +342,17 @@ export const reconciler = createReconciler<
|
|
|
331
342
|
|
|
332
343
|
if (key === "internal_transform") {
|
|
333
344
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
334
|
-
node.internal_transform = value as
|
|
345
|
+
node.internal_transform = value as AnsiTransformer;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (key === "internal_ansi") {
|
|
350
|
+
node.internal_ansi = value === true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (key === "internal_textStyle") {
|
|
355
|
+
node.internal_textStyle = value as SemanticTextStyle | undefined;
|
|
335
356
|
continue;
|
|
336
357
|
}
|
|
337
358
|
|
package/src/render-background.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { samplePaint } from "#/color/sample.ts";
|
|
2
2
|
import { type DOMNode } from "#/dom.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Canvas } from "#/screen/canvas.ts";
|
|
4
|
+
import { cellAttributes, createCell } from "#/screen/index.ts";
|
|
4
5
|
|
|
5
|
-
export const renderBackground = (x: number, y: number, node: DOMNode, output:
|
|
6
|
-
if (
|
|
6
|
+
export const renderBackground = (x: number, y: number, node: DOMNode, output: Canvas): void => {
|
|
7
|
+
if (node.style.backgroundColor === undefined) {
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -23,16 +24,36 @@ export const renderBackground = (x: number, y: number, node: DOMNode, output: Ou
|
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const bounds = {
|
|
28
|
+
x: x + leftBorderWidth,
|
|
29
|
+
y: y + topBorderHeight,
|
|
30
|
+
width: contentWidth,
|
|
31
|
+
height: contentHeight,
|
|
32
|
+
};
|
|
33
|
+
const lines = Array.from({ length: contentHeight }, (_unused, row) =>
|
|
34
|
+
Array.from({ length: contentWidth }, (_empty, column) => {
|
|
35
|
+
const resetBackground = node.style.backgroundColor === "";
|
|
36
|
+
const background = resetBackground
|
|
37
|
+
? undefined
|
|
38
|
+
: samplePaint(
|
|
39
|
+
node.style.backgroundColor!,
|
|
40
|
+
bounds.x + column,
|
|
41
|
+
bounds.y + row,
|
|
42
|
+
bounds,
|
|
43
|
+
output.paintContext,
|
|
44
|
+
);
|
|
45
|
+
return createCell(
|
|
46
|
+
" ",
|
|
47
|
+
1,
|
|
48
|
+
{
|
|
49
|
+
...(background ? { background } : {}),
|
|
50
|
+
underline: "none",
|
|
51
|
+
attributes: cellAttributes.none,
|
|
52
|
+
},
|
|
53
|
+
undefined,
|
|
54
|
+
resetBackground ? { background: true } : undefined,
|
|
55
|
+
);
|
|
56
|
+
}),
|
|
31
57
|
);
|
|
32
|
-
|
|
33
|
-
for (let row = 0; row < contentHeight; row++) {
|
|
34
|
-
output.write(x + leftBorderWidth, y + topBorderHeight + row, backgroundLine, {
|
|
35
|
-
transformers: [],
|
|
36
|
-
});
|
|
37
|
-
}
|
|
58
|
+
output.writeCells(bounds.x, bounds.y, lines);
|
|
38
59
|
};
|
package/src/render-border.ts
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { graphemes } from "#/ansi/graphemes.ts";
|
|
2
|
+
import { stringWidth } from "#/ansi/string-width.ts";
|
|
3
|
+
import type { Paint, PaintContext } from "#/color/paint.ts";
|
|
4
|
+
import { samplePaint } from "#/color/sample.ts";
|
|
3
5
|
import { type DOMNode } from "#/dom.ts";
|
|
4
6
|
import { BOXES } from "#/glyphs.ts";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
import type { Canvas } from "#/screen/canvas.ts";
|
|
8
|
+
import type { Rect } from "#/screen/geometry.ts";
|
|
9
|
+
import { cellAttributes, createCell, type Cell } from "#/screen/index.ts";
|
|
10
|
+
|
|
11
|
+
const stylePiece = (
|
|
12
|
+
segment: string,
|
|
13
|
+
x: number,
|
|
14
|
+
y: number,
|
|
15
|
+
bounds: Rect,
|
|
16
|
+
context: PaintContext,
|
|
17
|
+
fg?: Paint,
|
|
18
|
+
bg?: Paint,
|
|
19
|
+
dim?: boolean,
|
|
20
|
+
): Cell[] => {
|
|
21
|
+
let column = x;
|
|
22
|
+
return [...graphemes(segment)].map((grapheme) => {
|
|
23
|
+
const foreground = fg ? samplePaint(fg, column, y, bounds, context) : undefined;
|
|
24
|
+
const background = bg ? samplePaint(bg, column, y, bounds, context) : undefined;
|
|
25
|
+
const width = Math.max(1, stringWidth(grapheme));
|
|
26
|
+
column += width;
|
|
27
|
+
return createCell(grapheme, width, {
|
|
28
|
+
...(foreground ? { foreground } : {}),
|
|
29
|
+
...(background ? { background } : {}),
|
|
30
|
+
underline: "none",
|
|
31
|
+
attributes: dim ? cellAttributes.faint : cellAttributes.none,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
15
34
|
};
|
|
16
35
|
|
|
17
|
-
export const renderBorder = (x: number, y: number, node: DOMNode, output:
|
|
36
|
+
export const renderBorder = (x: number, y: number, node: DOMNode, output: Canvas): void => {
|
|
18
37
|
if (node.style.borderStyle) {
|
|
19
38
|
const width = node.yogaNode!.getComputedWidth();
|
|
20
39
|
const height = node.yogaNode!.getComputedHeight();
|
|
40
|
+
const bounds = { x, y, width, height };
|
|
21
41
|
const box =
|
|
22
42
|
typeof node.style.borderStyle === "string"
|
|
23
43
|
? BOXES[node.style.borderStyle]
|
|
@@ -52,19 +72,12 @@ export const renderBorder = (x: number, y: number, node: DOMNode, output: Output
|
|
|
52
72
|
|
|
53
73
|
const contentWidth = width - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
const topBorder = showTopBorder
|
|
56
76
|
? (showLeftBorder ? box.topLeft : "") +
|
|
57
77
|
box.top.repeat(contentWidth) +
|
|
58
78
|
(showRightBorder ? box.topRight : "")
|
|
59
79
|
: undefined;
|
|
60
80
|
|
|
61
|
-
topBorder &&= stylePiece(
|
|
62
|
-
topBorder,
|
|
63
|
-
topBorderColor,
|
|
64
|
-
topBorderBackgroundColor,
|
|
65
|
-
dimTopBorderColor,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
81
|
let verticalBorderHeight = height;
|
|
69
82
|
|
|
70
83
|
if (showTopBorder) {
|
|
@@ -75,60 +88,80 @@ export const renderBorder = (x: number, y: number, node: DOMNode, output: Output
|
|
|
75
88
|
verticalBorderHeight -= 1;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (showLeftBorder) {
|
|
81
|
-
const one = stylePiece(
|
|
82
|
-
box.left,
|
|
83
|
-
leftBorderColor,
|
|
84
|
-
leftBorderBackgroundColor,
|
|
85
|
-
dimLeftBorderColor,
|
|
86
|
-
);
|
|
87
|
-
leftBorder = (one + "\n").repeat(verticalBorderHeight);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let rightBorder = "";
|
|
91
|
-
|
|
92
|
-
if (showRightBorder) {
|
|
93
|
-
const one = stylePiece(
|
|
94
|
-
box.right,
|
|
95
|
-
rightBorderColor,
|
|
96
|
-
rightBorderBackgroundColor,
|
|
97
|
-
dimRightBorderColor,
|
|
98
|
-
);
|
|
99
|
-
rightBorder = (one + "\n").repeat(verticalBorderHeight);
|
|
100
|
-
}
|
|
91
|
+
const offsetY = showTopBorder ? 1 : 0;
|
|
101
92
|
|
|
102
|
-
|
|
93
|
+
const bottomBorder = showBottomBorder
|
|
103
94
|
? (showLeftBorder ? box.bottomLeft : "") +
|
|
104
95
|
box.bottom.repeat(contentWidth) +
|
|
105
96
|
(showRightBorder ? box.bottomRight : "")
|
|
106
97
|
: undefined;
|
|
107
|
-
bottomBorder &&= stylePiece(
|
|
108
|
-
bottomBorder,
|
|
109
|
-
bottomBorderColor,
|
|
110
|
-
bottomBorderBackgroundColor,
|
|
111
|
-
dimBottomBorderColor,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const offsetY = showTopBorder ? 1 : 0;
|
|
115
98
|
|
|
116
99
|
if (topBorder) {
|
|
117
|
-
output.
|
|
100
|
+
output.writeCells(x, y, [
|
|
101
|
+
stylePiece(
|
|
102
|
+
topBorder,
|
|
103
|
+
x,
|
|
104
|
+
y,
|
|
105
|
+
bounds,
|
|
106
|
+
output.paintContext,
|
|
107
|
+
topBorderColor,
|
|
108
|
+
topBorderBackgroundColor,
|
|
109
|
+
dimTopBorderColor,
|
|
110
|
+
),
|
|
111
|
+
]);
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
if (
|
|
121
|
-
output.
|
|
114
|
+
if (showLeftBorder) {
|
|
115
|
+
output.writeCells(
|
|
116
|
+
x,
|
|
117
|
+
y + offsetY,
|
|
118
|
+
Array.from({ length: verticalBorderHeight }, (_, row) =>
|
|
119
|
+
stylePiece(
|
|
120
|
+
box.left,
|
|
121
|
+
x,
|
|
122
|
+
y + offsetY + row,
|
|
123
|
+
bounds,
|
|
124
|
+
output.paintContext,
|
|
125
|
+
leftBorderColor,
|
|
126
|
+
leftBorderBackgroundColor,
|
|
127
|
+
dimLeftBorderColor,
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
if (
|
|
125
|
-
output.
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
if (showRightBorder) {
|
|
134
|
+
output.writeCells(
|
|
135
|
+
x + width - 1,
|
|
136
|
+
y + offsetY,
|
|
137
|
+
Array.from({ length: verticalBorderHeight }, (_, row) =>
|
|
138
|
+
stylePiece(
|
|
139
|
+
box.right,
|
|
140
|
+
x + width - 1,
|
|
141
|
+
y + offsetY + row,
|
|
142
|
+
bounds,
|
|
143
|
+
output.paintContext,
|
|
144
|
+
rightBorderColor,
|
|
145
|
+
rightBorderBackgroundColor,
|
|
146
|
+
dimRightBorderColor,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
if (bottomBorder) {
|
|
131
|
-
output.
|
|
153
|
+
output.writeCells(x, y + height - 1, [
|
|
154
|
+
stylePiece(
|
|
155
|
+
bottomBorder,
|
|
156
|
+
x,
|
|
157
|
+
y + height - 1,
|
|
158
|
+
bounds,
|
|
159
|
+
output.paintContext,
|
|
160
|
+
bottomBorderColor,
|
|
161
|
+
bottomBorderBackgroundColor,
|
|
162
|
+
dimBottomBorderColor,
|
|
163
|
+
),
|
|
164
|
+
]);
|
|
132
165
|
}
|
|
133
166
|
}
|
|
134
167
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PaintContext } from "#/color/paint.ts";
|
|
2
|
+
import { type DOMElement } from "#/dom.ts";
|
|
3
|
+
import { paintTree, renderAccessibleText } from "#/paint-tree.ts";
|
|
4
|
+
import { Canvas } from "#/screen/canvas.ts";
|
|
5
|
+
import { type ColorProfile } from "#/screen/index.ts";
|
|
6
|
+
import type { Screen } from "#/screen/screen.ts";
|
|
7
|
+
|
|
8
|
+
type Result = {
|
|
9
|
+
screen?: Screen;
|
|
10
|
+
staticScreen?: Screen;
|
|
11
|
+
accessibleText?: string;
|
|
12
|
+
staticAccessibleText?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RendererOptions = {
|
|
16
|
+
colorProfile?: ColorProfile;
|
|
17
|
+
paintContext?: PaintContext;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const renderDimension = (value: number): number =>
|
|
21
|
+
Number.isFinite(value) ? Math.max(0, Math.ceil(value)) : 0;
|
|
22
|
+
|
|
23
|
+
export const renderFrame = (
|
|
24
|
+
node: DOMElement,
|
|
25
|
+
isScreenReaderEnabled: boolean,
|
|
26
|
+
options: RendererOptions = {},
|
|
27
|
+
): Result => {
|
|
28
|
+
if (node.yogaNode) {
|
|
29
|
+
if (isScreenReaderEnabled) {
|
|
30
|
+
const output = renderAccessibleText(node, {
|
|
31
|
+
skipStaticElements: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let staticOutput = "";
|
|
35
|
+
|
|
36
|
+
if (node.staticNode) {
|
|
37
|
+
staticOutput = renderAccessibleText(node.staticNode, {
|
|
38
|
+
skipStaticElements: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
accessibleText: output,
|
|
44
|
+
staticAccessibleText: staticOutput,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const output = new Canvas({
|
|
49
|
+
width: renderDimension(node.yogaNode.getComputedWidth()),
|
|
50
|
+
height: renderDimension(node.yogaNode.getComputedHeight()),
|
|
51
|
+
colorProfile: options.colorProfile,
|
|
52
|
+
paintContext: options.paintContext,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
paintTree(node, output, {
|
|
56
|
+
skipStaticElements: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let staticOutput;
|
|
60
|
+
|
|
61
|
+
if (node.staticNode?.yogaNode) {
|
|
62
|
+
staticOutput = new Canvas({
|
|
63
|
+
width: renderDimension(node.staticNode.yogaNode.getComputedWidth()),
|
|
64
|
+
height: renderDimension(node.staticNode.yogaNode.getComputedHeight()),
|
|
65
|
+
colorProfile: options.colorProfile,
|
|
66
|
+
paintContext: options.paintContext,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
paintTree(node.staticNode, staticOutput, {
|
|
70
|
+
skipStaticElements: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
screen: output.finish(),
|
|
76
|
+
...(staticOutput ? { staticScreen: staticOutput.finish() } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
accessibleText: "",
|
|
82
|
+
};
|
|
83
|
+
};
|
package/src/render-to-string.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { LegacyRoot } from "react-reconciler/constants.js";
|
|
3
3
|
|
|
4
|
+
import { detectColorLevel } from "#/capabilities/detect.ts";
|
|
4
5
|
import { createNode, type DOMElement } from "#/dom.ts";
|
|
5
6
|
import { reconciler } from "#/reconciler.ts";
|
|
6
|
-
import {
|
|
7
|
+
import { renderFrame } from "#/render-frame.ts";
|
|
8
|
+
import { colorProfileFromLevel, type ColorProfile } from "#/screen/index.ts";
|
|
9
|
+
import { serializeScreen } from "#/screen/serialize.ts";
|
|
7
10
|
import { Yoga } from "#/yoga/index.ts";
|
|
8
11
|
|
|
9
12
|
export type RenderToStringOptions = {
|
|
@@ -13,6 +16,12 @@ export type RenderToStringOptions = {
|
|
|
13
16
|
@default 80
|
|
14
17
|
*/
|
|
15
18
|
columns?: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
Color profile used for deterministic serialization. By default this retains
|
|
22
|
+
the process color level for Ink compatibility.
|
|
23
|
+
*/
|
|
24
|
+
colorProfile?: ColorProfile;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -44,6 +53,7 @@ console.log(output);
|
|
|
44
53
|
*/
|
|
45
54
|
export const renderToString = (node: ReactNode, options?: RenderToStringOptions): string => {
|
|
46
55
|
const columns = options?.columns ?? 80;
|
|
56
|
+
const colorProfile = options?.colorProfile ?? colorProfileFromLevel(detectColorLevel());
|
|
47
57
|
|
|
48
58
|
// Create a standalone root node — no stdout, stdin, or terminal bindings
|
|
49
59
|
const rootNode: DOMElement = createNode("ink-root");
|
|
@@ -62,7 +72,10 @@ export const renderToString = (node: ReactNode, options?: RenderToStringOptions)
|
|
|
62
72
|
};
|
|
63
73
|
|
|
64
74
|
rootNode.onImmediateRender = () => {
|
|
65
|
-
const {
|
|
75
|
+
const { staticScreen } = renderFrame(rootNode, false, { colorProfile });
|
|
76
|
+
const staticOutput = staticScreen
|
|
77
|
+
? `${serializeScreen(staticScreen, { colorProfile, styles: colorProfile !== "none" })}\n`
|
|
78
|
+
: "";
|
|
66
79
|
if (staticOutput && staticOutput !== "\n") {
|
|
67
80
|
capturedStaticOutput += staticOutput;
|
|
68
81
|
}
|
|
@@ -99,7 +112,10 @@ export const renderToString = (node: ReactNode, options?: RenderToStringOptions)
|
|
|
99
112
|
|
|
100
113
|
// Yoga layout has already been calculated by onComputeLayout during commit.
|
|
101
114
|
// Render the DOM tree to a string — this captures the dynamic (non-static) output.
|
|
102
|
-
const {
|
|
115
|
+
const { screen } = renderFrame(rootNode, false, { colorProfile });
|
|
116
|
+
const output = screen
|
|
117
|
+
? serializeScreen(screen, { colorProfile, styles: colorProfile !== "none" })
|
|
118
|
+
: "";
|
|
103
119
|
|
|
104
120
|
// Tear down: unmount the tree so the reconciler cleans up child nodes
|
|
105
121
|
// and runs effect cleanup functions. The reconciler detaches removed
|
|
@@ -116,9 +132,8 @@ export const renderToString = (node: ReactNode, options?: RenderToStringOptions)
|
|
|
116
132
|
new Error(String(uncaughtError));
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// so renderToString returns clean output.
|
|
135
|
+
// Static terminal output ends with a newline so dynamic output starts on a
|
|
136
|
+
// fresh line. Strip it here so renderToString returns clean output.
|
|
122
137
|
const normalizedStaticOutput = capturedStaticOutput.endsWith("\n")
|
|
123
138
|
? capturedStaticOutput.slice(0, -1)
|
|
124
139
|
: capturedStaticOutput;
|