@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.2
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 +299 -299
- package/dist/ansi.d.ts +223 -0
- package/dist/ansi.js +2 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-BhYGjb7h.js} +1 -1
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1657 -0
- package/dist/index.js +4643 -0
- package/dist/sgr-CMfEpjSk.d.ts +91 -0
- package/dist/truncate-CBiyyZzw.js +2156 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +37 -17
- package/src/ansi/chalk.ts +179 -0
- package/src/ansi/cursor.ts +48 -0
- package/src/ansi/east-asian-width.ts +215 -0
- package/src/ansi/escapes.ts +128 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/sgr.ts +237 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +236 -0
- package/src/ansi/strip.ts +33 -0
- package/src/ansi/supports-color.ts +213 -0
- package/src/ansi/tokenize.ts +453 -0
- package/src/ansi/truncate.ts +194 -0
- package/src/ansi/widest-line.ts +12 -0
- package/src/ansi/wrap.ts +766 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/auto-bind.ts +41 -0
- package/src/boxes.ts +100 -0
- package/src/code-excerpt.ts +39 -0
- package/src/colorize.ts +60 -0
- package/src/components/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/App.tsx +781 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +8 -0
- package/src/components/Box.tsx +116 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/ErrorOverview.tsx +133 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Newline.tsx +15 -0
- package/src/components/Spacer.tsx +10 -0
- package/src/components/Static.tsx +59 -0
- package/src/components/StderrContext.ts +26 -0
- package/src/components/StdinContext.ts +49 -0
- package/src/components/StdoutContext.ts +28 -0
- package/src/components/Text.tsx +144 -0
- package/src/components/Transform.tsx +37 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools-window-polyfill.ts +73 -0
- package/src/devtools.ts +43 -0
- package/src/dom.ts +292 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +36 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +83 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-window-size.ts +41 -0
- package/src/indent-string.ts +16 -0
- package/src/index.ts +44 -0
- package/src/ink.tsx +1506 -0
- package/src/input-parser.ts +283 -0
- package/src/instances.ts +9 -0
- package/src/is-in-ci.ts +7 -0
- package/src/kitty-keyboard.ts +57 -0
- package/src/log-update.ts +370 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/output.ts +308 -0
- package/src/parse-keypress.ts +516 -0
- package/src/parse-stack-line.ts +139 -0
- package/src/patch-console.ts +62 -0
- package/src/quick-lru.ts +85 -0
- package/src/reconciler.ts +451 -0
- package/src/render-background.ts +38 -0
- package/src/render-border.ts +134 -0
- package/src/render-node-to-output.ts +191 -0
- package/src/render-to-string.ts +131 -0
- package/src/render.ts +276 -0
- package/src/renderer.ts +73 -0
- package/src/sanitize-ansi.ts +33 -0
- package/src/signal-exit.ts +107 -0
- package/src/squash-text-nodes.ts +40 -0
- package/src/stream.ts +30 -0
- package/src/styles.ts +748 -0
- package/src/terminal-size.ts +57 -0
- package/src/throttle.ts +73 -0
- package/src/types.ts +15 -0
- package/src/utils.ts +40 -0
- package/src/wrap-text.ts +50 -0
- package/src/write-synchronized.ts +9 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2920 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- package/dist/index.mjs +0 -10072
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { chalk, type ForegroundColorName } from "../ansi/chalk.ts";
|
|
4
|
+
import { colorize } from "../colorize.ts";
|
|
5
|
+
import { type Styles } from "../styles.ts";
|
|
6
|
+
import { type LiteralUnion } from "../types.ts";
|
|
7
|
+
import { accessibilityContext } from "./AccessibilityContext.ts";
|
|
8
|
+
import { backgroundContext } from "./BackgroundContext.ts";
|
|
9
|
+
|
|
10
|
+
export type Props = {
|
|
11
|
+
/**
|
|
12
|
+
A label for the element for screen readers.
|
|
13
|
+
*/
|
|
14
|
+
readonly "aria-label"?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
Hide the element from screen readers.
|
|
18
|
+
*/
|
|
19
|
+
readonly "aria-hidden"?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
Change text color. Ink uses Chalk under the hood, so all its functionality is supported.
|
|
23
|
+
*/
|
|
24
|
+
readonly color?: LiteralUnion<ForegroundColorName, string>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Same as `color`, but for the background.
|
|
28
|
+
*/
|
|
29
|
+
readonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Dim the color (make it less bright).
|
|
33
|
+
*/
|
|
34
|
+
readonly dimColor?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
Make the text bold.
|
|
38
|
+
*/
|
|
39
|
+
readonly bold?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
Make the text italic.
|
|
43
|
+
*/
|
|
44
|
+
readonly italic?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Make the text underlined.
|
|
48
|
+
*/
|
|
49
|
+
readonly underline?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
Make the text crossed out with a line.
|
|
53
|
+
*/
|
|
54
|
+
readonly strikethrough?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
Inverse background and foreground colors.
|
|
58
|
+
*/
|
|
59
|
+
readonly inverse?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
This property tells Ink to wrap or truncate text if its width is larger than the container. If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines. If `hard` is passed, Ink will fill each line to the full column width, breaking words as necessary. If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off.
|
|
63
|
+
*/
|
|
64
|
+
readonly wrap?: Styles["textWrap"];
|
|
65
|
+
|
|
66
|
+
readonly children?: ReactNode;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
This component can display text and change its style to make it bold, underlined, italic, or strikethrough.
|
|
71
|
+
*/
|
|
72
|
+
export function Text({
|
|
73
|
+
color,
|
|
74
|
+
backgroundColor,
|
|
75
|
+
dimColor = false,
|
|
76
|
+
bold = false,
|
|
77
|
+
italic = false,
|
|
78
|
+
underline = false,
|
|
79
|
+
strikethrough = false,
|
|
80
|
+
inverse = false,
|
|
81
|
+
wrap = "wrap",
|
|
82
|
+
children,
|
|
83
|
+
"aria-label": ariaLabel,
|
|
84
|
+
"aria-hidden": ariaHidden = false,
|
|
85
|
+
}: Props) {
|
|
86
|
+
const { isScreenReaderEnabled } = useContext(accessibilityContext);
|
|
87
|
+
const inheritedBackgroundColor = useContext(backgroundContext);
|
|
88
|
+
const childrenOrAriaLabel = isScreenReaderEnabled && ariaLabel ? ariaLabel : children;
|
|
89
|
+
|
|
90
|
+
if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const transform = (text: string): string => {
|
|
95
|
+
if (dimColor) {
|
|
96
|
+
text = chalk.dim(text);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (color) {
|
|
100
|
+
text = colorize(text, color, "foreground");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use explicit backgroundColor if provided, otherwise use inherited from parent Box
|
|
104
|
+
const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
|
|
105
|
+
if (effectiveBackgroundColor) {
|
|
106
|
+
text = colorize(text, effectiveBackgroundColor, "background");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (bold) {
|
|
110
|
+
text = chalk.bold(text);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (italic) {
|
|
114
|
+
text = chalk.italic(text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (underline) {
|
|
118
|
+
text = chalk.underline(text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (strikethrough) {
|
|
122
|
+
text = chalk.strikethrough(text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (inverse) {
|
|
126
|
+
text = chalk.inverse(text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return text;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (isScreenReaderEnabled && ariaHidden) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ink-text
|
|
138
|
+
style={{ flexGrow: 0, flexShrink: 1, flexDirection: "row", textWrap: wrap }}
|
|
139
|
+
internal_transform={transform}
|
|
140
|
+
>
|
|
141
|
+
{childrenOrAriaLabel}
|
|
142
|
+
</ink-text>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { accessibilityContext } from "./AccessibilityContext.ts";
|
|
4
|
+
|
|
5
|
+
export type Props = {
|
|
6
|
+
/**
|
|
7
|
+
Screen-reader-specific text to output. If this is set, all children will be ignored.
|
|
8
|
+
*/
|
|
9
|
+
readonly accessibilityLabel?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Function that transforms children output. It accepts children and must return transformed children as well. Note that when children use `<Text>` styling props (e.g. `color`, `bold`), the string will contain ANSI escape codes.
|
|
13
|
+
*/
|
|
14
|
+
readonly transform: (children: string, index: number) => string;
|
|
15
|
+
|
|
16
|
+
readonly children?: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Transform a string representation of React components before they're written to output. For example, you might want to apply a gradient to text, add a clickable link, or create some text effects. These use cases can't accept React nodes as input; they expect a string. That's what the <Transform> component does: it gives you an output string of its child components and lets you transform it in any way.
|
|
21
|
+
*/
|
|
22
|
+
export function Transform({ children, transform, accessibilityLabel }: Props) {
|
|
23
|
+
const { isScreenReaderEnabled } = useContext(accessibilityContext);
|
|
24
|
+
|
|
25
|
+
if (children === undefined || children === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<ink-text
|
|
31
|
+
style={{ flexGrow: 0, flexShrink: 1, flexDirection: "row" }}
|
|
32
|
+
internal_transform={transform}
|
|
33
|
+
>
|
|
34
|
+
{isScreenReaderEnabled && accessibilityLabel ? accessibilityLabel : children}
|
|
35
|
+
</ink-text>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ansiEscapes } from "./ansi/escapes.ts";
|
|
2
|
+
|
|
3
|
+
export type CursorPosition = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const showCursorEscape = ansiEscapes.cursorShow;
|
|
9
|
+
export const hideCursorEscape = ansiEscapes.cursorHide;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Compare two cursor positions. Returns true if they differ.
|
|
13
|
+
*/
|
|
14
|
+
export const cursorPositionChanged = (
|
|
15
|
+
a: CursorPosition | undefined,
|
|
16
|
+
b: CursorPosition | undefined,
|
|
17
|
+
): boolean => a?.x !== b?.x || a?.y !== b?.y;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Build escape sequence to move cursor from the bottom of the output to the target position and show it.
|
|
21
|
+
|
|
22
|
+
`bottomLine` is the row the renderer left the cursor on, counted from the top of the output.
|
|
23
|
+
That is always `lines.length - 1` for `lines = str.split('\n')`, whether or not the output ends
|
|
24
|
+
with a newline:
|
|
25
|
+
|
|
26
|
+
- With a trailing newline, `split` yields one extra empty element and the renderer stops just
|
|
27
|
+
past the last visible line — which is `lines.length - 1`.
|
|
28
|
+
- Without one, there is no extra element and the renderer deliberately stops on the last visible
|
|
29
|
+
line instead of moving past it — which is also `lines.length - 1`.
|
|
30
|
+
|
|
31
|
+
This is the same row basis `buildReturnToBottom` measures from, so the two stay in step.
|
|
32
|
+
*/
|
|
33
|
+
export const buildCursorSuffix = (
|
|
34
|
+
bottomLine: number,
|
|
35
|
+
cursorPosition: CursorPosition | undefined,
|
|
36
|
+
): string => {
|
|
37
|
+
if (!cursorPosition) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const moveUp = bottomLine - cursorPosition.y;
|
|
42
|
+
return (
|
|
43
|
+
(moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : "") +
|
|
44
|
+
ansiEscapes.cursorTo(cursorPosition.x) +
|
|
45
|
+
showCursorEscape
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
Build escape sequence to move cursor from previousCursorPosition back to the bottom of output.
|
|
51
|
+
This must be done before eraseLines or any operation that assumes cursor is at the bottom.
|
|
52
|
+
*/
|
|
53
|
+
export const buildReturnToBottom = (
|
|
54
|
+
previousLineCount: number,
|
|
55
|
+
previousCursorPosition: CursorPosition | undefined,
|
|
56
|
+
): string => {
|
|
57
|
+
if (!previousCursorPosition) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// PreviousLineCount is the raw `split('\n')` length, so `previousLineCount - 1`
|
|
62
|
+
// is the row the cursor was left on regardless of a trailing newline — the same
|
|
63
|
+
// basis `buildCursorSuffix` takes as its `bottomLine`.
|
|
64
|
+
const down = previousLineCount - 1 - previousCursorPosition.y;
|
|
65
|
+
return (down > 0 ? ansiEscapes.cursorDown(down) : "") + ansiEscapes.cursorTo(0);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CursorOnlyInput = {
|
|
69
|
+
cursorWasShown: boolean;
|
|
70
|
+
previousLineCount: number;
|
|
71
|
+
previousCursorPosition: CursorPosition | undefined;
|
|
72
|
+
cursorPosition: CursorPosition | undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
Build the escape sequence for cursor-only updates (output unchanged, cursor moved).
|
|
77
|
+
Hides cursor if it was previously shown, returns to bottom, then repositions.
|
|
78
|
+
|
|
79
|
+
`buildReturnToBottom` has just placed the cursor on row `previousLineCount - 1`, so the
|
|
80
|
+
suffix measures from there rather than recomputing the row from the output.
|
|
81
|
+
*/
|
|
82
|
+
export const buildCursorOnlySequence = (input: CursorOnlyInput): string => {
|
|
83
|
+
const hidePrefix = input.cursorWasShown ? hideCursorEscape : "";
|
|
84
|
+
const returnToBottom = buildReturnToBottom(input.previousLineCount, input.previousCursorPosition);
|
|
85
|
+
const cursorSuffix = buildCursorSuffix(input.previousLineCount - 1, input.cursorPosition);
|
|
86
|
+
return hidePrefix + returnToBottom + cursorSuffix;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
Build the prefix that hides cursor and returns to bottom before erasing or rewriting.
|
|
91
|
+
Returns empty string if cursor was not shown.
|
|
92
|
+
*/
|
|
93
|
+
export const buildReturnToBottomPrefix = (
|
|
94
|
+
cursorWasShown: boolean,
|
|
95
|
+
previousLineCount: number,
|
|
96
|
+
previousCursorPosition: CursorPosition | undefined,
|
|
97
|
+
): string => {
|
|
98
|
+
if (!cursorWasShown) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return hideCursorEscape + buildReturnToBottom(previousLineCount, previousCursorPosition);
|
|
103
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Ignoring missing types error to avoid adding another dependency for this hack to work
|
|
2
|
+
import ws from "ws";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
5
|
+
const customGlobal = globalThis as any;
|
|
6
|
+
|
|
7
|
+
// These things must exist before importing `react-devtools-core`
|
|
8
|
+
// Using ||= intentionally to set falsy values, not just null/undefined
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
11
|
+
customGlobal.WebSocket ||= ws;
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
14
|
+
customGlobal.window ||= globalThis;
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
17
|
+
customGlobal.self ||= globalThis;
|
|
18
|
+
|
|
19
|
+
// Filter out Ink's internal components from devtools for a cleaner view.
|
|
20
|
+
// Also, ince `react-devtools-shared` package isn't published on npm, we can't
|
|
21
|
+
// use its types, that's why there are hard-coded values in `type` fields below.
|
|
22
|
+
// See https://github.com/facebook/react/blob/edf6eac8a181860fd8a2d076a43806f1237495a1/packages/react-devtools-shared/src/types.js#L24
|
|
23
|
+
customGlobal.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [
|
|
24
|
+
{
|
|
25
|
+
// ComponentFilterElementType
|
|
26
|
+
type: 1,
|
|
27
|
+
// ElementTypeHostComponent
|
|
28
|
+
value: 7,
|
|
29
|
+
isEnabled: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
// ComponentFilterDisplayName
|
|
33
|
+
type: 2,
|
|
34
|
+
value: "InternalApp",
|
|
35
|
+
isEnabled: true,
|
|
36
|
+
isValid: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// ComponentFilterDisplayName
|
|
40
|
+
type: 2,
|
|
41
|
+
value: "InternalAppContext",
|
|
42
|
+
isEnabled: true,
|
|
43
|
+
isValid: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
// ComponentFilterDisplayName
|
|
47
|
+
type: 2,
|
|
48
|
+
value: "InternalStdoutContext",
|
|
49
|
+
isEnabled: true,
|
|
50
|
+
isValid: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// ComponentFilterDisplayName
|
|
54
|
+
type: 2,
|
|
55
|
+
value: "InternalStderrContext",
|
|
56
|
+
isEnabled: true,
|
|
57
|
+
isValid: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
// ComponentFilterDisplayName
|
|
61
|
+
type: 2,
|
|
62
|
+
value: "InternalStdinContext",
|
|
63
|
+
isEnabled: true,
|
|
64
|
+
isValid: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// ComponentFilterDisplayName
|
|
68
|
+
type: 2,
|
|
69
|
+
value: "InternalFocusContext",
|
|
70
|
+
isEnabled: true,
|
|
71
|
+
isValid: true,
|
|
72
|
+
},
|
|
73
|
+
];
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable import-x/order */
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line import-x/no-unassigned-import
|
|
4
|
+
import "./devtools-window-polyfill.ts";
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6
|
+
// @ts-expect-error
|
|
7
|
+
import devtools from "react-devtools-core";
|
|
8
|
+
import WebSocket from "ws";
|
|
9
|
+
|
|
10
|
+
const isDevToolsReachable = async (): Promise<boolean> =>
|
|
11
|
+
new Promise((resolve) => {
|
|
12
|
+
const socket = new WebSocket("ws://localhost:8097");
|
|
13
|
+
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
socket.terminate();
|
|
16
|
+
resolve(false);
|
|
17
|
+
}, 2000);
|
|
18
|
+
// Don't let the timeout keep the process alive on its own
|
|
19
|
+
timeout.unref();
|
|
20
|
+
|
|
21
|
+
socket.on("open", () => {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
socket.terminate();
|
|
24
|
+
resolve(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
socket.on("error", () => {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
socket.terminate();
|
|
30
|
+
resolve(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (await isDevToolsReachable()) {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
36
|
+
(devtools as any).initialize();
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
38
|
+
(devtools as any).connectToDevTools();
|
|
39
|
+
} else {
|
|
40
|
+
console.warn(
|
|
41
|
+
"SIGIL_DEV is set to true, but the React DevTools server is not running. Start it with:\n\n$ npx react-devtools\n",
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { measureText } from "./measure-text.ts";
|
|
2
|
+
import { type OutputTransformer } from "./render-node-to-output.ts";
|
|
3
|
+
import { squashTextNodes } from "./squash-text-nodes.ts";
|
|
4
|
+
import { type Styles } from "./styles.ts";
|
|
5
|
+
import { wrapText } from "./wrap-text.ts";
|
|
6
|
+
import { Yoga, type Node as YogaNode } from "./yoga/index.ts";
|
|
7
|
+
|
|
8
|
+
type InkNode = {
|
|
9
|
+
parentNode: DOMElement | undefined;
|
|
10
|
+
yogaNode?: YogaNode;
|
|
11
|
+
internal_static?: boolean;
|
|
12
|
+
style: Styles;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type LayoutListener = () => void;
|
|
16
|
+
|
|
17
|
+
export type TextName = "#text";
|
|
18
|
+
export type ElementNames = "ink-root" | "ink-box" | "ink-text" | "ink-virtual-text";
|
|
19
|
+
|
|
20
|
+
export type NodeNames = ElementNames | TextName;
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
23
|
+
export type DOMElement = {
|
|
24
|
+
nodeName: ElementNames;
|
|
25
|
+
attributes: Record<string, DOMNodeAttribute>;
|
|
26
|
+
childNodes: DOMNode[];
|
|
27
|
+
internal_transform?: OutputTransformer;
|
|
28
|
+
|
|
29
|
+
internal_accessibility?: {
|
|
30
|
+
role?:
|
|
31
|
+
| "button"
|
|
32
|
+
| "checkbox"
|
|
33
|
+
| "combobox"
|
|
34
|
+
| "list"
|
|
35
|
+
| "listbox"
|
|
36
|
+
| "listitem"
|
|
37
|
+
| "menu"
|
|
38
|
+
| "menuitem"
|
|
39
|
+
| "option"
|
|
40
|
+
| "progressbar"
|
|
41
|
+
| "radio"
|
|
42
|
+
| "radiogroup"
|
|
43
|
+
| "tab"
|
|
44
|
+
| "tablist"
|
|
45
|
+
| "table"
|
|
46
|
+
| "textbox"
|
|
47
|
+
| "timer"
|
|
48
|
+
| "toolbar";
|
|
49
|
+
state?: {
|
|
50
|
+
busy?: boolean;
|
|
51
|
+
checked?: boolean;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
expanded?: boolean;
|
|
54
|
+
multiline?: boolean;
|
|
55
|
+
multiselectable?: boolean;
|
|
56
|
+
readonly?: boolean;
|
|
57
|
+
required?: boolean;
|
|
58
|
+
selected?: boolean;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Internal properties
|
|
63
|
+
isStaticDirty?: boolean;
|
|
64
|
+
staticNode?: DOMElement;
|
|
65
|
+
// Tracks the previous commit's `staticNode` so the reconciler can detect identity changes (mount, unmount, key-driven remount) and reset `fullStaticOutput`.
|
|
66
|
+
previousStaticNode?: DOMElement;
|
|
67
|
+
onComputeLayout?: () => void;
|
|
68
|
+
onRender?: () => void;
|
|
69
|
+
onImmediateRender?: () => void;
|
|
70
|
+
onStaticChange?: () => void;
|
|
71
|
+
internal_layoutListeners?: Set<LayoutListener>;
|
|
72
|
+
} & InkNode;
|
|
73
|
+
|
|
74
|
+
export type TextNode = {
|
|
75
|
+
nodeName: TextName;
|
|
76
|
+
nodeValue: string;
|
|
77
|
+
} & InkNode;
|
|
78
|
+
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
80
|
+
export type DOMNode<T = { nodeName: NodeNames }> = T extends {
|
|
81
|
+
nodeName: infer U;
|
|
82
|
+
}
|
|
83
|
+
? U extends "#text"
|
|
84
|
+
? TextNode
|
|
85
|
+
: DOMElement
|
|
86
|
+
: never;
|
|
87
|
+
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
89
|
+
export type DOMNodeAttribute = boolean | string | number;
|
|
90
|
+
|
|
91
|
+
export const createNode = (nodeName: ElementNames): DOMElement => {
|
|
92
|
+
const node: DOMElement = {
|
|
93
|
+
nodeName,
|
|
94
|
+
style: {},
|
|
95
|
+
attributes: {},
|
|
96
|
+
childNodes: [],
|
|
97
|
+
parentNode: undefined,
|
|
98
|
+
yogaNode: nodeName === "ink-virtual-text" ? undefined : Yoga.Node.create(),
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
100
|
+
internal_accessibility: {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (nodeName === "ink-text") {
|
|
104
|
+
node.yogaNode?.setMeasureFunc(measureTextNode.bind(null, node));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return node;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const appendChildNode = (node: DOMElement, childNode: DOMElement): void => {
|
|
111
|
+
if (childNode.parentNode) {
|
|
112
|
+
removeChildNode(childNode.parentNode, childNode);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
childNode.parentNode = node;
|
|
116
|
+
node.childNodes.push(childNode);
|
|
117
|
+
|
|
118
|
+
if (childNode.yogaNode) {
|
|
119
|
+
node.yogaNode?.insertChild(childNode.yogaNode, node.yogaNode.getChildCount());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
|
|
123
|
+
markNodeAsDirty(node);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const insertBeforeNode = (
|
|
128
|
+
node: DOMElement,
|
|
129
|
+
newChildNode: DOMNode,
|
|
130
|
+
beforeChildNode: DOMNode,
|
|
131
|
+
): void => {
|
|
132
|
+
if (newChildNode.parentNode) {
|
|
133
|
+
removeChildNode(newChildNode.parentNode, newChildNode);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
newChildNode.parentNode = node;
|
|
137
|
+
|
|
138
|
+
const index = node.childNodes.indexOf(beforeChildNode);
|
|
139
|
+
if (index >= 0) {
|
|
140
|
+
node.childNodes.splice(index, 0, newChildNode);
|
|
141
|
+
if (newChildNode.yogaNode) {
|
|
142
|
+
node.yogaNode?.insertChild(newChildNode.yogaNode, index);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
node.childNodes.push(newChildNode);
|
|
146
|
+
|
|
147
|
+
if (newChildNode.yogaNode) {
|
|
148
|
+
node.yogaNode?.insertChild(newChildNode.yogaNode, node.yogaNode.getChildCount());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
|
|
153
|
+
markNodeAsDirty(node);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const removeChildNode = (node: DOMElement, removeNode: DOMNode): void => {
|
|
158
|
+
if (removeNode.yogaNode) {
|
|
159
|
+
removeNode.parentNode?.yogaNode?.removeChild(removeNode.yogaNode);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
removeNode.parentNode = undefined;
|
|
163
|
+
|
|
164
|
+
const index = node.childNodes.indexOf(removeNode);
|
|
165
|
+
if (index >= 0) {
|
|
166
|
+
node.childNodes.splice(index, 1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
|
|
170
|
+
markNodeAsDirty(node);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const nullifyYogaNodes = (node: DOMNode): void => {
|
|
175
|
+
node.yogaNode = undefined;
|
|
176
|
+
|
|
177
|
+
if (node.nodeName !== "#text") {
|
|
178
|
+
for (const childNode of node.childNodes) {
|
|
179
|
+
nullifyYogaNodes(childNode);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
Detach a removed subtree from the layout engine: drop the measure callback
|
|
186
|
+
and null the `yogaNode` reference on every DOM node within it.
|
|
187
|
+
|
|
188
|
+
Nulling the references makes every `?.yogaNode` guard in the codebase
|
|
189
|
+
effective for removed nodes, turns lingering access into a safe no-op (see
|
|
190
|
+
QwenLM/qwen-code#6820), and lets the garbage collector reclaim the Yoga
|
|
191
|
+
tree along with its closures.
|
|
192
|
+
*/
|
|
193
|
+
export const detachYogaSubtree = (removeNode: DOMNode): void => {
|
|
194
|
+
removeNode.yogaNode?.unsetMeasureFunc();
|
|
195
|
+
nullifyYogaNodes(removeNode);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const setAttribute = (node: DOMElement, key: string, value: DOMNodeAttribute): void => {
|
|
199
|
+
if (key === "internal_accessibility") {
|
|
200
|
+
node.internal_accessibility = value as DOMElement["internal_accessibility"];
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
node.attributes[key] = value;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const setStyle = (node: DOMNode, style?: Styles): void => {
|
|
208
|
+
// Rendering code assumes style is always an object.
|
|
209
|
+
node.style = style ?? {};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const createTextNode = (text: string): TextNode => {
|
|
213
|
+
const node: TextNode = {
|
|
214
|
+
nodeName: "#text",
|
|
215
|
+
nodeValue: text,
|
|
216
|
+
yogaNode: undefined,
|
|
217
|
+
parentNode: undefined,
|
|
218
|
+
style: {},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
setTextNodeValue(node, text);
|
|
222
|
+
|
|
223
|
+
return node;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const measureTextNode = function (node: DOMNode, width: number): { width: number; height: number } {
|
|
227
|
+
const text = node.nodeName === "#text" ? node.nodeValue : squashTextNodes(node);
|
|
228
|
+
|
|
229
|
+
const dimensions = measureText(text);
|
|
230
|
+
|
|
231
|
+
// Text fits into container, no need to wrap
|
|
232
|
+
if (dimensions.width <= width) {
|
|
233
|
+
return dimensions;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// This is happening when <Box> is shrinking child nodes and Yoga asks
|
|
237
|
+
// if we can fit this text node in a <1px space, so we just tell Yoga "no"
|
|
238
|
+
if (dimensions.width >= 1 && width > 0 && width < 1) {
|
|
239
|
+
return dimensions;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const textWrap = node.style?.textWrap ?? "wrap";
|
|
243
|
+
const wrappedText = wrapText(text, width, textWrap);
|
|
244
|
+
|
|
245
|
+
return measureText(wrappedText);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const findClosestYogaNode = (node?: DOMNode): YogaNode | undefined => {
|
|
249
|
+
if (!node?.parentNode) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return node.yogaNode ?? findClosestYogaNode(node.parentNode);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const markNodeAsDirty = (node?: DOMNode): void => {
|
|
257
|
+
// Mark closest Yoga node as dirty to measure text dimensions again
|
|
258
|
+
const yogaNode = findClosestYogaNode(node);
|
|
259
|
+
yogaNode?.markDirty();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const setTextNodeValue = (node: TextNode, text: string): void => {
|
|
263
|
+
if (typeof text !== "string") {
|
|
264
|
+
text = String(text);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
node.nodeValue = text;
|
|
268
|
+
markNodeAsDirty(node);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const addLayoutListener = (rootNode: DOMElement, listener: LayoutListener): (() => void) => {
|
|
272
|
+
if (rootNode.nodeName !== "ink-root") {
|
|
273
|
+
return () => {};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
rootNode.internal_layoutListeners ??= new Set();
|
|
277
|
+
rootNode.internal_layoutListeners.add(listener);
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
rootNode.internal_layoutListeners?.delete(listener);
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export const emitLayoutListeners = (rootNode: DOMElement): void => {
|
|
285
|
+
if (rootNode.nodeName !== "ink-root" || !rootNode.internal_layoutListeners) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const listener of rootNode.internal_layoutListeners) {
|
|
290
|
+
listener();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Yoga, type Node as YogaNode } from "./yoga/index.ts";
|
|
2
|
+
|
|
3
|
+
export const getMaxWidth = (yogaNode: YogaNode) => {
|
|
4
|
+
return (
|
|
5
|
+
yogaNode.getComputedWidth() -
|
|
6
|
+
yogaNode.getComputedPadding(Yoga.EDGE_LEFT) -
|
|
7
|
+
yogaNode.getComputedPadding(Yoga.EDGE_RIGHT) -
|
|
8
|
+
yogaNode.getComputedBorder(Yoga.EDGE_LEFT) -
|
|
9
|
+
yogaNode.getComputedBorder(Yoga.EDGE_RIGHT)
|
|
10
|
+
);
|
|
11
|
+
};
|