@jxsuite/runtime 3.1.0 → 4.0.1
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/dist/runtime.js +1460 -516
- package/dist/runtime.js.map +8 -8
- package/package.json +3 -3
- package/src/css.ts +767 -3
- package/src/expression.ts +34 -2
- package/src/runtime.ts +1917 -330
- package/src/statements.ts +42 -5
- package/src/types.ts +73 -1
package/src/css.ts
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* The CSS-authoring rules that are not the DOM.
|
|
3
3
|
*
|
|
4
4
|
* `@jxsuite/runtime` renders a document in a browser, and every consumer of its root export pays
|
|
5
|
-
* for that: the element registry, the reactive scope, `@vue/reactivity`. These
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* for that: the element registry, the reactive scope, `@vue/reactivity`. These exports need none of
|
|
6
|
+
* it. They are pure string and regex math over what a `style` block MEANS — how a property name is
|
|
7
|
+
* spelled in CSS, which media query an `@`-key resolves to, and which selector pair a
|
|
8
8
|
* scheme-conditional rule dual-emits as.
|
|
9
9
|
*
|
|
10
|
+
* The rule builder joined them for the same reason: it is what a style object MEANS as a list of
|
|
11
|
+
* CSS rules, and three emitters used to answer that question three different ways. Its only import
|
|
12
|
+
* is `@jxsuite/schema/guards`, which imports nothing but types.
|
|
13
|
+
*
|
|
10
14
|
* They are a subpath of their own because of who else needs them. `@jxsuite/site/site-style` turns
|
|
11
15
|
* `project.json`'s `style` into a stylesheet, and it runs in places the DOM runtime cannot go — a
|
|
12
16
|
* static build, and a Cloudflare Worker serving a live preview. Importing the root export for
|
|
@@ -17,6 +21,9 @@
|
|
|
17
21
|
* has to change. This is a narrow door beside the wide one, not a replacement for it.
|
|
18
22
|
*/
|
|
19
23
|
|
|
24
|
+
import { isRef, isTemplateString } from "@jxsuite/schema/guards";
|
|
25
|
+
import type { JxRef, JxStyle } from "@jxsuite/schema/types";
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Convert camelCase to kebab-case.
|
|
22
29
|
*
|
|
@@ -60,6 +67,262 @@ export function resolveAtQuery(atKey: string, mediaQueries: Record<string, strin
|
|
|
60
67
|
return null;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* At-rules whose body is a list of DECLARATIONS rather than a list of rules.
|
|
72
|
+
*
|
|
73
|
+
* Every other `@`-key in a `style` object wraps selectors — `@media`, `@supports`,
|
|
74
|
+
* `@starting-style` — so both emitters assume a selector child and write `@key { <selector> { … }
|
|
75
|
+
* }`. These four have no selector: `@position-try --flip { inset-block-start: auto }` IS the body,
|
|
76
|
+
* and wrapping it in one produces a block the parser discards without a word.
|
|
77
|
+
*
|
|
78
|
+
* That is why an anchored dropdown had no custom fallback to declare. `position-try-fallbacks` can
|
|
79
|
+
* name `flip-block` and friends without this, but a bespoke fallback position — the thing you need
|
|
80
|
+
* when the built-in flips do not fit — could not be expressed at all.
|
|
81
|
+
*
|
|
82
|
+
* The name is part of the key (`@position-try --flip`), so this is a PREFIX match. `@keyframes` is
|
|
83
|
+
* deliberately absent, and stays absent: its body is neither declarations nor selectors but
|
|
84
|
+
* keyframe stops, which is a third shape with its own predicate ({@link isKeyframesAtRule}) and its
|
|
85
|
+
* own serializer. Adding it here would be the tempting one-line fix and it deletes the animation
|
|
86
|
+
* without a word — every child of a keyframes block is a block, `declarationsOf` skips blocks, and
|
|
87
|
+
* a rule with no declarations is never emitted.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} atKey - An `@`-prefixed style key
|
|
90
|
+
* @returns {boolean} True when the block is emitted verbatim, with no selector inside
|
|
91
|
+
* @docs framework/concepts/styling
|
|
92
|
+
*/
|
|
93
|
+
export function isDeclarationAtRule(atKey: string): boolean {
|
|
94
|
+
return (
|
|
95
|
+
atKey.startsWith("@position-try") ||
|
|
96
|
+
atKey.startsWith("@property") ||
|
|
97
|
+
atKey.startsWith("@font-face") ||
|
|
98
|
+
atKey.startsWith("@counter-style")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The at-rule whose body is a list of KEYFRAME BLOCKS: `@keyframes <name>`.
|
|
104
|
+
*
|
|
105
|
+
* The third body shape, and the only one. Its children are keyframe selectors — `from`, `to`,
|
|
106
|
+
* `50%`, `"0%, 100%"` — which name a point in an animation's timeline and not an element. Scoping
|
|
107
|
+
* one produces `@keyframes toast-in { .x from { … } }`, which a browser parses without complaint
|
|
108
|
+
* into a keyframes rule holding NO keyframes: the `animation` declaration still names a live
|
|
109
|
+
* animation, and that animation animates nothing. That is how the Studio toast lost its entry
|
|
110
|
+
* animation the day it became a Jx document.
|
|
111
|
+
*
|
|
112
|
+
* The name is part of the key, so this is a PREFIX match like {@link isDeclarationAtRule}.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} atKey - An `@`-prefixed style key
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
* @docs framework/concepts/styling
|
|
117
|
+
*/
|
|
118
|
+
export function isKeyframesAtRule(atKey: string): boolean {
|
|
119
|
+
return atKey.startsWith("@keyframes");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve one nested style key against its parent selector.
|
|
124
|
+
*
|
|
125
|
+
* `&` splices, `:`/`.`/`[` concatenate, anything else is a descendant. Extracted because the same
|
|
126
|
+
* four-branch decision was written THREE times in `applyStyle` — the top-level nested loop,
|
|
127
|
+
* `emitNested`'s recursion and `emitMediaNested` — and three copies of one decision is three
|
|
128
|
+
* chances for a selector to mean different things depending on how deeply it was nested.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} scope - The parent selector
|
|
131
|
+
* @param {string} key - The nested key, e.g. `":hover"`, `"& > li"`, `".child"`
|
|
132
|
+
* @returns {string} The resolved selector
|
|
133
|
+
*/
|
|
134
|
+
export function resolveNestedSelector(scope: string, key: string): string {
|
|
135
|
+
const resolved: string[] = [];
|
|
136
|
+
for (const scopePart of splitSelectorList(scope)) {
|
|
137
|
+
for (const keyPart of splitSelectorList(key)) {
|
|
138
|
+
resolved.push(resolveOneNestedSelector(scopePart, keyPart));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return resolved.join(", ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** One member of a scope against one member of a key — the four-branch decision itself. */
|
|
145
|
+
function resolveOneNestedSelector(scope: string, key: string): string {
|
|
146
|
+
if (key.startsWith("&")) {
|
|
147
|
+
return key.replaceAll("&", scope);
|
|
148
|
+
}
|
|
149
|
+
if (key.startsWith("[") || key.startsWith(":") || key.startsWith(".")) {
|
|
150
|
+
return `${scope}${key}`;
|
|
151
|
+
}
|
|
152
|
+
return `${scope} ${key}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Split a selector list on its top-level commas.
|
|
157
|
+
*
|
|
158
|
+
* A comma inside `:is()`, `:where()`, `:not()`, an attribute value or a quoted string separates
|
|
159
|
+
* nothing, so depth and quotes are tracked. Members are trimmed and an empty member is dropped; a
|
|
160
|
+
* selector with no top-level comma comes back as itself, so callers never special-case the single
|
|
161
|
+
* form.
|
|
162
|
+
*
|
|
163
|
+
* Why this exists: a nested key was spliced onto its scope as ONE string, so `"& .a, & .b"` with a
|
|
164
|
+
* `":hover"` inside it emitted `#x .a, #x .b:hover` — the first member got the hover rule with no
|
|
165
|
+
* hover — and the second `&` of the key was never replaced at all. CSS Nesting distributes a nested
|
|
166
|
+
* selector over every member of the parent list (its implicit `:is()`); the flattener now does the
|
|
167
|
+
* same, member by member.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} selector - A selector or a selector list
|
|
170
|
+
* @returns {string[]} The members, in order
|
|
171
|
+
*/
|
|
172
|
+
export function splitSelectorList(selector: string): string[] {
|
|
173
|
+
const members: string[] = [];
|
|
174
|
+
let depth = 0;
|
|
175
|
+
let quote: string | null = null;
|
|
176
|
+
let start = 0;
|
|
177
|
+
for (let i = 0; i < selector.length; i += 1) {
|
|
178
|
+
const ch = selector[i]!;
|
|
179
|
+
if (quote) {
|
|
180
|
+
if (ch === "\\") {
|
|
181
|
+
i += 1;
|
|
182
|
+
} else if (ch === quote) {
|
|
183
|
+
quote = null;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === '"' || ch === "'") {
|
|
188
|
+
quote = ch;
|
|
189
|
+
} else if (ch === "(" || ch === "[") {
|
|
190
|
+
depth += 1;
|
|
191
|
+
} else if (ch === ")" || ch === "]") {
|
|
192
|
+
depth = Math.max(0, depth - 1);
|
|
193
|
+
} else if (ch === "," && depth === 0) {
|
|
194
|
+
members.push(selector.slice(start, i));
|
|
195
|
+
start = i + 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
members.push(selector.slice(start));
|
|
199
|
+
const trimmed = members.map((member) => member.trim()).filter((member) => member !== "");
|
|
200
|
+
return trimmed.length > 0 ? trimmed : [selector];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Studio-canvas popover selector transposition — the style half of `setCanvasDelinkPopovers`.
|
|
205
|
+
*
|
|
206
|
+
* The canvas renames `popover` to `data-jx-popover` so an open panel leaves the TOP LAYER and
|
|
207
|
+
* becomes an ordinary positioned element the editor can measure, select and grow the artboard for.
|
|
208
|
+
* The moment it does, `:popover-open` matches nothing — so an authored open state would simply not
|
|
209
|
+
* apply, and the author would be styling a rule they could never see.
|
|
210
|
+
*
|
|
211
|
+
* `[data-jx-popover-open]` is the stand-in, and the substitution is exact in the way that matters:
|
|
212
|
+
* a pseudo-class and an attribute selector are both specificity (0,1,0), so a `:popover-open` block
|
|
213
|
+
* still wins and loses against the same neighbours it does on the shipped page.
|
|
214
|
+
*
|
|
215
|
+
* `null` means "do not emit this rule at all", and `::backdrop` is the one thing it is returned
|
|
216
|
+
* for. There is no backdrop pseudo-element outside the top layer; synthesising one would paint a
|
|
217
|
+
* full-canvas scrim over the document being edited, and emitting an inert rule would mark the
|
|
218
|
+
* selector as "styled" in the Style panel while doing nothing. Preview renders it natively.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} selector - A fully resolved selector, canvas-side
|
|
221
|
+
* @returns {string | null} The selector to emit, or null to emit nothing
|
|
222
|
+
* @docs framework/concepts/overlays
|
|
223
|
+
*/
|
|
224
|
+
export function transposeCanvasPopoverSelector(selector: string): string | null {
|
|
225
|
+
return transposeCanvasOverlaySelector(selector);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** What {@link transposeCanvasOverlaySelector} needs to know about the element that owns a rule. */
|
|
229
|
+
export interface CanvasOverlayTransposeOptions {
|
|
230
|
+
/**
|
|
231
|
+
* The rule was authored on a `<dialog>`. It answers `[open]` only for a compound that names no
|
|
232
|
+
* element type at all (`&[open]`, `#d[open]`, `.panel[open]`) — a compound that names one is
|
|
233
|
+
* decided by that type instead, so a `<div>`'s `& dialog[open]` is transposed and a `<dialog>`'s
|
|
234
|
+
* `& details[open]` is not.
|
|
235
|
+
*/
|
|
236
|
+
dialog?: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A character that ends the compound selector to the left of it. */
|
|
240
|
+
const COMPOUND_BREAK = /[\s>+~,()]/;
|
|
241
|
+
|
|
242
|
+
/** The leading type selector of a compound, if it has one. */
|
|
243
|
+
const LEADING_TYPE = /^[A-Za-z_\u{00A0}-\u{FFFF}][\w\u{00A0}-\u{FFFF}-]*/u;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Rewrite each `[open]` according to the compound it belongs to, not the element the style object
|
|
247
|
+
* hangs off. A compound naming `dialog` is the dialog's own open state and is transposed; one
|
|
248
|
+
* naming any other type (`details`, a custom element) is left alone; one naming no type — or naming
|
|
249
|
+
* the scope handle, which IS the styled element — inherits `options.dialog`.
|
|
250
|
+
*/
|
|
251
|
+
function transposeOpenAttribute(selector: string, options: CanvasOverlayTransposeOptions): string {
|
|
252
|
+
let out = "";
|
|
253
|
+
let from = 0;
|
|
254
|
+
for (;;) {
|
|
255
|
+
const at = selector.indexOf("[open]", from);
|
|
256
|
+
if (at === -1) {
|
|
257
|
+
return out + selector.slice(from);
|
|
258
|
+
}
|
|
259
|
+
let start = at;
|
|
260
|
+
while (start > 0 && !COMPOUND_BREAK.test(selector[start - 1] as string)) {
|
|
261
|
+
start -= 1;
|
|
262
|
+
}
|
|
263
|
+
const type = LEADING_TYPE.exec(selector.slice(start, at))?.[0]?.toLowerCase();
|
|
264
|
+
const dialog = type === undefined ? options.dialog === true : type === "dialog";
|
|
265
|
+
out += selector.slice(from, at) + (dialog ? "[data-jx-dialog-open]" : "[open]");
|
|
266
|
+
from = at + "[open]".length;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Studio-canvas overlay selector transposition, for dialogs as well as popovers — the style half of
|
|
272
|
+
* `setCanvasDelinkPopovers` and `setCanvasDelinkCommands` together.
|
|
273
|
+
*
|
|
274
|
+
* A dialog the canvas shows in place is not `:modal` and, its `open` renamed with the rest of the
|
|
275
|
+
* invoker machinery, not `[open]` either; `[data-jx-dialog-open]` is the stand-in for both, at the
|
|
276
|
+
* same (0,1,0) specificity. `::backdrop` is dropped for the reason given above: neither kind of
|
|
277
|
+
* overlay has one outside the top layer.
|
|
278
|
+
*
|
|
279
|
+
* `[inert]` travels with `commandfor` and a dialog's `open`, and for the same reason: the canvas
|
|
280
|
+
* renames the attribute on every stamped node so the editor can select into an inert region, so a
|
|
281
|
+
* rule the author wrote against `[inert]` would match on the built page and silently not on the
|
|
282
|
+
* canvas. `[data-jx-inert]` is the same (0,1,0) specificity, so it wins and loses against the same
|
|
283
|
+
* neighbours. Unlike `[open]` it needs no option: `inert` is renamed whatever the tag is, where
|
|
284
|
+
* `<details open>` keeps its attribute and so its `[open]` must keep matching.
|
|
285
|
+
*
|
|
286
|
+
* **`[open]` follows the compound it is written on, not the element the style object hangs off.** A
|
|
287
|
+
* compound naming `dialog` is transposed whatever owns the rule, so a wrapper's `& dialog[open]`
|
|
288
|
+
* keeps matching; a compound naming any other type is left alone, so a dialog's `& details[open]`
|
|
289
|
+
* is not corrupted into a selector that can never match. Only a compound with no type of its own —
|
|
290
|
+
* `&[open]`, `#d[open]`, or the runtime's own scope handle — falls back to `options.dialog`. The
|
|
291
|
+
* handle needs no special case: it is not a parseable type name, so the scan finds none and the
|
|
292
|
+
* fallback is what answers for it, which is the right answer because the handle IS the styled
|
|
293
|
+
* element.
|
|
294
|
+
*
|
|
295
|
+
* Three shapes are out of reach of that scan and take the fallback: `:is(dialog)[open]`, a quoted
|
|
296
|
+
* attribute value carrying selector punctuation, and any functional pseudo between the type and the
|
|
297
|
+
* attribute — `dialog:not(.x)[open]` reads as no type, because a parenthesis breaks the backscan.
|
|
298
|
+
*
|
|
299
|
+
* **The rename is per NODE and the selector is not**, so one limitation survives whatever this
|
|
300
|
+
* does. `open` is renamed only on a node the canvas stamped, and a rule may address both stamped
|
|
301
|
+
* and unstamped dialogs at once: a stamped wrapper styling `& dialog[open]` whose dialog comes from
|
|
302
|
+
* a component's own template now emits an attribute that dialog does not carry. Before this, the
|
|
303
|
+
* same rule failed the other way round, on the stamped dialog. Whichever way it is decided one of
|
|
304
|
+
* the two is wrong, so it is decided for the addressable one and stated here rather than papered
|
|
305
|
+
* over.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} selector - A fully resolved selector, canvas-side
|
|
308
|
+
* @param {CanvasOverlayTransposeOptions} [options]
|
|
309
|
+
* @returns {string | null} The selector to emit, or null to emit nothing
|
|
310
|
+
* @docs framework/concepts/overlays
|
|
311
|
+
*/
|
|
312
|
+
export function transposeCanvasOverlaySelector(
|
|
313
|
+
selector: string,
|
|
314
|
+
options: CanvasOverlayTransposeOptions = {},
|
|
315
|
+
): string | null {
|
|
316
|
+
if (selector.includes("::backdrop")) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const transposed = selector
|
|
320
|
+
.replaceAll(":popover-open", "[data-jx-popover-open]")
|
|
321
|
+
.replaceAll(":modal", "[data-jx-dialog-open]")
|
|
322
|
+
.replaceAll("[inert]", "[data-jx-inert]");
|
|
323
|
+
return transposeOpenAttribute(transposed, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
63
326
|
// ─── Color Schemes ────────────────────────────────────────────────────────────
|
|
64
327
|
|
|
65
328
|
/**
|
|
@@ -117,3 +380,504 @@ export function schemeSelectors(
|
|
|
117
380
|
forced: `:where(:root[${COLOR_SCHEME_ATTR}="${scheme}"]) ${selector}`,
|
|
118
381
|
};
|
|
119
382
|
}
|
|
383
|
+
|
|
384
|
+
// ─── The Rule Builder ─────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Where a rule points, relative to the element the style object was authored on.
|
|
388
|
+
*
|
|
389
|
+
* The runtime needs this to decide what it is allowed to SHARE. A `self` rule can be interned and
|
|
390
|
+
* pointed at by any number of elements, because everything it says applies to whichever element
|
|
391
|
+
* carries the scope handle. A `descendant` rule cannot be shared the moment it reads a custom
|
|
392
|
+
* property, because `var()` resolves from the nearest ancestor that set it and a shared handle has
|
|
393
|
+
* more than one such ancestor. `unscoped` is a declaration-body at-rule — `@font-face`,
|
|
394
|
+
* `@position-try` — or a `@keyframes` block, whose name is document-global and which is therefore
|
|
395
|
+
* hoisted once rather than emitted per element.
|
|
396
|
+
*/
|
|
397
|
+
export type CssRuleTarget = "self" | "descendant" | "unscoped";
|
|
398
|
+
|
|
399
|
+
/** One keyframe stop: its keyframe selector, verbatim as authored, and what it declares. */
|
|
400
|
+
export interface CssKeyframeBlock {
|
|
401
|
+
/** `from`, `to`, `50%`, `"0%, 100%"` — a point on the timeline, never an element selector. */
|
|
402
|
+
selector: string;
|
|
403
|
+
/** Kebab-cased property/value pairs, in authored order. */
|
|
404
|
+
declarations: readonly (readonly [string, string])[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** One emitted CSS rule: its parts, its text, and a content hash of the two. */
|
|
408
|
+
export interface CssRule {
|
|
409
|
+
/** The rule as one `insertRule` argument, or one line of a `<style>` element. */
|
|
410
|
+
text: string;
|
|
411
|
+
/** At-rule wrappers, outermost first. Empty for a top-level rule. */
|
|
412
|
+
conditions: readonly string[];
|
|
413
|
+
/**
|
|
414
|
+
* The resolved selector, or null for a rule that has none: a declaration-body at-rule, or a
|
|
415
|
+
* `@keyframes` block (whose stops are in {@link CssRule.blocks} instead).
|
|
416
|
+
*/
|
|
417
|
+
selector: string | null;
|
|
418
|
+
/** Kebab-cased property/value pairs, in authored order. Empty for a `@keyframes` block. */
|
|
419
|
+
declarations: readonly (readonly [string, string])[];
|
|
420
|
+
/**
|
|
421
|
+
* The stops of a `@keyframes` block, in authored order. Absent for every other rule.
|
|
422
|
+
*
|
|
423
|
+
* `declarations` cannot describe N stops each with its own list, so it stays empty here and this
|
|
424
|
+
* carries the body. Nothing in the repository reads either field — `text`, `key` and `target` are
|
|
425
|
+
* the whole cross-consumer contract — but a rule that plainly has declarations must not report
|
|
426
|
+
* none without saying where they went.
|
|
427
|
+
*/
|
|
428
|
+
blocks?: readonly CssKeyframeBlock[];
|
|
429
|
+
/**
|
|
430
|
+
* The block's `$description`, if it wrote one: why the rule exists, in the author's words.
|
|
431
|
+
*
|
|
432
|
+
* It is NOT in {@link CssRule.text}, and that is the point. `text` is what a sheet inserts and
|
|
433
|
+
* what {@link hashCss} interns, so prose there would make two identical rules two rules and put a
|
|
434
|
+
* paragraph in every adopted sheet at runtime. A static emitter — the one writing a stylesheet a
|
|
435
|
+
* person will read — renders it as a comment; every other consumer ignores it, exactly as a
|
|
436
|
+
* document's own `$description` is ignored.
|
|
437
|
+
*/
|
|
438
|
+
description?: string;
|
|
439
|
+
/** See {@link CssRuleTarget}. */
|
|
440
|
+
target: CssRuleTarget;
|
|
441
|
+
/** FNV-1a base36 hash of `text` — the dedup key. */
|
|
442
|
+
key: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Hooks that let one builder serve the DOM runtime, the compiler, the site builder and Studio. */
|
|
446
|
+
export interface CssBuildOptions {
|
|
447
|
+
/**
|
|
448
|
+
* The selector the style object hangs off. Every nested key resolves against it, so the runtime
|
|
449
|
+
* passes a placeholder it substitutes afterwards and the compiler passes `#id` / `.jx-N` / a
|
|
450
|
+
* tag.
|
|
451
|
+
*/
|
|
452
|
+
scope?: string | null;
|
|
453
|
+
/** The project's `$media` map, for resolving `@--name` keys. */
|
|
454
|
+
mediaQueries?: Record<string, string>;
|
|
455
|
+
/** Applied to every declaration value. The canvas passes `canvasStyleValue`; hosts pass identity. */
|
|
456
|
+
transposeValue?: (value: string) => string;
|
|
457
|
+
/**
|
|
458
|
+
* Applied to every resolved selector. Returning null drops the rule entirely, which is how the
|
|
459
|
+
* canvas refuses to synthesise a `::backdrop` outside the top layer.
|
|
460
|
+
*/
|
|
461
|
+
transposeSelector?: (selector: string) => string | null;
|
|
462
|
+
/**
|
|
463
|
+
* Resolve a value the builder cannot serialize on its own: a `${…}` template or a `{ $ref }`.
|
|
464
|
+
*
|
|
465
|
+
* The runtime returns `var(--jx-rN)` and writes the real value into that custom property from an
|
|
466
|
+
* effect, so the declaration lives in a rule where `:hover` can override it while the reactive
|
|
467
|
+
* part stays inline. Hosts with no scope to evaluate against omit the hook, and the declaration
|
|
468
|
+
* is dropped — which is what the compiler already did for template strings, and what the runtime
|
|
469
|
+
* should always have done with a `$ref` it was instead reading as a nested selector.
|
|
470
|
+
*/
|
|
471
|
+
/**
|
|
472
|
+
* Resolve a reactive value to what the declaration should say, or `null` to emit no declaration
|
|
473
|
+
* at all. `target` is the rule's own target, so a resolver can treat a value on the element
|
|
474
|
+
* itself differently from one on a descendant.
|
|
475
|
+
*/
|
|
476
|
+
resolveValue?: (property: string, value: string | JxRef, target: CssRuleTarget) => string | null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** A style-object key that names a nested selector rather than a CSS property. */
|
|
480
|
+
export function isNestedSelectorKey(key: string): boolean {
|
|
481
|
+
return key.startsWith(":") || key.startsWith(".") || key.startsWith("&") || key.startsWith("[");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A style-object key as a CSS property name.
|
|
486
|
+
*
|
|
487
|
+
* Custom properties pass through untouched. They are case-SENSITIVE — `--fooBar` and `--foo-bar`
|
|
488
|
+
* are two different properties — so kebab-casing one renames it, and the `var(--fooBar)` that reads
|
|
489
|
+
* it finds nothing.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} property
|
|
492
|
+
* @returns {string}
|
|
493
|
+
*/
|
|
494
|
+
export function cssPropertyName(property: string): string {
|
|
495
|
+
return property.startsWith("--") ? property : camelToKebab(property);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* FNV-1a (32-bit), base36. Not a cryptographic hash and does not need to be: it is a dedup key and
|
|
500
|
+
* an SSR-stable replacement for `Math.random()`, and a collision costs two elements sharing a rule
|
|
501
|
+
* set that says the same thing.
|
|
502
|
+
*
|
|
503
|
+
* @param {string} text
|
|
504
|
+
* @returns {string}
|
|
505
|
+
*/
|
|
506
|
+
export function hashCss(text: string): string {
|
|
507
|
+
/* oxlint-disable no-bitwise, unicorn/prefer-code-point -- FNV-1a IS bitwise arithmetic over
|
|
508
|
+
16-bit code UNITS; `codePointAt` would consume a surrogate pair as one unit and change the
|
|
509
|
+
hash of any string containing an astral character, for no benefit to a non-cryptographic
|
|
510
|
+
content key. The final `>>> 0` is what keeps the result unsigned, and so base36-clean. */
|
|
511
|
+
let h = 2_166_136_261;
|
|
512
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
513
|
+
h ^= text.charCodeAt(i);
|
|
514
|
+
h = Math.imul(h, 16_777_619);
|
|
515
|
+
}
|
|
516
|
+
return (h >>> 0).toString(36);
|
|
517
|
+
/* oxlint-enable no-bitwise, unicorn/prefer-code-point */
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Serialize one rule: declarations, wrapped in a selector unless there is none, wrapped in its
|
|
522
|
+
* at-rule conditions from the inside out.
|
|
523
|
+
*
|
|
524
|
+
* @param {readonly string[]} conditions
|
|
525
|
+
* @param {string | null} selector
|
|
526
|
+
* @param {readonly (readonly [string, string])[]} declarations
|
|
527
|
+
* @returns {string}
|
|
528
|
+
*/
|
|
529
|
+
export function cssRuleText(
|
|
530
|
+
conditions: readonly string[],
|
|
531
|
+
selector: string | null,
|
|
532
|
+
declarations: readonly (readonly [string, string])[],
|
|
533
|
+
): string {
|
|
534
|
+
const body = declarations.map(([property, value]) => `${property}: ${value}`).join("; ");
|
|
535
|
+
let text = selector === null ? body : `${selector} { ${body} }`;
|
|
536
|
+
for (let i = conditions.length - 1; i >= 0; i -= 1) {
|
|
537
|
+
text = `${conditions[i]} { ${text} }`;
|
|
538
|
+
}
|
|
539
|
+
return text;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Serialize one `@keyframes` block: every stop, in authored order, inside ONE at-rule, wrapped in
|
|
544
|
+
* its enclosing conditions.
|
|
545
|
+
*
|
|
546
|
+
* One rule, not one per stop, and that is a correctness requirement rather than tidiness. CSS
|
|
547
|
+
* Animations 1: where two `@keyframes` rules share a name, the last in document order wins and the
|
|
548
|
+
* earlier ones are ignored ENTIRELY. So a block split into `@keyframes fade { from { … } }` plus
|
|
549
|
+
* `@keyframes fade { to { … } }` is valid CSS that animates only its last stop, which is the shape
|
|
550
|
+
* the site-style and compiler paths emitted before this existed.
|
|
551
|
+
*
|
|
552
|
+
* @param {readonly string[]} conditions
|
|
553
|
+
* @param {string} name - The animation name, as authored
|
|
554
|
+
* @param {readonly CssKeyframeBlock[]} blocks
|
|
555
|
+
* @returns {string}
|
|
556
|
+
*/
|
|
557
|
+
function cssKeyframesText(
|
|
558
|
+
conditions: readonly string[],
|
|
559
|
+
name: string,
|
|
560
|
+
blocks: readonly CssKeyframeBlock[],
|
|
561
|
+
): string {
|
|
562
|
+
const body = blocks
|
|
563
|
+
.map(
|
|
564
|
+
({ selector, declarations }) =>
|
|
565
|
+
`${selector} { ${declarations.map(([property, value]) => `${property}: ${value}`).join("; ")} }`,
|
|
566
|
+
)
|
|
567
|
+
.join(" ");
|
|
568
|
+
let text = `@keyframes ${name} { ${body} }`;
|
|
569
|
+
for (let i = conditions.length - 1; i >= 0; i -= 1) {
|
|
570
|
+
text = `${conditions[i]} { ${text} }`;
|
|
571
|
+
}
|
|
572
|
+
return text;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* A block with its `@keyframes` children removed, for the forced-scheme twin of a dual emission.
|
|
577
|
+
*
|
|
578
|
+
* A scheme-pure query emits twice, once media-guarded and once under the root attribute, because a
|
|
579
|
+
* SELECTOR can be re-pointed at the forced state. A `@keyframes` name cannot: it is document-global
|
|
580
|
+
* and has no selector, so a second copy would be a second definition of one name, and the
|
|
581
|
+
* unconditional copy would silently replace the media-guarded one for every visitor. The
|
|
582
|
+
* media-guarded copy is the one that keeps the author's condition, so it is the one that survives.
|
|
583
|
+
*/
|
|
584
|
+
function withoutKeyframes(block: JxStyle): JxStyle {
|
|
585
|
+
const keyframeKeys = Object.keys(block).filter((key) => isKeyframesAtRule(key));
|
|
586
|
+
if (keyframeKeys.length === 0) {
|
|
587
|
+
return block;
|
|
588
|
+
}
|
|
589
|
+
const rest: JxStyle = { ...block };
|
|
590
|
+
for (const key of keyframeKeys) {
|
|
591
|
+
delete rest[key];
|
|
592
|
+
}
|
|
593
|
+
return rest;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Whether a nested key COMPOUNDS onto its scope rather than descending from it.
|
|
598
|
+
*
|
|
599
|
+
* `:hover`, `.wide`, `[open]` and their `&`-spliced spellings still match the element the style was
|
|
600
|
+
* authored on; `> li` and `span` match something inside it. That distinction is the whole of
|
|
601
|
+
* {@link CssRuleTarget}, so it is decided once, here.
|
|
602
|
+
*/
|
|
603
|
+
function compoundsOntoScope(key: string): boolean {
|
|
604
|
+
const rest = key.startsWith("&") ? key.slice(1) : key;
|
|
605
|
+
return rest === "" || rest.startsWith(":") || rest.startsWith(".") || rest.startsWith("[");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Turn one authored `style` object into a flat list of CSS rules.
|
|
610
|
+
*
|
|
611
|
+
* This is the single answer to "what does a Jx style object MEAN as CSS", shared by the DOM
|
|
612
|
+
* runtime, the compiler's static emitter and the site-style builder. Before it, three emitters each
|
|
613
|
+
* walked the shape with their own recursion and each dropped a different combination: the runtime
|
|
614
|
+
* lost `selector → @media`, the compiler lost `@media → selector → pseudo`, and neither composed
|
|
615
|
+
* at-rules at all.
|
|
616
|
+
*
|
|
617
|
+
* The recursion carries `{ selector, conditions }` rather than special-casing depth, so nesting and
|
|
618
|
+
* at-rules compose to any depth in either order by construction. A scheme-pure media query
|
|
619
|
+
* dual-emits through {@link schemeSelectors} — a media-guarded copy for the OS preference and an
|
|
620
|
+
* unconditional copy for the forced root attribute — and the forced copy keeps recursing, so an
|
|
621
|
+
* `@--md` block inside a `@(prefers-color-scheme: dark)` block survives both branches.
|
|
622
|
+
*
|
|
623
|
+
* Nesting is FLATTENED, never handed to the browser as native CSS nesting: `&` is resolved here and
|
|
624
|
+
* no `&` reaches the output. That is deliberate — Jx compounds `.child` onto its scope where CSS
|
|
625
|
+
* Nesting would make it a descendant — and it is also what keeps the output parseable by the test
|
|
626
|
+
* DOM, which has no native nesting.
|
|
627
|
+
*
|
|
628
|
+
* @param {JxStyle} style - The authored style object
|
|
629
|
+
* @param {CssBuildOptions} [options]
|
|
630
|
+
* @returns {CssRule[]} Rules in cascade order: a scope's own declarations before its nested blocks
|
|
631
|
+
* @docs framework/concepts/styling
|
|
632
|
+
*/
|
|
633
|
+
export function buildStyleRules(style: JxStyle, options: CssBuildOptions = {}): CssRule[] {
|
|
634
|
+
const {
|
|
635
|
+
scope = null,
|
|
636
|
+
mediaQueries = {},
|
|
637
|
+
transposeValue = (value: string) => value,
|
|
638
|
+
transposeSelector = (selector: string) => selector,
|
|
639
|
+
resolveValue,
|
|
640
|
+
} = options;
|
|
641
|
+
const rules: CssRule[] = [];
|
|
642
|
+
|
|
643
|
+
const isBlock = (value: unknown): value is JxStyle =>
|
|
644
|
+
value !== null && typeof value === "object" && !Array.isArray(value) && !isRef(value);
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* The blocks written under one key: the block itself, or each of an ARRAY of them, in order.
|
|
648
|
+
*
|
|
649
|
+
* An object's keys are unique, so a key can name a rule only once — and `@font-face` is the
|
|
650
|
+
* at-rule whose identity is not in its key, so a family with three weights had no spelling at
|
|
651
|
+
* all. It was not refused either: an array here emitted NOTHING, silently, so a style block that
|
|
652
|
+
* looked complete shipped no faces and the page fell to its fallback stack.
|
|
653
|
+
*
|
|
654
|
+
* **Only a DECLARATION at-rule takes the array**, and the narrowness is the point rather than
|
|
655
|
+
* caution. Those four are leaves — `walkAt` emits their declarations and recurses into nothing —
|
|
656
|
+
* so admitting a list there teaches no other walker anything new. Under a selector key an array
|
|
657
|
+
* would say what one block already says, while every style walker in the repo (the overlay lint,
|
|
658
|
+
* the a11y lint, the canvas) assumes a block key holds ONE block; allowing it there would leave
|
|
659
|
+
* those reading past it in silence, which is the failure this codebase keeps finding.
|
|
660
|
+
*/
|
|
661
|
+
const blocksOf = (key: string, value: unknown): JxStyle[] => {
|
|
662
|
+
if (isBlock(value)) {
|
|
663
|
+
return [value];
|
|
664
|
+
}
|
|
665
|
+
if (!isDeclarationAtRule(key) || !Array.isArray(value) || value.length === 0) {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
return value.every((entry) => isBlock(entry)) ? value : [];
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const declarationValue = (
|
|
672
|
+
property: string,
|
|
673
|
+
value: unknown,
|
|
674
|
+
reactive: boolean,
|
|
675
|
+
target: CssRuleTarget,
|
|
676
|
+
): string | null => {
|
|
677
|
+
if (value === undefined || value === null) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
/* Defect: a `{ $ref }` is a VALUE. Read as an object it looked like a nested selector, and the
|
|
681
|
+
runtime emitted `[data-jx="…"] color { $ref: #/state/tint }` — a rule for an element named
|
|
682
|
+
`color`, and a declaration whose property is `$ref`. */
|
|
683
|
+
if (isRef(value)) {
|
|
684
|
+
return reactive ? (resolveValue?.(property, value, target) ?? null) : null;
|
|
685
|
+
}
|
|
686
|
+
if (typeof value === "object") {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const raw = String(value);
|
|
690
|
+
if (isTemplateString(raw)) {
|
|
691
|
+
return reactive ? (resolveValue?.(property, raw, target) ?? null) : null;
|
|
692
|
+
}
|
|
693
|
+
return transposeValue(raw);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* `reactive: false` drops a `${…}` or `{ $ref }` instead of resolving it, and only a `@keyframes`
|
|
698
|
+
* stop passes it. The resolver's answer is `var(--jx-rN-M)`, a custom property the runtime writes
|
|
699
|
+
* INLINE on the one element that declared the style — but a keyframes block is hoisted once for
|
|
700
|
+
* the whole document, so the variable would be read where it was never set. Worse, the hoist is
|
|
701
|
+
* refcounted by rule TEXT: two elements naming one animation would produce two different texts,
|
|
702
|
+
* hence two definitions of one name, and the later would erase the earlier.
|
|
703
|
+
*/
|
|
704
|
+
const declarationsOf = (
|
|
705
|
+
node: JxStyle,
|
|
706
|
+
skipSelectorKeys: boolean,
|
|
707
|
+
reactive = true,
|
|
708
|
+
target: CssRuleTarget = "self",
|
|
709
|
+
): [string, string][] => {
|
|
710
|
+
const declarations: [string, string][] = [];
|
|
711
|
+
for (const [key, value] of Object.entries(node)) {
|
|
712
|
+
/* `blocksOf`, not `isBlock`: an array of blocks is a rule written more than once, never a
|
|
713
|
+
declaration value, and reading it as one emitted `@font-face: [object Object]`. */
|
|
714
|
+
if (blocksOf(key, value).length > 0) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// A scalar under a selector or at-rule key is an invalid shape, not a declaration.
|
|
718
|
+
if (skipSelectorKeys && (key.startsWith("@") || isNestedSelectorKey(key))) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
/* `$`-prefixed keys are METADATA, the way they are everywhere else in a document. No CSS
|
|
722
|
+
property begins with `$` — a custom property begins with `--` — so this can refuse the
|
|
723
|
+
whole prefix rather than name one key. `$description` used to emit
|
|
724
|
+
`$description: why this exists;`: an invalid declaration the parser drops in silence, which
|
|
725
|
+
also put the prose inside the text `hashCss` interns. */
|
|
726
|
+
if (key.startsWith("$")) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const resolved = declarationValue(key, value, reactive, target);
|
|
730
|
+
if (resolved !== null) {
|
|
731
|
+
declarations.push([cssPropertyName(key), resolved]);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return declarations;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const emit = (
|
|
738
|
+
conditions: readonly string[],
|
|
739
|
+
selector: string | null,
|
|
740
|
+
declarations: readonly (readonly [string, string])[],
|
|
741
|
+
target: CssRuleTarget,
|
|
742
|
+
description?: unknown,
|
|
743
|
+
) => {
|
|
744
|
+
if (declarations.length === 0) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
let resolved = selector;
|
|
748
|
+
if (resolved !== null) {
|
|
749
|
+
const transposed = transposeSelector(resolved);
|
|
750
|
+
if (transposed === null) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
resolved = transposed;
|
|
754
|
+
}
|
|
755
|
+
const text = cssRuleText(conditions, resolved, declarations);
|
|
756
|
+
rules.push({
|
|
757
|
+
text,
|
|
758
|
+
conditions: [...conditions],
|
|
759
|
+
selector: resolved,
|
|
760
|
+
declarations,
|
|
761
|
+
target,
|
|
762
|
+
key: hashCss(text),
|
|
763
|
+
...(typeof description === "string" && description !== "" ? { description } : {}),
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Emit a whole `@keyframes` block as ONE unscoped rule.
|
|
769
|
+
*
|
|
770
|
+
* The enclosing `selector` is not a parameter, which is the fix stated as a signature: a keyframe
|
|
771
|
+
* selector is a point on a timeline, so there is nothing for a scope to compound onto or descend
|
|
772
|
+
* from. Its key is therefore taken VERBATIM — no `resolveNestedSelector`, no selector-list split
|
|
773
|
+
* (`"0%, 100%"` is one valid keyframe selector already), and no `transposeSelector`, whose canvas
|
|
774
|
+
* implementation may return null and would delete a stop out of an otherwise sound animation.
|
|
775
|
+
* `transposeValue` still runs on every declaration, so a `translateY(10vh)` stop keeps the
|
|
776
|
+
* canvas's viewport-to-container rewrite.
|
|
777
|
+
*
|
|
778
|
+
* It pushes onto `rules` rather than going through `emit`, which serializes a declaration list
|
|
779
|
+
* and refuses an empty one.
|
|
780
|
+
*/
|
|
781
|
+
const emitKeyframes = (atKey: string, block: JxStyle, conditions: readonly string[]) => {
|
|
782
|
+
const name = atKey.slice("@keyframes".length).trim();
|
|
783
|
+
if (name === "") {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const blocks: CssKeyframeBlock[] = [];
|
|
787
|
+
for (const [key, value] of Object.entries(block)) {
|
|
788
|
+
if (!isBlock(value)) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
/* A stop holds declarations and nothing else. A nested selector or at-rule inside one is not
|
|
792
|
+
valid CSS, and `declarationsOf` drops both shapes rather than emitting a block a parser
|
|
793
|
+
would throw the whole animation away over. */
|
|
794
|
+
const declarations = declarationsOf(value, true, false, "unscoped");
|
|
795
|
+
if (declarations.length > 0) {
|
|
796
|
+
blocks.push({ declarations, selector: key.trim() });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (blocks.length === 0) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const text = cssKeyframesText(conditions, name, blocks);
|
|
803
|
+
rules.push({
|
|
804
|
+
blocks,
|
|
805
|
+
conditions: [...conditions],
|
|
806
|
+
declarations: [],
|
|
807
|
+
key: hashCss(text),
|
|
808
|
+
selector: null,
|
|
809
|
+
target: "unscoped",
|
|
810
|
+
text,
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const walkAt = (
|
|
815
|
+
atKey: string,
|
|
816
|
+
block: JxStyle,
|
|
817
|
+
selector: string | null,
|
|
818
|
+
conditions: readonly string[],
|
|
819
|
+
target: CssRuleTarget,
|
|
820
|
+
) => {
|
|
821
|
+
/* Not a query. `@--` is the canvas's base-width block — the value a breakpoint panel renders
|
|
822
|
+
at when no named breakpoint applies — and resolving it would emit `@media --`. */
|
|
823
|
+
if (atKey === "@--") {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (isDeclarationAtRule(atKey)) {
|
|
827
|
+
emit(
|
|
828
|
+
[...conditions, atKey],
|
|
829
|
+
null,
|
|
830
|
+
declarationsOf(block, false, true, "unscoped"),
|
|
831
|
+
"unscoped",
|
|
832
|
+
block["$description"],
|
|
833
|
+
);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (isKeyframesAtRule(atKey)) {
|
|
837
|
+
emitKeyframes(atKey, block, conditions);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const query = resolveAtQuery(atKey, mediaQueries);
|
|
841
|
+
const atRule = query === null ? atKey : `@media ${query}`;
|
|
842
|
+
const scheme = query === null ? null : pureSchemeOf(query);
|
|
843
|
+
if (scheme !== null && selector !== null) {
|
|
844
|
+
const { auto, forced } = schemeSelectors(selector, scheme);
|
|
845
|
+
walk(block, auto, [...conditions, atRule], target);
|
|
846
|
+
walk(withoutKeyframes(block), forced, conditions, target);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
walk(block, selector, [...conditions, atRule], target);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
function walk(
|
|
853
|
+
node: JxStyle,
|
|
854
|
+
selector: string | null,
|
|
855
|
+
conditions: readonly string[],
|
|
856
|
+
target: CssRuleTarget,
|
|
857
|
+
) {
|
|
858
|
+
emit(
|
|
859
|
+
conditions,
|
|
860
|
+
selector,
|
|
861
|
+
declarationsOf(node, true, true, target),
|
|
862
|
+
target,
|
|
863
|
+
node["$description"],
|
|
864
|
+
);
|
|
865
|
+
for (const [key, value] of Object.entries(node)) {
|
|
866
|
+
for (const block of blocksOf(key, value)) {
|
|
867
|
+
if (key.startsWith("@")) {
|
|
868
|
+
walkAt(key, block, selector, conditions, target);
|
|
869
|
+
} else if (selector !== null) {
|
|
870
|
+
walk(
|
|
871
|
+
block,
|
|
872
|
+
resolveNestedSelector(selector, key),
|
|
873
|
+
conditions,
|
|
874
|
+
compoundsOntoScope(key) ? target : "descendant",
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
walk(style, scope, [], "self");
|
|
882
|
+
return rules;
|
|
883
|
+
}
|