@nubbin/core 0.1.0-rc.5 → 0.1.0-rc.7
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/index.d.ts +238 -62
- package/dist/index.js +602 -137
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
+
// src/NubbinIssueCode.ts
|
|
2
|
+
var NubbinIssueCode = {
|
|
3
|
+
// Registration — a block, catalog or registry the developer wrote cannot be used as written.
|
|
4
|
+
BlockVersion: "block-version",
|
|
5
|
+
SlotBounds: "slot-bounds",
|
|
6
|
+
SlotAllowUnknown: "slot-allow-unknown",
|
|
7
|
+
DuplicateBlockName: "duplicate-block-name",
|
|
8
|
+
InvalidDefaults: "invalid-defaults",
|
|
9
|
+
HintPathUnresolvable: "hint-path-unresolvable",
|
|
10
|
+
HintNotAddressable: "hint-not-addressable",
|
|
11
|
+
// Schema — what the consumer brought does not answer the one door core reads a schema through.
|
|
12
|
+
NotStandardSchema: "not-standard-schema",
|
|
13
|
+
NoJsonSchema: "no-json-schema",
|
|
14
|
+
// Structure — the document's graph cannot become a tree.
|
|
15
|
+
NoRoots: "no-roots",
|
|
16
|
+
UnknownBlock: "unknown-block",
|
|
17
|
+
DanglingChild: "dangling-child",
|
|
18
|
+
Cycle: "cycle",
|
|
19
|
+
Unreachable: "unreachable",
|
|
20
|
+
SlotNotAllowed: "slot-not-allowed",
|
|
21
|
+
SlotMin: "slot-min",
|
|
22
|
+
SlotMax: "slot-max",
|
|
23
|
+
// Props — the values a node carries, judged against the block's schema.
|
|
24
|
+
InvalidProps: "invalid-props",
|
|
25
|
+
UnknownProp: "unknown-prop",
|
|
26
|
+
// Document operations — the caller named something the document does not hold.
|
|
27
|
+
NoSuchNode: "no-such-node",
|
|
28
|
+
DuplicateNodeId: "duplicate-node-id",
|
|
29
|
+
PathNotAddressable: "path-not-addressable",
|
|
30
|
+
InvalidRoute: "invalid-route",
|
|
31
|
+
// Render — the artifact and the registry serving it disagree.
|
|
32
|
+
BlockNotLoaded: "block-not-loaded",
|
|
33
|
+
NoHoleResolver: "no-hole-resolver",
|
|
34
|
+
NotOneHostElement: "not-one-host-element",
|
|
35
|
+
// Store — a write the store cannot honour.
|
|
36
|
+
ArtifactNotStored: "artifact-not-stored"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/summariseIssues.ts
|
|
40
|
+
function summariseIssues(issues) {
|
|
41
|
+
const lines = issues.map(
|
|
42
|
+
(issue) => issue.at === void 0 ? `${issue.message} [${issue.code}]` : `${issue.at} [${issue.code}]: ${issue.message}`
|
|
43
|
+
);
|
|
44
|
+
const only = lines[0];
|
|
45
|
+
if (lines.length === 1 && only !== void 0) {
|
|
46
|
+
return only;
|
|
47
|
+
}
|
|
48
|
+
return `Refused with ${issues.length} issue(s):
|
|
49
|
+
${lines.join("\n")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/NubbinError.ts
|
|
53
|
+
var NubbinError = class extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* The first issue's code, so the common case reads as
|
|
56
|
+
* `error.code === NubbinIssueCode.UnknownProp` with no array to index and no string to typo.
|
|
57
|
+
* Every refusal but `compile`'s carries exactly one issue; read `issues` for the rest.
|
|
58
|
+
*/
|
|
59
|
+
code;
|
|
60
|
+
issues;
|
|
61
|
+
constructor(issues) {
|
|
62
|
+
const first = issues[0];
|
|
63
|
+
if (first === void 0) {
|
|
64
|
+
throw new Error("NubbinError needs an issue \u2014 a refusal with no cause names nothing");
|
|
65
|
+
}
|
|
66
|
+
super(summariseIssues(issues));
|
|
67
|
+
this.name = "NubbinError";
|
|
68
|
+
this.code = first.code;
|
|
69
|
+
this.issues = issues;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/refuse.ts
|
|
74
|
+
function refuse(code, message, at) {
|
|
75
|
+
throw new NubbinError([at === void 0 ? { code, message } : { code, message, at }]);
|
|
76
|
+
}
|
|
77
|
+
|
|
1
78
|
// src/adapters/isStandardJsonSchemaCapable.ts
|
|
2
79
|
function isStandardJsonSchemaCapable(value) {
|
|
3
80
|
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
@@ -16,7 +93,10 @@ var OPTIONS = {
|
|
|
16
93
|
};
|
|
17
94
|
function projectJsonSchema(schema) {
|
|
18
95
|
if (!isStandardJsonSchemaCapable(schema)) {
|
|
19
|
-
|
|
96
|
+
refuse(
|
|
97
|
+
NubbinIssueCode.NoJsonSchema,
|
|
98
|
+
"Schema does not expose the Standard JSON Schema converter (spec >= 1.1)"
|
|
99
|
+
);
|
|
20
100
|
}
|
|
21
101
|
return schema["~standard"].jsonSchema.input(OPTIONS);
|
|
22
102
|
}
|
|
@@ -106,17 +186,47 @@ var zodAdapter = {
|
|
|
106
186
|
}
|
|
107
187
|
};
|
|
108
188
|
|
|
109
|
-
// src/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
${
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
// src/requireNode.ts
|
|
190
|
+
function requireNode(version, nodeId) {
|
|
191
|
+
const node = version.elements[nodeId];
|
|
192
|
+
if (node === void 0) {
|
|
193
|
+
refuse(
|
|
194
|
+
NubbinIssueCode.NoSuchNode,
|
|
195
|
+
`no node "${nodeId}" in document "${version.documentId}"`,
|
|
196
|
+
nodeId
|
|
197
|
+
);
|
|
118
198
|
}
|
|
119
|
-
|
|
199
|
+
return node;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/withElements.ts
|
|
203
|
+
function withElements(version, ...nodes) {
|
|
204
|
+
const elements = { ...version.elements };
|
|
205
|
+
for (const node of nodes) {
|
|
206
|
+
elements[node.id] = node;
|
|
207
|
+
}
|
|
208
|
+
return { ...version, elements };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/withSlotChild.ts
|
|
212
|
+
function withSlotChild(node, slot, childId, index) {
|
|
213
|
+
const children = [...node.slots?.[slot] ?? []];
|
|
214
|
+
children.splice(index ?? children.length, 0, childId);
|
|
215
|
+
return { ...node, slots: { ...node.slots, [slot]: children } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/addNode.ts
|
|
219
|
+
function addNode(version, parentId, slot, node, index) {
|
|
220
|
+
const parent = requireNode(version, parentId);
|
|
221
|
+
if (version.elements[node.id] !== void 0) {
|
|
222
|
+
refuse(
|
|
223
|
+
NubbinIssueCode.DuplicateNodeId,
|
|
224
|
+
`document "${version.documentId}" already holds a node "${node.id}"`,
|
|
225
|
+
node.id
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return withElements(version, withSlotChild(parent, slot, node.id, index), node);
|
|
229
|
+
}
|
|
120
230
|
|
|
121
231
|
// src/checkRollback.ts
|
|
122
232
|
function checkRollback(artifact, registry) {
|
|
@@ -152,6 +262,43 @@ function checkCompatibility(live, registry) {
|
|
|
152
262
|
return { checked: live.length, compatible: incompatible.length === 0, incompatible };
|
|
153
263
|
}
|
|
154
264
|
|
|
265
|
+
// src/routeSegmentIssue.ts
|
|
266
|
+
var SEGMENT = /^[A-Za-z0-9\-._~%!$&'()+,;=:@]+$/;
|
|
267
|
+
var PARAM = /^\[[A-Za-z0-9_-]+\]$/;
|
|
268
|
+
function routeSegmentIssue(segment, isLast) {
|
|
269
|
+
if (segment === "") {
|
|
270
|
+
return "a segment names nothing";
|
|
271
|
+
}
|
|
272
|
+
if (segment === "*") {
|
|
273
|
+
return isLast ? void 0 : "a prefix star is only the final segment";
|
|
274
|
+
}
|
|
275
|
+
if (segment.startsWith("[") || segment.endsWith("]")) {
|
|
276
|
+
return PARAM.test(segment) ? void 0 : `"${segment}" is not a param segment`;
|
|
277
|
+
}
|
|
278
|
+
return SEGMENT.test(segment) ? void 0 : `"${segment}" carries a character a route cannot`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/assertValidRoute.ts
|
|
282
|
+
function assertValidRoute(route) {
|
|
283
|
+
const unaddressable = (why) => refuse(NubbinIssueCode.InvalidRoute, `route "${route}" is not addressable: ${why}`, route);
|
|
284
|
+
if (!route.startsWith("/")) {
|
|
285
|
+
unaddressable("a route starts at the root, with a slash");
|
|
286
|
+
}
|
|
287
|
+
if (route === "/") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (route.endsWith("/")) {
|
|
291
|
+
unaddressable("a trailing slash would key a second pointer to one page");
|
|
292
|
+
}
|
|
293
|
+
const segments = route.slice(1).split("/");
|
|
294
|
+
for (const [index, segment] of segments.entries()) {
|
|
295
|
+
const issue = routeSegmentIssue(segment, index === segments.length - 1);
|
|
296
|
+
if (issue !== void 0) {
|
|
297
|
+
unaddressable(issue);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
155
302
|
// src/artifactNodeOf.ts
|
|
156
303
|
function artifactNodeOf(node, resolve) {
|
|
157
304
|
const { props, holes } = resolve(node);
|
|
@@ -191,7 +338,7 @@ function wireSlots(version, built) {
|
|
|
191
338
|
|
|
192
339
|
// src/denormalize.ts
|
|
193
340
|
function denormalize(version, resolve) {
|
|
194
|
-
const pending = [version.
|
|
341
|
+
const pending = [...version.roots];
|
|
195
342
|
const built = /* @__PURE__ */ new Map();
|
|
196
343
|
while (pending.length > 0) {
|
|
197
344
|
const id = pending.pop();
|
|
@@ -204,22 +351,25 @@ function denormalize(version, resolve) {
|
|
|
204
351
|
}
|
|
205
352
|
}
|
|
206
353
|
wireSlots(version, built);
|
|
207
|
-
|
|
208
|
-
|
|
354
|
+
return version.roots.flatMap((id) => {
|
|
355
|
+
const root = built.get(id);
|
|
356
|
+
return root === void 0 ? [] : [root];
|
|
357
|
+
});
|
|
209
358
|
}
|
|
210
359
|
|
|
211
360
|
// src/fnv1a.ts
|
|
212
|
-
var OFFSET_BASIS =
|
|
213
|
-
var PRIME =
|
|
361
|
+
var OFFSET_BASIS = 14695981039346656037n;
|
|
362
|
+
var PRIME = 1099511628211n;
|
|
363
|
+
var MASK = 0xffffffffffffffffn;
|
|
214
364
|
var HEX_RADIX = 16;
|
|
215
|
-
var HEX_WIDTH =
|
|
365
|
+
var HEX_WIDTH = 16;
|
|
216
366
|
function fnv1a(input) {
|
|
217
367
|
let hash = OFFSET_BASIS;
|
|
218
368
|
for (let index = 0; index < input.length; index += 1) {
|
|
219
|
-
hash ^= input.charCodeAt(index);
|
|
220
|
-
hash =
|
|
369
|
+
hash ^= BigInt(input.charCodeAt(index));
|
|
370
|
+
hash = hash * PRIME & MASK;
|
|
221
371
|
}
|
|
222
|
-
return
|
|
372
|
+
return hash.toString(HEX_RADIX).padStart(HEX_WIDTH, "0");
|
|
223
373
|
}
|
|
224
374
|
|
|
225
375
|
// src/hashArtifact.ts
|
|
@@ -232,18 +382,66 @@ function hashArtifact(artifact) {
|
|
|
232
382
|
return fnv1a(JSON.stringify(artifact, sortKeys));
|
|
233
383
|
}
|
|
234
384
|
|
|
385
|
+
// src/isUnknownProps.ts
|
|
386
|
+
function isUnknownProps(value) {
|
|
387
|
+
if (typeof value !== "object") return false;
|
|
388
|
+
return value !== null && !Array.isArray(value);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/droppedKeyPaths.ts
|
|
392
|
+
function droppedKeyPaths(written, parsed, prefix = "") {
|
|
393
|
+
const dropped = [];
|
|
394
|
+
for (const [key, value] of Object.entries(written)) {
|
|
395
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
396
|
+
if (!Object.hasOwn(parsed, key)) {
|
|
397
|
+
dropped.push(path);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const kept = parsed[key];
|
|
401
|
+
if (isUnknownProps(value) && isUnknownProps(kept)) {
|
|
402
|
+
dropped.push(...droppedKeyPaths(value, kept, path));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return dropped;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/splitPath.ts
|
|
409
|
+
function splitPath(path) {
|
|
410
|
+
const [head, ...tail] = path.split(".");
|
|
411
|
+
if (head === void 0 || head === "" || head.includes("[]")) {
|
|
412
|
+
refuse(NubbinIssueCode.PathNotAddressable, `path "${path}" is not addressable`, path);
|
|
413
|
+
}
|
|
414
|
+
return { head, tail };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/takeAtPath.ts
|
|
418
|
+
function takeAtPath(target, path) {
|
|
419
|
+
const { head, tail } = splitPath(path);
|
|
420
|
+
if (!Object.hasOwn(target, head)) return { rest: target, taken: false };
|
|
421
|
+
if (tail.length === 0) {
|
|
422
|
+
const remaining = { ...target };
|
|
423
|
+
delete remaining[head];
|
|
424
|
+
return { rest: remaining, taken: true };
|
|
425
|
+
}
|
|
426
|
+
const child = target[head];
|
|
427
|
+
if (typeof child !== "object" || child === null || Array.isArray(child)) {
|
|
428
|
+
return { rest: target, taken: false };
|
|
429
|
+
}
|
|
430
|
+
const inner = takeAtPath(child, tail.join("."));
|
|
431
|
+
if (!inner.taken) return { rest: target, taken: false };
|
|
432
|
+
return { rest: { ...target, [head]: inner.rest }, taken: true };
|
|
433
|
+
}
|
|
434
|
+
|
|
235
435
|
// src/partitionProps.ts
|
|
236
436
|
function partitionProps(validated, hints) {
|
|
237
|
-
|
|
437
|
+
let props = { ...validated };
|
|
238
438
|
const holes = {};
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
holes[key] = data;
|
|
246
|
-
}
|
|
439
|
+
for (const [path, hint] of Object.entries(hints?.fields ?? {})) {
|
|
440
|
+
if (hint.data === void 0) continue;
|
|
441
|
+
const { rest, taken } = takeAtPath(props, path);
|
|
442
|
+
if (!taken) continue;
|
|
443
|
+
props = rest;
|
|
444
|
+
holes[path] = hint.data;
|
|
247
445
|
}
|
|
248
446
|
return { props, holes };
|
|
249
447
|
}
|
|
@@ -256,12 +454,6 @@ function formatIssuePath(path) {
|
|
|
256
454
|
).join(".");
|
|
257
455
|
}
|
|
258
456
|
|
|
259
|
-
// src/isUnknownProps.ts
|
|
260
|
-
function isUnknownProps(value) {
|
|
261
|
-
if (typeof value !== "object") return false;
|
|
262
|
-
return value !== null && !Array.isArray(value);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
457
|
// src/isStandardSchema.ts
|
|
266
458
|
function isStandardSchema(value) {
|
|
267
459
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -274,11 +466,15 @@ function isStandardSchema(value) {
|
|
|
274
466
|
// src/standardValidate.ts
|
|
275
467
|
function standardValidate(schema, value) {
|
|
276
468
|
if (!isStandardSchema(schema)) {
|
|
277
|
-
|
|
469
|
+
refuse(
|
|
470
|
+
NubbinIssueCode.NotStandardSchema,
|
|
471
|
+
"Schema does not implement Standard Schema (`~standard.validate`)"
|
|
472
|
+
);
|
|
278
473
|
}
|
|
279
474
|
const result = schema["~standard"].validate(value);
|
|
280
475
|
if (result instanceof Promise) {
|
|
281
|
-
|
|
476
|
+
refuse(
|
|
477
|
+
NubbinIssueCode.NotStandardSchema,
|
|
282
478
|
"Schema validates asynchronously; compile and registration require synchronous validation"
|
|
283
479
|
);
|
|
284
480
|
}
|
|
@@ -290,9 +486,9 @@ function validateNodeProps(node, schema) {
|
|
|
290
486
|
const result = standardValidate(schema, node.props);
|
|
291
487
|
if (result.issues !== void 0) {
|
|
292
488
|
const issues = result.issues.map((issue) => ({
|
|
293
|
-
|
|
489
|
+
at: node.id,
|
|
294
490
|
path: formatIssuePath(issue.path),
|
|
295
|
-
code:
|
|
491
|
+
code: NubbinIssueCode.InvalidProps,
|
|
296
492
|
message: issue.message
|
|
297
493
|
}));
|
|
298
494
|
return { issues };
|
|
@@ -301,9 +497,9 @@ function validateNodeProps(node, schema) {
|
|
|
301
497
|
return {
|
|
302
498
|
issues: [
|
|
303
499
|
{
|
|
304
|
-
|
|
500
|
+
at: node.id,
|
|
305
501
|
path: "",
|
|
306
|
-
code:
|
|
502
|
+
code: NubbinIssueCode.InvalidProps,
|
|
307
503
|
message: "block props must parse to an object"
|
|
308
504
|
}
|
|
309
505
|
]
|
|
@@ -316,20 +512,29 @@ function validateNodeProps(node, schema) {
|
|
|
316
512
|
function resolveAllProps(version, catalog) {
|
|
317
513
|
const resolved = /* @__PURE__ */ new Map();
|
|
318
514
|
const issues = [];
|
|
515
|
+
const reported = [];
|
|
319
516
|
for (const node of Object.values(version.elements)) {
|
|
320
517
|
const entry = catalog[node.block];
|
|
321
518
|
if (entry === void 0) {
|
|
322
519
|
const message = `"${node.block}" has no catalog entry, so its props cannot be validated`;
|
|
323
|
-
issues.push({ code:
|
|
520
|
+
issues.push({ code: NubbinIssueCode.UnknownBlock, message, at: node.id, path: "block" });
|
|
324
521
|
continue;
|
|
325
522
|
}
|
|
326
523
|
const { value, issues: propIssues } = validateNodeProps(node, entry.schema);
|
|
327
524
|
issues.push(...propIssues);
|
|
328
525
|
if (value !== void 0) {
|
|
526
|
+
for (const path of droppedKeyPaths(node.props, value)) {
|
|
527
|
+
reported.push({
|
|
528
|
+
code: NubbinIssueCode.UnknownProp,
|
|
529
|
+
message: `"${path}" is not a field of ${node.block}, so the schema did not keep it`,
|
|
530
|
+
at: node.id,
|
|
531
|
+
path
|
|
532
|
+
});
|
|
533
|
+
}
|
|
329
534
|
resolved.set(node.id, partitionProps(value, entry.ui));
|
|
330
535
|
}
|
|
331
536
|
}
|
|
332
|
-
return { resolved, issues };
|
|
537
|
+
return { resolved, issues, reported };
|
|
333
538
|
}
|
|
334
539
|
|
|
335
540
|
// src/usedBlockVersions.ts
|
|
@@ -352,12 +557,21 @@ function pushCycleFrame(stack, state, version, id) {
|
|
|
352
557
|
stack.push({ id, edges: slotEdges(node), next: 0 });
|
|
353
558
|
}
|
|
354
559
|
|
|
355
|
-
// src/
|
|
356
|
-
function
|
|
357
|
-
|
|
560
|
+
// src/toIssue.ts
|
|
561
|
+
function toIssue(code, message, at, path) {
|
|
562
|
+
return {
|
|
563
|
+
code,
|
|
564
|
+
message,
|
|
565
|
+
...at === void 0 ? {} : { at },
|
|
566
|
+
...path === void 0 ? {} : { path }
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/findCyclesFrom.ts
|
|
571
|
+
function findCyclesFrom(version, root, state) {
|
|
358
572
|
const stack = [];
|
|
359
573
|
const issues = [];
|
|
360
|
-
pushCycleFrame(stack, state, version,
|
|
574
|
+
pushCycleFrame(stack, state, version, root);
|
|
361
575
|
while (stack.length > 0) {
|
|
362
576
|
const frame = stack.at(-1);
|
|
363
577
|
if (frame === void 0) break;
|
|
@@ -369,12 +583,14 @@ function findCycles(version) {
|
|
|
369
583
|
}
|
|
370
584
|
frame.next += 1;
|
|
371
585
|
if (state.get(edge.childId) === "visiting") {
|
|
372
|
-
issues.push(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
586
|
+
issues.push(
|
|
587
|
+
toIssue(
|
|
588
|
+
NubbinIssueCode.Cycle,
|
|
589
|
+
`"${frame.id}" reaches back to "${edge.childId}", so the graph cannot flatten into a tree`,
|
|
590
|
+
frame.id,
|
|
591
|
+
`slots.${edge.slot}`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
378
594
|
} else if (!state.has(edge.childId)) {
|
|
379
595
|
pushCycleFrame(stack, state, version, edge.childId);
|
|
380
596
|
}
|
|
@@ -382,32 +598,58 @@ function findCycles(version) {
|
|
|
382
598
|
return issues;
|
|
383
599
|
}
|
|
384
600
|
|
|
601
|
+
// src/findCycles.ts
|
|
602
|
+
function findCycles(version) {
|
|
603
|
+
const state = /* @__PURE__ */ new Map();
|
|
604
|
+
return version.roots.flatMap(
|
|
605
|
+
(root) => state.has(root) ? [] : findCyclesFrom(version, root, state)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
385
609
|
// src/findDanglingChildren.ts
|
|
386
610
|
function findDanglingChildren(version) {
|
|
387
611
|
const issues = [];
|
|
388
|
-
if (version.elements[version.root] === void 0) {
|
|
389
|
-
issues.push({
|
|
390
|
-
nodeId: version.root,
|
|
391
|
-
path: "root",
|
|
392
|
-
code: "dangling-child",
|
|
393
|
-
message: `root "${version.root}" has no matching element`
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
612
|
for (const node of Object.values(version.elements)) {
|
|
397
613
|
for (const edge of slotEdges(node)) {
|
|
398
614
|
if (version.elements[edge.childId] === void 0) {
|
|
399
|
-
issues.push(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
615
|
+
issues.push(
|
|
616
|
+
toIssue(
|
|
617
|
+
NubbinIssueCode.DanglingChild,
|
|
618
|
+
`child "${edge.childId}" has no matching element`,
|
|
619
|
+
node.id,
|
|
620
|
+
`slots.${edge.slot}`
|
|
621
|
+
)
|
|
622
|
+
);
|
|
405
623
|
}
|
|
406
624
|
}
|
|
407
625
|
}
|
|
408
626
|
return issues;
|
|
409
627
|
}
|
|
410
628
|
|
|
629
|
+
// src/findRootIssues.ts
|
|
630
|
+
function findRootIssues(version) {
|
|
631
|
+
if (version.roots.length === 0) {
|
|
632
|
+
return [
|
|
633
|
+
{
|
|
634
|
+
at: "",
|
|
635
|
+
path: "roots",
|
|
636
|
+
code: NubbinIssueCode.NoRoots,
|
|
637
|
+
message: "a document needs at least one root, and this one names none"
|
|
638
|
+
}
|
|
639
|
+
];
|
|
640
|
+
}
|
|
641
|
+
return version.roots.flatMap(
|
|
642
|
+
(root) => version.elements[root] === void 0 ? [
|
|
643
|
+
{
|
|
644
|
+
at: root,
|
|
645
|
+
path: "roots",
|
|
646
|
+
code: NubbinIssueCode.DanglingChild,
|
|
647
|
+
message: `root "${root}" has no matching element`
|
|
648
|
+
}
|
|
649
|
+
] : []
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
411
653
|
// src/disallowedChildren.ts
|
|
412
654
|
function disallowedChildren(parent, path, childIds, allow, version) {
|
|
413
655
|
if (allow === void 0) return [];
|
|
@@ -416,9 +658,9 @@ function disallowedChildren(parent, path, childIds, allow, version) {
|
|
|
416
658
|
const child = version.elements[childId];
|
|
417
659
|
if (child === void 0 || allow.includes(child.block)) continue;
|
|
418
660
|
issues.push({
|
|
419
|
-
|
|
661
|
+
at: childId,
|
|
420
662
|
path,
|
|
421
|
-
code:
|
|
663
|
+
code: NubbinIssueCode.SlotNotAllowed,
|
|
422
664
|
message: `"${child.block}" is not allowed in ${path} of "${parent.block}"; allowed: ${allow.join(", ")}`
|
|
423
665
|
});
|
|
424
666
|
}
|
|
@@ -430,20 +672,20 @@ function slotBoundIssues(parentId, path, count, constraint) {
|
|
|
430
672
|
const { min, max } = constraint;
|
|
431
673
|
const bounds = [
|
|
432
674
|
{
|
|
433
|
-
code:
|
|
675
|
+
code: NubbinIssueCode.SlotMin,
|
|
434
676
|
limit: min,
|
|
435
677
|
breached: min !== void 0 && count < min,
|
|
436
678
|
sense: "at least"
|
|
437
679
|
},
|
|
438
680
|
{
|
|
439
|
-
code:
|
|
681
|
+
code: NubbinIssueCode.SlotMax,
|
|
440
682
|
limit: max,
|
|
441
683
|
breached: max !== void 0 && count > max,
|
|
442
684
|
sense: "at most"
|
|
443
685
|
}
|
|
444
686
|
];
|
|
445
687
|
return bounds.filter((bound) => bound.breached).map((bound) => ({
|
|
446
|
-
|
|
688
|
+
at: parentId,
|
|
447
689
|
path,
|
|
448
690
|
code: bound.code,
|
|
449
691
|
message: `${path} holds ${count} of ${bound.sense} ${bound.limit}`
|
|
@@ -456,9 +698,9 @@ function slotIssuesAt(parent, slotName, childIds, constraint, version) {
|
|
|
456
698
|
if (constraint === void 0) {
|
|
457
699
|
return [
|
|
458
700
|
{
|
|
459
|
-
|
|
701
|
+
at: parent.id,
|
|
460
702
|
path,
|
|
461
|
-
code:
|
|
703
|
+
code: NubbinIssueCode.SlotNotAllowed,
|
|
462
704
|
message: `"${parent.block}" declares no slot "${slotName}"`
|
|
463
705
|
}
|
|
464
706
|
];
|
|
@@ -485,28 +727,41 @@ function findSlotViolations(version, registry) {
|
|
|
485
727
|
// src/findUnknownBlocks.ts
|
|
486
728
|
function findUnknownBlocks(version, registry) {
|
|
487
729
|
return Object.values(version.elements).filter((node) => registry.get(node.block) === void 0).map((node) => ({
|
|
488
|
-
|
|
730
|
+
at: node.id,
|
|
489
731
|
path: "block",
|
|
490
|
-
code:
|
|
732
|
+
code: NubbinIssueCode.UnknownBlock,
|
|
491
733
|
message: `"${node.block}" is not a registered block`
|
|
492
734
|
}));
|
|
493
735
|
}
|
|
494
736
|
|
|
495
|
-
// src/
|
|
496
|
-
function
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
while (
|
|
500
|
-
const id =
|
|
501
|
-
const node = id === void 0 ? void 0 :
|
|
502
|
-
if (node === void 0)
|
|
737
|
+
// src/idsReachableFrom.ts
|
|
738
|
+
function idsReachableFrom(elements, seeds) {
|
|
739
|
+
const found = /* @__PURE__ */ new Set();
|
|
740
|
+
const pending = [...seeds];
|
|
741
|
+
while (pending.length > 0) {
|
|
742
|
+
const id = pending.pop();
|
|
743
|
+
const node = id === void 0 ? void 0 : elements[id];
|
|
744
|
+
if (node === void 0) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
503
747
|
for (const edge of slotEdges(node)) {
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
748
|
+
if (found.has(edge.childId)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
found.add(edge.childId);
|
|
752
|
+
pending.push(edge.childId);
|
|
507
753
|
}
|
|
508
754
|
}
|
|
509
|
-
return
|
|
755
|
+
return found;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/reachableIds.ts
|
|
759
|
+
function reachableIds(version) {
|
|
760
|
+
const reached = idsReachableFrom(version.elements, version.roots);
|
|
761
|
+
for (const root of version.roots) {
|
|
762
|
+
reached.add(root);
|
|
763
|
+
}
|
|
764
|
+
return reached;
|
|
510
765
|
}
|
|
511
766
|
|
|
512
767
|
// src/findUnreachable.ts
|
|
@@ -516,10 +771,10 @@ function findUnreachable(version) {
|
|
|
516
771
|
for (const node of Object.values(version.elements)) {
|
|
517
772
|
if (reached.has(node.id)) continue;
|
|
518
773
|
issues.push({
|
|
519
|
-
|
|
774
|
+
at: node.id,
|
|
520
775
|
path: "",
|
|
521
|
-
code:
|
|
522
|
-
message: `no slot reaches "${node.id}" from
|
|
776
|
+
code: NubbinIssueCode.Unreachable,
|
|
777
|
+
message: `no slot reaches "${node.id}" from any root`
|
|
523
778
|
});
|
|
524
779
|
}
|
|
525
780
|
return issues;
|
|
@@ -527,8 +782,11 @@ function findUnreachable(version) {
|
|
|
527
782
|
|
|
528
783
|
// src/validateStructure.ts
|
|
529
784
|
function validateStructure(version, registry) {
|
|
785
|
+
const rootIssues = findRootIssues(version);
|
|
786
|
+
if (version.roots.length === 0) return rootIssues;
|
|
530
787
|
return [
|
|
531
788
|
...findUnknownBlocks(version, registry),
|
|
789
|
+
...rootIssues,
|
|
532
790
|
...findDanglingChildren(version),
|
|
533
791
|
...findCycles(version),
|
|
534
792
|
...findUnreachable(version),
|
|
@@ -537,26 +795,26 @@ function validateStructure(version, registry) {
|
|
|
537
795
|
}
|
|
538
796
|
|
|
539
797
|
// src/version.constants.ts
|
|
540
|
-
var NUBBIN_VERSION = "0.1.0-rc.
|
|
798
|
+
var NUBBIN_VERSION = "0.1.0-rc.7";
|
|
541
799
|
|
|
542
800
|
// src/compile.ts
|
|
543
801
|
function compile(version, catalog, registry, route) {
|
|
802
|
+
assertValidRoute(route);
|
|
544
803
|
const structural = validateStructure(version, registry);
|
|
545
|
-
if (structural.length > 0) throw new
|
|
546
|
-
const { resolved, issues } = resolveAllProps(version, catalog);
|
|
547
|
-
if (issues.length > 0) throw new
|
|
804
|
+
if (structural.length > 0) throw new NubbinError(structural);
|
|
805
|
+
const { resolved, issues, reported } = resolveAllProps(version, catalog);
|
|
806
|
+
if (issues.length > 0) throw new NubbinError(issues);
|
|
548
807
|
const tree = denormalize(version, (node) => resolved.get(node.id) ?? { props: {}, holes: {} });
|
|
549
808
|
const content = {
|
|
550
809
|
route,
|
|
551
810
|
documentId: version.documentId,
|
|
552
811
|
documentVersion: version.version,
|
|
553
|
-
registryFingerprint: registry.fingerprint(),
|
|
554
812
|
blockVersions: usedBlockVersions(version, registry),
|
|
555
813
|
tree,
|
|
556
814
|
meta: version.meta,
|
|
557
815
|
compiledWith: NUBBIN_VERSION
|
|
558
816
|
};
|
|
559
|
-
return { ...content, hash: hashArtifact(content) };
|
|
817
|
+
return { artifact: { ...content, hash: hashArtifact(content) }, issues: reported };
|
|
560
818
|
}
|
|
561
819
|
|
|
562
820
|
// src/unknownAllowEntries.ts
|
|
@@ -571,7 +829,8 @@ function assertSlotAllows(blocks) {
|
|
|
571
829
|
const known = new Set(blocks.map((block) => block.name));
|
|
572
830
|
const unresolved = blocks.flatMap((block) => unknownAllowEntries(block, known));
|
|
573
831
|
if (unresolved.length > 0) {
|
|
574
|
-
|
|
832
|
+
refuse(
|
|
833
|
+
NubbinIssueCode.SlotAllowUnknown,
|
|
575
834
|
`Slot allow lists name ${unresolved.join(", ")}, which no registered block defines. Registered blocks: ${[...known].sort().join(", ")}`
|
|
576
835
|
);
|
|
577
836
|
}
|
|
@@ -582,37 +841,25 @@ function createRegistry(blocks) {
|
|
|
582
841
|
const byName = /* @__PURE__ */ new Map();
|
|
583
842
|
for (const block of blocks) {
|
|
584
843
|
if (byName.has(block.name)) {
|
|
585
|
-
|
|
586
|
-
|
|
844
|
+
refuse(
|
|
845
|
+
NubbinIssueCode.DuplicateBlockName,
|
|
846
|
+
`Duplicate block name "${block.name}" \u2014 names are the identity nodes resolve through`,
|
|
847
|
+
block.name
|
|
587
848
|
);
|
|
588
849
|
}
|
|
589
850
|
byName.set(block.name, block);
|
|
590
851
|
}
|
|
591
852
|
assertSlotAllows([...byName.values()]);
|
|
592
|
-
const signature = [...byName.values()].map((block) => `${block.name}@${block.version}`).sort().join("\n");
|
|
593
|
-
const fingerprint = fnv1a(signature);
|
|
594
853
|
return {
|
|
595
854
|
get: (name) => byName.get(name),
|
|
596
|
-
names: () => [...byName.keys()]
|
|
597
|
-
fingerprint: () => fingerprint
|
|
855
|
+
names: () => [...byName.keys()]
|
|
598
856
|
};
|
|
599
857
|
}
|
|
600
858
|
|
|
601
859
|
// src/assertBlockVersion.ts
|
|
602
860
|
function assertBlockVersion(name, version) {
|
|
603
861
|
if (!Number.isInteger(version) || version < 1) {
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// src/assertMigrateKeys.ts
|
|
609
|
-
var FIRST_MIGRATABLE_VERSION = 2;
|
|
610
|
-
function assertMigrateKeys(name, version, migrate) {
|
|
611
|
-
for (const key of Object.keys(migrate ?? {})) {
|
|
612
|
-
const target = Number(key);
|
|
613
|
-
if (target < FIRST_MIGRATABLE_VERSION || target > version) {
|
|
614
|
-
throw new Error(`${name}: migrate key ${key} is outside the reachable range 2..${version}`);
|
|
615
|
-
}
|
|
862
|
+
refuse(NubbinIssueCode.BlockVersion, "version must be an integer of 1 or more", name);
|
|
616
863
|
}
|
|
617
864
|
}
|
|
618
865
|
|
|
@@ -620,7 +867,7 @@ function assertMigrateKeys(name, version, migrate) {
|
|
|
620
867
|
function assertSlotBounds(name, slots) {
|
|
621
868
|
for (const [slot, { min, max }] of Object.entries(slots)) {
|
|
622
869
|
if (min !== void 0 && max !== void 0 && min > max) {
|
|
623
|
-
|
|
870
|
+
refuse(NubbinIssueCode.SlotBounds, `min ${min} is above max ${max}`, `${name} slots.${slot}`);
|
|
624
871
|
}
|
|
625
872
|
}
|
|
626
873
|
}
|
|
@@ -629,7 +876,6 @@ function assertSlotBounds(name, slots) {
|
|
|
629
876
|
function defineBlock(block) {
|
|
630
877
|
assertBlockVersion(block.name, block.version);
|
|
631
878
|
assertSlotBounds(block.name, block.slots);
|
|
632
|
-
assertMigrateKeys(block.name, block.version, block.migrate);
|
|
633
879
|
return block;
|
|
634
880
|
}
|
|
635
881
|
|
|
@@ -638,20 +884,37 @@ function resolveHintPaths(blockName, schema, fields) {
|
|
|
638
884
|
const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
|
|
639
885
|
const unresolved = Object.keys(fields).filter((path) => !known.has(path));
|
|
640
886
|
if (unresolved.length > 0) {
|
|
641
|
-
|
|
642
|
-
|
|
887
|
+
refuse(
|
|
888
|
+
NubbinIssueCode.HintPathUnresolvable,
|
|
889
|
+
`ui.fields references ${unresolved.map((p) => `"${p}"`).join(", ")}, which the schema does not define. Known paths: ${[...known].join(", ")}`,
|
|
890
|
+
blockName
|
|
643
891
|
);
|
|
644
892
|
}
|
|
645
893
|
}
|
|
646
894
|
|
|
647
895
|
// src/assertDataHintAddressable.ts
|
|
648
896
|
function assertDataHintAddressable(blockName, fields) {
|
|
897
|
+
const seen = [];
|
|
649
898
|
for (const [path, hint] of Object.entries(fields)) {
|
|
650
|
-
if (hint.data
|
|
651
|
-
|
|
652
|
-
|
|
899
|
+
if (hint.data === void 0) continue;
|
|
900
|
+
if (path.includes("[]")) {
|
|
901
|
+
refuse(
|
|
902
|
+
NubbinIssueCode.HintNotAddressable,
|
|
903
|
+
`ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`,
|
|
904
|
+
blockName
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const nested = seen.find(
|
|
908
|
+
(other) => path.startsWith(`${other}.`) || other.startsWith(`${path}.`)
|
|
909
|
+
);
|
|
910
|
+
if (nested !== void 0) {
|
|
911
|
+
refuse(
|
|
912
|
+
NubbinIssueCode.HintNotAddressable,
|
|
913
|
+
`ui.fields["${nested}"] and ui.fields["${path}"] both set \`data\`, but their paths overlap \u2014 two holes over one value have no defined order of application`,
|
|
914
|
+
blockName
|
|
653
915
|
);
|
|
654
916
|
}
|
|
917
|
+
seen.push(path);
|
|
655
918
|
}
|
|
656
919
|
}
|
|
657
920
|
|
|
@@ -660,7 +923,11 @@ function assertValidDefaults(blockName, schema, defaults) {
|
|
|
660
923
|
const result = standardValidate(schema, defaults);
|
|
661
924
|
if (result.issues === void 0) return;
|
|
662
925
|
const detail = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
663
|
-
|
|
926
|
+
refuse(
|
|
927
|
+
NubbinIssueCode.InvalidDefaults,
|
|
928
|
+
`defaults do not satisfy the schema \u2014 ${detail}`,
|
|
929
|
+
blockName
|
|
930
|
+
);
|
|
664
931
|
}
|
|
665
932
|
|
|
666
933
|
// src/defineCatalog.ts
|
|
@@ -698,8 +965,40 @@ function formatCompatibilityReport(report) {
|
|
|
698
965
|
return [summary, ...report.incompatible.map(formatRouteIncompatibility)].join("\n");
|
|
699
966
|
}
|
|
700
967
|
|
|
968
|
+
// src/withoutSlotChildren.ts
|
|
969
|
+
function withoutSlotChildren(node, removed) {
|
|
970
|
+
const entries = Object.entries(node.slots ?? {});
|
|
971
|
+
const holdsRemoved = entries.some(([, children]) => children.some((id) => removed.has(id)));
|
|
972
|
+
if (!holdsRemoved) {
|
|
973
|
+
return node;
|
|
974
|
+
}
|
|
975
|
+
const slots = Object.fromEntries(
|
|
976
|
+
entries.map(([slot, children]) => [slot, children.filter((id) => !removed.has(id))])
|
|
977
|
+
);
|
|
978
|
+
return { ...node, slots };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/detachIds.ts
|
|
982
|
+
function detachIds(version, ids) {
|
|
983
|
+
const elements = {};
|
|
984
|
+
for (const [id, node] of Object.entries(version.elements)) {
|
|
985
|
+
elements[id] = withoutSlotChildren(node, ids);
|
|
986
|
+
}
|
|
987
|
+
return { ...version, roots: version.roots.filter((id) => !ids.has(id)), elements };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/moveNode.ts
|
|
991
|
+
function moveNode(version, nodeId, toParentId, toSlot, index) {
|
|
992
|
+
requireNode(version, nodeId);
|
|
993
|
+
requireNode(version, toParentId);
|
|
994
|
+
const detached = detachIds(version, /* @__PURE__ */ new Set([nodeId]));
|
|
995
|
+
const parent = requireNode(detached, toParentId);
|
|
996
|
+
return withElements(detached, withSlotChild(parent, toSlot, nodeId, index));
|
|
997
|
+
}
|
|
998
|
+
|
|
701
999
|
// src/parseMatchKind.ts
|
|
702
1000
|
function parseMatchKind(route) {
|
|
1001
|
+
assertValidRoute(route);
|
|
703
1002
|
if (route.endsWith("/*")) {
|
|
704
1003
|
return "prefix";
|
|
705
1004
|
}
|
|
@@ -709,31 +1008,193 @@ function parseMatchKind(route) {
|
|
|
709
1008
|
return "exact";
|
|
710
1009
|
}
|
|
711
1010
|
|
|
1011
|
+
// src/removeNode.ts
|
|
1012
|
+
function removeNode(version, nodeId) {
|
|
1013
|
+
requireNode(version, nodeId);
|
|
1014
|
+
const removed = idsReachableFrom(version.elements, [nodeId]).add(nodeId);
|
|
1015
|
+
const detached = detachIds(version, removed);
|
|
1016
|
+
const elements = {};
|
|
1017
|
+
for (const [id, node] of Object.entries(detached.elements)) {
|
|
1018
|
+
if (!removed.has(id)) {
|
|
1019
|
+
elements[id] = node;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { ...detached, elements };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/defineStandardSchema.ts
|
|
1026
|
+
var STANDARD_SCHEMA_VERSION = 1;
|
|
1027
|
+
function defineStandardSchema(issuesOf, jsonSchemaOf) {
|
|
1028
|
+
return {
|
|
1029
|
+
"~standard": {
|
|
1030
|
+
version: STANDARD_SCHEMA_VERSION,
|
|
1031
|
+
vendor: "nubbin",
|
|
1032
|
+
validate: (value) => {
|
|
1033
|
+
const issues = issuesOf(value);
|
|
1034
|
+
return issues.length > 0 ? { issues } : { value };
|
|
1035
|
+
},
|
|
1036
|
+
jsonSchema: { input: jsonSchemaOf, output: jsonSchemaOf }
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/richText.constants.ts
|
|
1042
|
+
var RICH_TEXT_MARKS = ["strong", "em", "code"];
|
|
1043
|
+
var RICH_TEXT_BLOCK_KINDS = ["paragraph", "listItem"];
|
|
1044
|
+
var RICH_TEXT_SPAN_KEYS = ["text", "marks", "href"];
|
|
1045
|
+
var RICH_TEXT_BLOCK_KEYS = ["kind", "spans"];
|
|
1046
|
+
|
|
1047
|
+
// src/isRichTextBlockKind.ts
|
|
1048
|
+
function isRichTextBlockKind(value) {
|
|
1049
|
+
return RICH_TEXT_BLOCK_KINDS.some((kind) => kind === value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/nestedSchemaIssues.ts
|
|
1053
|
+
function nestedSchemaIssues(schema, value, prefix) {
|
|
1054
|
+
const result = standardValidate(schema, value);
|
|
1055
|
+
if (result.issues === void 0) return [];
|
|
1056
|
+
return result.issues.map((issue) => ({
|
|
1057
|
+
message: issue.message,
|
|
1058
|
+
path: [...prefix, ...issue.path ?? []]
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/isRichTextMark.ts
|
|
1063
|
+
function isRichTextMark(value) {
|
|
1064
|
+
return RICH_TEXT_MARKS.some((mark) => mark === value);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/richTextMarkIssues.ts
|
|
1068
|
+
function richTextMarkIssues(marks) {
|
|
1069
|
+
if (marks === void 0) return [];
|
|
1070
|
+
if (!Array.isArray(marks)) return [{ message: "marks must be an array", path: ["marks"] }];
|
|
1071
|
+
return marks.flatMap(
|
|
1072
|
+
(mark, index) => isRichTextMark(mark) ? [] : [
|
|
1073
|
+
{
|
|
1074
|
+
message: `unknown mark ${JSON.stringify(mark)}; expected one of ${RICH_TEXT_MARKS.join(", ")}`,
|
|
1075
|
+
path: ["marks", index]
|
|
1076
|
+
}
|
|
1077
|
+
]
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/unexpectedKeyIssues.ts
|
|
1082
|
+
function unexpectedKeyIssues(value, allowed) {
|
|
1083
|
+
return Object.keys(value).filter((key) => !allowed.includes(key)).map((key) => ({ message: `unexpected key "${key}"`, path: [key] }));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/richTextSpanIssues.ts
|
|
1087
|
+
function richTextSpanIssues(value) {
|
|
1088
|
+
if (!isUnknownProps(value)) return [{ message: "a span must be an object", path: [] }];
|
|
1089
|
+
const issues = [];
|
|
1090
|
+
if (typeof value.text !== "string") {
|
|
1091
|
+
issues.push({ message: "text must be a string", path: ["text"] });
|
|
1092
|
+
}
|
|
1093
|
+
issues.push(...richTextMarkIssues(value.marks));
|
|
1094
|
+
if (value.href !== void 0 && typeof value.href !== "string") {
|
|
1095
|
+
issues.push({ message: "href must be a string", path: ["href"] });
|
|
1096
|
+
}
|
|
1097
|
+
issues.push(...unexpectedKeyIssues(value, RICH_TEXT_SPAN_KEYS));
|
|
1098
|
+
return issues;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/richTextSpanSchema.ts
|
|
1102
|
+
var richTextSpanSchema = defineStandardSchema(richTextSpanIssues, () => ({
|
|
1103
|
+
type: "object",
|
|
1104
|
+
properties: {
|
|
1105
|
+
text: { type: "string" },
|
|
1106
|
+
marks: { type: "array", items: { type: "string", enum: [...RICH_TEXT_MARKS] } },
|
|
1107
|
+
href: { type: "string" }
|
|
1108
|
+
},
|
|
1109
|
+
required: ["text"],
|
|
1110
|
+
additionalProperties: false
|
|
1111
|
+
}));
|
|
1112
|
+
|
|
1113
|
+
// src/richTextBlockIssues.ts
|
|
1114
|
+
function richTextBlockIssues(value) {
|
|
1115
|
+
if (!isUnknownProps(value)) return [{ message: "a block must be an object", path: [] }];
|
|
1116
|
+
const issues = [];
|
|
1117
|
+
if (!isRichTextBlockKind(value.kind)) {
|
|
1118
|
+
issues.push({
|
|
1119
|
+
message: `unknown kind ${JSON.stringify(value.kind)}; expected one of ${RICH_TEXT_BLOCK_KINDS.join(", ")}`,
|
|
1120
|
+
path: ["kind"]
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
if (Array.isArray(value.spans)) {
|
|
1124
|
+
issues.push(
|
|
1125
|
+
...value.spans.flatMap(
|
|
1126
|
+
(span, index) => nestedSchemaIssues(richTextSpanSchema, span, ["spans", index])
|
|
1127
|
+
)
|
|
1128
|
+
);
|
|
1129
|
+
} else {
|
|
1130
|
+
issues.push({ message: "spans must be an array", path: ["spans"] });
|
|
1131
|
+
}
|
|
1132
|
+
issues.push(...unexpectedKeyIssues(value, RICH_TEXT_BLOCK_KEYS));
|
|
1133
|
+
return issues;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/richTextBlockSchema.ts
|
|
1137
|
+
var richTextBlockSchema = defineStandardSchema(
|
|
1138
|
+
richTextBlockIssues,
|
|
1139
|
+
(options) => ({
|
|
1140
|
+
type: "object",
|
|
1141
|
+
properties: {
|
|
1142
|
+
kind: { type: "string", enum: [...RICH_TEXT_BLOCK_KINDS] },
|
|
1143
|
+
spans: { type: "array", items: richTextSpanSchema["~standard"].jsonSchema.input(options) }
|
|
1144
|
+
},
|
|
1145
|
+
required: ["kind", "spans"],
|
|
1146
|
+
additionalProperties: false
|
|
1147
|
+
})
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
// src/richTextIssues.ts
|
|
1151
|
+
function richTextIssues(value) {
|
|
1152
|
+
if (!Array.isArray(value)) {
|
|
1153
|
+
return [{ message: "rich text must be an array of blocks", path: [] }];
|
|
1154
|
+
}
|
|
1155
|
+
return value.flatMap(
|
|
1156
|
+
(block, index) => nestedSchemaIssues(richTextBlockSchema, block, [index])
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/richTextSchema.ts
|
|
1161
|
+
var richTextSchema = defineStandardSchema(richTextIssues, (options) => ({
|
|
1162
|
+
type: "array",
|
|
1163
|
+
items: richTextBlockSchema["~standard"].jsonSchema.input(options)
|
|
1164
|
+
}));
|
|
1165
|
+
|
|
1166
|
+
// src/richText.ts
|
|
1167
|
+
function richText() {
|
|
1168
|
+
return richTextSchema;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
712
1171
|
// src/setAtPath.ts
|
|
713
1172
|
function setAtPath(target, path, value) {
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
throw new Error(`path "${path}" is not addressable`);
|
|
717
|
-
}
|
|
718
|
-
if (rest.length === 0) {
|
|
1173
|
+
const { head, tail } = splitPath(path);
|
|
1174
|
+
if (tail.length === 0) {
|
|
719
1175
|
return { ...target, [head]: value };
|
|
720
1176
|
}
|
|
721
1177
|
const child = target[head];
|
|
1178
|
+
if (Array.isArray(child)) {
|
|
1179
|
+
refuse(
|
|
1180
|
+
NubbinIssueCode.PathNotAddressable,
|
|
1181
|
+
`path "${path}" descends into an array at "${head}", which addresses no field`,
|
|
1182
|
+
path
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
722
1185
|
const base = typeof child === "object" && child !== null ? child : {};
|
|
723
|
-
return { ...target, [head]: setAtPath(base,
|
|
1186
|
+
return { ...target, [head]: setAtPath(base, tail.join("."), value) };
|
|
724
1187
|
}
|
|
725
1188
|
|
|
726
1189
|
// src/setNodeProp.ts
|
|
727
1190
|
function setNodeProp(version, nodeId, path, value) {
|
|
728
|
-
const node = version
|
|
729
|
-
|
|
730
|
-
throw new Error(`no node "${nodeId}" in document "${version.documentId}"`);
|
|
731
|
-
}
|
|
732
|
-
const edited = { ...node, props: setAtPath(node.props, path, value) };
|
|
733
|
-
return { ...version, elements: { ...version.elements, [nodeId]: edited } };
|
|
1191
|
+
const node = requireNode(version, nodeId);
|
|
1192
|
+
return withElements(version, { ...node, props: setAtPath(node.props, path, value) });
|
|
734
1193
|
}
|
|
735
1194
|
export {
|
|
736
|
-
|
|
1195
|
+
NubbinError,
|
|
1196
|
+
NubbinIssueCode,
|
|
1197
|
+
addNode,
|
|
737
1198
|
checkCompatibility,
|
|
738
1199
|
checkRollback,
|
|
739
1200
|
compile,
|
|
@@ -741,7 +1202,11 @@ export {
|
|
|
741
1202
|
defineBlock,
|
|
742
1203
|
defineCatalog,
|
|
743
1204
|
formatCompatibilityReport,
|
|
1205
|
+
moveNode,
|
|
744
1206
|
parseMatchKind,
|
|
1207
|
+
refuse,
|
|
1208
|
+
removeNode,
|
|
1209
|
+
richText,
|
|
745
1210
|
setAtPath,
|
|
746
1211
|
setNodeProp,
|
|
747
1212
|
zodAdapter
|