@svelterm/core 0.1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/src/components/spinner.d.ts +11 -0
  4. package/dist/src/components/spinner.js +19 -0
  5. package/dist/src/components/text-buffer.d.ts +21 -0
  6. package/dist/src/components/text-buffer.js +87 -0
  7. package/dist/src/css/animation-runner.d.ts +17 -0
  8. package/dist/src/css/animation-runner.js +72 -0
  9. package/dist/src/css/animation.d.ts +5 -0
  10. package/dist/src/css/animation.js +6 -0
  11. package/dist/src/css/calc.d.ts +5 -0
  12. package/dist/src/css/calc.js +130 -0
  13. package/dist/src/css/color.d.ts +1 -0
  14. package/dist/src/css/color.js +157 -0
  15. package/dist/src/css/compute.d.ts +63 -0
  16. package/dist/src/css/compute.js +606 -0
  17. package/dist/src/css/defaults.d.ts +8 -0
  18. package/dist/src/css/defaults.js +44 -0
  19. package/dist/src/css/incremental.d.ts +9 -0
  20. package/dist/src/css/incremental.js +46 -0
  21. package/dist/src/css/index.d.ts +5 -0
  22. package/dist/src/css/index.js +3 -0
  23. package/dist/src/css/media.d.ts +11 -0
  24. package/dist/src/css/media.js +59 -0
  25. package/dist/src/css/parser.d.ts +20 -0
  26. package/dist/src/css/parser.js +241 -0
  27. package/dist/src/css/selector.d.ts +17 -0
  28. package/dist/src/css/selector.js +272 -0
  29. package/dist/src/css/specificity.d.ts +7 -0
  30. package/dist/src/css/specificity.js +89 -0
  31. package/dist/src/css/values.d.ts +17 -0
  32. package/dist/src/css/values.js +58 -0
  33. package/dist/src/css/variables.d.ts +6 -0
  34. package/dist/src/css/variables.js +42 -0
  35. package/dist/src/debug/console.d.ts +16 -0
  36. package/dist/src/debug/console.js +65 -0
  37. package/dist/src/debug/server.d.ts +22 -0
  38. package/dist/src/debug/server.js +90 -0
  39. package/dist/src/headless.d.ts +21 -0
  40. package/dist/src/headless.js +26 -0
  41. package/dist/src/index.d.ts +18 -0
  42. package/dist/src/index.js +485 -0
  43. package/dist/src/input/dispatch.d.ts +18 -0
  44. package/dist/src/input/dispatch.js +70 -0
  45. package/dist/src/input/focus.d.ts +18 -0
  46. package/dist/src/input/focus.js +81 -0
  47. package/dist/src/input/hit.d.ts +3 -0
  48. package/dist/src/input/hit.js +29 -0
  49. package/dist/src/input/keyboard.d.ts +9 -0
  50. package/dist/src/input/keyboard.js +100 -0
  51. package/dist/src/input/mouse.d.ts +7 -0
  52. package/dist/src/input/mouse.js +35 -0
  53. package/dist/src/input/scroll.d.ts +2 -0
  54. package/dist/src/input/scroll.js +24 -0
  55. package/dist/src/layout/cache.d.ts +4 -0
  56. package/dist/src/layout/cache.js +8 -0
  57. package/dist/src/layout/engine.d.ts +9 -0
  58. package/dist/src/layout/engine.js +455 -0
  59. package/dist/src/layout/flex.d.ts +4 -0
  60. package/dist/src/layout/flex.js +30 -0
  61. package/dist/src/layout/incremental.d.ts +8 -0
  62. package/dist/src/layout/incremental.js +58 -0
  63. package/dist/src/layout/size.d.ts +2 -0
  64. package/dist/src/layout/size.js +25 -0
  65. package/dist/src/layout/text.d.ts +7 -0
  66. package/dist/src/layout/text.js +52 -0
  67. package/dist/src/render/ansi.d.ts +23 -0
  68. package/dist/src/render/ansi.js +108 -0
  69. package/dist/src/render/border.d.ts +4 -0
  70. package/dist/src/render/border.js +60 -0
  71. package/dist/src/render/buffer.d.ts +23 -0
  72. package/dist/src/render/buffer.js +70 -0
  73. package/dist/src/render/context.d.ts +19 -0
  74. package/dist/src/render/context.js +98 -0
  75. package/dist/src/render/diff.d.ts +2 -0
  76. package/dist/src/render/diff.js +53 -0
  77. package/dist/src/render/incremental-paint.d.ts +10 -0
  78. package/dist/src/render/incremental-paint.js +94 -0
  79. package/dist/src/render/paint-text.d.ts +29 -0
  80. package/dist/src/render/paint-text.js +120 -0
  81. package/dist/src/render/paint.d.ts +5 -0
  82. package/dist/src/render/paint.js +220 -0
  83. package/dist/src/render/queue.d.ts +24 -0
  84. package/dist/src/render/queue.js +54 -0
  85. package/dist/src/render/scrollbar.d.ts +3 -0
  86. package/dist/src/render/scrollbar.js +19 -0
  87. package/dist/src/render/snapshot.d.ts +18 -0
  88. package/dist/src/render/snapshot.js +126 -0
  89. package/dist/src/renderer/default.d.ts +3 -0
  90. package/dist/src/renderer/default.js +3 -0
  91. package/dist/src/renderer/index.d.ts +11 -0
  92. package/dist/src/renderer/index.js +116 -0
  93. package/dist/src/renderer/node.d.ts +44 -0
  94. package/dist/src/renderer/node.js +153 -0
  95. package/dist/src/terminal/screen.d.ts +10 -0
  96. package/dist/src/terminal/screen.js +31 -0
  97. package/dist/src/terminal/stdin-router.d.ts +31 -0
  98. package/dist/src/terminal/stdin-router.js +133 -0
  99. package/package.json +64 -0
