@yahoo/uds-create-config 4.10.5 → 4.12.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.
Files changed (42) hide show
  1. package/dist/capture/capture-to-spec.d.ts +69 -0
  2. package/dist/capture/capture-to-spec.js +433 -0
  3. package/dist/capture/captured-tree.d.ts +65 -0
  4. package/dist/capture/captured-tree.js +201 -0
  5. package/dist/capture/class-grammar.d.ts +64 -0
  6. package/dist/capture/class-grammar.js +131 -0
  7. package/dist/capture/fixtures/tree-from-html.d.ts +3 -0
  8. package/dist/capture/icon-resolver.d.ts +59 -0
  9. package/dist/capture/icon-resolver.js +205 -0
  10. package/dist/capture/node-ops.d.ts +2 -0
  11. package/dist/capture/node-ops.js +146 -0
  12. package/dist/capture/node-patches.d.ts +32 -0
  13. package/dist/capture/node-patches.js +99 -0
  14. package/dist/configs/CanvasConfig.d.ts +1 -1
  15. package/dist/configs/CanvasConfig.js +3 -1
  16. package/dist/configs/react-native-system.d.ts +20 -20
  17. package/dist/configs/react-native-system.js +1 -1
  18. package/dist/configs/system.d.ts +4 -4
  19. package/dist/configs/system.js +2 -2
  20. package/dist/entities/native/NativeModifier.d.ts +1 -1
  21. package/dist/entities/native/NativeStyleProperty.d.ts +15 -15
  22. package/dist/entities/system/Component.js +3 -3
  23. package/dist/entities/system/Modifier.d.ts +2 -2
  24. package/dist/entities/system/copy-plan.js +1 -1
  25. package/dist/entities/system/element.d.ts +2 -1
  26. package/dist/entities/system/element.js +3 -1
  27. package/dist/entities/system/icon-library.js +1 -1
  28. package/dist/entities/system/link-plan.js +1 -1
  29. package/dist/framework/Config.js +1 -1
  30. package/dist/framework/defineEntity.js +1 -1
  31. package/dist/framework/inline-sources.js +1 -1
  32. package/dist/framework/plan.js +1 -1
  33. package/dist/framework/projections.js +2 -2
  34. package/dist/framework/render-spec.js +2 -2
  35. package/dist/index.d.ts +9 -4
  36. package/dist/index.js +13 -8
  37. package/dist/migrations/2.0.0/v1-artifact.d.ts +3 -3
  38. package/dist/spec/cell.js +1 -1
  39. package/dist/spec/prop-conditions.js +1 -1
  40. package/dist/spec/slot.js +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +2 -1
