@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.
Files changed (138) hide show
  1. package/README.md +21 -9
  2. package/THIRD_PARTY_NOTICES.md +39 -1
  3. package/dist/Text-BobFKi74.d.ts +452 -0
  4. package/dist/ansi.d.ts +122 -131
  5. package/dist/ansi.js +86 -6
  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-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -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-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/env-YVw64yZS.js +9 -0
  20. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  21. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  22. package/dist/index-DZ88EXJv.d.ts +21 -0
  23. package/dist/index.d.ts +143 -498
  24. package/dist/index.js +1026 -2244
  25. package/dist/osc-CCH7xDoS.js +71 -0
  26. package/dist/osc-Cn0fw77g.d.ts +23 -0
  27. package/dist/paint-C19minOS.d.ts +81 -0
  28. package/dist/query-vaIeGOkH.d.ts +152 -0
  29. package/dist/router.d.ts +392 -0
  30. package/dist/router.js +709 -0
  31. package/dist/sample-Cqw1bjUL.js +445 -0
  32. package/dist/screen-BOSLQ8fF.d.ts +49 -0
  33. package/dist/screen-CiPytswf.js +342 -0
  34. package/dist/screen.d.ts +5 -0
  35. package/dist/screen.js +5 -0
  36. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  37. package/dist/serialize-BTkAZgw1.js +79 -0
  38. package/dist/session-aZr9O8h3.js +665 -0
  39. package/dist/sgr-BhwaWAJB.js +246 -0
  40. package/dist/store-CgrG9K4y.d.ts +72 -0
  41. package/dist/string-width-CijQwpIk.js +69 -0
  42. package/dist/strip-BvU4toXG.js +6 -0
  43. package/dist/terminal.d.ts +118 -0
  44. package/dist/terminal.js +2 -0
  45. package/dist/tokenize-AjqbvtiT.js +1242 -0
  46. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  47. package/dist/truncate-D31fhU6i.js +562 -0
  48. package/dist/use-focus-BzqAJi0n.js +1337 -0
  49. package/package.json +41 -9
  50. package/src/ansi/chalk.ts +5 -3
  51. package/src/ansi/escapes.ts +14 -0
  52. package/src/ansi/graphemes.ts +8 -0
  53. package/src/ansi/hyperlink.ts +44 -0
  54. package/src/ansi/index.ts +3 -1
  55. package/src/ansi/osc.ts +77 -0
  56. package/src/ansi/tokenize.ts +3 -4
  57. package/src/capabilities/color-policy.ts +34 -0
  58. package/src/capabilities/detect.ts +594 -0
  59. package/src/capabilities/index.ts +37 -0
  60. package/src/capabilities/query.ts +657 -0
  61. package/src/capabilities/store.ts +379 -0
  62. package/src/color/index.ts +3 -0
  63. package/src/color/paint.ts +169 -0
  64. package/src/color/palette.ts +48 -0
  65. package/src/color/sample.ts +323 -0
  66. package/src/color.ts +1 -0
  67. package/src/components/AnsiText.tsx +42 -0
  68. package/src/components/App.tsx +98 -10
  69. package/src/components/BackgroundContext.ts +2 -3
  70. package/src/components/Box.tsx +0 -8
  71. package/src/components/CursorContext.ts +1 -1
  72. package/src/components/Hyperlink.tsx +56 -0
  73. package/src/components/TerminalOscContext.ts +25 -0
  74. package/src/components/Text.tsx +22 -45
  75. package/src/components/Transform.tsx +1 -1
  76. package/src/dom.ts +11 -2
  77. package/src/global.d.ts +3 -0
  78. package/src/hooks/use-capabilities.ts +73 -0
  79. package/src/hooks/use-cursor.ts +2 -2
  80. package/src/hooks/use-terminal-osc.ts +59 -0
  81. package/src/index.ts +45 -1
  82. package/src/ink.tsx +213 -153
  83. package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
  84. package/src/reconciler.ts +24 -3
  85. package/src/render-background.ts +36 -15
  86. package/src/render-border.ts +94 -61
  87. package/src/render-frame.ts +83 -0
  88. package/src/render-to-string.ts +21 -6
  89. package/src/render.ts +15 -6
  90. package/src/router/components.tsx +343 -0
  91. package/src/router/context.ts +41 -0
  92. package/src/router/history.ts +194 -0
  93. package/src/router/hooks.tsx +391 -0
  94. package/src/router/index.ts +34 -0
  95. package/src/router/matcher.ts +571 -0
  96. package/src/screen/ansi.ts +184 -0
  97. package/src/screen/canvas.ts +160 -0
  98. package/src/screen/cell.ts +138 -0
  99. package/src/screen/color-profile.ts +47 -0
  100. package/src/screen/geometry.ts +9 -0
  101. package/src/screen/index.ts +6 -0
  102. package/src/screen/screen.ts +305 -0
  103. package/src/screen/serialize.ts +129 -0
  104. package/src/screen.ts +1 -0
  105. package/src/semantic-text-style.ts +118 -0
  106. package/src/squash-text-nodes.ts +2 -5
  107. package/src/structured-text.ts +325 -0
  108. package/src/styles.ts +19 -14
  109. package/src/terminal/index.ts +2 -0
  110. package/src/terminal/inline-presenter.ts +120 -0
  111. package/src/terminal/input.ts +86 -0
  112. package/src/terminal/render-scheduler.ts +37 -0
  113. package/src/terminal/screen-presenter.ts +188 -0
  114. package/src/terminal/session.ts +407 -0
  115. package/src/terminal.ts +1 -0
  116. package/src/testing/browser.ts +588 -0
  117. package/src/testing/emulators.ts +205 -0
  118. package/src/testing/explorer-app/index.html +12 -0
  119. package/src/testing/explorer-app/main.ts +381 -0
  120. package/src/testing/explorer-app/style.css +194 -0
  121. package/src/testing/explorer-app/tsconfig.json +15 -0
  122. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  123. package/src/testing/index.ts +26 -0
  124. package/src/testing/keys.ts +56 -0
  125. package/src/testing/live.ts +85 -0
  126. package/src/testing/matchers.ts +70 -0
  127. package/src/testing/public.ts +94 -0
  128. package/src/testing/terminal.ts +349 -0
  129. package/src/testing/vitest.ts +157 -0
  130. package/src/transform-adapter.ts +14 -0
  131. package/src/wrap-text.ts +4 -0
  132. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  133. package/dist/truncate-Cr6xVFMa.js +0 -2330
  134. package/src/ansi/supports-color.ts +0 -207
  135. package/src/colorize.ts +0 -60
  136. package/src/log-update.ts +0 -370
  137. package/src/output.ts +0 -308
  138. package/src/renderer.ts +0 -73
