@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +42 -29
  3. package/dist/src/cli/build.d.ts +13 -0
  4. package/dist/src/cli/build.js +119 -0
  5. package/dist/src/cli/bundle.d.ts +25 -0
  6. package/dist/src/cli/bundle.js +61 -0
  7. package/dist/src/cli/dev.d.ts +10 -0
  8. package/dist/src/cli/dev.js +152 -0
  9. package/dist/src/cli/devtools.d.ts +9 -0
  10. package/dist/src/cli/devtools.js +47 -0
  11. package/dist/src/cli/init.d.ts +8 -0
  12. package/dist/src/cli/init.js +153 -0
  13. package/dist/src/cli/main.d.ts +9 -0
  14. package/dist/src/cli/main.js +52 -0
  15. package/dist/src/cli/svt-bin.d.ts +2 -0
  16. package/dist/src/cli/svt-bin.js +6 -0
  17. package/dist/src/cli/svt.d.ts +14 -0
  18. package/dist/src/cli/svt.js +76 -0
  19. package/dist/src/components/text-buffer.js +8 -5
  20. package/dist/src/css/animation-runner.d.ts +15 -6
  21. package/dist/src/css/animation-runner.js +80 -29
  22. package/dist/src/css/animation.d.ts +12 -0
  23. package/dist/src/css/animation.js +21 -0
  24. package/dist/src/css/calc.js +4 -3
  25. package/dist/src/css/color.d.ts +19 -0
  26. package/dist/src/css/color.js +371 -62
  27. package/dist/src/css/compute.d.ts +30 -3
  28. package/dist/src/css/compute.js +272 -33
  29. package/dist/src/css/defaults.d.ts +1 -1
  30. package/dist/src/css/defaults.js +9 -0
  31. package/dist/src/css/easing.d.ts +9 -0
  32. package/dist/src/css/easing.js +95 -0
  33. package/dist/src/css/incremental.d.ts +1 -1
  34. package/dist/src/css/incremental.js +2 -2
  35. package/dist/src/css/interpolate.d.ts +13 -0
  36. package/dist/src/css/interpolate.js +41 -0
  37. package/dist/src/css/parser.js +59 -3
  38. package/dist/src/css/pseudo-elements.d.ts +9 -0
  39. package/dist/src/css/pseudo-elements.js +97 -0
  40. package/dist/src/css/selector.d.ts +17 -2
  41. package/dist/src/css/selector.js +128 -13
  42. package/dist/src/css/specificity.js +17 -6
  43. package/dist/src/css/values.d.ts +6 -1
  44. package/dist/src/css/values.js +13 -6
  45. package/dist/src/debug/context.d.ts +13 -0
  46. package/dist/src/debug/context.js +11 -0
  47. package/dist/src/debug/css.d.ts +12 -0
  48. package/dist/src/debug/css.js +28 -0
  49. package/dist/src/debug/dom.d.ts +17 -0
  50. package/dist/src/debug/dom.js +92 -0
  51. package/dist/src/devtools/DevTools.compiled.js +327 -0
  52. package/dist/src/devtools/DevTools.css.js +1 -0
  53. package/dist/src/devtools/client.d.ts +36 -0
  54. package/dist/src/devtools/client.js +76 -0
  55. package/dist/src/framelog.d.ts +54 -0
  56. package/dist/src/framelog.js +99 -0
  57. package/dist/src/headless.js +12 -4
  58. package/dist/src/index.d.ts +65 -3
  59. package/dist/src/index.js +609 -81
  60. package/dist/src/input/checkable.d.ts +8 -0
  61. package/dist/src/input/checkable.js +66 -0
  62. package/dist/src/input/details.d.ts +6 -0
  63. package/dist/src/input/details.js +34 -0
  64. package/dist/src/input/focus.d.ts +6 -0
  65. package/dist/src/input/focus.js +27 -9
  66. package/dist/src/input/keyboard.d.ts +2 -2
  67. package/dist/src/input/keyboard.js +32 -5
  68. package/dist/src/input/label.d.ts +8 -0
  69. package/dist/src/input/label.js +53 -0
  70. package/dist/src/input/modal.d.ts +9 -0
  71. package/dist/src/input/modal.js +28 -0
  72. package/dist/src/input/mouse.d.ts +2 -2
  73. package/dist/src/input/mouse.js +15 -2
  74. package/dist/src/input/select.d.ts +12 -0
  75. package/dist/src/input/select.js +63 -0
  76. package/dist/src/input/selection.d.ts +48 -0
  77. package/dist/src/input/selection.js +150 -0
  78. package/dist/src/layout/engine.d.ts +2 -0
  79. package/dist/src/layout/engine.js +1084 -142
  80. package/dist/src/layout/flex.js +4 -4
  81. package/dist/src/layout/size.js +3 -2
  82. package/dist/src/layout/text.d.ts +3 -2
  83. package/dist/src/layout/text.js +96 -17
  84. package/dist/src/layout/unicode.d.ts +20 -0
  85. package/dist/src/layout/unicode.js +121 -0
  86. package/dist/src/render/animation-clock.d.ts +51 -0
  87. package/dist/src/render/animation-clock.js +213 -0
  88. package/dist/src/render/ansi-text.d.ts +26 -0
  89. package/dist/src/render/ansi-text.js +131 -0
  90. package/dist/src/render/ansi.d.ts +18 -0
  91. package/dist/src/render/ansi.js +64 -19
  92. package/dist/src/render/border.js +166 -17
  93. package/dist/src/render/buffer.d.ts +1 -0
  94. package/dist/src/render/buffer.js +5 -2
  95. package/dist/src/render/color-depth.d.ts +8 -0
  96. package/dist/src/render/color-depth.js +59 -0
  97. package/dist/src/render/context.d.ts +1 -0
  98. package/dist/src/render/context.js +17 -21
  99. package/dist/src/render/cursor-emit.d.ts +18 -0
  100. package/dist/src/render/cursor-emit.js +50 -0
  101. package/dist/src/render/diff.d.ts +12 -0
  102. package/dist/src/render/diff.js +120 -0
  103. package/dist/src/render/generation.d.ts +9 -0
  104. package/dist/src/render/generation.js +14 -0
  105. package/dist/src/render/graphics-layer.d.ts +27 -0
  106. package/dist/src/render/graphics-layer.js +86 -0
  107. package/dist/src/render/image.d.ts +27 -0
  108. package/dist/src/render/image.js +113 -0
  109. package/dist/src/render/incremental-paint.d.ts +7 -3
  110. package/dist/src/render/incremental-paint.js +52 -79
  111. package/dist/src/render/inline.d.ts +59 -0
  112. package/dist/src/render/inline.js +219 -0
  113. package/dist/src/render/kitty-graphics.d.ts +24 -0
  114. package/dist/src/render/kitty-graphics.js +58 -0
  115. package/dist/src/render/paint-text.js +68 -22
  116. package/dist/src/render/paint.d.ts +8 -1
  117. package/dist/src/render/paint.js +328 -30
  118. package/dist/src/render/png.d.ts +13 -0
  119. package/dist/src/render/png.js +145 -0
  120. package/dist/src/render/scrollbar.d.ts +8 -2
  121. package/dist/src/render/scrollbar.js +71 -14
  122. package/dist/src/render/snapshot.js +3 -1
  123. package/dist/src/renderer/default.d.ts +7 -0
  124. package/dist/src/renderer/default.js +11 -0
  125. package/dist/src/renderer/index.d.ts +8 -2
  126. package/dist/src/renderer/index.js +4 -2
  127. package/dist/src/renderer/node.d.ts +109 -0
  128. package/dist/src/renderer/node.js +165 -1
  129. package/dist/src/terminal/capabilities.d.ts +33 -0
  130. package/dist/src/terminal/capabilities.js +66 -0
  131. package/dist/src/terminal/clipboard.d.ts +9 -0
  132. package/dist/src/terminal/clipboard.js +39 -0
  133. package/dist/src/terminal/io.d.ts +82 -0
  134. package/dist/src/terminal/io.js +155 -0
  135. package/dist/src/terminal/screen.d.ts +3 -10
  136. package/dist/src/terminal/screen.js +5 -28
  137. package/dist/src/terminal/stdin-router.d.ts +8 -5
  138. package/dist/src/terminal/stdin-router.js +22 -11
  139. package/dist/src/utils/node-map.d.ts +24 -0
  140. package/dist/src/utils/node-map.js +75 -0
  141. package/dist/src/vite/config.d.ts +62 -0
  142. package/dist/src/vite/config.js +191 -0
  143. package/docs/compatibility.md +67 -0
  144. package/docs/debug/devtools.md +40 -0
  145. package/docs/debug/svt.md +50 -0
  146. package/docs/distribution.md +106 -0
  147. package/docs/elements.md +120 -0
  148. package/docs/getting-started.md +177 -0
  149. package/docs/guide/css.md +187 -0
  150. package/docs/guide/input.md +143 -0
  151. package/docs/guide/layout.md +171 -0
  152. package/docs/guide/theming.md +94 -0
  153. package/docs/how-it-works.md +115 -0
  154. package/docs/inline-mode.md +77 -0
  155. package/docs/layout.md +106 -0
  156. package/docs/motion.md +91 -0
  157. package/docs/reference/README.md +65 -0
  158. package/docs/reference/css/properties/border-corner.md +82 -0
  159. package/docs/reference/css/properties/border-style.md +168 -0
  160. package/docs/reference.md +226 -0
  161. package/docs/selectors.md +80 -0
  162. package/docs/terminal-css.md +149 -0
  163. package/docs/terminals.md +83 -0
  164. 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
