@pre.style/core 3.1.0-alpha.1 → 3.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +395 -6
- package/dist/cn.d.ts +2 -1
- package/dist/cn.js +4 -2
- package/dist/compiler/block.d.ts +3 -0
- package/dist/compiler/block.js +17 -4
- package/dist/compiler/compiler.d.ts +36 -0
- package/dist/compiler/compiler.js +187 -44
- package/dist/compiler/config.d.ts +1 -0
- package/dist/compiler/config.js +4 -0
- package/dist/compiler/custom-properties.d.ts +5 -0
- package/dist/compiler/custom-properties.js +150 -0
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/interpolate.d.ts +11 -0
- package/dist/compiler/interpolate.js +282 -0
- package/dist/compiler/libraries.d.ts +2 -0
- package/dist/compiler/libraries.js +9 -2
- package/dist/compiler/mapper.js +3 -1
- package/dist/compiler/selectors.d.ts +3 -0
- package/dist/compiler/selectors.js +38 -0
- package/dist/compiler/stylesheet.js +9 -0
- package/dist/compiler/variables.d.ts +13 -0
- package/dist/compiler/variables.js +272 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/runtime-dev.d.ts +2 -0
- package/dist/runtime-dev.js +9 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +99 -0
- package/package.json +11 -1
- package/package.schema.json +8 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { PreStyleError } from './block.js';
|
|
2
|
+
// Each slot is written as this variable, plus its index, until the block is
|
|
3
|
+
// parsed. See `variables.ts`.
|
|
4
|
+
export const SLOT_PREFIX = '--__pre-style-slot-';
|
|
5
|
+
// Stands for an expression while scanning, since it can't be in CSS.
|
|
6
|
+
const EXPRESSION = '\0';
|
|
7
|
+
// Ends a token, so `${w}px` is one slot, and `${a} ${b}` is two.
|
|
8
|
+
const DELIMITER = /[\s,()/;:{}!"']/;
|
|
9
|
+
const IDENT = /[\w-]/;
|
|
10
|
+
// At-rules that can't read a runtime value: their descriptors don't accept
|
|
11
|
+
// `var()`, or, for `@page`, have no element to read it from. Others, such as
|
|
12
|
+
// `@keyframes`, `@position-try`, and `@function`, read it from the element
|
|
13
|
+
// that uses them.
|
|
14
|
+
const NO_VARIABLES = /^@(font-face|property|page|counter-style|font-feature-values|font-palette-values|view-transition)\b/i;
|
|
15
|
+
// Sass at-rules whose arguments are evaluated, rather than written out.
|
|
16
|
+
const SASS_AT_RULES = /^@(include|return|if|else|each|for|while|debug|warn|error|function|mixin)\b/;
|
|
17
|
+
const POSITION_MESSAGE = "`${…}` can only be used in a declaration's value, because it becomes a `var()`, which isn't valid in selectors, property names, or at-rule conditions. To share media queries, use Sass variables.";
|
|
18
|
+
const NO_VARIABLES_MESSAGE = "`${…}` can't be used here: this at-rule doesn't accept `var()`, or has no element for the runtime to set it on.";
|
|
19
|
+
const QUOTED_MESSAGE = '`${…}` can\'t be used inside a quoted string or `url()`, because it becomes a `var()`, which doesn\'t work there. Pass the whole value instead: `content: ${JSON.stringify(label)}`, or `--image: ${`url("${path}")`}` with `background-image: var(--image)`.';
|
|
20
|
+
const TYPE_RUNTIME_MESSAGE = "`type()` and its default are written to the CSS file, so they can't use `${…}`.";
|
|
21
|
+
const TYPE_DEFAULT_MESSAGE = '`type()` needs a default, because a typed `@property` requires an `initial-value`: `var(${w}px type(<length>), 120px)`.';
|
|
22
|
+
const FUNCTION_MESSAGE = "`${…}` can't be used as a function's name.";
|
|
23
|
+
// The optional type at the end of `var()`'s first argument, as in `attr()`.
|
|
24
|
+
const TYPE = /\s+type\(\s*(.*?)\s*\)\s*$/s;
|
|
25
|
+
const newlines = (text) => text.split('\n').length - 1;
|
|
26
|
+
// Replaces each `${…}` in a block with a placeholder variable. `strings` are
|
|
27
|
+
// the text around the expressions, and `expressions` their source, which is
|
|
28
|
+
// only used for positions, so errors and lines match the file.
|
|
29
|
+
export function interpolate(strings, expressions, lang) {
|
|
30
|
+
const text = strings.join(EXPRESSION);
|
|
31
|
+
const slots = [];
|
|
32
|
+
// The position of the expression at an offset in `text`.
|
|
33
|
+
const positionAt = (offset) => {
|
|
34
|
+
let written = '';
|
|
35
|
+
let index = 0;
|
|
36
|
+
for (let i = 0; i < offset; i++) {
|
|
37
|
+
if (text[i] === EXPRESSION)
|
|
38
|
+
written += `\${${expressions[index++]}}`;
|
|
39
|
+
else
|
|
40
|
+
written += text[i];
|
|
41
|
+
}
|
|
42
|
+
const lastLine = written.lastIndexOf('\n');
|
|
43
|
+
return { line: newlines(written), column: written.length - lastLine };
|
|
44
|
+
};
|
|
45
|
+
const fail = (message, offset) => {
|
|
46
|
+
throw new PreStyleError(message, positionAt(offset));
|
|
47
|
+
};
|
|
48
|
+
// How many expressions come before an offset.
|
|
49
|
+
const expressionIndex = (offset) => text.slice(0, offset).split(EXPRESSION).length - 1;
|
|
50
|
+
// Adds a slot for the text at [start, end), and returns its placeholder's
|
|
51
|
+
// name, and the newlines to write after it, so later lines stay in place.
|
|
52
|
+
const addSlot = (start, end) => {
|
|
53
|
+
const first = expressionIndex(start);
|
|
54
|
+
const parts = text.slice(start, end).split(EXPRESSION);
|
|
55
|
+
const used = Array.from({ length: parts.length - 1 }, (_, i) => first + i);
|
|
56
|
+
const index = slots.push({ strings: parts, expressions: used }) - 1;
|
|
57
|
+
const lines = newlines(text.slice(start, end)) + used.reduce((n, e) => n + newlines(expressions[e]), 0);
|
|
58
|
+
return { name: `${SLOT_PREFIX}${index}`, lines: '\n'.repeat(lines) };
|
|
59
|
+
};
|
|
60
|
+
// What's open: parentheses and Sass's `#{…}`, which don't end statements,
|
|
61
|
+
// and blocks, with the text before each.
|
|
62
|
+
const open = [];
|
|
63
|
+
const atStatementLevel = () => open.length === 0 || open.at(-1).type === '{';
|
|
64
|
+
let statementStart = 0;
|
|
65
|
+
// The end of the string starting at `start`, an error if it has an
|
|
66
|
+
// expression in it.
|
|
67
|
+
const stringEnd = (start) => {
|
|
68
|
+
let i = start + 1;
|
|
69
|
+
for (; i < text.length && text[i] !== text[start]; i++) {
|
|
70
|
+
if (text[i] === '\\')
|
|
71
|
+
i++;
|
|
72
|
+
else if (text[i] === EXPRESSION)
|
|
73
|
+
fail(QUOTED_MESSAGE, i);
|
|
74
|
+
}
|
|
75
|
+
return i + 1;
|
|
76
|
+
};
|
|
77
|
+
// What ends the statement at `start`: `;`, `{`, `}`, or nothing.
|
|
78
|
+
const statementEnd = (start) => {
|
|
79
|
+
let depth = 0;
|
|
80
|
+
for (let i = start; i < text.length; i++) {
|
|
81
|
+
const ch = text[i];
|
|
82
|
+
if (ch === '"' || ch === "'")
|
|
83
|
+
i = stringEnd(i) - 1;
|
|
84
|
+
else if (ch === '(')
|
|
85
|
+
depth++;
|
|
86
|
+
else if (ch === ')')
|
|
87
|
+
depth--;
|
|
88
|
+
else if (ch === '#' && text[i + 1] === '{') {
|
|
89
|
+
depth++;
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
else if (ch === '}' && depth > 0)
|
|
93
|
+
depth--;
|
|
94
|
+
else if (depth === 0 && (ch === ';' || ch === '{' || ch === '}'))
|
|
95
|
+
return ch;
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
};
|
|
99
|
+
// Checks that a slot starting at `start` and ending at `end` is in a
|
|
100
|
+
// declaration's value.
|
|
101
|
+
const checkPosition = (start, end) => {
|
|
102
|
+
const offset = text.indexOf(EXPRESSION, start);
|
|
103
|
+
for (const { type, header } of open)
|
|
104
|
+
if (type === '{' && NO_VARIABLES.test(header.trim()))
|
|
105
|
+
fail(NO_VARIABLES_MESSAGE, offset);
|
|
106
|
+
const statement = text.slice(statementStart, start).trimStart();
|
|
107
|
+
if (statement.startsWith('@')) {
|
|
108
|
+
if (lang !== 'scss' || !SASS_AT_RULES.test(statement))
|
|
109
|
+
fail(POSITION_MESSAGE, offset);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!statement.includes(':') || statementEnd(end) === '{')
|
|
113
|
+
fail(POSITION_MESSAGE, offset);
|
|
114
|
+
};
|
|
115
|
+
// The end of `var()`'s first argument, starting at `start`.
|
|
116
|
+
const firstArgumentEnd = (start) => {
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (let i = start; i < text.length; i++) {
|
|
119
|
+
const ch = text[i];
|
|
120
|
+
if (ch === '"' || ch === "'")
|
|
121
|
+
i = stringEnd(i) - 1;
|
|
122
|
+
else if (ch === '(')
|
|
123
|
+
depth++;
|
|
124
|
+
else if (ch === ')') {
|
|
125
|
+
if (depth === 0)
|
|
126
|
+
return i;
|
|
127
|
+
depth--;
|
|
128
|
+
}
|
|
129
|
+
else if (ch === ',' && depth === 0)
|
|
130
|
+
return i;
|
|
131
|
+
}
|
|
132
|
+
return text.length;
|
|
133
|
+
};
|
|
134
|
+
// The parenthesis that closes the `var()` whose fallback starts at `start`.
|
|
135
|
+
const closingParenthesis = (start) => {
|
|
136
|
+
let end = firstArgumentEnd(start);
|
|
137
|
+
while (text[end] === ',')
|
|
138
|
+
end = firstArgumentEnd(end + 1);
|
|
139
|
+
return end;
|
|
140
|
+
};
|
|
141
|
+
let code = '';
|
|
142
|
+
// Where the current token starts, in `text` and in `code`.
|
|
143
|
+
let tokenStart = 0;
|
|
144
|
+
let tokenCode = 0;
|
|
145
|
+
// Keeps a comment's text, without its expressions, and the lines they took.
|
|
146
|
+
const comment = (start, end) => {
|
|
147
|
+
const first = expressionIndex(start);
|
|
148
|
+
const parts = text.slice(start, end).split(EXPRESSION);
|
|
149
|
+
let lines = '';
|
|
150
|
+
for (let i = 0; i < parts.length - 1; i++)
|
|
151
|
+
lines += '\n'.repeat(newlines(expressions[first + i]));
|
|
152
|
+
return parts.join('') + lines;
|
|
153
|
+
};
|
|
154
|
+
let i = 0;
|
|
155
|
+
while (i < text.length) {
|
|
156
|
+
const ch = text[i];
|
|
157
|
+
if (ch === '/' && text[i + 1] === '*') {
|
|
158
|
+
const close = text.indexOf('*/', i + 2);
|
|
159
|
+
const end = close === -1 ? text.length : close + 2;
|
|
160
|
+
code += comment(i, end);
|
|
161
|
+
i = end;
|
|
162
|
+
tokenStart = i;
|
|
163
|
+
tokenCode = code.length;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (lang === 'scss' && ch === '/' && text[i + 1] === '/') {
|
|
167
|
+
const newline = text.indexOf('\n', i);
|
|
168
|
+
const end = newline === -1 ? text.length : newline;
|
|
169
|
+
code += comment(i, end);
|
|
170
|
+
i = end;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '"' || ch === "'") {
|
|
174
|
+
const end = stringEnd(i);
|
|
175
|
+
code += text.slice(i, end);
|
|
176
|
+
i = end;
|
|
177
|
+
tokenStart = i;
|
|
178
|
+
tokenCode = code.length;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const standsAlone = i === 0 || !IDENT.test(text[i - 1]);
|
|
182
|
+
// An unquoted `url()` is one token, which can't hold a `var()`.
|
|
183
|
+
if (standsAlone && text.slice(i, i + 4).toLowerCase() === 'url(') {
|
|
184
|
+
const argument = /\s*/y;
|
|
185
|
+
argument.lastIndex = i + 4;
|
|
186
|
+
const first = text[i + 4 + argument.exec(text)[0].length];
|
|
187
|
+
if (first !== '"' && first !== "'") {
|
|
188
|
+
const close = text.indexOf(')', i);
|
|
189
|
+
const end = close === -1 ? text.length : close + 1;
|
|
190
|
+
const inside = text.indexOf(EXPRESSION, i);
|
|
191
|
+
if (inside !== -1 && inside < end)
|
|
192
|
+
fail(QUOTED_MESSAGE, inside);
|
|
193
|
+
code += text.slice(i, end);
|
|
194
|
+
i = end;
|
|
195
|
+
tokenStart = i;
|
|
196
|
+
tokenCode = code.length;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// `var(${w}px, 120px)`: the first argument is one slot, and what follows
|
|
201
|
+
// is an ordinary fallback.
|
|
202
|
+
if (standsAlone && text.slice(i, i + 4).toLowerCase() === 'var(') {
|
|
203
|
+
const end = firstArgumentEnd(i + 4);
|
|
204
|
+
const raw = text.slice(i + 4, end);
|
|
205
|
+
const typed = TYPE.exec(raw);
|
|
206
|
+
if (typed?.[1].includes(EXPRESSION))
|
|
207
|
+
fail(TYPE_RUNTIME_MESSAGE, text.indexOf(EXPRESSION, i));
|
|
208
|
+
const argument = typed === null ? raw : raw.slice(0, typed.index);
|
|
209
|
+
if (argument.includes(EXPRESSION)) {
|
|
210
|
+
const start = i + 4 + (argument.length - argument.trimStart().length);
|
|
211
|
+
const trimmedEnd = i + 4 + argument.trimEnd().length;
|
|
212
|
+
checkPosition(start, end);
|
|
213
|
+
// `type(*)` is the same as no type.
|
|
214
|
+
const syntax = typed === null || typed[1] === '*' ? null : typed[1];
|
|
215
|
+
if (syntax !== null) {
|
|
216
|
+
if (text[end] !== ',')
|
|
217
|
+
fail(TYPE_DEFAULT_MESSAGE, start);
|
|
218
|
+
const close = closingParenthesis(end + 1);
|
|
219
|
+
const inDefault = text.slice(end, close).indexOf(EXPRESSION);
|
|
220
|
+
if (inDefault !== -1)
|
|
221
|
+
fail(TYPE_RUNTIME_MESSAGE, end + inDefault);
|
|
222
|
+
}
|
|
223
|
+
const slot = addSlot(start, trimmedEnd);
|
|
224
|
+
const type = syntax === null ? '' : `-t${Buffer.from(syntax).toString('hex')}`;
|
|
225
|
+
const skipped = '\n'.repeat(newlines(text.slice(trimmedEnd, end)));
|
|
226
|
+
code += `var(${slot.name}${type}${slot.lines}${skipped}`;
|
|
227
|
+
open.push({ type: '(', header: '' });
|
|
228
|
+
i = end;
|
|
229
|
+
tokenStart = i;
|
|
230
|
+
tokenCode = code.length;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (ch === EXPRESSION) {
|
|
235
|
+
let end = i;
|
|
236
|
+
while (end < text.length && !DELIMITER.test(text[end]))
|
|
237
|
+
end++;
|
|
238
|
+
if (text[end] === '(')
|
|
239
|
+
fail(FUNCTION_MESSAGE, i);
|
|
240
|
+
checkPosition(tokenStart, end);
|
|
241
|
+
const slot = addSlot(tokenStart, end);
|
|
242
|
+
code = `${code.slice(0, tokenCode)}var(${slot.name})${slot.lines}`;
|
|
243
|
+
i = end;
|
|
244
|
+
tokenStart = i;
|
|
245
|
+
tokenCode = code.length;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === '#' && text[i + 1] === '{') {
|
|
249
|
+
open.push({ type: '#{', header: '' });
|
|
250
|
+
code += '#{';
|
|
251
|
+
i += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (ch === '(')
|
|
255
|
+
open.push({ type: '(', header: '' });
|
|
256
|
+
else if (ch === ')') {
|
|
257
|
+
if (open.at(-1)?.type === '(')
|
|
258
|
+
open.pop();
|
|
259
|
+
}
|
|
260
|
+
else if (ch === '{') {
|
|
261
|
+
if (atStatementLevel()) {
|
|
262
|
+
open.push({ type: '{', header: text.slice(statementStart, i) });
|
|
263
|
+
statementStart = i + 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (ch === '}') {
|
|
267
|
+
if (open.length > 0)
|
|
268
|
+
open.pop();
|
|
269
|
+
if (atStatementLevel())
|
|
270
|
+
statementStart = i + 1;
|
|
271
|
+
}
|
|
272
|
+
else if (ch === ';' && atStatementLevel())
|
|
273
|
+
statementStart = i + 1;
|
|
274
|
+
code += ch;
|
|
275
|
+
i++;
|
|
276
|
+
if (DELIMITER.test(ch)) {
|
|
277
|
+
tokenStart = i;
|
|
278
|
+
tokenCode = code.length;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { code, slots };
|
|
282
|
+
}
|
|
@@ -4,6 +4,7 @@ export interface LibraryConfig {
|
|
|
4
4
|
exclude?: string | string[];
|
|
5
5
|
prependFiles?: PrependFiles;
|
|
6
6
|
keyframeLocalization?: boolean;
|
|
7
|
+
customPropertyLocalization?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface ResolvedLibrary {
|
|
9
10
|
name: string;
|
|
@@ -12,5 +13,6 @@ export interface ResolvedLibrary {
|
|
|
12
13
|
exclude: string[];
|
|
13
14
|
prependFiles?: PrependFiles;
|
|
14
15
|
keyframeLocalization?: boolean;
|
|
16
|
+
customPropertyLocalization?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];
|
|
@@ -3,7 +3,13 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { checkField } from './config.js';
|
|
4
4
|
// Keys a library's field shares with the app's. `libraries` and
|
|
5
5
|
// `quirksMode` only apply to the app.
|
|
6
|
-
const SHARED_KEYS = new Set([
|
|
6
|
+
const SHARED_KEYS = new Set([
|
|
7
|
+
'include',
|
|
8
|
+
'exclude',
|
|
9
|
+
'prependFiles',
|
|
10
|
+
'keyframeLocalization',
|
|
11
|
+
'customPropertyLocalization',
|
|
12
|
+
]);
|
|
7
13
|
const APP_KEYS = new Set(['libraries', 'quirksMode']);
|
|
8
14
|
const DEFAULT_INCLUDE = ['**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,astro}'];
|
|
9
15
|
const readPackage = (dir) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
@@ -44,7 +50,7 @@ export function findLibraries(root, overrides = {}, warn = () => { }) {
|
|
|
44
50
|
for (const warning of checkField(config, where, new Set([...SHARED_KEYS, ...APP_KEYS])))
|
|
45
51
|
warn(warning);
|
|
46
52
|
}
|
|
47
|
-
const { include, exclude, prependFiles, keyframeLocalization } = config === true ? {} : config;
|
|
53
|
+
const { include, exclude, prependFiles, keyframeLocalization, customPropertyLocalization } = config === true ? {} : config;
|
|
48
54
|
libraries.push({
|
|
49
55
|
name,
|
|
50
56
|
dir,
|
|
@@ -52,6 +58,7 @@ export function findLibraries(root, overrides = {}, warn = () => { }) {
|
|
|
52
58
|
exclude: exclude === undefined ? [] : [exclude].flat(),
|
|
53
59
|
prependFiles,
|
|
54
60
|
keyframeLocalization,
|
|
61
|
+
customPropertyLocalization,
|
|
55
62
|
});
|
|
56
63
|
// A library's peer dependencies are installed by the app, and found the
|
|
57
64
|
// same way from the library.
|
package/dist/compiler/mapper.js
CHANGED
|
@@ -29,13 +29,15 @@ async function openProject(params) {
|
|
|
29
29
|
const compiler = createCompiler({
|
|
30
30
|
root: app.dir,
|
|
31
31
|
prependFiles: app.config.prependFiles,
|
|
32
|
-
libraries: libraries.map(({ dir, prependFiles, keyframeLocalization }) => ({
|
|
32
|
+
libraries: libraries.map(({ dir, prependFiles, keyframeLocalization, customPropertyLocalization }) => ({
|
|
33
33
|
dir,
|
|
34
34
|
prependFiles,
|
|
35
35
|
keyframeLocalization,
|
|
36
|
+
customPropertyLocalization,
|
|
36
37
|
})),
|
|
37
38
|
quirksMode: app.config.quirksMode,
|
|
38
39
|
keyframeLocalization: app.config.keyframeLocalization,
|
|
40
|
+
customPropertyLocalization: app.config.customPropertyLocalization,
|
|
39
41
|
warn: () => { },
|
|
40
42
|
});
|
|
41
43
|
projects.set(params.projectHandle, compiler);
|
|
@@ -6,3 +6,6 @@ export declare function compose(parent: SelectorList, child: SelectorList): Sele
|
|
|
6
6
|
export declare function withClass(selectors: SelectorList, className: string): SelectorList;
|
|
7
7
|
export declare function lvhfaRank(selectors: SelectorList): number;
|
|
8
8
|
export declare function repair<T>(selectors: T): T;
|
|
9
|
+
type Specificity = [ids: number, classes: number, types: number];
|
|
10
|
+
export declare function specificity(selectors: SelectorList): Specificity;
|
|
11
|
+
export {};
|
|
@@ -128,3 +128,41 @@ export function repair(selectors) {
|
|
|
128
128
|
};
|
|
129
129
|
return fix(selectors);
|
|
130
130
|
}
|
|
131
|
+
// A selector list's specificity: its most specific selector's.
|
|
132
|
+
export function specificity(selectors) {
|
|
133
|
+
let best = [0, 0, 0];
|
|
134
|
+
for (const selector of selectors) {
|
|
135
|
+
const current = ofSelector(selector);
|
|
136
|
+
const difference = current[0] - best[0] || current[1] - best[1] || current[2] - best[2];
|
|
137
|
+
if (difference > 0)
|
|
138
|
+
best = current;
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
function ofSelector(selector) {
|
|
143
|
+
const total = [0, 0, 0];
|
|
144
|
+
const add = (other) => {
|
|
145
|
+
total[0] += other[0];
|
|
146
|
+
total[1] += other[1];
|
|
147
|
+
total[2] += other[2];
|
|
148
|
+
};
|
|
149
|
+
for (const component of selector) {
|
|
150
|
+
if (component.type === 'id')
|
|
151
|
+
total[0]++;
|
|
152
|
+
else if (component.type === 'class' || component.type === 'attribute')
|
|
153
|
+
total[1]++;
|
|
154
|
+
else if (component.type === 'type' || component.type === 'pseudo-element')
|
|
155
|
+
total[2]++;
|
|
156
|
+
else if (component.type === 'pseudo-class' && component.kind !== 'where') {
|
|
157
|
+
// `:is()`, `:not()`, and `:has()` count as their most specific argument.
|
|
158
|
+
if (component.selectors)
|
|
159
|
+
add(specificity(component.selectors));
|
|
160
|
+
else {
|
|
161
|
+
total[1]++;
|
|
162
|
+
if (component.of)
|
|
163
|
+
add(specificity(component.of));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return total;
|
|
168
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
2
2
|
import { atRuleInfo } from './at-rules.js';
|
|
3
3
|
import { addDeclarations, addGlobal, dropOverridden, key, parseRules, PreStyleError, valueOf, walk, } from './block.js';
|
|
4
|
+
import { localizeCustomProperties } from './custom-properties.js';
|
|
4
5
|
import { localizeKeyframes } from './keyframes.js';
|
|
5
6
|
import { compose, isElement } from './selectors.js';
|
|
6
7
|
// Classes used as context for another class (`.card:hover .title`), or in a
|
|
@@ -145,6 +146,14 @@ export function parseStylesheet(code, lang, file, id, options) {
|
|
|
145
146
|
}
|
|
146
147
|
for (const target of classes.values())
|
|
147
148
|
target.atoms = dropOverridden(target.atoms);
|
|
149
|
+
// Custom properties are local to the whole file, like keyframes.
|
|
150
|
+
if (options.customPropertyLocalization) {
|
|
151
|
+
const all = [...classes.values()].flatMap((target) => target.atoms);
|
|
152
|
+
const local = localizeCustomProperties(all, parsed.globals, `file\n${id}`, lang, options);
|
|
153
|
+
parsed.globals = local.globals;
|
|
154
|
+
for (const target of classes.values())
|
|
155
|
+
target.atoms = local.atoms(target.atoms);
|
|
156
|
+
}
|
|
148
157
|
// Keyframes are local to the whole file, as in CSS Modules.
|
|
149
158
|
if (options.keyframeLocalization) {
|
|
150
159
|
const local = localizeKeyframes(parsed.globals);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Atom, GlobalRule } from './block.ts';
|
|
2
|
+
export declare const VARIABLE_PREFIX = "--\0pre.style-variable:";
|
|
3
|
+
export interface Variable {
|
|
4
|
+
key: string;
|
|
5
|
+
property: string | null;
|
|
6
|
+
slot: number;
|
|
7
|
+
selector: string[] | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function extractVariables(atoms: Atom[], globals: GlobalRule[]): {
|
|
10
|
+
atoms: Atom[];
|
|
11
|
+
globals: GlobalRule[];
|
|
12
|
+
variables: Variable[];
|
|
13
|
+
};
|