@omniharmonic/openspace-ops 0.0.0-stage → 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.
package/dist/index.js ADDED
@@ -0,0 +1,2503 @@
1
+ // src/apply.ts
2
+ import { produce } from "immer";
3
+
4
+ // src/doc.ts
5
+ import {
6
+ GRID_COLUMNS,
7
+ resolve
8
+ } from "@omniharmonic/openspace-schema";
9
+
10
+ // src/ops.ts
11
+ import {
12
+ BlockIdSchema,
13
+ BlockSchema,
14
+ BlockStyleSchema,
15
+ CollectionConfigSchema,
16
+ CollectionItemSchema,
17
+ CollectionNameSchema,
18
+ GridItemSchema,
19
+ GridRectSchema,
20
+ MAX_GRID_ROWS,
21
+ MediaMetaSchema,
22
+ MediaPathSchema,
23
+ NavItemSchema,
24
+ PageIdSchema,
25
+ PageSchema,
26
+ responsive,
27
+ SectionIdSchema,
28
+ SectionSchema,
29
+ SlugSchema,
30
+ VariantNameSchema
31
+ } from "@omniharmonic/openspace-schema";
32
+ import { z } from "zod";
33
+ var OpMetaShape = {
34
+ id: z.string().min(1).max(128),
35
+ actor: z.string().min(1).max(128),
36
+ ts: z.number().finite().min(0)
37
+ };
38
+ var FOOTER = "footer";
39
+ var ContainerIdSchema = z.union([PageIdSchema, z.literal(FOOTER)]);
40
+ var PositionSchema = z.union([
41
+ z.int().min(0),
42
+ z.literal("start"),
43
+ z.literal("end"),
44
+ z.strictObject({ before: z.string().min(1) }),
45
+ z.strictObject({ after: z.string().min(1) })
46
+ ]);
47
+ var PathSchema = z.string().min(1).max(300).regex(/^[^.]+(?:\.[^.]+)*$/, { message: "Expected a dot-separated path" });
48
+ var PatchShape = {
49
+ set: z.record(PathSchema, z.unknown()).optional(),
50
+ unset: z.array(PathSchema).max(500).optional()
51
+ };
52
+ var RowsSchema = responsive(z.int().min(1).max(MAX_GRID_ROWS));
53
+ var BreakpointSchema = z.enum(["d", "t", "m"]);
54
+ var op = (name2, shape) => z.strictObject({ op: z.literal(name2), ...OpMetaShape, ...shape });
55
+ var BlockUpdateOpSchema = op("block.update", {
56
+ page: ContainerIdSchema,
57
+ block: BlockIdSchema,
58
+ /** Field values to set (top-level field keys). */
59
+ set: z.record(z.string().min(1).max(100), z.unknown()).optional(),
60
+ /** Field keys to remove. */
61
+ unset: z.array(z.string().min(1).max(100)).max(200).optional(),
62
+ /** New variant; `null` removes it. */
63
+ variant: VariantNameSchema.nullable().optional(),
64
+ /** New style overrides (replaced whole); `null` removes them. */
65
+ style: BlockStyleSchema.nullable().optional(),
66
+ /** Stack column; `null` removes it. */
67
+ column: z.int().min(0).max(3).nullable().optional(),
68
+ /** Layers label; `null` removes it. */
69
+ label: z.string().max(80).nullable().optional(),
70
+ /** Hide the block on the published site; `null` (or `false`) shows it again. */
71
+ hidden: z.boolean().nullable().optional()
72
+ });
73
+ var BlockInsertOpSchema = op("block.insert", {
74
+ page: ContainerIdSchema,
75
+ section: SectionIdSchema,
76
+ at: PositionSchema,
77
+ block: BlockSchema,
78
+ /** Grid placement when inserting into a grid section (auto-placed at the bottom if absent). */
79
+ place: GridItemSchema.optional(),
80
+ /** Exact grid rows to set afterwards (used by inverses); otherwise rows auto-grow to fit. */
81
+ rows: RowsSchema.optional()
82
+ });
83
+ var BlockMoveOpSchema = op("block.move", {
84
+ page: ContainerIdSchema,
85
+ block: BlockIdSchema,
86
+ to: z.strictObject({
87
+ section: SectionIdSchema,
88
+ at: PositionSchema,
89
+ /** Grid placement in the destination (kept/auto-placed if absent). */
90
+ place: GridItemSchema.optional(),
91
+ /** Stack column in the destination; `null` removes it. */
92
+ column: z.int().min(0).max(3).nullable().optional(),
93
+ /** Exact rows for the destination grid (used by inverses). */
94
+ rows: RowsSchema.optional()
95
+ }),
96
+ /** Exact rows for the source grid section after the block leaves (used by inverses). */
97
+ fromRows: RowsSchema.optional()
98
+ });
99
+ var BlockDeleteOpSchema = op("block.delete", {
100
+ page: ContainerIdSchema,
101
+ block: BlockIdSchema,
102
+ /** Exact rows for the grid section afterwards (used by inverses). */
103
+ rows: RowsSchema.optional()
104
+ });
105
+ var GridPlaceOpSchema = op("grid.place", {
106
+ page: ContainerIdSchema,
107
+ section: SectionIdSchema,
108
+ block: BlockIdSchema,
109
+ bp: BreakpointSchema,
110
+ /** The placement; `null` removes a tablet/mobile override (desktop is required). */
111
+ rect: GridRectSchema.nullable(),
112
+ /** Exact rows to set (used by inverses); otherwise rows auto-grow to fit the placement. */
113
+ rows: RowsSchema.optional()
114
+ });
115
+ var SectionInsertOpSchema = op("section.insert", {
116
+ page: PageIdSchema,
117
+ at: PositionSchema,
118
+ section: SectionSchema
119
+ });
120
+ var SectionMoveOpSchema = op("section.move", {
121
+ page: PageIdSchema,
122
+ section: SectionIdSchema,
123
+ at: PositionSchema
124
+ });
125
+ var SectionDeleteOpSchema = op("section.delete", {
126
+ page: PageIdSchema,
127
+ section: SectionIdSchema
128
+ });
129
+ var SectionUpdateOpSchema = op("section.update", {
130
+ page: ContainerIdSchema,
131
+ section: SectionIdSchema,
132
+ ...PatchShape
133
+ });
134
+ var StylesPatchOpSchema = op("styles.patch", {
135
+ patch: z.strictObject(PatchShape)
136
+ });
137
+ var PageCreateOpSchema = op("page.create", {
138
+ page: PageSchema,
139
+ /** Optionally replace the navigation tree in the same step (e.g. to add the new page). */
140
+ nav: z.array(NavItemSchema).optional()
141
+ });
142
+ var PageUpdateOpSchema = op("page.update", {
143
+ page: PageIdSchema,
144
+ ...PatchShape
145
+ });
146
+ var PageDeleteOpSchema = op("page.delete", {
147
+ page: PageIdSchema,
148
+ /** Optionally replace the navigation tree in the same step (e.g. to drop links to the page). */
149
+ nav: z.array(NavItemSchema).optional()
150
+ });
151
+ var NavUpdateOpSchema = op("nav.update", {
152
+ tree: z.array(NavItemSchema)
153
+ });
154
+ var SiteUpdateOpSchema = op("site.update", { ...PatchShape });
155
+ var MediaAddOpSchema = op("media.add", {
156
+ path: MediaPathSchema,
157
+ meta: MediaMetaSchema
158
+ });
159
+ var MediaRemoveOpSchema = op("media.remove", {
160
+ path: MediaPathSchema
161
+ });
162
+ var CollectionUpsertOpSchema = op("collection.upsert", {
163
+ collection: CollectionNameSchema,
164
+ item: CollectionItemSchema
165
+ });
166
+ var CollectionDeleteOpSchema = op("collection.delete", {
167
+ collection: CollectionNameSchema,
168
+ slug: SlugSchema,
169
+ /**
170
+ * Keep the collection's (now empty) entry. By default a collection with no items left is
171
+ * removed from `collections` (its settings in `collectionConfigs` are untouched).
172
+ */
173
+ keepEmpty: z.boolean().optional()
174
+ });
175
+ var CollectionConfigUpsertOpSchema = op("collectionConfig.upsert", {
176
+ collection: CollectionNameSchema,
177
+ config: CollectionConfigSchema.nullable(),
178
+ /** With `config: null`: keep an empty `collectionConfigs` object instead of dropping it. */
179
+ keepEmpty: z.boolean().optional()
180
+ });
181
+ var OpSchema = z.discriminatedUnion("op", [
182
+ BlockUpdateOpSchema,
183
+ BlockInsertOpSchema,
184
+ BlockMoveOpSchema,
185
+ BlockDeleteOpSchema,
186
+ GridPlaceOpSchema,
187
+ SectionInsertOpSchema,
188
+ SectionMoveOpSchema,
189
+ SectionDeleteOpSchema,
190
+ SectionUpdateOpSchema,
191
+ StylesPatchOpSchema,
192
+ PageCreateOpSchema,
193
+ PageUpdateOpSchema,
194
+ PageDeleteOpSchema,
195
+ NavUpdateOpSchema,
196
+ SiteUpdateOpSchema,
197
+ MediaAddOpSchema,
198
+ MediaRemoveOpSchema,
199
+ CollectionUpsertOpSchema,
200
+ CollectionDeleteOpSchema,
201
+ CollectionConfigUpsertOpSchema
202
+ ]);
203
+ var OP_TYPES = OpSchema.options.map((o) => o.shape.op.value);
204
+ function stamp(body, meta) {
205
+ return { ...body, id: meta.id, actor: meta.actor, ts: meta.ts };
206
+ }
207
+ function parseOp(value) {
208
+ const r = OpSchema.safeParse(value);
209
+ if (r.success) return { ok: true, op: r.data };
210
+ return {
211
+ ok: false,
212
+ errors: r.error.issues.map((i) => ({
213
+ path: i.path.map((p) => typeof p === "symbol" ? String(p) : p),
214
+ message: i.message
215
+ }))
216
+ };
217
+ }
218
+
219
+ // src/doc.ts
220
+ var OpError = class extends Error {
221
+ path;
222
+ constructor(message, path = []) {
223
+ super(message);
224
+ this.name = "OpError";
225
+ this.path = path;
226
+ }
227
+ };
228
+ function containerPath(container) {
229
+ return container === FOOTER ? ["site", "footer"] : ["pages", container];
230
+ }
231
+ function sectionsOf(doc, container) {
232
+ if (container === FOOTER) return [doc.site.footer];
233
+ return doc.pages[container]?.sections;
234
+ }
235
+ function getPage(doc, id) {
236
+ return Object.hasOwn(doc.pages, id) ? doc.pages[id] : void 0;
237
+ }
238
+ function findSection(doc, container, sectionId) {
239
+ const sections = sectionsOf(doc, container);
240
+ if (!sections) return void 0;
241
+ const index = sections.findIndex((s) => s.id === sectionId);
242
+ if (index < 0) return void 0;
243
+ return { section: sections[index], index };
244
+ }
245
+ function findBlock(doc, container, blockId) {
246
+ const sections = sectionsOf(doc, container);
247
+ if (!sections) return void 0;
248
+ for (let i = 0; i < sections.length; i++) {
249
+ const section = sections[i];
250
+ const index = section.blocks.findIndex((b) => b.id === blockId);
251
+ if (index >= 0)
252
+ return { section, sectionIndex: i, block: section.blocks[index], index };
253
+ }
254
+ return void 0;
255
+ }
256
+ function locateSection(doc, sectionId) {
257
+ if (doc.site.footer.id === sectionId)
258
+ return { container: FOOTER, index: 0, section: doc.site.footer };
259
+ for (const page of Object.values(doc.pages)) {
260
+ const index = page.sections.findIndex((s) => s.id === sectionId);
261
+ if (index >= 0) return { container: page.id, index, section: page.sections[index] };
262
+ }
263
+ return void 0;
264
+ }
265
+ function locateBlock(doc, blockId) {
266
+ const inFooter = findBlock(doc, FOOTER, blockId);
267
+ if (inFooter) return { container: FOOTER, ...inFooter };
268
+ for (const page of Object.values(doc.pages)) {
269
+ const loc = findBlock(doc, page.id, blockId);
270
+ if (loc) return { container: page.id, ...loc };
271
+ }
272
+ return void 0;
273
+ }
274
+ function indexIds(doc) {
275
+ const sections = /* @__PURE__ */ new Map();
276
+ const blocks = /* @__PURE__ */ new Map();
277
+ const add = (container, s) => {
278
+ sections.set(s.id, container);
279
+ for (const b of s.blocks) blocks.set(b.id, container);
280
+ };
281
+ add(FOOTER, doc.site.footer);
282
+ for (const page of Object.values(doc.pages)) for (const s of page.sections) add(page.id, s);
283
+ return { sections, blocks };
284
+ }
285
+ function resolvePosition(ids, at, path) {
286
+ if (typeof at === "number") {
287
+ if (at > ids.length)
288
+ throw new OpError(`Position ${at} is out of range (0\u2013${ids.length})`, [...path, "at"]);
289
+ return at;
290
+ }
291
+ if (at === "start") return 0;
292
+ if (at === "end") return ids.length;
293
+ const anchor = "before" in at ? at.before : at.after;
294
+ const i = ids.indexOf(anchor);
295
+ if (i < 0) throw new OpError(`Position anchor ${anchor} is not a sibling here`, [...path, "at"]);
296
+ return "before" in at ? i : i + 1;
297
+ }
298
+ function effectiveRect(item, bp) {
299
+ if (bp === "t") return item.t ?? item.d;
300
+ return item[bp];
301
+ }
302
+ function usedRows(layout, bp) {
303
+ let max = 0;
304
+ for (const item of Object.values(layout.items)) {
305
+ const r = effectiveRect(item, bp);
306
+ if (r) max = Math.max(max, r.y + r.h - 1);
307
+ }
308
+ return max;
309
+ }
310
+ function raiseRows(layout) {
311
+ for (const bp of ["d", "t", "m"]) {
312
+ const need2 = usedRows(layout, bp);
313
+ if (need2 > resolve(layout.rows, bp)) layout.rows[bp] = need2;
314
+ }
315
+ }
316
+ var AUTO_PLACE = { w: 12, h: 4 };
317
+ function autoPlace(layout) {
318
+ const item = {
319
+ d: {
320
+ x: 1,
321
+ y: usedRows(layout, "d") + 1,
322
+ w: Math.min(AUTO_PLACE.w, GRID_COLUMNS.d),
323
+ h: AUTO_PLACE.h
324
+ }
325
+ };
326
+ if (Object.values(layout.items).some((i) => i.m !== void 0)) {
327
+ item.m = { x: 1, y: usedRows(layout, "m") + 1, w: GRID_COLUMNS.m, h: AUTO_PLACE.h };
328
+ }
329
+ return item;
330
+ }
331
+ function copyRows(rows) {
332
+ const out = { d: rows.d };
333
+ if (rows.t !== void 0) out.t = rows.t;
334
+ if (rows.m !== void 0) out.m = rows.m;
335
+ return out;
336
+ }
337
+
338
+ // src/paths.ts
339
+ var FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
340
+ function findForbiddenKey(value, depth = 0) {
341
+ if (depth > 64 || value === null || typeof value !== "object") return void 0;
342
+ if (Array.isArray(value)) {
343
+ for (const v of value) {
344
+ const k = findForbiddenKey(v, depth + 1);
345
+ if (k) return k;
346
+ }
347
+ return void 0;
348
+ }
349
+ for (const key2 of Object.keys(value)) {
350
+ if (FORBIDDEN_SEGMENTS.has(key2)) return key2;
351
+ const k = findForbiddenKey(value[key2], depth + 1);
352
+ if (k) return k;
353
+ }
354
+ return void 0;
355
+ }
356
+ function isForbiddenKey(key2) {
357
+ return FORBIDDEN_SEGMENTS.has(key2);
358
+ }
359
+ function isPlainObject(value) {
360
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
361
+ const proto = Object.getPrototypeOf(value);
362
+ return proto === Object.prototype || proto === null;
363
+ }
364
+ function deepEqual(a, b) {
365
+ if (a === b) return true;
366
+ if (typeof a !== typeof b || a === null || b === null) return false;
367
+ if (Array.isArray(a)) {
368
+ if (!Array.isArray(b) || a.length !== b.length) return false;
369
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
370
+ return true;
371
+ }
372
+ if (Array.isArray(b)) return false;
373
+ if (typeof a === "object") {
374
+ const ao = a;
375
+ const bo = b;
376
+ const ak = Object.keys(ao).filter((k) => ao[k] !== void 0);
377
+ const bk = Object.keys(bo).filter((k) => bo[k] !== void 0);
378
+ if (ak.length !== bk.length) return false;
379
+ for (const k of ak) {
380
+ if (!Object.hasOwn(bo, k) || !deepEqual(ao[k], bo[k])) return false;
381
+ }
382
+ return true;
383
+ }
384
+ return false;
385
+ }
386
+ function clone(value) {
387
+ if (Array.isArray(value)) return value.map((v) => clone(v));
388
+ if (isPlainObject(value)) {
389
+ const out = {};
390
+ for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = clone(v);
391
+ return out;
392
+ }
393
+ return value;
394
+ }
395
+ function splitPath(path) {
396
+ return path.split(".");
397
+ }
398
+ function checkPath(path) {
399
+ if (path.length === 0) return "Empty path";
400
+ for (const seg of splitPath(path)) {
401
+ if (seg.length === 0) return `Empty segment in path "${path}"`;
402
+ if (FORBIDDEN_SEGMENTS.has(seg)) return `Forbidden path segment "${seg}"`;
403
+ }
404
+ return void 0;
405
+ }
406
+ function isPathPrefix(a, b) {
407
+ return a === b || b.startsWith(`${a}.`);
408
+ }
409
+ function pathsOverlap(a, b) {
410
+ return isPathPrefix(a, b) || isPathPrefix(b, a);
411
+ }
412
+ function overlappingPaths(paths) {
413
+ const out = /* @__PURE__ */ new Set();
414
+ for (let i = 0; i < paths.length; i++)
415
+ for (let j = i + 1; j < paths.length; j++) {
416
+ const a = paths[i];
417
+ const b = paths[j];
418
+ if (pathsOverlap(a, b)) out.add(a.length >= b.length ? a : b);
419
+ }
420
+ return [...out];
421
+ }
422
+ function lookupPath(root, path) {
423
+ const segs = splitPath(path);
424
+ let cur = root;
425
+ for (let i = 0; i < segs.length; i++) {
426
+ const seg = segs[i];
427
+ if (!isPlainObject(cur)) return { exists: false, value: void 0, blocked: true };
428
+ if (!Object.hasOwn(cur, seg) || cur[seg] === void 0) {
429
+ return {
430
+ exists: false,
431
+ value: void 0,
432
+ missingAt: segs.slice(0, i + 1).join("."),
433
+ blocked: false
434
+ };
435
+ }
436
+ cur = cur[seg];
437
+ }
438
+ return { exists: true, value: cur, blocked: false };
439
+ }
440
+ function setPath(root, path, value) {
441
+ const segs = splitPath(path);
442
+ let cur = root;
443
+ for (let i = 0; i < segs.length - 1; i++) {
444
+ const seg = segs[i];
445
+ const next = cur[seg];
446
+ if (next === void 0) {
447
+ const created = {};
448
+ cur[seg] = created;
449
+ cur = cur[seg];
450
+ } else if (isPlainObject(next)) {
451
+ cur = next;
452
+ } else {
453
+ throw new Error(`Cannot set "${path}": "${segs.slice(0, i + 1).join(".")}" is not an object`);
454
+ }
455
+ }
456
+ cur[segs[segs.length - 1]] = clone(value);
457
+ }
458
+ function unsetPath(root, path) {
459
+ const segs = splitPath(path);
460
+ let cur = root;
461
+ for (let i = 0; i < segs.length - 1; i++) {
462
+ if (!isPlainObject(cur)) return;
463
+ cur = cur[segs[i]];
464
+ }
465
+ if (isPlainObject(cur)) delete cur[segs[segs.length - 1]];
466
+ }
467
+ function applyPatch(root, patch) {
468
+ for (const p of patch.unset ?? []) unsetPath(root, p);
469
+ for (const [p, v] of Object.entries(patch.set ?? {})) setPath(root, p, v);
470
+ }
471
+ function invertPatch(root, patch) {
472
+ const set = {};
473
+ const unset = /* @__PURE__ */ new Set();
474
+ for (const p of patch.unset ?? []) {
475
+ const l = lookupPath(root, p);
476
+ if (l.exists) set[p] = clone(l.value);
477
+ }
478
+ for (const p of Object.keys(patch.set ?? {})) {
479
+ const l = lookupPath(root, p);
480
+ if (l.exists) set[p] = clone(l.value);
481
+ else if (l.missingAt !== void 0) unset.add(l.missingAt);
482
+ }
483
+ return { set, unset: [...unset] };
484
+ }
485
+ function diffObjects(before, after, skip = [], prefix = "") {
486
+ const set = {};
487
+ const unset = [];
488
+ const walk2 = (a, b, base, top) => {
489
+ for (const k of Object.keys(a)) {
490
+ if (top && skip.includes(k)) continue;
491
+ if (a[k] === void 0) continue;
492
+ if (b[k] === void 0) unset.push(base + k);
493
+ }
494
+ for (const k of Object.keys(b)) {
495
+ if (top && skip.includes(k)) continue;
496
+ const bv = b[k];
497
+ if (bv === void 0) continue;
498
+ const av = a[k];
499
+ if (av === void 0) set[base + k] = clone(bv);
500
+ else if (isPlainObject(av) && isPlainObject(bv)) walk2(av, bv, `${base + k}.`, false);
501
+ else if (!deepEqual(av, bv)) set[base + k] = clone(bv);
502
+ }
503
+ };
504
+ walk2(before, after, prefix, true);
505
+ return { set, unset };
506
+ }
507
+ function isEmptyPatch(patch) {
508
+ return Object.keys(patch.set ?? {}).length === 0 && (patch.unset ?? []).length === 0;
509
+ }
510
+ function patchPaths(patch) {
511
+ return [...patch.unset ?? [], ...Object.keys(patch.set ?? {})];
512
+ }
513
+ function isJson(value, depth = 0) {
514
+ if (depth > 64) return false;
515
+ if (value === null) return true;
516
+ switch (typeof value) {
517
+ case "boolean":
518
+ case "string":
519
+ return true;
520
+ case "number":
521
+ return Number.isFinite(value);
522
+ case "object":
523
+ if (Array.isArray(value)) return value.every((v) => isJson(v, depth + 1));
524
+ if (!isPlainObject(value)) return false;
525
+ return Object.values(value).every((v) => isJson(v, depth + 1));
526
+ default:
527
+ return false;
528
+ }
529
+ }
530
+
531
+ // src/apply.ts
532
+ var PROTECTED_PATHS = {
533
+ page: ["id", "sections"],
534
+ section: ["id", "blocks"],
535
+ site: ["nav"],
536
+ styles: []
537
+ };
538
+ function checkPatch(patch, protectedKeys, path, what) {
539
+ const paths = patchPaths(patch);
540
+ for (const p of paths) {
541
+ const problem = checkPath(p);
542
+ if (problem) throw new OpError(problem, path);
543
+ const top = p.split(".")[0];
544
+ if (protectedKeys.includes(top)) {
545
+ const hint = top === "nav" ? "use nav.update" : top === "sections" || top === "blocks" ? "use the section/block ops" : "ids never change";
546
+ throw new OpError(`Cannot patch ${what} "${top}" directly (${hint})`, path);
547
+ }
548
+ }
549
+ const overlaps2 = overlappingPaths(paths);
550
+ if (overlaps2.length > 0)
551
+ throw new OpError(`Overlapping patch paths: ${overlaps2.join(", ")}`, path);
552
+ }
553
+ function requireSections(doc, container) {
554
+ const sections = sectionsOf(doc, container);
555
+ if (!sections) throw new OpError(`Page ${container} does not exist`, ["pages", container]);
556
+ return sections;
557
+ }
558
+ function requireSection(doc, container, id) {
559
+ requireSections(doc, container);
560
+ const loc = findSection(doc, container, id);
561
+ if (!loc)
562
+ throw new OpError(`Section ${id} is not on ${describe(container)}`, containerPath(container));
563
+ return loc.section;
564
+ }
565
+ function describe(container) {
566
+ return container === FOOTER ? "the footer" : `page ${container}`;
567
+ }
568
+ function requirePage(doc, id, op2) {
569
+ if (id === FOOTER) throw new OpError(`${op2} is not allowed on the footer`, ["site", "footer"]);
570
+ const page = getPage(doc, id);
571
+ if (!page) throw new OpError(`Page ${id} does not exist`, ["pages", id]);
572
+ return page;
573
+ }
574
+ function placeInGrid(section, blockId, item) {
575
+ if (section.layout.mode !== "grid") return;
576
+ section.layout.items[blockId] = item ? clone(item) : autoPlace(section.layout);
577
+ }
578
+ function mutate(doc, op2) {
579
+ switch (op2.op) {
580
+ case "block.update": {
581
+ for (const k of [...op2.unset ?? [], ...Object.keys(op2.set ?? {})])
582
+ if (isForbiddenKey(k)) throw new OpError(`Forbidden field key "${k}"`, ["set", k]);
583
+ requireSections(doc, op2.page);
584
+ const loc = findBlock(doc, op2.page, op2.block);
585
+ if (!loc)
586
+ throw new OpError(
587
+ `Block ${op2.block} is not on ${describe(op2.page)}`,
588
+ containerPath(op2.page)
589
+ );
590
+ const b = loc.block;
591
+ for (const k of op2.unset ?? []) delete b.fields[k];
592
+ for (const [k, v] of Object.entries(op2.set ?? {})) b.fields[k] = clone(v);
593
+ if (op2.variant !== void 0) {
594
+ if (op2.variant === null) delete b.variant;
595
+ else b.variant = op2.variant;
596
+ }
597
+ if (op2.style !== void 0) {
598
+ if (op2.style === null) delete b.style;
599
+ else b.style = clone(op2.style);
600
+ }
601
+ if (op2.label !== void 0) {
602
+ if (op2.label === null) delete b.label;
603
+ else b.label = op2.label;
604
+ }
605
+ if (op2.hidden !== void 0) {
606
+ if (!op2.hidden) delete b.hidden;
607
+ else b.hidden = true;
608
+ }
609
+ if (op2.column !== void 0) {
610
+ if (op2.column === null) delete b.column;
611
+ else b.column = op2.column;
612
+ }
613
+ return;
614
+ }
615
+ case "block.insert": {
616
+ const section = requireSection(doc, op2.page, op2.section);
617
+ const at = resolvePosition(
618
+ section.blocks.map((b) => b.id),
619
+ op2.at,
620
+ [...containerPath(op2.page), "sections", op2.section]
621
+ );
622
+ if (section.blocks.some((b) => b.id === op2.block.id))
623
+ throw new OpError(`Block ${op2.block.id} already exists`, ["block", "id"]);
624
+ section.blocks.splice(at, 0, clone(op2.block));
625
+ if (section.layout.mode === "grid") {
626
+ placeInGrid(section, op2.block.id, op2.place);
627
+ if (op2.rows) section.layout.rows = copyRows(op2.rows);
628
+ else raiseRows(section.layout);
629
+ }
630
+ return;
631
+ }
632
+ case "block.move": {
633
+ requireSections(doc, op2.page);
634
+ const from = findBlock(doc, op2.page, op2.block);
635
+ if (!from)
636
+ throw new OpError(
637
+ `Block ${op2.block} is not on ${describe(op2.page)}`,
638
+ containerPath(op2.page)
639
+ );
640
+ const dest = requireSection(doc, op2.page, op2.to.section);
641
+ const src = from.section;
642
+ const same = src.id === dest.id;
643
+ const [block] = src.blocks.splice(from.index, 1);
644
+ if (src.layout.mode === "grid" && !same) {
645
+ delete src.layout.items[block.id];
646
+ if (op2.fromRows) src.layout.rows = copyRows(op2.fromRows);
647
+ }
648
+ const at = resolvePosition(
649
+ dest.blocks.map((b) => b.id),
650
+ op2.to.at,
651
+ [...containerPath(op2.page), "sections", dest.id]
652
+ );
653
+ dest.blocks.splice(at, 0, block);
654
+ const moved = dest.blocks[at];
655
+ if (dest.layout.mode === "grid") {
656
+ if (!same || op2.to.place || !dest.layout.items[block.id])
657
+ placeInGrid(dest, block.id, op2.to.place);
658
+ if (op2.to.rows) dest.layout.rows = copyRows(op2.to.rows);
659
+ else raiseRows(dest.layout);
660
+ }
661
+ if (op2.to.column !== void 0) {
662
+ if (op2.to.column === null) delete moved.column;
663
+ else moved.column = op2.to.column;
664
+ } else if (dest.layout.mode === "stack" && moved.column !== void 0) {
665
+ const cols = dest.layout.columns.d.length;
666
+ if (moved.column >= cols) moved.column = cols - 1;
667
+ }
668
+ return;
669
+ }
670
+ case "block.delete": {
671
+ requireSections(doc, op2.page);
672
+ const loc = findBlock(doc, op2.page, op2.block);
673
+ if (!loc)
674
+ throw new OpError(
675
+ `Block ${op2.block} is not on ${describe(op2.page)}`,
676
+ containerPath(op2.page)
677
+ );
678
+ loc.section.blocks.splice(loc.index, 1);
679
+ if (loc.section.layout.mode === "grid") {
680
+ delete loc.section.layout.items[op2.block];
681
+ if (op2.rows) loc.section.layout.rows = copyRows(op2.rows);
682
+ }
683
+ return;
684
+ }
685
+ case "grid.place": {
686
+ const section = requireSection(doc, op2.page, op2.section);
687
+ const base = [...containerPath(op2.page), "sections", op2.section];
688
+ if (section.layout.mode !== "grid")
689
+ throw new OpError(`Section ${op2.section} is not a grid section`, [
690
+ ...base,
691
+ "layout",
692
+ "mode"
693
+ ]);
694
+ if (!section.blocks.some((b) => b.id === op2.block))
695
+ throw new OpError(`Block ${op2.block} is not in section ${op2.section}`, [...base, "blocks"]);
696
+ const item = section.layout.items[op2.block];
697
+ if (op2.rect === null) {
698
+ if (op2.bp === "d")
699
+ throw new OpError("The desktop placement cannot be removed", [
700
+ ...base,
701
+ "layout",
702
+ "items",
703
+ op2.block,
704
+ "d"
705
+ ]);
706
+ if (item) delete item[op2.bp];
707
+ } else if (item) {
708
+ item[op2.bp] = clone(op2.rect);
709
+ } else {
710
+ if (op2.bp !== "d")
711
+ throw new OpError(`Block ${op2.block} needs a desktop placement first`, [
712
+ ...base,
713
+ "layout",
714
+ "items",
715
+ op2.block
716
+ ]);
717
+ section.layout.items[op2.block] = { d: clone(op2.rect) };
718
+ }
719
+ if (op2.rows) section.layout.rows = copyRows(op2.rows);
720
+ else raiseRows(section.layout);
721
+ return;
722
+ }
723
+ case "section.insert": {
724
+ const page = requirePage(doc, op2.page, "section.insert");
725
+ const at = resolvePosition(
726
+ page.sections.map((s) => s.id),
727
+ op2.at,
728
+ ["pages", op2.page]
729
+ );
730
+ if (page.sections.some((s) => s.id === op2.section.id))
731
+ throw new OpError(`Section ${op2.section.id} already exists`, ["section", "id"]);
732
+ page.sections.splice(at, 0, clone(op2.section));
733
+ return;
734
+ }
735
+ case "section.move": {
736
+ const page = requirePage(doc, op2.page, "section.move");
737
+ const from = page.sections.findIndex((s) => s.id === op2.section);
738
+ if (from < 0)
739
+ throw new OpError(`Section ${op2.section} is not on page ${op2.page}`, ["pages", op2.page]);
740
+ const [section] = page.sections.splice(from, 1);
741
+ const at = resolvePosition(
742
+ page.sections.map((s) => s.id),
743
+ op2.at,
744
+ ["pages", op2.page]
745
+ );
746
+ page.sections.splice(at, 0, section);
747
+ return;
748
+ }
749
+ case "section.delete": {
750
+ const page = requirePage(doc, op2.page, "section.delete");
751
+ const from = page.sections.findIndex((s) => s.id === op2.section);
752
+ if (from < 0)
753
+ throw new OpError(`Section ${op2.section} is not on page ${op2.page}`, ["pages", op2.page]);
754
+ page.sections.splice(from, 1);
755
+ return;
756
+ }
757
+ case "section.update": {
758
+ const section = requireSection(doc, op2.page, op2.section);
759
+ checkPatch(
760
+ op2,
761
+ PROTECTED_PATHS.section,
762
+ [...containerPath(op2.page), "sections", op2.section],
763
+ "section"
764
+ );
765
+ applyPatch(section, op2);
766
+ return;
767
+ }
768
+ case "styles.patch": {
769
+ checkPatch(op2.patch, PROTECTED_PATHS.styles, ["styles"], "styles");
770
+ applyPatch(doc.styles, op2.patch);
771
+ return;
772
+ }
773
+ case "page.create": {
774
+ if (getPage(doc, op2.page.id))
775
+ throw new OpError(`Page ${op2.page.id} already exists`, ["pages", op2.page.id]);
776
+ doc.pages[op2.page.id] = clone(op2.page);
777
+ if (op2.nav) doc.site.nav = clone(op2.nav);
778
+ return;
779
+ }
780
+ case "page.update": {
781
+ const page = requirePage(doc, op2.page, "page.update");
782
+ checkPatch(op2, PROTECTED_PATHS.page, ["pages", op2.page], "page");
783
+ applyPatch(page, op2);
784
+ return;
785
+ }
786
+ case "page.delete": {
787
+ requirePage(doc, op2.page, "page.delete");
788
+ delete doc.pages[op2.page];
789
+ if (op2.nav) doc.site.nav = clone(op2.nav);
790
+ return;
791
+ }
792
+ case "nav.update": {
793
+ doc.site.nav = clone(op2.tree);
794
+ return;
795
+ }
796
+ case "site.update": {
797
+ checkPatch(op2, PROTECTED_PATHS.site, ["site"], "site");
798
+ applyPatch(doc.site, op2);
799
+ return;
800
+ }
801
+ case "media.add": {
802
+ doc.media[op2.path] = clone(op2.meta);
803
+ return;
804
+ }
805
+ case "media.remove": {
806
+ if (!Object.hasOwn(doc.media, op2.path))
807
+ throw new OpError(`Media ${op2.path} does not exist`, ["media", op2.path]);
808
+ delete doc.media[op2.path];
809
+ return;
810
+ }
811
+ case "collection.upsert": {
812
+ const items = Object.hasOwn(doc.collections, op2.collection) ? doc.collections[op2.collection] : void 0;
813
+ if (items) items[op2.item.slug] = clone(op2.item);
814
+ else doc.collections[op2.collection] = { [op2.item.slug]: clone(op2.item) };
815
+ return;
816
+ }
817
+ case "collection.delete": {
818
+ const items = Object.hasOwn(doc.collections, op2.collection) ? doc.collections[op2.collection] : void 0;
819
+ if (!items || !Object.hasOwn(items, op2.slug))
820
+ throw new OpError(`Item ${op2.collection}/${op2.slug} does not exist`, [
821
+ "collections",
822
+ op2.collection,
823
+ op2.slug
824
+ ]);
825
+ delete items[op2.slug];
826
+ if (!op2.keepEmpty && Object.keys(items).length === 0) delete doc.collections[op2.collection];
827
+ return;
828
+ }
829
+ case "collectionConfig.upsert": {
830
+ if (op2.config === null) {
831
+ const configs = doc.collectionConfigs;
832
+ if (!configs || !Object.hasOwn(configs, op2.collection))
833
+ throw new OpError(`Collection ${op2.collection} has no settings`, [
834
+ "collectionConfigs",
835
+ op2.collection
836
+ ]);
837
+ delete configs[op2.collection];
838
+ if (!op2.keepEmpty && Object.keys(configs).length === 0) delete doc.collectionConfigs;
839
+ } else {
840
+ if (!doc.collectionConfigs) doc.collectionConfigs = {};
841
+ doc.collectionConfigs[op2.collection] = clone(op2.config);
842
+ }
843
+ return;
844
+ }
845
+ default: {
846
+ const never = op2;
847
+ throw new OpError(`Unknown op ${never.op}`, ["op"]);
848
+ }
849
+ }
850
+ }
851
+ function apply(doc, op2) {
852
+ return produce(doc, (draft) => {
853
+ mutate(draft, op2);
854
+ });
855
+ }
856
+ function applyAll(doc, ops) {
857
+ return produce(doc, (draft) => {
858
+ ops.forEach((op2, index) => {
859
+ try {
860
+ mutate(draft, op2);
861
+ } catch (e) {
862
+ if (e instanceof OpError) throw Object.assign(e, { index });
863
+ throw e;
864
+ }
865
+ });
866
+ });
867
+ }
868
+
869
+ // src/diff.ts
870
+ function pruneNav(nav, pageId) {
871
+ const out = [];
872
+ for (const item of nav) {
873
+ if (item.type === "page" && item.page === pageId) continue;
874
+ if (item.type === "button" && item.link.type === "page" && item.link.page === pageId) continue;
875
+ if (item.type === "folder") {
876
+ const folder = clone(item);
877
+ if (folder.page === pageId) delete folder.page;
878
+ folder.items = folder.items.filter((leaf) => !(leaf.type === "page" && leaf.page === pageId));
879
+ out.push(folder);
880
+ continue;
881
+ }
882
+ out.push(clone(item));
883
+ }
884
+ return out;
885
+ }
886
+ function navRefersTo(nav, pageId) {
887
+ return !deepEqual(pruneNav(nav, pageId), nav);
888
+ }
889
+ function diff(before, after, options = {}) {
890
+ const actor = options.actor ?? "system";
891
+ const ts = options.ts ?? 0;
892
+ const prefix = options.idPrefix ?? "diff-";
893
+ const ops = [];
894
+ let work = before;
895
+ const emit = (body) => {
896
+ const meta = { id: `${prefix}${ops.length + 1}`, actor, ts };
897
+ const op2 = { ...body, ...meta };
898
+ ops.push(op2);
899
+ work = apply(work, op2);
900
+ };
901
+ const afterSections = /* @__PURE__ */ new Map();
902
+ const afterBlocks = /* @__PURE__ */ new Set();
903
+ const noteSection = (container, s) => {
904
+ afterSections.set(s.id, container);
905
+ for (const b of s.blocks) afterBlocks.add(b.id);
906
+ };
907
+ noteSection(FOOTER, after.site.footer);
908
+ for (const page of Object.values(after.pages))
909
+ for (const s of page.sections) noteSection(page.id, s);
910
+ const styles = diffObjects(before.styles, after.styles);
911
+ if (!isEmptyPatch(styles)) emit({ op: "styles.patch", patch: compactPatch(styles) });
912
+ for (const id of Object.keys(before.pages)) {
913
+ if (Object.hasOwn(after.pages, id)) continue;
914
+ const nav = work.site.nav;
915
+ emit(
916
+ navRefersTo(nav, id) ? { op: "page.delete", page: id, nav: pruneNav(nav, id) } : { op: "page.delete", page: id }
917
+ );
918
+ }
919
+ for (const page of Object.values(work.pages))
920
+ for (const s of [...page.sections])
921
+ if (!afterSections.has(s.id)) emit({ op: "section.delete", page: page.id, section: s.id });
922
+ const removeBlock = (id) => {
923
+ const loc = locateBlock(work, id);
924
+ if (loc) emit({ op: "block.delete", page: loc.container, block: id });
925
+ };
926
+ const allBlocks = (doc) => {
927
+ const ids = doc.site.footer.blocks.map((b) => b.id);
928
+ for (const p of Object.values(doc.pages))
929
+ for (const s of p.sections) for (const b of s.blocks) ids.push(b.id);
930
+ return ids;
931
+ };
932
+ for (const id of allBlocks(work)) if (!afterBlocks.has(id)) removeBlock(id);
933
+ for (const page of Object.values(after.pages)) {
934
+ if (getPage(work, page.id)) continue;
935
+ const reuses = page.sections.some(
936
+ (s) => locateSection(work, s.id) || s.blocks.some((b) => locateBlock(work, b.id))
937
+ );
938
+ emit({ op: "page.create", page: reuses ? { ...clone(page), sections: [] } : clone(page) });
939
+ }
940
+ const insertWhole = (page, at, section) => {
941
+ for (const b of section.blocks) removeBlock(b.id);
942
+ emit({ op: "section.insert", page, at, section: clone(section) });
943
+ };
944
+ const needsReplace = (w, s) => w.layout.mode !== s.layout.mode || w.layout.mode === "stack" && s.layout.mode === "stack" && w.layout.columns.d.length !== s.layout.columns.d.length;
945
+ const syncBlocks = (container, target) => {
946
+ target.blocks.forEach((ab, i) => {
947
+ const cur = liveSection(work, container, target.id);
948
+ const grid = target.layout.mode === "grid" ? target.layout : void 0;
949
+ if (cur.blocks[i]?.id !== ab.id) {
950
+ const loc = locateBlock(work, ab.id);
951
+ if (loc && loc.container === container) {
952
+ const to = { section: target.id, at: i };
953
+ const item = grid?.items[ab.id];
954
+ if (item) to.place = clone(item);
955
+ if (!grid) to.column = ab.column ?? null;
956
+ emit({ op: "block.move", page: container, block: ab.id, to });
957
+ } else {
958
+ if (loc) removeBlock(ab.id);
959
+ const item = grid?.items[ab.id];
960
+ emit({
961
+ op: "block.insert",
962
+ page: container,
963
+ section: target.id,
964
+ at: i,
965
+ block: clone(ab),
966
+ ...item ? { place: clone(item) } : {}
967
+ });
968
+ }
969
+ }
970
+ const wb = liveSection(work, container, target.id).blocks[i];
971
+ const set = {};
972
+ const unset = [];
973
+ for (const k of Object.keys(wb.fields))
974
+ if (!Object.hasOwn(ab.fields, k) || ab.fields[k] === void 0) unset.push(k);
975
+ for (const [k, v] of Object.entries(ab.fields))
976
+ if (v !== void 0 && !deepEqual(wb.fields[k], v)) set[k] = clone(v);
977
+ const upd = {
978
+ op: "block.update",
979
+ page: container,
980
+ block: ab.id
981
+ };
982
+ if (Object.keys(set).length) upd.set = set;
983
+ if (unset.length) upd.unset = unset;
984
+ if (!deepEqual(wb.variant, ab.variant)) upd.variant = ab.variant ?? null;
985
+ if (!deepEqual(wb.style, ab.style)) upd.style = ab.style ? clone(ab.style) : null;
986
+ if (!deepEqual(wb.column, ab.column)) upd.column = ab.column ?? null;
987
+ if (!deepEqual(wb.label, ab.label)) upd.label = ab.label ?? null;
988
+ if (Boolean(wb.hidden) !== Boolean(ab.hidden)) upd.hidden = ab.hidden ? true : null;
989
+ if (Object.keys(upd).length > 3) emit(upd);
990
+ if (grid) {
991
+ const wl = liveSection(work, container, target.id).layout;
992
+ const witem = wl.mode === "grid" ? wl.items[ab.id] : void 0;
993
+ const sitem = grid.items[ab.id];
994
+ if (sitem)
995
+ for (const bp of ["d", "t", "m"]) {
996
+ if (deepEqual(witem?.[bp], sitem[bp])) continue;
997
+ const rect = sitem[bp];
998
+ emit({
999
+ op: "grid.place",
1000
+ page: container,
1001
+ section: target.id,
1002
+ block: ab.id,
1003
+ bp,
1004
+ rect: rect ? clone(rect) : null
1005
+ });
1006
+ }
1007
+ }
1008
+ });
1009
+ };
1010
+ {
1011
+ const target = after.site.footer;
1012
+ const w = work.site.footer;
1013
+ if (w.id !== target.id || needsReplace(w, target)) {
1014
+ for (const b of target.blocks) if (!w.blocks.some((x) => x.id === b.id)) removeBlock(b.id);
1015
+ emit({ op: "site.update", set: { footer: clone(target) } });
1016
+ } else syncBlocks(FOOTER, target);
1017
+ }
1018
+ for (const page of Object.values(after.pages)) {
1019
+ page.sections.forEach((target, j) => {
1020
+ const cur = getPage(work, page.id).sections;
1021
+ if (cur[j]?.id !== target.id) {
1022
+ const loc = locateSection(work, target.id);
1023
+ if (loc && loc.container === page.id) {
1024
+ emit({ op: "section.move", page: page.id, section: target.id, at: j });
1025
+ } else {
1026
+ if (loc && loc.container !== FOOTER)
1027
+ emit({ op: "section.delete", page: loc.container, section: target.id });
1028
+ insertWhole(page.id, j, target);
1029
+ return;
1030
+ }
1031
+ }
1032
+ const w = getPage(work, page.id).sections[j];
1033
+ if (needsReplace(w, target)) {
1034
+ emit({ op: "section.delete", page: page.id, section: target.id });
1035
+ insertWhole(page.id, j, target);
1036
+ return;
1037
+ }
1038
+ syncBlocks(page.id, target);
1039
+ });
1040
+ }
1041
+ const syncSection = (container, target) => {
1042
+ const w = findSection(work, container, target.id)?.section;
1043
+ if (!w) return;
1044
+ const patch = diffObjects(w, target, ["id", "blocks"]);
1045
+ if (!isEmptyPatch(patch))
1046
+ emit({ op: "section.update", page: container, section: target.id, ...compactPatch(patch) });
1047
+ };
1048
+ syncSection(FOOTER, after.site.footer);
1049
+ for (const page of Object.values(after.pages))
1050
+ for (const s of page.sections) syncSection(page.id, s);
1051
+ for (const page of Object.values(after.pages)) {
1052
+ const w = getPage(work, page.id);
1053
+ const patch = diffObjects(w, page, ["id", "sections"]);
1054
+ if (!isEmptyPatch(patch)) emit({ op: "page.update", page: page.id, ...compactPatch(patch) });
1055
+ }
1056
+ const site = diffObjects(work.site, after.site, [
1057
+ "nav",
1058
+ "footer"
1059
+ ]);
1060
+ if (!isEmptyPatch(site)) emit({ op: "site.update", ...compactPatch(site) });
1061
+ if (!deepEqual(work.site.nav, after.site.nav))
1062
+ emit({ op: "nav.update", tree: clone(after.site.nav) });
1063
+ for (const path of Object.keys(work.media))
1064
+ if (!Object.hasOwn(after.media, path)) emit({ op: "media.remove", path });
1065
+ for (const [path, meta] of Object.entries(after.media))
1066
+ if (!deepEqual(work.media[path], meta)) emit({ op: "media.add", path, meta: clone(meta) });
1067
+ for (const [name2, items] of Object.entries(work.collections)) {
1068
+ const keep = after.collections[name2];
1069
+ const gone = Object.keys(items).filter((slug) => !keep || !Object.hasOwn(keep, slug));
1070
+ for (const slug of gone)
1071
+ emit({
1072
+ op: "collection.delete",
1073
+ collection: name2,
1074
+ slug,
1075
+ ...keep ? { keepEmpty: true } : {}
1076
+ });
1077
+ }
1078
+ for (const [name2, items] of Object.entries(after.collections))
1079
+ for (const [slug, item] of Object.entries(items))
1080
+ if (!deepEqual(work.collections[name2]?.[slug], item))
1081
+ emit({ op: "collection.upsert", collection: name2, item: clone(item) });
1082
+ const wc = work.collectionConfigs ?? {};
1083
+ const ac = after.collectionConfigs;
1084
+ const goneConfigs = Object.keys(wc).filter((n) => !ac || !Object.hasOwn(ac, n));
1085
+ for (const name2 of goneConfigs)
1086
+ emit({
1087
+ op: "collectionConfig.upsert",
1088
+ collection: name2,
1089
+ config: null,
1090
+ ...ac ? { keepEmpty: true } : {}
1091
+ });
1092
+ for (const [name2, config] of Object.entries(ac ?? {}))
1093
+ if (!deepEqual(wc[name2], config))
1094
+ emit({ op: "collectionConfig.upsert", collection: name2, config: clone(config) });
1095
+ return ops;
1096
+ }
1097
+ function compactPatch(patch) {
1098
+ const out = {};
1099
+ if (Object.keys(patch.set).length) out.set = patch.set;
1100
+ if (patch.unset.length) out.unset = patch.unset;
1101
+ return out;
1102
+ }
1103
+ function liveSection(doc, container, id) {
1104
+ const loc = findSection(doc, container, id);
1105
+ if (!loc) throw new Error(`diff: section ${id} vanished from ${container}`);
1106
+ return loc.section;
1107
+ }
1108
+
1109
+ // src/invert.ts
1110
+ function compact(value) {
1111
+ const out = {};
1112
+ for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = v;
1113
+ return out;
1114
+ }
1115
+ function need(value, message, path) {
1116
+ if (value === void 0) throw new OpError(message, path);
1117
+ return value;
1118
+ }
1119
+ function blockLoc(doc, page, block) {
1120
+ need(sectionsOf(doc, page), `Page ${page} does not exist`, containerPath(page));
1121
+ return need(findBlock(doc, page, block), `Block ${block} does not exist`, containerPath(page));
1122
+ }
1123
+ function sectionLoc(doc, page, section) {
1124
+ need(sectionsOf(doc, page), `Page ${page} does not exist`, containerPath(page));
1125
+ return need(
1126
+ findSection(doc, page, section),
1127
+ `Section ${section} does not exist`,
1128
+ containerPath(page)
1129
+ );
1130
+ }
1131
+ function invert(doc, op2, meta) {
1132
+ const m = meta ?? { id: `${op2.id}:undo`, actor: op2.actor, ts: op2.ts };
1133
+ const body = invertBody(doc, op2);
1134
+ return { ...body, id: m.id, actor: m.actor, ts: m.ts };
1135
+ }
1136
+ function invertBody(doc, op2) {
1137
+ switch (op2.op) {
1138
+ case "block.update": {
1139
+ const { block } = blockLoc(doc, op2.page, op2.block);
1140
+ const set = {};
1141
+ const unset = [];
1142
+ const keys = /* @__PURE__ */ new Set([...op2.unset ?? [], ...Object.keys(op2.set ?? {})]);
1143
+ for (const k of keys) {
1144
+ if (Object.hasOwn(block.fields, k) && block.fields[k] !== void 0)
1145
+ set[k] = clone(block.fields[k]);
1146
+ else if (op2.set && Object.hasOwn(op2.set, k)) unset.push(k);
1147
+ }
1148
+ return compact({
1149
+ op: "block.update",
1150
+ page: op2.page,
1151
+ block: op2.block,
1152
+ set: Object.keys(set).length ? set : void 0,
1153
+ unset: unset.length ? unset : void 0,
1154
+ variant: op2.variant !== void 0 ? block.variant ?? null : void 0,
1155
+ style: op2.style !== void 0 ? block.style ? clone(block.style) : null : void 0,
1156
+ column: op2.column !== void 0 ? block.column ?? null : void 0,
1157
+ label: op2.label !== void 0 ? block.label ?? null : void 0,
1158
+ hidden: op2.hidden !== void 0 ? block.hidden ?? null : void 0
1159
+ });
1160
+ }
1161
+ case "block.insert": {
1162
+ const { section } = sectionLoc(doc, op2.page, op2.section);
1163
+ return compact({
1164
+ op: "block.delete",
1165
+ page: op2.page,
1166
+ block: op2.block.id,
1167
+ rows: section.layout.mode === "grid" ? copyRows(section.layout.rows) : void 0
1168
+ });
1169
+ }
1170
+ case "block.delete": {
1171
+ const loc = blockLoc(doc, op2.page, op2.block);
1172
+ const layout = loc.section.layout;
1173
+ const item = layout.mode === "grid" ? layout.items[op2.block] : void 0;
1174
+ return compact({
1175
+ op: "block.insert",
1176
+ page: op2.page,
1177
+ section: loc.section.id,
1178
+ at: loc.index,
1179
+ block: clone(loc.block),
1180
+ place: item ? clone(item) : void 0,
1181
+ rows: layout.mode === "grid" ? copyRows(layout.rows) : void 0
1182
+ });
1183
+ }
1184
+ case "block.move": {
1185
+ const from = blockLoc(doc, op2.page, op2.block);
1186
+ const dest = sectionLoc(doc, op2.page, op2.to.section).section;
1187
+ const src = from.section;
1188
+ const same = src.id === dest.id;
1189
+ const srcItem = src.layout.mode === "grid" ? src.layout.items[op2.block] : void 0;
1190
+ return compact({
1191
+ op: "block.move",
1192
+ page: op2.page,
1193
+ block: op2.block,
1194
+ to: compact({
1195
+ section: src.id,
1196
+ at: from.index,
1197
+ place: srcItem ? clone(srcItem) : void 0,
1198
+ column: from.block.column ?? null,
1199
+ rows: src.layout.mode === "grid" ? copyRows(src.layout.rows) : void 0
1200
+ }),
1201
+ fromRows: !same && dest.layout.mode === "grid" ? copyRows(dest.layout.rows) : void 0
1202
+ });
1203
+ }
1204
+ case "grid.place": {
1205
+ const { section } = sectionLoc(doc, op2.page, op2.section);
1206
+ if (section.layout.mode !== "grid")
1207
+ throw new OpError(`Section ${op2.section} is not a grid section`, containerPath(op2.page));
1208
+ const item = section.layout.items[op2.block];
1209
+ const prev = item?.[op2.bp];
1210
+ return {
1211
+ op: "grid.place",
1212
+ page: op2.page,
1213
+ section: op2.section,
1214
+ block: op2.block,
1215
+ bp: op2.bp,
1216
+ rect: prev ? clone(prev) : null,
1217
+ rows: copyRows(section.layout.rows)
1218
+ };
1219
+ }
1220
+ case "section.insert":
1221
+ return { op: "section.delete", page: op2.page, section: op2.section.id };
1222
+ case "section.delete": {
1223
+ const loc = sectionLoc(doc, op2.page, op2.section);
1224
+ return { op: "section.insert", page: op2.page, at: loc.index, section: clone(loc.section) };
1225
+ }
1226
+ case "section.move": {
1227
+ const loc = sectionLoc(doc, op2.page, op2.section);
1228
+ return { op: "section.move", page: op2.page, section: op2.section, at: loc.index };
1229
+ }
1230
+ case "section.update": {
1231
+ const loc = sectionLoc(doc, op2.page, op2.section);
1232
+ return {
1233
+ op: "section.update",
1234
+ page: op2.page,
1235
+ section: op2.section,
1236
+ ...invertPatch(loc.section, op2)
1237
+ };
1238
+ }
1239
+ case "styles.patch":
1240
+ return { op: "styles.patch", patch: invertPatch(doc.styles, op2.patch) };
1241
+ case "page.create":
1242
+ return compact({
1243
+ op: "page.delete",
1244
+ page: op2.page.id,
1245
+ nav: op2.nav ? clone(doc.site.nav) : void 0
1246
+ });
1247
+ case "page.delete": {
1248
+ const page = need(getPage(doc, op2.page), `Page ${op2.page} does not exist`, [
1249
+ "pages",
1250
+ op2.page
1251
+ ]);
1252
+ return compact({
1253
+ op: "page.create",
1254
+ page: clone(page),
1255
+ nav: op2.nav ? clone(doc.site.nav) : void 0
1256
+ });
1257
+ }
1258
+ case "page.update": {
1259
+ const page = need(getPage(doc, op2.page), `Page ${op2.page} does not exist`, [
1260
+ "pages",
1261
+ op2.page
1262
+ ]);
1263
+ return { op: "page.update", page: op2.page, ...invertPatch(page, op2) };
1264
+ }
1265
+ case "nav.update":
1266
+ return { op: "nav.update", tree: clone(doc.site.nav) };
1267
+ case "site.update":
1268
+ return { op: "site.update", ...invertPatch(doc.site, op2) };
1269
+ case "media.add": {
1270
+ const prev = Object.hasOwn(doc.media, op2.path) ? doc.media[op2.path] : void 0;
1271
+ return prev ? { op: "media.add", path: op2.path, meta: clone(prev) } : { op: "media.remove", path: op2.path };
1272
+ }
1273
+ case "media.remove": {
1274
+ const prev = need(
1275
+ Object.hasOwn(doc.media, op2.path) ? doc.media[op2.path] : void 0,
1276
+ `Media ${op2.path} does not exist`,
1277
+ ["media", op2.path]
1278
+ );
1279
+ return { op: "media.add", path: op2.path, meta: clone(prev) };
1280
+ }
1281
+ case "collection.upsert": {
1282
+ const items = Object.hasOwn(doc.collections, op2.collection) ? doc.collections[op2.collection] : void 0;
1283
+ const prev = items && Object.hasOwn(items, op2.item.slug) ? items[op2.item.slug] : void 0;
1284
+ return prev ? { op: "collection.upsert", collection: op2.collection, item: clone(prev) } : compact({
1285
+ op: "collection.delete",
1286
+ collection: op2.collection,
1287
+ slug: op2.item.slug,
1288
+ keepEmpty: items ? true : void 0
1289
+ });
1290
+ }
1291
+ case "collection.delete": {
1292
+ const items = Object.hasOwn(doc.collections, op2.collection) ? doc.collections[op2.collection] : void 0;
1293
+ const prev = need(
1294
+ items && Object.hasOwn(items, op2.slug) ? items[op2.slug] : void 0,
1295
+ `Item ${op2.collection}/${op2.slug} does not exist`,
1296
+ ["collections", op2.collection, op2.slug]
1297
+ );
1298
+ return { op: "collection.upsert", collection: op2.collection, item: clone(prev) };
1299
+ }
1300
+ case "collectionConfig.upsert": {
1301
+ const configs = doc.collectionConfigs;
1302
+ const prev = configs && Object.hasOwn(configs, op2.collection) ? configs[op2.collection] : void 0;
1303
+ if (op2.config === null) {
1304
+ const p = need(prev, `Collection ${op2.collection} has no settings`, [
1305
+ "collectionConfigs",
1306
+ op2.collection
1307
+ ]);
1308
+ return { op: "collectionConfig.upsert", collection: op2.collection, config: clone(p) };
1309
+ }
1310
+ return prev ? { op: "collectionConfig.upsert", collection: op2.collection, config: clone(prev) } : compact({
1311
+ op: "collectionConfig.upsert",
1312
+ collection: op2.collection,
1313
+ config: null,
1314
+ keepEmpty: configs ? true : void 0
1315
+ });
1316
+ }
1317
+ default: {
1318
+ const never = op2;
1319
+ throw new OpError(`Unknown op ${never.op}`, ["op"]);
1320
+ }
1321
+ }
1322
+ }
1323
+ function invertAll(doc, ops) {
1324
+ const out = [];
1325
+ let cur = doc;
1326
+ for (const op2 of ops) {
1327
+ out.push(invert(cur, op2));
1328
+ cur = apply(cur, op2);
1329
+ }
1330
+ return out.reverse();
1331
+ }
1332
+
1333
+ // src/validate.ts
1334
+ import {
1335
+ PageSchema as PageSchema2,
1336
+ SectionSchema as SectionSchema2,
1337
+ SiteDocumentSchema,
1338
+ SiteSchema,
1339
+ StylesSchema
1340
+ } from "@omniharmonic/openspace-schema";
1341
+ import { produce as produce2 } from "immer";
1342
+ function zodIssues(error, prefix) {
1343
+ return error.issues.map((i) => ({
1344
+ path: [...prefix, ...i.path.map((p) => typeof p === "symbol" ? String(p) : p)],
1345
+ message: i.message
1346
+ }));
1347
+ }
1348
+ function siteRefIssues(doc) {
1349
+ const issues = [];
1350
+ const has = (id) => Object.hasOwn(doc.pages, id);
1351
+ const checkItem = (item, path) => {
1352
+ const it = item;
1353
+ if (it.type === "page" && !has(it.page))
1354
+ issues.push({
1355
+ path: [...path, "page"],
1356
+ message: `Navigation links to missing page ${it.page}`
1357
+ });
1358
+ if (it.type === "folder") {
1359
+ if (it.page && !has(it.page))
1360
+ issues.push({
1361
+ path: [...path, "page"],
1362
+ message: `Folder lands on missing page ${it.page}`
1363
+ });
1364
+ it.items.forEach((leaf, j) => {
1365
+ checkItem(leaf, [...path, "items", j]);
1366
+ });
1367
+ }
1368
+ if (it.type === "button" && it.link.type === "page" && !has(it.link.page))
1369
+ issues.push({
1370
+ path: [...path, "link", "page"],
1371
+ message: `Button links to missing page ${it.link.page}`
1372
+ });
1373
+ };
1374
+ doc.site.nav.forEach((item, i) => {
1375
+ checkItem(item, ["site", "nav", i]);
1376
+ });
1377
+ const link = doc.site.announcement?.link;
1378
+ if (link?.type === "page" && !has(link.page))
1379
+ issues.push({
1380
+ path: ["site", "announcement", "link", "page"],
1381
+ message: `Announcement links to missing page ${link.page}`
1382
+ });
1383
+ return issues;
1384
+ }
1385
+ function walk(value, path, visit) {
1386
+ if (Array.isArray(value)) {
1387
+ value.forEach((v, i) => {
1388
+ walk(v, [...path, i], visit);
1389
+ });
1390
+ } else if (isPlainObject(value)) {
1391
+ visit(value, path);
1392
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k], visit);
1393
+ }
1394
+ }
1395
+ function contentLinksTo(doc, pageId) {
1396
+ const out = [];
1397
+ const visit = (o, p) => {
1398
+ if (o.type === "page" && o.page === pageId)
1399
+ out.push({ path: p, message: `Links to deleted page ${pageId}` });
1400
+ };
1401
+ walk(doc.pages, ["pages"], visit);
1402
+ walk(doc.site.footer, ["site", "footer"], visit);
1403
+ return out;
1404
+ }
1405
+ function mediaRefs(doc, path) {
1406
+ const out = [];
1407
+ const scan = (value, p) => {
1408
+ if (typeof value === "string") {
1409
+ if (value === path) out.push({ path: p, message: `Still references removed media ${path}` });
1410
+ } else if (Array.isArray(value)) {
1411
+ value.forEach((v, i) => {
1412
+ scan(v, [...p, i]);
1413
+ });
1414
+ } else if (isPlainObject(value)) for (const [k, v] of Object.entries(value)) scan(v, [...p, k]);
1415
+ };
1416
+ scan(doc.pages, ["pages"]);
1417
+ scan(doc.site, ["site"]);
1418
+ scan(doc.collections, ["collections"]);
1419
+ return out;
1420
+ }
1421
+ function slugIssues(doc, pageId) {
1422
+ const page = doc.pages[pageId];
1423
+ if (!page) return [];
1424
+ for (const other of Object.values(doc.pages)) {
1425
+ if (other.id !== pageId && other.slug === page.slug)
1426
+ return [
1427
+ {
1428
+ path: ["pages", pageId, "slug"],
1429
+ message: `Slug "${page.slug}" is also used by ${other.id}`
1430
+ }
1431
+ ];
1432
+ }
1433
+ return [];
1434
+ }
1435
+ function collectionIssues(doc, name2) {
1436
+ const items = Object.values(doc.collections[name2] ?? {});
1437
+ const kinds = new Set(items.map((i) => i.kind));
1438
+ const out = [];
1439
+ if (kinds.size > 1)
1440
+ out.push({ path: ["collections", name2], message: `Collection ${name2} mixes item kinds` });
1441
+ const configKind = doc.collectionConfigs?.[name2]?.kind;
1442
+ if (configKind && configKind !== "custom") {
1443
+ for (const item of items)
1444
+ if (item.kind !== configKind)
1445
+ out.push({
1446
+ path: ["collections", name2, item.slug, "kind"],
1447
+ message: `Collection ${name2} is a ${configKind} collection but ${item.slug} is ${item.kind}`
1448
+ });
1449
+ }
1450
+ return out;
1451
+ }
1452
+ function newIdIssues(doc, sections, blocks, path) {
1453
+ const idx = indexIds(doc);
1454
+ const out = [];
1455
+ const seen = /* @__PURE__ */ new Set();
1456
+ const claim = (id, kind, taken) => {
1457
+ if (taken || seen.has(id)) out.push({ path, message: `${kind} id ${id} is already used` });
1458
+ seen.add(id);
1459
+ };
1460
+ for (const s of sections) {
1461
+ claim(s.id, "Section", idx.sections.has(s.id));
1462
+ for (const b of s.blocks) claim(b.id, "Block", idx.blocks.has(b.id));
1463
+ }
1464
+ for (const b of blocks) claim(b.id, "Block", idx.blocks.has(b.id));
1465
+ return out;
1466
+ }
1467
+ function globalIdIssues(doc) {
1468
+ const r = SiteDocumentSchema.safeParse(doc);
1469
+ if (r.success) return [];
1470
+ return zodIssues(r.error, []).filter((i) => /id .* is used in both/.test(i.message));
1471
+ }
1472
+ function containerIssues(doc, container) {
1473
+ if (container === FOOTER) {
1474
+ const r2 = SectionSchema2.safeParse(doc.site.footer);
1475
+ return r2.success ? [] : zodIssues(r2.error, ["site", "footer"]);
1476
+ }
1477
+ const page = doc.pages[container];
1478
+ if (!page) return [{ path: ["pages", container], message: `Page ${container} does not exist` }];
1479
+ const r = PageSchema2.safeParse(page);
1480
+ return r.success ? [] : zodIssues(r.error, ["pages", container]);
1481
+ }
1482
+ function blockIssues(registry, doc, container, ids) {
1483
+ if (!registry || ids.length === 0) return [];
1484
+ const want = new Set(ids);
1485
+ const out = [];
1486
+ const base = containerPath(container);
1487
+ (sectionsOf(doc, container) ?? []).forEach((s, i) => {
1488
+ s.blocks.forEach((b, j) => {
1489
+ if (!want.has(b.id)) return;
1490
+ const r = registry.validate(b);
1491
+ if (!r.success)
1492
+ for (const issue of r.issues)
1493
+ out.push({
1494
+ path: [...base, "sections", i, "blocks", j, ...issue.path],
1495
+ message: issue.message
1496
+ });
1497
+ });
1498
+ });
1499
+ return out;
1500
+ }
1501
+ function setValueIssues(op2) {
1502
+ const sets = [];
1503
+ if (op2.op === "styles.patch") sets.push(op2.patch.set ?? {});
1504
+ else if (op2.op === "block.update" || op2.op === "section.update" || op2.op === "page.update" || op2.op === "site.update")
1505
+ sets.push(op2.set ?? {});
1506
+ const out = [];
1507
+ for (const set of sets)
1508
+ for (const [k, v] of Object.entries(set))
1509
+ if (v === void 0 || !isJson(v))
1510
+ out.push({ path: ["set", k], message: `Value for "${k}" is not plain JSON` });
1511
+ return out;
1512
+ }
1513
+ function validate(doc, input, options = {}) {
1514
+ const forbidden = findForbiddenKey(input);
1515
+ if (forbidden)
1516
+ return { ok: false, errors: [{ path: [], message: `Forbidden key "${forbidden}"` }] };
1517
+ let op2;
1518
+ if (options.trusted) op2 = input;
1519
+ else {
1520
+ const parsed = parseOp(input);
1521
+ if (!parsed.ok) return { ok: false, errors: parsed.errors };
1522
+ op2 = parsed.op;
1523
+ }
1524
+ const valueIssues = setValueIssues(op2);
1525
+ if (valueIssues.length) return { ok: false, errors: valueIssues };
1526
+ const pre = [];
1527
+ switch (op2.op) {
1528
+ case "block.insert":
1529
+ pre.push(...newIdIssues(doc, [], [op2.block], ["block", "id"]));
1530
+ break;
1531
+ case "section.insert":
1532
+ pre.push(...newIdIssues(doc, [op2.section], [], ["section", "id"]));
1533
+ break;
1534
+ case "page.create":
1535
+ pre.push(...newIdIssues(doc, op2.page.sections, [], ["page", "sections"]));
1536
+ break;
1537
+ case "media.add":
1538
+ if (op2.meta.path !== op2.path)
1539
+ pre.push({
1540
+ path: ["meta", "path"],
1541
+ message: `Media path ${op2.meta.path} does not match ${op2.path}`
1542
+ });
1543
+ for (const [p, m] of Object.entries(doc.media))
1544
+ if (p !== op2.path && m.id === op2.meta.id)
1545
+ pre.push({
1546
+ path: ["meta", "id"],
1547
+ message: `Media id ${op2.meta.id} is already used by ${p}`
1548
+ });
1549
+ break;
1550
+ }
1551
+ if (pre.length) return { ok: false, errors: pre };
1552
+ let next;
1553
+ try {
1554
+ next = produce2(doc, (draft) => {
1555
+ mutate(draft, op2);
1556
+ });
1557
+ } catch (e) {
1558
+ if (e instanceof OpError) return { ok: false, errors: [{ path: e.path, message: e.message }] };
1559
+ return {
1560
+ ok: false,
1561
+ errors: [{ path: [], message: e instanceof Error ? e.message : String(e) }]
1562
+ };
1563
+ }
1564
+ const errors = [];
1565
+ const warnings = [];
1566
+ switch (op2.op) {
1567
+ case "block.update":
1568
+ errors.push(
1569
+ ...containerIssues(next, op2.page),
1570
+ ...blockIssues(options.registry, next, op2.page, [op2.block])
1571
+ );
1572
+ break;
1573
+ case "block.insert":
1574
+ errors.push(
1575
+ ...containerIssues(next, op2.page),
1576
+ ...blockIssues(options.registry, next, op2.page, [op2.block.id])
1577
+ );
1578
+ break;
1579
+ case "block.move":
1580
+ case "block.delete":
1581
+ case "grid.place":
1582
+ case "section.move":
1583
+ case "section.delete":
1584
+ errors.push(...containerIssues(next, op2.page));
1585
+ break;
1586
+ case "section.update":
1587
+ errors.push(...containerIssues(next, op2.page));
1588
+ break;
1589
+ case "section.insert":
1590
+ errors.push(
1591
+ ...containerIssues(next, op2.page),
1592
+ ...blockIssues(
1593
+ options.registry,
1594
+ next,
1595
+ op2.page,
1596
+ op2.section.blocks.map((b) => b.id)
1597
+ )
1598
+ );
1599
+ break;
1600
+ case "page.create":
1601
+ errors.push(
1602
+ ...containerIssues(next, op2.page.id),
1603
+ ...slugIssues(next, op2.page.id),
1604
+ ...blockIssues(
1605
+ options.registry,
1606
+ next,
1607
+ op2.page.id,
1608
+ op2.page.sections.flatMap((s) => s.blocks.map((b) => b.id))
1609
+ )
1610
+ );
1611
+ if (op2.nav) errors.push(...siteSchemaIssues(next), ...siteRefIssues(next));
1612
+ break;
1613
+ case "page.update":
1614
+ errors.push(...containerIssues(next, op2.page));
1615
+ if (patchPaths(op2).some((p) => isPathPrefix("slug", p)))
1616
+ errors.push(...slugIssues(next, op2.page));
1617
+ break;
1618
+ case "page.delete":
1619
+ if (op2.nav) errors.push(...siteSchemaIssues(next));
1620
+ errors.push(...siteRefIssues(next));
1621
+ warnings.push(...contentLinksTo(next, op2.page));
1622
+ break;
1623
+ case "nav.update":
1624
+ errors.push(...siteSchemaIssues(next), ...siteRefIssues(next));
1625
+ break;
1626
+ case "site.update":
1627
+ errors.push(...siteSchemaIssues(next), ...siteRefIssues(next));
1628
+ if (errors.length === 0 && patchPaths(op2).some((p) => isPathPrefix("footer", p)))
1629
+ errors.push(...globalIdIssues(next));
1630
+ break;
1631
+ case "styles.patch": {
1632
+ const r = StylesSchema.safeParse(next.styles);
1633
+ if (!r.success) errors.push(...zodIssues(r.error, ["styles"]));
1634
+ break;
1635
+ }
1636
+ case "media.add":
1637
+ break;
1638
+ case "media.remove":
1639
+ warnings.push(...mediaRefs(next, op2.path));
1640
+ break;
1641
+ case "collection.upsert":
1642
+ case "collectionConfig.upsert":
1643
+ errors.push(...collectionIssues(next, op2.collection));
1644
+ break;
1645
+ case "collection.delete":
1646
+ break;
1647
+ }
1648
+ if (errors.length) return { ok: false, errors };
1649
+ return { ok: true, doc: next, warnings };
1650
+ }
1651
+ function siteSchemaIssues(doc) {
1652
+ const r = SiteSchema.safeParse(doc.site);
1653
+ return r.success ? [] : zodIssues(r.error, ["site"]);
1654
+ }
1655
+ function validateAll(doc, ops, options = {}) {
1656
+ let cur = doc;
1657
+ const warnings = [];
1658
+ for (let index = 0; index < ops.length; index++) {
1659
+ const r = validate(cur, ops[index], options);
1660
+ if (!r.ok) return { ok: false, index, errors: r.errors };
1661
+ cur = r.doc;
1662
+ warnings.push(...r.warnings);
1663
+ }
1664
+ return { ok: true, doc: cur, warnings };
1665
+ }
1666
+ function validateDocument(doc, options = {}) {
1667
+ const r = SiteDocumentSchema.safeParse(doc);
1668
+ if (!r.success) return { ok: false, errors: zodIssues(r.error, []) };
1669
+ const errors = siteRefIssues(r.data);
1670
+ if (options.registry)
1671
+ for (const issue of options.registry.validateDocument(r.data))
1672
+ errors.push({ path: issue.path, message: issue.message });
1673
+ if (errors.length) return { ok: false, errors };
1674
+ return { ok: true, doc, warnings: [] };
1675
+ }
1676
+
1677
+ // src/rebase.ts
1678
+ var edit = (...path) => ({ path, kind: "edit" });
1679
+ var order = (...path) => ({ path, kind: "order" });
1680
+ var sectionContent = (section) => [
1681
+ order("section", section, "blocks"),
1682
+ order("section", section, "props", "layout")
1683
+ ];
1684
+ function pageReq(page) {
1685
+ return page === FOOTER ? [] : [["page", page]];
1686
+ }
1687
+ function footprint(op2, doc) {
1688
+ const fp = baseFootprint(op2);
1689
+ if (doc && (op2.op === "block.update" || op2.op === "block.move" || op2.op === "block.delete" || op2.op === "grid.place")) {
1690
+ const loc = findBlock(doc, op2.page, op2.block);
1691
+ if (loc) {
1692
+ fp.requires.push(["section", loc.section.id]);
1693
+ if (op2.op !== "block.update" && op2.op !== "grid.place")
1694
+ fp.touches.push(...sectionContent(loc.section.id));
1695
+ }
1696
+ }
1697
+ if (op2.op === "section.delete") fp.touches.push(order("page", op2.page, "sections"));
1698
+ return fp;
1699
+ }
1700
+ function baseFootprint(op2) {
1701
+ switch (op2.op) {
1702
+ case "block.update": {
1703
+ const t = [];
1704
+ for (const k of [...op2.unset ?? [], ...Object.keys(op2.set ?? {})])
1705
+ t.push(edit("block", op2.block, "fields", k));
1706
+ if (op2.variant !== void 0) t.push(edit("block", op2.block, "variant"));
1707
+ if (op2.style !== void 0) t.push(edit("block", op2.block, "style"));
1708
+ if (op2.column !== void 0) t.push(edit("block", op2.block, "column"));
1709
+ if (op2.label !== void 0) t.push(edit("block", op2.block, "label"));
1710
+ if (op2.hidden !== void 0) t.push(edit("block", op2.block, "hidden"));
1711
+ return { touches: t, requires: pageReq(op2.page) };
1712
+ }
1713
+ case "block.insert":
1714
+ return {
1715
+ touches: [{ path: ["block", op2.block.id], kind: "create" }, ...sectionContent(op2.section)],
1716
+ requires: [...pageReq(op2.page), ["section", op2.section]]
1717
+ };
1718
+ case "block.move": {
1719
+ const t = [
1720
+ edit("block", op2.block, "position"),
1721
+ ...sectionContent(op2.to.section),
1722
+ order("block", op2.block, "section")
1723
+ ];
1724
+ if (op2.to.column !== void 0) t.push(edit("block", op2.block, "column"));
1725
+ if (op2.to.place) t.push(edit("block", op2.block, "grid"));
1726
+ return { touches: t, requires: [...pageReq(op2.page), ["section", op2.to.section]] };
1727
+ }
1728
+ case "block.delete":
1729
+ return {
1730
+ touches: [
1731
+ { path: ["block", op2.block], kind: "delete" },
1732
+ order("block", op2.block, "section")
1733
+ ],
1734
+ requires: pageReq(op2.page)
1735
+ };
1736
+ case "grid.place":
1737
+ return {
1738
+ touches: [
1739
+ edit("block", op2.block, "grid", op2.bp),
1740
+ order("section", op2.section, "props", "layout", "rows"),
1741
+ order("section", op2.section, "props", "layout", "items", op2.block, op2.bp)
1742
+ ],
1743
+ requires: [...pageReq(op2.page), ["section", op2.section]]
1744
+ };
1745
+ case "section.insert":
1746
+ return {
1747
+ touches: [
1748
+ { path: ["section", op2.section.id], kind: "create" },
1749
+ order("page", op2.page, "sections")
1750
+ ],
1751
+ requires: pageReq(op2.page)
1752
+ };
1753
+ case "section.move":
1754
+ return {
1755
+ touches: [edit("section", op2.section, "position"), order("page", op2.page, "sections")],
1756
+ requires: pageReq(op2.page)
1757
+ };
1758
+ case "section.delete":
1759
+ return {
1760
+ touches: [{ path: ["section", op2.section], kind: "delete" }],
1761
+ requires: pageReq(op2.page)
1762
+ };
1763
+ case "section.update":
1764
+ return {
1765
+ touches: patchPaths(op2).map((p) => edit("section", op2.section, "props", ...splitPath(p))),
1766
+ requires: pageReq(op2.page)
1767
+ };
1768
+ case "styles.patch":
1769
+ return {
1770
+ touches: patchPaths(op2.patch).map((p) => edit("styles", ...splitPath(p))),
1771
+ requires: []
1772
+ };
1773
+ case "page.create":
1774
+ return {
1775
+ touches: [{ path: ["page", op2.page.id], kind: "create" }, ...op2.nav ? [edit("nav")] : []],
1776
+ requires: []
1777
+ };
1778
+ case "page.update":
1779
+ return {
1780
+ touches: patchPaths(op2).map((p) => edit("page", op2.page, "props", ...splitPath(p))),
1781
+ requires: []
1782
+ };
1783
+ case "page.delete":
1784
+ return {
1785
+ touches: [{ path: ["page", op2.page], kind: "delete" }, ...op2.nav ? [edit("nav")] : []],
1786
+ requires: []
1787
+ };
1788
+ case "nav.update":
1789
+ return { touches: [edit("nav")], requires: [] };
1790
+ case "site.update":
1791
+ return { touches: patchPaths(op2).map((p) => edit("site", ...splitPath(p))), requires: [] };
1792
+ case "media.add":
1793
+ return { touches: [edit("media", op2.path)], requires: [] };
1794
+ case "media.remove":
1795
+ return { touches: [{ path: ["media", op2.path], kind: "delete" }], requires: [] };
1796
+ case "collection.upsert":
1797
+ return { touches: [edit("item", op2.collection, op2.item.slug)], requires: [] };
1798
+ case "collection.delete":
1799
+ return {
1800
+ touches: [{ path: ["item", op2.collection, op2.slug], kind: "delete" }],
1801
+ requires: []
1802
+ };
1803
+ case "collectionConfig.upsert":
1804
+ return { touches: [edit("collectionConfig", op2.collection)], requires: [] };
1805
+ }
1806
+ }
1807
+ function isPrefix(a, b) {
1808
+ if (a.length > b.length) return false;
1809
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1810
+ return true;
1811
+ }
1812
+ var overlaps = (a, b) => isPrefix(a, b) || isPrefix(b, a);
1813
+ function wins(a, b) {
1814
+ if (a.ts !== b.ts) return a.ts > b.ts;
1815
+ return a.actor >= b.actor;
1816
+ }
1817
+ function independent(a, b, doc) {
1818
+ const fa = footprint(a, doc);
1819
+ const fb = footprint(b, doc);
1820
+ for (const ta of fa.touches)
1821
+ for (const tb of fb.touches) if (overlaps(ta.path, tb.path)) return false;
1822
+ for (const r of fa.requires)
1823
+ for (const tb of fb.touches) if (tb.kind === "delete" && isPrefix(tb.path, r)) return false;
1824
+ for (const r of fb.requires)
1825
+ for (const ta of fa.touches) if (ta.kind === "delete" && isPrefix(ta.path, r)) return false;
1826
+ return true;
1827
+ }
1828
+ function strip(op2, paths) {
1829
+ const hit = (p) => paths.some((q) => overlaps(p, q));
1830
+ const stripPatch = (patch, base) => {
1831
+ const set = {};
1832
+ for (const [k, v] of Object.entries(patch.set ?? {}))
1833
+ if (!hit([...base, ...splitPath(k)])) set[k] = v;
1834
+ const unset = (patch.unset ?? []).filter((k) => !hit([...base, ...splitPath(k)]));
1835
+ if (Object.keys(set).length === 0 && unset.length === 0) return null;
1836
+ const out = { ...patch };
1837
+ if (Object.keys(set).length) out.set = set;
1838
+ else delete out.set;
1839
+ if (unset.length) out.unset = unset;
1840
+ else delete out.unset;
1841
+ return out;
1842
+ };
1843
+ switch (op2.op) {
1844
+ case "block.update": {
1845
+ const base = ["block", op2.block];
1846
+ const fields = stripPatch({ set: op2.set, unset: op2.unset }, [...base, "fields"]);
1847
+ const next = { ...op2 };
1848
+ delete next.set;
1849
+ delete next.unset;
1850
+ if (fields?.set) next.set = fields.set;
1851
+ if (fields?.unset) next.unset = fields.unset;
1852
+ for (const prop of ["variant", "style", "column", "label", "hidden"])
1853
+ if (next[prop] !== void 0 && hit([...base, prop])) delete next[prop];
1854
+ const empty = next.set === void 0 && next.unset === void 0 && next.variant === void 0 && next.style === void 0 && next.column === void 0 && next.label === void 0 && next.hidden === void 0;
1855
+ return empty ? null : next;
1856
+ }
1857
+ case "styles.patch": {
1858
+ const patch = stripPatch(op2.patch, ["styles"]);
1859
+ return patch ? { ...op2, patch } : null;
1860
+ }
1861
+ case "site.update":
1862
+ return stripPatch(op2, ["site"]);
1863
+ case "page.update":
1864
+ return stripPatch(op2, ["page", op2.page, "props"]);
1865
+ case "section.update":
1866
+ return stripPatch(op2, ["section", op2.section, "props"]);
1867
+ case "page.create":
1868
+ case "page.delete": {
1869
+ const own = op2.op === "page.create" ? op2.page.id : op2.page;
1870
+ if (hit(["page", own])) return null;
1871
+ const { nav: _nav, ...rest } = op2;
1872
+ return rest;
1873
+ }
1874
+ default:
1875
+ return null;
1876
+ }
1877
+ }
1878
+ var key = (p) => p.join("/");
1879
+ function rebase(localPending, remoteOps, options = {}) {
1880
+ const conflicts = [];
1881
+ const dropped = [];
1882
+ const kept = [];
1883
+ const step = (d, o) => {
1884
+ if (!d) return void 0;
1885
+ try {
1886
+ return apply(d, o);
1887
+ } catch {
1888
+ return d;
1889
+ }
1890
+ };
1891
+ let rd = options.base;
1892
+ const remotes = remoteOps.map((r) => {
1893
+ const fp = footprint(r, rd);
1894
+ rd = step(rd, r);
1895
+ return { op: r, fp };
1896
+ });
1897
+ let ld = options.base;
1898
+ for (const local of localPending) {
1899
+ let cur = local;
1900
+ const localDoc = ld;
1901
+ ld = step(ld, local);
1902
+ for (const { op: remote, fp: rfp } of remotes) {
1903
+ if (!cur) break;
1904
+ const lfp = footprint(cur, localDoc);
1905
+ const deletedHits = [];
1906
+ for (const rt of rfp.touches) {
1907
+ if (rt.kind !== "delete") continue;
1908
+ const dependent = lfp.requires.some((r) => isPrefix(rt.path, r)) || lfp.touches.some((lt) => isPrefix(rt.path, lt.path) && lt.kind !== "create");
1909
+ if (dependent) deletedHits.push(rt.path);
1910
+ }
1911
+ if (deletedHits.length) {
1912
+ const bothDelete = lfp.touches.filter((lt) => lt.kind !== "order").every((lt) => lt.kind === "delete" && deletedHits.some((d) => isPrefix(d, lt.path)));
1913
+ if (!bothDelete)
1914
+ conflicts.push({
1915
+ local,
1916
+ remote,
1917
+ winner: "remote",
1918
+ reason: "deleted",
1919
+ paths: deletedHits.map(key),
1920
+ message: `${remote.actor} deleted this`
1921
+ });
1922
+ dropped.push(local);
1923
+ cur = null;
1924
+ break;
1925
+ }
1926
+ const pairs = [];
1927
+ for (const lt of lfp.touches) {
1928
+ if (lt.kind === "order") continue;
1929
+ for (const rt of rfp.touches)
1930
+ if (rt.kind !== "order" && overlaps(lt.path, rt.path)) pairs.push({ l: lt, r: rt });
1931
+ }
1932
+ if (pairs.length === 0) continue;
1933
+ const paths = [
1934
+ ...new Set(pairs.map((p) => key(p.l.path.length >= p.r.path.length ? p.l.path : p.r.path)))
1935
+ ];
1936
+ if (pairs.some((p) => p.l.kind === "create" && p.r.kind === "create")) {
1937
+ conflicts.push({
1938
+ local,
1939
+ remote,
1940
+ winner: "remote",
1941
+ reason: "duplicate",
1942
+ paths,
1943
+ message: `${remote.actor} already added this`
1944
+ });
1945
+ dropped.push(local);
1946
+ cur = null;
1947
+ break;
1948
+ }
1949
+ const localDeletes = pairs.some((p) => p.l.kind === "delete");
1950
+ if (localDeletes || wins(cur, remote)) {
1951
+ conflicts.push({
1952
+ local,
1953
+ remote,
1954
+ winner: "local",
1955
+ reason: "concurrent-edit",
1956
+ paths,
1957
+ message: `${remote.actor} changed this too`
1958
+ });
1959
+ continue;
1960
+ }
1961
+ conflicts.push({
1962
+ local,
1963
+ remote,
1964
+ winner: "remote",
1965
+ reason: "concurrent-edit",
1966
+ paths,
1967
+ message: `${remote.actor} changed this too`
1968
+ });
1969
+ cur = strip(
1970
+ cur,
1971
+ pairs.map((p) => p.r.path)
1972
+ );
1973
+ if (!cur) dropped.push(local);
1974
+ }
1975
+ if (cur) kept.push(cur);
1976
+ }
1977
+ if (!options.doc) return { ops: kept, conflicts, dropped };
1978
+ const ops = [];
1979
+ let doc = options.doc;
1980
+ for (const op2 of kept) {
1981
+ const fixed = normalizePositions(doc, op2);
1982
+ const r = validate(doc, fixed, { trusted: true });
1983
+ if (r.ok) {
1984
+ ops.push(fixed);
1985
+ doc = r.doc;
1986
+ } else {
1987
+ dropped.push(op2);
1988
+ conflicts.push({
1989
+ local: op2,
1990
+ winner: "remote",
1991
+ reason: "invalid",
1992
+ paths: [],
1993
+ message: r.errors[0]?.message ?? "This change no longer applies"
1994
+ });
1995
+ }
1996
+ }
1997
+ return { ops, conflicts, dropped };
1998
+ }
1999
+ function normalizePositions(doc, op2) {
2000
+ const fix = (ids, at) => {
2001
+ if (!ids) return at;
2002
+ if (typeof at === "number") return at > ids.length ? "end" : at;
2003
+ if (typeof at === "string") return at;
2004
+ const anchor = "before" in at ? at.before : at.after;
2005
+ return ids.includes(anchor) ? at : "end";
2006
+ };
2007
+ switch (op2.op) {
2008
+ case "block.insert": {
2009
+ const s = findSection(doc, op2.page, op2.section)?.section;
2010
+ return {
2011
+ ...op2,
2012
+ at: fix(
2013
+ s?.blocks.map((b) => b.id),
2014
+ op2.at
2015
+ )
2016
+ };
2017
+ }
2018
+ case "block.move": {
2019
+ const s = findSection(doc, op2.page, op2.to.section)?.section;
2020
+ const ids = s?.blocks.map((b) => b.id).filter((id) => id !== op2.block);
2021
+ return { ...op2, to: { ...op2.to, at: fix(ids, op2.to.at) } };
2022
+ }
2023
+ case "section.insert": {
2024
+ const p = getPage(doc, op2.page);
2025
+ return {
2026
+ ...op2,
2027
+ at: fix(
2028
+ p?.sections.map((s) => s.id),
2029
+ op2.at
2030
+ )
2031
+ };
2032
+ }
2033
+ case "section.move": {
2034
+ const p = getPage(doc, op2.page);
2035
+ return {
2036
+ ...op2,
2037
+ at: fix(
2038
+ p?.sections.map((s) => s.id).filter((id) => id !== op2.section),
2039
+ op2.at
2040
+ )
2041
+ };
2042
+ }
2043
+ default:
2044
+ return op2;
2045
+ }
2046
+ }
2047
+
2048
+ // src/roles.ts
2049
+ var ROLES = ["owner", "designer", "editor", "contributor"];
2050
+ function sectionLook(section) {
2051
+ const layout = section.layout;
2052
+ if (layout.mode !== "grid") return { layout, style: section.style };
2053
+ const order2 = new Map(section.blocks.map((b, i) => [b.id, i]));
2054
+ const items = Object.entries(layout.items).map(([id, item]) => [order2.get(id) ?? -1, item]).sort((a, b) => a[0] - b[0]);
2055
+ return { layout: { ...layout, items }, style: section.style };
2056
+ }
2057
+ function isKnownSectionLook(section, context) {
2058
+ const look = sectionLook(section);
2059
+ if (section.preset && context.presetSection) {
2060
+ const built = context.presetSection(section.preset, section.style?.colorTheme);
2061
+ if (built && deepEqual(sectionLook(built), look)) return true;
2062
+ }
2063
+ const doc = context.doc;
2064
+ if (!doc) return false;
2065
+ if (deepEqual(sectionLook(doc.site.footer), look)) return true;
2066
+ for (const page of Object.values(doc.pages))
2067
+ for (const s of page.sections) if (deepEqual(sectionLook(s), look)) return true;
2068
+ return false;
2069
+ }
2070
+ function lockedSectionsOf(doc) {
2071
+ const out = /* @__PURE__ */ new Set();
2072
+ if (doc.site.footer.locked) out.add(doc.site.footer.id);
2073
+ for (const page of Object.values(doc.pages))
2074
+ for (const s of page.sections) if (s.locked) out.add(s.id);
2075
+ return out;
2076
+ }
2077
+ var CONTENT_KINDS = /* @__PURE__ */ new Set(["text", "richText", "image"]);
2078
+ function isImageValue(v) {
2079
+ return isPlainObject(v) && typeof v.src === "string";
2080
+ }
2081
+ function contentShape(v) {
2082
+ if (typeof v === "string") return "string";
2083
+ if (isPlainObject(v) && v.type === "doc" && Array.isArray(v.content)) return "doc";
2084
+ if (isImageValue(v)) return "image";
2085
+ if (Array.isArray(v) && v.length > 0 && v.every(isImageValue)) return "images";
2086
+ return void 0;
2087
+ }
2088
+ function sameContentShape(before, after) {
2089
+ const a = contentShape(before);
2090
+ return a !== void 0 && a === contentShape(after);
2091
+ }
2092
+ function isTextOrImageValue(v) {
2093
+ if (typeof v === "string") return true;
2094
+ if (isPlainObject(v) && v.type === "doc" && Array.isArray(v.content)) return true;
2095
+ if (isImageValue(v)) return true;
2096
+ return Array.isArray(v) && v.length > 0 && v.every(isImageValue);
2097
+ }
2098
+ var deny = (reason) => ({ allowed: false, reason });
2099
+ function isInverseOf(op2, priorOp, docBefore) {
2100
+ let expected;
2101
+ try {
2102
+ expected = invert(docBefore, priorOp);
2103
+ } catch {
2104
+ return false;
2105
+ }
2106
+ const strip2 = ({ id: _i, actor: _a, ts: _t, ...body }) => body;
2107
+ return deepEqual(strip2(op2), strip2(expected));
2108
+ }
2109
+ function transformForRole(op2, role, lockedSections, context = {}) {
2110
+ const allow = { allowed: true, op: op2 };
2111
+ if (role === "owner" || role === "designer") return allow;
2112
+ const { doc, registry } = context;
2113
+ if (role === "contributor") {
2114
+ if (op2.op !== "collection.upsert")
2115
+ return deny("Contributors can only create and edit draft collection items");
2116
+ if (op2.item.frontmatter.status !== "draft")
2117
+ return deny("Contributors can only save items as drafts; an editor publishes them");
2118
+ if (!doc) return deny("Can't check this item without the current document");
2119
+ const existing = doc.collections[op2.collection]?.[op2.item.slug];
2120
+ if (existing && existing.frontmatter.status !== "draft")
2121
+ return deny("This item is already published or scheduled; only editors can change it");
2122
+ if (existing) {
2123
+ const author = context.itemAuthor?.(op2.collection, op2.item.slug);
2124
+ if (!context.userId || !author || author !== context.userId)
2125
+ return deny("Someone else started this draft; only they or an editor can change it");
2126
+ }
2127
+ return allow;
2128
+ }
2129
+ if (role !== "editor") return deny(`Unknown role ${String(role)}`);
2130
+ const locked = new Set(lockedSections);
2131
+ const sectionOfBlock = (page, block) => {
2132
+ if (!doc) return void 0;
2133
+ const loc = findBlock(doc, page, block) ?? locateBlock(doc, block);
2134
+ return loc?.section.id;
2135
+ };
2136
+ const isLocked = (section) => section === void 0 || locked.has(section);
2137
+ const sectionById = (page, id) => doc ? findSection(doc, page, id)?.section : void 0;
2138
+ const NO_GRID = "Editors can't reposition blocks on the grid";
2139
+ switch (op2.op) {
2140
+ case "styles.patch":
2141
+ return deny("Editors can't change site styles");
2142
+ case "grid.place":
2143
+ return deny("Editors can't reposition blocks on the grid");
2144
+ case "section.update": {
2145
+ const paths = patchPaths(op2);
2146
+ if (paths.some((p) => isPathPrefix("layout", p) || isPathPrefix("style", p)))
2147
+ return deny("Editors can't change section layout or style");
2148
+ if (paths.some((p) => isPathPrefix("locked", p)))
2149
+ return deny("Only owners and designers can lock or unlock sections");
2150
+ if (locked.has(op2.section)) return deny("This section is locked");
2151
+ return allow;
2152
+ }
2153
+ case "section.move":
2154
+ case "section.delete":
2155
+ if (locked.has(op2.section)) return deny("This section is locked");
2156
+ return allow;
2157
+ case "section.insert":
2158
+ if (op2.section.locked) return deny("Only owners and designers can lock sections");
2159
+ if (!isKnownSectionLook(op2.section, context))
2160
+ return deny(
2161
+ "Editors add sections from the section gallery (or duplicate one); its layout and style come from the design"
2162
+ );
2163
+ return allow;
2164
+ case "block.insert":
2165
+ if (locked.has(op2.section)) return deny("This section is locked");
2166
+ if (op2.place || op2.rows) return deny(NO_GRID);
2167
+ return allow;
2168
+ case "block.delete":
2169
+ if (isLocked(sectionOfBlock(op2.page, op2.block))) return deny("This section is locked");
2170
+ if (op2.rows) return deny(NO_GRID);
2171
+ return allow;
2172
+ case "block.move": {
2173
+ const from = sectionOfBlock(op2.page, op2.block);
2174
+ if (isLocked(from) || locked.has(op2.to.section)) return deny("This section is locked");
2175
+ if (op2.to.place || op2.to.rows || op2.fromRows) return deny(NO_GRID);
2176
+ if (from !== op2.to.section) {
2177
+ const src = sectionById(op2.page, from);
2178
+ const dest = sectionById(op2.page, op2.to.section);
2179
+ if (!src || !dest) return deny("Can't check this move without the current document");
2180
+ if (src.layout.mode === "grid" || dest.layout.mode === "grid") return deny(NO_GRID);
2181
+ }
2182
+ return allow;
2183
+ }
2184
+ case "block.update": {
2185
+ const section = sectionOfBlock(op2.page, op2.block);
2186
+ if (!isLocked(section)) return allow;
2187
+ if (op2.variant !== void 0 || op2.style !== void 0 || op2.column !== void 0 || op2.label !== void 0 || op2.hidden !== void 0)
2188
+ return deny("This section is locked: only its text and images can change");
2189
+ if (op2.unset?.length) return deny("This section is locked: fields can't be removed");
2190
+ const block = doc ? findBlock(doc, op2.page, op2.block)?.block : void 0;
2191
+ if (!block) return deny("Can't check this edit without the current document");
2192
+ const def = registry?.get(block.type);
2193
+ for (const [key2, value] of Object.entries(op2.set ?? {})) {
2194
+ const kind = def?.meta.fields[key2]?.kind;
2195
+ const ok = def ? kind !== void 0 && CONTENT_KINDS.has(kind) : block.fields[key2] !== void 0 && sameContentShape(block.fields[key2], value);
2196
+ if (!ok) return deny(`This section is locked: "${key2}" is not a text or image field`);
2197
+ }
2198
+ return allow;
2199
+ }
2200
+ case "site.update": {
2201
+ const paths = patchPaths(op2);
2202
+ if (paths.some((p) => /^(customCss|headInjection|footerInjection)(\.|$)/.test(p)))
2203
+ return deny("Editors can't change custom code");
2204
+ if (paths.some((p) => isPathPrefix("footer", p)))
2205
+ return deny("Editors change the footer through its blocks");
2206
+ if (paths.some((p) => isPathPrefix("header", p)))
2207
+ return deny("Editors can't change the header layout; ask a designer");
2208
+ return allow;
2209
+ }
2210
+ case "page.update":
2211
+ if (patchPaths(op2).some((p) => /^(headInjection|footerInjection)(\.|$)/.test(p)))
2212
+ return deny("Editors can't change custom code");
2213
+ return allow;
2214
+ case "page.create":
2215
+ if (op2.page.headInjection !== void 0 || op2.page.footerInjection !== void 0)
2216
+ return deny("Editors can't change custom code");
2217
+ if (op2.page.sections.some((s) => s.locked))
2218
+ return deny("Only owners and designers can lock sections");
2219
+ if (!op2.page.sections.every((s) => isKnownSectionLook(s, context)))
2220
+ return deny(
2221
+ "Editors build pages from the section gallery; layout and style come from the design"
2222
+ );
2223
+ return allow;
2224
+ case "page.delete": {
2225
+ const page = doc?.pages[op2.page];
2226
+ if (!page) return deny("Can't check this page without the current document");
2227
+ if (page.sections.some((s) => s.locked || locked.has(s.id)))
2228
+ return deny("This page has locked sections; only owners and designers can delete it");
2229
+ return allow;
2230
+ }
2231
+ default:
2232
+ return allow;
2233
+ }
2234
+ }
2235
+
2236
+ // src/summarize.ts
2237
+ var STYLE_LABELS = {
2238
+ "choices.palette.brand": "brand color",
2239
+ "choices.palette": "color palette",
2240
+ "choices.pairing": "fonts",
2241
+ "choices.density": "spacing",
2242
+ "choices.shape": "corner style",
2243
+ "choices.buttons": "button style",
2244
+ "choices.motion": "motion",
2245
+ "choices.imageTreatment": "image style",
2246
+ "overrides.type": "typography",
2247
+ "overrides.font": "typography",
2248
+ "overrides.space": "spacing",
2249
+ "overrides.radius": "corner style"
2250
+ };
2251
+ function styleLabel(path) {
2252
+ const color = /^overrides\.color\.([^.]+)/.exec(path);
2253
+ if (color) return `${color[1].replace(/-/g, " ")} color`;
2254
+ let best = "";
2255
+ for (const prefix of Object.keys(STYLE_LABELS))
2256
+ if ((path === prefix || path.startsWith(`${prefix}.`)) && prefix.length > best.length)
2257
+ best = prefix;
2258
+ return best ? STYLE_LABELS[best] : "site styles";
2259
+ }
2260
+ function stats(ops, doc) {
2261
+ const s = {
2262
+ ops: ops.length,
2263
+ pagesEdited: [],
2264
+ pagesCreated: [],
2265
+ pagesDeleted: [],
2266
+ footerEdited: false,
2267
+ sectionsAdded: 0,
2268
+ sectionsRemoved: 0,
2269
+ sectionsMoved: 0,
2270
+ blocksAdded: 0,
2271
+ blocksRemoved: 0,
2272
+ blocksMoved: 0,
2273
+ blocksEdited: 0,
2274
+ imagesAdded: 0,
2275
+ imagesRemoved: 0,
2276
+ filesAdded: 0,
2277
+ filesRemoved: 0,
2278
+ stylesChanged: [],
2279
+ navChanged: false,
2280
+ siteSettingsChanged: false,
2281
+ itemsSaved: 0,
2282
+ itemsDeleted: 0,
2283
+ collectionSettingsChanged: 0
2284
+ };
2285
+ const edited = /* @__PURE__ */ new Set();
2286
+ const created = /* @__PURE__ */ new Set();
2287
+ const deleted = /* @__PURE__ */ new Set();
2288
+ const editedBlocks = /* @__PURE__ */ new Set();
2289
+ const styleLabels = /* @__PURE__ */ new Set();
2290
+ const configs = /* @__PURE__ */ new Set();
2291
+ const touch = (page) => {
2292
+ if (page === FOOTER) s.footerEdited = true;
2293
+ else edited.add(page);
2294
+ };
2295
+ for (const op2 of ops) {
2296
+ switch (op2.op) {
2297
+ case "block.update":
2298
+ case "grid.place":
2299
+ touch(op2.page);
2300
+ editedBlocks.add(op2.block);
2301
+ break;
2302
+ case "block.insert":
2303
+ touch(op2.page);
2304
+ s.blocksAdded++;
2305
+ break;
2306
+ case "block.delete":
2307
+ touch(op2.page);
2308
+ s.blocksRemoved++;
2309
+ break;
2310
+ case "block.move":
2311
+ touch(op2.page);
2312
+ s.blocksMoved++;
2313
+ break;
2314
+ case "section.insert":
2315
+ touch(op2.page);
2316
+ s.sectionsAdded++;
2317
+ break;
2318
+ case "section.delete":
2319
+ touch(op2.page);
2320
+ s.sectionsRemoved++;
2321
+ break;
2322
+ case "section.move":
2323
+ touch(op2.page);
2324
+ s.sectionsMoved++;
2325
+ break;
2326
+ case "section.update":
2327
+ case "page.update":
2328
+ touch(op2.page);
2329
+ break;
2330
+ case "styles.patch":
2331
+ for (const p of patchPaths(op2.patch)) styleLabels.add(styleLabel(p));
2332
+ break;
2333
+ case "page.create":
2334
+ if (deleted.has(op2.page.id)) deleted.delete(op2.page.id);
2335
+ else created.add(op2.page.id);
2336
+ if (op2.nav) s.navChanged = true;
2337
+ break;
2338
+ case "page.delete":
2339
+ if (created.has(op2.page)) created.delete(op2.page);
2340
+ else deleted.add(op2.page);
2341
+ if (op2.nav) s.navChanged = true;
2342
+ break;
2343
+ case "nav.update":
2344
+ s.navChanged = true;
2345
+ break;
2346
+ case "site.update":
2347
+ if (patchPaths(op2).some((p) => p === "footer" || p.startsWith("footer.")))
2348
+ s.footerEdited = true;
2349
+ if (patchPaths(op2).some((p) => p !== "footer" && !p.startsWith("footer.")))
2350
+ s.siteSettingsChanged = true;
2351
+ break;
2352
+ case "media.add":
2353
+ if (!Object.hasOwn(doc.media, op2.path)) {
2354
+ if (op2.meta.kind === "image") s.imagesAdded++;
2355
+ else s.filesAdded++;
2356
+ }
2357
+ break;
2358
+ case "media.remove": {
2359
+ const kind = doc.media[op2.path]?.kind;
2360
+ if (kind === "image") s.imagesRemoved++;
2361
+ else s.filesRemoved++;
2362
+ break;
2363
+ }
2364
+ case "collection.upsert":
2365
+ s.itemsSaved++;
2366
+ break;
2367
+ case "collection.delete":
2368
+ s.itemsDeleted++;
2369
+ break;
2370
+ case "collectionConfig.upsert":
2371
+ configs.add(op2.collection);
2372
+ break;
2373
+ }
2374
+ }
2375
+ s.pagesCreated = [...created];
2376
+ s.pagesDeleted = [...deleted];
2377
+ s.pagesEdited = [...edited].filter((id) => !created.has(id) && !deleted.has(id));
2378
+ s.blocksEdited = editedBlocks.size;
2379
+ s.stylesChanged = [...styleLabels];
2380
+ s.collectionSettingsChanged = configs.size;
2381
+ return s;
2382
+ }
2383
+ function list(items, max = 3, noun = "pages") {
2384
+ if (items.length === 0) return "";
2385
+ if (items.length === 1) return items[0];
2386
+ if (items.length <= max)
2387
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
2388
+ const rest = items.length - (max - 1);
2389
+ return `${items.slice(0, max - 1).join(", ")} and ${rest} other ${noun}`;
2390
+ }
2391
+ var count = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
2392
+ function summarize(ops, doc) {
2393
+ const s = stats(ops, doc);
2394
+ const created = /* @__PURE__ */ new Map();
2395
+ for (const op2 of ops) if (op2.op === "page.create") created.set(op2.page.id, op2.page.title);
2396
+ const title = (id) => doc.pages[id]?.title ?? created.get(id) ?? id;
2397
+ const parts = [];
2398
+ const editedNames = s.pagesEdited.map(title);
2399
+ if (s.footerEdited) editedNames.push("the footer");
2400
+ if (editedNames.length) parts.push(`edited ${list(editedNames)}`);
2401
+ if (s.pagesCreated.length)
2402
+ parts.push(
2403
+ s.pagesCreated.length <= 2 ? `added ${s.pagesCreated.length === 1 ? "page" : "pages"} ${list(s.pagesCreated.map(title))}` : `added ${count(s.pagesCreated.length, "page")}`
2404
+ );
2405
+ if (s.pagesDeleted.length)
2406
+ parts.push(
2407
+ s.pagesDeleted.length <= 2 ? `deleted ${s.pagesDeleted.length === 1 ? "page" : "pages"} ${list(s.pagesDeleted.map(title))}` : `deleted ${count(s.pagesDeleted.length, "page")}`
2408
+ );
2409
+ if (s.imagesAdded) parts.push(`added ${count(s.imagesAdded, "image")}`);
2410
+ if (s.filesAdded) parts.push(`added ${count(s.filesAdded, "file")}`);
2411
+ if (s.imagesRemoved) parts.push(`removed ${count(s.imagesRemoved, "image")}`);
2412
+ if (s.filesRemoved) parts.push(`removed ${count(s.filesRemoved, "file")}`);
2413
+ if (s.stylesChanged.length) parts.push(`changed ${list(s.stylesChanged, 3, "styles")}`);
2414
+ if (s.navChanged) parts.push("updated navigation");
2415
+ if (s.siteSettingsChanged) parts.push("updated site settings");
2416
+ if (s.itemsSaved) parts.push(`saved ${count(s.itemsSaved, "collection item")}`);
2417
+ if (s.itemsDeleted) parts.push(`deleted ${count(s.itemsDeleted, "collection item")}`);
2418
+ if (s.collectionSettingsChanged)
2419
+ parts.push(`updated settings for ${count(s.collectionSettingsChanged, "collection")}`);
2420
+ if (parts.length === 0) return "No changes";
2421
+ const text = parts.join("; ");
2422
+ return text.charAt(0).toUpperCase() + text.slice(1);
2423
+ }
2424
+
2425
+ // src/index.ts
2426
+ var name = "@omniharmonic/openspace-ops";
2427
+ export {
2428
+ BlockDeleteOpSchema,
2429
+ BlockInsertOpSchema,
2430
+ BlockMoveOpSchema,
2431
+ BlockUpdateOpSchema,
2432
+ BreakpointSchema,
2433
+ CollectionConfigUpsertOpSchema,
2434
+ CollectionDeleteOpSchema,
2435
+ CollectionUpsertOpSchema,
2436
+ ContainerIdSchema,
2437
+ FOOTER,
2438
+ GridPlaceOpSchema,
2439
+ MediaAddOpSchema,
2440
+ MediaRemoveOpSchema,
2441
+ NavUpdateOpSchema,
2442
+ OP_TYPES,
2443
+ OpError,
2444
+ OpMetaShape,
2445
+ OpSchema,
2446
+ PROTECTED_PATHS,
2447
+ PageCreateOpSchema,
2448
+ PageDeleteOpSchema,
2449
+ PageUpdateOpSchema,
2450
+ PatchShape,
2451
+ PathSchema,
2452
+ PositionSchema,
2453
+ ROLES,
2454
+ RowsSchema,
2455
+ SectionDeleteOpSchema,
2456
+ SectionInsertOpSchema,
2457
+ SectionMoveOpSchema,
2458
+ SectionUpdateOpSchema,
2459
+ SiteUpdateOpSchema,
2460
+ StylesPatchOpSchema,
2461
+ apply,
2462
+ applyAll,
2463
+ autoPlace,
2464
+ containerPath,
2465
+ deepEqual,
2466
+ diff,
2467
+ effectiveRect,
2468
+ findBlock,
2469
+ findSection,
2470
+ footprint,
2471
+ independent,
2472
+ indexIds,
2473
+ invert,
2474
+ invertAll,
2475
+ isInverseOf,
2476
+ isJson,
2477
+ isKnownSectionLook,
2478
+ isTextOrImageValue,
2479
+ locateBlock,
2480
+ locateSection,
2481
+ lockedSectionsOf,
2482
+ mutate,
2483
+ name,
2484
+ parseOp,
2485
+ pruneNav,
2486
+ raiseRows,
2487
+ rebase,
2488
+ resolvePosition,
2489
+ sameContentShape,
2490
+ sectionsOf,
2491
+ siteRefIssues,
2492
+ stamp,
2493
+ stats,
2494
+ styleLabel,
2495
+ summarize,
2496
+ transformForRole,
2497
+ usedRows,
2498
+ validate,
2499
+ validateAll,
2500
+ validateDocument,
2501
+ wins
2502
+ };
2503
+ //# sourceMappingURL=index.js.map