@tmorrow/cre8-wc 2.0.2 → 2.0.4
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/a2ui/catalog.json +5622 -0
- package/a2ui/demo.html +287 -0
- package/a2ui/generate-catalog.mjs +243 -0
- package/a2ui/index.d.ts +4 -0
- package/a2ui/index.js +2 -0
- package/a2ui/index.ts +12 -0
- package/a2ui/registry.d.ts +3 -0
- package/a2ui/registry.js +166 -0
- package/a2ui/registry.ts +182 -0
- package/a2ui/renderer.d.ts +7 -0
- package/a2ui/renderer.js +108 -0
- package/a2ui/renderer.ts +156 -0
- package/a2ui/smoke-test.mjs +238 -0
- package/a2ui/types.d.ts +75 -0
- package/a2ui/types.js +1 -0
- package/a2ui/types.ts +80 -0
- package/cdn/cre8-wc.esm.js +3363 -2861
- package/cdn/cre8-wc.esm.js.map +1 -1
- package/cdn/cre8-wc.min.js +765 -262
- package/cdn/cre8-wc.min.js.map +1 -1
- package/lib/a2ui/index.d.ts +5 -0
- package/lib/a2ui/index.d.ts.map +1 -0
- package/lib/a2ui/index.js +3 -0
- package/lib/a2ui/index.js.map +1 -0
- package/lib/a2ui/registry.d.ts +4 -0
- package/lib/a2ui/registry.d.ts.map +1 -0
- package/lib/a2ui/registry.js +167 -0
- package/lib/a2ui/registry.js.map +1 -0
- package/lib/a2ui/renderer.d.ts +8 -0
- package/lib/a2ui/renderer.d.ts.map +1 -0
- package/lib/a2ui/renderer.js +109 -0
- package/lib/a2ui/renderer.js.map +1 -0
- package/lib/a2ui/types.d.ts +76 -0
- package/lib/a2ui/types.d.ts.map +1 -0
- package/lib/a2ui/types.js +2 -0
- package/lib/a2ui/types.js.map +1 -0
- package/lib/components/icon/icon.d.ts +2 -1
- package/lib/components/icon/icon.d.ts.map +1 -1
- package/lib/components/icon/icon.js +56 -55
- package/lib/components/icon/icon.js.map +1 -1
- package/lib/vite.config.cdn.js +1 -1
- package/lib/vite.config.cdn.js.map +1 -1
- package/lib/vite.config.js +1 -1
- package/lib/vite.config.js.map +1 -1
- package/mcp-manifest.json +2 -2
- package/package.json +15 -1
package/a2ui/registry.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export function registerCatalog(schema) {
|
|
2
|
+
const defs = schema.$defs?.components ?? {};
|
|
3
|
+
const components = new Map(Object.entries(defs));
|
|
4
|
+
const id = schema['x-a2ui']?.catalogId ?? schema.$id ?? 'unknown';
|
|
5
|
+
return { id, schema, components };
|
|
6
|
+
}
|
|
7
|
+
function validatePropValue(value, schema, path) {
|
|
8
|
+
if (value === undefined || value === null)
|
|
9
|
+
return;
|
|
10
|
+
if (!schema)
|
|
11
|
+
return;
|
|
12
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
13
|
+
throw new Error(`${path}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
|
|
14
|
+
}
|
|
15
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
16
|
+
const allowed = schema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
17
|
+
throw new Error(`${path}: value ${JSON.stringify(value)} not in enum [${allowed}]`);
|
|
18
|
+
}
|
|
19
|
+
if (schema.oneOf && schema.oneOf.length) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
for (const branch of schema.oneOf) {
|
|
22
|
+
try {
|
|
23
|
+
validatePropValue(value, branch, path);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
errors.push(e.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`${path}: value ${JSON.stringify(value)} matched none of oneOf branches: ${errors.join(' | ')}`);
|
|
31
|
+
}
|
|
32
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
33
|
+
if (types.length && !types.some((t) => matchesType(t, value))) {
|
|
34
|
+
throw new Error(`${path}: expected type ${types.join('|')}, got ${describeType(value)}`);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value) && schema.items) {
|
|
37
|
+
value.forEach((item, i) => validatePropValue(item, schema.items, `${path}[${i}]`));
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === 'object' &&
|
|
40
|
+
value !== null &&
|
|
41
|
+
!Array.isArray(value) &&
|
|
42
|
+
schema.properties) {
|
|
43
|
+
const obj = value;
|
|
44
|
+
if (schema.required) {
|
|
45
|
+
for (const req of schema.required) {
|
|
46
|
+
if (!(req in obj)) {
|
|
47
|
+
throw new Error(`${path}.${req}: required property missing`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [key, childVal] of Object.entries(obj)) {
|
|
52
|
+
const childSchema = schema.properties[key];
|
|
53
|
+
if (!childSchema) {
|
|
54
|
+
if (schema.additionalProperties === false) {
|
|
55
|
+
throw new Error(`${path}.${key}: unexpected property`);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
validatePropValue(childVal, childSchema, `${path}.${key}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function matchesType(t, v) {
|
|
64
|
+
switch (t) {
|
|
65
|
+
case 'string':
|
|
66
|
+
return typeof v === 'string';
|
|
67
|
+
case 'number':
|
|
68
|
+
return typeof v === 'number' && Number.isFinite(v);
|
|
69
|
+
case 'integer':
|
|
70
|
+
return typeof v === 'number' && Number.isInteger(v);
|
|
71
|
+
case 'boolean':
|
|
72
|
+
return typeof v === 'boolean';
|
|
73
|
+
case 'array':
|
|
74
|
+
return Array.isArray(v);
|
|
75
|
+
case 'object':
|
|
76
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
77
|
+
case 'null':
|
|
78
|
+
return v === null;
|
|
79
|
+
default:
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function describeType(v) {
|
|
84
|
+
if (v === null)
|
|
85
|
+
return 'null';
|
|
86
|
+
if (Array.isArray(v))
|
|
87
|
+
return 'array';
|
|
88
|
+
return typeof v;
|
|
89
|
+
}
|
|
90
|
+
export function validateSpec(spec, catalog, path = '$') {
|
|
91
|
+
if (!spec || typeof spec !== 'object') {
|
|
92
|
+
throw new Error(`${path}: spec must be an object`);
|
|
93
|
+
}
|
|
94
|
+
const s = spec;
|
|
95
|
+
if (typeof s.component !== 'string') {
|
|
96
|
+
throw new Error(`${path}: spec.component must be a string`);
|
|
97
|
+
}
|
|
98
|
+
if (!catalog.components.has(s.component)) {
|
|
99
|
+
throw new Error(`${path}: component "${s.component}" is not registered in catalog "${catalog.id}"`);
|
|
100
|
+
}
|
|
101
|
+
const def = catalog.components.get(s.component);
|
|
102
|
+
const allowedProps = new Set(Object.keys(def.properties?.props?.properties ?? {}));
|
|
103
|
+
const hasChildren = def.properties?.children !== undefined;
|
|
104
|
+
const allowedSlots = def.properties?.slots
|
|
105
|
+
? new Set(Object.keys(def.properties.slots.properties ?? {}))
|
|
106
|
+
: null;
|
|
107
|
+
if (s.props) {
|
|
108
|
+
if (typeof s.props !== 'object')
|
|
109
|
+
throw new Error(`${path}.props: must be an object`);
|
|
110
|
+
const propDefs = def.properties?.props?.properties ?? {};
|
|
111
|
+
for (const [prop, value] of Object.entries(s.props)) {
|
|
112
|
+
if (!allowedProps.has(prop)) {
|
|
113
|
+
throw new Error(`${path}.props.${prop}: not a declared prop on ${s.component}`);
|
|
114
|
+
}
|
|
115
|
+
validatePropValue(value, propDefs[prop], `${path}.props.${prop}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (s.children !== undefined) {
|
|
119
|
+
if (!hasChildren) {
|
|
120
|
+
throw new Error(`${path}.children: ${s.component} does not accept default children`);
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(s.children))
|
|
123
|
+
throw new Error(`${path}.children: must be an array`);
|
|
124
|
+
s.children.forEach((c, i) => {
|
|
125
|
+
if (typeof c === 'string')
|
|
126
|
+
return;
|
|
127
|
+
validateSpec(c, catalog, `${path}.children[${i}]`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (s.events !== undefined) {
|
|
131
|
+
if (!s.events || typeof s.events !== 'object' || Array.isArray(s.events)) {
|
|
132
|
+
throw new Error(`${path}.events: must be an object`);
|
|
133
|
+
}
|
|
134
|
+
for (const [evtName, binding] of Object.entries(s.events)) {
|
|
135
|
+
if (typeof binding === 'string')
|
|
136
|
+
continue;
|
|
137
|
+
if (!binding || typeof binding !== 'object') {
|
|
138
|
+
throw new Error(`${path}.events.${evtName}: must be a string or { handler } object`);
|
|
139
|
+
}
|
|
140
|
+
const b = binding;
|
|
141
|
+
if (typeof b.handler !== 'string' || b.handler.length === 0) {
|
|
142
|
+
throw new Error(`${path}.events.${evtName}.handler: must be a non-empty string`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (s.slots !== undefined) {
|
|
147
|
+
if (!allowedSlots) {
|
|
148
|
+
throw new Error(`${path}.slots: ${s.component} does not accept named slots`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof s.slots !== 'object')
|
|
151
|
+
throw new Error(`${path}.slots: must be an object`);
|
|
152
|
+
for (const [slotName, arr] of Object.entries(s.slots)) {
|
|
153
|
+
if (!allowedSlots.has(slotName)) {
|
|
154
|
+
throw new Error(`${path}.slots.${slotName}: not a declared slot on ${s.component}`);
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(arr)) {
|
|
157
|
+
throw new Error(`${path}.slots.${slotName}: must be an array`);
|
|
158
|
+
}
|
|
159
|
+
arr.forEach((c, i) => {
|
|
160
|
+
if (typeof c === 'string')
|
|
161
|
+
return;
|
|
162
|
+
validateSpec(c, catalog, `${path}.slots.${slotName}[${i}]`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/a2ui/registry.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { CatalogSchema, ComponentSpec, PropSchema, RegisteredCatalog } from './types.js';
|
|
2
|
+
|
|
3
|
+
export function registerCatalog(schema: CatalogSchema): RegisteredCatalog {
|
|
4
|
+
const defs = schema.$defs?.components ?? {};
|
|
5
|
+
const components = new Map(Object.entries(defs));
|
|
6
|
+
const id = schema['x-a2ui']?.catalogId ?? schema.$id ?? 'unknown';
|
|
7
|
+
return { id, schema, components };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function validatePropValue(value: unknown, schema: PropSchema | undefined, path: string): void {
|
|
11
|
+
if (value === undefined || value === null) return;
|
|
12
|
+
if (!schema) return;
|
|
13
|
+
|
|
14
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`${path}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (schema.enum && !schema.enum.includes(value as string)) {
|
|
21
|
+
const allowed = schema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
22
|
+
throw new Error(`${path}: value ${JSON.stringify(value)} not in enum [${allowed}]`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (schema.oneOf && schema.oneOf.length) {
|
|
26
|
+
const errors: string[] = [];
|
|
27
|
+
for (const branch of schema.oneOf) {
|
|
28
|
+
try {
|
|
29
|
+
validatePropValue(value, branch, path);
|
|
30
|
+
return;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
errors.push((e as Error).message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${path}: value ${JSON.stringify(value)} matched none of oneOf branches: ${errors.join(' | ')}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
41
|
+
if (types.length && !types.some((t) => matchesType(t, value))) {
|
|
42
|
+
throw new Error(`${path}: expected type ${types.join('|')}, got ${describeType(value)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(value) && schema.items) {
|
|
46
|
+
value.forEach((item, i) => validatePropValue(item, schema.items, `${path}[${i}]`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
typeof value === 'object' &&
|
|
51
|
+
value !== null &&
|
|
52
|
+
!Array.isArray(value) &&
|
|
53
|
+
schema.properties
|
|
54
|
+
) {
|
|
55
|
+
const obj = value as Record<string, unknown>;
|
|
56
|
+
if (schema.required) {
|
|
57
|
+
for (const req of schema.required) {
|
|
58
|
+
if (!(req in obj)) {
|
|
59
|
+
throw new Error(`${path}.${req}: required property missing`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const [key, childVal] of Object.entries(obj)) {
|
|
64
|
+
const childSchema = schema.properties[key];
|
|
65
|
+
if (!childSchema) {
|
|
66
|
+
if (schema.additionalProperties === false) {
|
|
67
|
+
throw new Error(`${path}.${key}: unexpected property`);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
validatePropValue(childVal, childSchema, `${path}.${key}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function matchesType(t: string, v: unknown): boolean {
|
|
77
|
+
switch (t) {
|
|
78
|
+
case 'string':
|
|
79
|
+
return typeof v === 'string';
|
|
80
|
+
case 'number':
|
|
81
|
+
return typeof v === 'number' && Number.isFinite(v);
|
|
82
|
+
case 'integer':
|
|
83
|
+
return typeof v === 'number' && Number.isInteger(v);
|
|
84
|
+
case 'boolean':
|
|
85
|
+
return typeof v === 'boolean';
|
|
86
|
+
case 'array':
|
|
87
|
+
return Array.isArray(v);
|
|
88
|
+
case 'object':
|
|
89
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
90
|
+
case 'null':
|
|
91
|
+
return v === null;
|
|
92
|
+
default:
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function describeType(v: unknown): string {
|
|
98
|
+
if (v === null) return 'null';
|
|
99
|
+
if (Array.isArray(v)) return 'array';
|
|
100
|
+
return typeof v;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateSpec(spec: unknown, catalog: RegisteredCatalog, path = '$'): asserts spec is ComponentSpec {
|
|
104
|
+
if (!spec || typeof spec !== 'object') {
|
|
105
|
+
throw new Error(`${path}: spec must be an object`);
|
|
106
|
+
}
|
|
107
|
+
const s = spec as Record<string, unknown>;
|
|
108
|
+
if (typeof s.component !== 'string') {
|
|
109
|
+
throw new Error(`${path}: spec.component must be a string`);
|
|
110
|
+
}
|
|
111
|
+
if (!catalog.components.has(s.component)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${path}: component "${s.component}" is not registered in catalog "${catalog.id}"`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const def = catalog.components.get(s.component)!;
|
|
118
|
+
const allowedProps = new Set(Object.keys(def.properties?.props?.properties ?? {}));
|
|
119
|
+
const hasChildren = def.properties?.children !== undefined;
|
|
120
|
+
const allowedSlots = def.properties?.slots
|
|
121
|
+
? new Set(Object.keys(def.properties.slots.properties ?? {}))
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
if (s.props) {
|
|
125
|
+
if (typeof s.props !== 'object') throw new Error(`${path}.props: must be an object`);
|
|
126
|
+
const propDefs = def.properties?.props?.properties ?? {};
|
|
127
|
+
for (const [prop, value] of Object.entries(s.props as Record<string, unknown>)) {
|
|
128
|
+
if (!allowedProps.has(prop)) {
|
|
129
|
+
throw new Error(`${path}.props.${prop}: not a declared prop on ${s.component}`);
|
|
130
|
+
}
|
|
131
|
+
validatePropValue(value, propDefs[prop], `${path}.props.${prop}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (s.children !== undefined) {
|
|
136
|
+
if (!hasChildren) {
|
|
137
|
+
throw new Error(`${path}.children: ${s.component} does not accept default children`);
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(s.children)) throw new Error(`${path}.children: must be an array`);
|
|
140
|
+
s.children.forEach((c, i) => {
|
|
141
|
+
if (typeof c === 'string') return;
|
|
142
|
+
validateSpec(c, catalog, `${path}.children[${i}]`);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (s.events !== undefined) {
|
|
147
|
+
if (!s.events || typeof s.events !== 'object' || Array.isArray(s.events)) {
|
|
148
|
+
throw new Error(`${path}.events: must be an object`);
|
|
149
|
+
}
|
|
150
|
+
for (const [evtName, binding] of Object.entries(s.events as Record<string, unknown>)) {
|
|
151
|
+
if (typeof binding === 'string') continue;
|
|
152
|
+
if (!binding || typeof binding !== 'object') {
|
|
153
|
+
throw new Error(`${path}.events.${evtName}: must be a string or { handler } object`);
|
|
154
|
+
}
|
|
155
|
+
const b = binding as Record<string, unknown>;
|
|
156
|
+
if (typeof b.handler !== 'string' || b.handler.length === 0) {
|
|
157
|
+
throw new Error(`${path}.events.${evtName}.handler: must be a non-empty string`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (s.slots !== undefined) {
|
|
163
|
+
if (!allowedSlots) {
|
|
164
|
+
throw new Error(`${path}.slots: ${s.component} does not accept named slots`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof s.slots !== 'object') throw new Error(`${path}.slots: must be an object`);
|
|
167
|
+
for (const [slotName, arr] of Object.entries(s.slots as Record<string, unknown>)) {
|
|
168
|
+
if (!allowedSlots.has(slotName)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${path}.slots.${slotName}: not a declared slot on ${s.component}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(arr)) {
|
|
174
|
+
throw new Error(`${path}.slots.${slotName}: must be an array`);
|
|
175
|
+
}
|
|
176
|
+
arr.forEach((c, i) => {
|
|
177
|
+
if (typeof c === 'string') return;
|
|
178
|
+
validateSpec(c, catalog, `${path}.slots.${slotName}[${i}]`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentSpec, EmittedEvent, RegisteredCatalog } from './types.js';
|
|
2
|
+
export interface RenderOptions {
|
|
3
|
+
root?: HTMLElement;
|
|
4
|
+
doc?: Document;
|
|
5
|
+
onEvent?: (evt: EmittedEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function render(spec: ComponentSpec, catalog: RegisteredCatalog, options?: RenderOptions): HTMLElement;
|
package/a2ui/renderer.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { validateSpec } from './registry.js';
|
|
2
|
+
export function render(spec, catalog, options = {}) {
|
|
3
|
+
validateSpec(spec, catalog);
|
|
4
|
+
const doc = options.doc ?? document;
|
|
5
|
+
const el = buildElement(spec, catalog, doc, '$', options.onEvent);
|
|
6
|
+
if (options.root)
|
|
7
|
+
options.root.replaceChildren(el);
|
|
8
|
+
return el;
|
|
9
|
+
}
|
|
10
|
+
function buildElement(spec, catalog, doc, path, onEvent) {
|
|
11
|
+
const def = catalog.components.get(spec.component);
|
|
12
|
+
const propDefs = def.properties?.props?.properties ?? {};
|
|
13
|
+
const el = doc.createElement(spec.component);
|
|
14
|
+
if (spec.props) {
|
|
15
|
+
for (const [key, value] of Object.entries(spec.props)) {
|
|
16
|
+
applyProp(el, key, value, propDefs[key]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (spec.events && onEvent) {
|
|
20
|
+
for (const [evtName, binding] of Object.entries(spec.events)) {
|
|
21
|
+
attachEvent(el, spec.component, path, evtName, binding, onEvent);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (spec.children) {
|
|
25
|
+
spec.children.forEach((child, i) => {
|
|
26
|
+
if (typeof child === 'string') {
|
|
27
|
+
el.appendChild(doc.createTextNode(child));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
el.appendChild(buildElement(child, catalog, doc, `${path}.children[${i}]`, onEvent));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (spec.slots) {
|
|
34
|
+
for (const [slotName, children] of Object.entries(spec.slots)) {
|
|
35
|
+
children.forEach((child, i) => {
|
|
36
|
+
if (typeof child === 'string') {
|
|
37
|
+
if (slotName === 'default') {
|
|
38
|
+
el.appendChild(doc.createTextNode(child));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const wrap = doc.createElement('span');
|
|
42
|
+
wrap.setAttribute('slot', slotName);
|
|
43
|
+
wrap.textContent = child;
|
|
44
|
+
el.appendChild(wrap);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const childEl = buildElement(child, catalog, doc, `${path}.slots.${slotName}[${i}]`, onEvent);
|
|
49
|
+
if (slotName !== 'default')
|
|
50
|
+
childEl.setAttribute('slot', slotName);
|
|
51
|
+
el.appendChild(childEl);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return el;
|
|
56
|
+
}
|
|
57
|
+
function attachEvent(el, component, path, eventName, binding, onEvent) {
|
|
58
|
+
const handler = typeof binding === 'string' ? binding : binding.handler;
|
|
59
|
+
const stop = typeof binding === 'object' && binding.stopPropagation === true;
|
|
60
|
+
const prevent = typeof binding === 'object' && binding.preventDefault === true;
|
|
61
|
+
el.addEventListener(eventName, (nativeEvent) => {
|
|
62
|
+
if (stop)
|
|
63
|
+
nativeEvent.stopPropagation();
|
|
64
|
+
if (prevent)
|
|
65
|
+
nativeEvent.preventDefault();
|
|
66
|
+
const detail = 'detail' in nativeEvent
|
|
67
|
+
? nativeEvent.detail
|
|
68
|
+
: undefined;
|
|
69
|
+
onEvent({
|
|
70
|
+
component,
|
|
71
|
+
path,
|
|
72
|
+
event: eventName,
|
|
73
|
+
handler,
|
|
74
|
+
detail,
|
|
75
|
+
nativeEvent,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function applyProp(el, key, value, schema) {
|
|
80
|
+
if (value === undefined || value === null)
|
|
81
|
+
return;
|
|
82
|
+
if (schema?.['x-kind'] === 'property') {
|
|
83
|
+
el[key] = value;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === 'boolean') {
|
|
87
|
+
if (value)
|
|
88
|
+
el.setAttribute(key, '');
|
|
89
|
+
else
|
|
90
|
+
el.removeAttribute(key);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
|
94
|
+
el[key] = value;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (schema && isComplexSchema(schema)) {
|
|
98
|
+
el[key] = value;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
el.setAttribute(key, String(value));
|
|
102
|
+
}
|
|
103
|
+
function isComplexSchema(schema) {
|
|
104
|
+
const t = schema.type;
|
|
105
|
+
if (Array.isArray(t))
|
|
106
|
+
return t.some((x) => x === 'object' || x === 'array');
|
|
107
|
+
return t === 'object' || t === 'array';
|
|
108
|
+
}
|
package/a2ui/renderer.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentSpec,
|
|
3
|
+
EmittedEvent,
|
|
4
|
+
EventBinding,
|
|
5
|
+
PropSchema,
|
|
6
|
+
RegisteredCatalog,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
import { validateSpec } from './registry.js';
|
|
9
|
+
|
|
10
|
+
export interface RenderOptions {
|
|
11
|
+
root?: HTMLElement;
|
|
12
|
+
doc?: Document;
|
|
13
|
+
onEvent?: (evt: EmittedEvent) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function render(
|
|
17
|
+
spec: ComponentSpec,
|
|
18
|
+
catalog: RegisteredCatalog,
|
|
19
|
+
options: RenderOptions = {}
|
|
20
|
+
): HTMLElement {
|
|
21
|
+
validateSpec(spec, catalog);
|
|
22
|
+
const doc = options.doc ?? document;
|
|
23
|
+
const el = buildElement(spec, catalog, doc, '$', options.onEvent);
|
|
24
|
+
if (options.root) options.root.replaceChildren(el);
|
|
25
|
+
return el;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildElement(
|
|
29
|
+
spec: ComponentSpec,
|
|
30
|
+
catalog: RegisteredCatalog,
|
|
31
|
+
doc: Document,
|
|
32
|
+
path: string,
|
|
33
|
+
onEvent?: (evt: EmittedEvent) => void
|
|
34
|
+
): HTMLElement {
|
|
35
|
+
const def = catalog.components.get(spec.component)!;
|
|
36
|
+
const propDefs = def.properties?.props?.properties ?? {};
|
|
37
|
+
const el = doc.createElement(spec.component);
|
|
38
|
+
|
|
39
|
+
if (spec.props) {
|
|
40
|
+
for (const [key, value] of Object.entries(spec.props)) {
|
|
41
|
+
applyProp(el, key, value, propDefs[key]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (spec.events && onEvent) {
|
|
46
|
+
for (const [evtName, binding] of Object.entries(spec.events)) {
|
|
47
|
+
attachEvent(el, spec.component, path, evtName, binding, onEvent);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (spec.children) {
|
|
52
|
+
spec.children.forEach((child, i) => {
|
|
53
|
+
if (typeof child === 'string') {
|
|
54
|
+
el.appendChild(doc.createTextNode(child));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
el.appendChild(buildElement(child, catalog, doc, `${path}.children[${i}]`, onEvent));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (spec.slots) {
|
|
62
|
+
for (const [slotName, children] of Object.entries(spec.slots)) {
|
|
63
|
+
children.forEach((child, i) => {
|
|
64
|
+
if (typeof child === 'string') {
|
|
65
|
+
if (slotName === 'default') {
|
|
66
|
+
el.appendChild(doc.createTextNode(child));
|
|
67
|
+
} else {
|
|
68
|
+
const wrap = doc.createElement('span');
|
|
69
|
+
wrap.setAttribute('slot', slotName);
|
|
70
|
+
wrap.textContent = child;
|
|
71
|
+
el.appendChild(wrap);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const childEl = buildElement(
|
|
76
|
+
child,
|
|
77
|
+
catalog,
|
|
78
|
+
doc,
|
|
79
|
+
`${path}.slots.${slotName}[${i}]`,
|
|
80
|
+
onEvent
|
|
81
|
+
);
|
|
82
|
+
if (slotName !== 'default') childEl.setAttribute('slot', slotName);
|
|
83
|
+
el.appendChild(childEl);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return el;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function attachEvent(
|
|
92
|
+
el: HTMLElement,
|
|
93
|
+
component: string,
|
|
94
|
+
path: string,
|
|
95
|
+
eventName: string,
|
|
96
|
+
binding: EventBinding,
|
|
97
|
+
onEvent: (evt: EmittedEvent) => void
|
|
98
|
+
): void {
|
|
99
|
+
const handler = typeof binding === 'string' ? binding : binding.handler;
|
|
100
|
+
const stop = typeof binding === 'object' && binding.stopPropagation === true;
|
|
101
|
+
const prevent = typeof binding === 'object' && binding.preventDefault === true;
|
|
102
|
+
|
|
103
|
+
el.addEventListener(eventName, (nativeEvent) => {
|
|
104
|
+
if (stop) nativeEvent.stopPropagation();
|
|
105
|
+
if (prevent) nativeEvent.preventDefault();
|
|
106
|
+
const detail = 'detail' in nativeEvent
|
|
107
|
+
? (nativeEvent as CustomEvent).detail
|
|
108
|
+
: undefined;
|
|
109
|
+
onEvent({
|
|
110
|
+
component,
|
|
111
|
+
path,
|
|
112
|
+
event: eventName,
|
|
113
|
+
handler,
|
|
114
|
+
detail,
|
|
115
|
+
nativeEvent,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function applyProp(
|
|
121
|
+
el: HTMLElement,
|
|
122
|
+
key: string,
|
|
123
|
+
value: unknown,
|
|
124
|
+
schema: PropSchema | undefined
|
|
125
|
+
): void {
|
|
126
|
+
if (value === undefined || value === null) return;
|
|
127
|
+
|
|
128
|
+
if (schema?.['x-kind'] === 'property') {
|
|
129
|
+
(el as unknown as Record<string, unknown>)[key] = value;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof value === 'boolean') {
|
|
134
|
+
if (value) el.setAttribute(key, '');
|
|
135
|
+
else el.removeAttribute(key);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
|
140
|
+
(el as unknown as Record<string, unknown>)[key] = value;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schema && isComplexSchema(schema)) {
|
|
145
|
+
(el as unknown as Record<string, unknown>)[key] = value;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
el.setAttribute(key, String(value));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isComplexSchema(schema: PropSchema): boolean {
|
|
153
|
+
const t = schema.type;
|
|
154
|
+
if (Array.isArray(t)) return t.some((x) => x === 'object' || x === 'array');
|
|
155
|
+
return t === 'object' || t === 'array';
|
|
156
|
+
}
|