@seamlet/astro 0.11.3 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,6 +80,73 @@ function defaultContent(fields) {
80
80
  );
81
81
  }
82
82
 
83
+ // src/schema/field-paths.ts
84
+ var RECORD_INDEX_PATTERN = /^\d+$/u;
85
+ var FIELD_PATH_SEPARATOR = ".";
86
+ function isRecordIndex(segment) {
87
+ return RECORD_INDEX_PATTERN.test(segment);
88
+ }
89
+ function resolveFieldPath(schema, text2) {
90
+ return resolveInFields(schema, text2.split(FIELD_PATH_SEPARATOR), []);
91
+ }
92
+ function resolveInFields(fields, segments, parentPath) {
93
+ const [name, ...rest] = segments;
94
+ const field = name !== void 0 && Object.hasOwn(fields, name) ? fields[name] : void 0;
95
+ if (name === void 0 || field === void 0) {
96
+ return void 0;
97
+ }
98
+ const path = [...parentPath, name];
99
+ if (rest.length === 0) {
100
+ return { kind: "field", path, field };
101
+ }
102
+ return resolveInsideField(field, rest, path);
103
+ }
104
+ function resolveInsideField(field, segments, path) {
105
+ switch (field.kind) {
106
+ case "group":
107
+ return resolveInFields(field.fields, segments, path);
108
+ case "collection":
109
+ return resolveInCollection(field, segments, path);
110
+ case "text":
111
+ case "richText":
112
+ case "number":
113
+ case "date":
114
+ case "image":
115
+ case "link":
116
+ case "toggle":
117
+ case "choice":
118
+ case "choices":
119
+ case "url":
120
+ case "time":
121
+ case "reference":
122
+ case "references":
123
+ return void 0;
124
+ }
125
+ }
126
+ function resolveInCollection(collection2, segments, collectionPath) {
127
+ const [indexText, ...rest] = segments;
128
+ if (indexText === void 0 || !isRecordIndex(indexText)) {
129
+ return void 0;
130
+ }
131
+ const index = Number(indexText);
132
+ const recordPath = [...collectionPath, index];
133
+ if (rest.length === 0) {
134
+ return { kind: "record", path: recordPath, collection: collection2, index };
135
+ }
136
+ return resolveInFields(collection2.item, rest, recordPath);
137
+ }
138
+
139
+ // src/schema/json-values.ts
140
+ function isJsonObject(value) {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value);
142
+ }
143
+ function isContentObject(value) {
144
+ return isJsonObject(value);
145
+ }
146
+ function isText(value) {
147
+ return typeof value === "string" && value.trim() !== "";
148
+ }
149
+
83
150
  // src/schema/link-addresses.ts
84
151
  var PHONE_PREFIX = "tel:";
85
152
  var EMAIL_PREFIX = "mailto:";
@@ -87,7 +154,7 @@ var WEB_PREFIX = "https://";
87
154
  var SITE_ORIGIN = "https://site.invalid";
88
155
  var FIRST_PRINTABLE_CODE = 32;
89
156
  var DELETE_CODE = 127;