@@ -0,0 +1,46 @@
1
+ import { resolveNode } from './compute.js';
2
+ import { collectVariables } from './variables.js';
3
+ const LAYOUT_PROPERTIES = [
4
+ 'display', 'flexDirection', 'justifyContent', 'alignItems', 'alignSelf',
5
+ 'gap', 'flexGrow', 'flexShrink', 'flexWrap',
6
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
7
+ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
8
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
9
+ 'borderStyle', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft',
10
+ 'position', 'top', 'right', 'bottom', 'left',
11
+ 'overflow', 'whiteSpace',
12
+ ];
13
+ /**
14
+ * Incremental style resolution — re-resolves dirty nodes and their
15
+ * descendants using the same resolveNode function as full resolution.
16
+ * Variables are collected once from the full tree for consistency.
17
+ */
18
+ export function resolveStylesIncremental(root, stylesheet, existingStyles, dirtyNodes, onResolve, onLayoutAffected) {
19
+ if (dirtyNodes.size === 0)
20
+ return existingStyles;
21
+ // Collect variables from the full tree — same as full resolution
22
+ const variables = collectVariables(root, stylesheet);
23
+ // Start with existing styles
24
+ const result = new Map(existingStyles);
25
+ for (const node of dirtyNodes) {
26
+ if (node.nodeType !== 'element')
27
+ continue;
28
+ const oldStyle = existingStyles.get(node.id);
29
+ // Re-resolve this node and all its descendants using the
30
+ // same resolveNode function as full resolution
31
+ resolveNode(node, stylesheet, result, variables);
32
+ onResolve?.(node.id);
33
+ const newStyle = result.get(node.id);
34
+ if (oldStyle && newStyle && onLayoutAffected && isLayoutAffecting(oldStyle, newStyle)) {
35
+ onLayoutAffected(node);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ function isLayoutAffecting(oldStyle, newStyle) {
41
+ for (const prop of LAYOUT_PROPERTIES) {
42
+ if (oldStyle[prop] !== newStyle[prop])
43
+ return true;
44
+ }
45
+ return false;
46
+ }
@@ -0,0 +1,5 @@
1
+ export { parseCSS } from './parser.js';
2
+ export type { CSSRule, CSSDeclaration, CSSStyleSheet } from './parser.js';
3
+ export { matchesSelector, parseSelector } from './selector.js';
4
+ export { resolveStyles } from './compute.js';
5
+ export type { ResolvedStyle } from './compute.js';
@@ -0,0 +1,3 @@
1
+ export { parseCSS } from './parser.js';
2
+ export { matchesSelector, parseSelector } from './selector.js';
3
+ export { resolveStyles } from './compute.js';
@@ -0,0 +1,11 @@
1
+ export interface MediaContext {
2
+ colorScheme: 'dark' | 'light';
3
+ displayMode: 'terminal' | 'screen';
4
+ width: number;
5
+ height: number;
6
+ }
7
+ /**
8
+ * Evaluate a media query condition against the current context.
9
+ * Supports: single conditions, "and" compound, "not" prefix.
10
+ */
11
+ export declare function evaluateMediaQuery(condition: string, context: MediaContext): boolean;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Evaluate a media query condition against the current context.
3
+ * Supports: single conditions, "and" compound, "not" prefix.
4
+ */
5
+ export function evaluateMediaQuery(condition, context) {
6
+ const trimmed = condition.trim();
7
+ // Handle "not" prefix
8
+ if (trimmed.startsWith('not ')) {
9
+ return !evaluateMediaQuery(trimmed.substring(4), context);
10
+ }
11
+ // Handle "and" compound: split on " and " or ") and ("
12
+ if (trimmed.includes(' and ')) {
13
+ const parts = splitAnd(trimmed);
14
+ return parts.every(part => evaluateSingle(part.trim(), context));
15
+ }
16
+ return evaluateSingle(trimmed, context);
17
+ }
18
+ function splitAnd(condition) {
19
+ // Split on ") and (" or " and "
20
+ return condition.split(/\)\s*and\s*\(|\s+and\s+/).map(part => {
21
+ // Strip outer parens
22
+ let p = part.trim();
23
+ if (p.startsWith('('))
24
+ p = p.substring(1);
25
+ if (p.endsWith(')'))
26
+ p = p.substring(0, p.length - 1);
27
+ return p.trim();
28
+ });
29
+ }
30
+ function evaluateSingle(condition, context) {
31
+ // Strip outer parens
32
+ let c = condition.trim();
33
+ if (c.startsWith('('))
34
+ c = c.substring(1);
35
+ if (c.endsWith(')'))
36
+ c = c.substring(0, c.length - 1);
37
+ c = c.trim();
38
+ const colonIdx = c.indexOf(':');
39
+ if (colonIdx === -1)
40
+ return false;
41
+ const feature = c.substring(0, colonIdx).trim();
42
+ const value = c.substring(colonIdx + 1).trim();
43
+ switch (feature) {
44
+ case 'prefers-color-scheme':
45
+ return value === context.colorScheme;
46
+ case 'display-mode':
47
+ return value === context.displayMode;
48
+ case 'min-width':
49
+ return context.width >= parseInt(value);
50
+ case 'max-width':
51
+ return context.width <= parseInt(value);
52
+ case 'min-height':
53
+ return context.height >= parseInt(value);
54
+ case 'max-height':
55
+ return context.height <= parseInt(value);
56
+ default:
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,20 @@
1
+ export interface CSSDeclaration {
2
+ property: string;
3
+ value: string;
4
+ }
5
+ export interface CSSRule {
6
+ selectors: string[];
7
+ declarations: CSSDeclaration[];
8
+ media?: string;
9
+ supports?: string;
10
+ container?: string;
11
+ }
12
+ export interface KeyframeStop {
13
+ offset: number;
14
+ declarations: CSSDeclaration[];
15
+ }
16
+ export interface CSSStyleSheet {
17
+ rules: CSSRule[];
18
+ keyframes: Map<string, KeyframeStop[]>;
19
+ }
20
+ export declare function parseCSS(css: string): CSSStyleSheet;
@@ -0,0 +1,241 @@
1
+ export function parseCSS(css) {
2
+ const rules = [];
3
+ const keyframes = new Map();
4
+ let pos = 0;
5
+ while (pos < css.length) {
6
+ pos = skipWhitespaceAndComments(css, pos);
7
+ if (pos >= css.length)
8
+ break;
9
+ // Check for @-rules
10
+ if (css.substring(pos, pos + 6) === '@media') {
11
+ pos = parseMediaBlock(css, pos, rules);
12
+ continue;
13
+ }
14
+ if (css.substring(pos, pos + 9) === '@supports') {
15
+ pos = parseSupportsBlock(css, pos, rules);
16
+ continue;
17
+ }
18
+ if (css.substring(pos, pos + 10) === '@container') {
19
+ pos = parseContainerBlock(css, pos, rules);
20
+ continue;
21
+ }
22
+ if (css.substring(pos, pos + 11) === '@keyframes ') {
23
+ pos = parseKeyframesBlock(css, pos, keyframes);
24
+ continue;
25
+ }
26
+ if (css.substring(pos, pos + 7) === '@import') {
27
+ // Skip @import — handled by bundler
28
+ pos = css.indexOf(';', pos);
29
+ if (pos === -1)
30
+ pos = css.length;
31
+ else
32
+ pos++;
33
+ continue;
34
+ }
35
+ pos = parseRule(css, pos, rules, undefined);
36
+ }
37
+ return { rules, keyframes };
38
+ }
39
+ function parseMediaBlock(css, start, rules) {
40
+ let pos = start + 6; // skip "@media"
41
+ pos = skipWhitespace(css, pos);
42
+ // Capture everything between @media and { as the condition string
43
+ const bracePos = css.indexOf('{', pos);
44
+ if (bracePos === -1)
45
+ return skipToClosingBrace(css, pos);
46
+ const rawCondition = css.substring(pos, bracePos).trim();
47
+ // Strip outer parens if single condition: "(foo: bar)" -> "foo: bar"
48
+ // Keep as-is for compound: "(foo: bar) and (baz: qux)"
49
+ const condition = rawCondition.startsWith('(') && !rawCondition.includes(') and (')
50
+ ? rawCondition.slice(1, -1).trim()
51
+ : rawCondition;
52
+ pos = bracePos + 1;
53
+ // Parse rules inside the @media block
54
+ while (pos < css.length) {
55
+ pos = skipWhitespaceAndComments(css, pos);
56
+ if (pos >= css.length || css[pos] === '}') {
57
+ pos++;
58
+ break;
59
+ }
60
+ pos = parseRule(css, pos, rules, condition);
61
+ }
62
+ return pos;
63
+ }
64
+ function parseContainerBlock(css, start, rules) {
65
+ let pos = start + 10; // skip "@container"
66
+ pos = skipWhitespace(css, pos);
67
+ const bracePos = css.indexOf('{', pos);
68
+ if (bracePos === -1)
69
+ return skipToClosingBrace(css, pos);
70
+ let condition = css.substring(pos, bracePos).trim();
71
+ if (condition.startsWith('(') && !condition.includes(') and (')) {
72
+ condition = condition.slice(1, -1).trim();
73
+ }
74
+ pos = bracePos + 1;
75
+ while (pos < css.length) {
76
+ pos = skipWhitespaceAndComments(css, pos);
77
+ if (pos >= css.length || css[pos] === '}') {
78
+ pos++;
79
+ break;
80
+ }
81
+ pos = parseRule(css, pos, rules, undefined, undefined, condition);
82
+ }
83
+ return pos;
84
+ }
85
+ function parseKeyframesBlock(css, start, keyframes) {
86
+ let pos = start + 11; // skip "@keyframes "
87
+ pos = skipWhitespace(css, pos);
88
+ // Parse name
89
+ const nameStart = pos;
90
+ while (pos < css.length && css[pos] !== '{' && css[pos] !== ' ')
91
+ pos++;
92
+ const name = css.substring(nameStart, pos).trim();
93
+ pos = skipWhitespace(css, pos);
94
+ if (pos >= css.length || css[pos] !== '{')
95
+ return pos;
96
+ pos++; // skip {
97
+ const stops = [];
98
+ while (pos < css.length) {
99
+ pos = skipWhitespaceAndComments(css, pos);
100
+ if (pos >= css.length || css[pos] === '}') {
101
+ pos++;
102
+ break;
103
+ }
104
+ // Parse offset: "from", "to", or percentage
105
+ const offsetEnd = css.indexOf('{', pos);
106
+ if (offsetEnd === -1)
107
+ break;
108
+ const offsetStr = css.substring(pos, offsetEnd).trim();
109
+ let offset = 0;
110
+ if (offsetStr === 'from')
111
+ offset = 0;
112
+ else if (offsetStr === 'to')
113
+ offset = 1;
114
+ else if (offsetStr.endsWith('%'))
115
+ offset = parseFloat(offsetStr) / 100;
116
+ pos = offsetEnd + 1;
117
+ // Parse declarations
118
+ const declarations = [];
119
+ while (pos < css.length) {
120
+ pos = skipWhitespace(css, pos);
121
+ if (pos >= css.length || css[pos] === '}') {
122
+ pos++;
123
+ break;
124
+ }
125
+ const colonPos = css.indexOf(':', pos);
126
+ if (colonPos === -1)
127
+ break;
128
+ const property = css.substring(pos, colonPos).trim();
129
+ const valueEnd = findValueEnd(css, colonPos + 1);
130
+ const value = css.substring(colonPos + 1, valueEnd).trim();
131
+ declarations.push({ property, value });
132
+ pos = valueEnd;
133
+ if (pos < css.length && css[pos] === ';')
134
+ pos++;
135
+ }
136
+ stops.push({ offset, declarations });
137
+ }
138
+ if (name && stops.length > 0) {
139
+ keyframes.set(name, stops);
140
+ }
141
+ return pos;
142
+ }
143
+ function parseSupportsBlock(css, start, rules) {
144
+ let pos = start + 9; // skip "@supports"
145
+ pos = skipWhitespace(css, pos);
146
+ const bracePos = css.indexOf('{', pos);
147
+ if (bracePos === -1)
148
+ return skipToClosingBrace(css, pos);
149
+ let condition = css.substring(pos, bracePos).trim();
150
+ if (condition.startsWith('('))
151
+ condition = condition.slice(1, -1).trim();
152
+ pos = bracePos + 1;
153
+ // Parse rules inside
154
+ while (pos < css.length) {
155
+ pos = skipWhitespaceAndComments(css, pos);
156
+ if (pos >= css.length || css[pos] === '}') {
157
+ pos++;
158
+ break;
159
+ }
160
+ pos = parseRule(css, pos, rules, undefined, condition);
161
+ }
162
+ return pos;
163
+ }
164
+ function parseRule(css, start, rules, media, supports, container) {
165
+ let pos = start;
166
+ const selectorEnd = css.indexOf('{', pos);
167
+ if (selectorEnd === -1)
168
+ return css.length;
169
+ const selectorText = css.substring(pos, selectorEnd).trim();
170
+ const selectors = selectorText.split(',').map(s => s.trim()).filter(Boolean);
171
+ pos = selectorEnd + 1;
172
+ const declarations = [];
173
+ while (pos < css.length) {
174
+ pos = skipWhitespace(css, pos);
175
+ if (pos >= css.length || css[pos] === '}') {
176
+ pos++;
177
+ break;
178
+ }
179
+ const colonPos = css.indexOf(':', pos);
180
+ if (colonPos === -1)
181
+ break;
182
+ const property = css.substring(pos, colonPos).trim();
183
+ const valueEnd = findValueEnd(css, colonPos + 1);
184
+ const value = css.substring(colonPos + 1, valueEnd).trim();
185
+ declarations.push({ property, value });
186
+ pos = valueEnd;
187
+ if (pos < css.length && css[pos] === ';')
188
+ pos++;
189
+ }
190
+ if (selectors.length > 0 && declarations.length > 0) {
191
+ rules.push({ selectors, declarations, media, supports, container });
192
+ }
193
+ return pos;
194
+ }
195
+ function skipToClosingBrace(css, pos) {
196
+ let depth = 0;
197
+ while (pos < css.length) {
198
+ if (css[pos] === '{')
199
+ depth++;
200
+ else if (css[pos] === '}') {
201
+ if (depth === 0)
202
+ return pos + 1;
203
+ depth--;
204
+ }
205
+ pos++;
206
+ }
207
+ return pos;
208
+ }
209
+ function findValueEnd(css, start) {
210
+ let pos = start;
211
+ let depth = 0;
212
+ while (pos < css.length) {
213
+ const ch = css[pos];
214
+ if (ch === '(')
215
+ depth++;
216
+ else if (ch === ')')
217
+ depth--;
218
+ else if (depth === 0 && (ch === ';' || ch === '}'))
219
+ return pos;
220
+ pos++;
221
+ }
222
+ return pos;
223
+ }
224
+ function skipWhitespace(css, pos) {
225
+ while (pos < css.length && /\s/.test(css[pos]))
226
+ pos++;
227
+ return pos;
228
+ }
229
+ function skipWhitespaceAndComments(css, pos) {
230
+ while (pos < css.length) {
231
+ pos = skipWhitespace(css, pos);
232
+ if (pos + 1 < css.length && css[pos] === '/' && css[pos + 1] === '*') {
233
+ const end = css.indexOf('*/', pos + 2);
234
+ pos = end === -1 ? css.length : end + 2;
235
+ }
236
+ else {
237
+ break;
238
+ }
239
+ }
240
+ return pos;
241
+ }
@@ -0,0 +1,17 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ export interface ParsedSelector {
3
+ tag?: string;
4
+ id?: string;
5
+ classes: string[];
6
+ pseudo?: string;
7
+ pseudoArg?: string;
8
+ attributes: AttrSelector[];
9
+ universal?: boolean;
10
+ }
11
+ interface AttrSelector {
12
+ name: string;
13
+ value?: string;
14
+ }
15
+ export declare function parseSelector(selector: string): ParsedSelector;
16
+ export declare function matchesSelector(node: TermNode, selector: string): boolean;
17
+ export {};
@@ -0,0 +1,272 @@
1
+ export function parseSelector(selector) {
2
+ const result = { classes: [], attributes: [] };
3
+ let pos = 0;
4
+ // Universal selector
5
+ if (pos < selector.length && selector[pos] === '*') {
6
+ result.universal = true;
7
+ pos++;
8
+ }
9
+ // Tag name
10
+ if (pos < selector.length && /[a-zA-Z]/.test(selector[pos])) {
11
+ const start = pos;
12
+ while (pos < selector.length && /[a-zA-Z0-9-]/.test(selector[pos]))
13
+ pos++;
14
+ result.tag = selector.substring(start, pos);
15
+ }
16
+ while (pos < selector.length) {
17
+ if (selector[pos] === '#') {
18
+ pos++;
19
+ const start = pos;
20
+ while (pos < selector.length && /[a-zA-Z0-9_-]/.test(selector[pos]))
21
+ pos++;
22
+ result.id = selector.substring(start, pos);
23
+ }
24
+ else if (selector[pos] === '.') {
25
+ pos++;
26
+ const start = pos;
27
+ while (pos < selector.length && /[a-zA-Z0-9_-]/.test(selector[pos]))
28
+ pos++;
29
+ result.classes.push(selector.substring(start, pos));
30
+ }
31
+ else if (selector[pos] === '[') {
32
+ pos++;
33
+ const attr = parseAttrSelector(selector, pos);
34
+ result.attributes.push(attr.selector);
35
+ pos = attr.end;
36
+ }
37
+ else if (selector[pos] === ':') {
38
+ pos++;
39
+ const start = pos;
40
+ while (pos < selector.length && /[a-zA-Z0-9_-]/.test(selector[pos]))
41
+ pos++;
42
+ const name = selector.substring(start, pos);
43
+ // Functional pseudo-class: :not(...), :nth-child(...)
44
+ if (pos < selector.length && selector[pos] === '(') {
45
+ pos++;
46
+ const argStart = pos;
47
+ let depth = 1;
48
+ while (pos < selector.length && depth > 0) {
49
+ if (selector[pos] === '(')
50
+ depth++;
51
+ else if (selector[pos] === ')')
52
+ depth--;
53
+ if (depth > 0)
54
+ pos++;
55
+ }
56
+ result.pseudoArg = selector.substring(argStart, pos).trim();
57
+ pos++; // skip closing )
58
+ }
59
+ result.pseudo = name;
60
+ }
61
+ else {
62
+ pos++;
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ function parseAttrSelector(selector, pos) {
68
+ const nameStart = pos;
69
+ while (pos < selector.length && selector[pos] !== '=' && selector[pos] !== ']')
70
+ pos++;
71
+ const name = selector.substring(nameStart, pos).trim();
72
+ if (pos < selector.length && selector[pos] === '=') {
73
+ pos++; // skip =
74
+ let value = '';
75
+ if (pos < selector.length && (selector[pos] === '"' || selector[pos] === "'")) {
76
+ const quote = selector[pos];
77
+ pos++;
78
+ const valStart = pos;
79
+ while (pos < selector.length && selector[pos] !== quote)
80
+ pos++;
81
+ value = selector.substring(valStart, pos);
82
+ pos++; // skip closing quote
83
+ }
84
+ while (pos < selector.length && selector[pos] !== ']')
85
+ pos++;
86
+ pos++; // skip ]
87
+ return { selector: { name, value }, end: pos };
88
+ }
89
+ while (pos < selector.length && selector[pos] !== ']')
90
+ pos++;
91
+ pos++; // skip ]
92
+ return { selector: { name }, end: pos };
93
+ }
94
+ export function matchesSelector(node, selector) {
95
+ if (node.nodeType !== 'element')
96
+ return false;
97
+ const trimmed = selector.trim();
98
+ if (trimmed === ':root')
99
+ return node.parent === null;
100
+ const parts = splitIntoParts(trimmed);
101
+ if (parts.length === 0)
102
+ return false;
103
+ return matchParts(node, parts, parts.length - 1);
104
+ }
105
+ function splitIntoParts(selector) {
106
+ const parts = [];
107
+ const tokens = tokenizeSelector(selector);
108
+ for (let i = 0; i < tokens.length; i++) {
109
+ const token = tokens[i];
110
+ if (token === '>' || token === ' ' || token === '+' || token === '~')
111
+ continue;
112
+ let combinator = '';
113
+ if (i > 0) {
114
+ const prev = tokens[i - 1];
115
+ if (prev === '>' || prev === '+' || prev === '~')
116
+ combinator = prev;
117
+ else
118
+ combinator = ' ';
119
+ }
120
+ parts.push({ selector: parseSelector(token), combinator });
121
+ }
122
+ return parts;
123
+ }
124
+ function tokenizeSelector(selector) {
125
+ const tokens = [];
126
+ let pos = 0;
127
+ let current = '';
128
+ let bracketDepth = 0;
129
+ let parenDepth = 0;
130
+ while (pos < selector.length) {
131
+ const ch = selector[pos];
132
+ if (ch === '[')
133
+ bracketDepth++;
134
+ if (ch === ']')
135
+ bracketDepth--;
136
+ if (ch === '(')
137
+ parenDepth++;
138
+ if (ch === ')')
139
+ parenDepth--;
140
+ if (bracketDepth > 0 || parenDepth > 0) {
141
+ current += ch;
142
+ pos++;
143
+ continue;
144
+ }
145
+ if (ch === '>' || ch === '+' || ch === '~') {
146
+ if (current.trim())
147
+ tokens.push(current.trim());
148
+ tokens.push(ch);
149
+ current = '';
150
+ pos++;
151
+ }
152
+ else if (ch === ' ') {
153
+ if (current.trim()) {
154
+ tokens.push(current.trim());
155
+ let next = pos + 1;
156
+ while (next < selector.length && selector[next] === ' ')
157
+ next++;
158
+ if (next < selector.length && !'> +~'.includes(selector[next])) {
159
+ tokens.push(' ');
160
+ }
161
+ }
162
+ current = '';
163
+ pos++;
164
+ }
165
+ else {
166
+ current += ch;
167
+ pos++;
168
+ }
169
+ }
170
+ if (current.trim())
171
+ tokens.push(current.trim());
172
+ return tokens;
173
+ }
174
+ function matchParts(node, parts, index) {
175
+ const part = parts[index];
176
+ if (!matchesParsed(node, part.selector))
177
+ return false;
178
+ if (index === 0)
179
+ return true;
180
+ const combinator = parts[index].combinator;
181
+ if (combinator === '>') {
182
+ if (!node.parent || node.parent.nodeType !== 'element')
183
+ return false;
184
+ return matchParts(node.parent, parts, index - 1);
185
+ }
186
+ if (combinator === '+') {
187
+ const prev = getPreviousElementSibling(node);
188
+ if (!prev)
189
+ return false;
190
+ return matchParts(prev, parts, index - 1);
191
+ }
192
+ if (combinator === '~') {
193
+ if (!node.parent)
194
+ return false;
195
+ const siblings = node.parent.children;
196
+ const myIndex = siblings.indexOf(node);
197
+ for (let i = myIndex - 1; i >= 0; i--) {
198
+ if (siblings[i].nodeType === 'element' && matchParts(siblings[i], parts, index - 1)) {
199
+ return true;
200
+ }
201
+ }
202
+ return false;
203
+ }
204
+ // Descendant: any ancestor
205
+ let ancestor = node.parent;
206
+ while (ancestor) {
207
+ if (ancestor.nodeType === 'element' && matchParts(ancestor, parts, index - 1)) {
208
+ return true;
209
+ }
210
+ ancestor = ancestor.parent;
211
+ }
212
+ return false;
213
+ }
214
+ function getPreviousElementSibling(node) {
215
+ if (!node.parent)
216
+ return null;
217
+ const siblings = node.parent.children;
218
+ const idx = siblings.indexOf(node);
219
+ for (let i = idx - 1; i >= 0; i--) {
220
+ if (siblings[i].nodeType === 'element')
221
+ return siblings[i];
222
+ }
223
+ return null;
224
+ }
225
+ function matchesParsed(node, parsed) {
226
+ if (parsed.tag && node.tag !== parsed.tag)
227
+ return false;
228
+ if (parsed.id && node.attributes.get('id') !== parsed.id)
229
+ return false;
230
+ const nodeClasses = node.classes;
231
+ for (const cls of parsed.classes) {
232
+ if (!nodeClasses.has(cls))
233
+ return false;
234
+ }
235
+ for (const attr of parsed.attributes) {
236
+ if (!node.attributes.has(attr.name))
237
+ return false;
238
+ if (attr.value !== undefined && node.attributes.get(attr.name) !== attr.value)
239
+ return false;
240
+ }
241
+ if (parsed.pseudo) {
242
+ if (!matchesPseudo(node, parsed.pseudo, parsed.pseudoArg))
243
+ return false;
244
+ }
245
+ return true;
246
+ }
247
+ function matchesPseudo(node, pseudo, arg) {
248
+ switch (pseudo) {
249
+ case 'root': return node.parent === null;
250
+ case 'focus': return node.attributes.get('data-focused') === 'true';
251
+ case 'hover': return node.attributes.get('data-hovered') === 'true';
252
+ case 'first-child': return isFirstChild(node);
253
+ case 'last-child': return isLastChild(node);
254
+ case 'not':
255
+ if (!arg)
256
+ return false;
257
+ return !matchesParsed(node, parseSelector(arg));
258
+ default: return false;
259
+ }
260
+ }
261
+ function isFirstChild(node) {
262
+ if (!node.parent)
263
+ return false;
264
+ const siblings = node.parent.children.filter(c => c.nodeType === 'element');
265
+ return siblings[0] === node;
266
+ }
267
+ function isLastChild(node) {
268
+ if (!node.parent)
269
+ return false;
270
+ const siblings = node.parent.children.filter(c => c.nodeType === 'element');
271
+ return siblings[siblings.length - 1] === node;
272
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Compute CSS specificity as [id, class, element] tuple.
3
+ * Higher tuple wins. Equal specificity: later rule wins.
4
+ */
5
+ export declare function computeSpecificity(selector: string): [number, number, number];
6
+ /** Compare two specificity tuples. Returns positive if a wins, negative if b wins, 0 if equal. */
7
+ export declare function compareSpecificity(a: [number, number, number], b: [number, number, number]): number;