@hueest/xray 0.1.0 → 0.3.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/dist/client.d.ts +64 -16
- package/dist/client.js +903 -72
- package/dist/core.d.ts +21 -27
- package/dist/core.js +39 -210
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +297 -48
- package/dist/index.js +674 -353
- package/dist/plate-BIr62u7l.d.ts +363 -0
- package/dist/plate-BRR6d8Se.js +156 -0
- package/dist/project-DORoPAo7.js +10224 -0
- package/dist/react.core-BUj8ziwh.d.ts +79 -0
- package/dist/react.core-DMIhXHZF.js +272 -0
- package/dist/react.d.ts +2 -8
- package/dist/react.dev.d.ts +2 -9
- package/dist/react.dev.js +153 -23
- package/dist/react.js +24 -12
- package/dist/serialize-Cs7hUxmK.d.ts +107 -0
- package/package.json +8 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/plate-DboxMKg-.d.ts +0 -196
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize-Bq6yJnNV.js +0 -1071
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { a as RenderNode, c as emptyPlate, i as PlateNode, n as MergedPlate, r as Plate, s as ViewCapture, t as LeafKind } from "./plate-BIr62u7l.js";
|
|
2
|
+
import { a as XrayCaptureWalker, i as WalkerDecision, n as CaptureTooLargeError, o as defineXrayCaptureWalker, r as CaptureWalkerContext, t as CaptureOptions } from "./serialize-Cs7hUxmK.js";
|
|
3
3
|
|
|
4
4
|
//#region src/chunk.d.ts
|
|
5
5
|
/**
|
|
@@ -9,36 +9,30 @@ import { CaptureOptions, CaptureRegimeOptions, CaptureTooLargeError, capture, ca
|
|
|
9
9
|
* chunks and renders the fallback.
|
|
10
10
|
*/
|
|
11
11
|
declare function serializePlate(merged: MergedPlate): Plate;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/merge.d.ts
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* genuinely-different DOM trees (mobile stacked vs desktop two-column) without
|
|
23
|
-
* stable keys proved unreliable: every heuristic mis-paired some nodes and
|
|
24
|
-
* remapped one view's rules onto another's elements, corrupting both. A lone
|
|
25
|
-
* capture is pixel-faithful; keeping each view lone preserves that. The cost
|
|
26
|
-
* is a larger plate (roughly the sum of the views) — bounded by the plate
|
|
27
|
-
* size cap — in exchange for correctness.
|
|
28
|
-
*
|
|
29
|
-
* Pure data-in/data-out — runs in node (plugin load) and in tests.
|
|
13
|
+
* Distinct plate names referenced by the tree's stitches, in first-seen order.
|
|
14
|
+
* Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
|
|
15
|
+
* gen-side stitch import graph straight off a `StoredPlate.tree`
|
|
16
|
+
* (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
|
|
17
|
+
* in the ADR 0022 cutover — it is the one node-side primitive that survived the
|
|
18
|
+
* `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
|
|
19
|
+
* in-browser `serializePlateIR`, project.ts).
|
|
30
20
|
*/
|
|
31
|
-
declare function mergeViews(file: PlateFile): MergedPlate;
|
|
32
|
-
/** Distinct plate names referenced by the tree's stitches, in first-seen order. */
|
|
33
21
|
declare function collectRefs(tree: RenderNode | null): string[];
|
|
34
|
-
/** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
|
|
35
|
-
declare function addCapture(file: PlateFile | null, name: string, capture: ViewCapture, breakpoints: readonly number[]): PlateFile;
|
|
36
22
|
//#endregion
|
|
37
23
|
//#region src/core.d.ts
|
|
38
24
|
/**
|
|
39
|
-
* Capture a live DOM subtree straight to a renderable Plate
|
|
40
|
-
*
|
|
41
|
-
* (
|
|
25
|
+
* Capture a live DOM subtree straight to a renderable Plate. Runs the same
|
|
26
|
+
* uniform pipeline the dev client does for a single View — measure (`captureView`),
|
|
27
|
+
* reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
|
|
28
|
+
* `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
|
|
29
|
+
* are the top-level elements to capture (a component's rendered roots). Pass the
|
|
30
|
+
* result to a `<Skeleton plate>`.
|
|
31
|
+
*
|
|
32
|
+
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
33
|
+
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
34
|
+
* threads through in dev, available here for driving capture directly. It is
|
|
35
|
+
* capture-only and never persisted into the returned Plate.
|
|
42
36
|
*/
|
|
43
37
|
declare function captureElement(roots: readonly Element[], options: CaptureOptions): Plate;
|
|
44
38
|
/**
|
|
@@ -52,4 +46,4 @@ declare function captureElement(roots: readonly Element[], options: CaptureOptio
|
|
|
52
46
|
*/
|
|
53
47
|
declare function renderPlateHtml(plate: Plate): string;
|
|
54
48
|
//#endregion
|
|
55
|
-
export { type CaptureOptions, type
|
|
49
|
+
export { type CaptureOptions, CaptureTooLargeError, type CaptureWalkerContext, type LeafKind, type MergedPlate, type Plate, type PlateNode, type ViewCapture, type WalkerDecision, type XrayCaptureWalker, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
|
package/dist/core.js
CHANGED
|
@@ -1,214 +1,28 @@
|
|
|
1
|
-
import { a as ROOT_ATTR,
|
|
2
|
-
import { i as
|
|
3
|
-
import {
|
|
4
|
-
//#region src/chunk.ts
|
|
5
|
-
/**
|
|
6
|
-
* Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
|
|
7
|
-
* 0008). This is the framework-neutral half of rendering: a skeleton is static,
|
|
8
|
-
* text-free DOM, so the structure is flattened to HTML strings here, at build,
|
|
9
|
-
* once — instead of every adapter walking a tree on the latency-critical first
|
|
10
|
-
* paint. The adapter only injects the strings and mounts a child plate at each
|
|
11
|
-
* stitch (see the React adapters). No escaping risk: bones carry only the
|
|
12
|
-
* content-addressed class tokens (ADR 0007), never user content.
|
|
13
|
-
*/
|
|
14
|
-
function nodeClass(node) {
|
|
15
|
-
return NODE_CLASS + (node.leaf ? ` ${LEAF_CLASS} ${LEAF_CLASS}-${node.leaf}` : "") + (node.cls ? ` ${node.cls}` : "");
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Whether this subtree holds a stitch (`ref`) anywhere — a stitched subtree
|
|
19
|
-
* can't collapse to a flat string, since a `ref` resolves to a child plate at
|
|
20
|
-
* render time and must stay a mount point.
|
|
21
|
-
*/
|
|
22
|
-
function hasStitch(node) {
|
|
23
|
-
return node.ref !== void 0 || (node.kids?.some(hasStitch) ?? false);
|
|
24
|
-
}
|
|
25
|
-
/** Serialize a stitch-free subtree to one HTML string (caller guarantees no `ref` below). */
|
|
26
|
-
function toHtml(node) {
|
|
27
|
-
const kids = node.kids ? node.kids.map(toHtml).join("") : "";
|
|
28
|
-
return `<div class="${nodeClass(node)}">${kids}</div>`;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Serialize a node list into chunks: each maximal stitch-free run becomes one
|
|
32
|
-
* HTML `string`; a `ref` node becomes `{ r }`; a node that isn't itself a stitch
|
|
33
|
-
* but holds one deeper stays a real element (`{ c, k }`) and recurses. Mirrors
|
|
34
|
-
* the element shape the runtime renderer would otherwise build per node.
|
|
35
|
-
*/
|
|
36
|
-
function toChunks(nodes) {
|
|
37
|
-
const out = [];
|
|
38
|
-
let buf = "";
|
|
39
|
-
const flush = () => {
|
|
40
|
-
if (buf) {
|
|
41
|
-
out.push(buf);
|
|
42
|
-
buf = "";
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
for (const node of nodes) {
|
|
46
|
-
if (!hasStitch(node)) {
|
|
47
|
-
buf += toHtml(node);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
flush();
|
|
51
|
-
if (node.ref !== void 0) out.push({ r: node.ref });
|
|
52
|
-
else out.push({
|
|
53
|
-
c: nodeClass(node),
|
|
54
|
-
k: toChunks(node.kids ?? [])
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
flush();
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The
|
|
62
|
-
* root's own content classes travel as `rootCls` (the adapter puts them on the
|
|
63
|
-
* root element); its children become `chunks`. A capture-less plate has null
|
|
64
|
-
* chunks and renders the fallback.
|
|
65
|
-
*/
|
|
66
|
-
function serializePlate(merged) {
|
|
67
|
-
const { v, name, tree, css } = merged;
|
|
68
|
-
if (!tree) return {
|
|
69
|
-
v,
|
|
70
|
-
name,
|
|
71
|
-
chunks: null,
|
|
72
|
-
css
|
|
73
|
-
};
|
|
74
|
-
const chunks = toChunks(tree.kids ?? []);
|
|
75
|
-
return tree.cls ? {
|
|
76
|
-
v,
|
|
77
|
-
name,
|
|
78
|
-
chunks,
|
|
79
|
-
rootCls: tree.cls,
|
|
80
|
-
css
|
|
81
|
-
} : {
|
|
82
|
-
v,
|
|
83
|
-
name,
|
|
84
|
-
chunks,
|
|
85
|
-
css
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
//#endregion
|
|
89
|
-
//#region src/merge.ts
|
|
90
|
-
/**
|
|
91
|
-
* Turn the per-view captures in a plate file into the one plate a Skeleton
|
|
92
|
-
* renders.
|
|
93
|
-
*
|
|
94
|
-
* Each view is kept whole — its own subtree, its own ids, its own rules —
|
|
95
|
-
* and shown only across the viewport range it owns, via `@media display:none`
|
|
96
|
-
* gates. We do NOT merge the views' trees into one. An earlier design (ADR
|
|
97
|
-
* 0004) did, sharing common structure and forking the rest, but aligning two
|
|
98
|
-
* genuinely-different DOM trees (mobile stacked vs desktop two-column) without
|
|
99
|
-
* stable keys proved unreliable: every heuristic mis-paired some nodes and
|
|
100
|
-
* remapped one view's rules onto another's elements, corrupting both. A lone
|
|
101
|
-
* capture is pixel-faithful; keeping each view lone preserves that. The cost
|
|
102
|
-
* is a larger plate (roughly the sum of the views) — bounded by the plate
|
|
103
|
-
* size cap — in exchange for correctness.
|
|
104
|
-
*
|
|
105
|
-
* Pure data-in/data-out — runs in node (plugin load) and in tests.
|
|
106
|
-
*/
|
|
107
|
-
function mergeViews(file) {
|
|
108
|
-
const views = file.views;
|
|
109
|
-
if (views.length === 0) return emptyPlate(file.name);
|
|
110
|
-
if (views.length === 1) {
|
|
111
|
-
const [only] = views;
|
|
112
|
-
if (!only) return emptyPlate(file.name);
|
|
113
|
-
const { tree, css } = classify(only.rules, only.tree, file.name);
|
|
114
|
-
return {
|
|
115
|
-
v: 1,
|
|
116
|
-
name: file.name,
|
|
117
|
-
tree,
|
|
118
|
-
css
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
const starts = views.map((r) => regimeFor(r.width, file.breakpoints).min ?? 0);
|
|
122
|
-
let nextId = 1;
|
|
123
|
-
const rootKids = [];
|
|
124
|
-
const rules = [];
|
|
125
|
-
views.forEach((view, i) => {
|
|
126
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
127
|
-
const kids = (view.tree.kids ?? []).map((kid) => cloneWithIds(kid, idMap, () => nextId++));
|
|
128
|
-
rootKids.push(...kids);
|
|
129
|
-
rules.push(...remapRules(view.rules, idMap));
|
|
130
|
-
const lo = i === 0 ? 0 : starts[i] ?? 0;
|
|
131
|
-
const hi = i === views.length - 1 ? Number.POSITIVE_INFINITY : starts[i + 1] ?? 0;
|
|
132
|
-
for (const kid of kids) {
|
|
133
|
-
if (lo > 0) rules.push(gate(kid.id, `(max-width: ${lo - .02}px)`));
|
|
134
|
-
if (hi !== Number.POSITIVE_INFINITY) rules.push(gate(kid.id, `(min-width: ${hi}px)`));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
const { tree, css } = classify(rules, {
|
|
138
|
-
id: 0,
|
|
139
|
-
kids: rootKids
|
|
140
|
-
}, file.name);
|
|
141
|
-
return {
|
|
142
|
-
v: 1,
|
|
143
|
-
name: file.name,
|
|
144
|
-
tree,
|
|
145
|
-
css
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
function gate(id, condition) {
|
|
149
|
-
return {
|
|
150
|
-
ids: [id],
|
|
151
|
-
decls: ["display: none !important"],
|
|
152
|
-
media: [condition],
|
|
153
|
-
spec: SPEC_GATE,
|
|
154
|
-
order: id
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/** Deep-clone a captured subtree into fresh ids, recording the mapping. */
|
|
158
|
-
function cloneWithIds(node, idMap, allocate) {
|
|
159
|
-
const id = allocate();
|
|
160
|
-
idMap.set(node.id, id);
|
|
161
|
-
const kids = node.kids?.map((kid) => cloneWithIds(kid, idMap, allocate));
|
|
162
|
-
return {
|
|
163
|
-
id,
|
|
164
|
-
...node.leaf ? { leaf: node.leaf } : {},
|
|
165
|
-
...node.ref !== void 0 ? { ref: node.ref } : {},
|
|
166
|
-
...kids ? { kids } : {}
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/** Distinct plate names referenced by the tree's stitches, in first-seen order. */
|
|
170
|
-
function collectRefs(tree) {
|
|
171
|
-
const names = [];
|
|
172
|
-
const seen = /* @__PURE__ */ new Set();
|
|
173
|
-
const visit = (node) => {
|
|
174
|
-
if (node.ref !== void 0 && !seen.has(node.ref)) {
|
|
175
|
-
seen.add(node.ref);
|
|
176
|
-
names.push(node.ref);
|
|
177
|
-
}
|
|
178
|
-
for (const kid of node.kids ?? []) visit(kid);
|
|
179
|
-
};
|
|
180
|
-
if (tree) visit(tree);
|
|
181
|
-
return names;
|
|
182
|
-
}
|
|
183
|
-
function remapRules(rules, idMap) {
|
|
184
|
-
return rules.map((rule) => ({
|
|
185
|
-
...rule,
|
|
186
|
-
ids: rule.ids.map((id) => idMap.get(id) ?? id)
|
|
187
|
-
}));
|
|
188
|
-
}
|
|
189
|
-
/** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
|
|
190
|
-
function addCapture(file, name, capture, breakpoints) {
|
|
191
|
-
const allBreakpoints = [...new Set([...file?.breakpoints ?? [], ...breakpoints])].toSorted((a, b) => a - b);
|
|
192
|
-
const spanKey = (width) => JSON.stringify(regimeFor(width, allBreakpoints));
|
|
193
|
-
const views = [...file?.views ?? [], capture];
|
|
194
|
-
const byView = /* @__PURE__ */ new Map();
|
|
195
|
-
for (const view of views) byView.set(spanKey(view.width), view);
|
|
196
|
-
return {
|
|
197
|
-
v: 1,
|
|
198
|
-
name,
|
|
199
|
-
breakpoints: allBreakpoints,
|
|
200
|
-
views: [...byView.values()].toSorted((a, b) => a.width - b.width)
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
1
|
+
import { a as ROOT_ATTR, d as emptyPlate, o as ROOT_CLASS, t as BASE_CSS } from "./plate-BRR6d8Se.js";
|
|
2
|
+
import { a as serializePlate, c as defineXrayCaptureWalker, i as collectRefs, l as runPlatePasses, o as CaptureTooLargeError, s as captureView, t as serializePlateIR } from "./project-DORoPAo7.js";
|
|
3
|
+
import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
|
|
204
4
|
//#region src/core.ts
|
|
205
5
|
/**
|
|
206
|
-
* Capture a live DOM subtree straight to a renderable Plate
|
|
207
|
-
*
|
|
208
|
-
* (
|
|
6
|
+
* Capture a live DOM subtree straight to a renderable Plate. Runs the same
|
|
7
|
+
* uniform pipeline the dev client does for a single View — measure (`captureView`),
|
|
8
|
+
* reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
|
|
9
|
+
* `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
|
|
10
|
+
* are the top-level elements to capture (a component's rendered roots). Pass the
|
|
11
|
+
* result to a `<Skeleton plate>`.
|
|
12
|
+
*
|
|
13
|
+
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
14
|
+
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
15
|
+
* threads through in dev, available here for driving capture directly. It is
|
|
16
|
+
* capture-only and never persisted into the returned Plate.
|
|
209
17
|
*/
|
|
210
18
|
function captureElement(roots, options) {
|
|
211
|
-
|
|
19
|
+
const plate = captureView(roots, {
|
|
20
|
+
walker: options.walker,
|
|
21
|
+
captureRootIsBoundary: options.captureRootIsBoundary
|
|
22
|
+
});
|
|
23
|
+
plate.name = options.name;
|
|
24
|
+
runPlatePasses(plate);
|
|
25
|
+
return serializePlate(serializePlateIR(plate));
|
|
212
26
|
}
|
|
213
27
|
/**
|
|
214
28
|
* Render a stitch-free Plate to a self-contained HTML string — the
|
|
@@ -222,9 +36,24 @@ function captureElement(roots, options) {
|
|
|
222
36
|
function renderPlateHtml(plate) {
|
|
223
37
|
const chunks = plate.chunks;
|
|
224
38
|
if (!chunks) return "";
|
|
225
|
-
if (chunks.length !== 1 || typeof chunks[0] !== "string") throw new Error("xray: renderPlateHtml renders stitch-free plates only — use a framework adapter for stitched plates");
|
|
226
39
|
const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
|
|
227
|
-
return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${plate.css}</style>${chunks
|
|
40
|
+
return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${escapeStyleText(plate.css)}</style>${chunksToHtml(chunks)}</div>`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Flatten chunks to one HTML string. A `string` concatenates; a template chunk
|
|
44
|
+
* (`{ t, n }`) expands to its cell repeated `n` times (the cell is stitch-free by
|
|
45
|
+
* construction); a stitch-free wrapper (`{ c, k }`) recurses inside a real `<div>`.
|
|
46
|
+
* A stitch (`{ r }`) resolves to a child plate at render and needs a framework
|
|
47
|
+
* adapter to mount, so a stitched plate throws here. Chunk HTML is xray-generated
|
|
48
|
+
* and interpolated raw, exactly as the single-string path was before.
|
|
49
|
+
*/
|
|
50
|
+
function chunksToHtml(chunks) {
|
|
51
|
+
let html = "";
|
|
52
|
+
for (const chunk of chunks) if (typeof chunk === "string") html += chunk;
|
|
53
|
+
else if ("t" in chunk) html += chunk.t.repeat(chunk.n);
|
|
54
|
+
else if ("r" in chunk) throw new Error("xray: renderPlateHtml renders stitch-free plates only — use a framework adapter for stitched plates");
|
|
55
|
+
else html += `<div class="${chunk.c}">${chunksToHtml(chunk.k)}</div>`;
|
|
56
|
+
return html;
|
|
228
57
|
}
|
|
229
58
|
//#endregion
|
|
230
|
-
export { CaptureTooLargeError,
|
|
59
|
+
export { CaptureTooLargeError, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/css-escape.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free raw-text safety for the one place a Plate's CSS string is
|
|
4
|
+
* interpolated into `<style>…</style>` markup.
|
|
5
|
+
*
|
|
6
|
+
* A plate's `css` is normally produced by the capture path (`filterLayoutDecls`
|
|
7
|
+
* + `classify`), which only ever emits well-formed `selector { prop: value }`
|
|
8
|
+
* rules — never a raw-text terminator. But a committed `plates/<name>.json` is
|
|
9
|
+
* UNTRUSTED at render time (it can be hand-edited or arrive over git, see the
|
|
10
|
+
* capture-input-validation plan's decisions), and a corrupt/malicious `css`
|
|
11
|
+
* could carry a `</style>` or an HTML comment delimiter. `<style>` is a
|
|
12
|
+
* raw-text element: the parser ends the element at the first `</style` it sees
|
|
13
|
+
* and resumes parsing the rest as ordinary HTML, so an unescaped terminator in
|
|
14
|
+
* the CSS body turns the tail of the plate into live markup.
|
|
15
|
+
*
|
|
16
|
+
* The React text-child path (`<style>{css}</style>`) is already safe — React
|
|
17
|
+
* escapes the child — but the two STRING paths interpolate `css` directly into
|
|
18
|
+
* a markup string (`renderPlateHtml` builds an HTML string; `react.core.tsx`
|
|
19
|
+
* feeds one to `dangerouslySetInnerHTML`). Those must run the body through this
|
|
20
|
+
* helper first.
|
|
21
|
+
*
|
|
22
|
+
* This lives in its own dependency-free module ON PURPOSE: it is imported by
|
|
23
|
+
* render/browser paths, so it must NOT pull in the Node-only validation code
|
|
24
|
+
* (valibot). It neutralizes only the sequences that can escape a raw-text
|
|
25
|
+
* `<style>` context — a `<` that begins `</style`, and the HTML comment / CDATA
|
|
26
|
+
* delimiters the tokenizer treats specially inside raw text — by inserting a
|
|
27
|
+
* CSS-comment break (`/**/`) that the CSS parser ignores. It does not, and is
|
|
28
|
+
* not meant to, sanitize CSS semantically; POST-time validation
|
|
29
|
+
* (`validate.ts`) is what rejects junk declarations.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Make a CSS body safe to interpolate into a `<style>…</style>` STRING. Breaks
|
|
35
|
+
* up the only sequences the HTML tokenizer acts on inside a raw-text `<style>`
|
|
36
|
+
* element, so none can terminate the element early or open a comment/CDATA run:
|
|
37
|
+
*
|
|
38
|
+
* - `</style` (the end-tag open, case-insensitive) — inserts the break between
|
|
39
|
+
* `<` and `/style` so the parser never matches the end tag.
|
|
40
|
+
* - `<!--`, `-->`, `<![CDATA[` — the comment/CDATA delimiters; split so neither
|
|
41
|
+
* a comment nor a CDATA section can open.
|
|
42
|
+
*
|
|
43
|
+
* The inserted `/**/` is an empty CSS comment: inert to the CSS parser (it
|
|
44
|
+
* cannot appear mid-token in a value we emit, and a stray one only ends a
|
|
45
|
+
* token early), so a well-formed plate is rendered byte-for-byte equivalently
|
|
46
|
+
* while a corrupt one can no longer break out of the style element.
|
|
47
|
+
*/
|
|
48
|
+
function escapeStyleText(css) {
|
|
49
|
+
return css.replace(/<(\/style)/gi, "</**/$1").replace(/<!--/g, "<!/**/--").replace(/-->/g, "--/**/>").replace(/<!\[CDATA\[/g, "<![/**/CDATA[");
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { escapeStyleText as t };
|
package/dist/hud.js
CHANGED
|
@@ -52,6 +52,7 @@ function installXrayHud() {
|
|
|
52
52
|
.bands { text-align: right }
|
|
53
53
|
.band.covered { color: #4ade80 }
|
|
54
54
|
.band.missing { color: #52525b }
|
|
55
|
+
.fixture { color: #fbbf24; margin-left: 6px }
|
|
55
56
|
.toggle { display: flex; gap: 6px; align-items: center; cursor: pointer; user-select: none }
|
|
56
57
|
.toggle input { accent-color: #4ade80; margin: 0 }
|
|
57
58
|
.action {
|
|
@@ -70,6 +71,8 @@ function installXrayHud() {
|
|
|
70
71
|
let extensionPresent = false;
|
|
71
72
|
let sweeping = false;
|
|
72
73
|
let sweepStatus = "";
|
|
74
|
+
let recording = false;
|
|
75
|
+
let recordStatus = "";
|
|
73
76
|
const render = () => {
|
|
74
77
|
panel.textContent = "";
|
|
75
78
|
const coverage = globalThis.__XRAY__?.coverage?.get();
|
|
@@ -82,6 +85,26 @@ function installXrayHud() {
|
|
|
82
85
|
if (capture) panel.append(toggleRow(capture, "Capture"));
|
|
83
86
|
const lightbox = globalThis.__XRAY__?.lightbox;
|
|
84
87
|
if (lightbox) panel.append(toggleRow(lightbox, "Light Box"));
|
|
88
|
+
const replay = globalThis.__XRAY__?.replay;
|
|
89
|
+
if (replay) panel.append(toggleRow(replay, "Replay"));
|
|
90
|
+
const fixtures = globalThis.__XRAY__?.fixtures;
|
|
91
|
+
if (fixtures) {
|
|
92
|
+
const button = document.createElement("button");
|
|
93
|
+
button.className = "action";
|
|
94
|
+
button.type = "button";
|
|
95
|
+
button.disabled = recording;
|
|
96
|
+
button.textContent = recording ? "Recording fixture…" : "Record fixture";
|
|
97
|
+
button.addEventListener("click", () => {
|
|
98
|
+
recordFixtures(fixtures);
|
|
99
|
+
});
|
|
100
|
+
panel.append(button);
|
|
101
|
+
if (recordStatus) {
|
|
102
|
+
const status = document.createElement("div");
|
|
103
|
+
status.className = "status";
|
|
104
|
+
status.textContent = recordStatus;
|
|
105
|
+
panel.append(status);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
85
108
|
if (globalThis.__XRAY__?.captureAllViews) if (extensionPresent) {
|
|
86
109
|
const button = document.createElement("button");
|
|
87
110
|
button.className = "action";
|
|
@@ -109,6 +132,12 @@ function installXrayHud() {
|
|
|
109
132
|
row.className = "plate";
|
|
110
133
|
const label = document.createElement("span");
|
|
111
134
|
label.textContent = name;
|
|
135
|
+
if (globalThis.__XRAY__?.fixtures?.has(name)) {
|
|
136
|
+
const tag = document.createElement("span");
|
|
137
|
+
tag.className = "fixture";
|
|
138
|
+
tag.textContent = "◆ fixture";
|
|
139
|
+
label.append(tag);
|
|
140
|
+
}
|
|
112
141
|
const bands = document.createElement("span");
|
|
113
142
|
bands.className = "bands";
|
|
114
143
|
for (const band of bandsOf(plateCoverage)) {
|
|
@@ -136,6 +165,21 @@ function installXrayHud() {
|
|
|
136
165
|
render();
|
|
137
166
|
}
|
|
138
167
|
}
|
|
168
|
+
async function recordFixtures(fixtures) {
|
|
169
|
+
if (recording || !fixtures) return;
|
|
170
|
+
recording = true;
|
|
171
|
+
recordStatus = "";
|
|
172
|
+
render();
|
|
173
|
+
try {
|
|
174
|
+
await fixtures.recordAll();
|
|
175
|
+
recordStatus = "Fixture recorded.";
|
|
176
|
+
} catch (error) {
|
|
177
|
+
recordStatus = `Failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
178
|
+
} finally {
|
|
179
|
+
recording = false;
|
|
180
|
+
render();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
139
183
|
const onHudMessage = (event) => {
|
|
140
184
|
if (event.source !== window) return;
|
|
141
185
|
if (isExtensionHello(event.data) && !extensionPresent) {
|
|
@@ -149,6 +193,7 @@ function installXrayHud() {
|
|
|
149
193
|
kind: "whois"
|
|
150
194
|
}, "*");
|
|
151
195
|
const unsubscribeCoverage = globalThis.__XRAY__?.coverage?.subscribe(render);
|
|
196
|
+
const unsubscribeFixtures = globalThis.__XRAY__?.fixtures?.subscribe(render);
|
|
152
197
|
render();
|
|
153
198
|
const mount = () => {
|
|
154
199
|
if (document.body) document.body.append(host);
|
|
@@ -158,6 +203,7 @@ function installXrayHud() {
|
|
|
158
203
|
return { dispose() {
|
|
159
204
|
window.removeEventListener("message", onHudMessage);
|
|
160
205
|
unsubscribeCoverage?.();
|
|
206
|
+
unsubscribeFixtures?.();
|
|
161
207
|
host.remove();
|
|
162
208
|
} };
|
|
163
209
|
}
|