@yahoo/uds-create-config 4.11.0 → 4.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture/capture-to-spec.d.ts +69 -0
- package/dist/capture/capture-to-spec.js +433 -0
- package/dist/capture/captured-tree.d.ts +65 -0
- package/dist/capture/captured-tree.js +201 -0
- package/dist/capture/class-grammar.d.ts +64 -0
- package/dist/capture/class-grammar.js +131 -0
- package/dist/capture/fixtures/tree-from-html.d.ts +3 -0
- package/dist/capture/icon-resolver.d.ts +59 -0
- package/dist/capture/icon-resolver.js +205 -0
- package/dist/capture/node-ops.d.ts +2 -0
- package/dist/capture/node-ops.js +146 -0
- package/dist/capture/node-patches.d.ts +32 -0
- package/dist/capture/node-patches.js +99 -0
- package/dist/configs/CanvasConfig.d.ts +1 -1
- package/dist/configs/CanvasConfig.js +3 -1
- package/dist/configs/react-native-system.d.ts +28 -20
- package/dist/configs/react-native-system.js +1 -1
- package/dist/configs/system.d.ts +12 -4
- package/dist/configs/system.js +4 -2
- package/dist/entities/native/NativeModifier.d.ts +1 -1
- package/dist/entities/native/NativeStyleProperty.d.ts +15 -15
- package/dist/entities/system/CanvasRole.d.ts +5 -1
- package/dist/entities/system/CanvasRole.js +7 -2
- package/dist/entities/system/Component.js +3 -3
- package/dist/entities/system/Icon.d.ts +2 -0
- package/dist/entities/system/Icon.js +9 -3
- package/dist/entities/system/Modifier.d.ts +2 -2
- package/dist/entities/system/copy-plan.js +1 -1
- package/dist/entities/system/defineComponent.d.ts +6 -3
- package/dist/entities/system/element.d.ts +2 -1
- package/dist/entities/system/element.js +3 -1
- package/dist/entities/system/icon-library.d.ts +21 -6
- package/dist/entities/system/icon-library.js +36 -15
- package/dist/entities/system/link-plan.js +1 -1
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +2 -2
- package/dist/framework/defineEntity.js +1 -1
- package/dist/framework/inline-sources.js +1 -1
- package/dist/framework/plan.js +1 -1
- package/dist/framework/projections.d.ts +27 -8
- package/dist/framework/projections.js +38 -9
- package/dist/framework/render-spec.js +2 -2
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +14 -9
- package/dist/migrations/2.0.0/v1-artifact.d.ts +7 -3
- package/dist/migrations/20260923005318_icon_canvas_tool.d.ts +12 -0
- package/dist/migrations/20260923005318_icon_canvas_tool.js +8 -0
- package/dist/renderer/glyphEntries.d.ts +6 -5
- package/dist/renderer/glyphEntries.js +17 -13
- package/dist/renderer/glyphRenderable.js +19 -12
- package/dist/renderer/icon-library.d.ts +5 -2
- package/dist/renderer/resolveGlyph.d.ts +6 -2
- package/dist/spec/cell.js +1 -1
- package/dist/spec/prop-conditions.js +1 -1
- package/dist/spec/slot.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
|
@@ -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,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 };
|