+ }
@@ -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 = skipWhitespace(css, 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
- if (colonPos === -1)
181
- break;
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
- pseudo?: string;
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 {};
@@ -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(...), :nth-child(...)
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
- result.pseudoArg = selector.substring(argStart, pos).trim();
58
+ arg = selector.substring(argStart, pos).trim();
57
59
  pos++; // skip closing )
58
60
  }
59
- result.pseudo = name;
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 && selector[pos] !== '=' && selector[pos] !== ']')
71
+ while (pos < selector.length && !'^$*~|=]'.includes(selector[pos]))
70
72
  pos++;
71
73
  const name = selector.substring(nameStart, pos).trim();
72
- if (pos < selector.length && selector[pos] === '=') {
73
- pos++; // skip =
74
- let value = '';
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
- if (!node.attributes.has(attr.name))
273
+ const actual = node.attributes.get(attr.name);
274
+ if (actual === undefined)
237
275
  return false;
238
- if (attr.value !== undefined && node.attributes.get(attr.name) !== attr.value)
276
+ if (attr.op !== undefined && !attrValueMatches(actual, attr.op, attr.value ?? ''))
239
277
  return false;
240
278
  }
241
- if (parsed.pseudo) {
242
- if (!matchesPseudo(node, parsed.pseudo, parsed.pseudoArg))
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 = specificity of its argument
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
- const argSpec = computeSpecificity(arg);
50
- ids += argSpec[0];
51
- classes += argSpec[1];
52
- elements += argSpec[2];
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++;
@@ -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
  */
@@ -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
- if (value.endsWith('cell')) {
11
- const num = parseFloat(value);
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
+ }