@hueest/xray 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/client.d.ts +42 -51
- package/dist/client.js +136 -142
- package/dist/core.d.ts +42 -34
- package/dist/core.js +39 -33
- package/dist/css-escape-DJ-BsrNm.js +47 -0
- package/dist/hud.d.ts +7 -8
- package/dist/hud.js +5 -5
- package/dist/index.d.ts +268 -250
- package/dist/index.js +369 -240
- package/dist/measure-CUQxB-Ic.d.ts +524 -0
- package/dist/{plate-BRR6d8Se.js → plate-CXIh5_OB.js} +61 -45
- package/dist/react.core-BS2yvqeh.js +301 -0
- package/dist/react.core-BkJoCgWJ.d.ts +115 -0
- package/dist/react.d.ts +11 -13
- package/dist/react.dev.d.ts +4 -6
- package/dist/react.dev.js +58 -64
- package/dist/react.js +27 -25
- package/dist/{project-BZosujs9.js → serialize-Cj-EHvMv.js} +877 -1027
- package/package.json +8 -6
- package/virtual.d.ts +25 -32
- package/dist/css-escape-N7bOusGW.js +0 -52
- package/dist/react.core-BC02ukac.js +0 -294
- package/dist/react.core-NhWc9qan.d.ts +0 -89
- package/dist/serialize-BgdGt34A.d.ts +0 -501
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-
|
|
1
|
+
import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-CXIh5_OB.js";
|
|
2
2
|
//#region src/ir.ts
|
|
3
3
|
/** A frozen `LiteStyleDeclaration` of empty strings, for entry-less nodes (root/stitch/synthetic). */
|
|
4
4
|
const EMPTY_STYLE = Object.freeze({
|
|
@@ -45,33 +45,35 @@ const EMPTY_STYLE = Object.freeze({
|
|
|
45
45
|
alignItems: "",
|
|
46
46
|
verticalAlign: ""
|
|
47
47
|
});
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The `LeafKind` values, as a runtime list (kept local so `ir.ts` has no runtime edge back to
|
|
50
|
+
* `measure.ts`).
|
|
51
|
+
*/
|
|
49
52
|
const LEAF_KINDS$1 = [
|
|
50
|
-
"text",
|
|
53
|
+
"text-line",
|
|
51
54
|
"text-block",
|
|
52
55
|
"media",
|
|
53
|
-
"box"
|
|
56
|
+
"box",
|
|
57
|
+
"hidden"
|
|
54
58
|
];
|
|
55
59
|
function isLeafKind(kind) {
|
|
56
60
|
return LEAF_KINDS$1.includes(kind);
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
|
-
* Run a per-View pass over every View of a `PlateIR` (ADR 0022 §3). The pass is
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `plate.views` directly instead (those stay deferred; ADR 0022 §2).
|
|
63
|
+
* Run a per-View pass over every View of a `PlateIR` (ADR 0022 §3). The pass is the
|
|
64
|
+
* annotate-in-place kind — it mutates `view.bundle.root`'s nodes (sets `dropped`/`folded`/`count`/…
|
|
65
|
+
* fields) and returns nothing; `eachView` just sequences it across the bundle's Views. A cross-View
|
|
66
|
+
* pass reads across `plate.views` directly instead (those stay deferred; ADR 0022 §2).
|
|
64
67
|
*/
|
|
65
68
|
function eachView(plate, pass) {
|
|
66
69
|
for (const view of plate.views) pass(view);
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
69
|
-
* Copy the bounded computed-style set off the live `CSSStyleDeclaration` into a
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* build site (it already holds the live `cs`) — the snapshot folds into Measure.
|
|
72
|
+
* Copy the bounded computed-style set off the live `CSSStyleDeclaration` into a frozen plain object
|
|
73
|
+
* (ADR 0022 invariant 5). The live view is NEVER stored, so a pass cannot re-read the DOM. Each
|
|
74
|
+
* prop coalesces to `''` so a node whose `cs` carries only a partial set (a test stub, or a
|
|
75
|
+
* UA-default-only element) still reads a safe `string`, never `undefined`. Called by the walk at
|
|
76
|
+
* each node's build site (it already holds the live `cs`) — the snapshot folds into Measure.
|
|
75
77
|
*/
|
|
76
78
|
function snapshotStyle(cs) {
|
|
77
79
|
return Object.freeze({
|
|
@@ -120,18 +122,15 @@ function snapshotStyle(cs) {
|
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
/**
|
|
123
|
-
* Read the (grouped) IR back into a per-View `ViewCapture`'s `{tree, rules}` — the
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* (`bundle.nodes`); author/carried/context/band rules carry their own
|
|
133
|
-
* `(spec, order)`, so their array position is immaterial — classify re-sorts by
|
|
134
|
-
* `(layered, spec, order)`.
|
|
125
|
+
* Read the (grouped) IR back into a per-View `ViewCapture`'s `{tree, rules}` — the inverse of the
|
|
126
|
+
* walk's IR build. The tree comes from the node structure; the flat rule list is REBUILT from the
|
|
127
|
+
* IR: each node's lifted author + inline rules (`decls.author`), its carried token defs
|
|
128
|
+
* (`decls.carried`), and its synthesized fallback/size rules (`decls.fallback`/`decls.size`), plus
|
|
129
|
+
* the bundle's non-node-anchored rules — the text-context rule and the `@container` rep bands. A
|
|
130
|
+
* rule shared across nodes (its `ids` spanning several) sits on each node's `decls.author` BY
|
|
131
|
+
* REFERENCE, so it is de-duplicated by identity and emitted once. Fallback/size order follows the
|
|
132
|
+
* walk's PUSH order (`bundle.nodes`); author/carried/context/band rules carry their own `(spec,
|
|
133
|
+
* order)`, so their array position is immaterial — classify re-sorts by `(layered, spec, order)`.
|
|
135
134
|
*/
|
|
136
135
|
function emit(bundle) {
|
|
137
136
|
const tree = irTree(bundle.root);
|
|
@@ -198,22 +197,17 @@ function irTree(node) {
|
|
|
198
197
|
return out;
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
|
-
* A node's EFFECTIVE kids — the kids `emit` actually projects, after lowering the
|
|
202
|
-
* `
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* flattened wrappers collapses to a fixpoint). Dropping the flattened node's
|
|
213
|
-
* rules matches the old destructive Flatten exactly — the spliced wrapper never
|
|
214
|
-
* reached emit, and `isPassthroughWrapper` guarantees it carries no tokens, zero
|
|
215
|
-
* margin, and a coincident box, so there is no layout to lose.
|
|
216
|
-
* - otherwise → keep it.
|
|
200
|
+
* A node's EFFECTIVE kids — the kids `emit` actually projects, after lowering the `dropped` and
|
|
201
|
+
* `flattened` annotations. Used by BOTH `irTree` (tree building) and `collectNodes` (rule
|
|
202
|
+
* collection) so structure and rules stay in lockstep, and by `templateIR`'s structural matching
|
|
203
|
+
* (`distil.ts`) so a run is templated on the shape `emit` will project, not the raw pre-Flatten
|
|
204
|
+
* tree (ADR 0022 §3). A FOLDED node yields NO kids — it projects as one synthetic text-bar leaf, so
|
|
205
|
+
* its raw subtree is suppressed (the glyph-fold survivor model, ADR 0022 §4). For each kid, in
|
|
206
|
+
* order: - `dropped` → omit it AND its whole subtree (a dropped node is never recursed). -
|
|
207
|
+
* `flattened` → splice it out: contribute NEITHER a tree node NOR its rules, and in its slot hoist
|
|
208
|
+
* the kid's OWN effective kids (recurse, so a chain of flattened wrappers collapses to a fixpoint).
|
|
209
|
+
* Dropping the flattened node's rules is safe: `isPassthroughWrapper` guarantees it carries no
|
|
210
|
+
* tokens, zero margin, and a coincident box, so there is no layout to lose. - otherwise → keep it.
|
|
217
211
|
*/
|
|
218
212
|
function effectiveKids(node) {
|
|
219
213
|
if (node.folded) return [];
|
|
@@ -226,38 +220,35 @@ function effectiveKids(node) {
|
|
|
226
220
|
return out;
|
|
227
221
|
}
|
|
228
222
|
/**
|
|
229
|
-
* A node's EFFECTIVE bone box — the rect downstream geometry passes read (ADR 0022
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* lives, so Superset / Crop never branch on `folded` themselves.
|
|
223
|
+
* A node's EFFECTIVE bone box — the rect downstream geometry passes read (ADR 0022 invariant 3). A
|
|
224
|
+
* glyph-fold survivor reports its `folded.box` (the union of the absorbed icon + the text unit);
|
|
225
|
+
* every other node reports its raw measured `box`. The single place "override ?? raw box" lives, so
|
|
226
|
+
* Superset / Crop never branch on `folded` themselves.
|
|
234
227
|
*/
|
|
235
228
|
function boneBox(node) {
|
|
236
229
|
return node.folded?.box ?? node.box;
|
|
237
230
|
}
|
|
238
231
|
/**
|
|
239
|
-
* A node's EFFECTIVE kind — the kind downstream shape/leaf passes see. A glyph-fold
|
|
240
|
-
*
|
|
241
|
-
* a thin container around one; the old merge re-stamped it `kind: 'text'`); every
|
|
232
|
+
* A node's EFFECTIVE kind — the kind downstream shape/leaf passes see. A glyph-fold survivor is a
|
|
233
|
+
* `text` leaf bar regardless of its raw `kind` (a text unit or a thin container around one); every
|
|
242
234
|
* other node reports its raw `kind`.
|
|
243
235
|
*/
|
|
244
236
|
function effectiveKind(node) {
|
|
245
|
-
return node.folded ? "text" : node.kind;
|
|
237
|
+
return node.folded ? "text-line" : node.kind;
|
|
246
238
|
}
|
|
247
239
|
/**
|
|
248
|
-
* A node's EFFECTIVE declarations — the decls `emit` projects (ADR 0022 §4). A
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* its raw `decls`.
|
|
240
|
+
* A node's EFFECTIVE declarations — the decls `emit` projects (ADR 0022 §4). A glyph-fold survivor
|
|
241
|
+
* emits ONLY its synthesized `folded.decls` (inline-block + width/height); its own
|
|
242
|
+
* author/carried/fallback/size are SUPPRESSED, since the bar replaces the original text unit. Every
|
|
243
|
+
* other node emits its raw `decls`.
|
|
253
244
|
*/
|
|
254
245
|
function effectiveDecls(node) {
|
|
255
246
|
return node.folded ? node.folded.decls : node.decls;
|
|
256
247
|
}
|
|
257
248
|
/**
|
|
258
|
-
* Map every node by its plate-local id. Used by the build phase's CSS-extract to
|
|
259
|
-
*
|
|
260
|
-
*
|
|
249
|
+
* Map every node by its plate-local id. Used by the build phase's CSS-extract to attach each lifted
|
|
250
|
+
* rule to the node(s) it targets. Ids are unique across the tree (`state.nextId++`), so the map is
|
|
251
|
+
* one-to-one.
|
|
261
252
|
*/
|
|
262
253
|
function indexById(root) {
|
|
263
254
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -271,11 +262,11 @@ function indexById(root) {
|
|
|
271
262
|
//#endregion
|
|
272
263
|
//#region src/css.ts
|
|
273
264
|
/**
|
|
274
|
-
* Pure CSS plumbing shared by both extraction strategies: which properties are
|
|
275
|
-
*
|
|
276
|
-
*
|
|
265
|
+
* Pure CSS plumbing shared by both extraction strategies: which properties are layout (vs paint),
|
|
266
|
+
* approximate selector specificity for cascade-preserving ordering, and rendering scoped rules back
|
|
267
|
+
* to a single plate CSS string.
|
|
277
268
|
*/
|
|
278
|
-
const EXACT_PROPS = new Set([
|
|
269
|
+
const EXACT_PROPS = /* @__PURE__ */ new Set([
|
|
279
270
|
"display",
|
|
280
271
|
"position",
|
|
281
272
|
"float",
|
|
@@ -352,17 +343,15 @@ function isLayoutProp(prop) {
|
|
|
352
343
|
return PATTERN_PROPS.some((re) => re.test(prop));
|
|
353
344
|
}
|
|
354
345
|
/**
|
|
355
|
-
* The legacy `-webkit-box` display family is the 2009 flexbox, used almost
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* captured in the text bone's measured (clamped) height, so the container just
|
|
365
|
-
* needs to be a normal block. Map the legacy values to their modern flow
|
|
346
|
+
* The legacy `-webkit-box` display family is the 2009 flexbox, used almost exclusively for the
|
|
347
|
+
* `-webkit-line-clamp` truncation idiom (`display: -webkit-box; -webkit-box-orient: vertical;
|
|
348
|
+
* -webkit-line-clamp: N; overflow: hidden`). Its companions `-webkit-box-orient` /
|
|
349
|
+
* `-webkit-line-clamp` are NOT carried layout props, so copying `display: -webkit-box` alone yields
|
|
350
|
+
* a broken box: with no `-webkit-box-orient` it defaults to a HORIZONTAL old-flexbox that collapses
|
|
351
|
+
* an empty bone child to zero width (invisible), and `overflow: hidden` then reads as a blank
|
|
352
|
+
* placeholder with the next sibling riding up under it. The idiom's only visible effect — clamping
|
|
353
|
+
* text to N lines — is already captured in the text bone's measured (clamped) height, so the
|
|
354
|
+
* container just needs to be a normal block. Map the legacy values to their modern flow
|
|
366
355
|
* equivalents; every other display value passes through untouched.
|
|
367
356
|
*/
|
|
368
357
|
const LEGACY_DISPLAY = {
|
|
@@ -382,20 +371,18 @@ function resolveVars(value, computed) {
|
|
|
382
371
|
return out;
|
|
383
372
|
}
|
|
384
373
|
/**
|
|
385
|
-
* A non-nesting `var(--name[, fallback])` matcher used by the FREEZE: the
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* one-level-at-a-time is correct and keeps the regex simple.
|
|
374
|
+
* A non-nesting `var(--name[, fallback])` matcher used by the FREEZE: the fallback group `[^()]*`
|
|
375
|
+
* deliberately stops at a nested paren, so a `var(--a, var(--b))` is rewritten head-first
|
|
376
|
+
* (replacing `--a` re-exposes the inner `var(--b)` for the next iteration). The freeze loops to a
|
|
377
|
+
* fixpoint, so one-level-at-a-time is correct and keeps the regex simple.
|
|
390
378
|
*/
|
|
391
379
|
const VAR_REF = /var\(\s*(--[\w-]+)\s*(?:,([^()]*))?\)/g;
|
|
392
380
|
/**
|
|
393
|
-
* The HEAD token name of EVERY `var(` in a value — both the top-level heads and
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* them in one pass regardless of nesting depth — simpler and more correct than a
|
|
381
|
+
* The HEAD token name of EVERY `var(` in a value — both the top-level heads and any head nested
|
|
382
|
+
* inside a fallback (`var(--a, var(--b))` → `--a`, `--b`). A fallback branch is reached by the
|
|
383
|
+
* engine only when its head is undefined, but a carried chain must stay self-contained whichever
|
|
384
|
+
* branch renders, so every head is a reachability seed. Matching `var(\s*(--name)` globally catches
|
|
385
|
+
* all of them in one pass regardless of nesting depth — simpler and more correct than a
|
|
399
386
|
* fallback-recursing regex (whose `[^()]*` cannot see into a nested `var()`).
|
|
400
387
|
*/
|
|
401
388
|
const VAR_HEAD = /var\(\s*(--[\w-]+)/g;
|
|
@@ -409,28 +396,24 @@ function referencedTokens(value) {
|
|
|
409
396
|
return out;
|
|
410
397
|
}
|
|
411
398
|
/**
|
|
412
|
-
* Freeze ONLY the `var()` references whose token was NOT carried (ADR 0019 §1).
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* a
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* runs, every surviving `var()` names a token the plate defines.
|
|
399
|
+
* Freeze ONLY the `var()` references whose token was NOT carried (ADR 0019 §1). A decl produced in
|
|
400
|
+
* carry mode keeps its raw `var(--x, …)` text; once the caller knows which tokens it actually
|
|
401
|
+
* emitted on the plate (`carried`), this resolves every leftover reference to its capture-time
|
|
402
|
+
* value — the irreducible freeze for a constant token, an un-anchorable wrapper token, or a
|
|
403
|
+
* genuinely unreadable one. A reference to a carried token is left INTACT so the plate's own
|
|
404
|
+
* definition drives it. The result is self-contained by construction: after this runs, every
|
|
405
|
+
* surviving `var()` names a token the plate defines.
|
|
420
406
|
*
|
|
421
|
-
* A leftover token resolves in this order: the caller's `frozen` map (the token's
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* self-containment. The `frozen` map already carries the cascade winner, so it is
|
|
428
|
-
* preferred where present.
|
|
407
|
+
* A leftover token resolves in this order: the caller's `frozen` map (the token's winning author
|
|
408
|
+
* value, read straight from the CSSOM) FIRST, then the matched element's `computed` style, then the
|
|
409
|
+
* var()'s own fallback. The author-value path is authoritative and independent of computed-style
|
|
410
|
+
* inheritance — computed style cannot always resolve a deeply-inherited custom property (and
|
|
411
|
+
* happy-dom never does), so without it a constant token could dangle and break self-containment.
|
|
412
|
+
* The `frozen` map already carries the cascade winner, so it is preferred where present.
|
|
429
413
|
*
|
|
430
|
-
* Operates on whole `prop: value` decl strings (splitting on the FIRST colon, so
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* to the heuristic, the documented last resort.
|
|
414
|
+
* Operates on whole `prop: value` decl strings (splitting on the FIRST colon, so a `calc()`/`url()`
|
|
415
|
+
* value with colons is untouched) and re-runs `fluidTracks` on a frozen grid track list — a token
|
|
416
|
+
* that froze to px-only tracks falls back to the heuristic, the documented last resort.
|
|
434
417
|
*/
|
|
435
418
|
function freezeUncarriedVars(decls, carried, computed, frozen) {
|
|
436
419
|
return decls.map((decl) => {
|
|
@@ -456,26 +439,24 @@ function freezeUncarriedVars(decls, carried, computed, frozen) {
|
|
|
456
439
|
});
|
|
457
440
|
}
|
|
458
441
|
/**
|
|
459
|
-
* A grid track list that is only `px` lengths is a frozen computed value —
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* the
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* pass through untouched.
|
|
442
|
+
* A grid track list that is only `px` lengths is a frozen computed value — authors write
|
|
443
|
+
* `fr`/`%`/`minmax`/`auto`, not sub-pixel tracks, so this is almost always a var()-driven `grid`
|
|
444
|
+
* shorthand the engine dropped, leaving us the resolved computed track sizes. Frozen px locks the
|
|
445
|
+
* grid to the capture width (off-centre at any other size); the author value is unrecoverable, so a
|
|
446
|
+
* responsive one is reconstructed. Already-fluid lists (fr/%/minmax/repeat/auto/…) pass through
|
|
447
|
+
* untouched.
|
|
466
448
|
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* no gutters, so every track becomes `1fr`.
|
|
449
|
+
* - Rows follow their content in a skeleton (the bones set the height), so a frozen row list becomes
|
|
450
|
+
* `auto` tracks.
|
|
451
|
+
* - Columns are almost always a centred layout: a wide content track flanked by gutters. Make the
|
|
452
|
+
* gutters flexible and equal (`1fr`) so the grid recentres at any width — this also erases the
|
|
453
|
+
* scrollbar-width asymmetry the capture picked up — and cap the content track(s) at the captured
|
|
454
|
+
* size (`minmax(0, px)`) so they shrink on narrow viewports but never overgrow. A uniform list
|
|
455
|
+
* (no track ≥2× the others — e.g. an even `repeat(n, 1fr)`) has no gutters, so every track
|
|
456
|
+
* becomes `1fr`.
|
|
476
457
|
*
|
|
477
|
-
* A single capture can't tell fixed tracks from flexible ones; the precise fix is
|
|
478
|
-
*
|
|
458
|
+
* A single capture can't tell fixed tracks from flexible ones; the precise fix is to diff track
|
|
459
|
+
* sizes across the capture-all widths (deferred).
|
|
479
460
|
*/
|
|
480
461
|
const PX_ONLY_TRACKS = /^\s*(?:-?[\d.]+px\s*)+$/;
|
|
481
462
|
function fluidTracks(value, axis) {
|
|
@@ -488,19 +469,16 @@ function fluidTracks(value, axis) {
|
|
|
488
469
|
return tracks.map((t, i) => isContent[i] ? `minmax(0, ${t}px)` : "1fr").join(" ");
|
|
489
470
|
}
|
|
490
471
|
/**
|
|
491
|
-
* Extract the layout-relevant declarations of a style block as `prop: value`
|
|
492
|
-
* strings.
|
|
472
|
+
* Extract the layout-relevant declarations of a style block as `prop: value` strings.
|
|
493
473
|
*
|
|
494
|
-
* Without `carry`, custom-property references are resolved against `computed`
|
|
495
|
-
*
|
|
496
|
-
* — the legacy ADR 0005 path, unchanged.
|
|
474
|
+
* Without `carry`, custom-property references are resolved against `computed` (the matched
|
|
475
|
+
* element's computed style) and FROZEN to their capture-time value (the ADR 0005 freeze).
|
|
497
476
|
*
|
|
498
|
-
* With `carry` (ADR 0019 §1), a `var()` is KEPT RAW and its referenced token
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* definition drives, instead of being locked to one viewport's px.
|
|
477
|
+
* With `carry` (ADR 0019 §1), a `var()` is KEPT RAW and its referenced token names are recorded
|
|
478
|
+
* into `carry.referenced`; the caller resolves the carry-vs-freeze decision globally (it alone sees
|
|
479
|
+
* every cross-sheet definition) and runs `freezeUncarriedVars` afterward to freeze only the
|
|
480
|
+
* leftovers. A responsive token thus survives as a live reference the plate's own emitted
|
|
481
|
+
* definition drives, instead of locking to one viewport's px.
|
|
504
482
|
*/
|
|
505
483
|
function filterLayoutDecls(style, computed, carry) {
|
|
506
484
|
const out = [];
|
|
@@ -523,10 +501,9 @@ function filterLayoutDecls(style, computed, carry) {
|
|
|
523
501
|
return out;
|
|
524
502
|
}
|
|
525
503
|
/**
|
|
526
|
-
* Approximate selector specificity packed as a single comparable number
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
* sum their arguments instead of taking the max.
|
|
504
|
+
* Approximate selector specificity packed as a single comparable number (`a * 1e6 + b * 1e3 + c`).
|
|
505
|
+
* Good enough to order rewritten rules so the original cascade winner still wins; known
|
|
506
|
+
* approximation: `:is()`/`:not()` sum their arguments instead of taking the max.
|
|
530
507
|
*/
|
|
531
508
|
function specificity(selector) {
|
|
532
509
|
let a = 0;
|
|
@@ -624,18 +601,17 @@ const BORDER_WIDTH_SATURATION = 4;
|
|
|
624
601
|
const SPACING_SATURATION = 64;
|
|
625
602
|
/** Type-scale (size×weight ratio over base) at which the axis saturates — ~a hero heading. */
|
|
626
603
|
const TYPE_SCALE_SATURATION = 2.5;
|
|
627
|
-
/**
|
|
604
|
+
/** Font-weight at/above which weight contributes its full bump to type-scale. */
|
|
628
605
|
const BOLD_WEIGHT = 700;
|
|
629
606
|
/** The base (`normal`) font weight type-scale measures a bump against. */
|
|
630
607
|
const NORMAL_WEIGHT = 400;
|
|
631
608
|
/** The CSS default font-size (px) used when the context base is missing/degenerate. */
|
|
632
609
|
const DEFAULT_FONT_SIZE = 16;
|
|
633
610
|
/**
|
|
634
|
-
* Axis weights for the convenience `score` blend. Contrast and size lead — a
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* one number has a sensible default. Tunable.
|
|
611
|
+
* Axis weights for the convenience `score` blend. Contrast and size lead — a skeleton bone exists
|
|
612
|
+
* because something PAINTS at some SIZE — with type-scale and spacing as secondary modifiers. They
|
|
613
|
+
* sum to 1 so `score` stays in 0..1. A per-axis consumer ignores this entirely; it exists only so a
|
|
614
|
+
* gate that wants one number has a sensible default. Tunable.
|
|
639
615
|
*/
|
|
640
616
|
const WEIGHTS = {
|
|
641
617
|
contrast: .35,
|
|
@@ -656,10 +632,10 @@ function pad$1(value) {
|
|
|
656
632
|
return Number.isFinite(n) ? n : 0;
|
|
657
633
|
}
|
|
658
634
|
/**
|
|
659
|
-
* A ramp from `lo` to `hi`: 0 at/below `lo`, 1 at/above `hi`, linear between.
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
635
|
+
* A ramp from `lo` to `hi`: 0 at/below `lo`, 1 at/above `hi`, linear between. Every axis funnels
|
|
636
|
+
* its raw measurement through one of these so "more signal" maps monotonically to a higher score
|
|
637
|
+
* and then saturates — past a point, a thicker border or a bigger heading does not read as
|
|
638
|
+
* proportionally more salient.
|
|
663
639
|
*/
|
|
664
640
|
function ramp(value, lo, hi) {
|
|
665
641
|
if (hi <= lo) return value > lo ? 1 : 0;
|
|
@@ -730,7 +706,7 @@ function spacingAxis(cs) {
|
|
|
730
706
|
}
|
|
731
707
|
/** True when justify/align-content/items declares a non-default distribution. */
|
|
732
708
|
function hasExplicitAlignment(cs) {
|
|
733
|
-
const inert = new Set([
|
|
709
|
+
const inert = /* @__PURE__ */ new Set([
|
|
734
710
|
"",
|
|
735
711
|
"normal",
|
|
736
712
|
"stretch",
|
|
@@ -761,14 +737,13 @@ function typeScaleAxis(cs, ctx) {
|
|
|
761
737
|
return ramp(sizeRatio * (1 + .3 * clamp01(((Number.isFinite(weightRaw) ? weightRaw : NORMAL_WEIGHT) - NORMAL_WEIGHT) / (BOLD_WEIGHT - NORMAL_WEIGHT))), 1, TYPE_SCALE_SATURATION);
|
|
762
738
|
}
|
|
763
739
|
/**
|
|
764
|
-
* Score a node's visual relevance from the four measurable-visual signals
|
|
765
|
-
*
|
|
766
|
-
* each in 0..1.
|
|
740
|
+
* Score a node's visual relevance from the four measurable-visual signals (ADR 0019 §4). Returns
|
|
741
|
+
* the per-axis vector plus a convenience blended scalar, each in 0..1.
|
|
767
742
|
*
|
|
768
|
-
* @param cs
|
|
769
|
-
* @param rect
|
|
770
|
-
* @param ctx
|
|
771
|
-
*
|
|
743
|
+
* @param cs The node's computed style (any `getPropertyValue` reader)
|
|
744
|
+
* @param rect The node's rendered box (a `getBoundingClientRect`-shaped value)
|
|
745
|
+
* @param ctx The measured context: nearest painted ancestor background, base font-size, and
|
|
746
|
+
* reference (root/viewport) area
|
|
772
747
|
*/
|
|
773
748
|
function salience(cs, rect, ctx) {
|
|
774
749
|
const contrast = contrastAxis(cs, ctx);
|
|
@@ -808,11 +783,10 @@ function createDiagnostics() {
|
|
|
808
783
|
};
|
|
809
784
|
}
|
|
810
785
|
/**
|
|
811
|
-
* Build a single diagnostic record with its canonical message, for callers
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
* everywhere. `detail` must never carry user CSS or captured text.
|
|
786
|
+
* Build a single diagnostic record with its canonical message, for callers outside the walk that
|
|
787
|
+
* surface a diagnostic directly (e.g. the client mapping `CaptureTooLargeError`, or the server
|
|
788
|
+
* reporting an invalid committed plate). Uses the same message table as the collector so the text
|
|
789
|
+
* is identical everywhere. `detail` must never carry user CSS or captured text.
|
|
816
790
|
*/
|
|
817
791
|
function makeDiagnostic(code, options) {
|
|
818
792
|
return {
|
|
@@ -823,10 +797,9 @@ function makeDiagnostic(code, options) {
|
|
|
823
797
|
};
|
|
824
798
|
}
|
|
825
799
|
/**
|
|
826
|
-
* The human-readable message per code, shared verbatim by browser and server so
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* aggregate count and any short detail.
|
|
800
|
+
* The human-readable message per code, shared verbatim by browser and server so their text cannot
|
|
801
|
+
* drift. Messages describe the omission and its fidelity cost; none echoes user CSS or captured
|
|
802
|
+
* text. `formatDiagnostic` appends the aggregate count and any short detail.
|
|
830
803
|
*/
|
|
831
804
|
const DIAGNOSTIC_MESSAGES = {
|
|
832
805
|
"unreadable-stylesheet": "could not read a stylesheet (likely cross-origin); rules from it were not captured",
|
|
@@ -841,11 +814,10 @@ const DIAGNOSTIC_MESSAGES = {
|
|
|
841
814
|
"invalid-plate-file": "a committed plate file could not be read (corrupt JSON, wrong shape, or a version mismatch) and was ignored"
|
|
842
815
|
};
|
|
843
816
|
/**
|
|
844
|
-
* The single shared formatter. Renders one diagnostic to a stable one-line
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
* user content.
|
|
817
|
+
* The single shared formatter. Renders one diagnostic to a stable one-line string used IDENTICALLY
|
|
818
|
+
* by the browser console and the Vite server output, so the two can never disagree. Shape:
|
|
819
|
+
* `<message> (<count>)[: <detail>]`. Count is omitted when 1 or absent; detail is appended only
|
|
820
|
+
* when present and is never user content.
|
|
849
821
|
*/
|
|
850
822
|
function formatDiagnostic(diagnostic) {
|
|
851
823
|
const count = diagnostic.count !== void 0 && diagnostic.count > 1 ? ` (${diagnostic.count})` : "";
|
|
@@ -859,14 +831,12 @@ function diagnosticsHeader(name) {
|
|
|
859
831
|
//#endregion
|
|
860
832
|
//#region src/style-cache.ts
|
|
861
833
|
/**
|
|
862
|
-
* A per-capture memo of `getComputedStyle(el)` keyed by element, scoped to ONE
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
* across captures or time (a stored style would silently go stale; the plan
|
|
869
|
-
* forbids it). `get` lazily computes and stores on first lookup.
|
|
834
|
+
* A per-capture memo of `getComputedStyle(el)` keyed by element, scoped to ONE synchronous capture
|
|
835
|
+
* (ADR 0025). The walk reads each entry node's own style through here, and the ancestor-walking
|
|
836
|
+
* helpers (contrast, line-height, clipping) memoize the parents they climb. A `CSSStyleDeclaration`
|
|
837
|
+
* is a live view, so this WeakMap lives only as long as the single capture that built it and is
|
|
838
|
+
* then dropped — it is NEVER cached across captures or time (a stored style would silently go
|
|
839
|
+
* stale; ADR 0025 forbids it). `get` lazily computes and stores on first lookup.
|
|
870
840
|
*/
|
|
871
841
|
var StyleCache = class {
|
|
872
842
|
#win;
|
|
@@ -889,11 +859,10 @@ function isInlineish(display) {
|
|
|
889
859
|
return display === "inline" || display.startsWith("inline-");
|
|
890
860
|
}
|
|
891
861
|
/**
|
|
892
|
-
* Does this container lay its children out in normal inline/block FLOW — so its
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
* while inline siblings in a block become one line.
|
|
862
|
+
* Does this container lay its children out in normal inline/block FLOW — so its inline-level
|
|
863
|
+
* children coalesce into one run (ADR 0021 §2) — versus flex / grid / table, which blockify every
|
|
864
|
+
* child into its own Bone? Keyed on the formatting context, not nesting depth: this is why two
|
|
865
|
+
* flex-column labels stay two Bones while inline siblings in a block become one line.
|
|
897
866
|
*/
|
|
898
867
|
function establishesInlineFlow(display) {
|
|
899
868
|
return !/\b(flex|grid|table|ruby)\b/.test(display);
|
|
@@ -906,28 +875,27 @@ function pxValue(value) {
|
|
|
906
875
|
return /(?:^|\s)-?[\d.]+px$/.test(value.trim()) ? Number.parseFloat(value) : null;
|
|
907
876
|
}
|
|
908
877
|
//#endregion
|
|
909
|
-
//#region src/
|
|
878
|
+
//#region src/distil.ts
|
|
910
879
|
/**
|
|
911
|
-
* An element at or below this opacity contributes no visible ink: opacity is not
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* to `NaN`, which fails the `<=` and is correctly kept.
|
|
880
|
+
* An element at or below this opacity contributes no visible ink: opacity is not inherited, but it
|
|
881
|
+
* composites the element AND its subtree as one group and then multiplies that group to (near)
|
|
882
|
+
* nothing, so every descendant is hidden too. Dropped like `display:none`/`visibility:hidden`. The
|
|
883
|
+
* small epsilon (not a strict `=== 0`) also catches the `opacity: 0.001` visually-hidden idiom
|
|
884
|
+
* while sparing genuinely faint-but-readable content. A missing/invalid value parses to `NaN`,
|
|
885
|
+
* which fails the `<=` and is correctly kept.
|
|
918
886
|
*/
|
|
919
887
|
const INVISIBLE_OPACITY = .01;
|
|
920
888
|
/**
|
|
921
|
-
* Does this IR node read as ONE single-line text bar a glyph icon can fold into?
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
889
|
+
* Does this IR node read as ONE single-line text bar a glyph icon can fold into? A bare `text`
|
|
890
|
+
* leaf, or a thin branch whose sole child is a `text` leaf (a `<span>label</span>` blockified in a
|
|
891
|
+
* flex row). A `text-block` (multi-line) or any structured branch does NOT — a glyph hugs one
|
|
892
|
+
* line.
|
|
925
893
|
*/
|
|
926
894
|
function textLineUnitIR(kid) {
|
|
927
|
-
if (kid.kind === "text" && kid.kids.length === 0) return true;
|
|
895
|
+
if (kid.kind === "text-line" && kid.kids.length === 0) return true;
|
|
928
896
|
if (kid.kind === "container" && kid.kids.length === 1) {
|
|
929
897
|
const only = kid.kids[0];
|
|
930
|
-
return only !== void 0 && only.kind === "text" && only.kids.length === 0;
|
|
898
|
+
return only !== void 0 && only.kind === "text-line" && only.kids.length === 0;
|
|
931
899
|
}
|
|
932
900
|
return false;
|
|
933
901
|
}
|
|
@@ -939,59 +907,52 @@ function boxGlyphGeometry(icon, text, lineHeight) {
|
|
|
939
907
|
return (icon.x >= text.x + text.width ? icon.x - (text.x + text.width) : text.x - (icon.x + icon.width)) <= lineHeight * .6;
|
|
940
908
|
}
|
|
941
909
|
/**
|
|
942
|
-
* Is this text bar a multi-line BLOCK rather than a single LINE? Reads the bar's
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
* reads as a single line, exactly as before.
|
|
910
|
+
* Is this text bar a multi-line BLOCK rather than a single LINE? Reads the bar's `ink` snapshot
|
|
911
|
+
* (the clamped run box and the parent's resolved line-height the walk recorded) and applies the
|
|
912
|
+
* `height >= lineHeight × 1.5` threshold (ADR 0021 §4). An unresolved line-height (0) reads as a
|
|
913
|
+
* single line.
|
|
947
914
|
*/
|
|
948
915
|
function inkIsMultiLine(ink) {
|
|
949
916
|
const runHeight = ink.lineRects[0]?.height ?? 0;
|
|
950
917
|
return ink.lineHeight > 0 && runHeight >= ink.lineHeight * 1.5;
|
|
951
918
|
}
|
|
952
919
|
/**
|
|
953
|
-
* MARK-INK — the classification pass (ADR 0022 invariant 5
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
920
|
+
* MARK-INK — the classification pass (ADR 0022 invariant 5). It owns BONE-KIND assignment, reading
|
|
921
|
+
* ONLY the IR snapshot (never the live DOM): `isMedia`, `paint`, `ink`, and `style`. The walk
|
|
922
|
+
* builds STRUCTURE + decls + the synthetic text bars and leaves a PROVISIONAL kind; `markInk`
|
|
923
|
+
* finalizes the leaf kinds and folds invisibility into a `dropped: { reason: 'invisible' }`
|
|
924
|
+
* annotation. Runs BEFORE `foldGlyphRowsIR` (which reads `kind: 'media'` / `'text-line'`).
|
|
925
|
+
*
|
|
926
|
+
* Per-node, the rules — UPGRADE-ONLY for the inferred kinds, so an explicit kind the walk set (a
|
|
927
|
+
* `ctx.bone()` bone, the `@container` text-rep's `text-block` bone) is never downgraded:
|
|
928
|
+
*
|
|
929
|
+
* - A DECLARED bone (`markKind`, from `data-xr-bone-kind`; ADR 0018; ADR 0022 §5) → re-stamped a
|
|
930
|
+
* CHILDLESS bone of that kind: the walk descended normally (measuring the full subtree into the
|
|
931
|
+
* IR), and here every effective kid is marked `dropped: { reason: 'collapsed' }` and the node
|
|
932
|
+
* synthesizes its own leaf display+size, so `effectiveKids`/emit lower it to one bone. This
|
|
933
|
+
* pre-empts the inferred kinds below (a declared kind is authoritative) and shares the Absorb
|
|
934
|
+
* re-stamp's display/size synthesis;
|
|
935
|
+
* - A text bar (`ink` present) → `text` (one baseline) or `text-block` (≥2);
|
|
936
|
+
* - An empty/childless ELEMENT (`el !== null`, no raw kids) that is `isMedia` → `media`; else a
|
|
937
|
+
* `container` seed whose `paint.surface` paints → `box` (the empty-element painted-box-vs-spacer
|
|
938
|
+
* split, off `paint` instead of a live `hasVisibleBackground`); an explicit seed (media/box/text)
|
|
939
|
+
* is never overwritten, and a non-painted spacer is left as a container.
|
|
960
940
|
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
* - a DECLARED bone (`markKind`, from `data-xr-bone-kind`; ADR 0018; ADR 0022 §5)
|
|
965
|
-
* → re-stamped a CHILDLESS bone of that kind: the walk descended normally
|
|
966
|
-
* (measuring the full subtree into the IR), and here every effective kid is
|
|
967
|
-
* marked `dropped: { reason: 'collapsed' }` and the node synthesizes its own
|
|
968
|
-
* leaf display+size, so `effectiveKids`/emit lower it to one bone. This pre-empts
|
|
969
|
-
* the inferred kinds below (a declared kind is authoritative) and shares the
|
|
970
|
-
* Absorb re-stamp's display/size synthesis;
|
|
971
|
-
* - a text bar (`ink` present) → `text` (one baseline) or `text-block` (≥2),
|
|
972
|
-
* the line-vs-block decision MOVED out of `textBar`;
|
|
973
|
-
* - an empty/childless ELEMENT (`el !== null`, no raw kids) that is `isMedia`
|
|
974
|
-
* → `media`; else a `container` seed whose `paint.surface` paints → `box`
|
|
975
|
-
* (the empty-element painted-box-vs-spacer split, off `paint` instead of a
|
|
976
|
-
* live `hasVisibleBackground`); an explicit seed (media/box/text) is never
|
|
977
|
-
* overwritten, and a non-painted spacer is left as a container.
|
|
941
|
+
* Invisibility (ADR 0022 §4) is annotated `dropped: { reason: 'invisible' }`. The two CSS
|
|
942
|
+
* mechanisms reach differently, and `markInk` models both off the snapshot, threading the inherited
|
|
943
|
+
* context DOWN (Measure DESCENDS both; it skips only `display: none`):
|
|
978
944
|
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
* -
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
* drops ONLY when it has no surviving effective kid — a re-shown descendant keeps
|
|
991
|
-
* the bone-free ancestors alive so it survives. This fixes the latent bug where a
|
|
992
|
-
* hidden ancestor used to vanish a re-shown subtree. A synthetic text bar carries
|
|
993
|
-
* no own visibility (empty snapshot), so it correctly INHERITS its parent's.
|
|
994
|
-
* Idempotent — never overwrites a prior drop (a glyph-folded icon keeps `folded`).
|
|
945
|
+
* - `opacity <= INVISIBLE_OPACITY` composites the element AND its whole subtree as one group and
|
|
946
|
+
* multiplies it to nothing — every descendant is hidden too and NOTHING below can re-show. The
|
|
947
|
+
* TOPMOST group-hidden node drops; `effectiveKids` lowers the whole subtree, so the descendants
|
|
948
|
+
* need no individual drop.
|
|
949
|
+
* - `visibility: hidden|collapse` is INHERITED but an ELEMENT descendant can override it
|
|
950
|
+
* (`visibility: visible` re-shows). A node is visibility-hidden when its own value hides it OR it
|
|
951
|
+
* inherits hidden and does not re-show; such a node drops ONLY when it has no surviving effective
|
|
952
|
+
* kid — a re-shown descendant keeps the bone-free ancestors alive so it survives; a hidden
|
|
953
|
+
* ancestor can never vanish a re-shown subtree. A synthetic text bar carries no own visibility
|
|
954
|
+
* (empty snapshot), so it correctly INHERITS its parent's. Idempotent — never overwrites a prior
|
|
955
|
+
* drop (a glyph-folded icon keeps `folded`).
|
|
995
956
|
*
|
|
996
957
|
* Pure: reads only the IR. Tested with a hand-built IR (markInk.test.ts).
|
|
997
958
|
*/
|
|
@@ -1031,7 +992,7 @@ function markInk(node, inherited = NOT_HIDDEN) {
|
|
|
1031
992
|
return;
|
|
1032
993
|
}
|
|
1033
994
|
if (node.ink) {
|
|
1034
|
-
node.kind = inkIsMultiLine(node.ink) ? "text-block" : "text";
|
|
995
|
+
node.kind = inkIsMultiLine(node.ink) ? "text-block" : "text-line";
|
|
1035
996
|
return;
|
|
1036
997
|
}
|
|
1037
998
|
if (node.el !== null && node.kids.length === 0) {
|
|
@@ -1061,49 +1022,46 @@ function markInk(node, inherited = NOT_HIDDEN) {
|
|
|
1061
1022
|
}
|
|
1062
1023
|
}
|
|
1063
1024
|
/**
|
|
1064
|
-
* The Salience contrast axis at/above which a NON-fill surface (a contrasting
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* scores ~0 and is left as a container, never over-absorbed. Tunable.
|
|
1025
|
+
* The Salience contrast axis at/above which a NON-fill surface (a contrasting border or a
|
|
1026
|
+
* box-shadow) is strong enough to read as one Bone on its own — Absorb's trigger for the cases the
|
|
1027
|
+
* contrasting fill never covered. Set so an INTENTIONAL, visible surface clears it and an
|
|
1028
|
+
* incidental hairline does not: a box-shadow scores 1; a contrasting border needs ~2px (the
|
|
1029
|
+
* contrast axis ramps border width 0→`BORDER_WIDTH_SATURATION`=4px, so 2px → 0.5) AND an opaque,
|
|
1030
|
+
* page-contrasting colour. Conservative by design — a 1px same-as-page divider scores ~0 and is
|
|
1031
|
+
* left as a container, never over-absorbed. Tunable.
|
|
1072
1032
|
*/
|
|
1073
1033
|
const ABSORB_CONTRAST_MIN = .5;
|
|
1074
1034
|
/**
|
|
1075
|
-
* Does this node's SURFACE absorb its label content into one Bone (ADR 0021 §3)?
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1035
|
+
* Does this node's SURFACE absorb its label content into one Bone (ADR 0021 §3)? PURE — reads ONLY
|
|
1036
|
+
* the snapshot Measure resolved at build time, never the DOM. Two additive paths, neither weakening
|
|
1037
|
+
* the other:
|
|
1078
1038
|
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1039
|
+
* - FILL — `paint.contrastingFill`, the resolved `hasContrastingFill` boolean: a filled control
|
|
1040
|
+
* collapses to one Bone.
|
|
1041
|
+
* - BORDER / BOX-SHADOW — the resolved Salience contrast axis (graded) at/above
|
|
1042
|
+
* `ABSORB_CONTRAST_MIN`. The axis is the MAX of fill/border/shadow, so this path only ever ADDS
|
|
1043
|
+
* surfaces the fill boolean missed: a card outlined or lifted off the page whose content is
|
|
1044
|
+
* label-only is itself the visual.
|
|
1085
1045
|
*
|
|
1086
|
-
* Conservative: a node with neither a contrasting fill NOR a strong border/shadow
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1046
|
+
* Conservative: a node with neither a contrasting fill NOR a strong border/shadow surface returns
|
|
1047
|
+
* false and keeps its children. An entry-less node (root/stitch/ synthetic bar) has no
|
|
1048
|
+
* `paint`/`salience` snapshot, so it never absorbs.
|
|
1089
1049
|
*/
|
|
1090
1050
|
function surfaceAbsorbs(node) {
|
|
1091
1051
|
if (node.paint?.contrastingFill) return true;
|
|
1092
1052
|
return node.salience !== void 0 && node.salience.contrast >= ABSORB_CONTRAST_MIN;
|
|
1093
1053
|
}
|
|
1094
1054
|
/**
|
|
1095
|
-
* Does this node's EFFECTIVE content reduce to a single label/run and nothing else
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
* MEDIA (`media` is never a swallowing surface, ADR 0021 §3), or a Stitch makes the
|
|
1106
|
-
* node a container/frame — draw the parts, keep the surface as a frame.
|
|
1055
|
+
* Does this node's EFFECTIVE content reduce to a single label/run and nothing else (ADR 0021 §3)?
|
|
1056
|
+
* PURE IR predicate over `effectiveKids`/`effectiveKind` — never the DOM. Every effective kid must
|
|
1057
|
+
* read as a text label drawing one bone with no independent structure:
|
|
1058
|
+
*
|
|
1059
|
+
* - A bare text leaf (`text` / `text-block`), incl. a glyph-fold survivor (an effective `text` bar);
|
|
1060
|
+
* - A thin wrapper whose sole effective kid is itself a single text leaf (a `<span>label</span>`
|
|
1061
|
+
* blockified in a flex/grid control, or the `@container` text-rep wrapper around one `text-block`
|
|
1062
|
+
* bone). A SECOND distinct shape (a `box`, a non-folded sibling structure), any REPLACED MEDIA
|
|
1063
|
+
* (`media` is never a swallowing surface, ADR 0021 §3), or a Stitch makes the node a
|
|
1064
|
+
* container/frame — draw the parts, keep the surface as a frame.
|
|
1107
1065
|
*/
|
|
1108
1066
|
function isLabelContent(node) {
|
|
1109
1067
|
return effectiveKids(node).every(isLabelUnit);
|
|
@@ -1111,43 +1069,37 @@ function isLabelContent(node) {
|
|
|
1111
1069
|
/** A single effective kid that reads as one text label (no media, no distinct shape, no stitch). */
|
|
1112
1070
|
function isLabelUnit(node) {
|
|
1113
1071
|
const kind = effectiveKind(node);
|
|
1114
|
-
if (kind === "text" || kind === "text-block") return true;
|
|
1072
|
+
if (kind === "text-line" || kind === "text-block") return true;
|
|
1115
1073
|
if (kind !== "container") return false;
|
|
1116
1074
|
const kids = effectiveKids(node);
|
|
1117
1075
|
const only = kids[0];
|
|
1118
1076
|
if (kids.length !== 1 || only === void 0) return false;
|
|
1119
1077
|
const onlyKind = effectiveKind(only);
|
|
1120
|
-
return (onlyKind === "text" || onlyKind === "text-block") && effectiveKids(only).length === 0;
|
|
1078
|
+
return (onlyKind === "text-line" || onlyKind === "text-block") && effectiveKids(only).length === 0;
|
|
1121
1079
|
}
|
|
1122
1080
|
const NOT_HIDDEN = {
|
|
1123
1081
|
groupHidden: false,
|
|
1124
1082
|
visHidden: false
|
|
1125
1083
|
};
|
|
1126
1084
|
/**
|
|
1127
|
-
* Fold a glyph-sized icon Bone into an adjacent single-line text Bone — the
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
* text-line bar sized to their union. A bigger or gutter-gapped icon fails the
|
|
1134
|
-
* geometry and stays its own Bone.
|
|
1085
|
+
* Fold a glyph-sized icon Bone into an adjacent single-line text Bone — the geometry-driven group
|
|
1086
|
+
* pass (ADR 0021 §2, "row anything"), run over the built IR. For each NON-FLOW parent (flex / grid
|
|
1087
|
+
* / table / ruby — normal flow already folds its icon into the run during the walk), a `media` kid
|
|
1088
|
+
* that hugs a neighbouring element-backed text-line on the same row (`boxGlyphGeometry` over the
|
|
1089
|
+
* snapshotted boxes) folds WITH that neighbour into one text-line bar sized to their union. A
|
|
1090
|
+
* bigger or gutter-gapped icon fails the geometry and stays its own Bone.
|
|
1135
1091
|
*
|
|
1136
|
-
* ANNOTATE-ONLY (ADR 0022 §4): the pass NEVER restructures `kids`. For each match it
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* effective-node accessors (`effectiveKind`/`effectiveKids`), so a folded node behaves
|
|
1144
|
-
* exactly like the old synthetic bar without the tree surgery.
|
|
1092
|
+
* ANNOTATE-ONLY (ADR 0022 §4): the pass NEVER restructures `kids`. For each match it SETS `folded`
|
|
1093
|
+
* on the SURVIVING text unit — the effective bone box (their union) and the synthesized
|
|
1094
|
+
* inline-block + width/height decls of the bar — and marks the absorbed icon `dropped: { reason:
|
|
1095
|
+
* 'folded' }`. The surviving node keeps its `id` and its place in `bundle.nodes`, so `emit`
|
|
1096
|
+
* projects one text-bar leaf (icon gone, unit as a `text` leaf sized to the union); every
|
|
1097
|
+
* downstream pass reads the union via `boneBox(node)` and the leaf shape via the effective-node
|
|
1098
|
+
* accessors (`effectiveKind`/`effectiveKids`).
|
|
1145
1099
|
*
|
|
1146
|
-
* Scan/skip semantics
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
* merge replaced the unit with an `el: null` bar, which the `el !== null` guard
|
|
1150
|
-
* rejected — `folded` is the explicit mirror, since the node is left in place here).
|
|
1100
|
+
* Scan/skip semantics: an icon prefers its LEFT neighbour (`i - 1` before `i + 1`); a dropped
|
|
1101
|
+
* (already-folded) icon never folds again; and a folded unit is never re-used as another icon's
|
|
1102
|
+
* neighbour.
|
|
1151
1103
|
*/
|
|
1152
1104
|
function foldGlyphRowsIR(node) {
|
|
1153
1105
|
for (const kid of node.kids) foldGlyphRowsIR(kid);
|
|
@@ -1184,29 +1136,22 @@ function foldGlyphRowsIR(node) {
|
|
|
1184
1136
|
}
|
|
1185
1137
|
}
|
|
1186
1138
|
/**
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
* Bone-free and position-faithful, shrinking the plate.
|
|
1139
|
+
* FLATTEN (ADR 0022, per-View): collapse passthrough wrapper chains. A layout-only container —
|
|
1140
|
+
* containers paint no Bone (only leaves carry `xr-leaf`), a single child, its box coincident with
|
|
1141
|
+
* that child, no margin or positional offset of its own — contributes nothing the child does not
|
|
1142
|
+
* already produce. Promoting the child into a normal-flow grandparent drops the wrapper Bone-free
|
|
1143
|
+
* and position-faithful, shrinking the plate.
|
|
1193
1144
|
*
|
|
1194
|
-
* Annotate-only (ADR 0022 §4): the pass NEVER restructures the tree. It SETS
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
* geometry/decls, so later passes and diagnostics still see it.
|
|
1145
|
+
* Annotate-only (ADR 0022 §4): the pass NEVER restructures the tree. It SETS `flattened = true` on
|
|
1146
|
+
* each passthrough wrapper that should be spliced; Serialize (`emit`, ir.ts) is the sole structural
|
|
1147
|
+
* mutation — it omits a `flattened` node and hoists its kids into the parent's slot. The wrapper
|
|
1148
|
+
* stays in the IR with its geometry/decls, so later passes and diagnostics still see it.
|
|
1199
1149
|
*
|
|
1200
|
-
* Bottom-up so a chain collapses to a fixpoint
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
* what keep it position-faithful. The marked set is the wrappers the old
|
|
1206
|
-
* destructive Flatten removed via `collapseWrapperChain` — same predicate, gate,
|
|
1207
|
-
* and chain walk (modulo the per-hop 1px coincidence tolerance, evaluated against
|
|
1208
|
-
* the unmutated neighbor rather than the collapsed survivor; transitive within
|
|
1209
|
-
* tolerance, so the set agrees in every realistic chain — see `markWrapperChain`).
|
|
1150
|
+
* Bottom-up so a chain collapses to a fixpoint (`<div><div><div>x</div></div></div>` → one node:
|
|
1151
|
+
* every wrapper in the chain is marked). Only flattens wrappers under a normal-flow parent — a
|
|
1152
|
+
* flex/grid item's slot needs the wrapper's flex sizing, which the splice does not transfer — and
|
|
1153
|
+
* never crosses a Stitch (opaque, ADR 0006). The conditions in `isPassthroughWrapper` are what keep
|
|
1154
|
+
* it position-faithful.
|
|
1210
1155
|
*/
|
|
1211
1156
|
function flattenIR(node) {
|
|
1212
1157
|
if (node.kind === "stitch") return;
|
|
@@ -1214,14 +1159,10 @@ function flattenIR(node) {
|
|
|
1214
1159
|
if (establishesInlineFlow(node.style.display)) for (const kid of node.kids) markWrapperChain(kid);
|
|
1215
1160
|
}
|
|
1216
1161
|
/**
|
|
1217
|
-
* Mark every passthrough wrapper in the chain rooted at `node` `flattened`, down to
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* coincidence check is wrapper-vs-its-own-child (the old code, walking the already
|
|
1222
|
-
* collapsed kids, checked wrapper-vs-survivor). Coincidence is transitive within
|
|
1223
|
-
* the 1px tolerance, so the marked set agrees in every realistic chain. Sets fields
|
|
1224
|
-
* only; never touches `kids`.
|
|
1162
|
+
* Mark every passthrough wrapper in the chain rooted at `node` `flattened`, down to the first node
|
|
1163
|
+
* that survives. Walks the unmutated chain, so each coincidence check is wrapper-vs-its-own-child;
|
|
1164
|
+
* coincidence is transitive within the 1px tolerance, so a whole chain marks consistently. Sets
|
|
1165
|
+
* fields only; never touches `kids`.
|
|
1225
1166
|
*/
|
|
1226
1167
|
function markWrapperChain(node) {
|
|
1227
1168
|
let cursor = node;
|
|
@@ -1233,11 +1174,10 @@ function markWrapperChain(node) {
|
|
|
1233
1174
|
}
|
|
1234
1175
|
}
|
|
1235
1176
|
/**
|
|
1236
|
-
* A container the skeleton can drop without moving anything: one child, its box
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
* descendant's `var()` reads it). A stitch child has no box, so it is never
|
|
1177
|
+
* A container the skeleton can drop without moving anything: one child, its box coincident with the
|
|
1178
|
+
* child (so no padding/border insetting it and no size of its own), no margin and `position:
|
|
1179
|
+
* static` with no `transform` (so no offset the promoted child would lose), and no carried token
|
|
1180
|
+
* def anchored to it (a descendant's `var()` reads it). A stitch child has no box, so it is never
|
|
1241
1181
|
* coincident — stitch wrappers are conservatively kept.
|
|
1242
1182
|
*/
|
|
1243
1183
|
function isPassthroughWrapper(node) {
|
|
@@ -1262,28 +1202,25 @@ function marginIsZero(margin) {
|
|
|
1262
1202
|
return margin.split(/\s+/).every((v) => v === "0px" || v === "0");
|
|
1263
1203
|
}
|
|
1264
1204
|
/**
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
* `
|
|
1270
|
-
* leaves empty (`orphaned`).
|
|
1205
|
+
* SUPERSET (ADR 0022, per-View). When a Bone's box sits wholly inside a SOLID Bone's box, the
|
|
1206
|
+
* enclosing Bone _supersets_ it — it paints the same area, so the two render as one and the layered
|
|
1207
|
+
* "bone-over-bone" look the inner one would add is barely perceptible. Annotate the inner Bone (the
|
|
1208
|
+
* `subset`) `dropped` so `emit` lowers it out, then prune any container the drop leaves empty
|
|
1209
|
+
* (`orphaned`).
|
|
1271
1210
|
*
|
|
1272
|
-
* Order / opacity / stacking are IRRELEVANT — it is a set union, so the superset
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
* is a subset. Non-destructive — the node stays in the IR (see `IRNode.dropped`).
|
|
1280
|
-
* Runs last of the IR passes.
|
|
1211
|
+
* Order / opacity / stacking are IRRELEVANT — it is a set union, so the superset always wins
|
|
1212
|
+
* regardless of which paints on top. The subset can be ANY kind (a text line / block on a solid
|
|
1213
|
+
* surface is the same redundant bone-over-bone, so it goes too); only the SUPERSET must be SOLID —
|
|
1214
|
+
* a gappy `text` Bone or a paint-less branch never supersets, since the union must actually fill
|
|
1215
|
+
* the area. Decided by geometry, global within the plate (an overlay nested in one subtree can
|
|
1216
|
+
* superset media in another); a stitch has no box, so it neither supersets nor is a subset.
|
|
1217
|
+
* Non-destructive — the node stays in the IR (see `IRNode.dropped`). Runs last of the IR passes.
|
|
1281
1218
|
*
|
|
1282
|
-
* WATCH IN QA: a large solid Bone supersets ANY bone inside it — intended for a
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1219
|
+
* WATCH IN QA: a large solid Bone supersets ANY bone inside it — intended for a filled card
|
|
1220
|
+
* collapsing its on-surface lines/badges, but a full-bleed background `<img>` would also swallow a
|
|
1221
|
+
* button/CTA or headline laid on it. A white/outline card stays a paint-less container, so its
|
|
1222
|
+
* content is kept; only a CONTRASTING (bone-producing) surface supersets. If meaningful content
|
|
1223
|
+
* vanishes, add a guard.
|
|
1287
1224
|
*/
|
|
1288
1225
|
function supersetIR(root, diagnostics) {
|
|
1289
1226
|
const leaves = [];
|
|
@@ -1306,13 +1243,12 @@ function supersetIR(root, diagnostics) {
|
|
|
1306
1243
|
pruneEmptyBranches(root);
|
|
1307
1244
|
}
|
|
1308
1245
|
/**
|
|
1309
|
-
* Gather every leaf with its edges (any kind — all are
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
* only be a container if it truly paints that rect, so a boxless solid is omitted.
|
|
1246
|
+
* Gather every leaf with its edges (any kind — all are droppable). A leaf without its own box falls
|
|
1247
|
+
* back to its nearest box-bearing ancestor's box: a multi-line `text-block` reflow bone is created
|
|
1248
|
+
* boxless (it sizes via `@container` bands, not a rect), but its rendered extent IS its ancestor
|
|
1249
|
+
* element's box, so without this fallback an overlay caption that wrapped to a `text-block` would
|
|
1250
|
+
* never be recognized as contained. A SOLID (`box`/`media`) bone never falls back: it can only be a
|
|
1251
|
+
* container if it truly paints that rect, so a boxless solid is omitted.
|
|
1316
1252
|
*/
|
|
1317
1253
|
function collectLeaves(node, out, inherited) {
|
|
1318
1254
|
const own = edgesOf(node);
|
|
@@ -1328,17 +1264,16 @@ function collectLeaves(node, out, inherited) {
|
|
|
1328
1264
|
}
|
|
1329
1265
|
/** A painted leaf (carries a bone): `box`/`media`/`text`/`text-block`. */
|
|
1330
1266
|
function isLeaf(kind) {
|
|
1331
|
-
return kind === "box" || kind === "media" || kind === "text" || kind === "text-block";
|
|
1267
|
+
return kind === "box" || kind === "media" || kind === "text-line" || kind === "text-block";
|
|
1332
1268
|
}
|
|
1333
1269
|
/** A filled bone that paints its whole rect — `box`/`media`. Only these may be the container. */
|
|
1334
1270
|
function isSolidLeaf(kind) {
|
|
1335
1271
|
return kind === "box" || kind === "media";
|
|
1336
1272
|
}
|
|
1337
1273
|
/**
|
|
1338
|
-
* A node's EFFECTIVE bone box as edges, or null if missing/degenerate (0-area). Reads
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
* synthetic merged bar had.
|
|
1274
|
+
* A node's EFFECTIVE bone box as edges, or null if missing/degenerate (0-area). Reads `boneBox`
|
|
1275
|
+
* (ADR 0022 invariant 3), so a glyph-fold survivor reports its `folded.box` (the icon+text union)
|
|
1276
|
+
* instead of its raw text box.
|
|
1342
1277
|
*/
|
|
1343
1278
|
function edgesOf(node) {
|
|
1344
1279
|
const b = boneBox(node);
|
|
@@ -1355,10 +1290,10 @@ function coversFully(outer, inner) {
|
|
|
1355
1290
|
return outer.left <= inner.left + 1 && outer.top <= inner.top + 1 && outer.right >= inner.right - 1 && outer.bottom >= inner.bottom - 1;
|
|
1356
1291
|
}
|
|
1357
1292
|
/**
|
|
1358
|
-
* After Superset, drop any container it emptied — a container whose every child
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1293
|
+
* After Superset, drop any container it emptied — a container whose every child is now dropped
|
|
1294
|
+
* paints nothing and adds only dead structure. Bottom-up, so an emptied parent propagates up its
|
|
1295
|
+
* chain. Skips originally-empty containers (no kids — other passes own those) and never drops the
|
|
1296
|
+
* root.
|
|
1362
1297
|
*/
|
|
1363
1298
|
function pruneEmptyBranches(node) {
|
|
1364
1299
|
for (const kid of node.kids) pruneEmptyBranches(kid);
|
|
@@ -1366,70 +1301,61 @@ function pruneEmptyBranches(node) {
|
|
|
1366
1301
|
if (node.kids.every((kid) => kid.dropped !== void 0)) node.dropped = { reason: "orphaned" };
|
|
1367
1302
|
}
|
|
1368
1303
|
/**
|
|
1369
|
-
* Crop's ONLY knob: a multiplier on a clipping context's visible bottom, so the
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1304
|
+
* Crop's ONLY knob: a multiplier on a clipping context's visible bottom, so the fold sits a
|
|
1305
|
+
* half-context BELOW the visible edge. It covers render-time reflow to a taller viewport/container
|
|
1306
|
+
* (ADR 0004) — the captured fold is a width-dependent, render-time line, so this buffer keeps it
|
|
1307
|
+
* deliberately conservative. The buffer is applied ONLY at the final per-leaf compare, never
|
|
1308
|
+
* accumulated into the clip bottom threaded to children, so it never compounds across nesting
|
|
1309
|
+
* levels.
|
|
1375
1310
|
*/
|
|
1376
1311
|
const CROP_BUFFER = 1.5;
|
|
1377
1312
|
/**
|
|
1378
|
-
* CROP —
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
* horizontal axis closes the off-screen-RIGHT carousel-card case Template cannot
|
|
1313
|
+
* CROP — the past-the-fold drop (ADR 0022, per-View). 2D: drops every Bone whose box sits past the
|
|
1314
|
+
* FOLD on EITHER axis — its TOP below the visible BOTTOM, or its LEFT past the visible RIGHT edge,
|
|
1315
|
+
* of that Bone's NEAREST CLIPPING CONTEXT (a vertically- OR horizontally-scrolling /
|
|
1316
|
+
* overflow-clipping ancestor, or the viewport as the base case) — then orphan-prunes the emptied
|
|
1317
|
+
* containers. The horizontal axis closes the off-screen-RIGHT carousel-card case Template cannot
|
|
1384
1318
|
* dedup (a single long run past its scroller's clip).
|
|
1385
1319
|
*
|
|
1386
|
-
* - **No spacer.** Nothing reserves the dropped extent; the skeleton simply ends
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
* `
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
* `
|
|
1406
|
-
*
|
|
1407
|
-
* so the viewport folds (`viewportHeight`/`viewportWidth`) and the clip-ancestor
|
|
1408
|
-
* bottoms/rights (`box.bottom`/`box.right`) live in the SAME space — no rebasing.
|
|
1409
|
-
* - **Architecture (ADR 0022 invariants 3 & 5).** Measure only snapshots the raw
|
|
1410
|
-
* clip inputs (`overflow-y`/`overflow-x`, `scroll*`/`client*`); THIS pass derives
|
|
1411
|
-
* the clip bottom/right. The walk records numbers, the pure pass owns all
|
|
1412
|
-
* derivation.
|
|
1320
|
+
* - **No spacer.** Nothing reserves the dropped extent; the skeleton simply ends at the fold.
|
|
1321
|
+
* - **Why no CLS.** A dropped Bone was off-screen (below the viewport fold, or right of it) or inside
|
|
1322
|
+
* a scroll/overflow container past its clip — never visible at capture. When real content lands
|
|
1323
|
+
* it just grows the scroll height/width (or, for `overflow:hidden`/`clip`, is never visible at
|
|
1324
|
+
* all); off-screen / clipped shifts do not score CLS, so the drop is layout-shift-free.
|
|
1325
|
+
* - **Reflow caveat (ADR 0004).** The fold is a render-time, width-dependent line (text rewraps at
|
|
1326
|
+
* the render width, not the capture width), so the capture-time fold is conservative via
|
|
1327
|
+
* `CROP_BUFFER`; a viewport / container larger than the one captured briefly shows blank past the
|
|
1328
|
+
* fold until content fills — transient, no shift.
|
|
1329
|
+
* - **LTR-first; RTL a known limit.** The horizontal fold drops content past the RIGHT edge (the
|
|
1330
|
+
* off-screen direction in LTR). In an RTL carousel the off-screen cards sit to the LEFT, so the
|
|
1331
|
+
* right-edge fold does not catch them — an accepted known limit (the base-case viewport fold
|
|
1332
|
+
* still drops anything genuinely right of `innerWidth`).
|
|
1333
|
+
* - **Per-View.** Views are independent and breakpoint-gated (ADR 0004); each carries its own folds
|
|
1334
|
+
* (`viewportHeight`/`viewportWidth` = that View's `window.innerHeight`/`innerWidth`).
|
|
1335
|
+
* - **Coordinate space.** Boxes are raw `getBoundingClientRect` (viewport-relative), so the viewport
|
|
1336
|
+
* folds (`viewportHeight`/`viewportWidth`) and the clip-ancestor bottoms/rights
|
|
1337
|
+
* (`box.bottom`/`box.right`) live in the SAME space — no rebasing.
|
|
1338
|
+
* - **Architecture (ADR 0022 invariants 3 & 5).** Measure only snapshots the raw clip inputs
|
|
1339
|
+
* (`overflow-y`/`overflow-x`, `scroll*`/`client*`); THIS pass derives the clip bottom/right. The
|
|
1340
|
+
* walk records numbers, the pure pass owns all derivation.
|
|
1413
1341
|
*
|
|
1414
|
-
* A top-down traversal threading the RAW (un-buffered) clip bottom AND right: a node
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
* `
|
|
1420
|
-
*
|
|
1421
|
-
* only annotate `dropped`.
|
|
1342
|
+
* A top-down traversal threading the RAW (un-buffered) clip bottom AND right: a node lives inside
|
|
1343
|
+
* its PARENT's clip context, so the folds that judge a node are its parent's clip bottom/right; the
|
|
1344
|
+
* node's own clipping constrains its CHILDREN. The buffer is applied only at the leaf compare so it
|
|
1345
|
+
* never compounds down nesting. Reuses `isLeaf`/`isSolidLeaf`/`edgesOf` and the boxless-fallback
|
|
1346
|
+
* rule from `collectLeaves`, and `pruneEmptyBranches` for the orphan sweep. Non-destructive
|
|
1347
|
+
* (`IRNode.dropped`); `emit` lowers it. Order vs Superset is immaterial — both only annotate
|
|
1348
|
+
* `dropped`.
|
|
1422
1349
|
*/
|
|
1423
1350
|
function cropIR(root, viewportHeight, viewportWidth, diagnostics) {
|
|
1424
1351
|
cropNode(root, viewportHeight, viewportWidth, null, diagnostics);
|
|
1425
1352
|
pruneEmptyBranches(root);
|
|
1426
1353
|
}
|
|
1427
1354
|
/**
|
|
1428
|
-
* Does this node clip its content vertically — a scroll/overflow context whose
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
* (
|
|
1432
|
-
* not snapshotted (overflow-y was not clipping at measure time), this is correctly
|
|
1355
|
+
* Does this node clip its content vertically — a scroll/overflow context whose visible bottom is a
|
|
1356
|
+
* fold for its children? `overflow-y` is one of auto|scroll|hidden|clip AND the content actually
|
|
1357
|
+
* overflows the client box (`scrollHeight > clientHeight`, +1 absorbing sub-pixel). When the
|
|
1358
|
+
* metrics were not snapshotted (overflow-y was not clipping at measure time), this is correctly
|
|
1433
1359
|
* false.
|
|
1434
1360
|
*/
|
|
1435
1361
|
function nodeClipsY(node) {
|
|
@@ -1437,14 +1363,13 @@ function nodeClipsY(node) {
|
|
|
1437
1363
|
return node.scrollHeight !== void 0 && node.clientHeight !== void 0 && node.scrollHeight > node.clientHeight + 1;
|
|
1438
1364
|
}
|
|
1439
1365
|
/**
|
|
1440
|
-
* Does this node clip its content HORIZONTALLY — a scroll/overflow context whose
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
* wrapped items are judged against the wider ancestor/viewport fold and kept.
|
|
1366
|
+
* Does this node clip its content HORIZONTALLY — a scroll/overflow context whose visible right edge
|
|
1367
|
+
* is a fold for its children (a carousel)? The horizontal mirror of `nodeClipsY`: `overflow-x` is
|
|
1368
|
+
* one of auto|scroll|hidden|clip AND the content actually overflows the client box (`scrollWidth >
|
|
1369
|
+
* clientWidth`, +1 absorbing sub-pixel). When the metrics were not snapshotted (overflow-x was not
|
|
1370
|
+
* clipping at measure time), this is correctly false — so a `flex-wrap`/grid that merely WRAPS to
|
|
1371
|
+
* more rows (`overflow-x: visible`) never tightens the horizontal fold; its wrapped items are
|
|
1372
|
+
* judged against the wider ancestor/viewport fold and kept.
|
|
1448
1373
|
*/
|
|
1449
1374
|
function nodeClipsX(node) {
|
|
1450
1375
|
if (!/^(auto|scroll|hidden|clip)$/.test(node.style.overflowX)) return false;
|
|
@@ -1452,13 +1377,12 @@ function nodeClipsX(node) {
|
|
|
1452
1377
|
}
|
|
1453
1378
|
/**
|
|
1454
1379
|
* Recurse top-down threading `(inheritedAncestorBox, clipBottom, clipRight)`.
|
|
1455
|
-
* `clipBottom`/`clipRight` are the folds that apply to THIS node (its parent
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
* content is never over-cropped.
|
|
1380
|
+
* `clipBottom`/`clipRight` are the folds that apply to THIS node (its parent context's visible
|
|
1381
|
+
* bottom / right edge); `inheritedAncestorBox` is the nearest box-bearing ancestor's edges, for the
|
|
1382
|
+
* boxless-leaf fallback (identical to `collectLeaves`). The clip bottom/right passed to children
|
|
1383
|
+
* tightens to this node's box bottom/right when the node itself clips that axis — and ONLY then: a
|
|
1384
|
+
* node that merely wraps (overflow:visible) never tightens, so normal in-viewport wrapped content
|
|
1385
|
+
* is never over-cropped.
|
|
1462
1386
|
*/
|
|
1463
1387
|
function cropNode(node, clipBottom, clipRight, inheritedAncestorBox, diagnostics) {
|
|
1464
1388
|
const own = edgesOf(node);
|
|
@@ -1475,33 +1399,30 @@ function cropNode(node, clipBottom, clipRight, inheritedAncestorBox, diagnostics
|
|
|
1475
1399
|
for (const kid of effectiveKids(node)) cropNode(kid, childClipBottom, childClipRight, own ?? inheritedAncestorBox, diagnostics);
|
|
1476
1400
|
}
|
|
1477
1401
|
/**
|
|
1478
|
-
*
|
|
1479
|
-
* structurally-similar sibling cells (a feed/grid) into ONE representative cell
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
* ships one cell instead of N.
|
|
1402
|
+
* TEMPLATE (per-View, runs last): collapse a run of >= MIN_TEMPLATE_RUN consecutive,
|
|
1403
|
+
* structurally-similar sibling cells (a feed/grid) into ONE representative cell carrying `count =
|
|
1404
|
+
* N`. The serializer emits the cell's HTML once plus the count and the renderer expands it back to
|
|
1405
|
+
* N (chunk.ts / react.core.tsx), so the plate ships one cell instead of N.
|
|
1483
1406
|
*
|
|
1484
|
-
* Template runs LAST of the IR passes (after Superset and Crop), so it compresses ONLY
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1407
|
+
* Template runs LAST of the IR passes (after Superset and Crop), so it compresses ONLY what
|
|
1408
|
+
* survives the removal passes — what a viewer would actually see. Crop drops a run's off-screen
|
|
1409
|
+
* tail first; Template then groups the on-screen prefix and `count = run.length` renders back to
|
|
1410
|
+
* exactly that surviving N. Template is therefore LOSSLESS compression (one cell + a faithful
|
|
1411
|
+
* count), so it warns about nothing; the lossy off-screen removal is Crop's job
|
|
1412
|
+
* (`dropped-cropped`).
|
|
1490
1413
|
*
|
|
1491
|
-
* Cells need not be pixel-identical — text length varies cell to cell — so the
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
* badge
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
* dropped cell breaks the run). The dropped cells' `@container` rep bands are pruned
|
|
1501
|
-
* from `extraRules` so no dead CSS ships.
|
|
1414
|
+
* Cells need not be pixel-identical — text length varies cell to cell — so the match is STRUCTURAL:
|
|
1415
|
+
* a multiset of every descendant's `structuralHash` (kind + author-decls signature, dimensions
|
|
1416
|
+
* excluded), compared within a loose tolerance (TEMPLATE_SHAPE_TOLERANCE differing nodes, so a
|
|
1417
|
+
* minority FEATURED badge still groups). The template takes the run's MODAL structure (a minority
|
|
1418
|
+
* badge normalizes away) and, for each fixed-size bone, the rounded median width/height across the
|
|
1419
|
+
* run; text-block bones reflow via `@container`, so their dims are left alone. Only stitch-free,
|
|
1420
|
+
* count-free, NON-dropped cells are templated (the cell must flatten to one HTML string; nested
|
|
1421
|
+
* templates are out of scope; a dropped cell breaks the run). The dropped cells' `@container` rep
|
|
1422
|
+
* bands are pruned from `extraRules` so no dead CSS ships.
|
|
1502
1423
|
*
|
|
1503
|
-
* Loose by design — the tolerance is a single knob; tighten it toward 0 (exact)
|
|
1504
|
-
*
|
|
1424
|
+
* Loose by design — the tolerance is a single knob; tighten it toward 0 (exact) if false positives
|
|
1425
|
+
* surface.
|
|
1505
1426
|
*/
|
|
1506
1427
|
const MIN_TEMPLATE_RUN = 3;
|
|
1507
1428
|
const TEMPLATE_SHAPE_TOLERANCE = 1;
|
|
@@ -1512,7 +1433,10 @@ function templateIR(bundle) {
|
|
|
1512
1433
|
collapseTemplateRuns(bundle.root, dropped);
|
|
1513
1434
|
if (dropped.size > 0) bundle.extraRules = bundle.extraRules.filter((rule) => !rule.ids.every((id) => dropped.has(id)));
|
|
1514
1435
|
}
|
|
1515
|
-
/**
|
|
1436
|
+
/**
|
|
1437
|
+
* Recurse bottom-up, then replace each maximal template run among a node's kids with one cell +
|
|
1438
|
+
* count.
|
|
1439
|
+
*/
|
|
1516
1440
|
function collapseTemplateRuns(node, dropped) {
|
|
1517
1441
|
if (node.folded) return;
|
|
1518
1442
|
for (const kid of node.kids) collapseTemplateRuns(kid, dropped);
|
|
@@ -1537,13 +1461,12 @@ function collapseTemplateRuns(node, dropped) {
|
|
|
1537
1461
|
node.kids = out;
|
|
1538
1462
|
}
|
|
1539
1463
|
/**
|
|
1540
|
-
* Maximal run of consecutive templateable kids from `start` matching a common modal
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
* `dropped`, `emit` lowers it).
|
|
1464
|
+
* Maximal run of consecutive templateable kids from `start` matching a common modal shape within
|
|
1465
|
+
* tolerance. A `dropped` cell is non-templatable (`isStaticCell` rejects it), so it breaks the run
|
|
1466
|
+
* AND is rejected as a first cell — Template runs LAST, after Crop, so it only ever groups
|
|
1467
|
+
* NON-dropped (on-screen surviving) cells. Crop drops a contiguous off-screen tail, so this yields
|
|
1468
|
+
* the surviving prefix as the run; the dropped tail flows through `collapseTemplateRuns`'s
|
|
1469
|
+
* else-branch unchanged (still `dropped`, `emit` lowers it).
|
|
1547
1470
|
*/
|
|
1548
1471
|
function templateRun(kids, start) {
|
|
1549
1472
|
const first = kids[start];
|
|
@@ -1560,21 +1483,21 @@ function templateRun(kids, start) {
|
|
|
1560
1483
|
}
|
|
1561
1484
|
return run;
|
|
1562
1485
|
}
|
|
1563
|
-
/**
|
|
1486
|
+
/**
|
|
1487
|
+
* A clear majority of the run matches its modal shape exactly (>= MIN_TEMPLATE_MAJORITY) — else
|
|
1488
|
+
* there's no single template.
|
|
1489
|
+
*/
|
|
1564
1490
|
function hasModalMajority(run) {
|
|
1565
1491
|
const shapes = run.map(cellShape);
|
|
1566
1492
|
const modalKey = modalShape(shapes).join(SHAPE_SEP);
|
|
1567
1493
|
return shapes.filter((shape) => shape.join(SHAPE_SEP) === modalKey).length >= run.length * MIN_TEMPLATE_MAJORITY;
|
|
1568
1494
|
}
|
|
1569
1495
|
/**
|
|
1570
|
-
* Replace a run with one template cell: modal structure, median fixed-bone dims, and
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
* faithful count — so there is nothing to warn about; the lossy off-screen removal is
|
|
1576
|
-
* Crop's job and already emits `dropped-cropped`. The median dims are taken over the
|
|
1577
|
-
* modal cells.
|
|
1496
|
+
* Replace a run with one template cell: modal structure, median fixed-bone dims, and `count =
|
|
1497
|
+
* run.length` — the full SURVIVING count. Template runs LAST (after Crop), so the run holds only
|
|
1498
|
+
* on-screen cells (Crop already dropped any off-screen tail, and a dropped cell breaks the run in
|
|
1499
|
+
* `templateRun`); the count renders back to exactly the N a viewer would see. The median dims are
|
|
1500
|
+
* taken over the modal cells.
|
|
1578
1501
|
*/
|
|
1579
1502
|
function synthesizeTemplate(run, dropped) {
|
|
1580
1503
|
const modalKey = modalShape(run.map(cellShape)).join(SHAPE_SEP);
|
|
@@ -1587,23 +1510,26 @@ function synthesizeTemplate(run, dropped) {
|
|
|
1587
1510
|
return template;
|
|
1588
1511
|
}
|
|
1589
1512
|
/**
|
|
1590
|
-
* A cell that can flatten to one HTML string: no stitch and no (nested) template
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1513
|
+
* A cell that can flatten to one HTML string: no stitch and no (nested) template anywhere within.
|
|
1514
|
+
* Reads the EFFECTIVE structure (ADR 0022 §3): a `flattened` wrapper contributes nothing of its own
|
|
1515
|
+
* and is judged purely by its hoisted kids, so the verdict matches what `emit` projects — mirroring
|
|
1516
|
+
* `effectiveKids`.
|
|
1594
1517
|
*
|
|
1595
|
-
* A `dropped` cell is NON-templatable: Template runs LAST (after Crop), so a dropped
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1518
|
+
* A `dropped` cell is NON-templatable: Template runs LAST (after Crop), so a dropped (off-screen /
|
|
1519
|
+
* superseted / etc.) cell must never be grouped — it breaks the run in `templateRun` and is
|
|
1520
|
+
* rejected as a first cell, so Template only ever compresses the on-screen survivors.
|
|
1521
|
+
* `effectiveKids` already prunes a dropped descendant, so only the cell's OWN drop needs the
|
|
1522
|
+
* explicit guard here.
|
|
1600
1523
|
*/
|
|
1601
1524
|
function isStaticCell(node) {
|
|
1602
1525
|
if (node.dropped !== void 0) return false;
|
|
1603
1526
|
if (!node.flattened && (node.kind === "stitch" || node.ref !== void 0 || node.count !== void 0)) return false;
|
|
1604
1527
|
return effectiveKids(node).every(isStaticCell);
|
|
1605
1528
|
}
|
|
1606
|
-
/**
|
|
1529
|
+
/**
|
|
1530
|
+
* This bone's structural identity for templating: kind + a dimension-free author-decls signature.
|
|
1531
|
+
* Memoized.
|
|
1532
|
+
*/
|
|
1607
1533
|
function structuralHash(node) {
|
|
1608
1534
|
if (node.structuralHash !== void 0) return node.structuralHash;
|
|
1609
1535
|
const decls = effectiveDecls(node);
|
|
@@ -1614,11 +1540,10 @@ function structuralHash(node) {
|
|
|
1614
1540
|
return hash;
|
|
1615
1541
|
}
|
|
1616
1542
|
/**
|
|
1617
|
-
* A cell's shape: the sorted multiset of every EFFECTIVE descendant's
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
* its content in a coincident passthrough wrapper. Mirrors `effectiveKids`.
|
|
1543
|
+
* A cell's shape: the sorted multiset of every EFFECTIVE descendant's `structuralHash`. Reads
|
|
1544
|
+
* through `flattened` (ADR 0022 §3): a flattened wrapper contributes no hash of its own and its
|
|
1545
|
+
* kids are hoisted, so a run of cells templates on the shape `emit` projects — identical whether or
|
|
1546
|
+
* not each cell wraps its content in a coincident passthrough wrapper. Mirrors `effectiveKids`.
|
|
1622
1547
|
*/
|
|
1623
1548
|
function cellShape(node) {
|
|
1624
1549
|
const out = [];
|
|
@@ -1671,11 +1596,10 @@ function shapeDistance(a, b) {
|
|
|
1671
1596
|
return diff + (a.length - i) + (b.length - j);
|
|
1672
1597
|
}
|
|
1673
1598
|
/**
|
|
1674
|
-
* Median (rounded) fixed-size bone dims across the modal cells, written onto the
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* uncommon and guarded, not corrected here.
|
|
1599
|
+
* Median (rounded) fixed-size bone dims across the modal cells, written onto the template's bones.
|
|
1600
|
+
* Bones are paired by pre-order INDEX across cells; a kind guard skips mismatches, so a same-shape
|
|
1601
|
+
* cell whose DOM order differs (flex `order`, `row-reverse`) just drops out of the median rather
|
|
1602
|
+
* than contaminating it — uncommon and guarded, not corrected here.
|
|
1679
1603
|
*/
|
|
1680
1604
|
function applyMedianDims(template, cells) {
|
|
1681
1605
|
const lists = cells.map(fixedSizeLeaves);
|
|
@@ -1707,16 +1631,16 @@ function applyMedianDims(template, cells) {
|
|
|
1707
1631
|
}
|
|
1708
1632
|
}
|
|
1709
1633
|
/**
|
|
1710
|
-
* A cell's fixed-size bones (text-line / box / media) in EFFECTIVE pre-order;
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1634
|
+
* A cell's fixed-size bones (text-line / box / media) in EFFECTIVE pre-order; text-block reflows,
|
|
1635
|
+
* so it's excluded. Reads through `flattened` (a passthrough wrapper is a bone-free container
|
|
1636
|
+
* anyway, so this matches `cellShape`'s ordering and the structure `emit` projects — ADR 0022 §3,
|
|
1637
|
+
* mirroring `effectiveKids`).
|
|
1714
1638
|
*/
|
|
1715
1639
|
function fixedSizeLeaves(node) {
|
|
1716
1640
|
const out = [];
|
|
1717
1641
|
const visit = (n) => {
|
|
1718
1642
|
const kind = effectiveKind(n);
|
|
1719
|
-
if (!n.flattened && (kind === "text" || kind === "box" || kind === "media") && effectiveDecls(n).size) out.push(n);
|
|
1643
|
+
if (!n.flattened && (kind === "text-line" || kind === "box" || kind === "media") && effectiveDecls(n).size) out.push(n);
|
|
1720
1644
|
for (const kid of effectiveKids(n)) visit(kid);
|
|
1721
1645
|
};
|
|
1722
1646
|
visit(node);
|
|
@@ -5763,13 +5687,13 @@ function endsWithLineStartProhibitedText(text) {
|
|
|
5763
5687
|
const last = getLastCodePoint(text);
|
|
5764
5688
|
return last !== null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));
|
|
5765
5689
|
}
|
|
5766
|
-
const keepAllGlueChars = new Set([
|
|
5690
|
+
const keepAllGlueChars = /* @__PURE__ */ new Set([
|
|
5767
5691
|
"\xA0",
|
|
5768
5692
|
" ",
|
|
5769
5693
|
"",
|
|
5770
5694
|
""
|
|
5771
5695
|
]);
|
|
5772
|
-
const keepAllDashBreakChars = new Set([
|
|
5696
|
+
const keepAllDashBreakChars = /* @__PURE__ */ new Set([
|
|
5773
5697
|
"-",
|
|
5774
5698
|
"‐",
|
|
5775
5699
|
"–",
|
|
@@ -5790,7 +5714,7 @@ function canContinueKeepAllTextRun(previousText, breakAfterPunctuation) {
|
|
|
5790
5714
|
if (endsWithKeepAllDashBreakText(previousText)) return false;
|
|
5791
5715
|
return true;
|
|
5792
5716
|
}
|
|
5793
|
-
const kinsokuStart = new Set([
|
|
5717
|
+
const kinsokuStart = /* @__PURE__ */ new Set([
|
|
5794
5718
|
",",
|
|
5795
5719
|
".",
|
|
5796
5720
|
"!",
|
|
@@ -5818,7 +5742,7 @@ const kinsokuStart = new Set([
|
|
|
5818
5742
|
"ヽ",
|
|
5819
5743
|
"ヾ"
|
|
5820
5744
|
]);
|
|
5821
|
-
const kinsokuEnd = new Set([
|
|
5745
|
+
const kinsokuEnd = /* @__PURE__ */ new Set([
|
|
5822
5746
|
"\"",
|
|
5823
5747
|
"(",
|
|
5824
5748
|
"[",
|
|
@@ -5843,8 +5767,8 @@ const kinsokuEnd = new Set([
|
|
|
5843
5767
|
"〘",
|
|
5844
5768
|
"〚"
|
|
5845
5769
|
]);
|
|
5846
|
-
const forwardStickyGlue = new Set(["'", "’"]);
|
|
5847
|
-
const leftStickyPunctuation = new Set([
|
|
5770
|
+
const forwardStickyGlue = /* @__PURE__ */ new Set(["'", "’"]);
|
|
5771
|
+
const leftStickyPunctuation = /* @__PURE__ */ new Set([
|
|
5848
5772
|
".",
|
|
5849
5773
|
",",
|
|
5850
5774
|
"!",
|
|
@@ -5872,14 +5796,14 @@ const leftStickyPunctuation = new Set([
|
|
|
5872
5796
|
"›",
|
|
5873
5797
|
"…"
|
|
5874
5798
|
]);
|
|
5875
|
-
const arabicNoSpaceTrailingPunctuation = new Set([
|
|
5799
|
+
const arabicNoSpaceTrailingPunctuation = /* @__PURE__ */ new Set([
|
|
5876
5800
|
":",
|
|
5877
5801
|
".",
|
|
5878
5802
|
"،",
|
|
5879
5803
|
"؛"
|
|
5880
5804
|
]);
|
|
5881
|
-
const myanmarMedialGlue = new Set(["၏"]);
|
|
5882
|
-
const closingQuoteChars = new Set([
|
|
5805
|
+
const myanmarMedialGlue = /* @__PURE__ */ new Set(["၏"]);
|
|
5806
|
+
const closingQuoteChars = /* @__PURE__ */ new Set([
|
|
5883
5807
|
"”",
|
|
5884
5808
|
"’",
|
|
5885
5809
|
"»",
|
|
@@ -6255,7 +6179,7 @@ function mergeUrlQueryRuns(segmentation) {
|
|
|
6255
6179
|
starts
|
|
6256
6180
|
};
|
|
6257
6181
|
}
|
|
6258
|
-
const numericJoinerChars = new Set([
|
|
6182
|
+
const numericJoinerChars = /* @__PURE__ */ new Set([
|
|
6259
6183
|
":",
|
|
6260
6184
|
"-",
|
|
6261
6185
|
"/",
|
|
@@ -6268,7 +6192,7 @@ const numericJoinerChars = new Set([
|
|
|
6268
6192
|
]);
|
|
6269
6193
|
const wordInternalSymbolRe = /[\p{P}\p{S}\p{Co}]/u;
|
|
6270
6194
|
const emojiPresentationRe$1 = /\p{Emoji_Presentation}/u;
|
|
6271
|
-
const noSpaceWordBreakAfterChars = new Set([
|
|
6195
|
+
const noSpaceWordBreakAfterChars = /* @__PURE__ */ new Set([
|
|
6272
6196
|
"?",
|
|
6273
6197
|
"֊",
|
|
6274
6198
|
"-",
|
|
@@ -8037,7 +7961,7 @@ function textBar(run, foldedMedia, state, depth) {
|
|
|
8037
7961
|
const node = {
|
|
8038
7962
|
id: state.nextId++,
|
|
8039
7963
|
el: null,
|
|
8040
|
-
kind: "text",
|
|
7964
|
+
kind: "text-line",
|
|
8041
7965
|
kids: [],
|
|
8042
7966
|
depth,
|
|
8043
7967
|
decls: { fallback: ["display: inline-block", "vertical-align: middle"] },
|
|
@@ -8065,13 +7989,12 @@ function textBar(run, foldedMedia, state, depth) {
|
|
|
8065
7989
|
return node;
|
|
8066
7990
|
}
|
|
8067
7991
|
/**
|
|
8068
|
-
* CSS generic font-family keywords — none names a concrete face, so pretext's
|
|
8069
|
-
*
|
|
8070
|
-
*
|
|
8071
|
-
*
|
|
8072
|
-
* falls back. The list is the CSS generic set plus the `ui-*` system aliases.
|
|
7992
|
+
* CSS generic font-family keywords — none names a concrete face, so pretext's canvas `measureText`
|
|
7993
|
+
* can't agree with what the browser actually resolved (pretext's own README flags `system-ui` as
|
|
7994
|
+
* unsafe on macOS). A run whose first family token is one of these has no capture-matched named
|
|
7995
|
+
* font, so the rep falls back. The list is the CSS generic set plus the `ui-*` system aliases.
|
|
8073
7996
|
*/
|
|
8074
|
-
const GENERIC_FONT_FAMILIES = new Set([
|
|
7997
|
+
const GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
|
|
8075
7998
|
"serif",
|
|
8076
7999
|
"sans-serif",
|
|
8077
8000
|
"monospace",
|
|
@@ -8092,10 +8015,10 @@ const GENERIC_FONT_FAMILIES = new Set([
|
|
|
8092
8015
|
"revert-layer"
|
|
8093
8016
|
]);
|
|
8094
8017
|
/**
|
|
8095
|
-
* The first font-family token, unquoted and lowercased, or null when the list is
|
|
8096
|
-
*
|
|
8097
|
-
*
|
|
8098
|
-
*
|
|
8018
|
+
* The first font-family token, unquoted and lowercased, or null when the list is empty.
|
|
8019
|
+
* `getComputedStyle().fontFamily` serializes the resolved list (the first entry is the face the
|
|
8020
|
+
* browser used for this run), comma-separated with quoted multi-word names — `"Helvetica Neue",
|
|
8021
|
+
* Arial, sans-serif` → `helvetica neue`.
|
|
8099
8022
|
*/
|
|
8100
8023
|
function firstFontFamily(fontFamily) {
|
|
8101
8024
|
const first = splitTopLevelComma(fontFamily)[0];
|
|
@@ -8122,11 +8045,11 @@ function splitTopLevelComma(value) {
|
|
|
8122
8045
|
return out;
|
|
8123
8046
|
}
|
|
8124
8047
|
/**
|
|
8125
|
-
* Build the canvas `font` shorthand pretext wants — `<style> <weight> <size>px
|
|
8126
|
-
*
|
|
8127
|
-
*
|
|
8128
|
-
*
|
|
8129
|
-
*
|
|
8048
|
+
* Build the canvas `font` shorthand pretext wants — `<style> <weight> <size>px <family-list>` —
|
|
8049
|
+
* from the run's resolved computed style, mirroring CSS exactly (the families pass through
|
|
8050
|
+
* verbatim, quotes and all, so the canvas resolves the SAME face). Returns null when the size is
|
|
8051
|
+
* not a usable px length. Order follows the canvas/CSS `font` shorthand: style, then weight, then
|
|
8052
|
+
* size, then family.
|
|
8130
8053
|
*/
|
|
8131
8054
|
function canvasFontShorthand(cs) {
|
|
8132
8055
|
const sizePx = pxValue(cs.fontSize);
|
|
@@ -8139,14 +8062,12 @@ function canvasFontShorthand(cs) {
|
|
|
8139
8062
|
return parts.join(" ");
|
|
8140
8063
|
}
|
|
8141
8064
|
/**
|
|
8142
|
-
* Does this environment have real canvas text metrics (the feature gate for the
|
|
8143
|
-
*
|
|
8144
|
-
*
|
|
8145
|
-
*
|
|
8146
|
-
*
|
|
8147
|
-
*
|
|
8148
|
-
* the rep is skipped entirely and the run takes the existing pill-bar fallback,
|
|
8149
|
-
* keeping the no-canvas path byte-identical to before this change.
|
|
8065
|
+
* Does this environment have real canvas text metrics (the feature gate for the whole rep)? pretext
|
|
8066
|
+
* relies on Canvas 2D `measureText` AND `Intl.Segmenter` (its documented runtime requirement); a
|
|
8067
|
+
* layout-less env (happy-dom in tests, older runtimes) has neither, so `prepare()`/`layout()`
|
|
8068
|
+
* cannot compute there. Probe once per capture: a 2d context whose `measureText` returns a positive
|
|
8069
|
+
* width for a known glyph, plus a present `Intl.Segmenter`. When this is false the rep is skipped
|
|
8070
|
+
* entirely and the run takes the pill-bar fallback.
|
|
8150
8071
|
*/
|
|
8151
8072
|
function hasUsableTextMetrics(win) {
|
|
8152
8073
|
if (!("Intl" in win) || !("Segmenter" in Intl)) return false;
|
|
@@ -8160,11 +8081,10 @@ function hasUsableTextMetrics(win) {
|
|
|
8160
8081
|
}
|
|
8161
8082
|
}
|
|
8162
8083
|
/**
|
|
8163
|
-
* The nearest block-level ancestor that establishes the run's wrapping width,
|
|
8164
|
-
*
|
|
8165
|
-
*
|
|
8166
|
-
*
|
|
8167
|
-
* slot). Returns the parent itself when it is already block-level.
|
|
8084
|
+
* The nearest block-level ancestor that establishes the run's wrapping width, starting from the
|
|
8085
|
+
* text's parent. Inline wrappers (`<b>`, `<span>`) don't bound wrapping; the first non-inline
|
|
8086
|
+
* ancestor's content box is the available width the injected `container-type: inline-size` wrapper
|
|
8087
|
+
* will assume (it fills its slot). Returns the parent itself when it is already block-level.
|
|
8168
8088
|
*/
|
|
8169
8089
|
function wrappingBlock(parent, styles) {
|
|
8170
8090
|
for (let host = parent; host; host = host.parentElement) if (!isInlineish(styles.get(host).display)) return host;
|
|
@@ -8176,12 +8096,10 @@ function innerContentWidth(el, styles) {
|
|
|
8176
8096
|
return el.getBoundingClientRect().width - pad(cs.paddingLeft) - pad(cs.paddingRight) - pad(cs.borderLeftWidth) - pad(cs.borderRightWidth);
|
|
8177
8097
|
}
|
|
8178
8098
|
/**
|
|
8179
|
-
* Count the run's rendered lines from its `Range` rects: `getClientRects()`
|
|
8180
|
-
*
|
|
8181
|
-
*
|
|
8182
|
-
*
|
|
8183
|
-
* predicted line count at the capture width disagrees with what the browser
|
|
8184
|
-
* actually rendered, the rep would be wrong for this run and we fall back.
|
|
8099
|
+
* Count the run's rendered lines from its `Range` rects: `getClientRects()` yields one rect per
|
|
8100
|
+
* line fragment, so distinct `top` values (collapsed within 1px) are the line count. The
|
|
8101
|
+
* capture-time AGREEMENT check: if pretext's predicted line count at the capture width disagrees
|
|
8102
|
+
* with what the browser actually rendered, the rep would be wrong for this run, so it falls back.
|
|
8185
8103
|
*/
|
|
8186
8104
|
function renderedLineCount(range) {
|
|
8187
8105
|
const rects = Array.from(range.getClientRects());
|
|
@@ -8193,25 +8111,22 @@ function renderedLineCount(range) {
|
|
|
8193
8111
|
return Math.max(1, tops.length);
|
|
8194
8112
|
}
|
|
8195
8113
|
/**
|
|
8196
|
-
* The active `-webkit-line-clamp` line cap governing the run, or null. The clamp
|
|
8197
|
-
*
|
|
8198
|
-
*
|
|
8199
|
-
*
|
|
8200
|
-
*
|
|
8201
|
-
*
|
|
8202
|
-
*
|
|
8203
|
-
*
|
|
8204
|
-
*
|
|
8205
|
-
*
|
|
8206
|
-
* vertical `-webkit-box-orient` (the idiom). A clamp value without the vertical
|
|
8207
|
-
* orientation has no effect and is ignored, matching the browser.
|
|
8114
|
+
* The active `-webkit-line-clamp` line cap governing the run, or null. The clamp idiom (`display:
|
|
8115
|
+
* -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: N; overflow: hidden`) clips text
|
|
8116
|
+
* to N lines, so a clamped run NEVER shows more than N no matter how narrow the container gets.
|
|
8117
|
+
* pretext measures the UNCLAMPED wrap, and a `Range`'s rects ignore the clip too
|
|
8118
|
+
* (`renderedLineCount`), so the agreement check passes on the full count while the run actually
|
|
8119
|
+
* renders at most N — without this the rep would emit bands TALLER than the clamp ever shows.
|
|
8120
|
+
* Capping the band sweep at N makes a clamped block exact and bounds its stack to ≤N. Walks
|
|
8121
|
+
* ancestors (the clamp may sit a few wrappers above the text node) and returns the nearest ACTIVE
|
|
8122
|
+
* clamp — a positive integer `-webkit-line-clamp` on a vertical `-webkit-box-orient` (the idiom). A
|
|
8123
|
+
* clamp value without the vertical orientation has no effect and is ignored, matching the browser.
|
|
8208
8124
|
*
|
|
8209
|
-
* Detection keys on the line-clamp + box-orient PAIR, deliberately NOT on
|
|
8210
|
-
*
|
|
8211
|
-
*
|
|
8212
|
-
*
|
|
8213
|
-
*
|
|
8214
|
-
* the pair is the reliable cross-engine signal.
|
|
8125
|
+
* Detection keys on the line-clamp + box-orient PAIR, deliberately NOT on computed `display`:
|
|
8126
|
+
* Chrome normalizes an active `display: -webkit-box` clamp to `display: flow-root` in computed
|
|
8127
|
+
* style (verified), so a `display === -webkit-box` guard silently never matched in Chrome — the bug
|
|
8128
|
+
* this comment guards against. `-webkit-box-orient` and `-webkit-line-clamp` ARE exposed in
|
|
8129
|
+
* computed style, so the pair is the reliable cross-engine signal.
|
|
8215
8130
|
*/
|
|
8216
8131
|
function activeLineClamp(parent, styles) {
|
|
8217
8132
|
for (let el = parent; el; el = el.parentElement) {
|
|
@@ -8224,24 +8139,21 @@ function activeLineClamp(parent, styles) {
|
|
|
8224
8139
|
return null;
|
|
8225
8140
|
}
|
|
8226
8141
|
/**
|
|
8227
|
-
* The line-count step function across the width range, swept at 1px so each
|
|
8228
|
-
*
|
|
8229
|
-
*
|
|
8230
|
-
*
|
|
8231
|
-
*
|
|
8232
|
-
* `
|
|
8233
|
-
*
|
|
8234
|
-
*
|
|
8235
|
-
* band stack is bounded no matter how many lines the run wraps to at the narrow
|
|
8236
|
-
* end.
|
|
8142
|
+
* The line-count step function across the width range, swept at 1px so each band threshold lands on
|
|
8143
|
+
* the exact width the run changes line count (measured to coincide with the browser's real wrap
|
|
8144
|
+
* points to the pixel). Walks widths ascending and records every transition; the first entry is the
|
|
8145
|
+
* baseline (most lines, narrowest), each later entry a `min-width` step. `maxWidth` caps the sweep
|
|
8146
|
+
* at the largest realistic container width (the run may fit on one line before reaching it).
|
|
8147
|
+
* `lines` is clamped to `maxLines` (the caller passes `MAX_REP_LINES`, or a smaller
|
|
8148
|
+
* `-webkit-line-clamp` cap), so the band stack is bounded no matter how many lines the run wraps to
|
|
8149
|
+
* at the narrow end.
|
|
8237
8150
|
*/
|
|
8238
8151
|
/**
|
|
8239
|
-
* Cap on the line-count bands per run. A run that wraps to more lines than this at
|
|
8240
|
-
*
|
|
8241
|
-
*
|
|
8242
|
-
*
|
|
8243
|
-
*
|
|
8244
|
-
* Tunable.
|
|
8152
|
+
* Cap on the line-count bands per run. A run that wraps to more lines than this at the narrow end
|
|
8153
|
+
* clamps here (a tall block reads as "tall" — a line or two off at an extreme-narrow width is
|
|
8154
|
+
* imperceptible on swap), so the band stack can never explode — an unbounded sweep produced 40–70
|
|
8155
|
+
* bands per run on real content. Kept above realistic mobile paragraph line counts so it does not
|
|
8156
|
+
* clip genuine content. Tunable.
|
|
8245
8157
|
*/
|
|
8246
8158
|
const MAX_REP_LINES = 16;
|
|
8247
8159
|
function lineCountStepFunction(prepared, lh, minWidth, maxWidth, maxLines) {
|
|
@@ -8261,45 +8173,41 @@ function lineCountStepFunction(prepared, lh, minWidth, maxWidth, maxLines) {
|
|
|
8261
8173
|
return bands;
|
|
8262
8174
|
}
|
|
8263
8175
|
/**
|
|
8264
|
-
* The minimum container width to sweep from. The bone never renders narrower
|
|
8265
|
-
*
|
|
8266
|
-
*
|
|
8267
|
-
*
|
|
8176
|
+
* The minimum container width to sweep from. The bone never renders narrower than one line-height
|
|
8177
|
+
* tall, and sweeping below a few px is wasted work; a small floor keeps the sweep bounded for a
|
|
8178
|
+
* pathologically long run. Real containers are far wider, so this only caps the bottom of the band
|
|
8179
|
+
* stack.
|
|
8268
8180
|
*/
|
|
8269
8181
|
const REP_MIN_WIDTH = 16;
|
|
8270
8182
|
/**
|
|
8271
|
-
* Cap on the sweep's upper width: the largest realistic container width. The rep
|
|
8272
|
-
*
|
|
8273
|
-
*
|
|
8274
|
-
*
|
|
8275
|
-
*
|
|
8276
|
-
* single-line width would only emit wasted bands. Tunable; ≈ the largest common
|
|
8277
|
-
* breakpoint.
|
|
8183
|
+
* Cap on the sweep's upper width: the largest realistic container width. The rep runs during the
|
|
8184
|
+
* walk, before per-plate breakpoints are derived, so it cannot read the actual biggest breakpoint —
|
|
8185
|
+
* this fixed bound stands in for it. Beyond it (a text run spanning a full ultra-wide viewport), a
|
|
8186
|
+
* height mismatch on swap is imperceptible against that much space, so sweeping out to the run's
|
|
8187
|
+
* single-line width would only emit wasted bands. Tunable; ≈ the largest common breakpoint.
|
|
8278
8188
|
*/
|
|
8279
8189
|
const REP_MAX_WIDTH = 1920;
|
|
8280
8190
|
/**
|
|
8281
|
-
* Attempt the `@container` line-count rep for a wrapping text run (ADR 0019 §2).
|
|
8282
|
-
*
|
|
8283
|
-
*
|
|
8284
|
-
*
|
|
8285
|
-
*
|
|
8286
|
-
* fallback is always graceful and never a regression:
|
|
8191
|
+
* Attempt the `@container` line-count rep for a wrapping text run (ADR 0019 §2). Returns the
|
|
8192
|
+
* leaf-scoped wrapper `IRNode` (a branch with one full-width text bone child, both pushed to
|
|
8193
|
+
* `state.nodes`) and emits its reflow band rules onto `state`, OR returns null to signal the caller
|
|
8194
|
+
* to keep the existing measured pill bar. Null is returned for everything the rep can't safely
|
|
8195
|
+
* cover, so a fallback is always graceful and never a regression:
|
|
8287
8196
|
*
|
|
8288
|
-
*
|
|
8289
|
-
*
|
|
8290
|
-
*
|
|
8291
|
-
*
|
|
8292
|
-
*
|
|
8293
|
-
*
|
|
8294
|
-
*
|
|
8295
|
-
*
|
|
8296
|
-
*
|
|
8197
|
+
* - No usable canvas text metrics (every test env; older runtimes) — feature gate;
|
|
8198
|
+
* - No resolved parent / no named capture-matched font / no px line-height;
|
|
8199
|
+
* - A single-line or short run (stays the thin pill bar — a fat block misreads);
|
|
8200
|
+
* - An out-of-model wrap mode pretext doesn't model: `text-align: justify`, `hyphens: auto`,
|
|
8201
|
+
* `word-break: break-all`, `white-space` ≠ normal/pre-wrap;
|
|
8202
|
+
* - An unbreakable token wider than the container (the browser overflows it on one line, pretext
|
|
8203
|
+
* wraps it — they diverge);
|
|
8204
|
+
* - Pretext's predicted line count disagreeing with the browser's actual wrap at the capture width (a
|
|
8205
|
+
* self-check that also catches font substitution).
|
|
8297
8206
|
*
|
|
8298
|
-
* On success the wrapper carries `container-type: inline-size` + a plate-local
|
|
8299
|
-
*
|
|
8300
|
-
*
|
|
8301
|
-
*
|
|
8302
|
-
* author container (ADR 0019 §2) is a deferred optimization.
|
|
8207
|
+
* On success the wrapper carries `container-type: inline-size` + a plate-local unique
|
|
8208
|
+
* `container-name`, and the bone gets `@container <name> (min-width: …) { height: calc(N × lh) }`
|
|
8209
|
+
* bands — reflowing by CSS alone, no frozen height, no render JS. v1 ALWAYS injects a fresh named
|
|
8210
|
+
* container; reusing an immediate author container (ADR 0019 §2) is a deferred optimization.
|
|
8303
8211
|
*/
|
|
8304
8212
|
function textLineCountRep(run, range, height, parent, state, depth) {
|
|
8305
8213
|
if (!parent) return null;
|
|
@@ -8395,12 +8303,11 @@ function textLineCountRep(run, range, height, parent, state, depth) {
|
|
|
8395
8303
|
return wrapperNode;
|
|
8396
8304
|
}
|
|
8397
8305
|
/**
|
|
8398
|
-
* A `Range` rect is the tight union of the glyph line boxes, so a wrapped run
|
|
8399
|
-
*
|
|
8400
|
-
*
|
|
8401
|
-
*
|
|
8402
|
-
*
|
|
8403
|
-
* A single line is left untouched — its line box comes from the parent's pinned
|
|
8306
|
+
* A `Range` rect is the tight union of the glyph line boxes, so a wrapped run measures lines ×
|
|
8307
|
+
* glyph-height — short of the lines × line-height the source actually occupied (a 2-line paragraph
|
|
8308
|
+
* rendered ~3px short per line). When the parent's line-height resolves to px and the run is
|
|
8309
|
+
* clearly multi-line, round to a whole number of line boxes so the bone reserves the real block
|
|
8310
|
+
* height. A single line is left untouched — its line box comes from the parent's pinned
|
|
8404
8311
|
* line-height, and the thin glyph bar reads as text rather than a fat block.
|
|
8405
8312
|
*/
|
|
8406
8313
|
function snapToLines(height, parent, styles) {
|
|
@@ -8603,12 +8510,11 @@ function rightmostCompound(selector) {
|
|
|
8603
8510
|
return selector;
|
|
8604
8511
|
}
|
|
8605
8512
|
/**
|
|
8606
|
-
* Index author rules the way a browser's rule hash does: bucket each selector
|
|
8607
|
-
*
|
|
8608
|
-
*
|
|
8609
|
-
*
|
|
8610
|
-
*
|
|
8611
|
-
* fall back to a universal bucket tested against every element.
|
|
8513
|
+
* Index author rules the way a browser's rule hash does: bucket each selector by a token its
|
|
8514
|
+
* matching element must carry (id, a class, or tag), so an element only tests the rules that could
|
|
8515
|
+
* possibly apply to it. CSS-module class names are unique, so this turns an O(rules × nodes) sweep
|
|
8516
|
+
* into a near-linear one. Selectors keyed on nothing concrete (`*`, attribute-only) fall back to a
|
|
8517
|
+
* universal bucket tested against every element.
|
|
8612
8518
|
*/
|
|
8613
8519
|
function indexRules(found, diagnostics) {
|
|
8614
8520
|
const index = {
|
|
@@ -8654,15 +8560,14 @@ function indexRules(found, diagnostics) {
|
|
|
8654
8560
|
return index;
|
|
8655
8561
|
}
|
|
8656
8562
|
/**
|
|
8657
|
-
* Scan every flattened author rule for custom-property declarations and group
|
|
8658
|
-
*
|
|
8659
|
-
*
|
|
8660
|
-
*
|
|
8661
|
-
*
|
|
8662
|
-
*
|
|
8663
|
-
*
|
|
8664
|
-
*
|
|
8665
|
-
* to a captured node or to :root), so the scan stays simple and skips nothing.
|
|
8563
|
+
* Scan every flattened author rule for custom-property declarations and group them by token name. A
|
|
8564
|
+
* token may be defined many times (a `:root` base plus an `@media` override, or per-theme blocks);
|
|
8565
|
+
* each definition keeps its conditions and selector so the carry decision can see whether ANY of
|
|
8566
|
+
* them is conditional and where each anchors. Reuses the SAME flatten `copyRules` already paid for
|
|
8567
|
+
* (`collectStyleRules`), so this adds one extra pass over the rules, no new CSSOM traversal.
|
|
8568
|
+
* Dynamic-pseudo / pseudo-element rules never define carried layout tokens, but they are cheap to
|
|
8569
|
+
* include and harmless (their selectors won't anchor to a captured node or to :root), so the scan
|
|
8570
|
+
* stays simple and skips nothing.
|
|
8666
8571
|
*/
|
|
8667
8572
|
function collectTokenDefs(found) {
|
|
8668
8573
|
const defs = /* @__PURE__ */ new Map();
|
|
@@ -8695,12 +8600,11 @@ function collectTokenDefs(found) {
|
|
|
8695
8600
|
return defs;
|
|
8696
8601
|
}
|
|
8697
8602
|
/**
|
|
8698
|
-
* A selector that puts a custom property in scope for the WHOLE captured subtree
|
|
8699
|
-
*
|
|
8700
|
-
*
|
|
8701
|
-
*
|
|
8702
|
-
*
|
|
8703
|
-
* matching it against the captured elements instead.
|
|
8603
|
+
* A selector that puts a custom property in scope for the WHOLE captured subtree — `:root`, `html`,
|
|
8604
|
+
* or the universal `*` (optionally wrapped in zero-specificity `:where()`/`:is()`, and ignoring a
|
|
8605
|
+
* trailing dynamic state). Such a definition inherits down into every node, so it anchors on the
|
|
8606
|
+
* plate's scoped root (node id 0). Anything more specific is an element-level (re)definition
|
|
8607
|
+
* handled by matching it against the captured elements instead.
|
|
8704
8608
|
*/
|
|
8705
8609
|
const ROOT_SCOPE_SELECTOR = /^(?::root|html|\*)$/;
|
|
8706
8610
|
function isRootScopeSelector(selector) {
|
|
@@ -8712,12 +8616,11 @@ function hasConditionalDef(defs) {
|
|
|
8712
8616
|
return defs.some((def) => def.media.length > 0 || def.container.length > 0);
|
|
8713
8617
|
}
|
|
8714
8618
|
/**
|
|
8715
|
-
* The plate-id a token definition anchors to, or null when it cannot be anchored
|
|
8716
|
-
*
|
|
8717
|
-
*
|
|
8718
|
-
*
|
|
8719
|
-
*
|
|
8720
|
-
* matches NO captured element and is not root-scope is un-anchorable.
|
|
8619
|
+
* The plate-id a token definition anchors to, or null when it cannot be anchored on the captured
|
|
8620
|
+
* tree (an un-captured wrapper). Root-scope selectors (`:root`/`html`/`*`) anchor on the scoped
|
|
8621
|
+
* root (id 0, emitted as `ids: []`); an element-level definition anchors on every captured element
|
|
8622
|
+
* it matches (a token redefined on a card anchors on that card's node). A definition that matches
|
|
8623
|
+
* NO captured element and is not root-scope is un-anchorable.
|
|
8721
8624
|
*/
|
|
8722
8625
|
function anchorIdsFor(def, elNodes) {
|
|
8723
8626
|
if (isRootScopeSelector(def.selector)) return [0];
|
|
@@ -8726,13 +8629,12 @@ function anchorIdsFor(def, elNodes) {
|
|
|
8726
8629
|
return ids.length > 0 ? ids : null;
|
|
8727
8630
|
}
|
|
8728
8631
|
/**
|
|
8729
|
-
* The winning UNCONDITIONAL definition value of a token — its constant value,
|
|
8730
|
-
*
|
|
8731
|
-
*
|
|
8732
|
-
*
|
|
8733
|
-
*
|
|
8734
|
-
*
|
|
8735
|
-
* style cannot resolve an inherited custom property.
|
|
8632
|
+
* The winning UNCONDITIONAL definition value of a token — its constant value, picked by the cascade
|
|
8633
|
+
* among definitions that carry no `@media`/`@container` condition: highest specificity, then latest
|
|
8634
|
+
* source order. Null when the token has no unconditional definition (it only exists inside a query
|
|
8635
|
+
* — purely conditional). This is what a constant token freezes to, sourced from the author CSSOM
|
|
8636
|
+
* rather than computed style, so the freeze is correct even where computed style cannot resolve an
|
|
8637
|
+
* inherited custom property.
|
|
8736
8638
|
*/
|
|
8737
8639
|
function constantValueOf(defs) {
|
|
8738
8640
|
let best = null;
|
|
@@ -8927,39 +8829,39 @@ function attachCarried(nodeById, id, rule) {
|
|
|
8927
8829
|
(node.decls.carried ??= []).push(rule);
|
|
8928
8830
|
}
|
|
8929
8831
|
//#endregion
|
|
8930
|
-
//#region src/
|
|
8832
|
+
//#region src/measure.ts
|
|
8931
8833
|
/**
|
|
8932
|
-
* Capture-only DOM
|
|
8933
|
-
*
|
|
8934
|
-
*
|
|
8935
|
-
* `captureWalker` is the 10% escape hatch.
|
|
8834
|
+
* Capture-only DOM Hints (ADR 0018), read DURING the walk to shape the `PlateNode` tree and NEVER
|
|
8835
|
+
* written into the tree, the Plate, or the persisted JSON. They are the declarative 90% of capture
|
|
8836
|
+
* customization; the programmable `captureWalker` is the 10% escape hatch.
|
|
8936
8837
|
*
|
|
8937
8838
|
* - `data-xr-ignore` drops the element AND its subtree (it contributes no bone).
|
|
8938
|
-
* - `data-xr-bone-kind="text|media|box"` collapses the element's whole subtree
|
|
8839
|
+
* - `data-xr-bone-kind="text-line|text-block|media|box|hidden"` collapses the element's whole subtree
|
|
8939
8840
|
* into ONE Bone leaf of the given kind, sized from the element's own box.
|
|
8940
8841
|
*
|
|
8941
|
-
* Both are subordinate to the stitch guard (ADR 0006): a nested `<Skeleton>`
|
|
8942
|
-
*
|
|
8943
|
-
*
|
|
8842
|
+
* Both are subordinate to the stitch guard (ADR 0006): a nested `<Skeleton>` boundary always
|
|
8843
|
+
* becomes a stitch first, so neither hint can make a boundary leak into the parent plate. See
|
|
8844
|
+
* `walk`.
|
|
8944
8845
|
*/
|
|
8945
8846
|
const XR_IGNORE_ATTR = "data-xr-ignore";
|
|
8946
8847
|
const XR_BONE_KIND_ATTR = "data-xr-bone-kind";
|
|
8947
8848
|
/** The `LeafKind` values an author may legally name in `data-xr-bone-kind`. */
|
|
8948
8849
|
const LEAF_KINDS = [
|
|
8949
|
-
"text",
|
|
8850
|
+
"text-line",
|
|
8950
8851
|
"text-block",
|
|
8951
8852
|
"media",
|
|
8952
|
-
"box"
|
|
8853
|
+
"box",
|
|
8854
|
+
"hidden"
|
|
8953
8855
|
];
|
|
8954
8856
|
/** Narrow an arbitrary `data-xr-bone-kind` string to a `LeafKind`, or null if unknown. */
|
|
8955
8857
|
function asLeafKind(value) {
|
|
8956
8858
|
return LEAF_KINDS.find((kind) => kind === value) ?? null;
|
|
8957
8859
|
}
|
|
8958
8860
|
/**
|
|
8959
|
-
* Typed identity wrapper for authoring a capture walker (ADR 0018) — the
|
|
8960
|
-
*
|
|
8961
|
-
*
|
|
8962
|
-
*
|
|
8861
|
+
* Typed identity wrapper for authoring a capture walker (ADR 0018) — the `defineConfig` pattern. It
|
|
8862
|
+
* returns its argument unchanged at runtime; its only job is to infer and check the
|
|
8863
|
+
* `XrayCaptureWalker` shape at the call site so an author gets completion and a typo in a hook name
|
|
8864
|
+
* is caught.
|
|
8963
8865
|
*/
|
|
8964
8866
|
function defineXrayCaptureWalker(walker) {
|
|
8965
8867
|
return walker;
|
|
@@ -8970,7 +8872,7 @@ function isElementNode(node) {
|
|
|
8970
8872
|
function isTextNode(node) {
|
|
8971
8873
|
return node.nodeType === 3;
|
|
8972
8874
|
}
|
|
8973
|
-
const SKIP_TAGS = new Set([
|
|
8875
|
+
const SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
8974
8876
|
"SCRIPT",
|
|
8975
8877
|
"STYLE",
|
|
8976
8878
|
"LINK",
|
|
@@ -8979,7 +8881,7 @@ const SKIP_TAGS = new Set([
|
|
|
8979
8881
|
"NOSCRIPT",
|
|
8980
8882
|
"TITLE"
|
|
8981
8883
|
]);
|
|
8982
|
-
const MEDIA_TAGS = new Set([
|
|
8884
|
+
const MEDIA_TAGS = /* @__PURE__ */ new Set([
|
|
8983
8885
|
"IMG",
|
|
8984
8886
|
"SVG",
|
|
8985
8887
|
"VIDEO",
|
|
@@ -9002,10 +8904,10 @@ const RUN_BREAKERS = [
|
|
|
9002
8904
|
`[${ROOT_ATTR}]`
|
|
9003
8905
|
].join(",");
|
|
9004
8906
|
/**
|
|
9005
|
-
* A child element whose glyph ink belongs to its parent block's run rather than
|
|
9006
|
-
*
|
|
9007
|
-
*
|
|
9008
|
-
*
|
|
8907
|
+
* A child element whose glyph ink belongs to its parent block's run rather than to a Bone of its
|
|
8908
|
+
* own: a pure `display: inline` text wrapper (`<span>`, `<a>`, `<em>`) with no replaced-media or
|
|
8909
|
+
* nested-Skeleton descendant. Inline-block and friends are atomic boxes, not text flow, so they are
|
|
8910
|
+
* NOT folded.
|
|
9009
8911
|
*/
|
|
9010
8912
|
function isInlineTextElement(el, cs, styles) {
|
|
9011
8913
|
if (cs.display !== "inline") return false;
|
|
@@ -9018,7 +8920,10 @@ function isInlineTextElement(el, cs, styles) {
|
|
|
9018
8920
|
}
|
|
9019
8921
|
return true;
|
|
9020
8922
|
}
|
|
9021
|
-
/**
|
|
8923
|
+
/**
|
|
8924
|
+
* Collect the rendered text nodes under an inline text element into a run, skipping non-rendered
|
|
8925
|
+
* tags.
|
|
8926
|
+
*/
|
|
9022
8927
|
function collectInlineText(el, into) {
|
|
9023
8928
|
for (const child of Array.from(el.childNodes)) if (isTextNode(child)) into.push(child);
|
|
9024
8929
|
else if (isElementNode(child) && !SKIP_TAGS.has(child.tagName.toUpperCase())) collectInlineText(child, into);
|
|
@@ -9035,9 +8940,9 @@ function runTextRect(run) {
|
|
|
9035
8940
|
return r.getBoundingClientRect();
|
|
9036
8941
|
}
|
|
9037
8942
|
/**
|
|
9038
|
-
* The rect of the text a leading glyph-media would abut: the immediately
|
|
9039
|
-
*
|
|
9040
|
-
*
|
|
8943
|
+
* The rect of the text a leading glyph-media would abut: the immediately following text node or
|
|
8944
|
+
* inline text element (e.g. the "4.9" after a "★" icon). Replaced media and absent/blank text yield
|
|
8945
|
+
* null — a glyph needs text to hug.
|
|
9041
8946
|
*/
|
|
9042
8947
|
function nextTextRect(nodes, i, styles) {
|
|
9043
8948
|
const next = nodes[i + 1];
|
|
@@ -9054,15 +8959,13 @@ function nextTextRect(nodes, i, styles) {
|
|
|
9054
8959
|
return null;
|
|
9055
8960
|
}
|
|
9056
8961
|
/**
|
|
9057
|
-
* The pure GEOMETRY of the glyph-fold (ADR 0021 §2): does a media rect read as a
|
|
9058
|
-
*
|
|
9059
|
-
*
|
|
9060
|
-
*
|
|
9061
|
-
*
|
|
9062
|
-
*
|
|
9063
|
-
*
|
|
9064
|
-
* `foldGlyphRows`). `adj` is the text it abuts. This size + spacing test is
|
|
9065
|
-
* Salience's surviving role.
|
|
8962
|
+
* The pure GEOMETRY of the glyph-fold (ADR 0021 §2): does a media rect read as a GLYPH hugging the
|
|
8963
|
+
* text `adj` on the same ROW — emoji-sized (≤ 1.5× line-height), sharing the line (≥ 50% vertical
|
|
8964
|
+
* overlap), and abutting within ~a space (gap ≤ 0.6× line-height)? A bigger icon, or one set off by
|
|
8965
|
+
* a gutter (a section-heading icon), fails and stays its own Bone. DISPLAY-AGNOSTIC on purpose: the
|
|
8966
|
+
* caller decides whether to also require inline-level (`foldsAsGlyph`, the in-flow path) or rely on
|
|
8967
|
+
* geometry alone (the flex/grid "row anything" post-pass, `foldGlyphRows`). `adj` is the text it
|
|
8968
|
+
* abuts. This size + spacing test is Salience's surviving role.
|
|
9066
8969
|
*/
|
|
9067
8970
|
function glyphGeometry(mediaRect, adj, lineHeight) {
|
|
9068
8971
|
if (lineHeight <= 0 || !adj) return false;
|
|
@@ -9074,27 +8977,23 @@ function foldsAsGlyph(mediaCs, mediaRect, adj, lineHeight) {
|
|
|
9074
8977
|
return isInlineish(mediaCs.display) && glyphGeometry(mediaRect, adj, lineHeight);
|
|
9075
8978
|
}
|
|
9076
8979
|
/**
|
|
9077
|
-
* APPLY-WALKER — the programmable capture walker as a PASS over the measured IR
|
|
9078
|
-
*
|
|
9079
|
-
*
|
|
9080
|
-
*
|
|
9081
|
-
*
|
|
9082
|
-
*
|
|
9083
|
-
*
|
|
9084
|
-
*
|
|
9085
|
-
*
|
|
9086
|
-
*
|
|
9087
|
-
*
|
|
9088
|
-
* - `keep` (or no `element` hook) → recurse into the kid's kids (defer to the
|
|
9089
|
-
* default classifier).
|
|
8980
|
+
* APPLY-WALKER — the programmable capture walker as a PASS over the measured IR (ADR 0018; ADR 0022
|
|
8981
|
+
* §5). The walker does not run during the DOM walk (the full subtree is always measured); each
|
|
8982
|
+
* element is offered TOP-DOWN over the built IR and the decision is LOWERED here by setting the
|
|
8983
|
+
* same `markKind`/`dropped` fields the `data-xr-*` hint path sets, so `markInk` (which runs next,
|
|
8984
|
+
* in `build`) realizes them uniformly:
|
|
8985
|
+
*
|
|
8986
|
+
* - `ignore` → `dropped: { reason: 'ignored' }`; the node + subtree lower (NOT recursed — the descent
|
|
8987
|
+
* stops).
|
|
8988
|
+
* - `bone` → `markKind = kind ?? inferLeafKind(el)`; `markInk`'s re-stamp lowers it to ONE childless
|
|
8989
|
+
* bone of that kind (NOT recursed).
|
|
8990
|
+
* - `keep` (or no `element` hook) → recurse into the kid's kids (defer to the default classifier).
|
|
9090
8991
|
*
|
|
9091
|
-
* Iterates a node's KIDS rather than visiting the node itself, so the synthetic
|
|
9092
|
-
*
|
|
9093
|
-
*
|
|
9094
|
-
*
|
|
9095
|
-
*
|
|
9096
|
-
* returning before descending). Pure on `el` — the `ctx` helpers return a
|
|
9097
|
-
* decision; this pass is the only mutator.
|
|
8992
|
+
* Iterates a node's KIDS rather than visiting the node itself, so the synthetic root / synthetic
|
|
8993
|
+
* text bars (`el === null`) are never offered — but their kids are still visited. A stitch (`kind:
|
|
8994
|
+
* 'stitch'`, no `el`) is skipped (opaque — ADR 0006: a walker can never reach a nested
|
|
8995
|
+
* `<Skeleton>`), and a kid the walker ignores/bones is not recursed. Pure on `el` — the `ctx`
|
|
8996
|
+
* helpers return a decision; this pass is the only mutator.
|
|
9098
8997
|
*/
|
|
9099
8998
|
function applyWalker(node, walker) {
|
|
9100
8999
|
if (!walker.element) return;
|
|
@@ -9132,13 +9031,12 @@ function parseRgb(value) {
|
|
|
9132
9031
|
}
|
|
9133
9032
|
const rgbCache = /* @__PURE__ */ new Map();
|
|
9134
9033
|
/**
|
|
9135
|
-
* Resolve any computed CSS colour to sRGB bytes. `rgb()/rgba()` parse directly
|
|
9136
|
-
*
|
|
9137
|
-
*
|
|
9138
|
-
*
|
|
9139
|
-
*
|
|
9140
|
-
*
|
|
9141
|
-
* Cached by string: the same token recurs across every node of a themed card.
|
|
9034
|
+
* Resolve any computed CSS colour to sRGB bytes. `rgb()/rgba()` parse directly (the cheap path, and
|
|
9035
|
+
* the only one a layout-less test env needs); everything else — `oklch()`, `color()`, `hsl()`,
|
|
9036
|
+
* named — is painted onto a 1×1 canvas and read back, since browsers serialise a computed colour in
|
|
9037
|
+
* the space it was authored in (Chrome returns `oklch(...)` verbatim), which a regex can't read.
|
|
9038
|
+
* Without this an oklch-filled control reads as no fill and never becomes a box. Cached by string:
|
|
9039
|
+
* the same token recurs across every node of a themed card.
|
|
9142
9040
|
*/
|
|
9143
9041
|
function toRgb(value, win) {
|
|
9144
9042
|
const direct = parseRgb(value);
|
|
@@ -9177,13 +9075,12 @@ function hasVisibleBackground(cs, win) {
|
|
|
9177
9075
|
/** Fill that visibly contrasts with the nearest painted ancestor — meaning ~24 per channel. */
|
|
9178
9076
|
const FILL_CONTRAST = 72;
|
|
9179
9077
|
/**
|
|
9180
|
-
* The nearest ancestor that actually paints a background, resolved to sRGB, or
|
|
9181
|
-
*
|
|
9182
|
-
* per
|
|
9183
|
-
*
|
|
9184
|
-
*
|
|
9185
|
-
*
|
|
9186
|
-
* salience.ts scorer takes `behind` as an input rather than climbing itself).
|
|
9078
|
+
* The nearest ancestor that actually paints a background, resolved to sRGB, or null when nothing up
|
|
9079
|
+
* the chain paints. Ancestor styles come from the per-capture memo (`StyleCache`): a deep card's
|
|
9080
|
+
* parents are climbed once per check but resolved once per capture. This is the `behind` colour
|
|
9081
|
+
* both the `hasContrastingFill` boolean and the Salience contrast axis score a node's own
|
|
9082
|
+
* fill/border against — extracted so the two read the SAME measured ground (the salience.ts scorer
|
|
9083
|
+
* takes `behind` as an input rather than climbing itself).
|
|
9187
9084
|
*/
|
|
9188
9085
|
function nearestPaintedBackground(el, win, styles) {
|
|
9189
9086
|
for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
|
@@ -9193,17 +9090,14 @@ function nearestPaintedBackground(el, win, styles) {
|
|
|
9193
9090
|
return null;
|
|
9194
9091
|
}
|
|
9195
9092
|
/**
|
|
9196
|
-
* Does this element paint a fill that stands out from what's behind it? A
|
|
9197
|
-
*
|
|
9198
|
-
*
|
|
9199
|
-
* (so a white button on a white card doesn't, but a blue one does).
|
|
9093
|
+
* Does this element paint a fill that stands out from what's behind it? A background image always
|
|
9094
|
+
* counts; a background color counts only when it's mostly opaque and clearly differs from the
|
|
9095
|
+
* nearest ancestor's background (so a white button on a white card doesn't, but a blue one does).
|
|
9200
9096
|
*
|
|
9201
|
-
* This is the fill-ONLY signal Absorb
|
|
9202
|
-
*
|
|
9203
|
-
*
|
|
9204
|
-
*
|
|
9205
|
-
* top to catch a contrasting BORDER or BOX-SHADOW surface (ADR 0021 §3 — "the
|
|
9206
|
-
* existing `hasContrastingFill` branch, generalized").
|
|
9097
|
+
* This is the fill-ONLY signal Absorb keys on. Measure runs it once and snapshots the result onto
|
|
9098
|
+
* `IRNode.paint.contrastingFill`; the pure `markInk`-side `surfaceAbsorbs` reads that snapshot for
|
|
9099
|
+
* the fill case and ADDS the Salience contrast axis on top to catch a contrasting BORDER or
|
|
9100
|
+
* BOX-SHADOW surface (ADR 0021 §3 — the `hasContrastingFill` branch, generalized).
|
|
9207
9101
|
*/
|
|
9208
9102
|
function hasContrastingFill(el, cs, win, styles) {
|
|
9209
9103
|
if (hasPaintedImage(cs)) return true;
|
|
@@ -9218,12 +9112,11 @@ function hasContrastingFill(el, cs, win, styles) {
|
|
|
9218
9112
|
return Math.abs(bg.r - base.r) + Math.abs(bg.g - base.g) + Math.abs(bg.b - base.b) > FILL_CONTRAST;
|
|
9219
9113
|
}
|
|
9220
9114
|
/**
|
|
9221
|
-
* The base/context font-size in px the Salience type-scale axis is measured
|
|
9222
|
-
*
|
|
9223
|
-
*
|
|
9224
|
-
*
|
|
9225
|
-
*
|
|
9226
|
-
* lookup free, so it is read on demand at each score site.
|
|
9115
|
+
* The base/context font-size in px the Salience type-scale axis is measured relative to: the
|
|
9116
|
+
* inherited font-size of the capture subtree's context (the parent of the first real element),
|
|
9117
|
+
* defaulting to the CSS 16px when there is no context or it does not resolve to px. Computed once
|
|
9118
|
+
* per capture would be ideal, but the contexts the walk touches are few and the StyleCache makes
|
|
9119
|
+
* the parent lookup free, so it is read on demand at each score site.
|
|
9227
9120
|
*/
|
|
9228
9121
|
function contextBaseFontSizePx(el, styles) {
|
|
9229
9122
|
const parent = el.parentElement;
|
|
@@ -9232,13 +9125,12 @@ function contextBaseFontSizePx(el, styles) {
|
|
|
9232
9125
|
return sizePx !== null && sizePx > 0 ? sizePx : 16;
|
|
9233
9126
|
}
|
|
9234
9127
|
/**
|
|
9235
|
-
* Score one element's visual relevance by handing salience.ts the three inputs
|
|
9236
|
-
*
|
|
9237
|
-
*
|
|
9238
|
-
*
|
|
9239
|
-
*
|
|
9240
|
-
*
|
|
9241
|
-
* rather than dividing by zero. Pure pass-through — the scorer owns the math.
|
|
9128
|
+
* Score one element's visual relevance by handing salience.ts the three inputs the walk already
|
|
9129
|
+
* has: the element's computed style, its rendered box, and the measured context (nearest painted
|
|
9130
|
+
* ancestor `behind`, base font-size, the capture root's area for the size axis). `rootArea` is
|
|
9131
|
+
* threaded from `walkAll` so every node is normalized against the SAME reference; a non-positive
|
|
9132
|
+
* value (a layout-less env, or an unmeasured root) disables the size axis in the scorer rather than
|
|
9133
|
+
* dividing by zero. Pure pass-through — the scorer owns the math.
|
|
9242
9134
|
*/
|
|
9243
9135
|
function nodeSalience(el, cs, rect, rootArea, win, styles) {
|
|
9244
9136
|
return salience(cs, rect, {
|
|
@@ -9252,21 +9144,17 @@ function rgbaString(c) {
|
|
|
9252
9144
|
return c.a < 1 ? `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})` : `rgb(${c.r}, ${c.g}, ${c.b})`;
|
|
9253
9145
|
}
|
|
9254
9146
|
/**
|
|
9255
|
-
* Measure-time paint + salience resolution (ADR 0022 invariants 3 & 5
|
|
9256
|
-
*
|
|
9257
|
-
*
|
|
9258
|
-
* and snapshots the result onto the node so a future markInk pass reads it off the
|
|
9259
|
-
* IR rather than re-touching the DOM. This is ADDITIVE: classification still reads
|
|
9260
|
-
* its own live values, so the emitted plate is byte-identical; these fields are
|
|
9261
|
-
* populated-but-not-yet-read.
|
|
9147
|
+
* Measure-time paint + salience resolution (ADR 0022 invariants 3 & 5). Runs the canvas-resolving
|
|
9148
|
+
* helpers eagerly — once, per element entry, while layout is warm — and snapshots the result onto
|
|
9149
|
+
* the node, so `markInk`/Absorb read it off the IR rather than re-touching the DOM.
|
|
9262
9150
|
*
|
|
9263
|
-
* - `paint.surface`/`bg`/`contrastingFill`/`radius` come from the existing
|
|
9264
|
-
* `
|
|
9265
|
-
*
|
|
9266
|
-
*
|
|
9267
|
-
* - `salience` is `nodeSalience`, which resolves the ancestor-climbed `behind`
|
|
9268
|
-
*
|
|
9269
|
-
*
|
|
9151
|
+
* - `paint.surface`/`bg`/`contrastingFill`/`radius` come from the existing `hasVisibleBackground` /
|
|
9152
|
+
* `toRgb` / `hasContrastingFill` helpers and the computed `border-radius`. The `bg` is the
|
|
9153
|
+
* canvas-resolved colour, so a later pass single-sources the colour through `toRgb` (no second
|
|
9154
|
+
* parse path).
|
|
9155
|
+
* - `salience` is `nodeSalience`, which resolves the ancestor-climbed `behind` (via `toRgb`) and base
|
|
9156
|
+
* font-size — both legal at measure time — so salience's own colour mirror never has to fall back
|
|
9157
|
+
* on the capture path.
|
|
9270
9158
|
*/
|
|
9271
9159
|
function resolvePaintAndSalience(el, cs, rect, state) {
|
|
9272
9160
|
const bg = toRgb(cs.backgroundColor, state.win);
|
|
@@ -9281,6 +9169,11 @@ function resolvePaintAndSalience(el, cs, rect, state) {
|
|
|
9281
9169
|
salience: nodeSalience(el, cs, rect, state.rootArea, state.win, state.styles)
|
|
9282
9170
|
};
|
|
9283
9171
|
}
|
|
9172
|
+
/**
|
|
9173
|
+
* Thrown when a capture's measured tree exceeds a Collect limit — almost always a `<Skeleton>`
|
|
9174
|
+
* sitting too high in the tree. Raised after the cheap walk and before the O(rules × nodes) CSS
|
|
9175
|
+
* extraction, so an oversized capture is refused without freezing the main thread on it.
|
|
9176
|
+
*/
|
|
9284
9177
|
var CaptureTooLargeError = class extends Error {
|
|
9285
9178
|
/** The full measured size of the surviving tree that tripped the gate. */
|
|
9286
9179
|
size;
|
|
@@ -9301,20 +9194,17 @@ var CaptureTooLargeError = class extends Error {
|
|
|
9301
9194
|
}
|
|
9302
9195
|
};
|
|
9303
9196
|
/**
|
|
9304
|
-
* The build phase
|
|
9305
|
-
*
|
|
9306
|
-
*
|
|
9307
|
-
*
|
|
9308
|
-
* `
|
|
9309
|
-
*
|
|
9310
|
-
* Gates on node count BEFORE the O(author rules × nodes) extract, as the legacy
|
|
9311
|
-
* path did.
|
|
9197
|
+
* The build phase: the walk builds the light-DOM IR directly (`walkAll` returns `{ root, nodes,
|
|
9198
|
+
* extraRules }`); this lifts the author CSS onto it. The CSS-extract (`copyRules`) fills each
|
|
9199
|
+
* node's `decls.author` (the lifted CSSOM + inline rules targeting it) and `decls.carried` (the
|
|
9200
|
+
* carried token-definition rules anchored to it, ADR 0019 §1), so the IR holds every node's
|
|
9201
|
+
* authored CSS — the substrate `emit` reads back out. Gates on the Collect limits BEFORE the
|
|
9202
|
+
* O(author rules × nodes) extract.
|
|
9312
9203
|
*
|
|
9313
|
-
* `copyRules` still returns the full flat rule list (its primary job here is the
|
|
9314
|
-
*
|
|
9315
|
-
*
|
|
9316
|
-
*
|
|
9317
|
-
* bundle for `emit` to fold back in.
|
|
9204
|
+
* `copyRules` still returns the full flat rule list (its primary job here is the `decls`
|
|
9205
|
+
* population); `emit` rebuilds that list from the IR, so the return is dropped — EXCEPT the one
|
|
9206
|
+
* rule not anchored to any node: the text-context rule (`ids: []`, the inherited parent font
|
|
9207
|
+
* `baseRules` derives), lifted onto the bundle for `emit` to fold back in.
|
|
9318
9208
|
*/
|
|
9319
9209
|
function build(walked, doc, win, styles, diagnostics, collect, walker) {
|
|
9320
9210
|
const bundle = {
|
|
@@ -9335,28 +9225,27 @@ function build(walked, doc, win, styles, diagnostics, collect, walker) {
|
|
|
9335
9225
|
return bundle;
|
|
9336
9226
|
}
|
|
9337
9227
|
/**
|
|
9338
|
-
* Run the per-View
|
|
9339
|
-
*
|
|
9340
|
-
* `
|
|
9341
|
-
*
|
|
9342
|
-
*
|
|
9343
|
-
*
|
|
9344
|
-
*
|
|
9345
|
-
* has moved on). `diagnostics` collects the Superset/Crop notes.
|
|
9228
|
+
* Run the per-View Distil passes over one measured `IRBundle`, in their fixed, load-bearing order
|
|
9229
|
+
* (ADR 0022 §3). Annotate-only — each pass sets fields (`folded`/ `flattened`/`count`/`dropped`);
|
|
9230
|
+
* `emit`/Serialize lower them. Kept apart from `build` (the Measure core — `markInk` + the Collect
|
|
9231
|
+
* gate + the CSS-extract — where `markInk` must run before the gate) so Distil is a pure `bundle →
|
|
9232
|
+
* bundle` step `distilPlate` runs once per View over a whole `PlateIR` after the sweep (reading the
|
|
9233
|
+
* snapshotted `ViewIR.viewportHeight`, since `window` has moved on). `diagnostics` collects the
|
|
9234
|
+
* Superset/Crop notes.
|
|
9346
9235
|
*
|
|
9347
9236
|
* Order: glyph-fold → Flatten → Superset → Crop → Template.
|
|
9348
|
-
*
|
|
9349
|
-
* -
|
|
9350
|
-
*
|
|
9351
|
-
*
|
|
9352
|
-
*
|
|
9353
|
-
*
|
|
9354
|
-
* (vertical) and `viewportWidth`
|
|
9355
|
-
*
|
|
9356
|
-
*
|
|
9357
|
-
* Crop drops a run's off-screen tail first; Template then groups the
|
|
9358
|
-
* and the faithful surviving `count` falls out of the fold (a dropped cell
|
|
9359
|
-
* template run).
|
|
9237
|
+
*
|
|
9238
|
+
* - Glyph-fold first: folds glyph-sized icons into adjacent text lines (ADR 0021 §2).
|
|
9239
|
+
* - Flatten next: collapses passthrough wrapper chains over the folded tree (ADR 0021 §1). These two
|
|
9240
|
+
* are lossless normalization the removal passes read.
|
|
9241
|
+
* - Superset / Crop: the "drop the imperceptible" pair — bone-on-bone (Superset) and off-screen
|
|
9242
|
+
* (Crop) — both only annotate `dropped`, so their relative order is immaterial; Superset stays
|
|
9243
|
+
* before Crop. Crop's base-case folds are `viewportHeight` (vertical) and `viewportWidth`
|
|
9244
|
+
* (horizontal); CROP_BUFFER stays internal to the pass.
|
|
9245
|
+
* - Template LAST: compression operates only on what SURVIVES the removal passes — what a viewer
|
|
9246
|
+
* would actually see. Crop drops a run's off-screen tail first; Template then groups the
|
|
9247
|
+
* on-screen prefix and the faithful surviving `count` falls out of the fold (a dropped cell
|
|
9248
|
+
* breaks a template run).
|
|
9360
9249
|
*/
|
|
9361
9250
|
function runPasses(bundle, viewportHeight, viewportWidth, diagnostics) {
|
|
9362
9251
|
foldGlyphRowsIR(bundle.root);
|
|
@@ -9366,19 +9255,17 @@ function runPasses(bundle, viewportHeight, viewportWidth, diagnostics) {
|
|
|
9366
9255
|
templateIR(bundle);
|
|
9367
9256
|
}
|
|
9368
9257
|
/**
|
|
9369
|
-
* Run the
|
|
9370
|
-
*
|
|
9371
|
-
*
|
|
9372
|
-
*
|
|
9373
|
-
*
|
|
9374
|
-
*
|
|
9375
|
-
*
|
|
9376
|
-
*
|
|
9377
|
-
*
|
|
9378
|
-
* The orchestration (step 4.4) calls this once after building the bundle in one
|
|
9379
|
-
* session.
|
|
9258
|
+
* Run the Distil passes over every View of a `PlateIR` (ADR 0022 §3) — the `PlateIR → PlateIR` pass
|
|
9259
|
+
* model, via `eachView`. Each View gets its own diagnostics collector (the passes' Superset/Crop
|
|
9260
|
+
* notes), merged onto the View's existing (Measure-time) diagnostics — a dev-only transient
|
|
9261
|
+
* (stripped before disk), so the merge order is immaterial. Crop reads the View's snapshotted
|
|
9262
|
+
* folds: the vertical base case is `viewportHeight` (an unset one passes `Infinity`, disabling the
|
|
9263
|
+
* base-case viewport fold — a fold of 0 would crop EVERYTHING, `top >= 1.5 * 0`, so a
|
|
9264
|
+
* manually-built bundle still folds only at a per-node clipping ancestor); the horizontal base case
|
|
9265
|
+
* is the View's `width` (its captured `innerWidth`, always set). The orchestration calls this once
|
|
9266
|
+
* after building the bundle in one session.
|
|
9380
9267
|
*/
|
|
9381
|
-
function
|
|
9268
|
+
function distilPlate(plate) {
|
|
9382
9269
|
eachView(plate, (view) => {
|
|
9383
9270
|
const diagnostics = createDiagnostics();
|
|
9384
9271
|
runPasses(view.bundle, view.viewportHeight ?? Number.POSITIVE_INFINITY, view.width, diagnostics);
|
|
@@ -9387,19 +9274,17 @@ function runPlatePasses(plate) {
|
|
|
9387
9274
|
});
|
|
9388
9275
|
}
|
|
9389
9276
|
/**
|
|
9390
|
-
* Measure the SURVIVING (non-dropped) tree in one DFS pass: total `nodes`, max
|
|
9391
|
-
*
|
|
9277
|
+
* Measure the SURVIVING (non-dropped) tree in one DFS pass: total `nodes`, max nesting `depth`, and
|
|
9278
|
+
* `breadth` — the widest level (most nodes at any one depth).
|
|
9392
9279
|
*
|
|
9393
|
-
* Walks via `effectiveKids`, so a `dropped` node (and its non-re-shown subtree) and
|
|
9394
|
-
*
|
|
9395
|
-
*
|
|
9396
|
-
*
|
|
9397
|
-
* keeping `nodes` in the same units as the legacy `entries.length` gate.
|
|
9280
|
+
* Walks via `effectiveKids`, so a `dropped` node (and its non-re-shown subtree) and a `flattened`
|
|
9281
|
+
* wrapper are handled exactly as `emit` will lower them — the gate measures the real post-mark
|
|
9282
|
+
* output, not every element Measure descended. The synthetic root (id 0, el null) is NOT a measured
|
|
9283
|
+
* entry, so it is excluded and `nodes` counts only measured entries.
|
|
9398
9284
|
*
|
|
9399
|
-
* Widest-level `breadth` is the realistic "how wide does this tree get" signal: a
|
|
9400
|
-
*
|
|
9401
|
-
*
|
|
9402
|
-
* Collect gate.
|
|
9285
|
+
* Widest-level `breadth` is the realistic "how wide does this tree get" signal: a DOM tree's total
|
|
9286
|
+
* tracks its widest level (~5×), not `breadth × depth` (which assumes every level is maximally wide
|
|
9287
|
+
* — a fractal that never occurs). Feeds the Collect gate.
|
|
9403
9288
|
*/
|
|
9404
9289
|
function measureTree(root) {
|
|
9405
9290
|
let nodes = 0;
|
|
@@ -9424,17 +9309,15 @@ function measureTree(root) {
|
|
|
9424
9309
|
};
|
|
9425
9310
|
}
|
|
9426
9311
|
/**
|
|
9427
|
-
* Measure one View's subtree into an `IRBundle` — the Measure stage (ADR 0022
|
|
9428
|
-
*
|
|
9429
|
-
*
|
|
9430
|
-
* capture
|
|
9431
|
-
*
|
|
9432
|
-
*
|
|
9433
|
-
*
|
|
9434
|
-
* `
|
|
9435
|
-
*
|
|
9436
|
-
* `PlateIR` View) — each shape the result without re-walking. `emit`/Serialize
|
|
9437
|
-
* read the IR back out.
|
|
9312
|
+
* Measure one View's subtree into an `IRBundle` — the Measure stage (ADR 0022 invariant 5): the
|
|
9313
|
+
* sole DOM-touching code on the capture path. Sets up the per-capture computed-style memo (built
|
|
9314
|
+
* here, discarded when this synchronous capture returns — never outlives the call, so no
|
|
9315
|
+
* cross-capture caching of live styles), runs the walk (which builds the light-DOM IR directly),
|
|
9316
|
+
* and runs the build phase: lift the author CSS onto each node's `decls`, then run the annotate
|
|
9317
|
+
* passes. Returns the bundle alongside the `win` (for the viewport width) and the `diagnostics`
|
|
9318
|
+
* collector, so the two callers — `captureBundle` (the raw bundle, for tests) and `captureView` (→
|
|
9319
|
+
* a `PlateIR` View) — each shape the result without re-walking. `emit`/Serialize read the IR back
|
|
9320
|
+
* out.
|
|
9438
9321
|
*/
|
|
9439
9322
|
function measureBundle(roots, options = {}) {
|
|
9440
9323
|
const doc = roots[0]?.ownerDocument;
|
|
@@ -9449,17 +9332,15 @@ function measureBundle(roots, options = {}) {
|
|
|
9449
9332
|
};
|
|
9450
9333
|
}
|
|
9451
9334
|
/**
|
|
9452
|
-
* The framework-agnostic capture core (ADR 0022 §1,§2): measure one View's subtree
|
|
9453
|
-
*
|
|
9454
|
-
*
|
|
9455
|
-
*
|
|
9456
|
-
*
|
|
9457
|
-
*
|
|
9458
|
-
*
|
|
9459
|
-
*
|
|
9460
|
-
*
|
|
9461
|
-
* ADR 0022 §2). With no `plate` it starts a fresh, as-yet-UNNAMED bundle — naming
|
|
9462
|
-
* and the breakpoint union are the orchestration's job (Views are orchestration,
|
|
9335
|
+
* The framework-agnostic capture core (ADR 0022 §1,§2): measure one View's subtree and APPEND it to
|
|
9336
|
+
* a per-Plate `PlateIR` bundle — ADR 0022 §1's `capture(el, ir?) → ir`. The single entry point for
|
|
9337
|
+
* capture: the dev client sweeps every View through it, and the framework-neutral `captureElement`
|
|
9338
|
+
* (core.ts) runs it once for the single-View case, both then distilling via `distilPlate` and
|
|
9339
|
+
* projecting via `serializePlateIR`. Walks the View's `roots` into an `IRBundle` (Measure + build),
|
|
9340
|
+
* wraps it as a `ViewIR` at the current viewport width with the capture's transient diagnostics,
|
|
9341
|
+
* and appends it to the passed-in `plate` (the orchestration threads ONE bundle through a Plate's
|
|
9342
|
+
* per-View calls in one session, ADR 0022 §2). With no `plate` it starts a fresh, as-yet-UNNAMED
|
|
9343
|
+
* bundle — naming and the breakpoint union are the orchestration's job (Views are orchestration,
|
|
9463
9344
|
* not core, §1), so the caller sets `name`/`breakpoints` on the returned `PlateIR`.
|
|
9464
9345
|
*/
|
|
9465
9346
|
function captureView(roots, options = {}, plate) {
|
|
@@ -9537,16 +9418,15 @@ function walk(el, state, depth, isCaptureRoot = false) {
|
|
|
9537
9418
|
ref: refName
|
|
9538
9419
|
};
|
|
9539
9420
|
}
|
|
9540
|
-
return
|
|
9421
|
+
return hintWalk(el, cs, state, depth);
|
|
9541
9422
|
}
|
|
9542
9423
|
/**
|
|
9543
|
-
* The built-in
|
|
9544
|
-
*
|
|
9545
|
-
*
|
|
9546
|
-
*
|
|
9547
|
-
* annotation can never override a nested `<Skeleton>` (ADR 0006).
|
|
9424
|
+
* The built-in Hint rung (ADR 0018), between the stitch guard and the default classifier. It honors
|
|
9425
|
+
* the capture-only `data-xr-*` hints and otherwise falls through to the default classifier. The
|
|
9426
|
+
* stitch guard already ran in `walk`, so a boundary element never reaches here — a hint can never
|
|
9427
|
+
* override a nested `<Skeleton>` (ADR 0006).
|
|
9548
9428
|
*/
|
|
9549
|
-
function
|
|
9429
|
+
function hintWalk(el, cs, state, depth) {
|
|
9550
9430
|
if (el.hasAttribute(XR_IGNORE_ATTR)) return ignoreElement(state);
|
|
9551
9431
|
const kindAttr = el.getAttribute(XR_BONE_KIND_ATTR);
|
|
9552
9432
|
if (kindAttr !== null) {
|
|
@@ -9557,27 +9437,24 @@ function annotationWalk(el, cs, state, depth) {
|
|
|
9557
9437
|
return defaultClassify(el, cs, state, depth);
|
|
9558
9438
|
}
|
|
9559
9439
|
/**
|
|
9560
|
-
* `data-xr-ignore`: drop the element and its subtree at WALK TIME. No Entry, no
|
|
9561
|
-
*
|
|
9562
|
-
*
|
|
9563
|
-
*
|
|
9564
|
-
*
|
|
9565
|
-
* new code per ignored element).
|
|
9440
|
+
* `data-xr-ignore`: drop the element and its subtree at WALK TIME. No Entry, no node, no descent —
|
|
9441
|
+
* it contributes nothing to the Plate, and never reaches the IR (so it never produces a `dropped:
|
|
9442
|
+
* 'ignored'`, which is the walker-only post-measure path; ADR 0022 §5). Aggregated under the
|
|
9443
|
+
* existing `dropped-tag` diagnostic (a hidden/non-visual drop is the same fidelity story; do not
|
|
9444
|
+
* spam a new code per ignored element).
|
|
9566
9445
|
*/
|
|
9567
9446
|
function ignoreElement(state) {
|
|
9568
9447
|
state.diagnostics.add("dropped-tag");
|
|
9569
9448
|
return null;
|
|
9570
9449
|
}
|
|
9571
9450
|
/**
|
|
9572
|
-
* Snapshot the raw clip inputs onto an IR node for the 2D Crop pass — one pair per
|
|
9573
|
-
*
|
|
9574
|
-
*
|
|
9575
|
-
*
|
|
9576
|
-
*
|
|
9577
|
-
*
|
|
9578
|
-
*
|
|
9579
|
-
* (the caller just read `getBoundingClientRect`). The walk only records; `cropIR`
|
|
9580
|
-
* derives the fold.
|
|
9451
|
+
* Snapshot the raw clip inputs onto an IR node for the 2D Crop pass — one pair per axis, each gated
|
|
9452
|
+
* on its OWN overflow not being `visible`. The vertical pair (`scrollHeight`/`clientHeight`, gated
|
|
9453
|
+
* on `overflow-y`) and the horizontal pair (`scrollWidth`/`clientWidth`, gated on `overflow-x`) are
|
|
9454
|
+
* element PROPS, not on the computed-style object and not derivable later, so they must be read at
|
|
9455
|
+
* measure time while layout is valid; reading them CAN force a layout, so each gate keeps it off
|
|
9456
|
+
* the `overflow: visible` majority for that axis. Layout is already warm here (the caller just read
|
|
9457
|
+
* `getBoundingClientRect`). The walk only records; `cropIR` derives the fold.
|
|
9581
9458
|
*/
|
|
9582
9459
|
function snapshotClipInputs(el, cs, node) {
|
|
9583
9460
|
if (cs.overflowY !== "visible") {
|
|
@@ -9590,23 +9467,19 @@ function snapshotClipInputs(el, cs, node) {
|
|
|
9590
9467
|
}
|
|
9591
9468
|
}
|
|
9592
9469
|
/**
|
|
9593
|
-
* The kind-inference heuristic shared by the walker's `ctx.bone()` without an
|
|
9594
|
-
*
|
|
9595
|
-
*
|
|
9596
|
-
*
|
|
9597
|
-
*
|
|
9598
|
-
*
|
|
9599
|
-
* keeping this one function is the single source of truth for "what kind is an
|
|
9600
|
-
* element that has no children to descend into".
|
|
9470
|
+
* The kind-inference heuristic shared by the walker's `ctx.bone()` without an explicit kind (via
|
|
9471
|
+
* the `applyWalker` pass) and the default classifier (ADR 0018, Q4 — ONE code path). A media tag is
|
|
9472
|
+
* `media`; anything else collapsed to a single bone reads as a `box`. The default classifier
|
|
9473
|
+
* reaches the same outcomes through its own structural branches (MEDIA_TAGS -> media, contrasting
|
|
9474
|
+
* fill / painted empty -> box), so keeping this one function is the single source of truth for
|
|
9475
|
+
* "what kind is an element that has no children to descend into".
|
|
9601
9476
|
*/
|
|
9602
9477
|
function inferLeafKind(el) {
|
|
9603
9478
|
return MEDIA_TAGS.has(el.tagName.toUpperCase()) ? "media" : "box";
|
|
9604
9479
|
}
|
|
9605
9480
|
/**
|
|
9606
|
-
* The DEFAULT classifier — the
|
|
9607
|
-
*
|
|
9608
|
-
* annotations this is the only path taken, and its output is byte-identical to
|
|
9609
|
-
* before the ladder was introduced.
|
|
9481
|
+
* The DEFAULT classifier — the precedence ladder's last rung (ADR 0018). With no walker and no
|
|
9482
|
+
* hints this is the only path taken.
|
|
9610
9483
|
*/
|
|
9611
9484
|
function defaultClassify(el, cs, state, depth, markKind) {
|
|
9612
9485
|
const tag = el.tagName.toUpperCase();
|
|
@@ -9726,22 +9599,21 @@ function defaultClassify(el, cs, state, depth, markKind) {
|
|
|
9726
9599
|
//#endregion
|
|
9727
9600
|
//#region src/chunk.ts
|
|
9728
9601
|
/**
|
|
9729
|
-
* Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
|
|
9730
|
-
*
|
|
9731
|
-
*
|
|
9732
|
-
*
|
|
9733
|
-
*
|
|
9734
|
-
*
|
|
9735
|
-
* content-addressed class tokens (ADR 0007), never user content.
|
|
9602
|
+
* Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR 0008). This is the
|
|
9603
|
+
* framework-neutral half of rendering: a skeleton is static, text-free DOM, so the structure is
|
|
9604
|
+
* flattened to HTML strings here, at build, once — instead of every adapter walking a tree on the
|
|
9605
|
+
* latency-critical first paint. The adapter only injects the strings and mounts a child plate at
|
|
9606
|
+
* each stitch (see the React adapters). No escaping risk: bones carry only the content-addressed
|
|
9607
|
+
* class tokens (ADR 0007), never user content.
|
|
9736
9608
|
*/
|
|
9737
9609
|
function nodeClass(node) {
|
|
9738
9610
|
return NODE_CLASS + (node.leaf ? ` ${LEAF_CLASS} ${LEAF_CLASS}-${node.leaf}` : "") + (node.cls ? ` ${node.cls}` : "");
|
|
9739
9611
|
}
|
|
9740
9612
|
/**
|
|
9741
|
-
* Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch
|
|
9742
|
-
*
|
|
9743
|
-
*
|
|
9744
|
-
*
|
|
9613
|
+
* Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch (`ref`, a mount point
|
|
9614
|
+
* resolved to a child plate) or a template (`count`, a cell the adapter repeats). Either keeps the
|
|
9615
|
+
* subtree from collapsing to one flat string, since both are expanded at render rather than baked
|
|
9616
|
+
* into the markup.
|
|
9745
9617
|
*/
|
|
9746
9618
|
function hasDynamic(node) {
|
|
9747
9619
|
return node.ref !== void 0 || node.count !== void 0 || (node.kids?.some(hasDynamic) ?? false);
|
|
@@ -9752,13 +9624,12 @@ function toHtml(node) {
|
|
|
9752
9624
|
return `<div class="${nodeClass(node)}">${kids}</div>`;
|
|
9753
9625
|
}
|
|
9754
9626
|
/**
|
|
9755
|
-
* Serialize a node list into chunks: each maximal run with no render-time dynamic
|
|
9756
|
-
*
|
|
9757
|
-
*
|
|
9758
|
-
*
|
|
9759
|
-
*
|
|
9760
|
-
*
|
|
9761
|
-
* `toHtml` is a plain string the adapter repeats.
|
|
9627
|
+
* Serialize a node list into chunks: each maximal run with no render-time dynamic node becomes one
|
|
9628
|
+
* HTML `string`; a `ref` node becomes `{ r }`; a `count` node becomes `{ t: html, n }` (its cell's
|
|
9629
|
+
* HTML once + the repeat count); a node that isn't itself dynamic but holds one deeper stays a real
|
|
9630
|
+
* element (`{ c, k }`) and recurses. Mirrors the element shape the runtime renderer would otherwise
|
|
9631
|
+
* build per node. A `count` cell is guaranteed stitch-free by the capture pass, so its `toHtml` is
|
|
9632
|
+
* a plain string the adapter repeats.
|
|
9762
9633
|
*/
|
|
9763
9634
|
function toChunks(nodes) {
|
|
9764
9635
|
const out = [];
|
|
@@ -9789,41 +9660,41 @@ function toChunks(nodes) {
|
|
|
9789
9660
|
return out;
|
|
9790
9661
|
}
|
|
9791
9662
|
/**
|
|
9792
|
-
* Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The
|
|
9793
|
-
*
|
|
9794
|
-
*
|
|
9795
|
-
*
|
|
9663
|
+
* Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The root's own content
|
|
9664
|
+
* classes travel as `rootCls` (the adapter puts them on the root element); its children become
|
|
9665
|
+
* `chunks`. A capture-less plate has null chunks and renders the fallback.
|
|
9666
|
+
*
|
|
9667
|
+
* Two-artifact contract (ADR 0026): the returned `Plate` is the TREE artifact only; the input's
|
|
9668
|
+
* `css` is deliberately NOT copied onto it. The css string stays the producer's
|
|
9669
|
+
* (`GatedPlate`/`StoredPlate`) field, and each lowering site decides independently how its css
|
|
9670
|
+
* travels: the plugin's virtual `.css` modules + `export const css`, `captureElement`'s `{ plate,
|
|
9671
|
+
* css }` pair, or the `xray:plate` dev event, which ships no css.
|
|
9796
9672
|
*/
|
|
9797
9673
|
function serializePlate(merged) {
|
|
9798
|
-
const { v, name, tree
|
|
9674
|
+
const { v, name, tree } = merged;
|
|
9799
9675
|
if (!tree) return {
|
|
9800
9676
|
v,
|
|
9801
9677
|
name,
|
|
9802
|
-
chunks: null
|
|
9803
|
-
css
|
|
9678
|
+
chunks: null
|
|
9804
9679
|
};
|
|
9805
9680
|
const chunks = toChunks(tree.kids ?? []);
|
|
9806
9681
|
return tree.cls ? {
|
|
9807
9682
|
v,
|
|
9808
9683
|
name,
|
|
9809
9684
|
chunks,
|
|
9810
|
-
rootCls: tree.cls
|
|
9811
|
-
css
|
|
9685
|
+
rootCls: tree.cls
|
|
9812
9686
|
} : {
|
|
9813
9687
|
v,
|
|
9814
9688
|
name,
|
|
9815
|
-
chunks
|
|
9816
|
-
css
|
|
9689
|
+
chunks
|
|
9817
9690
|
};
|
|
9818
9691
|
}
|
|
9819
9692
|
/**
|
|
9820
|
-
* Distinct plate names referenced by the tree's stitches, in first-seen order.
|
|
9821
|
-
*
|
|
9822
|
-
*
|
|
9823
|
-
*
|
|
9824
|
-
*
|
|
9825
|
-
* `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
|
|
9826
|
-
* in-browser `serializePlateIR`, project.ts).
|
|
9693
|
+
* Distinct plate names referenced by the tree's stitches, in first-seen order. Walks a `RenderNode`
|
|
9694
|
+
* tree (the stored, post-classify shape), so it drives the gen-side stitch import graph straight
|
|
9695
|
+
* off a `StoredPlate.tree` (`renderPlateModule`, index.ts). The one node-side primitive needed at
|
|
9696
|
+
* build time; multi-View gating happens in the in-browser `serializePlateIR` (serialize.ts, ADR
|
|
9697
|
+
* 0022).
|
|
9827
9698
|
*/
|
|
9828
9699
|
function collectRefs(tree) {
|
|
9829
9700
|
const names = [];
|
|
@@ -9841,14 +9712,13 @@ function collectRefs(tree) {
|
|
|
9841
9712
|
//#endregion
|
|
9842
9713
|
//#region src/breakpoints.ts
|
|
9843
9714
|
/**
|
|
9844
|
-
* Per-plate breakpoint derivation (ADR 0004): the width thresholds that could
|
|
9845
|
-
*
|
|
9846
|
-
*
|
|
9847
|
-
*
|
|
9848
|
-
* adjacent views collapse at merge time.
|
|
9715
|
+
* Per-plate breakpoint derivation (ADR 0004): the width thresholds that could change this plate
|
|
9716
|
+
* come from the `@media` conditions on its own copied rules plus the queries the app evaluated
|
|
9717
|
+
* through `matchMedia` during capture. Thresholds partition the width axis into Views; identical
|
|
9718
|
+
* captures in adjacent Views collapse at Serialize.
|
|
9849
9719
|
*/
|
|
9850
9720
|
const FONT_SIZE_PX = 16;
|
|
9851
|
-
/** A breakpoint is the first integer px width of the
|
|
9721
|
+
/** A breakpoint is the first integer px width of the View ABOVE the boundary. */
|
|
9852
9722
|
function thresholdsOf(condition) {
|
|
9853
9723
|
const out = [];
|
|
9854
9724
|
const toPx = (value, unit) => Number.parseFloat(value) * (unit === "px" ? 1 : FONT_SIZE_PX);
|
|
@@ -9870,8 +9740,8 @@ function deriveBreakpoints(rules, matchMediaQueries = []) {
|
|
|
9870
9740
|
for (const query of matchMediaQueries) for (const bp of thresholdsOf(query)) set.add(bp);
|
|
9871
9741
|
return [...set].toSorted((a, b) => a - b);
|
|
9872
9742
|
}
|
|
9873
|
-
/** The
|
|
9874
|
-
function
|
|
9743
|
+
/** The View interval `[min, max)` a given viewport width falls into. */
|
|
9744
|
+
function viewSpanFor(width, breakpoints) {
|
|
9875
9745
|
let min;
|
|
9876
9746
|
let max;
|
|
9877
9747
|
for (const bp of breakpoints) if (bp <= width) min = bp;
|
|
@@ -9887,17 +9757,14 @@ function regimeFor(width, breakpoints) {
|
|
|
9887
9757
|
//#endregion
|
|
9888
9758
|
//#region src/classify.ts
|
|
9889
9759
|
/**
|
|
9890
|
-
* Turn id-form rules + an id-tree into the shipped plate: each distinct
|
|
9891
|
-
*
|
|
9892
|
-
*
|
|
9893
|
-
*
|
|
9894
|
-
*
|
|
9895
|
-
*
|
|
9896
|
-
*
|
|
9897
|
-
* (
|
|
9898
|
-
* unlayered; the existing cascade is reproduced by emission order (losers
|
|
9899
|
-
* first), so the classes are emitted in `(layered, spec, order)` order exactly
|
|
9900
|
-
* as `renderRules` did.
|
|
9760
|
+
* Turn id-form rules + an id-tree into the shipped plate: each distinct `(tier, media,
|
|
9761
|
+
* declarations)` becomes one class, named by a per-plate sequential id (`[data-xr-root]
|
|
9762
|
+
* .xr-<plate>-<n>`), and every node carries the class tokens it needs instead of an id (ADR 0007).
|
|
9763
|
+
* The id is a plain counter, not a content hash: the plate prefix already keeps tokens disjoint
|
|
9764
|
+
* across plates (the stitch-isolation guarantee), so a per-plate counter is collision-free by
|
|
9765
|
+
* construction — and low-entropy ids compress far better than random hashes (~31% smaller brotli on
|
|
9766
|
+
* a real plate; see the ADR 0007 amendment). Rules stay unlayered; the cascade is reproduced by
|
|
9767
|
+
* emission order (losers first), so the classes are emitted in `(layered, spec, order)` order.
|
|
9901
9768
|
*
|
|
9902
9769
|
* Pure data-in/data-out — runs at plugin load (node) and in tests.
|
|
9903
9770
|
*/
|
|
@@ -9978,15 +9845,14 @@ function render(node, classes) {
|
|
|
9978
9845
|
/** Content-class prefix; distinct from the fixed `xr-root/node/leaf` base words. */
|
|
9979
9846
|
const CLASS_PREFIX = "xr-";
|
|
9980
9847
|
/**
|
|
9981
|
-
* Font/text-rendering properties that are ALWAYS dead at generation. A bone paints
|
|
9982
|
-
*
|
|
9983
|
-
*
|
|
9984
|
-
* `
|
|
9985
|
-
*
|
|
9986
|
-
*
|
|
9987
|
-
* inline-block bones, not text.
|
|
9848
|
+
* Font/text-rendering properties that are ALWAYS dead at generation. A bone paints no glyphs, so
|
|
9849
|
+
* these never reach its box — yet capture still emits them (the inherited context rule, node
|
|
9850
|
+
* fallbacks, lifted author rules), and `filterLayoutDecls`/the validator legitimately accept them.
|
|
9851
|
+
* `line-height` and `font-size` are NOT here: each can be load-bearing, so `dropDeadDecls` keeps
|
|
9852
|
+
* them conditionally. KEEP `vertical-align`/`text-align`: they position inline-block bones, not
|
|
9853
|
+
* text.
|
|
9988
9854
|
*/
|
|
9989
|
-
const DROP_AT_GEN_ALWAYS = new Set([
|
|
9855
|
+
const DROP_AT_GEN_ALWAYS = /* @__PURE__ */ new Set([
|
|
9990
9856
|
"font",
|
|
9991
9857
|
"font-family",
|
|
9992
9858
|
"font-weight",
|
|
@@ -10000,11 +9866,11 @@ const DROP_AT_GEN_ALWAYS = new Set([
|
|
|
10000
9866
|
"text-wrap"
|
|
10001
9867
|
]);
|
|
10002
9868
|
/**
|
|
10003
|
-
* An element-font-relative length (`em`/`ex`/`ch`) — resolves against the
|
|
10004
|
-
*
|
|
10005
|
-
*
|
|
10006
|
-
*
|
|
10007
|
-
*
|
|
9869
|
+
* An element-font-relative length (`em`/`ex`/`ch`) — resolves against the element's own
|
|
9870
|
+
* `font-size`, so dropping a per-node `font-size` would shift it. EXCLUDES `rem` (root-relative,
|
|
9871
|
+
* unaffected): its `em` is preceded by `r`, never by a digit. Case-insensitive (author CSS may
|
|
9872
|
+
* write `1EM`); the trailing `\b` stops `2em` matching inside a longer identifier (`flex`,
|
|
9873
|
+
* `text`).
|
|
10008
9874
|
*/
|
|
10009
9875
|
const FONT_RELATIVE_LENGTH = /\d(?:em|ex|ch)\b/iu;
|
|
10010
9876
|
/** A flex/grid/table/ruby display — its child boxes get no inline line box. */
|
|
@@ -10015,21 +9881,20 @@ function declProp(decl) {
|
|
|
10015
9881
|
return colon < 0 ? "" : decl.slice(0, colon).trim().toLowerCase();
|
|
10016
9882
|
}
|
|
10017
9883
|
/**
|
|
10018
|
-
* Drop dead font/text decls before content-addressing. The always-dead props go
|
|
10019
|
-
*
|
|
9884
|
+
* Drop dead font/text decls before content-addressing. The always-dead props go unconditionally;
|
|
9885
|
+
* the two that CAN matter are kept narrowly:
|
|
10020
9886
|
*
|
|
10021
|
-
*
|
|
10022
|
-
*
|
|
10023
|
-
*
|
|
10024
|
-
*
|
|
10025
|
-
*
|
|
10026
|
-
*
|
|
10027
|
-
*
|
|
10028
|
-
*
|
|
10029
|
-
* means a dropped per-node `font-size` could shift a size.
|
|
9887
|
+
* - `line-height` survives only as the walk's per-row pin: a `SPEC_FALLBACK` rule on an inline-flow
|
|
9888
|
+
* node (no flex/grid/table/ruby display). That pin (measure.ts, `hasTextBar`) sizes the
|
|
9889
|
+
* inline-block text bone's line box, so dropping it drifts the row. The shared context
|
|
9890
|
+
* `line-height`, author copies, and flex/grid rows (text is a flex _item_ there — no line box)
|
|
9891
|
+
* are all dead.
|
|
9892
|
+
* - `font-size` survives only if some kept decl uses an element-font-relative unit (`em`/`ex`/`ch`)
|
|
9893
|
+
* that resolves against it. A plate on `px`/`rem` only (e.g. MW) drops it everywhere. The scan is
|
|
9894
|
+
* plate-wide: one such unit anywhere means a dropped per-node `font-size` could shift a size.
|
|
10030
9895
|
*
|
|
10031
|
-
* A rule emptied by the filter (the all-font context rule) is dropped downstream
|
|
10032
|
-
*
|
|
9896
|
+
* A rule emptied by the filter (the all-font context rule) is dropped downstream by the existing
|
|
9897
|
+
* `decls.length === 0` guard.
|
|
10033
9898
|
*/
|
|
10034
9899
|
function dropDeadDecls(rules) {
|
|
10035
9900
|
const keepFontSize = rules.some((rule) => rule.decls.some((decl) => !DROP_AT_GEN_ALWAYS.has(declProp(decl)) && FONT_RELATIVE_LENGTH.test(decl)));
|
|
@@ -10049,30 +9914,26 @@ function dropDeadDecls(rules) {
|
|
|
10049
9914
|
});
|
|
10050
9915
|
}
|
|
10051
9916
|
/**
|
|
10052
|
-
* Encode a plate name into a class-token prefix, injectively. This prefix
|
|
10053
|
-
*
|
|
10054
|
-
*
|
|
10055
|
-
* `<style>` block reorder another's classes.
|
|
9917
|
+
* Encode a plate name into a class-token prefix, injectively. This prefix _is_ the isolation
|
|
9918
|
+
* boundary across stitched plates (ADR 0007), so distinct plate names must never produce the same
|
|
9919
|
+
* prefix — a collision would let one plate's `<style>` block reorder another's classes.
|
|
10056
9920
|
*
|
|
10057
|
-
* Plate names are constrained by `isValidPlateName`: each path segment is
|
|
10058
|
-
*
|
|
10059
|
-
*
|
|
10060
|
-
*
|
|
10061
|
-
*
|
|
10062
|
-
*
|
|
10063
|
-
* Keeping `/` visually as `--` also leaves nested names readable in the
|
|
10064
|
-
* generated CSS (`product--card`).
|
|
9921
|
+
* Plate names are constrained by `isValidPlateName`: each path segment is `\w+(-\w+)*` (word chars
|
|
9922
|
+
* with single interior hyphens, no doubled or edge hyphen) and segments are joined by `/`. The
|
|
9923
|
+
* slash is the only char that is not class-safe, so rendering it as `--` is enough — and because no
|
|
9924
|
+
* segment holds a literal `--` or an edge hyphen, no `-` ever abuts a `/`, so every `--` in the
|
|
9925
|
+
* prefix unambiguously came from a `/`, making the map injective. Keeping `/` visually as `--` also
|
|
9926
|
+
* leaves nested names readable in the generated CSS (`product--card`).
|
|
10065
9927
|
*/
|
|
10066
9928
|
function encodePlateName(name) {
|
|
10067
9929
|
return name.replace(/\//g, "--");
|
|
10068
9930
|
}
|
|
10069
9931
|
/**
|
|
10070
|
-
* What the class identity keys on, alongside media + declarations:
|
|
10071
|
-
*
|
|
10072
|
-
*
|
|
10073
|
-
*
|
|
10074
|
-
*
|
|
10075
|
-
* One-line flip if QA ever surfaces a mis-order.
|
|
9932
|
+
* What the class identity keys on, alongside media + declarations: - `'full'` (the shipped
|
|
9933
|
+
* setting): the exact `spec` — provably no inversion. - `'tier'`: the discrete cascade tier —
|
|
9934
|
+
* marginally smaller (more bodies dedupe), but a rare same-tier inversion within the continuous
|
|
9935
|
+
* `author` band can mis-order one property on one node (ADR 0007). One-line flip if size ever
|
|
9936
|
+
* matters more than the inversion risk.
|
|
10076
9937
|
*/
|
|
10077
9938
|
const SPEC_FOLD = "full";
|
|
10078
9939
|
function tierLabel(spec) {
|
|
@@ -10084,44 +9945,39 @@ function tierLabel(spec) {
|
|
|
10084
9945
|
return "au";
|
|
10085
9946
|
}
|
|
10086
9947
|
//#endregion
|
|
10087
|
-
//#region src/
|
|
9948
|
+
//#region src/serialize.ts
|
|
10088
9949
|
/**
|
|
10089
|
-
* SERIALIZE — the single in-browser projection of a per-Plate IR into the shipped
|
|
10090
|
-
*
|
|
10091
|
-
*
|
|
10092
|
-
*
|
|
10093
|
-
*
|
|
10094
|
-
*
|
|
10095
|
-
* `flattened`/`count` fields), and lowers those annotations into the shipped shape.
|
|
9950
|
+
* SERIALIZE — the single in-browser projection of a per-Plate IR into the shipped `Plate` (ADR 0022
|
|
9951
|
+
* §5). The capture session measures every View into one `PlateIR` (`captureView`, measure.ts) and
|
|
9952
|
+
* runs the Distil passes over it ONCE (`distilPlate`, measure.ts); Serialize is the step AFTER — it
|
|
9953
|
+
* only PROJECTS, it runs no passes. It assumes each `view.bundle` is already measured AND distilled
|
|
9954
|
+
* (the annotate-only passes have set their `dropped`/`folded`/ `flattened`/`count` fields), and
|
|
9955
|
+
* lowers those annotations into the shipped shape.
|
|
10096
9956
|
*
|
|
10097
|
-
* It FOLDS `emit` (ir.ts) — emit is the per-View projection that reads the IR back
|
|
10098
|
-
*
|
|
10099
|
-
* `boneBox`/`effectiveKind`/`effectiveDecls`. Serialize calls it once per View, then
|
|
10100
|
-
*
|
|
10101
|
-
*
|
|
10102
|
-
*
|
|
10103
|
-
* `
|
|
10104
|
-
*
|
|
10105
|
-
*
|
|
10106
|
-
* (ADR 0022 §5), keeping the Tailwind/Bundle passthrough a clean gen-side seam and the
|
|
10107
|
-
* analysis-only geometry off the wire (only this structured projection is posted).
|
|
9957
|
+
* It FOLDS `emit` (ir.ts) — emit is the per-View projection that reads the IR back out into id-form
|
|
9958
|
+
* `{ tree, rules }`, lowering the annotations via `effectiveKids`/
|
|
9959
|
+
* `boneBox`/`effectiveKind`/`effectiveDecls`. Serialize calls it once per View, then carries every
|
|
9960
|
+
* View whole under ONE id-tree and gates each View by breakpoint in-line (the multi-View gate lives
|
|
9961
|
+
* HERE — ADR 0022), and runs `classify` to content-address the rules. It returns a `StoredPlate` —
|
|
9962
|
+
* the gated, content-addressed `{ tree, css }` PLUS `breakpoints` — NOT finished chunks: the
|
|
9963
|
+
* `serializePlate` (chunk.ts) lowering to `Plate` chunks stays GEN-SIDE at module-load (ADR 0022
|
|
9964
|
+
* §5), keeping the Tailwind/Bundle passthrough a clean gen-side seam and the analysis-only geometry
|
|
9965
|
+
* off the wire (only this structured projection is posted).
|
|
10108
9966
|
*
|
|
10109
|
-
* The wrapper-island gate (ADR 0022 §2 invariant). A View's breakpoint gate must
|
|
10110
|
-
*
|
|
10111
|
-
*
|
|
10112
|
-
*
|
|
10113
|
-
*
|
|
10114
|
-
*
|
|
10115
|
-
*
|
|
10116
|
-
*
|
|
10117
|
-
*
|
|
10118
|
-
*
|
|
10119
|
-
* contents`, so nothing overrides it). This also reduces the gate rules from
|
|
10120
|
-
* N-per-View to 1-per-View and is uniform across stitch top-kids, flattened-to-top
|
|
9967
|
+
* The wrapper-island gate (ADR 0022 §2 invariant). A View's breakpoint gate must hide the View's
|
|
9968
|
+
* ENTIRE subtree off-range — INCLUDING any stitch — regardless of whether a node has a serializable
|
|
9969
|
+
* class slot. Gating each top kid individually silently drops a gate that lands on a stitch,
|
|
9970
|
+
* because `toChunks` serializes a stitch as a bare `{ r }` with no class slot (the off-range View
|
|
9971
|
+
* collapse: a parent skeleton stitching nested children collapsed off-range). So each View's top
|
|
9972
|
+
* kids are wrapped in ONE synthetic CONTAINER "island" that ALWAYS carries a class; the island is
|
|
9973
|
+
* gated ONCE and its `display: none !important` inherits to any stitch inside. Active, the island
|
|
9974
|
+
* is `display: contents` so it generates no box — layout-neutral (`.xr-node` sets no display,
|
|
9975
|
+
* `.xr-root` is itself `display: contents`, so nothing overrides it). This also reduces the gate
|
|
9976
|
+
* rules from N-per-View to 1-per-View and is uniform across stitch top-kids, flattened-to-top
|
|
10121
9977
|
* stitches, and nested stitches alike.
|
|
10122
9978
|
*
|
|
10123
|
-
* Pure data-in/data-out over the (already-
|
|
10124
|
-
*
|
|
9979
|
+
* Pure data-in/data-out over the (already-distilled) IR — `emit` and everything below read only the
|
|
9980
|
+
* passed structures, re-measure nothing.
|
|
10125
9981
|
*/
|
|
10126
9982
|
function serializePlateIR(plate) {
|
|
10127
9983
|
const views = plate.views.map((view) => emit(view.bundle)).map((projected, i) => ({
|
|
@@ -10131,7 +9987,7 @@ function serializePlateIR(plate) {
|
|
|
10131
9987
|
if (views.length <= 1) {
|
|
10132
9988
|
const only = views[0];
|
|
10133
9989
|
if (!only) return {
|
|
10134
|
-
v:
|
|
9990
|
+
v: 1,
|
|
10135
9991
|
name: plate.name,
|
|
10136
9992
|
breakpoints: plate.breakpoints,
|
|
10137
9993
|
tree: null,
|
|
@@ -10139,14 +9995,14 @@ function serializePlateIR(plate) {
|
|
|
10139
9995
|
};
|
|
10140
9996
|
const { tree, css } = classify(only.rules, only.tree, plate.name);
|
|
10141
9997
|
return {
|
|
10142
|
-
v:
|
|
9998
|
+
v: 1,
|
|
10143
9999
|
name: plate.name,
|
|
10144
10000
|
breakpoints: plate.breakpoints,
|
|
10145
10001
|
tree,
|
|
10146
10002
|
css
|
|
10147
10003
|
};
|
|
10148
10004
|
}
|
|
10149
|
-
const starts = views.map((view) =>
|
|
10005
|
+
const starts = views.map((view) => viewSpanFor(view.width, plate.breakpoints).min ?? 0);
|
|
10150
10006
|
let nextId = 1;
|
|
10151
10007
|
const islands = [];
|
|
10152
10008
|
const rules = [];
|
|
@@ -10175,7 +10031,7 @@ function serializePlateIR(plate) {
|
|
|
10175
10031
|
kids: islands
|
|
10176
10032
|
}, plate.name);
|
|
10177
10033
|
return {
|
|
10178
|
-
v:
|
|
10034
|
+
v: 1,
|
|
10179
10035
|
name: plate.name,
|
|
10180
10036
|
breakpoints: plate.breakpoints,
|
|
10181
10037
|
tree,
|
|
@@ -10192,11 +10048,7 @@ function gate(id, condition) {
|
|
|
10192
10048
|
order: id
|
|
10193
10049
|
};
|
|
10194
10050
|
}
|
|
10195
|
-
/**
|
|
10196
|
-
* Deep-clone a captured subtree into fresh ids, recording the mapping. The
|
|
10197
|
-
* in-browser home of the old gen-side merge's id remap (`mergeViews`, removed in
|
|
10198
|
-
* the ADR 0022 cutover).
|
|
10199
|
-
*/
|
|
10051
|
+
/** Deep-clone a captured subtree into fresh ids, recording the mapping. */
|
|
10200
10052
|
function cloneWithIds(node, idMap, allocate) {
|
|
10201
10053
|
const id = allocate();
|
|
10202
10054
|
idMap.set(node.id, id);
|
|
@@ -10210,21 +10062,19 @@ function cloneWithIds(node, idMap, allocate) {
|
|
|
10210
10062
|
};
|
|
10211
10063
|
}
|
|
10212
10064
|
/**
|
|
10213
|
-
* Remap a View's rule ids onto its fresh-id-remapped nodes
|
|
10214
|
-
*
|
|
10215
|
-
*
|
|
10216
|
-
*
|
|
10217
|
-
*
|
|
10218
|
-
* #28 redux, the crooked multi-view layout). An unmapped id is one of two things:
|
|
10065
|
+
* Remap a View's rule ids onto its fresh-id-remapped nodes. Every View's nodes are re-id'd into one
|
|
10066
|
+
* shared global id space here, so a rule id that is NOT in this View's `idMap` must NOT keep its
|
|
10067
|
+
* per-View number: that number is some OTHER View's global id, and aliasing onto it
|
|
10068
|
+
* cross-contaminates Views (a box-sizing/flex rule from View B landing on View A's island/container
|
|
10069
|
+
* — the crooked multi-view layout). An unmapped id is one of two things:
|
|
10219
10070
|
*
|
|
10220
|
-
*
|
|
10221
|
-
*
|
|
10222
|
-
*
|
|
10223
|
-
*
|
|
10224
|
-
*
|
|
10225
|
-
*
|
|
10226
|
-
*
|
|
10227
|
-
* an empty `ids` to the root.
|
|
10071
|
+
* - `0` — the per-View synthetic root. Its rules (carried tokens, the context rule) belong on the ONE
|
|
10072
|
+
* shared plate root, also id 0, so map `0 → 0`.
|
|
10073
|
+
* - Any other unmapped id — a node a Distil pass DROPPED (Superset/Crop/glyph-fold) that a shared
|
|
10074
|
+
* author rule still lists by id. It has no node in this View's projected tree, so drop the id
|
|
10075
|
+
* (emit already prunes `extraRules` this way; author rules were the gap). A rule left with no ids
|
|
10076
|
+
* is itself dropped — note an originally-rootward rule (`ids: []`) is preserved as-is, since
|
|
10077
|
+
* `classify` resolves an empty `ids` to the root.
|
|
10228
10078
|
*/
|
|
10229
10079
|
function remapRules(rules, idMap) {
|
|
10230
10080
|
const out = [];
|
|
@@ -10247,4 +10097,4 @@ function remapRules(rules, idMap) {
|
|
|
10247
10097
|
return out;
|
|
10248
10098
|
}
|
|
10249
10099
|
//#endregion
|
|
10250
|
-
export { serializePlate as a, defineXrayCaptureWalker as c, formatDiagnostic as d, makeDiagnostic as f, collectRefs as i,
|
|
10100
|
+
export { serializePlate as a, defineXrayCaptureWalker as c, formatDiagnostic as d, makeDiagnostic as f, collectRefs as i, distilPlate as l, deriveBreakpoints as n, CaptureTooLargeError as o, emit as p, viewSpanFor as r, captureView as s, serializePlateIR as t, diagnosticsHeader as u };
|