@pre.style/core 3.1.0-alpha.2 → 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 +383 -3
- 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 +17 -0
- package/dist/compiler/compiler.js +90 -22
- 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/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);
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { transform } from 'lightningcss';
|
|
3
|
+
import { key, overrideKey, PreStyleError, valueOf } from './block.js';
|
|
4
|
+
import { atRuleInfo } from './at-rules.js';
|
|
5
|
+
import { SLOT_PREFIX } from './interpolate.js';
|
|
6
|
+
import { print } from './print.js';
|
|
7
|
+
import { withClass } from './selectors.js';
|
|
8
|
+
// A variable a block's runtime values set. In parsed rules, a generated
|
|
9
|
+
// variable's name is this prefix and a hash of what it's for, and the compiler
|
|
10
|
+
// gives it a name when printing, like localized keyframes.
|
|
11
|
+
export const VARIABLE_PREFIX = '--\0pre.style-variable:';
|
|
12
|
+
const isSlot = (node) => {
|
|
13
|
+
const value = node;
|
|
14
|
+
return value?.type === 'var' && value.value.name.ident.startsWith(SLOT_PREFIX);
|
|
15
|
+
};
|
|
16
|
+
// A placeholder's slot, and its type from `type()`, which is in its name (in
|
|
17
|
+
// hex) so that blocks differing only by type don't share a parse.
|
|
18
|
+
const parseSlot = (node) => {
|
|
19
|
+
const [, slot, type] = /^(\d+)(?:-t([\da-f]+))?/.exec(node.value.name.ident.slice(SLOT_PREFIX.length));
|
|
20
|
+
return {
|
|
21
|
+
slot: Number(slot),
|
|
22
|
+
syntax: type === undefined ? null : Buffer.from(type, 'hex').toString(),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
// Calls `visit` for each slot in a value, in the order they're written.
|
|
26
|
+
function eachSlot(value, visit) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value)
|
|
29
|
+
eachSlot(item, visit);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (value === null || typeof value !== 'object')
|
|
33
|
+
return;
|
|
34
|
+
if (isSlot(value))
|
|
35
|
+
visit(value);
|
|
36
|
+
for (const child of Object.values(value))
|
|
37
|
+
eachSlot(child, visit);
|
|
38
|
+
}
|
|
39
|
+
const IGNORED = (token) => token.type === 'token' && token.value?.type === 'white-space';
|
|
40
|
+
// The slot a declaration is entirely made of, for `--brand: ${color}` directly
|
|
41
|
+
// on the element, which can set `--brand` itself. Under at-rules or a selector
|
|
42
|
+
// variant, the generated class would have to be too, so those use a variable.
|
|
43
|
+
function directSlot(atom) {
|
|
44
|
+
if (atom.atRules.length > 0 || atom.variant !== null || atom.declaration.important)
|
|
45
|
+
return null;
|
|
46
|
+
const declaration = atom.declaration.value;
|
|
47
|
+
// Lightning CSS also uses `custom` for properties it doesn't parse, such as
|
|
48
|
+
// `content` with a `var()`.
|
|
49
|
+
if (declaration.property !== 'custom' || !atom.property.startsWith('--'))
|
|
50
|
+
return null;
|
|
51
|
+
const tokens = declaration.value.value.filter((t) => !IGNORED(t));
|
|
52
|
+
const [token] = tokens;
|
|
53
|
+
if (tokens.length !== 1 || !isSlot(token))
|
|
54
|
+
return null;
|
|
55
|
+
return !token.value.fallback || parseSlot(token).syntax !== null ? token : null;
|
|
56
|
+
}
|
|
57
|
+
const hash = (identity) => createHash('sha256').update(JSON.stringify(identity)).digest('base64url').slice(0, 16);
|
|
58
|
+
const SELF = '__pre-style-self__';
|
|
59
|
+
// A declaration's selector, for where to set its variables. Null when it's the
|
|
60
|
+
// element itself, as for `&:hover` and `.dark &`.
|
|
61
|
+
function selectorOf(atom) {
|
|
62
|
+
if (atom.variant === null)
|
|
63
|
+
return null;
|
|
64
|
+
const selectors = withClass(atom.variant.selectors, SELF);
|
|
65
|
+
const onElement = selectors.every((selector) => {
|
|
66
|
+
const compound = selector.slice(selector.findLastIndex((c) => c.type === 'combinator') + 1);
|
|
67
|
+
return (compound.some((c) => c.type === 'class' && c.name === SELF) &&
|
|
68
|
+
!compound.some((c) => c.type === 'pseudo-element'));
|
|
69
|
+
});
|
|
70
|
+
if (onElement)
|
|
71
|
+
return null;
|
|
72
|
+
const rule = {
|
|
73
|
+
type: 'style',
|
|
74
|
+
value: {
|
|
75
|
+
selectors,
|
|
76
|
+
// Empty rules aren't printed.
|
|
77
|
+
declarations: {
|
|
78
|
+
declarations: [
|
|
79
|
+
{
|
|
80
|
+
property: 'custom',
|
|
81
|
+
value: {
|
|
82
|
+
name: '--z',
|
|
83
|
+
value: [{ type: 'token', value: { type: 'ident', value: 'z' } }],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
rules: [],
|
|
89
|
+
loc: { source_index: 0, line: 0, column: 1 },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const { code } = print([rule], [], {}, false);
|
|
93
|
+
return code
|
|
94
|
+
.slice(0, code.indexOf('{'))
|
|
95
|
+
.trim()
|
|
96
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
97
|
+
.split(`.${SELF}`);
|
|
98
|
+
}
|
|
99
|
+
const atomIdentity = (atom, index) => [
|
|
100
|
+
atom.atRules.map((segment) => segment.key),
|
|
101
|
+
atom.variant?.key ?? '',
|
|
102
|
+
atom.property,
|
|
103
|
+
index,
|
|
104
|
+
];
|
|
105
|
+
// `@property --a { syntax: "*"; inherits: false; }`. Without it, an element
|
|
106
|
+
// with no value of its own takes its parent's instead of its default.
|
|
107
|
+
const registration = (name) => {
|
|
108
|
+
const rule = {
|
|
109
|
+
type: 'property',
|
|
110
|
+
value: {
|
|
111
|
+
name,
|
|
112
|
+
syntax: { type: 'universal' },
|
|
113
|
+
inherits: false,
|
|
114
|
+
initialValue: null,
|
|
115
|
+
loc: { source_index: 0, line: 0, column: 1 },
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
key: key(rule),
|
|
120
|
+
rule,
|
|
121
|
+
hoist: atRuleInfo('property').hoist,
|
|
122
|
+
overrideKey: null,
|
|
123
|
+
position: null,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
// Units and functions that depend on where they're used. Browsers drop a
|
|
127
|
+
// typed `@property` whose `initial-value` has them (tested in Chromium), and
|
|
128
|
+
// Lightning CSS doesn't report them.
|
|
129
|
+
const NOT_INDEPENDENT = /(?<![\w-])[+-]?[\d.]+(?:e[+-]?\d+)?(?:em|rem|ex|rex|ch|rch|lh|rlh|cap|rcap|ic|ric|cqw|cqh|cqi|cqb|cqmin|cqmax)\b|\b(?:env|attr)\(/i;
|
|
130
|
+
const QUOTED = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
|
|
131
|
+
// A slot's default, as CSS.
|
|
132
|
+
function defaultText(node) {
|
|
133
|
+
const rule = {
|
|
134
|
+
type: 'style',
|
|
135
|
+
value: {
|
|
136
|
+
selectors: [[{ type: 'class', name: 'z' }]],
|
|
137
|
+
declarations: {
|
|
138
|
+
declarations: [
|
|
139
|
+
{ property: 'custom', value: { name: '--z', value: node.value.fallback ?? [] } },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
rules: [],
|
|
143
|
+
loc: { source_index: 0, line: 0, column: 1 },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const { code } = print([rule], [], {}, false);
|
|
147
|
+
return /--z:\s*(.*?);?\s*\}/s.exec(code)[1];
|
|
148
|
+
}
|
|
149
|
+
// `@property --a { syntax: "<length>"; inherits: false; initial-value: 1px; }`.
|
|
150
|
+
function typedRegistration(name, syntax, initial, position) {
|
|
151
|
+
const invalid = (reason, cause) => new PreStyleError(`\`${initial}\` can't be the default of \`type(${syntax})\`: ${reason}`, position, { cause });
|
|
152
|
+
if (NOT_INDEPENDENT.test(initial.replace(QUOTED, '')))
|
|
153
|
+
throw invalid("a typed variable's default can't depend on where it's used, such as `em`, `rem`, container units, `env()`, or `attr()`, or browsers drop its `@property` rule. Use an absolute value, or leave out `type()`.");
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
transform({
|
|
157
|
+
filename: 'type.css',
|
|
158
|
+
code: Buffer.from(`@property --z{syntax:${JSON.stringify(syntax)};inherits:false;initial-value:${initial}}`),
|
|
159
|
+
visitor: {
|
|
160
|
+
StyleSheetExit(sheet) {
|
|
161
|
+
[parsed] = sheet.rules;
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw invalid(`it isn't valid for that type (${error.message}).`, error);
|
|
168
|
+
}
|
|
169
|
+
const rule = { ...parsed, value: { ...valueOf(parsed), name } };
|
|
170
|
+
return {
|
|
171
|
+
key: key(rule),
|
|
172
|
+
rule,
|
|
173
|
+
hoist: atRuleInfo('property').hoist,
|
|
174
|
+
overrideKey: null,
|
|
175
|
+
position,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const SLOT_MESSAGE = "`${…}` can only be used in a declaration's value, because it becomes a `var()`, which isn't valid here.";
|
|
179
|
+
// Points a slot at its variable, and registers the variable. A typed
|
|
180
|
+
// variable's default moves to its `initial-value`, so each type and default
|
|
181
|
+
// has its own variable.
|
|
182
|
+
function useVariable(node, identity, slot, syntax, variables, added, position, selector = null) {
|
|
183
|
+
const initial = syntax === null ? null : defaultText(node);
|
|
184
|
+
const variable = hash([identity, syntax, initial]);
|
|
185
|
+
const name = VARIABLE_PREFIX + variable;
|
|
186
|
+
variables.push({ key: variable, property: null, slot, selector });
|
|
187
|
+
node.value.name.ident = name;
|
|
188
|
+
if (syntax !== null)
|
|
189
|
+
node.value.fallback = null;
|
|
190
|
+
if (added.has(variable))
|
|
191
|
+
return;
|
|
192
|
+
added.set(variable, syntax === null ? registration(name) : typedRegistration(name, syntax, initial, position));
|
|
193
|
+
}
|
|
194
|
+
// Replaces the slots in a global rule, such as a `@keyframes`, with variables
|
|
195
|
+
// identified by the rule and their place in it, so the same rule in any block
|
|
196
|
+
// shares them.
|
|
197
|
+
function globalVariables(global, variables, added) {
|
|
198
|
+
const rule = structuredClone(global.rule);
|
|
199
|
+
const slots = [];
|
|
200
|
+
eachSlot(rule, (node) => slots.push(node));
|
|
201
|
+
const parsed = slots.map(parseSlot);
|
|
202
|
+
slots.forEach((node, index) => {
|
|
203
|
+
const type = parsed[index].syntax === null ? '' : `:${parsed[index].syntax}`;
|
|
204
|
+
node.value.name.ident = `${SLOT_PREFIX}${index}${type}`;
|
|
205
|
+
});
|
|
206
|
+
const identity = key(rule);
|
|
207
|
+
slots.forEach((node, index) => {
|
|
208
|
+
const { slot, syntax } = parsed[index];
|
|
209
|
+
useVariable(node, [identity, index], slot, syntax, variables, added, global.position);
|
|
210
|
+
});
|
|
211
|
+
const json = key(rule);
|
|
212
|
+
// Anywhere else, such as a selector Sass built, it isn't a `var()` node.
|
|
213
|
+
if (json.includes(SLOT_PREFIX))
|
|
214
|
+
throw new PreStyleError(SLOT_MESSAGE, global.position);
|
|
215
|
+
return { ...global, key: json, rule };
|
|
216
|
+
}
|
|
217
|
+
// Turns a parsed block's placeholder variables into its real ones. Runs after
|
|
218
|
+
// overridden atoms are dropped, so their slots are left out. Variables are
|
|
219
|
+
// identified by what they're for, so `width: ${w}px` in any block shares one
|
|
220
|
+
// variable and class.
|
|
221
|
+
export function extractVariables(atoms, globals) {
|
|
222
|
+
const variables = [];
|
|
223
|
+
const added = new Map();
|
|
224
|
+
const kept = globals.map((global) => global.key.includes(SLOT_PREFIX) ? globalVariables(global, variables, added) : global);
|
|
225
|
+
const keptAtoms = [];
|
|
226
|
+
for (const atom of atoms) {
|
|
227
|
+
const prefix = atom.atRules.map((segment) => segment.key).join() + (atom.variant?.key ?? '');
|
|
228
|
+
// Sass can move a placeholder, so its position is checked again here.
|
|
229
|
+
if (prefix.includes(SLOT_PREFIX))
|
|
230
|
+
throw new PreStyleError(SLOT_MESSAGE, atom.position);
|
|
231
|
+
if (!atom.declaration.key.includes(SLOT_PREFIX)) {
|
|
232
|
+
keptAtoms.push(atom);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const direct = directSlot(atom);
|
|
236
|
+
if (direct !== null) {
|
|
237
|
+
const { slot, syntax } = parseSlot(direct);
|
|
238
|
+
variables.push({
|
|
239
|
+
key: `direct:${atom.property}`,
|
|
240
|
+
property: atom.property,
|
|
241
|
+
slot,
|
|
242
|
+
selector: null,
|
|
243
|
+
});
|
|
244
|
+
// A typed custom property registers itself, under its own name.
|
|
245
|
+
if (syntax !== null) {
|
|
246
|
+
const registered = typedRegistration(atom.property, syntax, defaultText(direct), atom.position);
|
|
247
|
+
added.set(`direct:${atom.property}:${registered.key}`, {
|
|
248
|
+
...registered,
|
|
249
|
+
overrideKey: overrideKey(registered.rule),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const value = structuredClone(atom.declaration.value);
|
|
255
|
+
const selector = selectorOf(atom);
|
|
256
|
+
let index = 0;
|
|
257
|
+
eachSlot(value, (node) => {
|
|
258
|
+
const { slot, syntax } = parseSlot(node);
|
|
259
|
+
const identity = atomIdentity(atom, index++);
|
|
260
|
+
useVariable(node, identity, slot, syntax, variables, added, atom.position, selector);
|
|
261
|
+
});
|
|
262
|
+
keptAtoms.push({
|
|
263
|
+
...atom,
|
|
264
|
+
declaration: {
|
|
265
|
+
...atom.declaration,
|
|
266
|
+
key: key(value) + (atom.declaration.important ? '!' : ''),
|
|
267
|
+
value,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return { atoms: keptAtoms, globals: [...kept, ...added.values()], variables };
|
|
272
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export declare function
|
|
1
|
+
export type RuntimeValue = string | number | null | undefined;
|
|
2
|
+
export declare function css(_strings: TemplateStringsArray, ..._values: RuntimeValue[]): string;
|
|
3
|
+
export declare function scss(_strings: TemplateStringsArray, ..._values: RuntimeValue[]): string;
|
|
3
4
|
import { createCn } from './cn.ts';
|
|
4
5
|
export declare const cn: (...inputs: import("./cn.ts").ClassValue[]) => string;
|
|
5
6
|
export { createCn };
|
|
6
|
-
export type { ClassDictionary, ClassValue, CnTable } from './cn.ts';
|
|
7
|
+
export type { ClassDictionary, ClassValue, CnLookup, CnTable } from './cn.ts';
|