@yahoo/uds-create-config 4.11.0 → 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,201 @@
1
+ import { z } from "zod";
2
+ //#region src/capture/captured-tree.ts
3
+ const CapturedNode = z.lazy(() => z.object({
4
+ tag: z.string().min(1),
5
+ attrs: z.record(z.string(), z.string()).optional(),
6
+ children: z.array(z.union([z.string(), CapturedNode])).optional()
7
+ }));
8
+ const ELEMENT_NODE = 1;
9
+ const TEXT_NODE = 3;
10
+ var CapturedText = class {
11
+ nodeType = TEXT_NODE;
12
+ constructor(textContent, parentElement) {
13
+ this.textContent = textContent;
14
+ this.parentElement = parentElement;
15
+ }
16
+ };
17
+ /** The slice of the DOM element API the converter walks, over captured data. */
18
+ var CapturedElement = class {
19
+ nodeType = ELEMENT_NODE;
20
+ tagName;
21
+ childNodes = [];
22
+ parentElement = null;
23
+ constructor(tag, attrs) {
24
+ this.attrs = attrs;
25
+ this.tagName = tag.toLowerCase();
26
+ }
27
+ getAttribute(name) {
28
+ return this.attrs[name] ?? null;
29
+ }
30
+ hasAttribute(name) {
31
+ return name in this.attrs;
32
+ }
33
+ get attributes() {
34
+ return Object.entries(this.attrs).map(([name, value]) => ({
35
+ name,
36
+ value
37
+ }));
38
+ }
39
+ get textContent() {
40
+ return this.childNodes.map((node) => node.textContent).join("");
41
+ }
42
+ get children() {
43
+ return this.childNodes.filter(isCapturedElement);
44
+ }
45
+ querySelectorAll(selector) {
46
+ const matchers = parseSelector(selector);
47
+ const out = [];
48
+ const walk = (el) => {
49
+ for (const child of el.children) {
50
+ if (matchers.some((m) => m(child))) out.push(child);
51
+ walk(child);
52
+ }
53
+ };
54
+ walk(this);
55
+ return out;
56
+ }
57
+ querySelector(selector) {
58
+ const matchers = parseSelector(selector);
59
+ const walk = (el) => {
60
+ for (const child of el.children) {
61
+ if (matchers.some((m) => m(child))) return child;
62
+ const hit = walk(child);
63
+ if (hit) return hit;
64
+ }
65
+ return null;
66
+ };
67
+ return walk(this);
68
+ }
69
+ closest(selector) {
70
+ const matchers = parseSelector(selector);
71
+ let el = this;
72
+ while (el) {
73
+ if (matchers.some((m) => m(el))) return el;
74
+ el = el.parentElement;
75
+ }
76
+ return null;
77
+ }
78
+ /** Back to data, for a host that rewrites part of a tree. */
79
+ toJSON() {
80
+ return {
81
+ tag: this.tagName,
82
+ ...Object.keys(this.attrs).length > 0 ? { attrs: { ...this.attrs } } : {},
83
+ ...this.childNodes.length > 0 ? { children: this.childNodes.map((node) => isCapturedElement(node) ? node.toJSON() : node.textContent) } : {}
84
+ };
85
+ }
86
+ };
87
+ function isCapturedElement(node) {
88
+ return node.nodeType === ELEMENT_NODE;
89
+ }
90
+ /** A captured tree as elements the converter can walk. */
91
+ function elementFromCaptured(node, parent = null) {
92
+ const el = new CapturedElement(node.tag, node.attrs ?? {});
93
+ el.parentElement = parent;
94
+ for (const child of node.children ?? []) el.childNodes.push(typeof child === "string" ? new CapturedText(child, el) : elementFromCaptured(child, el));
95
+ return el;
96
+ }
97
+ /**
98
+ * A captured tree from a parsed DOM element, for a host: scripts, styles and templates carry no
99
+ * rendered content and are left out, as is whitespace-only text.
100
+ */
101
+ function capturedFromDom(el) {
102
+ const attrs = {};
103
+ for (const attribute of Array.from(el.attributes ?? [])) attrs[attribute.name] = attribute.value;
104
+ const children = [];
105
+ for (const child of Array.from(el.childNodes ?? [])) {
106
+ if (child.nodeType === TEXT_NODE) {
107
+ const text = child.textContent ?? "";
108
+ if (text.trim()) children.push(text);
109
+ continue;
110
+ }
111
+ if (child.nodeType !== ELEMENT_NODE) continue;
112
+ const tag = (child.tagName ?? "").toLowerCase();
113
+ if (SKIPPED_TAGS.has(tag)) continue;
114
+ children.push(capturedFromDom(child));
115
+ }
116
+ return {
117
+ tag: (el.tagName ?? "div").toLowerCase(),
118
+ ...Object.keys(attrs).length > 0 ? { attrs } : {},
119
+ ...children.length > 0 ? { children } : {}
120
+ };
121
+ }
122
+ const SKIPPED_TAGS = new Set([
123
+ "script",
124
+ "style",
125
+ "noscript",
126
+ "template",
127
+ "link"
128
+ ]);
129
+ const selectorCache = /* @__PURE__ */ new Map();
130
+ function parseSelector(selector) {
131
+ const cached = selectorCache.get(selector);
132
+ if (cached) return cached;
133
+ const matchers = selector.split(",").map((part) => compound(part.trim()));
134
+ selectorCache.set(selector, matchers);
135
+ return matchers;
136
+ }
137
+ const isTagStart = (ch) => /[a-zA-Z]/.test(ch);
138
+ const isTagChar = (ch) => /[\w-]/.test(ch);
139
+ /**
140
+ * One compound selector, read left to right in one pass: an optional tag, then any number of
141
+ * `[name]` or `[name=value]` tests, the value bare or quoted. A single scan rather than a pattern,
142
+ * since a pattern with nested repetition over `[` backtracks polynomially on a malformed selector.
143
+ */
144
+ function parseCompound(selector) {
145
+ const unsupported = () => /* @__PURE__ */ new Error(`Unsupported selector "${selector}".`);
146
+ let at = 0;
147
+ let tag;
148
+ if (at < selector.length && isTagStart(selector[at])) {
149
+ const start = at;
150
+ at += 1;
151
+ while (at < selector.length && isTagChar(selector[at])) at += 1;
152
+ tag = selector.slice(start, at).toLowerCase();
153
+ }
154
+ const attributes = [];
155
+ while (at < selector.length) {
156
+ if (selector[at] !== "[") throw unsupported();
157
+ at += 1;
158
+ const nameStart = at;
159
+ while (at < selector.length && selector[at] !== "]" && selector[at] !== "=") at += 1;
160
+ if (at === nameStart || at === selector.length) throw unsupported();
161
+ const name = selector.slice(nameStart, at);
162
+ let value;
163
+ if (selector[at] === "=") {
164
+ at += 1;
165
+ const quote = selector[at];
166
+ if (quote === "\"" || quote === "'") {
167
+ const close = selector.indexOf(quote, at + 1);
168
+ if (close === -1) throw unsupported();
169
+ value = selector.slice(at + 1, close);
170
+ at = close + 1;
171
+ } else {
172
+ const close = selector.indexOf("]", at);
173
+ if (close === -1) throw unsupported();
174
+ value = selector.slice(at, close);
175
+ at = close;
176
+ }
177
+ }
178
+ if (selector[at] !== "]") throw unsupported();
179
+ at += 1;
180
+ attributes.push(value === void 0 ? { name } : {
181
+ name,
182
+ value
183
+ });
184
+ }
185
+ return {
186
+ tag,
187
+ attributes
188
+ };
189
+ }
190
+ function compound(selector) {
191
+ const { tag, attributes } = parseCompound(selector);
192
+ return (el) => {
193
+ if (tag && el.tagName !== tag) return false;
194
+ for (const { name, value } of attributes) if (value === void 0) {
195
+ if (!el.hasAttribute(name)) return false;
196
+ } else if (el.getAttribute(name) !== value) return false;
197
+ return true;
198
+ };
199
+ }
200
+ //#endregion
201
+ export { CapturedNode, capturedFromDom, elementFromCaptured, isCapturedElement };
@@ -0,0 +1,64 @@
1
+ import { Config } from "../framework/Config.js";
2
+
3
+ //#region src/capture/class-grammar.d.ts
4
+ /**
5
+ * The class grammar an emitted component wears, inverted: the same config-derived functions the
6
+ * emitter spells classes with, read the other way. A style value's class comes from
7
+ * `stylePropClasses`, a style rule's condition class from `componentRuleClasses`, and a
8
+ * composite-typed prop's class from the composite's own `className`, so a captured class maps back
9
+ * to the prop and value that produced it without a hand-written table.
10
+ */
11
+ interface StylePropMatch {
12
+ /** The style property the class belongs to. */
13
+ readonly path: string;
14
+ /** The value as a call site spells it (`carbon/5`, `1.5`). */
15
+ readonly leaf: string;
16
+ }
17
+ interface ComponentGrammar {
18
+ /** A style rule's condition class → the prop values it stands for. */
19
+ readonly conditions: ReadonlyMap<string, Record<string, unknown>>;
20
+ /** A composite class on the root layer → the composite-typed prop and its value. */
21
+ readonly composites: ReadonlyMap<string, {
22
+ prop: string;
23
+ value: string;
24
+ }>;
25
+ /** The style property each declared style prop writes, inverted: style property path → prop name. */
26
+ readonly propForStyleProperty: ReadonlyMap<string, string>;
27
+ /** Every prop the component declares or forwards, by name. */
28
+ readonly declared: Readonly<Record<string, unknown>>;
29
+ /**
30
+ * Props read off one of the component's layers: a forward routed into a layer (`content`, landing
31
+ * on the prop `into` names, `children` unless the route says otherwise), and a string prop named
32
+ * like a layer, which reads that layer's text.
33
+ */
34
+ readonly layerProps: ReadonlyMap<string, {
35
+ layer: string;
36
+ content: boolean;
37
+ into?: string;
38
+ }>;
39
+ /** Layer classes → layer name. */
40
+ readonly layers: ReadonlyMap<string, string>;
41
+ }
42
+ /** A style property that writes one CSS property, read back from a CSS declaration. */
43
+ interface CssPropertyMatch {
44
+ readonly path: string;
45
+ /** Whether the property takes an off-scale `[value]`. */
46
+ readonly arbitrary: boolean;
47
+ /** The leaf whose value is this CSS value, as `var(--…)` or as the literal it resolves to. */
48
+ leafFor(cssValue: string): string | undefined;
49
+ /** The leaf that fills the container (`100%`), which is what a viewport unit means inside a frame. */
50
+ readonly fullLeaf: string | undefined;
51
+ }
52
+ interface ClassGrammar {
53
+ styleProp(cls: string): StylePropMatch | undefined;
54
+ /** The style properties that write `cssProperty`, the ones writing only it first. */
55
+ stylePropsFor(cssProperty: string): readonly CssPropertyMatch[];
56
+ /** Whether a leaf's value is in viewport units, which measure the canvas rather than the frame. */
57
+ viewportLeaf(path: string, leaf: string): boolean;
58
+ /** The leaf of a style property whose value fills the container (`100%`). */
59
+ fullLeaf(path: string): string | undefined;
60
+ component(path: string): ComponentGrammar | undefined;
61
+ }
62
+ declare function classGrammar(config: Config): ClassGrammar;
63
+ //#endregion
64
+ export { ClassGrammar, classGrammar };
@@ -0,0 +1,131 @@
1
+ import { isRef, pathOf } from "../framework/utils/refs.js";
2
+ import { className, cssVarRef, resolveComponentProps, resolveTokenValue, stylePropEntries } from "../framework/projections.js";
3
+ import { componentLayerClass, componentRuleClasses, stylePropClasses } from "../framework/class-names.js";
4
+ import { intoName, routesContent } from "../framework/prop-surface.js";
5
+ //#region src/capture/class-grammar.ts
6
+ const VIEWPORT_UNIT = /\d(vh|vw|dvh|dvw|svh|svw|lvh|lvw|vmin|vmax)\b/;
7
+ /** `0px` → `0`, collapsed spaces, lowercase keywords; what two spellings of one value share. */
8
+ function normalizeCssValue(value) {
9
+ return value.trim().replace(/\s+/g, " ").replace(/(^|[\s,(])0(?:px|rem|em|%)(?=$|[\s,)])/g, "$10").toLowerCase();
10
+ }
11
+ function classGrammar(config) {
12
+ const styleProps = /* @__PURE__ */ new Map();
13
+ for (const entity of config.list("style-property")) for (const [leaf, cls] of stylePropClasses(config, entity.path)) if (!styleProps.has(cls)) styleProps.set(cls, {
14
+ path: entity.path,
15
+ leaf
16
+ });
17
+ const composites = config.list("composite").map((entity) => ({
18
+ path: entity.path,
19
+ cls: className(config, "composite", entity.path)
20
+ }));
21
+ const byCss = /* @__PURE__ */ new Map();
22
+ const leafValues = /* @__PURE__ */ new Map();
23
+ const valuesOf = (path) => {
24
+ let known = leafValues.get(path);
25
+ if (known) return known;
26
+ known = /* @__PURE__ */ new Map();
27
+ for (const entry of stylePropEntries(config, path)) {
28
+ const spellings = [];
29
+ if (config.resolve("token", entry.value)) {
30
+ spellings.push(normalizeCssValue(cssVarRef(config, "token", entry.value)));
31
+ const literal = resolveTokenValue(config, entry.value);
32
+ if (typeof literal === "string") spellings.push(normalizeCssValue(literal));
33
+ } else spellings.push(normalizeCssValue(String(entry.value)));
34
+ known.set(entry.leaf, spellings);
35
+ }
36
+ leafValues.set(path, known);
37
+ return known;
38
+ };
39
+ const matchFor = (path, arbitrary) => ({
40
+ path,
41
+ arbitrary,
42
+ leafFor(cssValue) {
43
+ const wanted = normalizeCssValue(cssValue);
44
+ for (const [leaf, spellings] of valuesOf(path)) if (spellings.includes(wanted)) return leaf;
45
+ },
46
+ get fullLeaf() {
47
+ for (const [leaf, spellings] of valuesOf(path)) if (spellings.includes("100%")) return leaf;
48
+ }
49
+ });
50
+ for (const entity of config.list("style-property")) {
51
+ const body = entity.toJSON();
52
+ const properties = body.properties ?? [];
53
+ for (const property of properties) {
54
+ const list = byCss.get(property) ?? [];
55
+ list.push(matchFor(entity.path, body.arbitrary === true));
56
+ byCss.set(property, list);
57
+ }
58
+ }
59
+ const propertyCount = /* @__PURE__ */ new Map();
60
+ for (const entity of config.list("style-property")) propertyCount.set(entity.path, (entity.toJSON().properties ?? []).length);
61
+ for (const list of byCss.values()) list.sort((a, b) => (propertyCount.get(a.path) ?? 0) - (propertyCount.get(b.path) ?? 0));
62
+ const components = /* @__PURE__ */ new Map();
63
+ const componentGrammar = (path) => {
64
+ const component = config.resolve("component", path);
65
+ if (!component) return void 0;
66
+ const declared = resolveComponentProps(config, path);
67
+ const conditions = /* @__PURE__ */ new Map();
68
+ for (const rule of componentRuleClasses(config, path)) conditions.set(rule.className, rule.when);
69
+ const compositeProps = /* @__PURE__ */ new Map();
70
+ const propForStyleProperty = /* @__PURE__ */ new Map();
71
+ for (const [prop, declaration] of Object.entries(declared)) {
72
+ if (!declaration || typeof declaration !== "object") continue;
73
+ if (declaration.type === "styleProperty" && isRef(declaration.value)) {
74
+ const stylePath = pathOf(declaration.value.__ref);
75
+ if (!propForStyleProperty.has(stylePath)) propForStyleProperty.set(stylePath, prop);
76
+ }
77
+ if (declaration.type === "composite" && isRef(declaration.value)) {
78
+ const group = pathOf(declaration.value.__ref);
79
+ for (const composite of composites) {
80
+ if (!composite.path.startsWith(`${group}/`)) continue;
81
+ compositeProps.set(composite.cls, {
82
+ prop,
83
+ value: composite.path.slice(group.length + 1)
84
+ });
85
+ }
86
+ }
87
+ }
88
+ const layers = /* @__PURE__ */ new Map();
89
+ const body = component.toJSON();
90
+ for (const layer of Object.keys(body.layers ?? {})) layers.set(componentLayerClass(config, path, layer), layer);
91
+ const layerProps = /* @__PURE__ */ new Map();
92
+ for (const [prop, declaration] of Object.entries(declared)) {
93
+ if (!declaration || typeof declaration !== "object") continue;
94
+ if (declaration.type === "forward" && routesContent(config, body.layers, declaration)) {
95
+ const target = declaration.target;
96
+ if (isRef(target)) layerProps.set(prop, {
97
+ layer: pathOf(target.__ref),
98
+ content: true,
99
+ into: intoName(declaration.into)
100
+ });
101
+ } else if (declaration.type === "string" && body.layers?.[prop] !== void 0 && prop !== "root") layerProps.set(prop, {
102
+ layer: prop,
103
+ content: false
104
+ });
105
+ }
106
+ return {
107
+ conditions,
108
+ composites: compositeProps,
109
+ propForStyleProperty,
110
+ declared,
111
+ layerProps,
112
+ layers
113
+ };
114
+ };
115
+ return {
116
+ styleProp: (cls) => styleProps.get(cls),
117
+ stylePropsFor: (cssProperty) => byCss.get(cssProperty.toLowerCase()) ?? [],
118
+ viewportLeaf(path, leaf) {
119
+ return (valuesOf(path).get(leaf) ?? []).some((v) => VIEWPORT_UNIT.test(v));
120
+ },
121
+ fullLeaf(path) {
122
+ for (const [leaf, spellings] of valuesOf(path)) if (spellings.includes("100%")) return leaf;
123
+ },
124
+ component(path) {
125
+ if (!components.has(path)) components.set(path, componentGrammar(path));
126
+ return components.get(path);
127
+ }
128
+ };
129
+ }
130
+ //#endregion
131
+ export { classGrammar, normalizeCssValue };
@@ -0,0 +1,3 @@
1
+ import { type CapturedNodeJson } from '../captured-tree';
2
+ /** A captured tree from an HTML string, for tests: what a host's parser hands the op. */
3
+ export declare function treeFromHtml(html: string): CapturedNodeJson;
@@ -0,0 +1,59 @@
1
+ import { Config } from "../framework/Config.js";
2
+ import { CapturedNodeJson } from "./captured-tree.js";
3
+ import { IconHook } from "./capture-to-spec.js";
4
+ import { IconLibrary } from "../entities/system/icon-library.js";
5
+
6
+ //#region src/capture/icon-resolver.d.ts
7
+ /**
8
+ * Names an inline SVG by matching its path data against the glyphs of the config's icon libraries.
9
+ *
10
+ * Everything about a library is read off its group node: the package its glyphs come from
11
+ * (`metadata.from`), the named sizes (`sizes`) and the weights it ships (`variants`). The component
12
+ * that renders them is the one whose variant prop names the library; its props are found by what
13
+ * they reference: the prop bound to the library is the glyph name, the one bound to `#variants` is
14
+ * the weight, and the prop whose values are the library's size names is the size. The host reads
15
+ * the glyph files, since a package's layout on disk is the host's to know.
16
+ */
17
+ /** Where a library's glyph files come from: the catalogue package its metadata names, and its format. */
18
+ interface GlyphSource {
19
+ readonly library: IconLibrary;
20
+ /** The package the glyphs are read from (`metadata.from`). */
21
+ readonly module: string;
22
+ readonly format: string;
23
+ }
24
+ interface IconGlyph {
25
+ readonly name: string;
26
+ readonly variant: string;
27
+ readonly paths: readonly string[];
28
+ }
29
+ /** Every library whose group declares a metadata source and a component draws from, so a host knows
30
+ * which package to read. */
31
+ declare function glyphSourcesOf(config: Config): GlyphSource[];
32
+ /**
33
+ * The `icons` hook for `htmlToSpec`, over every library `glyphs` can read. A library the host
34
+ * cannot read (`glyphs` answers `undefined`) is skipped.
35
+ */
36
+ declare function iconResolver({
37
+ config,
38
+ glyphs
39
+ }: {
40
+ readonly config: Config;
41
+ readonly glyphs: (source: GlyphSource) => readonly IconGlyph[] | undefined;
42
+ }): IconHook;
43
+ /**
44
+ * A captured tree with its glyphs named, for a host: an inline `<svg>` the hook can name becomes a
45
+ * stamped root of the component that renders it, and an svg inside such a renderer's root stamps
46
+ * the renderer instead. An svg no library owns is left as it is, and counted.
47
+ */
48
+ declare function nameIconsInTree({
49
+ tree,
50
+ hook
51
+ }: {
52
+ tree: CapturedNodeJson;
53
+ hook: IconHook;
54
+ }): {
55
+ tree: CapturedNodeJson;
56
+ unresolved: number;
57
+ };
58
+ //#endregion
59
+ export { GlyphSource, IconGlyph, glyphSourcesOf, iconResolver, nameIconsInTree };
@@ -0,0 +1,205 @@
1
+ import { pathOf } from "../framework/utils/refs.js";
2
+ import { glyphComponentOf, resolveComponentProps } from "../framework/projections.js";
3
+ import { iconLibraries } from "../entities/system/icon-library.js";
4
+ //#region src/capture/icon-resolver.ts
5
+ /** Every library whose group declares a metadata source and a component draws from, so a host knows
6
+ * which package to read. */
7
+ function glyphSourcesOf(config) {
8
+ const out = [];
9
+ for (const library of iconLibraries(config)) {
10
+ const meta = config.groupMeta("icon", library.path);
11
+ const from = meta?.metadata?.from?.__ref;
12
+ const format = meta?.metadata?.format;
13
+ if (!from || !format || !glyphComponentOf(config, library.path)) continue;
14
+ out.push({
15
+ library,
16
+ module: pathOf(from),
17
+ format
18
+ });
19
+ }
20
+ return out;
21
+ }
22
+ function refOf(decl) {
23
+ return decl?.value?.__ref;
24
+ }
25
+ function listedValues(decl) {
26
+ const value = decl?.value;
27
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
28
+ if (value && typeof value === "object") return Object.keys(value);
29
+ return [];
30
+ }
31
+ function indexLibrary(config, library, glyphs) {
32
+ const drawer = glyphComponentOf(config, library.path);
33
+ if (!drawer) return void 0;
34
+ const component = drawer.path;
35
+ const declared = resolveComponentProps(config, component);
36
+ let nameProp;
37
+ let variantProp;
38
+ let sizeProp;
39
+ const sizeNames = Object.keys(library.sizes);
40
+ for (const [prop, decl] of Object.entries(declared)) {
41
+ if (decl?.type !== "variant") continue;
42
+ const ref = refOf(decl);
43
+ if (ref === `icon:${library.path}`) nameProp = prop;
44
+ else if (ref === `icon:${library.path}#variants`) variantProp = prop;
45
+ else if (sizeNames.length > 0 && sizeNames.every((size) => listedValues(decl).includes(size))) sizeProp = prop;
46
+ }
47
+ if (!nameProp) return void 0;
48
+ const defaultVariant = library.defaultVariant ?? library.variants[0];
49
+ const ordered = [...glyphs].sort((a, b) => {
50
+ const rank = (g) => g.variant === defaultVariant ? 0 : 1;
51
+ return rank(a) - rank(b);
52
+ });
53
+ const members = new Set(library.members);
54
+ const byPaths = /* @__PURE__ */ new Map();
55
+ for (const glyph of ordered) {
56
+ if (!members.has(glyph.name)) continue;
57
+ const key = glyph.paths.join("");
58
+ if (key && !byPaths.has(key)) byPaths.set(key, glyph);
59
+ }
60
+ return {
61
+ library,
62
+ component,
63
+ nameProp,
64
+ variantProp,
65
+ sizeProp,
66
+ defaultVariant,
67
+ byPaths
68
+ };
69
+ }
70
+ /** The size name whose px is nearest the rendered width. */
71
+ function sizeFor(sizes, width) {
72
+ let best;
73
+ let distance = Number.POSITIVE_INFINITY;
74
+ for (const [name, px] of Object.entries(sizes)) {
75
+ const d = Math.abs(px - width);
76
+ if (d < distance) {
77
+ distance = d;
78
+ best = name;
79
+ }
80
+ }
81
+ return best;
82
+ }
83
+ /**
84
+ * The `icons` hook for `htmlToSpec`, over every library `glyphs` can read. A library the host
85
+ * cannot read (`glyphs` answers `undefined`) is skipped.
86
+ */
87
+ function iconResolver({ config, glyphs }) {
88
+ const indexes = [];
89
+ for (const source of glyphSourcesOf(config)) {
90
+ const read = glyphs(source);
91
+ if (!read) continue;
92
+ const index = indexLibrary(config, source.library, read);
93
+ if (index) indexes.push(index);
94
+ }
95
+ const resolve = (svg) => {
96
+ const key = svg.paths.join("");
97
+ if (!key) return void 0;
98
+ for (const index of indexes) {
99
+ const glyph = index.byPaths.get(key);
100
+ if (!glyph) continue;
101
+ const props = { [index.nameProp]: glyph.name };
102
+ if (index.variantProp && glyph.variant !== index.defaultVariant) props[index.variantProp] = glyph.variant;
103
+ if (index.sizeProp && svg.width !== void 0) {
104
+ const size = sizeFor(index.library.sizes, svg.width);
105
+ if (size) props[index.sizeProp] = size;
106
+ }
107
+ return {
108
+ component: index.component,
109
+ props
110
+ };
111
+ }
112
+ };
113
+ return {
114
+ renderers: new Set(indexes.map((index) => index.component)),
115
+ resolve
116
+ };
117
+ }
118
+ /**
119
+ * A captured tree with its glyphs named, for a host: an inline `<svg>` the hook can name becomes a
120
+ * stamped root of the component that renders it, and an svg inside such a renderer's root stamps
121
+ * the renderer instead. An svg no library owns is left as it is, and counted.
122
+ */
123
+ function nameIconsInTree({ tree, hook }) {
124
+ let unresolved = 0;
125
+ const svgOf = (node) => {
126
+ const paths = [];
127
+ const walk = (n) => {
128
+ for (const child of n.children ?? []) {
129
+ if (typeof child === "string") continue;
130
+ if (child.tag === "path" && child.attrs?.d) paths.push(child.attrs.d);
131
+ walk(child);
132
+ }
133
+ };
134
+ walk(node);
135
+ const width = Number(node.attrs?.width);
136
+ return {
137
+ paths,
138
+ width: Number.isFinite(width) ? width : void 0
139
+ };
140
+ };
141
+ const stamped = (node, props) => {
142
+ const existing = node.attrs?.["data-uds-props"];
143
+ let merged = {};
144
+ if (existing) try {
145
+ merged = JSON.parse(existing);
146
+ } catch {
147
+ merged = {};
148
+ }
149
+ for (const [prop, value] of Object.entries(props)) merged[prop] ??= value;
150
+ return JSON.stringify(merged);
151
+ };
152
+ const visit = (node) => {
153
+ const component = node.attrs?.["data-uds-component"];
154
+ const isRoot = component !== void 0 && node.attrs?.["data-uds-layer"] === "root";
155
+ if (node.tag === "svg" && isRoot && component) {
156
+ const answer = hook.renderers.has(component) ? hook.resolve(svgOf(node)) : void 0;
157
+ if (!answer || answer.component !== component) return node;
158
+ return {
159
+ ...node,
160
+ attrs: {
161
+ ...node.attrs,
162
+ "data-uds-props": stamped(node, answer.props)
163
+ }
164
+ };
165
+ }
166
+ if (node.tag === "svg") {
167
+ const answer = hook.resolve(svgOf(node));
168
+ if (!answer) {
169
+ unresolved += 1;
170
+ return node;
171
+ }
172
+ return {
173
+ tag: "span",
174
+ attrs: {
175
+ "data-uds-component": answer.component,
176
+ "data-uds-layer": "root",
177
+ "data-uds-props": JSON.stringify(answer.props)
178
+ }
179
+ };
180
+ }
181
+ if (component && isRoot && hook.renderers.has(component)) {
182
+ const svg = (node.children ?? []).find((child) => typeof child !== "string" && child.tag === "svg");
183
+ const answer = svg ? hook.resolve(svgOf(svg)) : void 0;
184
+ if (svg && answer && answer.component === component) return {
185
+ ...node,
186
+ attrs: {
187
+ ...node.attrs,
188
+ "data-uds-props": stamped(node, answer.props)
189
+ },
190
+ children: (node.children ?? []).filter((child) => child !== svg)
191
+ };
192
+ }
193
+ if (!node.children?.length) return node;
194
+ return {
195
+ ...node,
196
+ children: node.children.map((child) => typeof child === "string" ? child : visit(child))
197
+ };
198
+ };
199
+ return {
200
+ tree: visit(tree),
201
+ unresolved
202
+ };
203
+ }
204
+ //#endregion
205
+ export { glyphSourcesOf, iconResolver, nameIconsInTree };
@@ -0,0 +1,2 @@
1
+ import { type UserSchemas } from '../framework/schemas';
2
+ export declare const captureNodeOps: UserSchemas;