@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11

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.
Files changed (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,59 @@
1
+ import { samplePaint } from "#/color/sample.ts";
2
+ import { type DOMNode } from "#/dom.ts";
3
+ import type { Canvas } from "#/screen/canvas.ts";
4
+ import { cellAttributes, createCell } from "#/screen/index.ts";
5
+
6
+ export const renderBackground = (x: number, y: number, node: DOMNode, output: Canvas): void => {
7
+ if (node.style.backgroundColor === undefined) {
8
+ return;
9
+ }
10
+
11
+ const width = node.yogaNode!.getComputedWidth();
12
+ const height = node.yogaNode!.getComputedHeight();
13
+
14
+ // Calculate the actual content area considering borders
15
+ const leftBorderWidth = node.style.borderStyle && node.style.borderLeft !== false ? 1 : 0;
16
+ const rightBorderWidth = node.style.borderStyle && node.style.borderRight !== false ? 1 : 0;
17
+ const topBorderHeight = node.style.borderStyle && node.style.borderTop !== false ? 1 : 0;
18
+ const bottomBorderHeight = node.style.borderStyle && node.style.borderBottom !== false ? 1 : 0;
19
+
20
+ const contentWidth = width - leftBorderWidth - rightBorderWidth;
21
+ const contentHeight = height - topBorderHeight - bottomBorderHeight;
22
+
23
+ if (!(contentWidth > 0 && contentHeight > 0)) {
24
+ return;
25
+ }
26
+
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
+ }),
57
+ );
58
+ output.writeCells(bounds.x, bounds.y, lines);
59
+ };
@@ -0,0 +1,167 @@
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";
5
+ import { type DOMNode } from "#/dom.ts";
6
+ import { BOXES } from "#/glyphs.ts";
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
+ });
34
+ };
35
+
36
+ export const renderBorder = (x: number, y: number, node: DOMNode, output: Canvas): void => {
37
+ if (node.style.borderStyle) {
38
+ const width = node.yogaNode!.getComputedWidth();
39
+ const height = node.yogaNode!.getComputedHeight();
40
+ const bounds = { x, y, width, height };
41
+ const box =
42
+ typeof node.style.borderStyle === "string"
43
+ ? BOXES[node.style.borderStyle]
44
+ : node.style.borderStyle;
45
+
46
+ const topBorderColor = node.style.borderTopColor ?? node.style.borderColor;
47
+ const bottomBorderColor = node.style.borderBottomColor ?? node.style.borderColor;
48
+ const leftBorderColor = node.style.borderLeftColor ?? node.style.borderColor;
49
+ const rightBorderColor = node.style.borderRightColor ?? node.style.borderColor;
50
+
51
+ const topBorderBackgroundColor =
52
+ node.style.borderTopBackgroundColor ?? node.style.borderBackgroundColor;
53
+ const bottomBorderBackgroundColor =
54
+ node.style.borderBottomBackgroundColor ?? node.style.borderBackgroundColor;
55
+ const leftBorderBackgroundColor =
56
+ node.style.borderLeftBackgroundColor ?? node.style.borderBackgroundColor;
57
+ const rightBorderBackgroundColor =
58
+ node.style.borderRightBackgroundColor ?? node.style.borderBackgroundColor;
59
+
60
+ const dimTopBorderColor = node.style.borderTopDimColor ?? node.style.borderDimColor;
61
+
62
+ const dimBottomBorderColor = node.style.borderBottomDimColor ?? node.style.borderDimColor;
63
+
64
+ const dimLeftBorderColor = node.style.borderLeftDimColor ?? node.style.borderDimColor;
65
+
66
+ const dimRightBorderColor = node.style.borderRightDimColor ?? node.style.borderDimColor;
67
+
68
+ const showTopBorder = node.style.borderTop !== false;
69
+ const showBottomBorder = node.style.borderBottom !== false;
70
+ const showLeftBorder = node.style.borderLeft !== false;
71
+ const showRightBorder = node.style.borderRight !== false;
72
+
73
+ const contentWidth = width - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);
74
+
75
+ const topBorder = showTopBorder
76
+ ? (showLeftBorder ? box.topLeft : "") +
77
+ box.top.repeat(contentWidth) +
78
+ (showRightBorder ? box.topRight : "")
79
+ : undefined;
80
+
81
+ let verticalBorderHeight = height;
82
+
83
+ if (showTopBorder) {
84
+ verticalBorderHeight -= 1;
85
+ }
86
+
87
+ if (showBottomBorder) {
88
+ verticalBorderHeight -= 1;
89
+ }
90
+
91
+ const offsetY = showTopBorder ? 1 : 0;
92
+
93
+ const bottomBorder = showBottomBorder
94
+ ? (showLeftBorder ? box.bottomLeft : "") +
95
+ box.bottom.repeat(contentWidth) +
96
+ (showRightBorder ? box.bottomRight : "")
97
+ : undefined;
98
+
99
+ if (topBorder) {
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
+ ]);
112
+ }
113
+
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
+ );
131
+ }
132
+
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
+ );
150
+ }
151
+
152
+ if (bottomBorder) {
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
+ ]);
165
+ }
166
+ }
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
+ };
@@ -0,0 +1,146 @@
1
+ import type { ReactNode } from "react";
2
+ import { LegacyRoot } from "react-reconciler/constants.js";
3
+
4
+ import { detectColorLevel } from "#/capabilities/detect.ts";
5
+ import { createNode, type DOMElement } from "#/dom.ts";
6
+ import { reconciler } from "#/reconciler.ts";
7
+ import { renderFrame } from "#/render-frame.ts";
8
+ import { colorProfileFromLevel, type ColorProfile } from "#/screen/index.ts";
9
+ import { serializeScreen } from "#/screen/serialize.ts";
10
+ import { Yoga } from "#/yoga/index.ts";
11
+
12
+ export type RenderToStringOptions = {
13
+ /**
14
+ Width of the virtual terminal in columns.
15
+
16
+ @default 80
17
+ */
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;
25
+ };
26
+
27
+ /**
28
+ Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
29
+
30
+ Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
31
+
32
+ **Notes:**
33
+
34
+ - Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
35
+ - `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
36
+ - `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
37
+ - The `<Static>` component is supported — its output is prepended to the dynamic output.
38
+ - If a component throws during rendering, the error is propagated to the caller after cleanup.
39
+
40
+ @example
41
+ ```
42
+ import {renderToString, Text, Box} from 'ink';
43
+
44
+ const output = renderToString(
45
+ <Box padding={1}>
46
+ <Text color="green">Hello World</Text>
47
+ </Box>,
48
+ {columns: 40}
49
+ );
50
+
51
+ console.log(output);
52
+ ```
53
+ */
54
+ export const renderToString = (node: ReactNode, options?: RenderToStringOptions): string => {
55
+ const columns = options?.columns ?? 80;
56
+ const colorProfile = options?.colorProfile ?? colorProfileFromLevel(detectColorLevel());
57
+
58
+ // Create a standalone root node — no stdout, stdin, or terminal bindings
59
+ const rootNode: DOMElement = createNode("ink-root");
60
+
61
+ // Capture static output from intermediate renders.
62
+ // The <Static> component uses useLayoutEffect to clear its children after
63
+ // the first commit. The reconciler's resetAfterCommit calls onImmediateRender
64
+ // when static content is dirty (and returns early, skipping the normal
65
+ // onRender callback), giving us a chance to capture it before it's cleared
66
+ // by the subsequent re-render.
67
+ let capturedStaticOutput = "";
68
+
69
+ rootNode.onComputeLayout = () => {
70
+ rootNode.yogaNode!.setWidth(columns);
71
+ rootNode.yogaNode!.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
72
+ };
73
+
74
+ rootNode.onImmediateRender = () => {
75
+ const { staticScreen } = renderFrame(rootNode, false, { colorProfile });
76
+ const staticOutput = staticScreen
77
+ ? `${serializeScreen(staticScreen, { colorProfile, styles: colorProfile !== "none" })}\n`
78
+ : "";
79
+ if (staticOutput && staticOutput !== "\n") {
80
+ capturedStaticOutput += staticOutput;
81
+ }
82
+ };
83
+
84
+ // Capture the first uncaught error so we can re-throw it after cleanup.
85
+ // React's reconciler catches component errors internally and reports them
86
+ // via onUncaughtError rather than letting them propagate. For a synchronous
87
+ // utility like renderToString, callers expect errors to throw.
88
+ let uncaughtError: unknown;
89
+
90
+ // Create a reconciler container in legacy (synchronous) mode.
91
+ // The four trailing callbacks are: onUncaughtError, onCaughtError,
92
+ // onRecoverableError, and onHostTransitionComplete.
93
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
94
+ const container = reconciler.createContainer(
95
+ rootNode,
96
+ LegacyRoot,
97
+ null,
98
+ false,
99
+ null,
100
+ "render-to-string",
101
+ (error: unknown) => {
102
+ uncaughtError ??= error;
103
+ },
104
+ () => {},
105
+ () => {},
106
+ () => {},
107
+ );
108
+
109
+ // Synchronously render the React tree into the container
110
+ reconciler.updateContainerSync(node, container, null, () => {});
111
+ reconciler.flushSyncWork();
112
+
113
+ // Yoga layout has already been calculated by onComputeLayout during commit.
114
+ // Render the DOM tree to a string — this captures the dynamic (non-static) output.
115
+ const { screen } = renderFrame(rootNode, false, { colorProfile });
116
+ const output = screen
117
+ ? serializeScreen(screen, { colorProfile, styles: colorProfile !== "none" })
118
+ : "";
119
+
120
+ // Tear down: unmount the tree so the reconciler cleans up child nodes
121
+ // and runs effect cleanup functions. The reconciler detaches removed
122
+ // subtrees (removeChildFromContainer → detachYogaSubtree); the garbage
123
+ // collector reclaims the Yoga nodes.
124
+ reconciler.updateContainerSync(null, container, null, () => {});
125
+ reconciler.flushSyncWork();
126
+
127
+ // Re-throw after full cleanup so callers see the original error.
128
+ if (uncaughtError !== undefined) {
129
+ throw uncaughtError instanceof Error
130
+ ? uncaughtError
131
+ : // eslint-disable-next-line @typescript-eslint/no-base-to-string
132
+ new Error(String(uncaughtError));
133
+ }
134
+
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.
137
+ const normalizedStaticOutput = capturedStaticOutput.endsWith("\n")
138
+ ? capturedStaticOutput.slice(0, -1)
139
+ : capturedStaticOutput;
140
+
141
+ if (normalizedStaticOutput && output) {
142
+ return normalizedStaticOutput + "\n" + output;
143
+ }
144
+
145
+ return normalizedStaticOutput || output;
146
+ };