@bestagentkits/render 0.1.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 (140) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/cli.d.ts +18 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +336 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/diagnostics.d.ts +45 -0
  9. package/dist/diagnostics.d.ts.map +1 -0
  10. package/dist/diagnostics.js +57 -0
  11. package/dist/diagnostics.js.map +1 -0
  12. package/dist/diagram/adapter.d.ts +71 -0
  13. package/dist/diagram/adapter.d.ts.map +1 -0
  14. package/dist/diagram/adapter.js +87 -0
  15. package/dist/diagram/adapter.js.map +1 -0
  16. package/dist/errors.d.ts +37 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +42 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/hash.d.ts +14 -0
  21. package/dist/hash.d.ts.map +1 -0
  22. package/dist/hash.js +27 -0
  23. package/dist/hash.js.map +1 -0
  24. package/dist/index.d.ts +43 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +25 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/internal/yaml-error.d.ts +14 -0
  29. package/dist/internal/yaml-error.d.ts.map +1 -0
  30. package/dist/internal/yaml-error.js +12 -0
  31. package/dist/internal/yaml-error.js.map +1 -0
  32. package/dist/ir.d.ts +77 -0
  33. package/dist/ir.d.ts.map +1 -0
  34. package/dist/ir.js +20 -0
  35. package/dist/ir.js.map +1 -0
  36. package/dist/json.d.ts +23 -0
  37. package/dist/json.d.ts.map +1 -0
  38. package/dist/json.js +47 -0
  39. package/dist/json.js.map +1 -0
  40. package/dist/registry/actions.d.ts +48 -0
  41. package/dist/registry/actions.d.ts.map +1 -0
  42. package/dist/registry/actions.js +274 -0
  43. package/dist/registry/actions.js.map +1 -0
  44. package/dist/registry/prop-schema.d.ts +108 -0
  45. package/dist/registry/prop-schema.d.ts.map +1 -0
  46. package/dist/registry/prop-schema.js +400 -0
  47. package/dist/registry/prop-schema.js.map +1 -0
  48. package/dist/registry/registry.d.ts +47 -0
  49. package/dist/registry/registry.d.ts.map +1 -0
  50. package/dist/registry/registry.js +201 -0
  51. package/dist/registry/registry.js.map +1 -0
  52. package/dist/registry/roster.d.ts +53 -0
  53. package/dist/registry/roster.d.ts.map +1 -0
  54. package/dist/registry/roster.js +692 -0
  55. package/dist/registry/roster.js.map +1 -0
  56. package/dist/render/blocks.d.ts +41 -0
  57. package/dist/render/blocks.d.ts.map +1 -0
  58. package/dist/render/blocks.js +749 -0
  59. package/dist/render/blocks.js.map +1 -0
  60. package/dist/render/charts.d.ts +29 -0
  61. package/dist/render/charts.d.ts.map +1 -0
  62. package/dist/render/charts.js +235 -0
  63. package/dist/render/charts.js.map +1 -0
  64. package/dist/render/document.d.ts +40 -0
  65. package/dist/render/document.d.ts.map +1 -0
  66. package/dist/render/document.js +90 -0
  67. package/dist/render/document.js.map +1 -0
  68. package/dist/render/escape.d.ts +46 -0
  69. package/dist/render/escape.d.ts.map +1 -0
  70. package/dist/render/escape.js +108 -0
  71. package/dist/render/escape.js.map +1 -0
  72. package/dist/render/render.d.ts +52 -0
  73. package/dist/render/render.d.ts.map +1 -0
  74. package/dist/render/render.js +216 -0
  75. package/dist/render/render.js.map +1 -0
  76. package/dist/render/runtime.d.ts +25 -0
  77. package/dist/render/runtime.d.ts.map +1 -0
  78. package/dist/render/runtime.js +451 -0
  79. package/dist/render/runtime.js.map +1 -0
  80. package/dist/render/styles.d.ts +15 -0
  81. package/dist/render/styles.d.ts.map +1 -0
  82. package/dist/render/styles.js +163 -0
  83. package/dist/render/styles.js.map +1 -0
  84. package/dist/render/verify.d.ts +25 -0
  85. package/dist/render/verify.d.ts.map +1 -0
  86. package/dist/render/verify.js +168 -0
  87. package/dist/render/verify.js.map +1 -0
  88. package/dist/spec/bindings.d.ts +19 -0
  89. package/dist/spec/bindings.d.ts.map +1 -0
  90. package/dist/spec/bindings.js +51 -0
  91. package/dist/spec/bindings.js.map +1 -0
  92. package/dist/spec/bounds.d.ts +43 -0
  93. package/dist/spec/bounds.d.ts.map +1 -0
  94. package/dist/spec/bounds.js +144 -0
  95. package/dist/spec/bounds.js.map +1 -0
  96. package/dist/spec/forbidden.d.ts +27 -0
  97. package/dist/spec/forbidden.d.ts.map +1 -0
  98. package/dist/spec/forbidden.js +91 -0
  99. package/dist/spec/forbidden.js.map +1 -0
  100. package/dist/spec/migrate.d.ts +33 -0
  101. package/dist/spec/migrate.d.ts.map +1 -0
  102. package/dist/spec/migrate.js +113 -0
  103. package/dist/spec/migrate.js.map +1 -0
  104. package/dist/spec/normalize.d.ts +29 -0
  105. package/dist/spec/normalize.d.ts.map +1 -0
  106. package/dist/spec/normalize.js +649 -0
  107. package/dist/spec/normalize.js.map +1 -0
  108. package/dist/spec/parse.d.ts +22 -0
  109. package/dist/spec/parse.d.ts.map +1 -0
  110. package/dist/spec/parse.js +62 -0
  111. package/dist/spec/parse.js.map +1 -0
  112. package/dist/spec/providers.d.ts +30 -0
  113. package/dist/spec/providers.d.ts.map +1 -0
  114. package/dist/spec/providers.js +64 -0
  115. package/dist/spec/providers.js.map +1 -0
  116. package/dist/spec/validate.d.ts +28 -0
  117. package/dist/spec/validate.d.ts.map +1 -0
  118. package/dist/spec/validate.js +68 -0
  119. package/dist/spec/validate.js.map +1 -0
  120. package/dist/theme/load-theme.d.ts +55 -0
  121. package/dist/theme/load-theme.d.ts.map +1 -0
  122. package/dist/theme/load-theme.js +219 -0
  123. package/dist/theme/load-theme.js.map +1 -0
  124. package/dist/theme/presets.d.ts +50 -0
  125. package/dist/theme/presets.d.ts.map +1 -0
  126. package/dist/theme/presets.js +304 -0
  127. package/dist/theme/presets.js.map +1 -0
  128. package/dist/theme/theme-catalog.d.ts +68 -0
  129. package/dist/theme/theme-catalog.d.ts.map +1 -0
  130. package/dist/theme/theme-catalog.js +198 -0
  131. package/dist/theme/theme-catalog.js.map +1 -0
  132. package/dist/theme/tokens.d.ts +38 -0
  133. package/dist/theme/tokens.d.ts.map +1 -0
  134. package/dist/theme/tokens.js +190 -0
  135. package/dist/theme/tokens.js.map +1 -0
  136. package/dist/version.d.ts +12 -0
  137. package/dist/version.d.ts.map +1 -0
  138. package/dist/version.js +12 -0
  139. package/dist/version.js.map +1 -0
  140. package/package.json +80 -0
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Normalizer: nested authoring Page Spec -> flat internal IR.
3
+ *
4
+ * The author-facing shape is optimized for authoring (nested, tolerant of
5
+ * omission, semantic). The IR is optimized for the compiler (flat, ordered,
6
+ * every node resolved). This module owns the crossing between them, including
7
+ * stable node IDs, slot wiring, and the cross-field checks that a per-prop
8
+ * schema cannot express.
9
+ */
10
+ import { DiagnosticBag, pathIndex, pathKey } from '../diagnostics.js';
11
+ import { isRenderError, RenderError } from '../errors.js';
12
+ import { derivedNodeId } from '../hash.js';
13
+ import { isPlainObject } from '../json.js';
14
+ import { NODE_ID_PATTERN, validateProps } from '../registry/prop-schema.js';
15
+ import { blockTypes, getBlockDefinition } from '../registry/registry.js';
16
+ import { VERSION } from '../version.js';
17
+ import { validateBindingsDeep } from './bindings.js';
18
+ import { checkBounds } from './bounds.js';
19
+ import { scanForbiddenKeys } from './forbidden.js';
20
+ import { migrateSpec } from './migrate.js';
21
+ import { parseSpec } from './parse.js';
22
+ import { EMBED_PROVIDER_NAMES, isEmbedProvider } from './providers.js';
23
+ const THEME_PRESET_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
24
+ const LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
25
+ const DEFAULT_THEME_PRESET = 'editorial';
26
+ export const NETWORK_CAPABILITIES = ['media', 'fonts', 'images'];
27
+ function requireString(value, path, bag, options) {
28
+ if (value === undefined) {
29
+ if (options.required === true) {
30
+ bag.add({ code: 'SPEC_VALIDATION_ERROR', path, message: 'required value is missing' });
31
+ }
32
+ return undefined;
33
+ }
34
+ if (typeof value !== 'string') {
35
+ bag.add({ code: 'SPEC_VALIDATION_ERROR', path, message: 'expected a string' });
36
+ return undefined;
37
+ }
38
+ if (value.length === 0 || value.length > options.maxLength) {
39
+ bag.add({
40
+ code: 'SPEC_VALIDATION_ERROR',
41
+ path,
42
+ message: `expected 1-${options.maxLength} characters, received ${value.length}`,
43
+ });
44
+ return undefined;
45
+ }
46
+ if (options.pattern !== undefined && !options.pattern.test(value)) {
47
+ bag.add({
48
+ code: 'SPEC_VALIDATION_ERROR',
49
+ path,
50
+ message: `"${value}" is not valid${options.patternHint === undefined ? '' : ` (${options.patternHint})`}`,
51
+ });
52
+ return undefined;
53
+ }
54
+ return value;
55
+ }
56
+ function normalizeEnvelope(document, bag) {
57
+ const metaValue = document.meta;
58
+ if (!isPlainObject(metaValue)) {
59
+ bag.add({ code: 'SPEC_VALIDATION_ERROR', path: '$.meta', message: 'expected a meta object' });
60
+ return undefined;
61
+ }
62
+ const title = requireString(metaValue.title, '$.meta.title', bag, {
63
+ maxLength: 200,
64
+ required: true,
65
+ });
66
+ const description = requireString(metaValue.description, '$.meta.description', bag, {
67
+ maxLength: 400,
68
+ });
69
+ const locale = requireString(metaValue.locale, '$.meta.locale', bag, {
70
+ maxLength: 35,
71
+ pattern: LOCALE_PATTERN,
72
+ patternHint: 'a language tag such as "en" or "en-US"',
73
+ }) ?? 'en';
74
+ const slug = requireString(metaValue.slug, '$.meta.slug', bag, { maxLength: 120 });
75
+ for (const key of Object.keys(metaValue)) {
76
+ if (!['title', 'description', 'locale', 'slug'].includes(key)) {
77
+ bag.add({
78
+ code: 'SPEC_VALIDATION_ERROR',
79
+ path: pathKey('$.meta', key),
80
+ message: `unknown meta field "${key}"`,
81
+ details: { allowed: ['title', 'description', 'locale', 'slug'] },
82
+ });
83
+ }
84
+ }
85
+ const themeValue = document.theme;
86
+ let theme = { preset: DEFAULT_THEME_PRESET };
87
+ if (themeValue !== undefined) {
88
+ if (!isPlainObject(themeValue)) {
89
+ bag.add({
90
+ code: 'SPEC_VALIDATION_ERROR',
91
+ path: '$.theme',
92
+ message: 'expected a theme object',
93
+ });
94
+ }
95
+ else {
96
+ const preset = requireString(themeValue.preset, '$.theme.preset', bag, {
97
+ maxLength: 64,
98
+ pattern: THEME_PRESET_PATTERN,
99
+ patternHint: 'lowercase letters, digits, and dashes',
100
+ });
101
+ const extendsName = requireString(themeValue.extends, '$.theme.extends', bag, {
102
+ maxLength: 64,
103
+ pattern: THEME_PRESET_PATTERN,
104
+ patternHint: 'lowercase letters, digits, and dashes',
105
+ });
106
+ theme = { preset: preset ?? DEFAULT_THEME_PRESET };
107
+ if (extendsName !== undefined)
108
+ theme.extends = extendsName;
109
+ const tokens = themeValue.tokens;
110
+ if (tokens !== undefined) {
111
+ if (!isPlainObject(tokens)) {
112
+ bag.add({
113
+ code: 'SPEC_VALIDATION_ERROR',
114
+ path: '$.theme.tokens',
115
+ message: 'expected a token object',
116
+ });
117
+ }
118
+ else {
119
+ theme.tokens = tokens;
120
+ }
121
+ }
122
+ for (const key of Object.keys(themeValue)) {
123
+ if (!['preset', 'extends', 'tokens'].includes(key)) {
124
+ bag.add({
125
+ code: 'SPEC_VALIDATION_ERROR',
126
+ path: pathKey('$.theme', key),
127
+ message: `unknown theme field "${key}"`,
128
+ details: { allowed: ['preset', 'extends', 'tokens'] },
129
+ });
130
+ }
131
+ }
132
+ }
133
+ }
134
+ const policyValue = document.policy;
135
+ let policy = { network: 'deny' };
136
+ if (policyValue !== undefined) {
137
+ if (!isPlainObject(policyValue)) {
138
+ bag.add({
139
+ code: 'SPEC_VALIDATION_ERROR',
140
+ path: '$.policy',
141
+ message: 'expected a policy object',
142
+ });
143
+ }
144
+ else {
145
+ const network = policyValue.network;
146
+ if (network !== undefined && network !== 'deny') {
147
+ if (!isPlainObject(network)) {
148
+ bag.add({
149
+ code: 'SPEC_VALIDATION_ERROR',
150
+ path: '$.policy.network',
151
+ message: 'expected "deny" or an object with an "allow" list',
152
+ });
153
+ }
154
+ else {
155
+ const allow = network.allow;
156
+ if (!Array.isArray(allow) || allow.length === 0) {
157
+ bag.add({
158
+ code: 'SPEC_VALIDATION_ERROR',
159
+ path: '$.policy.network.allow',
160
+ message: 'expected a non-empty list of capabilities',
161
+ details: { allowed: [...NETWORK_CAPABILITIES] },
162
+ });
163
+ }
164
+ else {
165
+ const capabilities = [];
166
+ for (let index = 0; index < allow.length; index += 1) {
167
+ const capability = allow[index];
168
+ if (typeof capability !== 'string' ||
169
+ !NETWORK_CAPABILITIES.includes(capability)) {
170
+ bag.add({
171
+ code: 'POLICY_VIOLATION',
172
+ path: pathIndex('$.policy.network.allow', index),
173
+ message: `unknown network capability "${String(capability)}"`,
174
+ details: { allowed: [...NETWORK_CAPABILITIES] },
175
+ });
176
+ continue;
177
+ }
178
+ capabilities.push(capability);
179
+ }
180
+ // A provider list is the second, narrower gate for remote media:
181
+ // declaring providers means only those hosts may be referenced.
182
+ const providerValue = network.providers;
183
+ const providers = [];
184
+ if (providerValue !== undefined) {
185
+ if (!Array.isArray(providerValue) || providerValue.length === 0) {
186
+ bag.add({
187
+ code: 'SPEC_VALIDATION_ERROR',
188
+ path: '$.policy.network.providers',
189
+ message: 'expected a non-empty list of provider names',
190
+ details: { allowed: [...EMBED_PROVIDER_NAMES] },
191
+ });
192
+ }
193
+ else {
194
+ for (let index = 0; index < providerValue.length; index += 1) {
195
+ const provider = providerValue[index];
196
+ if (typeof provider !== 'string' || !isEmbedProvider(provider)) {
197
+ bag.add({
198
+ code: 'POLICY_VIOLATION',
199
+ path: pathIndex('$.policy.network.providers', index),
200
+ message: `unknown media provider "${String(provider)}"`,
201
+ details: { allowed: [...EMBED_PROVIDER_NAMES] },
202
+ });
203
+ continue;
204
+ }
205
+ providers.push(provider);
206
+ }
207
+ }
208
+ }
209
+ for (const key of Object.keys(network)) {
210
+ if (key !== 'allow' && key !== 'providers') {
211
+ bag.add({
212
+ code: 'SPEC_VALIDATION_ERROR',
213
+ path: pathKey('$.policy.network', key),
214
+ message: `unknown network policy field "${key}"`,
215
+ details: { allowed: ['allow', 'providers'] },
216
+ });
217
+ }
218
+ }
219
+ policy =
220
+ providers.length === 0
221
+ ? { network: { allow: capabilities } }
222
+ : { network: { allow: capabilities, providers } };
223
+ }
224
+ }
225
+ }
226
+ for (const key of Object.keys(policyValue)) {
227
+ if (key !== 'network') {
228
+ bag.add({
229
+ code: 'SPEC_VALIDATION_ERROR',
230
+ path: pathKey('$.policy', key),
231
+ message: `unknown policy field "${key}"`,
232
+ details: { allowed: ['network'] },
233
+ });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ const stateValue = document.state;
239
+ const state = {};
240
+ if (stateValue !== undefined) {
241
+ if (!isPlainObject(stateValue)) {
242
+ bag.add({
243
+ code: 'SPEC_VALIDATION_ERROR',
244
+ path: '$.state',
245
+ message: 'expected a state object',
246
+ });
247
+ }
248
+ else {
249
+ for (const [key, value] of Object.entries(stateValue)) {
250
+ if (typeof value === 'object' && value !== null) {
251
+ bag.add({
252
+ code: 'SPEC_VALIDATION_ERROR',
253
+ path: pathKey('$.state', key),
254
+ message: 'state values must be a string, number, boolean, or null',
255
+ });
256
+ continue;
257
+ }
258
+ state[key] = value;
259
+ }
260
+ }
261
+ }
262
+ const blocks = document.blocks;
263
+ if (!Array.isArray(blocks) || blocks.length === 0) {
264
+ bag.add({
265
+ code: 'SPEC_VALIDATION_ERROR',
266
+ path: '$.blocks',
267
+ message: 'expected a non-empty array of blocks',
268
+ });
269
+ return undefined;
270
+ }
271
+ return {
272
+ meta: {
273
+ title: title ?? 'Untitled page',
274
+ locale,
275
+ ...(description === undefined ? {} : { description }),
276
+ ...(slug === undefined ? {} : { slug }),
277
+ },
278
+ theme,
279
+ policy,
280
+ state,
281
+ blocks,
282
+ };
283
+ }
284
+ function resolveNodeId(definition, authorId, path, context) {
285
+ if (authorId !== undefined) {
286
+ if (typeof authorId !== 'string' || !NODE_ID_PATTERN.test(authorId)) {
287
+ context.bag.add({
288
+ code: 'SPEC_VALIDATION_ERROR',
289
+ path: pathKey(path, 'id'),
290
+ message: `"${String(authorId)}" is not a valid id (lowercase letters, digits and dashes, 64 max)`,
291
+ });
292
+ }
293
+ else if (context.usedIds.has(authorId)) {
294
+ context.bag.add({
295
+ code: 'SPEC_VALIDATION_ERROR',
296
+ path: pathKey(path, 'id'),
297
+ message: `duplicate node id "${authorId}"`,
298
+ });
299
+ }
300
+ else {
301
+ context.usedIds.add(authorId);
302
+ return { id: authorId, authorSupplied: true };
303
+ }
304
+ }
305
+ const base = derivedNodeId(definition.type, path);
306
+ let candidate = base;
307
+ let suffix = 2;
308
+ while (context.usedIds.has(candidate)) {
309
+ candidate = `${base}-${suffix}`;
310
+ suffix += 1;
311
+ }
312
+ context.usedIds.add(candidate);
313
+ return { id: candidate, authorSupplied: false };
314
+ }
315
+ function buildNode(entry, parentId, depth, path, context) {
316
+ if (!isPlainObject(entry)) {
317
+ context.bag.add({ code: 'SPEC_VALIDATION_ERROR', path, message: 'expected a block object' });
318
+ return undefined;
319
+ }
320
+ const typeValue = entry.type;
321
+ if (typeof typeValue !== 'string') {
322
+ context.bag.add({
323
+ code: 'SPEC_VALIDATION_ERROR',
324
+ path: pathKey(path, 'type'),
325
+ message: 'each block requires a "type"',
326
+ });
327
+ return undefined;
328
+ }
329
+ const definition = getBlockDefinition(typeValue);
330
+ if (definition === undefined) {
331
+ context.bag.add({
332
+ code: 'SPEC_UNKNOWN_BLOCK',
333
+ path: pathKey(path, 'type'),
334
+ message: `unknown block type "${typeValue}"`,
335
+ details: { type: typeValue, known: blockTypes() },
336
+ });
337
+ return undefined;
338
+ }
339
+ const childSlot = definition.slots?.children;
340
+ const rawProps = {};
341
+ let childBlocks;
342
+ for (const [key, value] of Object.entries(entry)) {
343
+ if (key === 'type')
344
+ continue;
345
+ if (key === 'blocks') {
346
+ childBlocks = value;
347
+ continue;
348
+ }
349
+ rawProps[key] = value;
350
+ }
351
+ if (childBlocks !== undefined && childSlot === undefined) {
352
+ context.bag.add({
353
+ code: 'SPEC_VALIDATION_ERROR',
354
+ path: pathKey(path, 'blocks'),
355
+ message: `block type "${typeValue}" does not accept child blocks`,
356
+ });
357
+ }
358
+ const resolved = resolveNodeId(definition, entry.id, path, context);
359
+ const validated = validateProps(definition.props, rawProps, path, context.bag);
360
+ const withBindings = validateBindingsDeep(validated, path, context.bag);
361
+ const props = (isPlainObject(withBindings) ? withBindings : {});
362
+ const rawBindings = props.on;
363
+ const bindings = isPlainObject(rawBindings)
364
+ ? rawBindings
365
+ : {};
366
+ if (rawBindings !== undefined)
367
+ delete props.on;
368
+ const node = {
369
+ id: resolved.id,
370
+ type: definition.type,
371
+ typeVersion: definition.version,
372
+ kind: definition.kind,
373
+ path,
374
+ parentId,
375
+ depth,
376
+ props,
377
+ bindings,
378
+ children: [],
379
+ a11y: definition.a11y,
380
+ runtimeFeatures: [...definition.runtimeFeatures],
381
+ assets: [...definition.assets],
382
+ network: definition.network,
383
+ };
384
+ context.nodes.push(node);
385
+ if (childSlot !== undefined && childBlocks !== undefined) {
386
+ if (!Array.isArray(childBlocks)) {
387
+ context.bag.add({
388
+ code: 'SPEC_VALIDATION_ERROR',
389
+ path: pathKey(path, 'blocks'),
390
+ message: 'expected an array of child blocks',
391
+ });
392
+ }
393
+ else {
394
+ const min = childSlot.min ?? 0;
395
+ const max = childSlot.max ?? Number.MAX_SAFE_INTEGER;
396
+ if (childBlocks.length < min || childBlocks.length > max) {
397
+ context.bag.add({
398
+ code: 'SPEC_VALIDATION_ERROR',
399
+ path: pathKey(path, 'blocks'),
400
+ message: `slot accepts ${min}-${max} child blocks, received ${childBlocks.length}`,
401
+ });
402
+ }
403
+ const childPath = pathKey(path, 'blocks');
404
+ for (let index = 0; index < childBlocks.length; index += 1) {
405
+ const childId = buildNode(childBlocks[index], resolved.id, depth + 1, pathIndex(childPath, index), context);
406
+ if (childId !== undefined)
407
+ node.children.push(childId);
408
+ }
409
+ }
410
+ }
411
+ return resolved.id;
412
+ }
413
+ function numberProp(node, key) {
414
+ const value = node.props[key];
415
+ return typeof value === 'number' ? value : undefined;
416
+ }
417
+ function stringProp(node, key) {
418
+ const value = node.props[key];
419
+ return typeof value === 'string' ? value : undefined;
420
+ }
421
+ /** Checks that need more than one field, or more than one node. */
422
+ function postChecks(nodes, bag) {
423
+ let previousHeadingLevel;
424
+ for (const node of nodes) {
425
+ if (node.type === 'heading') {
426
+ const level = numberProp(node, 'level') ?? 2;
427
+ if (previousHeadingLevel === undefined && level > 1) {
428
+ bag.add({
429
+ code: 'SPEC_VALIDATION_ERROR',
430
+ severity: 'warning',
431
+ message: 'the first heading is not a level-1 heading',
432
+ path: node.path,
433
+ nodeId: node.id,
434
+ });
435
+ }
436
+ else if (previousHeadingLevel !== undefined && level > previousHeadingLevel + 1) {
437
+ bag.add({
438
+ code: 'SPEC_VALIDATION_ERROR',
439
+ severity: 'warning',
440
+ message: `heading level jumps from ${previousHeadingLevel} to ${level}`,
441
+ path: node.path,
442
+ nodeId: node.id,
443
+ });
444
+ }
445
+ previousHeadingLevel = level;
446
+ continue;
447
+ }
448
+ if (node.type === 'slider') {
449
+ const min = numberProp(node, 'min') ?? 0;
450
+ const max = numberProp(node, 'max') ?? 100;
451
+ const step = numberProp(node, 'step') ?? 1;
452
+ if (max <= min) {
453
+ bag.add({
454
+ code: 'SPEC_VALIDATION_ERROR',
455
+ message: `slider max (${max}) must be greater than min (${min})`,
456
+ path: pathKey(node.path, 'max'),
457
+ nodeId: node.id,
458
+ });
459
+ }
460
+ if (step > max - min) {
461
+ bag.add({
462
+ code: 'SPEC_VALIDATION_ERROR',
463
+ message: `slider step (${step}) is larger than its range (${max - min})`,
464
+ path: pathKey(node.path, 'step'),
465
+ nodeId: node.id,
466
+ });
467
+ }
468
+ }
469
+ if (node.type === 'table') {
470
+ const columns = node.props.columns;
471
+ const rows = node.props.rows;
472
+ if (Array.isArray(columns) && Array.isArray(rows)) {
473
+ for (let index = 0; index < rows.length; index += 1) {
474
+ const row = rows[index];
475
+ if (Array.isArray(row) && row.length !== columns.length) {
476
+ bag.add({
477
+ code: 'SPEC_VALIDATION_ERROR',
478
+ severity: 'warning',
479
+ message: `row has ${row.length} cells but the table declares ${columns.length} columns`,
480
+ path: pathIndex(pathKey(node.path, 'rows'), index),
481
+ nodeId: node.id,
482
+ });
483
+ }
484
+ }
485
+ }
486
+ }
487
+ if (node.type === 'chart' || node.type === 'progress') {
488
+ const labels = node.props.labels;
489
+ const series = node.props.series;
490
+ if (Array.isArray(labels) && Array.isArray(series)) {
491
+ for (let index = 0; index < series.length; index += 1) {
492
+ const entry = series[index];
493
+ if (!isPlainObject(entry))
494
+ continue;
495
+ const values = entry.values;
496
+ if (Array.isArray(values) && values.length !== labels.length) {
497
+ bag.add({
498
+ code: 'SPEC_VALIDATION_ERROR',
499
+ severity: 'warning',
500
+ message: `series "${String(entry.label ?? index)}" has ${values.length} values for ${labels.length} labels`,
501
+ path: pathIndex(pathKey(node.path, 'series'), index),
502
+ nodeId: node.id,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+ if (node.type === 'button' && Object.keys(node.bindings).length === 0) {
509
+ bag.add({
510
+ code: 'SPEC_VALIDATION_ERROR',
511
+ severity: 'warning',
512
+ message: 'button declares no action bindings and will render inert',
513
+ path: node.path,
514
+ nodeId: node.id,
515
+ });
516
+ }
517
+ if (node.type === 'link') {
518
+ const href = stringProp(node, 'href');
519
+ if (href?.startsWith('http://')) {
520
+ bag.add({
521
+ code: 'POLICY_VIOLATION',
522
+ severity: 'warning',
523
+ message: 'insecure http:// link',
524
+ path: pathKey(node.path, 'href'),
525
+ nodeId: node.id,
526
+ });
527
+ }
528
+ }
529
+ if (node.type === 'diagram-panel') {
530
+ const spec = node.props.spec;
531
+ if (!isPlainObject(spec)) {
532
+ bag.add({
533
+ code: 'SPEC_VALIDATION_ERROR',
534
+ message: 'diagram spec must be an object',
535
+ path: pathKey(node.path, 'spec'),
536
+ nodeId: node.id,
537
+ });
538
+ }
539
+ }
540
+ }
541
+ }
542
+ /** Build the IR without throwing: diagnostics are returned, not raised. */
543
+ export function normalizeSpec(input, options = {}) {
544
+ const bag = new DiagnosticBag();
545
+ // The pipeline starts at the input boundary: a spec may arrive as JSON or
546
+ // YAML text, or as an already-parsed object.
547
+ let document = input;
548
+ if (typeof input === 'string') {
549
+ try {
550
+ document = parseSpec(input, options);
551
+ }
552
+ catch (error) {
553
+ if (isRenderError(error)) {
554
+ bag.add({
555
+ code: error.code,
556
+ path: error.path ?? '$',
557
+ message: error.message,
558
+ });
559
+ return { ir: emptyIr(), diagnostics: bag.list(), bounds: emptyBounds() };
560
+ }
561
+ throw error;
562
+ }
563
+ }
564
+ const bounds = checkBounds(document, bag);
565
+ scanForbiddenKeys(document, bag);
566
+ const migrated = migrateSpec(document, bag);
567
+ if (migrated === undefined) {
568
+ return { ir: emptyIr(), diagnostics: bag.list(), bounds };
569
+ }
570
+ const envelope = normalizeEnvelope(migrated.document, bag);
571
+ const context = { bag, nodes: [], usedIds: new Set() };
572
+ const rootChildren = [];
573
+ if (envelope !== undefined) {
574
+ for (let index = 0; index < envelope.blocks.length; index += 1) {
575
+ const childId = buildNode(envelope.blocks[index], 'page', 1, pathIndex('$.blocks', index), context);
576
+ if (childId !== undefined)
577
+ rootChildren.push(childId);
578
+ }
579
+ }
580
+ postChecks(context.nodes, bag);
581
+ const root = {
582
+ id: 'page',
583
+ type: 'page',
584
+ typeVersion: 1,
585
+ kind: 'semantic',
586
+ path: '$',
587
+ parentId: null,
588
+ depth: 0,
589
+ props: envelope === undefined ? {} : { title: envelope.meta.title },
590
+ bindings: {},
591
+ children: rootChildren,
592
+ a11y: 'Emits one main landmark, a document title, and a language attribute.',
593
+ runtimeFeatures: [],
594
+ assets: [],
595
+ network: 'none',
596
+ };
597
+ const ir = {
598
+ irVersion: 1,
599
+ specVersion: migrated.originalVersion === 0 ? 1 : migrated.originalVersion,
600
+ compilerVersion: VERSION,
601
+ meta: envelope?.meta ?? { title: 'Untitled page', locale: 'en' },
602
+ theme: envelope?.theme ?? { preset: DEFAULT_THEME_PRESET },
603
+ policy: envelope?.policy ?? { network: 'deny' },
604
+ state: envelope?.state ?? {},
605
+ rootId: root.id,
606
+ nodes: [root, ...context.nodes],
607
+ warnings: bag.warnings(),
608
+ };
609
+ return { ir, diagnostics: bag.list(), bounds };
610
+ }
611
+ function emptyBounds() {
612
+ return { blocks: 0, nodes: 0, depth: 0, bytes: 0, longestString: 0, largestList: 0 };
613
+ }
614
+ function emptyIr() {
615
+ return {
616
+ irVersion: 1,
617
+ specVersion: 1,
618
+ compilerVersion: VERSION,
619
+ meta: { title: 'Untitled page', locale: 'en' },
620
+ theme: { preset: DEFAULT_THEME_PRESET },
621
+ policy: { network: 'deny' },
622
+ state: {},
623
+ rootId: 'page',
624
+ nodes: [],
625
+ warnings: [],
626
+ };
627
+ }
628
+ /**
629
+ * Normalize or throw.
630
+ *
631
+ * The thrown error carries the first error's code and path, and lists every
632
+ * diagnostic in `details.diagnostics` so a caller can render a full report.
633
+ */
634
+ export function normalize(input, options = {}) {
635
+ const result = normalizeSpec(input, options);
636
+ const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
637
+ const first = errors[0];
638
+ if (first !== undefined) {
639
+ throw new RenderError(first.code, first.message, {
640
+ path: first.path,
641
+ ...(first.nodeId === undefined ? {} : { nodeId: first.nodeId }),
642
+ // Keep the first error's own details usable (`allowed`, `scheme`, ...)
643
+ // and attach the full ordered report alongside them.
644
+ details: { ...(first.details ?? {}), diagnostics: errors },
645
+ });
646
+ }
647
+ return result.ir;
648
+ }
649
+ //# sourceMappingURL=normalize.js.map