@@ -0,0 +1,69 @@
1
+ import { Config } from "../framework/Config.js";
2
+ import { CapturedNodeJson } from "./captured-tree.js";
3
+
4
+ //#region src/capture/capture-to-spec.d.ts
5
+ /**
6
+ * A rendered page (or one element of it) as a canvas spec.
7
+ *
8
+ * Every emitted component marks its layers with `data-uds-component` and `data-uds-layer`, and its
9
+ * classes follow the grammar the config derives, so the tree of components and most of their props
10
+ * read straight off the DOM. With the dev flag on, a root layer also stamps its primitive props as
11
+ * `data-uds-props` and each slot layer its slot name as `data-uds-slot`; those answer exactly and
12
+ * replace the guesses below. Without them, slot content that a hand-authored render placed directly
13
+ * is routed by DOM order into `children`, and a prop with no class or text (an `id`, a boolean the
14
+ * render never paints) is not recovered.
15
+ */
16
+ interface SpecElement {
17
+ [key: string]: unknown;
18
+ type: string;
19
+ props: Record<string, unknown>;
20
+ children?: string[];
21
+ slots?: Record<string, string[]>;
22
+ }
23
+ interface Spec {
24
+ [key: string]: unknown;
25
+ root: string;
26
+ meta: Record<string, unknown>;
27
+ elements: Record<string, SpecElement>;
28
+ }
29
+ interface CaptureToSpecOptions {
30
+ /** The captured page, as the host parsed it. */
31
+ readonly tree: CapturedNodeJson;
32
+ readonly config: Config;
33
+ /** CSS selector for the subtree to convert; the whole document by default. */
34
+ readonly selector?: string;
35
+ /** Wraps every top-level element under this root; required when the capture has several. */
36
+ readonly root?: {
37
+ type: string;
38
+ props?: Record<string, unknown>;
39
+ };
40
+ /** The spec's frame width, recorded on `meta`; what `100vw` means inside the frame. */
41
+ readonly width?: number;
42
+ /** The frame height, what `100vh` means inside the frame. */
43
+ readonly height?: number;
44
+ /** Children kept per element, so a long list stays a list; `0` keeps every child. */
45
+ readonly childCap?: number;
46
+ /** Names a glyph the DOM does not: a bare `<svg>`, or a renderer whose name is not stamped. */
47
+ readonly icons?: IconHook;
48
+ }
49
+ interface IconAnswer {
50
+ /** The component that renders the glyph, as the config paths it. */
51
+ readonly component: string;
52
+ readonly props: Record<string, unknown>;
53
+ }
54
+ interface IconHook {
55
+ /** Components that render a glyph themselves; one whose SVG cannot be named is dropped. */
56
+ readonly renderers: ReadonlySet<string>;
57
+ resolve(svg: {
58
+ readonly paths: readonly string[];
59
+ readonly width?: number;
60
+ }): IconAnswer | undefined;
61
+ }
62
+ interface CaptureToSpecResult {
63
+ readonly spec: Spec;
64
+ /** Icon components the SVG hook could not name; their elements are dropped. */
65
+ readonly unresolvedIcons: number;
66
+ }
67
+ declare function specFromCapture(options: CaptureToSpecOptions): CaptureToSpecResult;
68
+ //#endregion
69
+ export { CaptureToSpecOptions, CaptureToSpecResult, IconAnswer, IconHook, Spec, SpecElement, specFromCapture };
@@ -0,0 +1,433 @@
1
+ import { elementFromCaptured, isCapturedElement } from "./captured-tree.js";
2
+ import { classGrammar, normalizeCssValue } from "./class-grammar.js";
3
+ //#region src/capture/capture-to-spec.ts
4
+ /** Inline declarations that are rendering plumbing rather than a prop. */
5
+ const SKIP_INLINE_PROPERTY = new Set([
6
+ "display",
7
+ "visibility",
8
+ "box-sizing",
9
+ "touch-action",
10
+ "pointer-events",
11
+ "user-select",
12
+ "will-change",
13
+ "cursor",
14
+ "transition",
15
+ "transition-property",
16
+ "transition-duration",
17
+ "transition-timing-function",
18
+ "transition-delay",
19
+ "animation",
20
+ "outline",
21
+ "outline-style",
22
+ "outline-width",
23
+ "outline-color",
24
+ "outline-offset",
25
+ "border-image",
26
+ "border-image-source",
27
+ "border-image-slice",
28
+ "border-image-width",
29
+ "border-image-outset",
30
+ "border-image-repeat"
31
+ ]);
32
+ /** Browser defaults a shorthand such as `border: none` expands into. */
33
+ const SKIP_INLINE_VALUE = new Set([
34
+ "none",
35
+ "initial",
36
+ "inherit",
37
+ "unset",
38
+ "revert",
39
+ "medium",
40
+ "currentcolor",
41
+ "auto"
42
+ ]);
43
+ const VIEWPORT_VALUE = /\d(vh|vw|dvh|dvw|svh|svw|lvh|lvw|vmin|vmax)\b/;
44
+ /** A value that is nothing but the viewport: `100vh`, `100dvw`. */
45
+ const WHOLE_VIEWPORT = /^\d+(\.\d+)?(vh|vw|dvh|dvw|svh|svw|lvh|lvw)$/;
46
+ /** Viewport units in an expression as frame pixels; a unit with no frame size to measure stays. */
47
+ function frameUnits(value, width, height) {
48
+ return value.replace(/(\d+(?:\.\d+)?)(vw|dvw|svw|lvw|vh|dvh|svh|lvh)\b/g, (whole, amount, unit) => {
49
+ const size = unit.endsWith("vw") ? width : height;
50
+ if (size === void 0) return whole;
51
+ const px = Number(amount) / 100 * size;
52
+ return `${Number.isInteger(px) ? px : px.toFixed(2)}px`;
53
+ });
54
+ }
55
+ /** Runtime channels and DOM plumbing a stamp carries that a spec never authors. */
56
+ const SKIP_STAMPED = new Set([
57
+ "as",
58
+ "style",
59
+ "key",
60
+ "ref",
61
+ "tabIndex",
62
+ "role",
63
+ "className",
64
+ "layerProps",
65
+ "hidden",
66
+ "htmlFor",
67
+ "href",
68
+ "target",
69
+ "rel"
70
+ ]);
71
+ const ROOT_LAYER = "[data-uds-component][data-uds-layer=\"root\"]";
72
+ const DEFAULT_CHILD_CAP = 12;
73
+ const isElement = isCapturedElement;
74
+ function specFromCapture(options) {
75
+ const tree = elementFromCaptured(options.tree);
76
+ let scope = tree;
77
+ if (options.selector) scope = tree.closest(options.selector) ?? tree.querySelector(options.selector);
78
+ if (!scope) throw new Error(`Nothing matches "${options.selector}" in the capture.`);
79
+ const converter = new Converter(options, classGrammar(options.config));
80
+ const top = scope.closest(ROOT_LAYER) === scope || scope.tagName === "svg" ? converter.convertTop(scope) : converter.convertChildren(scope);
81
+ let rootKey;
82
+ if (options.root) {
83
+ rootKey = "root";
84
+ converter.elements.root = {
85
+ type: options.root.type,
86
+ props: { ...options.root.props ?? {} },
87
+ children: top
88
+ };
89
+ } else if (top.length === 1) rootKey = top[0];
90
+ else throw new Error(`The capture has ${top.length} top-level components; pass \`root\` to wrap them.`);
91
+ const meta = {};
92
+ if (options.width !== void 0) meta.width = options.width;
93
+ return {
94
+ spec: {
95
+ root: rootKey,
96
+ meta,
97
+ elements: reachable(converter.elements, rootKey)
98
+ },
99
+ unresolvedIcons: converter.unresolvedIcons
100
+ };
101
+ }
102
+ /** Only what the root reaches is an element; anything else was a branch the cap cut. */
103
+ function reachable(elements, root) {
104
+ const kept = {};
105
+ const stack = [root];
106
+ while (stack.length > 0) {
107
+ const key = stack.pop();
108
+ if (kept[key] || !elements[key]) continue;
109
+ kept[key] = elements[key];
110
+ stack.push(...elements[key].children ?? []);
111
+ for (const fills of Object.values(elements[key].slots ?? {})) stack.push(...fills);
112
+ }
113
+ return kept;
114
+ }
115
+ var Converter = class {
116
+ elements = {};
117
+ unresolvedIcons = 0;
118
+ counter = 0;
119
+ childCap;
120
+ constructor(options, grammar) {
121
+ this.options = options;
122
+ this.grammar = grammar;
123
+ this.childCap = options.childCap ?? DEFAULT_CHILD_CAP;
124
+ }
125
+ convertTop(el) {
126
+ return this.convert(el);
127
+ }
128
+ convertChildren(el) {
129
+ const out = [];
130
+ for (let i = 0; i < el.childNodes.length; i += 1) {
131
+ if (this.childCap > 0 && out.length >= this.childCap) break;
132
+ const node = el.childNodes[i];
133
+ if (isElement(node)) out.push(...this.convert(node));
134
+ }
135
+ return out;
136
+ }
137
+ /** The components under `layer`, skipping every other layer `path` marks inside it. */
138
+ convertOutsideLayers(layer, path) {
139
+ const out = [];
140
+ for (let i = 0; i < layer.childNodes.length; i += 1) {
141
+ if (this.childCap > 0 && out.length >= this.childCap) break;
142
+ const node = layer.childNodes[i];
143
+ if (!isElement(node)) continue;
144
+ if (node.getAttribute("data-uds-component") === path && node.getAttribute("data-uds-layer") !== "root") continue;
145
+ if (node.tagName.toLowerCase() === "svg" || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
146
+ else if (visible(node)) out.push(...this.convertOutsideLayers(node, path));
147
+ }
148
+ return out;
149
+ }
150
+ convert(el) {
151
+ if (!visible(el)) return [];
152
+ const path = el.getAttribute("data-uds-component");
153
+ if (path && el.getAttribute("data-uds-layer") === "root") {
154
+ const key = this.component(el, path);
155
+ return key ? [key] : [];
156
+ }
157
+ if (el.tagName.toLowerCase() === "svg") return this.icon(el);
158
+ return this.convertChildren(el);
159
+ }
160
+ /** A bare `<svg>` placed as the component its library renders it with. */
161
+ icon(svg) {
162
+ const answer = this.options.icons?.resolve(svgOf(svg));
163
+ if (!answer) {
164
+ this.unresolvedIcons += 1;
165
+ return [];
166
+ }
167
+ const key = this.key(answer.component);
168
+ this.elements[key] = {
169
+ type: `component:${answer.component}`,
170
+ props: { ...answer.props }
171
+ };
172
+ return [key];
173
+ }
174
+ key(path) {
175
+ this.counter += 1;
176
+ return `${(path.split("/").pop() ?? "el").toLowerCase().replace(/[^a-z0-9]+/g, "") || "el"}${this.counter}`;
177
+ }
178
+ component(el, path) {
179
+ const grammar = this.grammar.component(path);
180
+ const props = {};
181
+ const stamped = stampedProps(el);
182
+ if (grammar) this.propsFromClasses(el, grammar, props);
183
+ Object.assign(props, stamped);
184
+ if (grammar) this.propsFromInlineStyle(el, grammar, props);
185
+ if (props.position === "fixed") props.position = "absolute";
186
+ const renders = this.options.icons?.renderers.has(path) ?? false;
187
+ if (renders) {
188
+ const svg = el.tagName.toLowerCase() === "svg" ? el : el.querySelector("svg");
189
+ const answer = svg ? this.options.icons?.resolve(svgOf(svg)) : void 0;
190
+ if (answer) for (const [prop, value] of Object.entries(answer.props)) props[prop] ??= value;
191
+ else if (Object.keys(stamped).length === 0) {
192
+ this.unresolvedIcons += 1;
193
+ return;
194
+ }
195
+ }
196
+ const key = this.key(path);
197
+ const element = {
198
+ type: `component:${path}`,
199
+ props
200
+ };
201
+ const declared = grammar?.declared ?? {};
202
+ const slots = {};
203
+ for (const layer of asArray(el.querySelectorAll("[data-uds-slot]"))) {
204
+ if (nearestRoot(layer) !== el || hiddenBetween(layer, el)) continue;
205
+ const names = slotNames(layer).filter((slot) => !grammar || slot in declared);
206
+ const intoOf = (slot) => grammar?.layerProps.get(slot)?.into ?? "children";
207
+ const ownChildren = names.filter((slot) => intoOf(slot) === "children");
208
+ for (const slot of names) {
209
+ const into = intoOf(slot);
210
+ if (into === "children" && slot !== ownChildren.at(-1)) continue;
211
+ const holder = into === "children" ? layer : slotLayerNamed(layer, into);
212
+ if (!holder) continue;
213
+ const fills = holder === layer ? this.convertOutsideLayers(layer, path) : this.convertChildren(holder);
214
+ if (fills.length > 0) slots[slot] = fills;
215
+ else if (holder !== layer) {
216
+ const text = collapse(ownText(holder));
217
+ if (text) props[slot] = text;
218
+ }
219
+ }
220
+ }
221
+ const claimed = /* @__PURE__ */ new Set();
222
+ for (const [prop, route] of grammar?.layerProps ?? []) {
223
+ if (props[prop] !== void 0 || slots[prop]) continue;
224
+ const layer = layerOf(el, path, route.layer);
225
+ if (!layer || !visible(layer) || hiddenBetween(layer, el)) continue;
226
+ const holdsComponents = layer.querySelector(`${ROOT_LAYER}, svg`) !== null;
227
+ if (!route.content && holdsComponents) continue;
228
+ claimed.add(layer);
229
+ if (route.content && holdsComponents) {
230
+ const fills = this.convertOutsideLayers(layer, path);
231
+ if (fills.length > 0) slots[prop] = fills;
232
+ } else {
233
+ const text = collapse(ownText(layer, claimed));
234
+ if (text) props[prop] = text;
235
+ }
236
+ }
237
+ const loose = [];
238
+ const looseSelector = renders ? ROOT_LAYER : `${ROOT_LAYER}, svg`;
239
+ for (const nested of asArray(el.querySelectorAll(looseSelector))) {
240
+ if (this.childCap > 0 && loose.length >= this.childCap) break;
241
+ if (nearestRoot(nested) !== el) continue;
242
+ if (hiddenBetween(nested, el)) continue;
243
+ const stampedLayer = nested.parentElement?.closest("[data-uds-slot]") ?? null;
244
+ if (stampedLayer && nearestRoot(stampedLayer) === el) continue;
245
+ if (withinAny(nested, claimed, el)) continue;
246
+ loose.push(...this.convert(nested));
247
+ }
248
+ if (loose.length > 0 && !slots.children) slots.children = loose;
249
+ if (Object.keys(slots).length > 0) element.slots = slots;
250
+ const aria = el.getAttribute("aria-label");
251
+ if (aria) {
252
+ if ("tooltip" in declared && props.tooltip === void 0) props.tooltip = aria;
253
+ props["aria-label"] = aria;
254
+ }
255
+ const textSlot = declared.children;
256
+ if (textSlot?.type === "slot" && textSlot.text && props.children === void 0 && !slots.children) {
257
+ const text = collapse(ownText(el, claimed));
258
+ if (text) props.children = text;
259
+ }
260
+ if (el.getAttribute("aria-selected") === "true" || el.getAttribute("aria-pressed") === "true") {
261
+ if ("selected" in declared) props.selected = true;
262
+ }
263
+ if (el.getAttribute("aria-checked") === "true") {
264
+ if ("checked" in declared) props.checked = true;
265
+ else if ("selected" in declared) props.selected = true;
266
+ }
267
+ if (grammar) {
268
+ for (const prop of Object.keys(props)) if (!(prop in declared) && prop !== "aria-label") delete props[prop];
269
+ }
270
+ this.elements[key] = element;
271
+ return key;
272
+ }
273
+ propsFromClasses(el, grammar, props) {
274
+ for (const cls of classesOf(el)) {
275
+ const condition = grammar.conditions.get(cls);
276
+ if (condition) {
277
+ Object.assign(props, condition);
278
+ continue;
279
+ }
280
+ const composite = grammar.composites.get(cls);
281
+ if (composite) {
282
+ props[composite.prop] = composite.value;
283
+ continue;
284
+ }
285
+ const style = this.grammar.styleProp(cls);
286
+ if (style) {
287
+ const prop = grammar.propForStyleProperty.get(style.path);
288
+ if (prop && props[prop] === void 0) props[prop] = this.frameValue(style.path, style.leaf);
289
+ }
290
+ }
291
+ }
292
+ /**
293
+ * A leaf as it reads inside a frame: a viewport-sized value (`100vh`) measures the canvas, so it
294
+ * becomes the leaf that fills the container instead.
295
+ */
296
+ frameValue(path, leaf) {
297
+ if (leaf === "") return true;
298
+ if (!this.grammar.viewportLeaf(path, leaf)) return leaf;
299
+ return this.grammar.fullLeaf(path) ?? leaf;
300
+ }
301
+ /**
302
+ * The element's inline styles as props: a component's own `style` is the one place a size the
303
+ * classes do not carry lives, so `width: 300px` becomes the declared width prop with `[300px]`,
304
+ * and `column-gap: var(--uds-spacing-2)` becomes the gap leaf `2`. A value with no leaf lands as
305
+ * an off-scale `[value]` only on a property that takes one. Rendering plumbing (display, box
306
+ * sizing, transitions, browser defaults) is not a prop and is skipped.
307
+ */
308
+ propsFromInlineStyle(el, grammar, props) {
309
+ const style = el.getAttribute("style");
310
+ if (!style) return;
311
+ for (const declaration of style.split(";")) {
312
+ const at = declaration.indexOf(":");
313
+ if (at < 0) continue;
314
+ const property = declaration.slice(0, at).trim().toLowerCase();
315
+ let value = normalizeCssValue(declaration.slice(at + 1));
316
+ if (!value || SKIP_INLINE_PROPERTY.has(property) || SKIP_INLINE_VALUE.has(value)) continue;
317
+ if (property === "position" && value === "fixed") value = "absolute";
318
+ if (VIEWPORT_VALUE.test(value) && !WHOLE_VIEWPORT.test(value)) value = frameUnits(value, this.options.width, this.options.height);
319
+ for (const option of this.grammar.stylePropsFor(property)) {
320
+ const prop = grammar.propForStyleProperty.get(option.path);
321
+ if (!prop) continue;
322
+ let next;
323
+ const leaf = option.leafFor(value);
324
+ if (WHOLE_VIEWPORT.test(value)) next = option.fullLeaf;
325
+ else if (leaf !== void 0) next = leaf === "" ? true : leaf;
326
+ else if (option.arbitrary) next = `[${value.replaceAll(" ", "_")}]`;
327
+ if (next === void 0) continue;
328
+ props[prop] = next;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ };
334
+ function stampedProps(el) {
335
+ const raw = el.getAttribute("data-uds-props");
336
+ if (!raw) return {};
337
+ let parsed;
338
+ try {
339
+ parsed = JSON.parse(raw);
340
+ } catch {
341
+ return {};
342
+ }
343
+ if (!parsed || typeof parsed !== "object") return {};
344
+ const out = {};
345
+ for (const [key, value] of Object.entries(parsed)) {
346
+ if (SKIP_STAMPED.has(key)) continue;
347
+ if (key.startsWith("data-") || key.startsWith("aria-") || key.startsWith("on")) continue;
348
+ out[key] = value;
349
+ }
350
+ return out;
351
+ }
352
+ /** The props a slot layer's marker names. */
353
+ function slotNames(layer) {
354
+ return (layer.getAttribute("data-uds-slot") ?? "").split(/\s+/).filter(Boolean);
355
+ }
356
+ /** The slot layer inside `layer` whose marker names `slot`, with no other slot layer between. */
357
+ function slotLayerNamed(layer, slot) {
358
+ return asArray(layer.querySelectorAll("[data-uds-slot]")).find((candidate) => slotNames(candidate).includes(slot) && candidate.parentElement?.closest("[data-uds-slot]") === layer);
359
+ }
360
+ function nearestRoot(el) {
361
+ return el.parentElement?.closest(ROOT_LAYER) ?? null;
362
+ }
363
+ function visible(el) {
364
+ const style = el.getAttribute("style") ?? "";
365
+ if (/display:\s*none/.test(style) || /visibility:\s*hidden/.test(style)) return false;
366
+ if (/(^|;)\s*opacity:\s*0+(\.0+)?\s*(;|$)/.test(style)) return false;
367
+ if (el.hasAttribute("hidden")) return false;
368
+ if (el.getAttribute("aria-hidden") === "true" && el.tagName.toLowerCase() !== "svg") return false;
369
+ if (el.getAttribute("role") === "tooltip") return false;
370
+ if (el.getAttribute("data-uds-props")?.includes("\"hidden\":true")) return false;
371
+ return true;
372
+ }
373
+ function classesOf(el) {
374
+ return (el.getAttribute("class") ?? "").split(/\s+/).filter(Boolean);
375
+ }
376
+ function ownText(el, skip) {
377
+ let text = "";
378
+ for (let i = 0; i < el.childNodes.length; i += 1) {
379
+ const node = el.childNodes[i];
380
+ if (!isElement(node)) {
381
+ text += node.textContent ?? "";
382
+ continue;
383
+ }
384
+ if (skip?.has(node)) continue;
385
+ if (node.hasAttribute("data-uds-slot")) continue;
386
+ if (node.getAttribute("data-uds-layer") === "root" && node.hasAttribute("data-uds-component")) continue;
387
+ if (!visible(node)) continue;
388
+ text += ownText(node, skip);
389
+ }
390
+ return text;
391
+ }
392
+ /** Whether an element between `el` and its component root `root` is hidden. */
393
+ function hiddenBetween(el, root) {
394
+ let node = el.parentElement;
395
+ while (node && node !== root) {
396
+ if (!visible(node)) return true;
397
+ node = node.parentElement;
398
+ }
399
+ return false;
400
+ }
401
+ /** Whether `el` sits inside one of `layers`, below `root`. */
402
+ function withinAny(el, layers, root) {
403
+ let node = el.parentElement;
404
+ while (node && node !== root) {
405
+ if (layers.has(node)) return true;
406
+ node = node.parentElement;
407
+ }
408
+ return false;
409
+ }
410
+ /** The component's own layer named `layer`, when the DOM marks one under this root. */
411
+ function layerOf(root, path, layer) {
412
+ for (const candidate of asArray(root.querySelectorAll(`[data-uds-component="${path}"][data-uds-layer="${layer}"]`))) if (nearestRoot(candidate) === root) return candidate;
413
+ }
414
+ function svgOf(svg) {
415
+ return {
416
+ paths: asArray(svg.querySelectorAll("path")).map((p) => p.getAttribute("d") ?? "").filter(Boolean),
417
+ width: numberAttr(svg, "width")
418
+ };
419
+ }
420
+ function collapse(text) {
421
+ return text.replace(/\s+/g, " ").trim();
422
+ }
423
+ function numberAttr(el, name) {
424
+ const raw = el.getAttribute(name);
425
+ if (raw === null) return void 0;
426
+ const value = Number.parseFloat(raw);
427
+ return Number.isFinite(value) ? value : void 0;
428
+ }
429
+ function asArray(list) {
430
+ return Array.from(list);
431
+ }
432
+ //#endregion
433
+ export { specFromCapture };
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/capture/captured-tree.d.ts
4
+ /**
5
+ * A captured page as plain data: the tree a host hands the `node/from-html` op. Each node is a tag,
6
+ * its attributes and its children, text included, which is everything the converter reads off a
7
+ * rendered screen: the component and layer markers, the class list, the inline style, the stamped
8
+ * props and the text. Parsing markup into this shape is the host's job, so the config package
9
+ * needs no DOM.
10
+ */
11
+ interface CapturedNodeJson {
12
+ readonly tag: string;
13
+ readonly attrs?: Readonly<Record<string, string>>;
14
+ readonly children?: ReadonlyArray<string | CapturedNodeJson>;
15
+ }
16
+ declare const CapturedNode: z.ZodType<CapturedNodeJson>;
17
+ declare class CapturedText {
18
+ readonly textContent: string;
19
+ readonly parentElement: CapturedElement | null;
20
+ readonly nodeType = 3;
21
+ constructor(textContent: string, parentElement: CapturedElement | null);
22
+ }
23
+ type CapturedChild = CapturedElement | CapturedText;
24
+ /** The slice of the DOM element API the converter walks, over captured data. */
25
+ declare class CapturedElement {
26
+ private readonly attrs;
27
+ readonly nodeType = 1;
28
+ readonly tagName: string;
29
+ readonly childNodes: CapturedChild[];
30
+ parentElement: CapturedElement | null;
31
+ constructor(tag: string, attrs: Readonly<Record<string, string>>);
32
+ getAttribute(name: string): string | null;
33
+ hasAttribute(name: string): boolean;
34
+ get attributes(): ReadonlyArray<{
35
+ name: string;
36
+ value: string;
37
+ }>;
38
+ get textContent(): string;
39
+ get children(): CapturedElement[];
40
+ querySelectorAll(selector: string): CapturedElement[];
41
+ querySelector(selector: string): CapturedElement | null;
42
+ closest(selector: string): CapturedElement | null;
43
+ /** Back to data, for a host that rewrites part of a tree. */
44
+ toJSON(): CapturedNodeJson;
45
+ }
46
+ /** A captured tree as elements the converter can walk. */
47
+ declare function elementFromCaptured(node: CapturedNodeJson, parent?: CapturedElement | null): CapturedElement;
48
+ /** The subset of a DOM element a host's parser exposes, whichever DOM it is. */
49
+ interface DomLikeElement {
50
+ readonly nodeType: number;
51
+ readonly tagName?: string;
52
+ readonly textContent?: string | null;
53
+ readonly attributes?: ArrayLike<{
54
+ readonly name: string;
55
+ readonly value: string;
56
+ }>;
57
+ readonly childNodes?: ArrayLike<DomLikeElement>;
58
+ }
59
+ /**
60
+ * A captured tree from a parsed DOM element, for a host: scripts, styles and templates carry no
61
+ * rendered content and are left out, as is whitespace-only text.
62
+ */
63
+ declare function capturedFromDom(el: DomLikeElement): CapturedNodeJson;
64
+ //#endregion
65
+ export { CapturedChild, CapturedElement, CapturedNode, CapturedNodeJson, CapturedText, DomLikeElement, capturedFromDom, elementFromCaptured };