@pre.style/core 3.1.0-alpha.3 → 3.1.0-alpha.4
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 +47 -21
- package/dist/compiler/interpolate.js +57 -21
- package/dist/runtime.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -246,14 +246,10 @@ css`
|
|
|
246
246
|
}
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
Values work in nested selectors too, including pseudo-elements and other elements. The generated class's rule uses the same selector, so the value reaches them
|
|
249
|
+
Values work in nested selectors too, including pseudo-elements and other elements. The generated class's rule uses the same selector, so the value reaches them:
|
|
250
250
|
|
|
251
251
|
```js
|
|
252
252
|
css`
|
|
253
|
-
&::before {
|
|
254
|
-
content: var(${JSON.stringify(String(count))}, "0");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
253
|
& > * {
|
|
258
254
|
margin-top: ${gap}px;
|
|
259
255
|
}
|
|
@@ -266,11 +262,38 @@ css`
|
|
|
266
262
|
|
|
267
263
|
```css
|
|
268
264
|
/* Inserted by the runtime */
|
|
269
|
-
._A3fK9
|
|
270
|
-
--A:
|
|
265
|
+
._A3fK9 > * {
|
|
266
|
+
--A: 8px;
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Strings and URLs
|
|
271
|
+
|
|
272
|
+
Write quotes around `${…}` for a string, with any text you like. The runtime escapes the value, and numbers work too. `url()` needs its quotes:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
css`
|
|
276
|
+
background-image: url("/img/${name}.png");
|
|
277
|
+
|
|
278
|
+
&::before {
|
|
279
|
+
content: var("Count: ${count}", "Loading");
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```css
|
|
285
|
+
/* Inserted by the runtime */
|
|
286
|
+
._A3fK9 {
|
|
287
|
+
--A: url("/img/photo.png");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
._B7xQ2::before {
|
|
291
|
+
--B: "Count: 5";
|
|
271
292
|
}
|
|
272
293
|
```
|
|
273
294
|
|
|
295
|
+
The whole string is the value, so a default goes around it, as above. `content: ${x}` without quotes doesn't add any: use it for values that are already CSS, such as `none` or `counter(n)`. In `scss` blocks, `#{…}` can't be in the same value as `${…}`.
|
|
296
|
+
|
|
274
297
|
#### Custom properties
|
|
275
298
|
|
|
276
299
|
Name a custom property to share a value: with other elements, which inherit it, with something else that reads that name, or across several declarations in a block. For a value that's only used on one element, write `${…}` directly instead. A name is global, so two blocks using the same one can affect each other. To avoid that, use distinctive names, or [`customPropertyLocalization`](#local-custom-properties).
|
|
@@ -408,18 +431,7 @@ css`
|
|
|
408
431
|
margin-${side}: 0; /* property names */
|
|
409
432
|
@media (min-width: ${bp}px) { top: 0; } /* at-rule conditions, see Sharing media queries */
|
|
410
433
|
@font-face { font-weight: ${weight}; } /* @font-face, @property, @page, and similar */
|
|
411
|
-
|
|
412
|
-
background-image: url(${path}); /* url() */
|
|
413
|
-
`;
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
For strings and URLs, pass the whole value:
|
|
417
|
-
|
|
418
|
-
```js
|
|
419
|
-
css`
|
|
420
|
-
content: ${JSON.stringify(label)};
|
|
421
|
-
--image: ${`url("${path}")`};
|
|
422
|
-
background-image: var(--image);
|
|
434
|
+
background-image: url(${path}); /* url() without quotes */
|
|
423
435
|
`;
|
|
424
436
|
```
|
|
425
437
|
|
|
@@ -448,16 +460,30 @@ In older browsers:
|
|
|
448
460
|
|
|
449
461
|
#### When to use `style` instead
|
|
450
462
|
|
|
451
|
-
|
|
463
|
+
A block compiles to a class string, so the runtime can't tell which element it's for. That's why each distinct value gets its own class, rather than one class whose value changes: two elements from the same block can have different values, as in a list. So the class changes with the value, and every new value adds a rule that's never removed.
|
|
464
|
+
|
|
465
|
+
That suits values from a small set, such as a theme color or a chosen size, where going back to a value reuses its class. For values that change constantly, such as a counter, a drag position, or anything set per frame, use the `style` attribute and read it from a static block. The class never changes, no rules are added, the server renders the real value, and no runtime is needed:
|
|
452
466
|
|
|
453
467
|
```jsx
|
|
454
468
|
const box = css`
|
|
455
|
-
transform: translateX(var(--x));
|
|
469
|
+
transform: translateX(var(--x, 0));
|
|
456
470
|
`;
|
|
457
471
|
|
|
458
472
|
<div className={box} style={{ "--x": `${x}px` }} />;
|
|
459
473
|
```
|
|
460
474
|
|
|
475
|
+
| | `${…}` | `style` |
|
|
476
|
+
| --- | --- | --- |
|
|
477
|
+
| The class | Changes with the value | Stays the same |
|
|
478
|
+
| CSS rules | One per distinct value, kept | None added |
|
|
479
|
+
| On the server | The default, until the runtime runs | The real value |
|
|
480
|
+
| In `cn` | Later values win | Not involved |
|
|
481
|
+
| Runtime | Needed | Not needed |
|
|
482
|
+
| In pseudo-elements and other elements | Reaches them | Only if the property inherits |
|
|
483
|
+
| Best for | Values from a small set | Values that change constantly |
|
|
484
|
+
|
|
485
|
+
The demos' Interpolation pages show both side by side.
|
|
486
|
+
|
|
461
487
|
### Selectors
|
|
462
488
|
|
|
463
489
|
Use `&` for the element, as in native CSS nesting. Anything that isn't the bare element (pseudo-classes, pseudo-elements, and complex selectors) becomes a selector variant:
|
|
@@ -16,7 +16,8 @@ const NO_VARIABLES = /^@(font-face|property|page|counter-style|font-feature-valu
|
|
|
16
16
|
const SASS_AT_RULES = /^@(include|return|if|else|each|for|while|debug|warn|error|function|mixin)\b/;
|
|
17
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
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
|
|
19
|
+
const UNQUOTED_URL_MESSAGE = '`${…}` can\'t be used in an unquoted `url()`. Add quotes: `url("${path}")`.';
|
|
20
|
+
const SASS_MESSAGE = "`#{…}` can't be used in the same value as `${…}`, because that value is set at runtime, after Sass has run. Use a Sass variable in a separate declaration.";
|
|
20
21
|
const TYPE_RUNTIME_MESSAGE = "`type()` and its default are written to the CSS file, so they can't use `${…}`.";
|
|
21
22
|
const TYPE_DEFAULT_MESSAGE = '`type()` needs a default, because a typed `@property` requires an `initial-value`: `var(${w}px type(<length>), 120px)`.';
|
|
22
23
|
const FUNCTION_MESSAGE = "`${…}` can't be used as a function's name.";
|
|
@@ -50,11 +51,26 @@ export function interpolate(strings, expressions, lang) {
|
|
|
50
51
|
// Adds a slot for the text at [start, end), and returns its placeholder's
|
|
51
52
|
// name, and the newlines to write after it, so later lines stay in place.
|
|
52
53
|
const addSlot = (start, end) => {
|
|
54
|
+
const span = text.slice(start, end);
|
|
55
|
+
if (lang === 'scss' && span.includes('#{'))
|
|
56
|
+
fail(SASS_MESSAGE, start);
|
|
53
57
|
const first = expressionIndex(start);
|
|
54
|
-
const parts =
|
|
55
|
-
const used =
|
|
58
|
+
const parts = span.split(EXPRESSION);
|
|
59
|
+
const used = [];
|
|
60
|
+
let quote = null;
|
|
61
|
+
for (let i = 0; i < span.length; i++) {
|
|
62
|
+
const ch = span[i];
|
|
63
|
+
if (ch === '\\')
|
|
64
|
+
i++;
|
|
65
|
+
else if (ch === EXPRESSION) {
|
|
66
|
+
const index = first + used.length;
|
|
67
|
+
used.push(quote === null ? index : ~index);
|
|
68
|
+
}
|
|
69
|
+
else if (quote === null ? ch === '"' || ch === "'" : ch === quote)
|
|
70
|
+
quote = quote === null ? ch : null;
|
|
71
|
+
}
|
|
56
72
|
const index = slots.push({ strings: parts, expressions: used }) - 1;
|
|
57
|
-
const lines = newlines(
|
|
73
|
+
const lines = newlines(span) + used.reduce((n, e) => n + newlines(expressions[e < 0 ? ~e : e]), 0);
|
|
58
74
|
return { name: `${SLOT_PREFIX}${index}`, lines: '\n'.repeat(lines) };
|
|
59
75
|
};
|
|
60
76
|
// What's open: parentheses and Sass's `#{…}`, which don't end statements,
|
|
@@ -62,15 +78,14 @@ export function interpolate(strings, expressions, lang) {
|
|
|
62
78
|
const open = [];
|
|
63
79
|
const atStatementLevel = () => open.length === 0 || open.at(-1).type === '{';
|
|
64
80
|
let statementStart = 0;
|
|
65
|
-
//
|
|
66
|
-
|
|
81
|
+
// Where the last `#{…}` ended, since it can't touch a slot.
|
|
82
|
+
let sassEnd = -1;
|
|
83
|
+
// The end of the string starting at `start`.
|
|
67
84
|
const stringEnd = (start) => {
|
|
68
85
|
let i = start + 1;
|
|
69
86
|
for (; i < text.length && text[i] !== text[start]; i++) {
|
|
70
87
|
if (text[i] === '\\')
|
|
71
88
|
i++;
|
|
72
|
-
else if (text[i] === EXPRESSION)
|
|
73
|
-
fail(QUOTED_MESSAGE, i);
|
|
74
89
|
}
|
|
75
90
|
return i + 1;
|
|
76
91
|
};
|
|
@@ -172,25 +187,43 @@ export function interpolate(strings, expressions, lang) {
|
|
|
172
187
|
}
|
|
173
188
|
if (ch === '"' || ch === "'") {
|
|
174
189
|
const end = stringEnd(i);
|
|
175
|
-
|
|
190
|
+
const string = text.slice(i, end);
|
|
191
|
+
// `"Count: ${i}"`: the whole string is the value, since a `var()` can't
|
|
192
|
+
// go inside one.
|
|
193
|
+
if (string.includes(EXPRESSION)) {
|
|
194
|
+
checkPosition(i, end);
|
|
195
|
+
const slot = addSlot(i, end);
|
|
196
|
+
code += `var(${slot.name})${slot.lines}`;
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
code += string;
|
|
176
200
|
i = end;
|
|
177
201
|
tokenStart = i;
|
|
178
202
|
tokenCode = code.length;
|
|
179
203
|
continue;
|
|
180
204
|
}
|
|
181
205
|
const standsAlone = i === 0 || !IDENT.test(text[i - 1]);
|
|
182
|
-
//
|
|
206
|
+
// A `url()` can't hold a `var()`, so it's a value as a whole.
|
|
183
207
|
if (standsAlone && text.slice(i, i + 4).toLowerCase() === 'url(') {
|
|
184
208
|
const argument = /\s*/y;
|
|
185
209
|
argument.lastIndex = i + 4;
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
const firstAt = i + 4 + argument.exec(text)[0].length;
|
|
211
|
+
const first = text[firstAt];
|
|
212
|
+
const quoted = first === '"' || first === "'";
|
|
213
|
+
const close = text.indexOf(')', quoted ? stringEnd(firstAt) : i);
|
|
214
|
+
const end = close === -1 ? text.length : close + 1;
|
|
215
|
+
const inside = text.slice(i, end).indexOf(EXPRESSION);
|
|
216
|
+
if (inside !== -1 && !quoted)
|
|
217
|
+
fail(UNQUOTED_URL_MESSAGE, i + inside);
|
|
218
|
+
if (inside !== -1 || !quoted) {
|
|
219
|
+
// `url("${path}")`: the whole `url()` is the value.
|
|
220
|
+
if (inside !== -1) {
|
|
221
|
+
checkPosition(i, end);
|
|
222
|
+
const slot = addSlot(i, end);
|
|
223
|
+
code += `var(${slot.name})${slot.lines}`;
|
|
224
|
+
}
|
|
225
|
+
else
|
|
226
|
+
code += text.slice(i, end);
|
|
194
227
|
i = end;
|
|
195
228
|
tokenStart = i;
|
|
196
229
|
tokenCode = code.length;
|
|
@@ -237,6 +270,8 @@ export function interpolate(strings, expressions, lang) {
|
|
|
237
270
|
end++;
|
|
238
271
|
if (text[end] === '(')
|
|
239
272
|
fail(FUNCTION_MESSAGE, i);
|
|
273
|
+
if (tokenStart === sassEnd || (text[end - 1] === '#' && text[end] === '{'))
|
|
274
|
+
fail(SASS_MESSAGE, i);
|
|
240
275
|
checkPosition(tokenStart, end);
|
|
241
276
|
const slot = addSlot(tokenStart, end);
|
|
242
277
|
code = `${code.slice(0, tokenCode)}var(${slot.name})${slot.lines}`;
|
|
@@ -264,9 +299,10 @@ export function interpolate(strings, expressions, lang) {
|
|
|
264
299
|
}
|
|
265
300
|
}
|
|
266
301
|
else if (ch === '}') {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
302
|
+
// Sass's `#{…}` is part of a value, so it doesn't end a statement.
|
|
303
|
+
if (open.pop()?.type === '#{')
|
|
304
|
+
sassEnd = i + 1;
|
|
305
|
+
else if (atStatementLevel())
|
|
270
306
|
statementStart = i + 1;
|
|
271
307
|
}
|
|
272
308
|
else if (ch === ';' && atStatementLevel())
|
package/dist/runtime.js
CHANGED
|
@@ -19,6 +19,8 @@ function hash(value, alphabet) {
|
|
|
19
19
|
}
|
|
20
20
|
return result;
|
|
21
21
|
}
|
|
22
|
+
// What can't be written as it is in a quoted CSS string.
|
|
23
|
+
const UNSAFE_IN_STRING = /["'\\\n\r\f]/g;
|
|
22
24
|
// The variable's value, or null when any of its values is null or undefined,
|
|
23
25
|
// so its default applies. Anything else is almost always a bug, which falling
|
|
24
26
|
// back would hide.
|
|
@@ -26,11 +28,15 @@ function text(strings, indexes, values, invalid) {
|
|
|
26
28
|
let result = strings[0];
|
|
27
29
|
let missing = false;
|
|
28
30
|
for (let i = 0; i < indexes.length; i++) {
|
|
29
|
-
const
|
|
31
|
+
const index = indexes[i];
|
|
32
|
+
const value = values[index < 0 ? ~index : index];
|
|
30
33
|
if (value === null || value === undefined)
|
|
31
34
|
missing = true;
|
|
32
35
|
else if (typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value)))
|
|
33
|
-
result +=
|
|
36
|
+
result +=
|
|
37
|
+
(index < 0
|
|
38
|
+
? String(value).replace(UNSAFE_IN_STRING, (c) => `\\${c.charCodeAt(0).toString(16)} `)
|
|
39
|
+
: value) + strings[i + 1];
|
|
34
40
|
else
|
|
35
41
|
invalid(value, strings);
|
|
36
42
|
}
|