@svelterm/core 0.1.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +425 -0
- package/README.md +42 -29
- package/dist/src/cli/build.d.ts +13 -0
- package/dist/src/cli/build.js +119 -0
- package/dist/src/cli/bundle.d.ts +25 -0
- package/dist/src/cli/bundle.js +61 -0
- package/dist/src/cli/dev.d.ts +10 -0
- package/dist/src/cli/dev.js +152 -0
- package/dist/src/cli/devtools.d.ts +9 -0
- package/dist/src/cli/devtools.js +47 -0
- package/dist/src/cli/init.d.ts +8 -0
- package/dist/src/cli/init.js +153 -0
- package/dist/src/cli/main.d.ts +9 -0
- package/dist/src/cli/main.js +52 -0
- package/dist/src/cli/svt-bin.d.ts +2 -0
- package/dist/src/cli/svt-bin.js +6 -0
- package/dist/src/cli/svt.d.ts +14 -0
- package/dist/src/cli/svt.js +76 -0
- package/dist/src/components/text-buffer.js +8 -5
- package/dist/src/css/animation-runner.d.ts +15 -6
- package/dist/src/css/animation-runner.js +80 -29
- package/dist/src/css/animation.d.ts +12 -0
- package/dist/src/css/animation.js +21 -0
- package/dist/src/css/calc.js +4 -3
- package/dist/src/css/color.d.ts +19 -0
- package/dist/src/css/color.js +371 -62
- package/dist/src/css/compute.d.ts +30 -3
- package/dist/src/css/compute.js +272 -33
- package/dist/src/css/defaults.d.ts +1 -1
- package/dist/src/css/defaults.js +9 -0
- package/dist/src/css/easing.d.ts +9 -0
- package/dist/src/css/easing.js +95 -0
- package/dist/src/css/incremental.d.ts +1 -1
- package/dist/src/css/incremental.js +2 -2
- package/dist/src/css/interpolate.d.ts +13 -0
- package/dist/src/css/interpolate.js +41 -0
- package/dist/src/css/parser.js +59 -3
- package/dist/src/css/pseudo-elements.d.ts +9 -0
- package/dist/src/css/pseudo-elements.js +97 -0
- package/dist/src/css/selector.d.ts +17 -2
- package/dist/src/css/selector.js +128 -13
- package/dist/src/css/specificity.js +17 -6
- package/dist/src/css/values.d.ts +6 -1
- package/dist/src/css/values.js +13 -6
- package/dist/src/debug/context.d.ts +13 -0
- package/dist/src/debug/context.js +11 -0
- package/dist/src/debug/css.d.ts +12 -0
- package/dist/src/debug/css.js +28 -0
- package/dist/src/debug/dom.d.ts +17 -0
- package/dist/src/debug/dom.js +92 -0
- package/dist/src/devtools/DevTools.compiled.js +327 -0
- package/dist/src/devtools/DevTools.css.js +1 -0
- package/dist/src/devtools/client.d.ts +36 -0
- package/dist/src/devtools/client.js +76 -0
- package/dist/src/framelog.d.ts +54 -0
- package/dist/src/framelog.js +99 -0
- package/dist/src/headless.js +12 -4
- package/dist/src/index.d.ts +65 -3
- package/dist/src/index.js +609 -81
- package/dist/src/input/checkable.d.ts +8 -0
- package/dist/src/input/checkable.js +66 -0
- package/dist/src/input/details.d.ts +6 -0
- package/dist/src/input/details.js +34 -0
- package/dist/src/input/focus.d.ts +6 -0
- package/dist/src/input/focus.js +27 -9
- package/dist/src/input/keyboard.d.ts +2 -2
- package/dist/src/input/keyboard.js +32 -5
- package/dist/src/input/label.d.ts +8 -0
- package/dist/src/input/label.js +53 -0
- package/dist/src/input/modal.d.ts +9 -0
- package/dist/src/input/modal.js +28 -0
- package/dist/src/input/mouse.d.ts +2 -2
- package/dist/src/input/mouse.js +15 -2
- package/dist/src/input/select.d.ts +12 -0
- package/dist/src/input/select.js +63 -0
- package/dist/src/input/selection.d.ts +48 -0
- package/dist/src/input/selection.js +150 -0
- package/dist/src/layout/engine.d.ts +2 -0
- package/dist/src/layout/engine.js +1084 -142
- package/dist/src/layout/flex.js +4 -4
- package/dist/src/layout/size.js +3 -2
- package/dist/src/layout/text.d.ts +3 -2
- package/dist/src/layout/text.js +96 -17
- package/dist/src/layout/unicode.d.ts +20 -0
- package/dist/src/layout/unicode.js +121 -0
- package/dist/src/render/animation-clock.d.ts +51 -0
- package/dist/src/render/animation-clock.js +213 -0
- package/dist/src/render/ansi-text.d.ts +26 -0
- package/dist/src/render/ansi-text.js +131 -0
- package/dist/src/render/ansi.d.ts +18 -0
- package/dist/src/render/ansi.js +64 -19
- package/dist/src/render/border.js +166 -17
- package/dist/src/render/buffer.d.ts +1 -0
- package/dist/src/render/buffer.js +5 -2
- package/dist/src/render/color-depth.d.ts +8 -0
- package/dist/src/render/color-depth.js +59 -0
- package/dist/src/render/context.d.ts +1 -0
- package/dist/src/render/context.js +17 -21
- package/dist/src/render/cursor-emit.d.ts +18 -0
- package/dist/src/render/cursor-emit.js +50 -0
- package/dist/src/render/diff.d.ts +12 -0
- package/dist/src/render/diff.js +120 -0
- package/dist/src/render/generation.d.ts +9 -0
- package/dist/src/render/generation.js +14 -0
- package/dist/src/render/graphics-layer.d.ts +27 -0
- package/dist/src/render/graphics-layer.js +86 -0
- package/dist/src/render/image.d.ts +27 -0
- package/dist/src/render/image.js +113 -0
- package/dist/src/render/incremental-paint.d.ts +7 -3
- package/dist/src/render/incremental-paint.js +52 -79
- package/dist/src/render/inline.d.ts +59 -0
- package/dist/src/render/inline.js +219 -0
- package/dist/src/render/kitty-graphics.d.ts +24 -0
- package/dist/src/render/kitty-graphics.js +58 -0
- package/dist/src/render/paint-text.js +68 -22
- package/dist/src/render/paint.d.ts +8 -1
- package/dist/src/render/paint.js +328 -30
- package/dist/src/render/png.d.ts +13 -0
- package/dist/src/render/png.js +145 -0
- package/dist/src/render/scrollbar.d.ts +8 -2
- package/dist/src/render/scrollbar.js +71 -14
- package/dist/src/render/snapshot.js +3 -1
- package/dist/src/renderer/default.d.ts +7 -0
- package/dist/src/renderer/default.js +11 -0
- package/dist/src/renderer/index.d.ts +8 -2
- package/dist/src/renderer/index.js +4 -2
- package/dist/src/renderer/node.d.ts +109 -0
- package/dist/src/renderer/node.js +165 -1
- package/dist/src/terminal/capabilities.d.ts +33 -0
- package/dist/src/terminal/capabilities.js +66 -0
- package/dist/src/terminal/clipboard.d.ts +9 -0
- package/dist/src/terminal/clipboard.js +39 -0
- package/dist/src/terminal/io.d.ts +82 -0
- package/dist/src/terminal/io.js +155 -0
- package/dist/src/terminal/screen.d.ts +3 -10
- package/dist/src/terminal/screen.js +5 -28
- package/dist/src/terminal/stdin-router.d.ts +8 -5
- package/dist/src/terminal/stdin-router.js +22 -11
- package/dist/src/utils/node-map.d.ts +24 -0
- package/dist/src/utils/node-map.js +75 -0
- package/dist/src/vite/config.d.ts +62 -0
- package/dist/src/vite/config.js +191 -0
- package/docs/compatibility.md +67 -0
- package/docs/debug/devtools.md +40 -0
- package/docs/debug/svt.md +50 -0
- package/docs/distribution.md +106 -0
- package/docs/elements.md +120 -0
- package/docs/getting-started.md +177 -0
- package/docs/guide/css.md +187 -0
- package/docs/guide/input.md +143 -0
- package/docs/guide/layout.md +171 -0
- package/docs/guide/theming.md +94 -0
- package/docs/how-it-works.md +115 -0
- package/docs/inline-mode.md +77 -0
- package/docs/layout.md +106 -0
- package/docs/motion.md +91 -0
- package/docs/reference/README.md +65 -0
- package/docs/reference/css/properties/border-corner.md +82 -0
- package/docs/reference/css/properties/border-style.md +168 -0
- package/docs/reference.md +226 -0
- package/docs/selectors.md +80 -0
- package/docs/terminal-css.md +149 -0
- package/docs/terminals.md +83 -0
- package/package.json +28 -7
|
@@ -15,7 +15,7 @@ const LAYOUT_PROPERTIES = [
|
|
|
15
15
|
* descendants using the same resolveNode function as full resolution.
|
|
16
16
|
* Variables are collected once from the full tree for consistency.
|
|
17
17
|
*/
|
|
18
|
-
export function resolveStylesIncremental(root, stylesheet, existingStyles, dirtyNodes, onResolve, onLayoutAffected) {
|
|
18
|
+
export function resolveStylesIncremental(root, stylesheet, existingStyles, dirtyNodes, onResolve, onLayoutAffected, scheme = 'dark') {
|
|
19
19
|
if (dirtyNodes.size === 0)
|
|
20
20
|
return existingStyles;
|
|
21
21
|
// Collect variables from the full tree — same as full resolution
|
|
@@ -28,7 +28,7 @@ export function resolveStylesIncremental(root, stylesheet, existingStyles, dirty
|
|
|
28
28
|
const oldStyle = existingStyles.get(node.id);
|
|
29
29
|
// Re-resolve this node and all its descendants using the
|
|
30
30
|
// same resolveNode function as full resolution
|
|
31
|
-
resolveNode(node, stylesheet, result, variables);
|
|
31
|
+
resolveNode(node, stylesheet, result, variables, scheme);
|
|
32
32
|
onResolve?.(node.id);
|
|
33
33
|
const newStyle = result.get(node.id);
|
|
34
34
|
if (oldStyle && newStyle && onLayoutAffected && isLayoutAffecting(oldStyle, newStyle)) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value interpolation for animations and transitions. Colours mix in RGB
|
|
3
|
+
* space; endpoints are returned exactly so ANSI palette names survive at
|
|
4
|
+
* t=0 and t=1.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Mix two resolved colours (SGR names or #rrggbb) at t ∈ [0,1].
|
|
8
|
+
* Returns null when either endpoint has no RGB value (`default`) —
|
|
9
|
+
* callers fall back to a discrete switch.
|
|
10
|
+
*/
|
|
11
|
+
export declare function lerpColor(from: string, to: string, t: number): string | null;
|
|
12
|
+
/** Linear interpolation rounded to whole cells. */
|
|
13
|
+
export declare function lerpNumber(from: number, to: number, t: number): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value interpolation for animations and transitions. Colours mix in RGB
|
|
3
|
+
* space; endpoints are returned exactly so ANSI palette names survive at
|
|
4
|
+
* t=0 and t=1.
|
|
5
|
+
*/
|
|
6
|
+
/** Nominal xterm palette values for the SGR colour names we emit. */
|
|
7
|
+
const ANSI_RGB = {
|
|
8
|
+
black: [0, 0, 0], red: [205, 0, 0], green: [0, 205, 0], yellow: [205, 205, 0],
|
|
9
|
+
blue: [0, 0, 238], magenta: [205, 0, 205], cyan: [0, 205, 205], white: [229, 229, 229],
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Mix two resolved colours (SGR names or #rrggbb) at t ∈ [0,1].
|
|
13
|
+
* Returns null when either endpoint has no RGB value (`default`) —
|
|
14
|
+
* callers fall back to a discrete switch.
|
|
15
|
+
*/
|
|
16
|
+
export function lerpColor(from, to, t) {
|
|
17
|
+
if (t <= 0)
|
|
18
|
+
return from;
|
|
19
|
+
if (t >= 1)
|
|
20
|
+
return to;
|
|
21
|
+
const a = colorToRgb(from);
|
|
22
|
+
const b = colorToRgb(to);
|
|
23
|
+
if (!a || !b)
|
|
24
|
+
return null;
|
|
25
|
+
const channels = a.map((channel, i) => Math.round(channel + (b[i] - channel) * t));
|
|
26
|
+
return '#' + channels.map(c => c.toString(16).padStart(2, '0')).join('');
|
|
27
|
+
}
|
|
28
|
+
/** Linear interpolation rounded to whole cells. */
|
|
29
|
+
export function lerpNumber(from, to, t) {
|
|
30
|
+
return Math.round(from + (to - from) * t);
|
|
31
|
+
}
|
|
32
|
+
function colorToRgb(color) {
|
|
33
|
+
if (color.startsWith('#') && color.length >= 7) {
|
|
34
|
+
return [
|
|
35
|
+
parseInt(color.slice(1, 3), 16),
|
|
36
|
+
parseInt(color.slice(3, 5), 16),
|
|
37
|
+
parseInt(color.slice(5, 7), 16),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
return ANSI_RGB[color] ?? null;
|
|
41
|
+
}
|
package/dist/src/css/parser.js
CHANGED
|
@@ -171,14 +171,28 @@ function parseRule(css, start, rules, media, supports, container) {
|
|
|
171
171
|
pos = selectorEnd + 1;
|
|
172
172
|
const declarations = [];
|
|
173
173
|
while (pos < css.length) {
|
|
174
|
-
pos =
|
|
174
|
+
pos = skipWhitespaceAndComments(css, pos);
|
|
175
175
|
if (pos >= css.length || css[pos] === '}') {
|
|
176
176
|
pos++;
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
|
+
// Check for nested @media inside a rule block
|
|
180
|
+
if (css.substring(pos, pos + 6) === '@media') {
|
|
181
|
+
// Flush current declarations as a rule
|
|
182
|
+
if (selectors.length > 0 && declarations.length > 0) {
|
|
183
|
+
rules.push({ selectors: [...selectors], declarations: [...declarations], media, supports, container });
|
|
184
|
+
declarations.length = 0;
|
|
185
|
+
}
|
|
186
|
+
pos = parseNestedMediaBlock(css, pos, rules, selectors, supports, container);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
179
189
|
const colonPos = css.indexOf(':', pos);
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
const nextBrace = css.indexOf('{', pos);
|
|
191
|
+
// If a brace comes before a colon, this might be a nested selector — skip it
|
|
192
|
+
if (colonPos === -1 || (nextBrace !== -1 && nextBrace < colonPos)) {
|
|
193
|
+
pos = skipToClosingBrace(css, pos);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
182
196
|
const property = css.substring(pos, colonPos).trim();
|
|
183
197
|
const valueEnd = findValueEnd(css, colonPos + 1);
|
|
184
198
|
const value = css.substring(colonPos + 1, valueEnd).trim();
|
|
@@ -192,6 +206,48 @@ function parseRule(css, start, rules, media, supports, container) {
|
|
|
192
206
|
}
|
|
193
207
|
return pos;
|
|
194
208
|
}
|
|
209
|
+
/** Parse a nested @media block inside a selector block */
|
|
210
|
+
function parseNestedMediaBlock(css, start, rules, parentSelectors, supports, container) {
|
|
211
|
+
let pos = start + 6; // skip "@media"
|
|
212
|
+
pos = skipWhitespace(css, pos);
|
|
213
|
+
const bracePos = css.indexOf('{', pos);
|
|
214
|
+
if (bracePos === -1)
|
|
215
|
+
return skipToClosingBrace(css, pos);
|
|
216
|
+
const rawCondition = css.substring(pos, bracePos).trim();
|
|
217
|
+
const condition = rawCondition.startsWith('(') && !rawCondition.includes(') and (')
|
|
218
|
+
? rawCondition.slice(1, -1).trim()
|
|
219
|
+
: rawCondition;
|
|
220
|
+
pos = bracePos + 1;
|
|
221
|
+
// Parse declarations inside the nested @media block
|
|
222
|
+
const declarations = [];
|
|
223
|
+
while (pos < css.length) {
|
|
224
|
+
pos = skipWhitespaceAndComments(css, pos);
|
|
225
|
+
if (pos >= css.length || css[pos] === '}') {
|
|
226
|
+
pos++;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
const colonPos = css.indexOf(':', pos);
|
|
230
|
+
if (colonPos === -1)
|
|
231
|
+
break;
|
|
232
|
+
const property = css.substring(pos, colonPos).trim();
|
|
233
|
+
const valueEnd = findValueEnd(css, colonPos + 1);
|
|
234
|
+
const value = css.substring(colonPos + 1, valueEnd).trim();
|
|
235
|
+
declarations.push({ property, value });
|
|
236
|
+
pos = valueEnd;
|
|
237
|
+
if (pos < css.length && css[pos] === ';')
|
|
238
|
+
pos++;
|
|
239
|
+
}
|
|
240
|
+
if (parentSelectors.length > 0 && declarations.length > 0) {
|
|
241
|
+
rules.push({
|
|
242
|
+
selectors: [...parentSelectors],
|
|
243
|
+
declarations,
|
|
244
|
+
media: condition,
|
|
245
|
+
supports,
|
|
246
|
+
container,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return pos;
|
|
250
|
+
}
|
|
195
251
|
function skipToClosingBrace(css, pos) {
|
|
196
252
|
let depth = 0;
|
|
197
253
|
while (pos < css.length) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
import { CSSStyleSheet } from './parser.js';
|
|
3
|
+
import { type ResolvedStyle } from './compute.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve ::before/::after for one element: build the pseudo's style from
|
|
6
|
+
* matching rules, materialise (or drop) its synthetic box on the node, and
|
|
7
|
+
* record the style under the synthetic node's id.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolvePseudoElements(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, vars: Map<string, string>, scheme: 'dark' | 'light'): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
import { matchesSelector, splitPseudoElement } from './selector.js';
|
|
3
|
+
import { computeSpecificity, compareSpecificity } from './specificity.js';
|
|
4
|
+
import { resolveVar } from './variables.js';
|
|
5
|
+
import { defaultStyle, applyDeclaration } from './compute.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve ::before/::after for one element: build the pseudo's style from
|
|
8
|
+
* matching rules, materialise (or drop) its synthetic box on the node, and
|
|
9
|
+
* record the style under the synthetic node's id.
|
|
10
|
+
*/
|
|
11
|
+
export function resolvePseudoElements(node, stylesheet, styles, vars, scheme) {
|
|
12
|
+
node.pseudoBefore = syncPseudo(node, node.pseudoBefore, 'before', stylesheet, styles, vars, scheme);
|
|
13
|
+
node.pseudoAfter = syncPseudo(node, node.pseudoAfter, 'after', stylesheet, styles, vars, scheme);
|
|
14
|
+
}
|
|
15
|
+
function syncPseudo(host, existing, which, stylesheet, styles, vars, scheme) {
|
|
16
|
+
const declarations = collectPseudoDeclarations(host, which, stylesheet, vars);
|
|
17
|
+
const content = resolveContent(declarations, host);
|
|
18
|
+
if (content === null) {
|
|
19
|
+
if (existing)
|
|
20
|
+
styles.delete(existing.id);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// Pseudo boxes behave like spans: inline, inheriting visuals at paint time
|
|
24
|
+
const style = defaultStyle('span');
|
|
25
|
+
for (const decl of declarations) {
|
|
26
|
+
if (decl.property === 'content')
|
|
27
|
+
continue;
|
|
28
|
+
applyDeclaration(style, decl.property, decl.value, scheme);
|
|
29
|
+
}
|
|
30
|
+
const pseudoNode = existing ?? createPseudoNode(host);
|
|
31
|
+
pseudoNode.children[0].text = content;
|
|
32
|
+
styles.set(pseudoNode.id, style);
|
|
33
|
+
return pseudoNode;
|
|
34
|
+
}
|
|
35
|
+
function collectPseudoDeclarations(host, which, stylesheet, vars) {
|
|
36
|
+
const scored = [];
|
|
37
|
+
let order = 0;
|
|
38
|
+
for (const rule of stylesheet.rules) {
|
|
39
|
+
for (const selector of rule.selectors) {
|
|
40
|
+
const { base, pseudoElement } = splitPseudoElement(selector);
|
|
41
|
+
if (pseudoElement !== which)
|
|
42
|
+
continue;
|
|
43
|
+
if (base !== '' && !matchesSelector(host, base))
|
|
44
|
+
continue;
|
|
45
|
+
const specificity = computeSpecificity(selector);
|
|
46
|
+
for (const decl of rule.declarations) {
|
|
47
|
+
if (decl.property.startsWith('--'))
|
|
48
|
+
continue;
|
|
49
|
+
scored.push({
|
|
50
|
+
property: decl.property,
|
|
51
|
+
value: resolveVar(decl.value, vars),
|
|
52
|
+
specificity,
|
|
53
|
+
order: order++,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
scored.sort((a, b) => {
|
|
59
|
+
const specCmp = compareSpecificity(a.specificity, b.specificity);
|
|
60
|
+
return specCmp !== 0 ? specCmp : a.order - b.order;
|
|
61
|
+
});
|
|
62
|
+
return scored;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The winning `content` value rendered to text, or null when the pseudo
|
|
66
|
+
* generates no box (no content declaration, `none`/`normal`, or empty).
|
|
67
|
+
*/
|
|
68
|
+
function resolveContent(declarations, host) {
|
|
69
|
+
const winner = declarations.filter(d => d.property === 'content').pop();
|
|
70
|
+
if (!winner)
|
|
71
|
+
return null;
|
|
72
|
+
const text = parseContentValue(winner.value, host);
|
|
73
|
+
return text === '' ? null : text;
|
|
74
|
+
}
|
|
75
|
+
const CONTENT_TOKEN = /"([^"]*)"|'([^']*)'|attr\(\s*([^)\s]+)\s*\)/g;
|
|
76
|
+
/** content: a space-separated sequence of quoted strings and attr() lookups. */
|
|
77
|
+
function parseContentValue(value, host) {
|
|
78
|
+
const trimmed = value.trim();
|
|
79
|
+
if (trimmed === 'none' || trimmed === 'normal')
|
|
80
|
+
return '';
|
|
81
|
+
let text = '';
|
|
82
|
+
for (const match of trimmed.matchAll(CONTENT_TOKEN)) {
|
|
83
|
+
if (match[3] !== undefined)
|
|
84
|
+
text += host.attributes.get(match[3]) ?? '';
|
|
85
|
+
else
|
|
86
|
+
text += match[1] ?? match[2] ?? '';
|
|
87
|
+
}
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
function createPseudoNode(host) {
|
|
91
|
+
const pseudoNode = new TermNode('element', 'svt-pseudo');
|
|
92
|
+
pseudoNode.parent = host;
|
|
93
|
+
const textNode = new TermNode('text', '');
|
|
94
|
+
textNode.parent = pseudoNode;
|
|
95
|
+
pseudoNode.children.push(textNode);
|
|
96
|
+
return pseudoNode;
|
|
97
|
+
}
|
|
@@ -3,15 +3,30 @@ export interface ParsedSelector {
|
|
|
3
3
|
tag?: string;
|
|
4
4
|
id?: string;
|
|
5
5
|
classes: string[];
|
|
6
|
-
|
|
7
|
-
pseudoArg?: string;
|
|
6
|
+
pseudos: PseudoSelector[];
|
|
8
7
|
attributes: AttrSelector[];
|
|
9
8
|
universal?: boolean;
|
|
10
9
|
}
|
|
10
|
+
interface PseudoSelector {
|
|
11
|
+
name: string;
|
|
12
|
+
arg?: string;
|
|
13
|
+
}
|
|
14
|
+
type AttrOp = '=' | '^=' | '$=' | '*=' | '~=' | '|=';
|
|
11
15
|
interface AttrSelector {
|
|
12
16
|
name: string;
|
|
17
|
+
op?: AttrOp;
|
|
13
18
|
value?: string;
|
|
14
19
|
}
|
|
15
20
|
export declare function parseSelector(selector: string): ParsedSelector;
|
|
21
|
+
export type PseudoElement = 'before' | 'after';
|
|
22
|
+
/**
|
|
23
|
+
* Split a trailing ::before/::after (or legacy single-colon form) off a
|
|
24
|
+
* selector. Selectors with a pseudo-element style a synthetic box, never
|
|
25
|
+
* the host element itself.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitPseudoElement(selector: string): {
|
|
28
|
+
base: string;
|
|
29
|
+
pseudoElement: PseudoElement | null;
|
|
30
|
+
};
|
|
16
31
|
export declare function matchesSelector(node: TermNode, selector: string): boolean;
|
|
17
32
|
export {};
|
package/dist/src/css/selector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { hasBooleanAttribute } from '../renderer/node.js';
|
|
1
2
|
export function parseSelector(selector) {
|
|
2
|
-
const result = { classes: [], attributes: [] };
|
|
3
|
+
const result = { classes: [], pseudos: [], attributes: [] };
|
|
3
4
|
let pos = 0;
|
|
4
5
|
// Universal selector
|
|
5
6
|
if (pos < selector.length && selector[pos] === '*') {
|
|
@@ -40,7 +41,8 @@ export function parseSelector(selector) {
|
|
|
40
41
|
while (pos < selector.length && /[a-zA-Z0-9_-]/.test(selector[pos]))
|
|
41
42
|
pos++;
|
|
42
43
|
const name = selector.substring(start, pos);
|
|
43
|
-
// Functional pseudo-class: :not(...), :
|
|
44
|
+
// Functional pseudo-class: :not(...), :where(...), :is(...)
|
|
45
|
+
let arg;
|
|
44
46
|
if (pos < selector.length && selector[pos] === '(') {
|
|
45
47
|
pos++;
|
|
46
48
|
const argStart = pos;
|
|
@@ -53,10 +55,10 @@ export function parseSelector(selector) {
|
|
|
53
55
|
if (depth > 0)
|
|
54
56
|
pos++;
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
arg = selector.substring(argStart, pos).trim();
|
|
57
59
|
pos++; // skip closing )
|
|
58
60
|
}
|
|
59
|
-
result.
|
|
61
|
+
result.pseudos.push({ name, arg });
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
64
|
pos++;
|
|
@@ -66,12 +68,20 @@ export function parseSelector(selector) {
|
|
|
66
68
|
}
|
|
67
69
|
function parseAttrSelector(selector, pos) {
|
|
68
70
|
const nameStart = pos;
|
|
69
|
-
while (pos < selector.length &&
|
|
71
|
+
while (pos < selector.length && !'^$*~|=]'.includes(selector[pos]))
|
|
70
72
|
pos++;
|
|
71
73
|
const name = selector.substring(nameStart, pos).trim();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
let op;
|
|
75
|
+
if ('^$*~|'.includes(selector[pos]) && selector[pos + 1] === '=') {
|
|
76
|
+
op = (selector[pos] + '=');
|
|
77
|
+
pos += 2;
|
|
78
|
+
}
|
|
79
|
+
else if (selector[pos] === '=') {
|
|
80
|
+
op = '=';
|
|
81
|
+
pos++;
|
|
82
|
+
}
|
|
83
|
+
if (op) {
|
|
84
|
+
let value;
|
|
75
85
|
if (pos < selector.length && (selector[pos] === '"' || selector[pos] === "'")) {
|
|
76
86
|
const quote = selector[pos];
|
|
77
87
|
pos++;
|
|
@@ -81,16 +91,43 @@ function parseAttrSelector(selector, pos) {
|
|
|
81
91
|
value = selector.substring(valStart, pos);
|
|
82
92
|
pos++; // skip closing quote
|
|
83
93
|
}
|
|
94
|
+
else {
|
|
95
|
+
const valStart = pos;
|
|
96
|
+
while (pos < selector.length && selector[pos] !== ']')
|
|
97
|
+
pos++;
|
|
98
|
+
value = selector.substring(valStart, pos).trim();
|
|
99
|
+
}
|
|
84
100
|
while (pos < selector.length && selector[pos] !== ']')
|
|
85
101
|
pos++;
|
|
86
102
|
pos++; // skip ]
|
|
87
|
-
return { selector: { name, value }, end: pos };
|
|
103
|
+
return { selector: { name, op, value }, end: pos };
|
|
88
104
|
}
|
|
89
105
|
while (pos < selector.length && selector[pos] !== ']')
|
|
90
106
|
pos++;
|
|
91
107
|
pos++; // skip ]
|
|
92
108
|
return { selector: { name }, end: pos };
|
|
93
109
|
}
|
|
110
|
+
function attrValueMatches(actual, op, operand) {
|
|
111
|
+
switch (op) {
|
|
112
|
+
case '=': return actual === operand;
|
|
113
|
+
case '^=': return operand !== '' && actual.startsWith(operand);
|
|
114
|
+
case '$=': return operand !== '' && actual.endsWith(operand);
|
|
115
|
+
case '*=': return operand !== '' && actual.includes(operand);
|
|
116
|
+
case '~=': return operand !== '' && actual.split(/\s+/).includes(operand);
|
|
117
|
+
case '|=': return actual === operand || actual.startsWith(operand + '-');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Split a trailing ::before/::after (or legacy single-colon form) off a
|
|
122
|
+
* selector. Selectors with a pseudo-element style a synthetic box, never
|
|
123
|
+
* the host element itself.
|
|
124
|
+
*/
|
|
125
|
+
export function splitPseudoElement(selector) {
|
|
126
|
+
const match = /^(.*?)::?(before|after)$/.exec(selector.trim());
|
|
127
|
+
if (!match)
|
|
128
|
+
return { base: selector, pseudoElement: null };
|
|
129
|
+
return { base: match[1], pseudoElement: match[2] };
|
|
130
|
+
}
|
|
94
131
|
export function matchesSelector(node, selector) {
|
|
95
132
|
if (node.nodeType !== 'element')
|
|
96
133
|
return false;
|
|
@@ -233,13 +270,14 @@ function matchesParsed(node, parsed) {
|
|
|
233
270
|
return false;
|
|
234
271
|
}
|
|
235
272
|
for (const attr of parsed.attributes) {
|
|
236
|
-
|
|
273
|
+
const actual = node.attributes.get(attr.name);
|
|
274
|
+
if (actual === undefined)
|
|
237
275
|
return false;
|
|
238
|
-
if (attr.
|
|
276
|
+
if (attr.op !== undefined && !attrValueMatches(actual, attr.op, attr.value ?? ''))
|
|
239
277
|
return false;
|
|
240
278
|
}
|
|
241
|
-
|
|
242
|
-
if (!matchesPseudo(node,
|
|
279
|
+
for (const pseudo of parsed.pseudos) {
|
|
280
|
+
if (!matchesPseudo(node, pseudo.name, pseudo.arg))
|
|
243
281
|
return false;
|
|
244
282
|
}
|
|
245
283
|
return true;
|
|
@@ -251,13 +289,90 @@ function matchesPseudo(node, pseudo, arg) {
|
|
|
251
289
|
case 'hover': return node.attributes.get('data-hovered') === 'true';
|
|
252
290
|
case 'first-child': return isFirstChild(node);
|
|
253
291
|
case 'last-child': return isLastChild(node);
|
|
292
|
+
case 'only-child': return isFirstChild(node) && isLastChild(node);
|
|
293
|
+
case 'empty': return hasNoRenderedChildren(node);
|
|
294
|
+
case 'first-of-type':
|
|
295
|
+
return matchesNth(node, '1', { fromEnd: false, sameType: true });
|
|
296
|
+
case 'last-of-type':
|
|
297
|
+
return matchesNth(node, '1', { fromEnd: true, sameType: true });
|
|
298
|
+
case 'only-of-type':
|
|
299
|
+
return matchesNth(node, '1', { fromEnd: false, sameType: true })
|
|
300
|
+
&& matchesNth(node, '1', { fromEnd: true, sameType: true });
|
|
301
|
+
case 'checked': return hasBooleanAttribute(node, 'checked');
|
|
302
|
+
case 'disabled':
|
|
303
|
+
return isFormControl(node) && hasBooleanAttribute(node, 'disabled');
|
|
304
|
+
case 'enabled':
|
|
305
|
+
return isFormControl(node) && !hasBooleanAttribute(node, 'disabled');
|
|
254
306
|
case 'not':
|
|
255
307
|
if (!arg)
|
|
256
308
|
return false;
|
|
257
309
|
return !matchesParsed(node, parseSelector(arg));
|
|
310
|
+
case 'where':
|
|
311
|
+
case 'is':
|
|
312
|
+
if (!arg)
|
|
313
|
+
return false;
|
|
314
|
+
return matchesSelectorList(node, arg);
|
|
315
|
+
case 'nth-child':
|
|
316
|
+
return matchesNth(node, arg, { fromEnd: false, sameType: false });
|
|
317
|
+
case 'nth-last-child':
|
|
318
|
+
return matchesNth(node, arg, { fromEnd: true, sameType: false });
|
|
319
|
+
case 'nth-of-type':
|
|
320
|
+
return matchesNth(node, arg, { fromEnd: false, sameType: true });
|
|
321
|
+
case 'nth-last-of-type':
|
|
322
|
+
return matchesNth(node, arg, { fromEnd: true, sameType: true });
|
|
258
323
|
default: return false;
|
|
259
324
|
}
|
|
260
325
|
}
|
|
326
|
+
function matchesNth(node, arg, opts) {
|
|
327
|
+
if (!arg || !node.parent)
|
|
328
|
+
return false;
|
|
329
|
+
const siblings = node.parent.children.filter(c => c.nodeType === 'element' && (!opts.sameType || c.tag === node.tag));
|
|
330
|
+
const position = siblings.indexOf(node);
|
|
331
|
+
if (position < 0)
|
|
332
|
+
return false;
|
|
333
|
+
const index = opts.fromEnd ? siblings.length - position : position + 1;
|
|
334
|
+
const formula = parseNth(arg);
|
|
335
|
+
if (!formula)
|
|
336
|
+
return false;
|
|
337
|
+
const { a, b } = formula;
|
|
338
|
+
// index = a*n + b for some integer n >= 0
|
|
339
|
+
if (a === 0)
|
|
340
|
+
return index === b;
|
|
341
|
+
const n = (index - b) / a;
|
|
342
|
+
return n >= 0 && Number.isInteger(n);
|
|
343
|
+
}
|
|
344
|
+
/** Parse an An+B expression ("odd", "even", "3", "2n", "2n+1", "-n+3"). */
|
|
345
|
+
function parseNth(arg) {
|
|
346
|
+
const s = arg.trim().toLowerCase();
|
|
347
|
+
if (s === 'odd')
|
|
348
|
+
return { a: 2, b: 1 };
|
|
349
|
+
if (s === 'even')
|
|
350
|
+
return { a: 2, b: 0 };
|
|
351
|
+
const match = /^([+-]?\d*)n\s*(?:([+-])\s*(\d+))?$|^([+-]?\d+)$/.exec(s);
|
|
352
|
+
if (!match)
|
|
353
|
+
return null;
|
|
354
|
+
if (match[4] !== undefined)
|
|
355
|
+
return { a: 0, b: parseInt(match[4]) };
|
|
356
|
+
const aText = match[1];
|
|
357
|
+
const a = aText === '' || aText === '+' ? 1 : aText === '-' ? -1 : parseInt(aText);
|
|
358
|
+
const b = match[2] ? (match[2] === '-' ? -1 : 1) * parseInt(match[3]) : 0;
|
|
359
|
+
return { a, b };
|
|
360
|
+
}
|
|
361
|
+
/** Match a comma-separated list of compound selectors (the argument of :where()/:is()). */
|
|
362
|
+
function matchesSelectorList(node, list) {
|
|
363
|
+
return list.split(',').some(item => matchesParsed(node, parseSelector(item.trim())));
|
|
364
|
+
}
|
|
365
|
+
/** Tags that participate in :enabled/:disabled matching, as in browsers. */
|
|
366
|
+
const FORM_CONTROL_TAGS = new Set([
|
|
367
|
+
'input', 'button', 'select', 'textarea', 'option', 'optgroup', 'fieldset',
|
|
368
|
+
]);
|
|
369
|
+
function isFormControl(node) {
|
|
370
|
+
return FORM_CONTROL_TAGS.has(node.tag ?? '');
|
|
371
|
+
}
|
|
372
|
+
/** :empty — comments and zero-length text nodes are ignored, as in browsers. */
|
|
373
|
+
function hasNoRenderedChildren(node) {
|
|
374
|
+
return node.children.every(child => child.nodeType === 'comment' || (child.nodeType === 'text' && !child.text));
|
|
375
|
+
}
|
|
261
376
|
function isFirstChild(node) {
|
|
262
377
|
if (!node.parent)
|
|
263
378
|
return false;
|
|
@@ -31,8 +31,8 @@ export function computeSpecificity(selector) {
|
|
|
31
31
|
const nameStart = pos;
|
|
32
32
|
pos = skipName(selector, pos);
|
|
33
33
|
const name = selector.substring(nameStart, pos);
|
|
34
|
-
if (name === 'not' && pos < selector.length && selector[pos] === '(') {
|
|
35
|
-
// :not() specificity =
|
|
34
|
+
if ((name === 'not' || name === 'is' || name === 'where') && pos < selector.length && selector[pos] === '(') {
|
|
35
|
+
// :not()/:is() specificity = most specific argument; :where() = zero
|
|
36
36
|
pos++;
|
|
37
37
|
const argStart = pos;
|
|
38
38
|
let depth = 1;
|
|
@@ -46,10 +46,12 @@ export function computeSpecificity(selector) {
|
|
|
46
46
|
}
|
|
47
47
|
const arg = selector.substring(argStart, pos);
|
|
48
48
|
pos++; // skip )
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (name !== 'where') {
|
|
50
|
+
const argSpec = mostSpecificArgument(arg);
|
|
51
|
+
ids += argSpec[0];
|
|
52
|
+
classes += argSpec[1];
|
|
53
|
+
elements += argSpec[2];
|
|
54
|
+
}
|
|
53
55
|
}
|
|
54
56
|
else if (pos < selector.length && selector[pos] === '(') {
|
|
55
57
|
classes++; // other functional pseudo-class
|
|
@@ -69,6 +71,15 @@ export function computeSpecificity(selector) {
|
|
|
69
71
|
}
|
|
70
72
|
return [ids, classes, elements];
|
|
71
73
|
}
|
|
74
|
+
function mostSpecificArgument(list) {
|
|
75
|
+
let best = [0, 0, 0];
|
|
76
|
+
for (const item of list.split(',')) {
|
|
77
|
+
const spec = computeSpecificity(item.trim());
|
|
78
|
+
if (compareSpecificity(spec, best) > 0)
|
|
79
|
+
best = spec;
|
|
80
|
+
}
|
|
81
|
+
return best;
|
|
82
|
+
}
|
|
72
83
|
function skipName(selector, pos) {
|
|
73
84
|
while (pos < selector.length && /[a-zA-Z0-9_-]/.test(selector[pos]))
|
|
74
85
|
pos++;
|
package/dist/src/css/values.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ResolvedStyle } from './compute.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a length in cells, returning the unrounded number, or null when the
|
|
4
|
+
* value is not a cell/ch length (keywords like `stretch` end in "ch" too).
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseCellLength(value: string): number | null;
|
|
2
7
|
/**
|
|
3
8
|
* Parse a cell value from CSS. Accepts:
|
|
4
|
-
* - `5cell` → 5
|
|
9
|
+
* - `5cell` / `5ch` → 5
|
|
5
10
|
* - `0` → 0 (unitless zero is valid CSS)
|
|
6
11
|
* - Returns 0 for unrecognised values (browser-only units like px, em, rem)
|
|
7
12
|
*/
|
package/dist/src/css/values.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
+
/** A number with the cell unit or its browser-CSS alias ch (one character width). */
|
|
2
|
+
const CELL_LENGTH = /^([+-]?(?:\d+\.?\d*|\.\d+))(cell|ch)$/;
|
|
3
|
+
/**
|
|
4
|
+
* Parse a length in cells, returning the unrounded number, or null when the
|
|
5
|
+
* value is not a cell/ch length (keywords like `stretch` end in "ch" too).
|
|
6
|
+
*/
|
|
7
|
+
export function parseCellLength(value) {
|
|
8
|
+
const match = CELL_LENGTH.exec(value.trim());
|
|
9
|
+
return match ? parseFloat(match[1]) : null;
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* Parse a cell value from CSS. Accepts:
|
|
3
|
-
* - `5cell` → 5
|
|
13
|
+
* - `5cell` / `5ch` → 5
|
|
4
14
|
* - `0` → 0 (unitless zero is valid CSS)
|
|
5
15
|
* - Returns 0 for unrecognised values (browser-only units like px, em, rem)
|
|
6
16
|
*/
|
|
7
17
|
export function parseCellValue(value) {
|
|
8
18
|
if (value === '0')
|
|
9
19
|
return 0;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return isNaN(num) ? 0 : Math.round(num);
|
|
13
|
-
}
|
|
14
|
-
return 0;
|
|
20
|
+
const length = parseCellLength(value);
|
|
21
|
+
return length === null ? 0 : Math.round(length);
|
|
15
22
|
}
|
|
16
23
|
export function parseSizeValue(value) {
|
|
17
24
|
if (value === 'auto')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TermNode } from '../renderer/node.js';
|
|
2
|
+
import type { ResolvedStyle } from '../css/compute.js';
|
|
3
|
+
import type { LayoutBox } from '../layout/engine.js';
|
|
4
|
+
/** Live render state the inspection domains read from. */
|
|
5
|
+
export interface DebugContext {
|
|
6
|
+
root: TermNode;
|
|
7
|
+
styles: () => Map<number, ResolvedStyle> | undefined;
|
|
8
|
+
layout: () => Map<number, LayoutBox> | undefined;
|
|
9
|
+
/** Schedule a repaint after a domain mutates the tree. */
|
|
10
|
+
requestRender?: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Depth-first search for a node by id under the context root. */
|
|
13
|
+
export declare function findNodeById(root: TermNode, id: number): TermNode | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Depth-first search for a node by id under the context root. */
|
|
2
|
+
export function findNodeById(root, id) {
|
|
3
|
+
if (root.id === id)
|
|
4
|
+
return root;
|
|
5
|
+
for (const child of root.children) {
|
|
6
|
+
const found = findNodeById(child, id);
|
|
7
|
+
if (found)
|
|
8
|
+
return found;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS domain — computed style inspection over the debug protocol.
|
|
3
|
+
* Returns the resolved style svelterm actually used to paint a node.
|
|
4
|
+
*/
|
|
5
|
+
import { type DebugContext } from './context.js';
|
|
6
|
+
import type { DebugDomain } from './server.js';
|
|
7
|
+
export declare class CssDomain implements DebugDomain {
|
|
8
|
+
private ctx;
|
|
9
|
+
constructor(ctx: DebugContext);
|
|
10
|
+
handle(method: string, params: Record<string, any>): any;
|
|
11
|
+
private computedStyle;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS domain — computed style inspection over the debug protocol.
|
|
3
|
+
* Returns the resolved style svelterm actually used to paint a node.
|
|
4
|
+
*/
|
|
5
|
+
import { findNodeById } from './context.js';
|
|
6
|
+
export class CssDomain {
|
|
7
|
+
ctx;
|
|
8
|
+
constructor(ctx) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
handle(method, params) {
|
|
12
|
+
switch (method) {
|
|
13
|
+
case 'getComputedStyle':
|
|
14
|
+
return { style: this.computedStyle(params.nodeId) };
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(`CSS.${method} not implemented`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
computedStyle(nodeId) {
|
|
20
|
+
if (!findNodeById(this.ctx.root, nodeId))
|
|
21
|
+
throw new Error(`No node with id ${nodeId}`);
|
|
22
|
+
const style = this.ctx.styles()?.get(nodeId);
|
|
23
|
+
if (!style)
|
|
24
|
+
throw new Error(`No computed style for node ${nodeId}`);
|
|
25
|
+
// Structured-clone-safe copy (drop any functions/undefined)
|
|
26
|
+
return JSON.parse(JSON.stringify(style));
|
|
27
|
+
}
|
|
28
|
+
}
|