@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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/src/components/spinner.d.ts +11 -0
- package/dist/src/components/spinner.js +19 -0
- package/dist/src/components/text-buffer.d.ts +21 -0
- package/dist/src/components/text-buffer.js +87 -0
- package/dist/src/css/animation-runner.d.ts +17 -0
- package/dist/src/css/animation-runner.js +72 -0
- package/dist/src/css/animation.d.ts +5 -0
- package/dist/src/css/animation.js +6 -0
- package/dist/src/css/calc.d.ts +5 -0
- package/dist/src/css/calc.js +130 -0
- package/dist/src/css/color.d.ts +1 -0
- package/dist/src/css/color.js +157 -0
- package/dist/src/css/compute.d.ts +63 -0
- package/dist/src/css/compute.js +606 -0
- package/dist/src/css/defaults.d.ts +8 -0
- package/dist/src/css/defaults.js +44 -0
- package/dist/src/css/incremental.d.ts +9 -0
- package/dist/src/css/incremental.js +46 -0
- package/dist/src/css/index.d.ts +5 -0
- package/dist/src/css/index.js +3 -0
- package/dist/src/css/media.d.ts +11 -0
- package/dist/src/css/media.js +59 -0
- package/dist/src/css/parser.d.ts +20 -0
- package/dist/src/css/parser.js +241 -0
- package/dist/src/css/selector.d.ts +17 -0
- package/dist/src/css/selector.js +272 -0
- package/dist/src/css/specificity.d.ts +7 -0
- package/dist/src/css/specificity.js +89 -0
- package/dist/src/css/values.d.ts +17 -0
- package/dist/src/css/values.js +58 -0
- package/dist/src/css/variables.d.ts +6 -0
- package/dist/src/css/variables.js +42 -0
- package/dist/src/debug/console.d.ts +16 -0
- package/dist/src/debug/console.js +65 -0
- package/dist/src/debug/server.d.ts +22 -0
- package/dist/src/debug/server.js +90 -0
- package/dist/src/headless.d.ts +21 -0
- package/dist/src/headless.js +26 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +485 -0
- package/dist/src/input/dispatch.d.ts +18 -0
- package/dist/src/input/dispatch.js +70 -0
- package/dist/src/input/focus.d.ts +18 -0
- package/dist/src/input/focus.js +81 -0
- package/dist/src/input/hit.d.ts +3 -0
- package/dist/src/input/hit.js +29 -0
- package/dist/src/input/keyboard.d.ts +9 -0
- package/dist/src/input/keyboard.js +100 -0
- package/dist/src/input/mouse.d.ts +7 -0
- package/dist/src/input/mouse.js +35 -0
- package/dist/src/input/scroll.d.ts +2 -0
- package/dist/src/input/scroll.js +24 -0
- package/dist/src/layout/cache.d.ts +4 -0
- package/dist/src/layout/cache.js +8 -0
- package/dist/src/layout/engine.d.ts +9 -0
- package/dist/src/layout/engine.js +455 -0
- package/dist/src/layout/flex.d.ts +4 -0
- package/dist/src/layout/flex.js +30 -0
- package/dist/src/layout/incremental.d.ts +8 -0
- package/dist/src/layout/incremental.js +58 -0
- package/dist/src/layout/size.d.ts +2 -0
- package/dist/src/layout/size.js +25 -0
- package/dist/src/layout/text.d.ts +7 -0
- package/dist/src/layout/text.js +52 -0
- package/dist/src/render/ansi.d.ts +23 -0
- package/dist/src/render/ansi.js +108 -0
- package/dist/src/render/border.d.ts +4 -0
- package/dist/src/render/border.js +60 -0
- package/dist/src/render/buffer.d.ts +23 -0
- package/dist/src/render/buffer.js +70 -0
- package/dist/src/render/context.d.ts +19 -0
- package/dist/src/render/context.js +98 -0
- package/dist/src/render/diff.d.ts +2 -0
- package/dist/src/render/diff.js +53 -0
- package/dist/src/render/incremental-paint.d.ts +10 -0
- package/dist/src/render/incremental-paint.js +94 -0
- package/dist/src/render/paint-text.d.ts +29 -0
- package/dist/src/render/paint-text.js +120 -0
- package/dist/src/render/paint.d.ts +5 -0
- package/dist/src/render/paint.js +220 -0
- package/dist/src/render/queue.d.ts +24 -0
- package/dist/src/render/queue.js +54 -0
- package/dist/src/render/scrollbar.d.ts +3 -0
- package/dist/src/render/scrollbar.js +19 -0
- package/dist/src/render/snapshot.d.ts +18 -0
- package/dist/src/render/snapshot.js +126 -0
- package/dist/src/renderer/default.d.ts +3 -0
- package/dist/src/renderer/default.js +3 -0
- package/dist/src/renderer/index.d.ts +11 -0
- package/dist/src/renderer/index.js +116 -0
- package/dist/src/renderer/node.d.ts +44 -0
- package/dist/src/renderer/node.js +153 -0
- package/dist/src/terminal/screen.d.ts +10 -0
- package/dist/src/terminal/screen.js +31 -0
- package/dist/src/terminal/stdin-router.d.ts +31 -0
- package/dist/src/terminal/stdin-router.js +133 -0
- 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,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;
|