@seamlet/astro 0.11.2 → 0.12.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.
@@ -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]) => {
@@ -457,11 +571,11 @@ function parseText(field, input, path) {
457
571
  return valid(input);
458
572
  }
459
573
  function parseRichText(field, input, path) {
460
- const read = typeof input === "string" ? richTextOf(input) : readRichText(input);
461
- if (read === void 0) {
574
+ const read2 = typeof input === "string" ? richTextOf(input) : readRichText(input);
575
+ if (read2 === void 0) {
462
576
  return invalidType(defaultValue(field), path);
463
577
  }
464
- const text2 = field.inline === true ? asOneParagraph(read) : read;
578
+ const text2 = field.inline === true ? asOneParagraph(read2) : read2;
465
579
  const hasUnsupportedLink = linkAddressesOf(text2).some(
466
580
  (href) => linkTargetOf(href) === void 0
467
581
  );
@@ -514,12 +628,12 @@ function parseImage(field, input, path) {
514
628
  if (input === null) {
515
629
  return valid(null);
516
630
  }
517
- const src = isRecord(input) ? input["src"] : void 0;
518
- const alt = isRecord(input) ? input["alt"] : void 0;
631
+ const src = isJsonObject(input) ? input["src"] : void 0;
632
+ const alt = isJsonObject(input) ? input["alt"] : void 0;
519
633
  if (typeof src !== "string" || typeof alt !== "string") {
520
634
  return invalidType(defaultValue(field), path);
521
635
  }
522
- const focus = isRecord(input) ? focusOf(input["focus"]) : void 0;
636
+ const focus = isJsonObject(input) ? focusOf(input["focus"]) : void 0;
523
637
  const image2 = focus === void 0 ? { src, alt } : { src, alt, focus };
524
638
  if (src.trim() === "") {
525
639
  return { value: image2, issues: [{ path, code: "missing-image-src" }] };
@@ -530,7 +644,7 @@ function parseLink(field, input, path) {
530
644
  if (input === null) {
531
645
  return valid(null);
532
646
  }
533
- if (!isRecord(input)) {
647
+ if (!isJsonObject(input)) {
534
648
  return invalidType(defaultValue(field), path);
535
649
  }
536
650
  const { text: text2, href } = input;
@@ -619,7 +733,7 @@ function parseCollection(field, input, path) {
619
733
  }
620
734
  function parseRecord(field, input, path) {
621
735
  const parsed = parseFields(field.item, input, path);
622
- const id = isRecord(input) ? input[RECORD_ID_KEY] : void 0;
736
+ const id = isJsonObject(input) ? input[RECORD_ID_KEY] : void 0;
623
737
  if (!isRecordId(id)) return parsed;
624
738
  return {
625
739
  value: { ...parsed.value, [RECORD_ID_KEY]: id },
@@ -644,7 +758,7 @@ function parseReferences(field, input, path) {
644
758
  return valid(ids);
645
759
  }
646
760
  function focusOf(input) {
647
- if (!isRecord(input)) return void 0;
761
+ if (!isJsonObject(input)) return void 0;
648
762
  const { x, y } = input;
649
763
  const isInside = isFraction(x) && isFraction(y);
650
764
  return isInside ? { x, y } : void 0;
@@ -659,9 +773,6 @@ function isCalendarDate(value) {
659
773
  const time2 = Date.parse(`${value}T00:00:00Z`);
660
774
  return !Number.isNaN(time2) && new Date(time2).toISOString().startsWith(value);
661
775
  }
662
- function isRecord(value) {
663
- return typeof value === "object" && value !== null && !Array.isArray(value);
664
- }
665
776
  function valid(value) {
666
777
  return { value, issues: [] };
667
778
  }
@@ -675,45 +786,6 @@ function editable() {
675
786
  return (...path) => ({ [FIELD_PATH_ATTRIBUTE]: formatFieldPath(path) });
676
787
  }
677
788
 
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
789
  // src/schema/reveal-markers.ts
718
790
  var REVEAL_ATTRIBUTE = "data-cms-reveal";
719
791
  var REVEALED_ATTRIBUTE = "data-cms-revealed";
@@ -787,125 +859,185 @@ function openingHoursField(options = {}) {
787
859
  }
788
860
 
789
861
  // src/schema/read-fields.ts
862
+ var TITLE_KINDS = /* @__PURE__ */ new Set([
863
+ "text",
864
+ "link"
865
+ ]);
790
866
  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;
867
+ const reading = checkFields(input);
868
+ return reading.status === "read" ? reading.fields : void 0;
869
+ }
870
+ function checkFields(input) {
871
+ const reading = readFieldsAt(input, []);
872
+ if (!reading.isRead) return { status: "invalid", problem: reading.problem };
873
+ const problem = firstUnknownCollection(reading.value, reading.value, []);
874
+ return problem === void 0 ? { status: "read", fields: reading.value } : { status: "invalid", problem };
875
+ }
876
+ function readFieldsAt(input, path) {
877
+ if (!isJsonObject(input)) return refused(path, "not-fields");
878
+ const fields = {};
879
+ for (const [name, value] of Object.entries(input)) {
880
+ const fieldPath = [...path, name];
881
+ if (!isFieldName(name)) return refused(fieldPath, "invalid-name");
882
+ const field = readField(value, fieldPath);
883
+ if (!field.isRead) return field;
884
+ fields[name] = field.value;
885
+ }
886
+ return read(fields);
800
887
  }
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;
888
+ function readField(input, path) {
889
+ if (!isJsonObject(input)) return refused(path, "not-fields");
890
+ const { kind, label } = input;
891
+ if (typeof label !== "string") return refused(path, "invalid-label");
892
+ if (!isFieldKind(kind)) return refused(path, "unknown-kind");
893
+ return readFieldOfKind({ kind, label, input, path });
807
894
  }
808
- function readFieldOfKind(kind, label, input) {
895
+ function readFieldOfKind(field) {
896
+ const { kind, label, input, path } = field;
809
897
  switch (kind) {
810
898
  case "text":
811
- return {
812
- kind,
813
- label,
814
- ...optional("multiline", input["multiline"], isBoolean),
815
- ...optional("maxLength", input["maxLength"], isCount)
816
- };
899
+ return withOptions({ kind, label }, path, [
900
+ optional("multiline", input, isBoolean),
901
+ optional("maxLength", input, isCount)
902
+ ]);
817
903
  case "richText":
818
- return {
819
- kind,
820
- label,
821
- ...optional("inline", input["inline"], isBoolean),
822
- ...optional("maxLength", input["maxLength"], isCount)
823
- };
904
+ return withOptions({ kind, label }, path, [
905
+ optional("inline", input, isBoolean),
906
+ optional("maxLength", input, isCount)
907
+ ]);
824
908
  case "number":
825
- return {
826
- kind,
827
- label,
828
- ...optional("min", input["min"], isNumber),
829
- ...optional("max", input["max"], isNumber)
830
- };
909
+ return withOptions({ kind, label }, path, [
910
+ optional("min", input, isNumber),
911
+ optional("max", input, isNumber)
912
+ ]);
831
913
  case "date":
832
914
  case "image":
833
915
  case "link":
834
916
  case "toggle":
835
917
  case "url":
836
918
  case "time":
837
- return { kind, label };
919
+ return read({ kind, label });
838
920
  case "choice":
839
921
  case "choices":
840
- return readChoiceField(kind, label, input["options"]);
922
+ return readChoiceField({ kind, label, input, path });
841
923
  case "reference":
842
924
  case "references":
843
- return readReferenceField(kind, label, input["collection"]);
925
+ return readReferenceField({ kind, label, input, path });
844
926
  case "group":
845
- return readGroupField(label, input["fields"]);
927
+ return readGroupField({ kind, label, input, path });
846
928
  case "collection":
847
- return readCollectionField(label, input);
929
+ return readCollectionField({ kind, label, input, path });
848
930
  }
849
931
  }
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 }) => ({
932
+ function readChoiceField(field) {
933
+ const { kind, label, input, path } = field;
934
+ const options = input["options"];
935
+ const isOptionList = Array.isArray(options) && options.every(isChoiceOption);
936
+ if (!isOptionList) return refused(path, "invalid-choice-options");
937
+ const knownOptions = options.map(({ value, label: optionLabel }) => ({
855
938
  value,
856
939
  label: optionLabel
857
940
  }));
858
- return { kind, label, options: read };
941
+ return read({ kind, label, options: knownOptions });
942
+ }
943
+ function readReferenceField(field) {
944
+ const { kind, label, input, path } = field;
945
+ const collection2 = input["collection"];
946
+ if (typeof collection2 !== "string") {
947
+ return refused(path, "unknown-collection");
948
+ }
949
+ return read({ kind, label, collection: collection2 });
950
+ }
951
+ function readGroupField(field) {
952
+ const { kind, label, input, path } = field;
953
+ const fields = readFieldsAt(input["fields"], path);
954
+ return fields.isRead ? read({ kind, label, fields: fields.value }) : fields;
955
+ }
956
+ function readCollectionField(field) {
957
+ const { kind, label, input, path } = field;
958
+ const item = readFieldsAt(input["item"], path);
959
+ if (!item.isRead) return item;
960
+ if (RECORD_ID_KEY in item.value) {
961
+ return refused([...path, RECORD_ID_KEY], "record-id-field");
962
+ }
963
+ const itemTitle = input["itemTitle"];
964
+ if (itemTitle === void 0) return read({ kind, label, item: item.value });
965
+ const titleField = typeof itemTitle === "string" && Object.hasOwn(item.value, itemTitle) ? item.value[itemTitle] : void 0;
966
+ const canTitleRecord = titleField !== void 0 && TITLE_KINDS.has(titleField.kind);
967
+ if (typeof itemTitle !== "string" || !canTitleRecord) {
968
+ return refused(path, "invalid-item-title");
969
+ }
970
+ return read({ kind, label, item: item.value, itemTitle });
859
971
  }
860
- function readReferenceField(kind, label, collection2) {
861
- return typeof collection2 === "string" ? { kind, label, collection: collection2 } : void 0;
972
+ function firstUnknownCollection(schema, fields, path) {
973
+ for (const [name, field] of Object.entries(fields)) {
974
+ const problem = unknownCollectionIn(schema, field, [...path, name]);
975
+ if (problem !== void 0) return problem;
976
+ }
977
+ return void 0;
978
+ }
979
+ function unknownCollectionIn(schema, field, path) {
980
+ switch (field.kind) {
981
+ case "reference":
982
+ case "references":
983
+ return referencesCollection(schema, field) ? void 0 : { path, reason: "unknown-collection" };
984
+ case "group":
985
+ return firstUnknownCollection(schema, field.fields, path);
986
+ case "collection":
987
+ return firstUnknownCollection(schema, field.item, path);
988
+ case "text":
989
+ case "richText":
990
+ case "number":
991
+ case "date":
992
+ case "image":
993
+ case "link":
994
+ case "toggle":
995
+ case "choice":
996
+ case "choices":
997
+ case "url":
998
+ case "time":
999
+ return void 0;
1000
+ }
862
1001
  }
863
- function readGroupField(label, input) {
864
- const fields = readFields(input);
865
- return fields === void 0 ? void 0 : { kind: "group", label, fields };
1002
+ function referencesCollection(schema, field) {
1003
+ return collectionFieldAt(schema, field.collection) !== void 0;
866
1004
  }
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
- };
1005
+ function withOptions(field, path, options) {
1006
+ const isEveryOptionRead = options.every((option) => option !== void 0);
1007
+ if (!isEveryOptionRead) return refused(path, "invalid-option");
1008
+ return read(Object.assign({}, field, ...options));
879
1009
  }
880
- function optional(key, value, isOfKind) {
881
- if (!isOfKind(value)) return {};
1010
+ function optional(key, input, isOfKind) {
1011
+ const value = input[key];
1012
+ if (value === void 0) return {};
1013
+ if (!isOfKind(value)) return void 0;
882
1014
  return { [key]: value };
883
1015
  }
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
- ]);
1016
+ var FIELD_KINDS = {
1017
+ text: true,
1018
+ richText: true,
1019
+ number: true,
1020
+ date: true,
1021
+ image: true,
1022
+ link: true,
1023
+ toggle: true,
1024
+ choice: true,
1025
+ choices: true,
1026
+ url: true,
1027
+ time: true,
1028
+ reference: true,
1029
+ references: true,
1030
+ group: true,
1031
+ collection: true
1032
+ };
901
1033
  function isFieldKind(value) {
902
- return typeof value === "string" && FIELD_KINDS.has(value);
1034
+ return typeof value === "string" && Object.hasOwn(FIELD_KINDS, value);
903
1035
  }
904
- function isChoiceOption(value) {
905
- return isRecord2(value) && typeof value["value"] === "string" && typeof value["label"] === "string";
1036
+ function isFieldName(name) {
1037
+ return name !== "" && !name.includes(FIELD_PATH_SEPARATOR) && !isRecordIndex(name);
906
1038
  }
907
- function isRecord2(value) {
908
- return typeof value === "object" && value !== null && !Array.isArray(value);
1039
+ function isChoiceOption(value) {
1040
+ return isJsonObject(value) && typeof value["value"] === "string" && typeof value["label"] === "string";
909
1041
  }
910
1042
  function isBoolean(value) {
911
1043
  return typeof value === "boolean";
@@ -916,6 +1048,12 @@ function isNumber(value) {
916
1048
  function isCount(value) {
917
1049
  return isNumber(value) && Number.isInteger(value) && value >= 0;
918
1050
  }
1051
+ function read(value) {
1052
+ return { isRead: true, value };
1053
+ }
1054
+ function refused(path, reason) {
1055
+ return { isRead: false, problem: { path, reason } };
1056
+ }
919
1057
 
920
1058
  // src/schema/site-config.ts
921
1059
  var SITE_CONFIG_FILE = "seamlet.json";
@@ -927,7 +1065,7 @@ function isSiteId(value) {
927
1065
  return typeof value === "string" && value.length <= LONGEST_SITE_ID && SITE_ID.test(value);
928
1066
  }
929
1067
  function readSiteConfig(input) {
930
- const fields = isRecord3(input) ? input : {};
1068
+ const fields = isJsonObject(input) ? input : {};
931
1069
  const { id, title } = fields;
932
1070
  const cms = originOf(fields["cms"]);
933
1071
  const hasId = isSiteId(id);
@@ -949,27 +1087,43 @@ function originOf(value) {
949
1087
  const url2 = new URL(value);
950
1088
  return WEB_PROTOCOLS.has(url2.protocol) ? url2.origin : void 0;
951
1089
  }
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
1090
 
959
1091
  // src/schema/site-description.ts
960
1092
  var SITE_DESCRIPTION_PATH = "/seamlet/site.json";
961
1093
  var PAGE_PATH = /^(\/[a-z0-9][a-z0-9-]*)+$/u;
962
1094
  var HOME_PAGE_PATH = "/";
963
1095
  function readSiteDescription(input) {
964
- if (!isRecord4(input)) return void 0;
1096
+ const reading = checkSiteDescription(input);
1097
+ return reading.status === "read" ? reading.description : void 0;
1098
+ }
1099
+ function checkSiteDescription(input) {
1100
+ if (!isJsonObject(input)) return invalid([{ code: "not-a-description" }]);
965
1101
  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 };
1102
+ const nameProblems = namingProblems(id, title);
1103
+ const schema = checkFields(input["schema"]);
1104
+ if (schema.status === "invalid") {
1105
+ const { problem } = schema;
1106
+ return invalid([...nameProblems, { code: "invalid-schema", problem }]);
1107
+ }
1108
+ const initial = parseContent(schema.fields, input["initialContent"]);
1109
+ const pages = checkPages(schema.fields, input["pages"]);
1110
+ const problems = [
1111
+ ...nameProblems,
1112
+ ...initialContentProblems(initial.issues),
1113
+ ...pages.problems
1114
+ ];
1115
+ const isNamed = isSiteId(id) && isText(title);
1116
+ if (!isNamed || pages.pages === void 0 || problems.length > 0) {
1117
+ return invalid(problems);
1118
+ }
1119
+ const description = {
1120
+ id,
1121
+ title,
1122
+ schema: schema.fields,
1123
+ initialContent: initial.content,
1124
+ pages: pages.pages
1125
+ };
1126
+ return { status: "read", description };
973
1127
  }
974
1128
  function isSearchTextsGroup(schema, path) {
975
1129
  const target = resolveFieldPath(schema, path);
@@ -977,30 +1131,53 @@ function isSearchTextsGroup(schema, path) {
977
1131
  const { title, description } = target.field.fields;
978
1132
  return title?.kind === "text" && description?.kind === "text";
979
1133
  }
980
- function readPages(schema, input) {
981
- if (!Array.isArray(input)) return void 0;
1134
+ function namingProblems(id, title) {
1135
+ const idProblems = isSiteId(id) ? [] : [{ code: "invalid-id" }];
1136
+ const titleProblems = isText(title) ? [] : [{ code: "invalid-title" }];
1137
+ return [...idProblems, ...titleProblems];
1138
+ }
1139
+ function initialContentProblems(issues) {
1140
+ return issues.length > 0 ? [{ code: "invalid-initial-content", issues }] : [];
1141
+ }
1142
+ function checkPages(schema, input) {
1143
+ if (!Array.isArray(input) || input.length === 0) {
1144
+ return { pages: void 0, problems: [{ code: "no-pages" }] };
1145
+ }
982
1146
  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;
1147
+ const readings = entries.map((entry) => readPage(schema, entry));
1148
+ const pageProblems = readings.flatMap(
1149
+ (reading, index) => typeof reading === "string" ? [{ code: "invalid-page", index, reason: reading }] : []
1150
+ );
1151
+ const pages = readings.filter((reading) => typeof reading !== "string");
1152
+ const problems = [...pageProblems, ...orderProblems(pages)];
1153
+ const [first, ...rest] = pages;
1154
+ const isSound = problems.length === 0 && first !== void 0;
1155
+ return { pages: isSound ? [first, ...rest] : void 0, problems };
1156
+ }
1157
+ function orderProblems(pages) {
1158
+ const paths = pages.map((page) => page.path);
1159
+ const repeated = paths.filter((path, index) => paths.indexOf(path) !== index);
1160
+ const isHomeMissing = paths.length > 0 && paths[0] !== HOME_PAGE_PATH;
1161
+ const homeProblems = isHomeMissing ? [{ code: "home-page-not-first" }] : [];
1162
+ const repeatProblems = [...new Set(repeated)].map(
1163
+ (path) => ({ code: "repeated-page-path", path })
1164
+ );
1165
+ return [...homeProblems, ...repeatProblems];
989
1166
  }
990
1167
  function readPage(schema, input) {
991
- if (!isRecord4(input)) return void 0;
1168
+ if (!isJsonObject(input)) return "not-a-page";
992
1169
  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;
1170
+ if (!isPagePath(path)) return "invalid-path";
1171
+ if (!isText(title)) return "invalid-title";
995
1172
  if (searchTexts === void 0) return { path, title };
996
1173
  const isSearchTexts = typeof searchTexts === "string" && isSearchTextsGroup(schema, searchTexts);
997
- return isSearchTexts ? { path, title, searchTexts } : void 0;
1174
+ return isSearchTexts ? { path, title, searchTexts } : "invalid-search-texts";
998
1175
  }
999
- function isRecord4(value) {
1000
- return typeof value === "object" && value !== null && !Array.isArray(value);
1176
+ function isPagePath(path) {
1177
+ return typeof path === "string" && (path === HOME_PAGE_PATH || PAGE_PATH.test(path));
1001
1178
  }
1002
- function isText2(value) {
1003
- return typeof value === "string" && value.trim() !== "";
1179
+ function invalid(problems) {
1180
+ return { status: "invalid", problems };
1004
1181
  }
1005
1182
 
1006
1183
  export {
@@ -1022,13 +1199,17 @@ export {
1022
1199
  collection,
1023
1200
  defaultValue,
1024
1201
  defaultContent,
1202
+ resolveFieldPath,
1203
+ isJsonObject,
1025
1204
  linkTargetOf,
1205
+ safeHref,
1026
1206
  hrefOf,
1027
1207
  richTextOf,
1028
1208
  plainTextOf,
1029
1209
  asOneParagraph,
1030
1210
  readRichText,
1031
1211
  normalizedLine,
1212
+ linesOf,
1032
1213
  RECORD_ID_KEY,
1033
1214
  isRecordId,
1034
1215
  newRecord,
@@ -1040,7 +1221,6 @@ export {
1040
1221
  parseContent,
1041
1222
  FIELD_PATH_ATTRIBUTE,
1042
1223
  editable,
1043
- resolveFieldPath,
1044
1224
  REVEAL_ATTRIBUTE,
1045
1225
  REVEALED_ATTRIBUTE,
1046
1226
  REVEAL_EVENT,
@@ -1050,10 +1230,13 @@ export {
1050
1230
  WEEKDAYS,
1051
1231
  openingHoursField,
1052
1232
  readFields,
1233
+ checkFields,
1053
1234
  SITE_CONFIG_FILE,
1054
1235
  isSiteId,
1055
1236
  readSiteConfig,
1056
1237
  SITE_DESCRIPTION_PATH,
1238
+ HOME_PAGE_PATH,
1057
1239
  readSiteDescription,
1240
+ checkSiteDescription,
1058
1241
  isSearchTextsGroup
1059
1242
  };