@@ -0,0 +1,305 @@
1
+ import { blend } from "#/color/sample.ts";
2
+ import { createCell, emptyCell, type Cell, type CellPatch, type CellStyle } from "#/screen/cell.ts";
3
+ import type { Rect } from "#/screen/geometry.ts";
4
+
5
+ export type Line = readonly Cell[];
6
+
7
+ /** A half-open range of changed columns in one row. */
8
+ export type DirtySpan = {
9
+ readonly y: number;
10
+ readonly start: number;
11
+ readonly end: number;
12
+ };
13
+
14
+ /**
15
+ A mutable two-dimensional terminal cell buffer. Cells themselves are immutable,
16
+ so current and next screens can compare them by value or safely share constants.
17
+ */
18
+ export class Screen {
19
+ #width: number;
20
+ #height: number;
21
+
22
+ #rows: Cell[][];
23
+ #painted: boolean[][];
24
+ #dirty: Array<{ start: number; end: number } | undefined>;
25
+
26
+ constructor(width: number, height: number) {
27
+ validateDimensions(width, height);
28
+
29
+ this.#width = width;
30
+ this.#height = height;
31
+ this.#rows = Array.from({ length: height }, () => Array<Cell>(width).fill(emptyCell));
32
+ this.#painted = Array.from({ length: height }, () => Array<boolean>(width).fill(false));
33
+ this.#dirty = Array.from({ length: height }, () =>
34
+ width > 0 ? { start: 0, end: width } : undefined,
35
+ );
36
+ }
37
+
38
+ get width(): number {
39
+ return this.#width;
40
+ }
41
+
42
+ /**
43
+ The populated length of one row: the nominal width, or more when overflow
44
+ writes (`textWrap: "none"` content) extended it past the screen's width.
45
+ */
46
+ rowLength(y: number): number {
47
+ return Math.max(this.#width, this.#rows[y]?.length ?? 0);
48
+ }
49
+
50
+ get height(): number {
51
+ return this.#height;
52
+ }
53
+
54
+ bounds(): Rect {
55
+ return { x: 0, y: 0, width: this.width, height: this.height };
56
+ }
57
+
58
+ cellAt(x: number, y: number): Cell | undefined {
59
+ return this.#rows[y]?.[x];
60
+ }
61
+
62
+ /** Whether a drawable explicitly touched this position. */
63
+ isPainted(x: number, y: number): boolean {
64
+ return this.#painted[y]?.[x] ?? false;
65
+ }
66
+
67
+ setCell(x: number, y: number, cell: Cell, options?: { overflow?: boolean }): void {
68
+ const row = this.#rows[y];
69
+ const overflow = options?.overflow === true;
70
+ if (!row || x < 0 || (!overflow && x >= this.width)) {
71
+ return;
72
+ }
73
+
74
+ if (!Number.isInteger(cell.width) || cell.width < 1) {
75
+ throw new Error("Only leading cells with a positive width may be written");
76
+ }
77
+
78
+ const finalColumn = x + cell.width;
79
+ if (overflow && finalColumn > row.length) {
80
+ this.#growRow(row, y, finalColumn);
81
+ }
82
+
83
+ const capacity = overflow ? row.length : this.width;
84
+ const clipped = finalColumn > capacity;
85
+ const overwriteEnd = Math.min(finalColumn, capacity);
86
+
87
+ for (let column = x; column < overwriteEnd; column++) {
88
+ this.#clearGraphemeAt(row, y, column);
89
+ }
90
+
91
+ if (clipped) {
92
+ for (let column = x; column < capacity; column++) {
93
+ row[column] = emptyCell;
94
+ }
95
+ this.#markDirty(y, x, capacity);
96
+
97
+ return;
98
+ }
99
+
100
+ row[x] = cell;
101
+ this.#painted[y]![x] = true;
102
+
103
+ for (let column = x + 1; column < finalColumn; column++) {
104
+ row[column] = {
105
+ grapheme: "",
106
+ width: 0,
107
+ style: cell.style,
108
+ ...(cell.hyperlink ? { hyperlink: cell.hyperlink } : {}),
109
+ };
110
+ this.#painted[y]![column] = true;
111
+ }
112
+ this.#markDirty(y, x, finalColumn);
113
+ }
114
+
115
+ /**
116
+ Composes independent cell channels over the existing cell. An undefined
117
+ patch is transparent; an explicit space in `content` paints a blank.
118
+ */
119
+ composeCell(
120
+ x: number,
121
+ y: number,
122
+ patch: CellPatch | undefined,
123
+ options?: { overflow?: boolean },
124
+ ): void {
125
+ if (!patch) {
126
+ return;
127
+ }
128
+
129
+ const row = this.#rows[y];
130
+ if (!row || x < 0 || (options?.overflow !== true && x >= this.width)) {
131
+ return;
132
+ }
133
+
134
+ let leadingColumn = x;
135
+ while (leadingColumn > 0 && row[leadingColumn]?.width === 0) {
136
+ leadingColumn--;
137
+ }
138
+
139
+ const destination = row[leadingColumn] ?? emptyCell;
140
+ const writeColumn = patch.content ? x : leadingColumn;
141
+ const style: CellStyle = {
142
+ ...this.#composeColor(destination.style, patch, "foreground"),
143
+ ...this.#composeColor(destination.style, patch, "background"),
144
+ ...this.#composeColor(destination.style, patch, "underlineColor"),
145
+ underline: patch.underline ?? destination.style.underline,
146
+ attributes: patch.attributes ?? destination.style.attributes,
147
+ };
148
+ const content = patch.content ?? destination;
149
+ const hyperlink =
150
+ patch.hyperlink === undefined ? destination.hyperlink : (patch.hyperlink ?? undefined);
151
+
152
+ this.setCell(
153
+ writeColumn,
154
+ y,
155
+ createCell(content.grapheme, content.width, style, hyperlink),
156
+ options,
157
+ );
158
+ }
159
+
160
+ /** Composes a patch throughout a rectangle, clipped to this screen. */
161
+ fill(bounds: Rect, patch: CellPatch | undefined): void {
162
+ if (!patch) {
163
+ return;
164
+ }
165
+
166
+ const startX = Math.max(0, bounds.x);
167
+ const startY = Math.max(0, bounds.y);
168
+ const endX = Math.min(this.width, bounds.x + bounds.width);
169
+ const endY = Math.min(this.height, bounds.y + bounds.height);
170
+ const step = Math.max(1, patch.content?.width ?? 1);
171
+
172
+ for (let y = startY; y < endY; y++) {
173
+ for (let x = startX; x < endX; x += step) {
174
+ this.composeCell(x, y, patch);
175
+ }
176
+ }
177
+ }
178
+
179
+ /** Resizes while preserving complete graphemes in the shared top-left area. */
180
+ resize(width: number, height: number): void {
181
+ validateDimensions(width, height);
182
+
183
+ const rows = Array.from({ length: height }, () => Array<Cell>(width).fill(emptyCell));
184
+ const painted = Array.from({ length: height }, () => Array<boolean>(width).fill(false));
185
+ const sharedHeight = Math.min(height, this.height);
186
+
187
+ for (let y = 0; y < sharedHeight; y++) {
188
+ const source = this.#rows[y]!;
189
+ const destination = rows[y]!;
190
+ for (let x = 0; x < Math.min(width, this.width); x++) {
191
+ const cell = source[x]!;
192
+ if (cell.width === 0 || x + cell.width > width) {
193
+ continue;
194
+ }
195
+
196
+ destination[x] = cell;
197
+ painted[y]![x] = this.#painted[y]![x]!;
198
+ for (let column = x + 1; column < x + cell.width; column++) {
199
+ destination[column] = source[column]!;
200
+ painted[y]![column] = this.#painted[y]![column]!;
201
+ }
202
+ }
203
+ }
204
+
205
+ this.#width = width;
206
+ this.#height = height;
207
+ this.#rows = rows;
208
+ this.#painted = painted;
209
+ this.#dirty = Array.from({ length: height }, () =>
210
+ width > 0 ? { start: 0, end: width } : undefined,
211
+ );
212
+ }
213
+
214
+ clear(): void {
215
+ for (const [y, row] of this.#rows.entries()) {
216
+ row.fill(emptyCell);
217
+ this.#painted[y]!.fill(true);
218
+ this.#markDirty(y, 0, this.rowLength(y));
219
+ }
220
+ }
221
+
222
+ toRows(): readonly Line[] {
223
+ return this.#rows;
224
+ }
225
+
226
+ dirtySpans(): readonly DirtySpan[] {
227
+ return this.#dirty.flatMap((span, y) => (span ? [{ y, ...span }] : []));
228
+ }
229
+
230
+ takeDirtySpans(): readonly DirtySpan[] {
231
+ const spans = this.dirtySpans();
232
+ this.#dirty.fill(undefined);
233
+ return spans;
234
+ }
235
+
236
+ /** Extends a row (and its paint mask) so overflow writes land past `width`. */
237
+ #growRow(row: Cell[], y: number, length: number): void {
238
+ const painted = this.#painted[y]!;
239
+ while (row.length < length) {
240
+ row.push(emptyCell);
241
+ painted.push(false);
242
+ }
243
+ }
244
+
245
+ #clearGraphemeAt(row: Cell[], y: number, x: number): void {
246
+ const existing = row[x];
247
+ if (!existing || existing === emptyCell) {
248
+ return;
249
+ }
250
+
251
+ let leadingColumn = x;
252
+ while (leadingColumn > 0 && row[leadingColumn]?.width === 0) {
253
+ leadingColumn--;
254
+ }
255
+
256
+ const leadingCell = row[leadingColumn];
257
+ const width = Math.max(1, leadingCell?.width ?? 1);
258
+ for (
259
+ let column = leadingColumn;
260
+ column < Math.min(leadingColumn + width, row.length);
261
+ column++
262
+ ) {
263
+ row[column] = emptyCell;
264
+ this.#painted[y]![column] = true;
265
+ }
266
+ this.#markDirty(y, leadingColumn, leadingColumn + width);
267
+ }
268
+
269
+ #composeColor(
270
+ destination: CellStyle,
271
+ patch: CellPatch,
272
+ channel: "foreground" | "background" | "underlineColor",
273
+ ): Partial<CellStyle> {
274
+ const value = patch[channel];
275
+ if (value === undefined) {
276
+ return destination[channel] ? { [channel]: destination[channel] } : {};
277
+ }
278
+
279
+ if (value === null) return {};
280
+ const destinationColor = destination[channel];
281
+ if (value.model === "rgb" && value.alpha < 255 && destinationColor) {
282
+ return { [channel]: blend(value, destinationColor) };
283
+ }
284
+ return { [channel]: value };
285
+ }
286
+
287
+ #markDirty(y: number, start: number, end: number): void {
288
+ if (y < 0 || y >= this.height || start >= end) {
289
+ return;
290
+ }
291
+
292
+ const clippedStart = Math.max(0, start);
293
+ const clippedEnd = Math.min(this.rowLength(y), end);
294
+ const current = this.#dirty[y];
295
+ this.#dirty[y] = current
296
+ ? { start: Math.min(current.start, clippedStart), end: Math.max(current.end, clippedEnd) }
297
+ : { start: clippedStart, end: clippedEnd };
298
+ }
299
+ }
300
+
301
+ function validateDimensions(width: number, height: number): void {
302
+ if (!Number.isInteger(width) || width < 0 || !Number.isInteger(height) || height < 0) {
303
+ throw new Error("Screen dimensions must be non-negative integers");
304
+ }
305
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ getEndCode,
3
+ styledCharsToString,
4
+ type AnsiCode,
5
+ type StyledChar,
6
+ } from "#/ansi/tokenize.ts";
7
+ import { cellAttributes, type Cell, type Color, type Hyperlink } from "#/screen/cell.ts";
8
+ import { quantizeColor, type ColorProfile } from "#/screen/color-profile.ts";
9
+ import type { Line, Screen } from "#/screen/screen.ts";
10
+
11
+ const ESC = "\u001B";
12
+ const BEL = "\u0007";
13
+
14
+ export type SerializeOptions = {
15
+ readonly colorProfile: ColorProfile;
16
+ readonly trimEnd?: boolean;
17
+ readonly styles?: boolean;
18
+ };
19
+
20
+ /** Serializes one structured cell run with minimal style transitions. */
21
+ export function serializeLine(line: Line, options: SerializeOptions): string {
22
+ const characters = line.map((cell) =>
23
+ styledCharFromCell(cell, options.colorProfile, options.styles ?? true),
24
+ );
25
+ const output = styledCharsToString(characters);
26
+ return options.trimEnd === false ? output : output.trimEnd();
27
+ }
28
+
29
+ export function serializeScreen(screen: Screen, options: SerializeOptions): string {
30
+ return screen
31
+ .toRows()
32
+ .map((line) => serializeLine(line, options))
33
+ .join("\n");
34
+ }
35
+
36
+ /** Converts a semantic cell to the canonical ANSI compatibility style state. */
37
+ export function styledCharFromCell(cell: Cell, profile: ColorProfile, styles = true): StyledChar {
38
+ return {
39
+ type: "char",
40
+ value: cell.grapheme,
41
+ fullWidth: cell.width > 1,
42
+ styles: styles ? ansiCodesFromCell(cell, profile) : [],
43
+ };
44
+ }
45
+
46
+ function ansiCodesFromCell(cell: Cell, profile: ColorProfile): AnsiCode[] {
47
+ const result: AnsiCode[] = [];
48
+ const { style } = cell;
49
+
50
+ if (cell.hyperlink) {
51
+ result.push(codePair(hyperlinkStart(cell.hyperlink)));
52
+ }
53
+
54
+ // This order mirrors the nesting produced by the Ink-compatible Text API,
55
+ // while remaining deterministic for cells produced by custom drawables.
56
+ addAttribute(result, style.attributes, cellAttributes.inverse, 7);
57
+ addAttribute(result, style.attributes, cellAttributes.hidden, 8);
58
+ addAttribute(result, style.attributes, cellAttributes.strikethrough, 9);
59
+
60
+ if (style.underline !== "none") {
61
+ const variant = {
62
+ single: "4",
63
+ double: "4:2",
64
+ curly: "4:3",
65
+ dotted: "4:4",
66
+ dashed: "4:5",
67
+ }[style.underline];
68
+ result.push(codePair(`${ESC}[${variant}m`));
69
+ }
70
+
71
+ addAttribute(result, style.attributes, cellAttributes.italic, 3);
72
+ addAttribute(result, style.attributes, cellAttributes.bold, 1);
73
+ addAttribute(result, style.attributes, cellAttributes.rapidBlink, 6);
74
+ addAttribute(result, style.attributes, cellAttributes.blink, 5);
75
+
76
+ const background = quantizeColor(style.background, profile);
77
+ if (background) {
78
+ result.push(codePair(colorCode(background, "background")));
79
+ }
80
+
81
+ const foreground = quantizeColor(style.foreground, profile);
82
+ if (foreground) {
83
+ result.push(codePair(colorCode(foreground, "foreground")));
84
+ }
85
+
86
+ addAttribute(result, style.attributes, cellAttributes.faint, 2);
87
+
88
+ const underlineColor = quantizeColor(style.underlineColor, profile);
89
+ if (underlineColor) {
90
+ result.push(codePair(colorCode(underlineColor, "underline")));
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ function addAttribute(
97
+ result: AnsiCode[],
98
+ attributes: number,
99
+ attribute: number,
100
+ sgr: number,
101
+ ): void {
102
+ if (Math.floor(attributes / attribute) % 2 === 1) {
103
+ result.push(codePair(`${ESC}[${sgr}m`));
104
+ }
105
+ }
106
+
107
+ function colorCode(color: Color, channel: "foreground" | "background" | "underline"): string {
108
+ const prefix = { foreground: 38, background: 48, underline: 58 }[channel];
109
+
110
+ if (color.model === "rgb") {
111
+ return `${ESC}[${prefix};2;${color.red};${color.green};${color.blue}m`;
112
+ }
113
+
114
+ if (channel === "underline" || color.index >= 16 || color.encoding === "ansi256") {
115
+ return `${ESC}[${prefix};5;${color.index}m`;
116
+ }
117
+
118
+ const basic = color.index < 8 ? color.index : color.index - 8 + 60;
119
+ const channelOffset = channel === "background" ? 40 : 30;
120
+ return `${ESC}[${channelOffset + basic}m`;
121
+ }
122
+
123
+ function hyperlinkStart(hyperlink: Hyperlink): string {
124
+ return `${ESC}]8;${hyperlink.parameters ?? ""};${hyperlink.url}${BEL}`;
125
+ }
126
+
127
+ function codePair(code: string): AnsiCode {
128
+ return { code, endCode: getEndCode(code) };
129
+ }
package/src/screen.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "#/screen/index.ts";
@@ -0,0 +1,118 @@
1
+ import { hexToRgb } from "#/ansi/sgr.ts";
2
+ import type { Paint } from "#/color/paint.ts";
3
+ import { cellAttributes, type CellStyle, type Color } from "#/screen/cell.ts";
4
+
5
+ export type SemanticTextStyle = {
6
+ readonly foreground?: Paint;
7
+ readonly background?: Paint;
8
+ readonly resetForeground?: boolean;
9
+ readonly resetBackground?: boolean;
10
+ readonly underline?: CellStyle["underline"];
11
+ readonly attributes: number;
12
+ };
13
+
14
+ export const emptySemanticTextStyle: SemanticTextStyle = {
15
+ attributes: cellAttributes.none,
16
+ };
17
+
18
+ const namedColors: Record<string, number> = {
19
+ black: 0,
20
+ red: 1,
21
+ green: 2,
22
+ yellow: 3,
23
+ blue: 4,
24
+ magenta: 5,
25
+ cyan: 6,
26
+ white: 7,
27
+ blackBright: 8,
28
+ gray: 8,
29
+ grey: 8,
30
+ redBright: 9,
31
+ greenBright: 10,
32
+ yellowBright: 11,
33
+ blueBright: 12,
34
+ magentaBright: 13,
35
+ cyanBright: 14,
36
+ whiteBright: 15,
37
+ };
38
+
39
+ const rgbPattern = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
40
+ const ansi256Pattern = /^ansi256\(\s?(\d+)\s?\)$/;
41
+
42
+ export function parseSemanticColor(value: string | Color | undefined): Color | undefined {
43
+ if (!value) {
44
+ return undefined;
45
+ }
46
+
47
+ if (typeof value !== "string") return value;
48
+
49
+ const named = namedColors[value];
50
+ if (named !== undefined) {
51
+ return { model: "indexed", index: named, encoding: "ansi16" };
52
+ }
53
+
54
+ if (value.startsWith("#")) {
55
+ const [red, green, blue] = hexToRgb(value);
56
+ return { model: "rgb", red, green, blue, alpha: 255 };
57
+ }
58
+
59
+ const rgb = rgbPattern.exec(value);
60
+ if (rgb) {
61
+ return {
62
+ model: "rgb",
63
+ red: Number(rgb[1]),
64
+ green: Number(rgb[2]),
65
+ blue: Number(rgb[3]),
66
+ alpha: 255,
67
+ };
68
+ }
69
+
70
+ const ansi256 = ansi256Pattern.exec(value);
71
+ if (ansi256) {
72
+ return { model: "indexed", index: Number(ansi256[1]), encoding: "ansi256" };
73
+ }
74
+
75
+ return undefined;
76
+ }
77
+
78
+ export function mergeSemanticTextStyles(
79
+ parent: SemanticTextStyle,
80
+ child: SemanticTextStyle | undefined,
81
+ ): SemanticTextStyle {
82
+ if (!child) {
83
+ return parent;
84
+ }
85
+
86
+ // Attribute flags are intentionally additive across nested Text nodes.
87
+ // eslint-disable-next-line no-bitwise
88
+ const attributes = parent.attributes | child.attributes;
89
+ const foreground = child.resetForeground ? undefined : (child.foreground ?? parent.foreground);
90
+ const background = child.resetBackground ? undefined : (child.background ?? parent.background);
91
+ return {
92
+ ...(foreground ? { foreground } : {}),
93
+ ...(background ? { background } : {}),
94
+ ...(child.resetForeground ? { resetForeground: true } : {}),
95
+ ...(child.resetBackground ? { resetBackground: true } : {}),
96
+ ...((child.underline ?? parent.underline)
97
+ ? { underline: child.underline ?? parent.underline }
98
+ : {}),
99
+ attributes,
100
+ };
101
+ }
102
+
103
+ export function semanticTextStyleToCellStyle(style: SemanticTextStyle): CellStyle {
104
+ const foreground = solidColor(style.foreground);
105
+ const background = solidColor(style.background);
106
+ return {
107
+ ...(foreground ? { foreground } : {}),
108
+ ...(background ? { background } : {}),
109
+ underline: style.underline ?? "none",
110
+ attributes: style.attributes,
111
+ };
112
+ }
113
+
114
+ function solidColor(paint: Paint | undefined): Color | undefined {
115
+ if (!paint) return undefined;
116
+ if (typeof paint === "string") return parseSemanticColor(paint);
117
+ return "model" in paint ? paint : undefined;
118
+ }
@@ -1,9 +1,7 @@
1
1
  import { type DOMElement } from "#/dom.ts";
2
2
  import { sanitizeAnsi } from "#/sanitize-ansi.ts";
3
3
 
4
- // Squashing text nodes allows to combine multiple text nodes into one and write
5
- // to `Output` instance only once. For example, <Text>hello{' '}world</Text>
6
- // is actually 3 text nodes, which would result 3 writes to `Output`.
4
+ // Squashing text nodes combines the host text nodes into one compatibility run.
7
5
  //
8
6
  // Also, this is necessary for libraries like ink-link (https://github.com/sindresorhus/ink-link),
9
7
  // which need to wrap all children at once, instead of wrapping 3 text nodes separately.
@@ -26,8 +24,7 @@ export const squashTextNodes = (node: DOMElement): string => {
26
24
  nodeText = squashTextNodes(childNode);
27
25
  }
28
26
 
29
- // Since these text nodes are being concatenated, `Output` instance won't be able to
30
- // apply children transform, so we have to do it manually here for each text node
27
+ // Nested compatibility transforms must run before the enclosing transform.
31
28
  if (nodeText.length > 0 && typeof childNode.internal_transform === "function") {
32
29
  nodeText = childNode.internal_transform(nodeText, index);
33
30
  }