90
- var BACKSLASH_CODE = 92;
157
+ var BACKSLASH = "\\";
91
158
  var WEB_ADDRESS = /^https?:\/\/[^\s/?#]+[^\s]*$/iu;
92
159
  var ANY_SCHEME = /^[a-z][a-z\d+.-]*:(?!\d)/iu;
93
160
  var PHONE_NUMBER = /^\+?[\d\s().-]*\d[\d\s().-]*$/u;
@@ -96,7 +163,7 @@ var EMAIL_ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
96
163
  function linkTargetOf(href) {
97
164
  const address = href;
98
165
  const hasSpacesAround = address !== address.trim();
99
- if (hasSpacesAround) {
166
+ if (hasSpacesAround || hasControlCharacter(address)) {
100
167
  return void 0;
101
168
  }
102
169
  if (isSitePath(address)) {
@@ -115,6 +182,9 @@ function linkTargetOf(href) {
115
182
  }
116
183
  return void 0;
117
184
  }
185
+ function safeHref(href) {
186
+ return linkTargetOf(href) === void 0 ? void 0 : href;
187
+ }
118
188
  function hrefOf(target) {
119
189
  switch (target.kind) {
120
190
  case "page":
@@ -132,18 +202,18 @@ function webAddressOf(typed) {
132
202
  return hasScheme || typed === "" ? typed : `${WEB_PREFIX}${typed}`;
133
203
  }
134
204
  function isSitePath(address) {
135
- if (!address.startsWith("/") || hasAmbiguousCharacter(address)) {
205
+ if (!address.startsWith("/") || address.includes(BACKSLASH)) {
136
206
  return false;
137
207
  }
138
208
  return new URL(address, SITE_ORIGIN).origin === SITE_ORIGIN;
139
209
  }
140
- function hasAmbiguousCharacter(address) {
210
+ function hasControlCharacter(address) {
141
211
  const codes = Array.from(
142
212
  { length: address.length },
143
213
  (_, index) => address.charCodeAt(index)
144
214
  );
145
215
  return codes.some(
146
- (code) => code === BACKSLASH_CODE || code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
216
+ (code) => code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
147
217
  );
148
218
  }
149
219
  function withoutPrefix(address, prefix) {
@@ -195,7 +265,7 @@ function normalizedLine(spans) {
195
265
  }, []);
196
266
  }
197
267
  function readBlock(input) {
198
- if (!isObject(input)) return void 0;
268
+ if (!isJsonObject(input)) return void 0;
199
269
  if (input["kind"] === "paragraph") {
200
270
  const spans = readLine(input["spans"]);
201
271
  return spans === void 0 ? void 0 : { kind: "paragraph", spans };
@@ -215,7 +285,7 @@ function readLine(input) {
215
285
  return isEverySpanRead ? normalizedLine(spans) : void 0;
216
286
  }
217
287
  function readSpan(input) {
218
- if (!isObject(input)) return void 0;
288
+ if (!isJsonObject(input)) return void 0;
219
289
  const { text: text2, bold, italic, href } = input;
220
290
  const hasValidMarks = typeof text2 === "string" && (bold === void 0 || bold === true) && (italic === void 0 || italic === true) && (href === void 0 || typeof href === "string");
221
291
  if (!hasValidMarks) return void 0;
@@ -243,9 +313,6 @@ function plainBlockOf(block) {
243
313
  function plainLineOf(line) {
244
314
  return line.map((span) => span.text).join("");
245
315
  }
246
- function isObject(value) {
247
- return typeof value === "object" && value !== null && !Array.isArray(value);
248
- }
249
316
 
250
317
  // src/schema/record-ids.ts
251
318
  var RECORD_ID_KEY = "id";
@@ -259,23 +326,63 @@ function newRecord(collection2, id) {
259
326
  function withRecordIds(schema, content, newId) {
260
327
  return withIdsInFields(schema, content, newId);
261
328
  }
262
- var COLLECTION_PATH_SEPARATOR = ".";
263
329
  function collectionAt(schema, content, path) {
264
- return collectionIn(schema, content, path.split(COLLECTION_PATH_SEPARATOR));
330
+ return collectionIn(schema, content, path.split(FIELD_PATH_SEPARATOR));
265
331
  }
266
332
  function collectionIn(fields, content, names) {
267
333
  const [name, ...rest] = names;
268
334
  const field = name !== void 0 && Object.hasOwn(fields, name) ? fields[name] : void 0;
269
335
  if (name === void 0 || field === void 0) return void 0;
270
336
  const value = content[name];
271
- if (rest.length === 0 && field.kind === "collection") {
272
- const records = Array.isArray(value) ? value.filter(isContent) : [];
273
- return { collection: field, records };
337
+ const isLast = rest.length === 0;
338
+ switch (field.kind) {
339
+ case "collection": {
340
+ const records = Array.isArray(value) ? value.filter(isContentObject) : [];
341
+ return isLast ? { collection: field, records } : void 0;
342
+ }
343
+ case "group":
344
+ return !isLast && isContentObject(value) ? collectionIn(field.fields, value, rest) : void 0;
345
+ case "text":
346
+ case "richText":
347
+ case "number":
348
+ case "date":
349
+ case "image":
350
+ case "link":
351
+ case "toggle":
352
+ case "choice":
353
+ case "choices":
354
+ case "url":
355
+ case "time":
356
+ case "reference":
357
+ case "references":
358
+ return void 0;
274
359
  }
275
- if (rest.length > 0 && field.kind === "group" && isContent(value)) {
276
- return collectionIn(field.fields, value, rest);
360
+ }
361
+ function collectionFieldAt(schema, path) {
362
+ const [name, ...rest] = path.split(FIELD_PATH_SEPARATOR);
363
+ const field = name !== void 0 && Object.hasOwn(schema, name) ? schema[name] : void 0;
364
+ if (field === void 0) return void 0;
365
+ const isLast = rest.length === 0;
366
+ switch (field.kind) {
367
+ case "collection":
368
+ return isLast ? field : void 0;
369
+ case "group":
370
+ return isLast ? void 0 : collectionFieldAt(field.fields, rest.join(FIELD_PATH_SEPARATOR));
371
+ case "text":
372
+ case "richText":
373
+ case "number":
374
+ case "date":
375
+ case "image":
376
+ case "link":
377
+ case "toggle":
378
+ case "choice":
379
+ case "choices":
380
+ case "url":
381
+ case "time":
382
+ case "reference":
383
+ case "references":
384
+ return void 0;
277
385
  }
278
- return void 0;
279
386
  }
280
387
  function findRecord(records, id) {
281
388
  if (id === null) return void 0;
@@ -289,13 +396,26 @@ function withIdsInFields(fields, content, newId) {
289
396
  return Object.fromEntries(entries);
290
397
  }
291
398
  function withIdsIn(field, value, newId) {
292
- if (field.kind === "group" && isContent(value)) {
293
- return withIdsInFields(field.fields, value, newId);
294
- }
295
- if (field.kind === "collection" && Array.isArray(value)) {
296
- return withIdsInRecords(field, value.filter(isContent), newId);
399
+ switch (field.kind) {
400
+ case "group":
401
+ return isContentObject(value) ? withIdsInFields(field.fields, value, newId) : value;
402
+ case "collection":
403
+ return Array.isArray(value) ? withIdsInRecords(field, value.filter(isContentObject), newId) : value;
404
+ case "text":
405
+ case "richText":
406
+ case "number":
407
+ case "date":
408
+ case "image":
409
+ case "link":
410
+ case "toggle":
411
+ case "choice":
412
+ case "choices":
413
+ case "url":
414
+ case "time":
415
+ case "reference":
416
+ case "references":
417
+ return value;
297
418
  }
298
- return value;
299
419
  }
300
420
  function withIdsInRecords(collection2, records, newId) {
301
421
  const taken = /* @__PURE__ */ new Set();
@@ -308,9 +428,6 @@ function withIdsInRecords(collection2, records, newId) {
308
428
  return { ...fields, [RECORD_ID_KEY]: id };
309
429
  });
310
430
  }
311
- function isContent(value) {
312
- return typeof value === "object" && value !== null && !Array.isArray(value);
313
- }
314
431
 
315
432
  // src/schema/reference-checks.ts
316
433
  function missingRecordIssues(schema, content) {
@@ -334,7 +451,7 @@ function issuesInField(field, value, path, idsOf) {
334
451
  case "references":
335
452
  return missingIds(stringsOf(value), field, path, idsOf);
336
453
  case "group":
337
- return isContent2(value) ? issuesInFields(field.fields, value, path, idsOf) : [];
454
+ return isContentObject(value) ? issuesInFields(field.fields, value, path, idsOf) : [];
338
455
  case "collection":
339
456
  return recordsOf(value).flatMap(
340
457
  (record, index) => issuesInFields(field.item, record, [...path, index], idsOf)
@@ -364,22 +481,19 @@ function recordIdsAt(schema, content, path) {
364
481
  return new Set(ids.filter((id) => typeof id === "string"));
365
482
  }
366
483
  function recordsOf(value) {
367
- return Array.isArray(value) ? value.filter(isContent2) : [];
484
+ return Array.isArray(value) ? value.filter(isContentObject) : [];
368
485
  }
369
486
  function stringsOf(value) {
370
487
  const list = Array.isArray(value) ? value : [];
371
488
  return list.filter((id) => typeof id === "string");
372
489
  }
373
- function isContent2(value) {
374
- return typeof value === "object" && value !== null && !Array.isArray(value);
375
- }
376
490
 
377
491
  // src/schema/parse-content.ts
378
492
  var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
379
493
  var TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d$/u;
380
494
  var GRAPHEMES = new Intl.Segmenter("ru-RU", { granularity: "grapheme" });
381
495
  function formatFieldPath(path) {
382
- return path.join(".");
496
+ return path.join(FIELD_PATH_SEPARATOR);
383
497
  }
384
498
  function countCharacters(text2) {
385
499
  return [...GRAPHEMES.segment(text2)].length;
@@ -393,7 +507,7 @@ function parseContent(schema, input) {
393
507
  };
394
508
  }
395
509
  function parseFields(fields, input, path) {
396
- if (!isRecord(input)) {
510
+ if (!isJsonObject(input)) {
397
511
  return invalidType(defaultContent(fields), path);
398
512
  }
399
513
  const parsedFields = Object.entries(fields).map(([name, field]) => {
@@ -444,24 +558,30 @@ function parseField(field, input, path) {
444
558
  }
445
559
  }
446
560
  function parseText(field, input, path) {
447
- if (typeof input !== "string") {
561
+ const typed = typedTextOf(input);
562
+ if (typed === void 0) {
448
563
  return invalidType(defaultValue(field), path);
449
564
  }
450
- const isTooLong = field.maxLength !== void 0 && countCharacters(input) > field.maxLength;
565
+ const isTooLong = field.maxLength !== void 0 && countCharacters(typed) > field.maxLength;
451
566
  if (isTooLong) {
452
567
  return {
453
- value: input,
568
+ value: typed,
454
569
  issues: [{ path, code: "too-long", maxLength: field.maxLength }]
455
570
  };
456
571
  }
457
- return valid(input);
572
+ return valid(typed);
573
+ }
574
+ function typedTextOf(input) {
575
+ if (typeof input === "string") return input;
576
+ const formatted = readRichText(input);
577
+ return formatted === void 0 ? void 0 : plainTextOf(formatted);
458
578
  }
459
579
  function parseRichText(field, input, path) {
460
- const read = typeof input === "string" ? richTextOf(input) : readRichText(input);
461
- if (read === void 0) {
580
+ const read2 = typeof input === "string" ? richTextOf(input) : readRichText(input);
581
+ if (read2 === void 0) {
462
582
  return invalidType(defaultValue(field), path);
463
583
  }
464
- const text2 = field.inline === true ? asOneParagraph(read) : read;
584
+ const text2 = field.inline === true ? asOneParagraph(read2) : read2;
465
585
  const hasUnsupportedLink = linkAddressesOf(text2).some(
466
586
  (href) => linkTargetOf(href) === void 0
467
587
  );
@@ -514,12 +634,12 @@ function parseImage(field, input, path) {
514
634
  if (input === null) {
515
635
  return valid(null);
516
636
  }
517
- const src = isRecord(input) ? input["src"] : void 0;
518
- const alt = isRecord(input) ? input["alt"] : void 0;
637
+ const src = isJsonObject(input) ? input["src"] : void 0;
638
+ const alt = isJsonObject(input) ? input["alt"] : void 0;
519
639
  if (typeof src !== "string" || typeof alt !== "string") {
520
640
  return invalidType(defaultValue(field), path);
521
641
  }
522
- const focus = isRecord(input) ? focusOf(input["focus"]) : void 0;
642
+ const focus = isJsonObject(input) ? focusOf(input["focus"]) : void 0;
523
643
  const image2 = focus === void 0 ? { src, alt } : { src, alt, focus };
524
644
  if (src.trim() === "") {
525
645
  return { value: image2, issues: [{ path, code: "missing-image-src" }] };
@@ -530,7 +650,7 @@ function parseLink(field, input, path) {
530
650
  if (input === null) {
531
651
  return valid(null);
532
652
  }
533
- if (!isRecord(input)) {
653
+ if (!isJsonObject(input)) {
534
654
  return invalidType(defaultValue(field), path);
535
655
  }
536
656
  const { text: text2, href } = input;
@@ -619,7 +739,7 @@ function parseCollection(field, input, path) {
619
739
  }
620
740
  function parseRecord(field, input, path) {
621
741
  const parsed = parseFields(field.item, input, path);
622
- const id = isRecord(input) ? input[RECORD_ID_KEY] : void 0;
742
+ const id = isJsonObject(input) ? input[RECORD_ID_KEY] : void 0;
623
743
  if (!isRecordId(id)) return parsed;
624
744
  return {
625
745
  value: { ...parsed.value, [RECORD_ID_KEY]: id },
@@ -644,7 +764,7 @@ function parseReferences(field, input, path) {
644
764
  return valid(ids);
645
765
  }
646
766
  function focusOf(input) {
647
- if (!isRecord(input)) return void 0;
767
+ if (!isJsonObject(input)) return void 0;
648
768
  const { x, y } = input;
649
769
  const isInside = isFraction(x) && isFraction(y);
650
770
  return isInside ? { x, y } : void 0;
@@ -659,9 +779,6 @@ function isCalendarDate(value) {
659
779
  const time2 = Date.parse(`${value}T00:00:00Z`);
660
780
  return !Number.isNaN(time2) && new Date(time2).toISOString().startsWith(value);
661
781
  }
662
- function isRecord(value) {
663
- return typeof value === "object" && value !== null && !Array.isArray(value);
664
- }
665
782
  function valid(value) {
666
783
  return { value, issues: [] };
667
784
  }
@@ -675,45 +792,6 @@ function editable() {
675
792
  return (...path) => ({ [FIELD_PATH_ATTRIBUTE]: formatFieldPath(path) });
676
793
  }
677
794
 
678
- // src/schema/field-paths.ts
679
- var RECORD_INDEX_PATTERN = /^\d+$/u;
680
- function resolveFieldPath(schema, text2) {
681
- return resolveInFields(schema, text2.split("."), []);
682
- }
683
- function resolveInFields(fields, segments, parentPath) {
684
- const [name, ...rest] = segments;
685
- const field = name !== void 0 && Object.hasOwn(fields, name) ? fields[name] : void 0;
686
- if (name === void 0 || field === void 0) {
687
- return void 0;
688
- }
689
- const path = [...parentPath, name];
690
- if (rest.length === 0) {
691
- return { kind: "field", path, field };
692
- }
693
- return resolveInsideField(field, rest, path);
694
- }
695
- function resolveInsideField(field, segments, path) {
696
- if (field.kind === "group") {
697
- return resolveInFields(field.fields, segments, path);
698
- }
699
- if (field.kind === "collection") {
700
- return resolveInCollection(field, segments, path);
701
- }
702
- return void 0;
703
- }
704
- function resolveInCollection(collection2, segments, collectionPath) {
705
- const [indexText, ...rest] = segments;
706
- if (indexText === void 0 || !RECORD_INDEX_PATTERN.test(indexText)) {
707
- return void 0;
708
- }
709
- const index = Number(indexText);
710
- const recordPath = [...collectionPath, index];
711
- if (rest.length === 0) {
712
- return { kind: "record", path: recordPath, collection: collection2, index };
713
- }
714
- return resolveInFields(collection2.item, rest, recordPath);
715
- }
716
-
717
795
  // src/schema/reveal-markers.ts
718
796
  var REVEAL_ATTRIBUTE = "data-cms-reveal";
719
797
  var REVEALED_ATTRIBUTE = "data-cms-revealed";
@@ -787,125 +865,185 @@ function openingHoursField(options = {}) {
787
865
  }
788
866
 
789
867
  // src/schema/read-fields.ts
868
+ var TITLE_KINDS = /* @__PURE__ */ new Set([
869
+ "text",
870
+ "link"
871
+ ]);
790
872
  function readFields(input) {
791
- if (!isRecord2(input)) return void 0;
792
- const entries = Object.entries(input).map(
793
- ([name, value]) => [name, readField(value)]
794
- );
795
- const readEntries = entries.flatMap(
796
- ([name, field]) => field === void 0 ? [] : [[name, field]]
797
- );
798
- const isEveryFieldRead = readEntries.length === entries.length;
799
- return isEveryFieldRead ? Object.fromEntries(readEntries) : void 0;
873
+ const reading = checkFields(input);
874
+ return reading.status === "read" ? reading.fields : void 0;
875
+ }
876
+ function checkFields(input) {
877
+ const reading = readFieldsAt(input, []);
878
+ if (!reading.isRead) return { status: "invalid", problem: reading.problem };
879
+ const problem = firstUnknownCollection(reading.value, reading.value, []);
880
+ return problem === void 0 ? { status: "read", fields: reading.value } : { status: "invalid", problem };
881
+ }
882
+ function readFieldsAt(input, path) {
883
+ if (!isJsonObject(input)) return refused(path, "not-fields");
884
+ const fields = {};
885
+ for (const [name, value] of Object.entries(input)) {
886
+ const fieldPath = [...path, name];
887
+ if (!isFieldName(name)) return refused(fieldPath, "invalid-name");
888
+ const field = readField(value, fieldPath);
889
+ if (!field.isRead) return field;
890
+ fields[name] = field.value;
891
+ }
892
+ return read(fields);
800
893
  }
801
- function readField(input) {
802
- if (!isRecord2(input)) return void 0;
803
- const label = input["label"];
804
- if (typeof label !== "string") return void 0;
805
- const kind = input["kind"];
806
- return isFieldKind(kind) ? readFieldOfKind(kind, label, input) : void 0;
894
+ function readField(input, path) {
895
+ if (!isJsonObject(input)) return refused(path, "not-fields");
896
+ const { kind, label } = input;
897
+ if (typeof label !== "string") return refused(path, "invalid-label");
898
+ if (!isFieldKind(kind)) return refused(path, "unknown-kind");
899
+ return readFieldOfKind({ kind, label, input, path });
807
900
  }
808
- function readFieldOfKind(kind, label, input) {
901
+ function readFieldOfKind(field) {
902
+ const { kind, label, input, path } = field;
809
903
  switch (kind) {
810
904
  case "text":
811
- return {
812
- kind,
813
- label,
814
- ...optional("multiline", input["multiline"], isBoolean),
815
- ...optional("maxLength", input["maxLength"], isCount)
816
- };
905
+ return withOptions({ kind, label }, path, [
906
+ optional("multiline", input, isBoolean),
907
+ optional("maxLength", input, isCount)
908
+ ]);
817
909
  case "richText":
818
- return {
819
- kind,
820
- label,
821
- ...optional("inline", input["inline"], isBoolean),
822
- ...optional("maxLength", input["maxLength"], isCount)
823
- };
910
+ return withOptions({ kind, label }, path, [
911
+ optional("inline", input, isBoolean),
912
+ optional("maxLength", input, isCount)
913
+ ]);
824
914
  case "number":
825
- return {
826
- kind,
827
- label,
828
- ...optional("min", input["min"], isNumber),
829
- ...optional("max", input["max"], isNumber)
830
- };
915
+ return withOptions({ kind, label }, path, [
916
+ optional("min", input, isNumber),
917
+ optional("max", input, isNumber)
918
+ ]);
831
919
  case "date":
832
920
  case "image":
833
921
  case "link":
834
922
  case "toggle":
835
923
  case "url":
836
924
  case "time":
837
- return { kind, label };
925
+ return read({ kind, label });
838
926
  case "choice":
839
927
  case "choices":
840
- return readChoiceField(kind, label, input["options"]);
928
+ return readChoiceField({ kind, label, input, path });
841
929
  case "reference":
842
930
  case "references":
843
- return readReferenceField(kind, label, input["collection"]);
931
+ return readReferenceField({ kind, label, input, path });
844
932
  case "group":
845
- return readGroupField(label, input["fields"]);
933
+ return readGroupField({ kind, label, input, path });
846
934
  case "collection":
847
- return readCollectionField(label, input);
935
+ return readCollectionField({ kind, label, input, path });
848
936
  }
849
937
  }
850
- function readChoiceField(kind, label, input) {
851
- if (!Array.isArray(input)) return void 0;
852
- const options = input;
853
- if (!options.every(isChoiceOption)) return void 0;
854
- const read = options.map(({ value, label: optionLabel }) => ({
938
+ function readChoiceField(field) {
939
+ const { kind, label, input, path } = field;
940
+ const options = input["options"];
941
+ const isOptionList = Array.isArray(options) && options.every(isChoiceOption);
942
+ if (!isOptionList) return refused(path, "invalid-choice-options");
943
+ const knownOptions = options.map(({ value, label: optionLabel }) => ({
855
944
  value,
856
945
  label: optionLabel
857
946
  }));
858
- return { kind, label, options: read };
947
+ return read({ kind, label, options: knownOptions });
948
+ }
949
+ function readReferenceField(field) {
950
+ const { kind, label, input, path } = field;
951
+ const collection2 = input["collection"];
952
+ if (typeof collection2 !== "string") {
953
+ return refused(path, "unknown-collection");
954
+ }
955
+ return read({ kind, label, collection: collection2 });
956
+ }
957
+ function readGroupField(field) {
958
+ const { kind, label, input, path } = field;
959
+ const fields = readFieldsAt(input["fields"], path);
960
+ return fields.isRead ? read({ kind, label, fields: fields.value }) : fields;
961
+ }
962
+ function readCollectionField(field) {
963
+ const { kind, label, input, path } = field;
964
+ const item = readFieldsAt(input["item"], path);
965
+ if (!item.isRead) return item;
966
+ if (RECORD_ID_KEY in item.value) {
967
+ return refused([...path, RECORD_ID_KEY], "record-id-field");
968
+ }
969
+ const itemTitle = input["itemTitle"];
970
+ if (itemTitle === void 0) return read({ kind, label, item: item.value });
971
+ const titleField = typeof itemTitle === "string" && Object.hasOwn(item.value, itemTitle) ? item.value[itemTitle] : void 0;
972
+ const canTitleRecord = titleField !== void 0 && TITLE_KINDS.has(titleField.kind);
973
+ if (typeof itemTitle !== "string" || !canTitleRecord) {
974
+ return refused(path, "invalid-item-title");
975
+ }
976
+ return read({ kind, label, item: item.value, itemTitle });
859
977
  }
860
- function readReferenceField(kind, label, collection2) {
861
- return typeof collection2 === "string" ? { kind, label, collection: collection2 } : void 0;
978
+ function firstUnknownCollection(schema, fields, path) {
979
+ for (const [name, field] of Object.entries(fields)) {
980
+ const problem = unknownCollectionIn(schema, field, [...path, name]);
981
+ if (problem !== void 0) return problem;
982
+ }
983
+ return void 0;
862
984
  }
863
- function readGroupField(label, input) {
864
- const fields = readFields(input);
865
- return fields === void 0 ? void 0 : { kind: "group", label, fields };
985
+ function unknownCollectionIn(schema, field, path) {
986
+ switch (field.kind) {
987
+ case "reference":
988
+ case "references":
989
+ return referencesCollection(schema, field) ? void 0 : { path, reason: "unknown-collection" };
990
+ case "group":
991
+ return firstUnknownCollection(schema, field.fields, path);
992
+ case "collection":
993
+ return firstUnknownCollection(schema, field.item, path);
994
+ case "text":
995
+ case "richText":
996
+ case "number":
997
+ case "date":
998
+ case "image":
999
+ case "link":
1000
+ case "toggle":
1001
+ case "choice":
1002
+ case "choices":
1003
+ case "url":
1004
+ case "time":
1005
+ return void 0;
1006
+ }
866
1007
  }
867
- function readCollectionField(label, input) {
868
- const item = readFields(input["item"]);
869
- if (item === void 0 || "id" in item) return void 0;
870
- const itemTitle = input["itemTitle"];
871
- const isTitleKnown = typeof itemTitle === "string" && itemTitle in item;
872
- if (itemTitle !== void 0 && !isTitleKnown) return void 0;
873
- return {
874
- kind: "collection",
875
- label,
876
- item,
877
- ...isTitleKnown ? { itemTitle } : {}
878
- };
1008
+ function referencesCollection(schema, field) {
1009
+ return collectionFieldAt(schema, field.collection) !== void 0;
1010
+ }
1011
+ function withOptions(field, path, options) {
1012
+ const isEveryOptionRead = options.every((option) => option !== void 0);
1013
+ if (!isEveryOptionRead) return refused(path, "invalid-option");
1014
+ return read(Object.assign({}, field, ...options));
879
1015
  }
880
- function optional(key, value, isOfKind) {
881
- if (!isOfKind(value)) return {};
1016
+ function optional(key, input, isOfKind) {
1017
+ const value = input[key];
1018
+ if (value === void 0) return {};
1019
+ if (!isOfKind(value)) return void 0;
882
1020
  return { [key]: value };
883
1021
  }
884
- var FIELD_KINDS = /* @__PURE__ */ new Set([
885
- "text",
886
- "richText",
887
- "number",
888
- "date",
889
- "image",
890
- "link",
891
- "toggle",
892
- "choice",
893
- "choices",
894
- "url",
895
- "time",
896
- "reference",
897
- "references",
898
- "group",
899
- "collection"
900
- ]);
1022
+ var FIELD_KINDS = {
1023
+ text: true,
1024
+ richText: true,
1025
+ number: true,
1026
+ date: true,
1027
+ image: true,
1028
+ link: true,
1029
+ toggle: true,
1030
+ choice: true,
1031
+ choices: true,
1032
+ url: true,
1033
+ time: true,
1034
+ reference: true,
1035
+ references: true,
1036
+ group: true,
1037
+ collection: true
1038
+ };
901
1039
  function isFieldKind(value) {
902
- return typeof value === "string" && FIELD_KINDS.has(value);
1040
+ return typeof value === "string" && Object.hasOwn(FIELD_KINDS, value);
903
1041
  }
904
- function isChoiceOption(value) {
905
- return isRecord2(value) && typeof value["value"] === "string" && typeof value["label"] === "string";
1042
+ function isFieldName(name) {
1043
+ return name !== "" && !name.includes(FIELD_PATH_SEPARATOR) && !isRecordIndex(name);
906
1044
  }
907
- function isRecord2(value) {
908
- return typeof value === "object" && value !== null && !Array.isArray(value);
1045
+ function isChoiceOption(value) {
1046
+ return isJsonObject(value) && typeof value["value"] === "string" && typeof value["label"] === "string";
909
1047
  }
910
1048
  function isBoolean(value) {
911
1049
  return typeof value === "boolean";
@@ -916,6 +1054,12 @@ function isNumber(value) {
916
1054
  function isCount(value) {
917
1055
  return isNumber(value) && Number.isInteger(value) && value >= 0;
918
1056
  }
1057
+ function read(value) {
1058
+ return { isRead: true, value };
1059
+ }
1060
+ function refused(path, reason) {
1061
+ return { isRead: false, problem: { path, reason } };
1062
+ }
919
1063
 
920
1064
  // src/schema/site-config.ts
921
1065
  var SITE_CONFIG_FILE = "seamlet.json";
@@ -927,7 +1071,7 @@ function isSiteId(value) {
927
1071
  return typeof value === "string" && value.length <= LONGEST_SITE_ID && SITE_ID.test(value);
928
1072
  }
929
1073
  function readSiteConfig(input) {
930
- const fields = isRecord3(input) ? input : {};
1074
+ const fields = isJsonObject(input) ? input : {};
931
1075
  const { id, title } = fields;
932
1076
  const cms = originOf(fields["cms"]);
933
1077
  const hasId = isSiteId(id);
@@ -949,27 +1093,43 @@ function originOf(value) {
949
1093
  const url2 = new URL(value);
950
1094
  return WEB_PROTOCOLS.has(url2.protocol) ? url2.origin : void 0;
951
1095
  }
952
- function isText(value) {
953
- return typeof value === "string" && value.trim() !== "";
954
- }
955
- function isRecord3(value) {
956
- return typeof value === "object" && value !== null && !Array.isArray(value);
957
- }
958
1096
 
959
1097
  // src/schema/site-description.ts
960
1098
  var SITE_DESCRIPTION_PATH = "/seamlet/site.json";
961
1099
  var PAGE_PATH = /^(\/[a-z0-9][a-z0-9-]*)+$/u;
962
1100
  var HOME_PAGE_PATH = "/";
963
1101
  function readSiteDescription(input) {
964
- if (!isRecord4(input)) return void 0;
1102
+ const reading = checkSiteDescription(input);
1103
+ return reading.status === "read" ? reading.description : void 0;
1104
+ }
1105
+ function checkSiteDescription(input) {
1106
+ if (!isJsonObject(input)) return invalid([{ code: "not-a-description" }]);
965
1107
  const { id, title } = input;
966
- const schema = readFields(input["schema"]);
967
- const isNamed = isSiteId(id) && isText2(title);
968
- if (!isNamed || schema === void 0) return void 0;
969
- const initial = parseContent(schema, input["initialContent"]);
970
- const pages = readPages(schema, input["pages"]);
971
- if (initial.issues.length > 0 || pages === void 0) return void 0;
972
- return { id, title, schema, initialContent: initial.content, pages };
1108
+ const nameProblems = namingProblems(id, title);
1109
+ const schema = checkFields(input["schema"]);
1110
+ if (schema.status === "invalid") {
1111
+ const { problem } = schema;
1112
+ return invalid([...nameProblems, { code: "invalid-schema", problem }]);
1113
+ }
1114
+ const initial = parseContent(schema.fields, input["initialContent"]);
1115
+ const pages = checkPages(schema.fields, input["pages"]);
1116
+ const problems = [
1117
+ ...nameProblems,
1118
+ ...initialContentProblems(initial.issues),
1119
+ ...pages.problems
1120
+ ];
1121
+ const isNamed = isSiteId(id) && isText(title);
1122
+ if (!isNamed || pages.pages === void 0 || problems.length > 0) {
1123
+ return invalid(problems);
1124
+ }
1125
+ const description = {
1126
+ id,
1127
+ title,
1128
+ schema: schema.fields,
1129
+ initialContent: initial.content,
1130
+ pages: pages.pages
1131
+ };
1132
+ return { status: "read", description };
973
1133
  }
974
1134
  function isSearchTextsGroup(schema, path) {
975
1135
  const target = resolveFieldPath(schema, path);
@@ -977,30 +1137,53 @@ function isSearchTextsGroup(schema, path) {
977
1137
  const { title, description } = target.field.fields;
978
1138
  return title?.kind === "text" && description?.kind === "text";
979
1139
  }
980
- function readPages(schema, input) {
981
- if (!Array.isArray(input)) return void 0;
1140
+ function namingProblems(id, title) {
1141
+ const idProblems = isSiteId(id) ? [] : [{ code: "invalid-id" }];
1142
+ const titleProblems = isText(title) ? [] : [{ code: "invalid-title" }];
1143
+ return [...idProblems, ...titleProblems];
1144
+ }
1145
+ function initialContentProblems(issues) {
1146
+ return issues.length > 0 ? [{ code: "invalid-initial-content", issues }] : [];
1147
+ }
1148
+ function checkPages(schema, input) {
1149
+ if (!Array.isArray(input) || input.length === 0) {
1150
+ return { pages: void 0, problems: [{ code: "no-pages" }] };
1151
+ }
982
1152
  const entries = input;
983
- const pages = entries.map((entry) => readPage(schema, entry));
984
- const read = pages.flatMap((page) => page === void 0 ? [] : [page]);
985
- const [first, ...rest] = read;
986
- const paths = new Set(read.map((page) => page.path));
987
- const isSound = read.length === pages.length && first?.path === HOME_PAGE_PATH && paths.size === read.length;
988
- return isSound ? [first, ...rest] : void 0;
1153
+ const readings = entries.map((entry) => readPage(schema, entry));
1154
+ const pageProblems = readings.flatMap(
1155
+ (reading, index) => typeof reading === "string" ? [{ code: "invalid-page", index, reason: reading }] : []
1156
+ );
1157
+ const pages = readings.filter((reading) => typeof reading !== "string");
1158
+ const problems = [...pageProblems, ...orderProblems(pages)];
1159
+ const [first, ...rest] = pages;
1160
+ const isSound = problems.length === 0 && first !== void 0;
1161
+ return { pages: isSound ? [first, ...rest] : void 0, problems };
1162
+ }
1163
+ function orderProblems(pages) {
1164
+ const paths = pages.map((page) => page.path);
1165
+ const repeated = paths.filter((path, index) => paths.indexOf(path) !== index);
1166
+ const isHomeMissing = paths.length > 0 && paths[0] !== HOME_PAGE_PATH;
1167
+ const homeProblems = isHomeMissing ? [{ code: "home-page-not-first" }] : [];
1168
+ const repeatProblems = [...new Set(repeated)].map(
1169
+ (path) => ({ code: "repeated-page-path", path })
1170
+ );
1171
+ return [...homeProblems, ...repeatProblems];
989
1172
  }
990
1173
  function readPage(schema, input) {
991
- if (!isRecord4(input)) return void 0;
1174
+ if (!isJsonObject(input)) return "not-a-page";
992
1175
  const { path, title, searchTexts } = input;
993
- const isPath = typeof path === "string" && (path === HOME_PAGE_PATH || PAGE_PATH.test(path));
994
- if (!isPath || !isText2(title)) return void 0;
1176
+ if (!isPagePath(path)) return "invalid-path";
1177
+ if (!isText(title)) return "invalid-title";
995
1178
  if (searchTexts === void 0) return { path, title };
996
1179
  const isSearchTexts = typeof searchTexts === "string" && isSearchTextsGroup(schema, searchTexts);
997
- return isSearchTexts ? { path, title, searchTexts } : void 0;
1180
+ return isSearchTexts ? { path, title, searchTexts } : "invalid-search-texts";
998
1181
  }
999
- function isRecord4(value) {
1000
- return typeof value === "object" && value !== null && !Array.isArray(value);
1182
+ function isPagePath(path) {
1183
+ return typeof path === "string" && (path === HOME_PAGE_PATH || PAGE_PATH.test(path));
1001
1184
  }
1002
- function isText2(value) {
1003
- return typeof value === "string" && value.trim() !== "";
1185
+ function invalid(problems) {
1186
+ return { status: "invalid", problems };
1004
1187
  }
1005
1188
 
1006
1189
  export {
@@ -1022,13 +1205,17 @@ export {
1022
1205
  collection,
1023
1206
  defaultValue,
1024
1207
  defaultContent,
1208
+ resolveFieldPath,
1209
+ isJsonObject,
1025
1210
  linkTargetOf,
1211
+ safeHref,
1026
1212
  hrefOf,
1027
1213
  richTextOf,
1028
1214
  plainTextOf,
1029
1215
  asOneParagraph,
1030
1216
  readRichText,
1031
1217
  normalizedLine,
1218
+ linesOf,
1032
1219
  RECORD_ID_KEY,
1033
1220
  isRecordId,
1034
1221
  newRecord,
@@ -1040,7 +1227,6 @@ export {
1040
1227
  parseContent,
1041
1228
  FIELD_PATH_ATTRIBUTE,
1042
1229
  editable,
1043
- resolveFieldPath,
1044
1230
  REVEAL_ATTRIBUTE,
1045
1231
  REVEALED_ATTRIBUTE,
1046
1232
  REVEAL_EVENT,
@@ -1050,10 +1236,13 @@ export {
1050
1236
  WEEKDAYS,
1051
1237
  openingHoursField,
1052
1238
  readFields,
1239
+ checkFields,
1053
1240
  SITE_CONFIG_FILE,
1054
1241
  isSiteId,
1055
1242
  readSiteConfig,
1056
1243
  SITE_DESCRIPTION_PATH,
1244
+ HOME_PAGE_PATH,
1057
1245
  readSiteDescription,
1246
+ checkSiteDescription,
1058
1247
  isSearchTextsGroup
1059
1248
  };