@lupinum/ginko 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +2 -2
  3. package/dist/basics.d.mts +492 -0
  4. package/dist/basics.d.ts +492 -0
  5. package/dist/basics.mjs +416 -0
  6. package/dist/cli/index.mjs +59 -56
  7. package/dist/index.d.mts +5 -4
  8. package/dist/index.d.ts +5 -4
  9. package/dist/index.mjs +3 -3
  10. package/dist/internal/search.mjs +6 -3
  11. package/dist/internal.d.mts +387 -41
  12. package/dist/internal.d.ts +387 -41
  13. package/dist/internal.mjs +267 -101
  14. package/dist/nuxt/module.d.mts +2 -2
  15. package/dist/nuxt/module.d.ts +2 -2
  16. package/dist/nuxt/module.mjs +108 -15
  17. package/dist/nuxt/runtime/GinkoImage.d.mts +1 -1
  18. package/dist/nuxt/runtime/GinkoMarkdown.mjs +2 -2
  19. package/dist/nuxt/runtime/composables.d.mts +4 -3
  20. package/dist/nuxt/runtime/composables.mjs +19 -15
  21. package/dist/nuxt/runtime/fetch.d.mts +3 -1
  22. package/dist/nuxt/runtime/fetch.mjs +12 -6
  23. package/dist/nuxt/runtime/markdown.d.mts +3 -0
  24. package/dist/nuxt/runtime/markdown.mjs +15 -3
  25. package/dist/nuxt/runtime/plugin.mjs +6 -35
  26. package/dist/nuxt/runtime/preview-listener.d.mts +13 -0
  27. package/dist/nuxt/runtime/preview-listener.mjs +37 -0
  28. package/dist/nuxt/runtime/preview.vue +3 -1
  29. package/dist/nuxt/runtime/schema.d.ts +6 -0
  30. package/dist/nuxt/runtime/server/bundles.d.ts +4 -0
  31. package/dist/nuxt/runtime/server/files-md.d.ts +2 -0
  32. package/dist/nuxt/runtime/server/files.d.ts +2 -0
  33. package/dist/nuxt/runtime/server/headers.d.ts +2 -0
  34. package/dist/nuxt/runtime/server/items.d.mts +12 -0
  35. package/dist/nuxt/runtime/server/items.d.ts +12 -0
  36. package/dist/nuxt/runtime/server/items.mjs +17 -0
  37. package/dist/nuxt/runtime/server/llms-full.d.ts +2 -0
  38. package/dist/nuxt/runtime/server/llms.d.ts +2 -0
  39. package/dist/nuxt/runtime/server/markdown-trees.d.ts +2 -0
  40. package/dist/nuxt/runtime/server/markdown.d.ts +2 -0
  41. package/dist/nuxt/runtime/server/markdown.mjs +1 -1
  42. package/dist/nuxt/runtime/server/platform-md.d.mts +3 -0
  43. package/dist/nuxt/runtime/server/platform-md.d.ts +3 -0
  44. package/dist/nuxt/runtime/server/platform-md.mjs +71 -0
  45. package/dist/nuxt/runtime/server/redirects.d.ts +2 -0
  46. package/dist/nuxt/runtime/server/robots.d.ts +2 -0
  47. package/dist/nuxt/runtime/server/search.d.ts +2 -0
  48. package/dist/nuxt/runtime/server/site-config.d.ts +14 -0
  49. package/dist/nuxt/runtime/server/sitemap-source.d.ts +2 -0
  50. package/dist/nuxt/runtime/server/sitemap-xml.d.ts +24 -0
  51. package/dist/nuxt/runtime/server/sitemap.d.ts +2 -0
  52. package/dist/nuxt/runtime/shared.d.mts +10 -3
  53. package/dist/nuxt/runtime/shared.mjs +2 -2
  54. package/dist/shared/{ginko.BBkZW2nW.d.ts → ginko.6cI8KXZP.d.mts} +2 -2
  55. package/dist/shared/{ginko.CrNRBkS5.mjs → ginko.B1-DoOea.mjs} +253 -309
  56. package/dist/shared/ginko.BNVC-g9n.mjs +10389 -0
  57. package/dist/shared/{ginko.Bwan3BB-.d.mts → ginko.B_pACpVU.d.ts} +2 -2
  58. package/dist/shared/ginko.BaFweQcB.d.mts +91 -0
  59. package/dist/shared/ginko.BiDXgjLy.d.ts +91 -0
  60. package/dist/shared/ginko.C-oz8wwI.mjs +95 -0
  61. package/dist/shared/{ginko.BDF3bW0C.d.ts → ginko.CLzVaDi4.d.ts} +56 -5
  62. package/dist/shared/ginko.C_nq6vk7.d.mts +1622 -0
  63. package/dist/shared/ginko.CjWB8N7f.d.ts +128 -0
  64. package/dist/shared/ginko.D1PeQ3ds.mjs +36 -0
  65. package/dist/shared/ginko.D2oht0Kx.d.ts +1622 -0
  66. package/dist/shared/ginko.DCHGkQ7P.mjs +116 -0
  67. package/dist/shared/ginko.DIl7l-75.mjs +26 -0
  68. package/dist/shared/{ginko.akHewCGt.d.mts → ginko.DkIc0ZZk.d.mts} +56 -5
  69. package/dist/shared/{ginko.D-fNojn9.d.mts → ginko.DrkUeYFP.d.mts} +824 -10
  70. package/dist/shared/{ginko.D-fNojn9.d.ts → ginko.DrkUeYFP.d.ts} +824 -10
  71. package/dist/shared/ginko.WAfoMhv1.mjs +601 -0
  72. package/dist/shared/ginko.o1yIKlgj.d.mts +128 -0
  73. package/dist/shared/{ginko.Dht0xxE5.mjs → ginko.raCzOPYC.mjs} +1 -2
  74. package/dist/shared/{ginko.C90fPpMP.mjs → ginko.t-gbwzxh.mjs} +128 -101
  75. package/dist/shared/ginko.yKMIjJ68.mjs +84 -0
  76. package/dist/vue.d.mts +5 -4
  77. package/dist/vue.d.ts +5 -4
  78. package/dist/vue.mjs +1 -1
  79. package/i18n/ar.json +212 -0
  80. package/i18n/de.json +9 -1
  81. package/i18n/en.json +9 -1
  82. package/i18n/es.json +12 -4
  83. package/i18n/fr.json +212 -0
  84. package/i18n/it.json +212 -0
  85. package/package.json +7 -3
  86. package/dist/shared/ginko.BTUfZCfa.d.mts +0 -172
  87. package/dist/shared/ginko.C-QjDn3O.mjs +0 -4944
  88. package/dist/shared/ginko.CQvxhyez.mjs +0 -40
  89. package/dist/shared/ginko.Dc0oNcDv.d.ts +0 -172
  90. package/dist/shared/ginko.Mn30dmlm.mjs +0 -20
@@ -1,33 +1,10 @@
1
- import { m as msg, o as labelOf, i as isTranslatable, l as effectiveAlt, A as ALT_MAX, g as effectiveMax, j as parseMarkdown, r as effectiveProfile, c as bare, J as DEFAULT_LOCALE, E as isLocale, k as sha256, v as MAX_LOCALES, D as isCurrency, n as coreMessage, x as componentProblems, K as labelProblem, L as MAX_CENTS, M as MARKDOWN_PROFILES, h as effectiveMin, I as profileCovers } from './ginko.C-QjDn3O.mjs';
2
- import { b as workWeekOf, w as weekInfoOf } from './ginko.CQvxhyez.mjs';
1
+ import { m as msg, u as labelOf, i as isTranslatable, q as effectiveAlt, A as ALT_MAX, v as ALT_INVISIBLE, g as effectiveMax, x as isShown, a as parseMarkdown, y as effectiveProfile, j as bare, z as fileRefUses, D as DEFAULT_LOCALE, C as isLocale, n as sha256, M as MAX_LOCALES, E as isCurrency, F as coreMessage, G as componentProblems, H as labelProblem, I as MAX_CENTS, J as MARKDOWN_PROFILES, l as effectiveMin, K as profileCovers } from './ginko.BNVC-g9n.mjs';
2
+ import { r as roundedMb, C as CALENDAR_NAMES, h as holidaysOf, S as STORED_HOLIDAYS, l as limitOf, f as formatMb } from './ginko.D1PeQ3ds.mjs';
3
+ import { f as firstDayShown, w as workWeekOf, a as startOfWeek, D as DEFAULT_WEEK, e as weekInfoOf } from './ginko.C-oz8wwI.mjs';
3
4
 
4
5
  const PREFILL_TOKENS = ["@today", "@workWeek", "@nextWorkWeek"];
5
6
  const isPrefillToken = (value) => PREFILL_TOKENS.includes(value);
6
7
 
7
- const MB = 1024 * 1024;
8
- const UPLOAD_TYPES = {
9
- "image/jpeg": { label: "JPEG", kind: "image", limit: 20 * MB },
10
- "image/png": { label: "PNG", kind: "image", limit: 20 * MB },
11
- "image/webp": { label: "WebP", kind: "image", limit: 20 * MB },
12
- "image/heic": { label: "HEIC", kind: "image", limit: 20 * MB },
13
- "application/pdf": { label: "PDF", kind: "file", limit: 10 * MB }
14
- };
15
- function acceptOf(kind) {
16
- return typesOf(kind).join(",");
17
- }
18
- function limitOf(kind) {
19
- return Math.max(...typesOf(kind).map((type) => UPLOAD_TYPES[type].limit));
20
- }
21
- function formatMb(bytes) {
22
- return `${roundedMb(bytes)}`.replace(".", ",");
23
- }
24
- function roundedMb(bytes) {
25
- return Math.round(bytes / MB * 10) / 10;
26
- }
27
- function typesOf(kind) {
28
- return Object.keys(UPLOAD_TYPES).filter((type) => UPLOAD_TYPES[type].kind === kind);
29
- }
30
-
31
8
  function staleOf(brick, data, today, week, planned = []) {
32
9
  const weekly = weeklyTask(brick);
33
10
  if (!weekly) return void 0;
@@ -43,6 +20,18 @@ function weeklyTask(brick) {
43
20
  }
44
21
  return void 0;
45
22
  }
23
+ function weekStartField(brick) {
24
+ if (brick.plans) return brick.plans.field;
25
+ const weekly = weeklyTask(brick);
26
+ if (weekly) return weekly.field;
27
+ for (const task of brick.tasks ?? []) {
28
+ if (task.kind === "remove") continue;
29
+ for (const [field, token] of Object.entries(task.prefill ?? {})) {
30
+ if ((token === "@workWeek" || token === "@nextWorkWeek") && brick.fields[field]?.type === "date") return field;
31
+ }
32
+ }
33
+ return void 0;
34
+ }
46
35
  function plannedWeeks(schema, singleKey, entriesOf) {
47
36
  const weeks = [];
48
37
  for (const [key, brick] of Object.entries(schema.bricks)) {
@@ -60,6 +49,32 @@ function plansThisWeek(schema, singleKey, task, entryBrick, data, thisWeek) {
60
49
  if (single?.kind !== "single" || plan?.kind !== "collection" || plan.retired || plan.plans?.brick !== singleKey) return false;
61
50
  return weeklyTask(single)?.task.id === task.id && data[plan.plans.field] === thisWeek;
62
51
  }
52
+ function duePlan(brick, data, today) {
53
+ const plan = brick.planned;
54
+ if (!plan) return void 0;
55
+ const next = data[plan.next];
56
+ const from = data[plan.from];
57
+ return next !== void 0 && next !== null && typeof from === "string" && from <= today ? next : void 0;
58
+ }
59
+ function createAsks(brick, key, task) {
60
+ const field = Object.hasOwn(brick.fields, key) ? brick.fields[key] : void 0;
61
+ if (task?.fields && !task.fields.includes(key)) return false;
62
+ return !!field && field.type !== "slug" && key !== brick.planned?.next && key !== brick.planned?.from;
63
+ }
64
+ function liveFrom(schema, brickKey, data, changed, today) {
65
+ const brick = Object.hasOwn(schema.bricks, brickKey) ? schema.bricks[brickKey] : void 0;
66
+ if (!brick) return void 0;
67
+ if (brick.kind === "collection" && brick.plans) {
68
+ const start = data[brick.plans.field];
69
+ const first = typeof start === "string" ? firstDayShown(start, schema.week) : void 0;
70
+ return first && first > today ? first : void 0;
71
+ }
72
+ const plan = brick.planned;
73
+ if (!plan || !changed.length || !changed.every((root) => root === plan.next || root === plan.from)) return void 0;
74
+ const from = data[plan.from];
75
+ const next = data[plan.next];
76
+ return next !== void 0 && next !== null && typeof from === "string" && from > today ? from : void 0;
77
+ }
63
78
 
64
79
  const replacements = { \u00E4: "ae", \u00F6: "oe", \u00FC: "ue", \u00DF: "ss", \u00C4: "ae", \u00D6: "oe", \u00DC: "ue" };
65
80
  function slugify(input) {
@@ -67,6 +82,13 @@ function slugify(input) {
67
82
  return slug || "eintrag";
68
83
  }
69
84
  const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
85
+ function slugBases(locales, texts) {
86
+ const main = locales[0];
87
+ return Object.fromEntries(locales.map((locale) => {
88
+ const own = texts[locale] ?? "";
89
+ return [locale, locale === main ? slugify(own) : own ? slugify(own) : void 0];
90
+ }));
91
+ }
70
92
  function uniqueSlug(base, taken) {
71
93
  if (!taken(base)) return base;
72
94
  for (let n = 2; ; n++) {
@@ -75,6 +97,7 @@ function uniqueSlug(base, taken) {
75
97
  }
76
98
  }
77
99
 
100
+ const SHAPE_CODES = /* @__PURE__ */ new Set(["TYPE", "NOT_OBJECT", "NOT_LOCALIZED", "UNKNOWN_FIELD", "UNKNOWN_LOCALE", "UNKNOWN_BRICK"]);
78
101
  const datePattern = /^\d{4}-\d{2}-\d{2}$/;
79
102
  const timePattern = /^([01]\d|2[0-3]):[0-5]\d$/;
80
103
  const rowIdPattern = /^[A-Za-z0-9_-]{10}$/;
@@ -83,11 +106,18 @@ async function validate(schema, brickKey, data, opts) {
83
106
  if (!brick) return { errors: [issue("", "UNKNOWN_BRICK", "unknownBrick", { brick: brickKey })], warnings: [] };
84
107
  const texts = [];
85
108
  const issues = checkFields(brick.fields, data, "", { locales: schema.locales, main: schema.locales[0], texts, refs: opts.refs, fileAlt: opts.fileAlt });
86
- for (const { field, text, path } of texts) {
87
- const { problemMessages: problems } = await parseMarkdown(text, { profile: effectiveProfile(field), components: schema.components });
88
- for (const problem of problems) issues.push(issue(path, "MARKDOWN", "markdown", { label: shown(field, schema.locales[0]), problem: bare(problem) }));
89
- }
90
- if (opts.mode !== "patch") return { errors: issues, warnings: [] };
109
+ for (const { field, text, path, locale } of texts) {
110
+ const { problemMessages: problems, value } = await parseMarkdown(text, { profile: effectiveProfile(field), components: schema.components });
111
+ const label = shown(field, schema.locales[0]);
112
+ for (const problem of problems) {
113
+ issues.push(problem.line === void 0 ? issue(path, "MARKDOWN", "markdown", { label, problem: bare(problem) }) : issue(path, "MARKDOWN", "markdownAt", { label, line: problem.line, problem: bare(problem) }));
114
+ }
115
+ if (opts.uploads) issues.push(...fileRefIssues(value.nodes, path, label, locale, schema, opts.uploads, opts.fileAlt));
116
+ }
117
+ const rules = ruleIssues(schema, brick, data);
118
+ if (opts.mode === "restore") return { errors: issues.filter((i) => SHAPE_CODES.has(i.code)), warnings: [...issues.filter((i) => !SHAPE_CODES.has(i.code)), ...rules.map((r) => r.issue)] };
119
+ if (opts.mode === "release") return { errors: issues, warnings: rules.map((r) => r.issue) };
120
+ if (opts.mode === "create") return { errors: [...issues, ...rules.map((r) => r.issue)], warnings: [] };
91
121
  const changed = opts.changed ?? /* @__PURE__ */ new Set();
92
122
  const errors = [];
93
123
  const warnings = [];
@@ -96,10 +126,59 @@ async function validate(schema, brickKey, data, opts) {
96
126
  if (related || issue2.code === "UNKNOWN_FIELD") errors.push(issue2);
97
127
  else warnings.push(issue2);
98
128
  }
129
+ for (const { issue: issue2, touches } of rules) {
130
+ if (touches.some((field) => [...changed].some((c) => c === field || c.startsWith(`${field}.`)))) errors.push(issue2);
131
+ else warnings.push(issue2);
132
+ }
99
133
  return { errors, warnings };
100
134
  }
135
+ function ruleIssues(schema, brick, data) {
136
+ if (!isRecord$1(data)) return [];
137
+ const out = [];
138
+ const main = schema.locales[0];
139
+ const plan = brick.planned;
140
+ if (plan) {
141
+ const set = (key) => data[key] !== void 0 && data[key] !== null;
142
+ const missing = set(plan.next) && !set(plan.from) ? plan.from : set(plan.from) && !set(plan.next) ? plan.next : void 0;
143
+ const field = missing && Object.hasOwn(brick.fields, missing) ? brick.fields[missing] : void 0;
144
+ if (missing && field) out.push({ issue: issue(missing, "REQUIRED", "fieldMissing", { label: shown(field, main) }), touches: [plan.next, plan.from] });
145
+ }
146
+ for (const [name, field] of Object.entries(brick.fields)) {
147
+ if (field.type !== "time" || !field.after) continue;
148
+ const [start2, end] = [data[field.after], data[name]];
149
+ if (typeof start2 === "string" && typeof end === "string" && timePattern.test(start2) && timePattern.test(end) && end <= start2) {
150
+ out.push({ issue: issue(name, "TIME_ORDER", "timeOrder", { start: shown(brick.fields[field.after], main), end: shown(field, main) }), touches: [field.after, name] });
151
+ }
152
+ }
153
+ const week = weekStartField(brick);
154
+ const start = week ? data[week] : void 0;
155
+ if (week && isDate(start) && startOfWeek(start, schema.week ?? DEFAULT_WEEK) !== start) {
156
+ out.push({ issue: issue(week, "NOT_WEEK_START", "notWeekStart", { label: shown(brick.fields[week], main) }), touches: [week] });
157
+ }
158
+ return out;
159
+ }
160
+ function fileRefIssues(nodes, path, label, locale, schema, uploads, fileAlt) {
161
+ const out = [];
162
+ const refs = fileRefUses(nodes, schema.components);
163
+ if (refs.some((ref) => !uploads(ref.id))) out.push(issue(path, "FILE_REF_MISSING", "fileRefMissing", { label }));
164
+ if (refs.some((ref) => ref.kind !== "link" && uploads(ref.id) === "file")) out.push(issue(path, "FILE_REF_NOT_IMAGE", "fileRefPdf", { label }));
165
+ const library = new Map(refs.filter((ref) => ref.kind === "img").map((ref) => [ref.node, fileAlt?.(ref.id)]));
166
+ let described = true;
167
+ eachNode(nodes, (node) => {
168
+ if (node[0] === "img" && !effectiveAlt({ [locale]: node[1].alt }, library.get(node), locale)) described = false;
169
+ });
170
+ if (!described) out.push(issue(path, "ALT_REQUIRED", "markdownImageAltMissing", { label }));
171
+ return out;
172
+ }
173
+ function eachNode(nodes, fn) {
174
+ for (const node of nodes) {
175
+ if (!Array.isArray(node) || typeof node[0] !== "string" || !isRecord$1(node[1])) continue;
176
+ fn(node);
177
+ eachNode(node.slice(2), fn);
178
+ }
179
+ }
101
180
  function checkFields(fields, data, path, ctx) {
102
- if (!isRecord$2(data)) return [issue(path, "NOT_OBJECT", "notObject")];
181
+ if (!isRecord$1(data)) return [issue(path, "NOT_OBJECT", "notObject")];
103
182
  const issues = [];
104
183
  for (const key of Object.keys(data)) {
105
184
  if (key === "_id" && path) continue;
@@ -110,7 +189,7 @@ function checkFields(fields, data, path, ctx) {
110
189
  const at = join(path, key);
111
190
  const value = data[key];
112
191
  if (value === void 0 || value === null) {
113
- if (!field.optional) issues.push(issue(at, "REQUIRED", "fieldMissing", { label: shown(field, ctx.main) }));
192
+ if (!field.optional && isShown(field, data)) issues.push(issue(at, "REQUIRED", "fieldMissing", { label: shown(field, ctx.main) }));
114
193
  continue;
115
194
  }
116
195
  issues.push(...checkField$1(field, value, at, ctx));
@@ -119,7 +198,7 @@ function checkFields(fields, data, path, ctx) {
119
198
  }
120
199
  function checkField$1(field, value, path, ctx) {
121
200
  if (isTranslatable(field)) {
122
- if (!isRecord$2(value)) return [issue(path, "NOT_LOCALIZED", "notLocalized", { label: shown(field, ctx.main) })];
201
+ if (!isRecord$1(value)) return [issue(path, "NOT_LOCALIZED", "notLocalized", { label: shown(field, ctx.main) })];
123
202
  const out = [];
124
203
  for (const locale of Object.keys(value)) {
125
204
  if (!ctx.locales.includes(locale)) out.push(issue(join(path, locale), "UNKNOWN_LOCALE", "unknownLocale", { locale }));
@@ -131,14 +210,15 @@ function checkField$1(field, value, path, ctx) {
131
210
  for (const locale of ctx.locales) {
132
211
  const v = value[locale];
133
212
  if (v === void 0 || v === null || v === "") continue;
134
- out.push(...checkScalar(field, v, join(path, locale), locale === ctx.main, ctx));
213
+ out.push(...checkScalar(field, v, join(path, locale), locale, ctx));
135
214
  }
136
215
  return out;
137
216
  }
138
- return checkScalar(field, value, path, true, ctx);
217
+ return checkScalar(field, value, path, void 0, ctx);
139
218
  }
140
219
  const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
141
- function checkScalar(field, value, path, isMain, ctx) {
220
+ function checkScalar(field, value, path, locale, ctx) {
221
+ const isMain = locale === void 0 || locale === ctx?.main;
142
222
  const label = shown(field, ctx?.main);
143
223
  const fail = (code, key, params) => [issue(path, code, key, { label, ...params })];
144
224
  switch (field.type) {
@@ -150,7 +230,7 @@ function checkScalar(field, value, path, isMain, ctx) {
150
230
  if (!field.optional && isMain && value.trim() === "") return fail("REQUIRED", "fieldEmpty");
151
231
  if (value.length > max) return fail("TOO_LONG", "tooLong", { max });
152
232
  if (CONTROL.test(value)) return fail("FORMAT", "controlChars");
153
- if (field.type === "markdown") ctx?.texts?.push({ field, text: value, path });
233
+ if (field.type === "markdown") ctx?.texts?.push({ field, text: value, path, locale: locale ?? ctx.main });
154
234
  if (field.type === "text" && field.format && value !== "") {
155
235
  const ok = field.format === "url" ? /^https?:\/\/[^\s]+$/i.test(value) : field.format === "email" ? /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) : /^\+?[0-9 ()/-]{5,30}$/.test(value) && (value.match(/[0-9]/g)?.length ?? 0) >= 5;
156
236
  if (!ok) return fail("FORMAT", "badFormat");
@@ -166,7 +246,7 @@ function checkScalar(field, value, path, isMain, ctx) {
166
246
  if (Math.abs(Math.round((value - field.min) / field.step) * field.step - (value - field.min)) > 1e-9) return fail("STEP", "badStep", { step: field.step });
167
247
  return [];
168
248
  case "money": {
169
- if (!isRecord$2(value) || !Number.isSafeInteger(value.amount) || Object.keys(value).some((k) => k !== "amount")) return fail("TYPE", "notMoney");
249
+ if (!isRecord$1(value) || !Number.isSafeInteger(value.amount) || Object.keys(value).some((k) => k !== "amount")) return fail("TYPE", "notMoney");
170
250
  const cents = value.amount;
171
251
  if (cents < (field.min ?? 0) || cents > effectiveMax(field)) return fail("RANGE", "moneyOutOfRange");
172
252
  return [];
@@ -178,7 +258,7 @@ function checkScalar(field, value, path, isMain, ctx) {
178
258
  case "time":
179
259
  return typeof value === "string" && timePattern.test(value) ? [] : fail("TIME", "badTime");
180
260
  case "daterange":
181
- if (!isRecord$2(value) || !isDate(value.from) || !isDate(value.to) || Object.keys(value).length !== 2) return fail("DATE", "badDateRange");
261
+ if (!isRecord$1(value) || !isDate(value.from) || !isDate(value.to) || Object.keys(value).length !== 2) return fail("DATE", "badDateRange");
182
262
  return value.from <= value.to ? [] : fail("DATE_ORDER", "dateRangeOrder");
183
263
  case "choice":
184
264
  return typeof value === "string" && field.options.some((o) => o.value === value) ? [] : fail("CHOICE", "badChoice");
@@ -205,7 +285,7 @@ function checkScalar(field, value, path, isMain, ctx) {
205
285
  const key = field.unique;
206
286
  const seen = /* @__PURE__ */ new Set();
207
287
  for (const row of value) {
208
- const v = isRecord$2(row) ? row[key] : void 0;
288
+ const v = isRecord$1(row) ? row[key] : void 0;
209
289
  if (typeof v !== "string") continue;
210
290
  if (seen.has(v)) {
211
291
  out.push(issue(path, "DUPLICATE", "duplicateRowValue", { label, value: v }));
@@ -216,7 +296,7 @@ function checkScalar(field, value, path, isMain, ctx) {
216
296
  }
217
297
  const ids = /* @__PURE__ */ new Set();
218
298
  value.forEach((row, index) => {
219
- const id = isRecord$2(row) && typeof row._id === "string" ? row._id : void 0;
299
+ const id = isRecord$1(row) && typeof row._id === "string" ? row._id : void 0;
220
300
  if (!id || !rowIdPattern.test(id)) {
221
301
  out.push(issue(join(path, String(index)), "ROW_ID", "badRowId", { label, row: index + 1 }));
222
302
  return;
@@ -231,12 +311,12 @@ function checkScalar(field, value, path, isMain, ctx) {
231
311
  return out;
232
312
  }
233
313
  case "image":
234
- if (!isRecord$2(value) || typeof value.file !== "string" || !isRecord$2(value.alt) || Object.keys(value).some((k) => k !== "file" && k !== "alt")) return fail("TYPE", "notImage");
314
+ if (!isRecord$1(value) || typeof value.file !== "string" || !isRecord$1(value.alt) || Object.keys(value).some((k) => k !== "file" && k !== "alt")) return fail("TYPE", "notImage");
235
315
  for (const l of Object.keys(value.alt)) if (!ctx.locales.includes(l)) return [issue(join(path, `alt.${l}`), "UNKNOWN_LOCALE", "unknownLocale", { locale: l })];
236
316
  if (effectiveAlt(value.alt, ctx.fileAlt?.(value.file), ctx.main) === "") return [issue(join(path, "alt"), "ALT_REQUIRED", "altMissing", { label })];
237
- return Object.entries(value.alt).filter(([, a]) => typeof a !== "string" || a.length > ALT_MAX).map(([l]) => issue(join(path, `alt.${l}`), "TOO_LONG", "altTooLong", { label }));
317
+ return Object.entries(value.alt).flatMap(([l, a]) => typeof a !== "string" || a.length > ALT_MAX ? [issue(join(path, `alt.${l}`), "TOO_LONG", "altTooLong", { label })] : ALT_INVISIBLE.test(a) ? [issue(join(path, `alt.${l}`), "FORMAT", "controlChars", { label })] : []);
238
318
  case "file":
239
- return isRecord$2(value) && typeof value.file === "string" && Object.keys(value).length === 1 ? [] : fail("TYPE", "notFile");
319
+ return isRecord$1(value) && typeof value.file === "string" && Object.keys(value).length === 1 ? [] : fail("TYPE", "notFile");
240
320
  }
241
321
  }
242
322
  function checkValue(schema, field, value, path) {
@@ -259,7 +339,7 @@ function fileProblem(field, file) {
259
339
  return void 0;
260
340
  }
261
341
  const shown = (field, main) => labelOf(field.label, DEFAULT_LOCALE, main ?? DEFAULT_LOCALE);
262
- function isRecord$2(v) {
342
+ function isRecord$1(v) {
263
343
  return typeof v === "object" && v !== null && !Array.isArray(v);
264
344
  }
265
345
  function join(path, key) {
@@ -271,12 +351,12 @@ function issue(path, code, key, params) {
271
351
  }
272
352
 
273
353
  const RESERVED_KEYS = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Object.prototype), "prototype"]);
354
+ const LINK_HEADS = /* @__PURE__ */ new Set(["page", "file"]);
274
355
  const UNSAFE_PATH = /\.\.|\/\/|[?#]/;
275
- const SCHEMA_KEYS = /* @__PURE__ */ new Set(["site", "name", "url", "timeZone", "locales", "currency", "week", "pages", "bricks", "migrations", "retired", "components", "agents"]);
356
+ const SCHEMA_KEYS = /* @__PURE__ */ new Set(["site", "name", "url", "timeZone", "locales", "currency", "week", "holidays", "pages", "bricks", "migrations", "retired", "components", "agents"]);
276
357
  const COMPONENT_KEYS = /* @__PURE__ */ new Set(["kind", "label", "describe", "props", "slots", "children", "parents", "editor"]);
277
- const BRICK_KEYS = /* @__PURE__ */ new Set(["pos", "kind", "source", "label", "describe", "fields", "layout", "tasks", "max", "order", "route", "visible", "untranslated", "plans", "drafts", "retired"]);
278
- const FIELD_KEYS = /* @__PURE__ */ new Set(["pos", "type", "label", "hint", "unit", "optional", "default", "translate", "retired", "max", "min", "step", "format", "currency", "from", "accept", "maxBytes", "options", "of", "itemLabel", "unique", "profile", "to", "multiple", "showWhen", "group"]);
279
- const SITE_BUDGET = { siteBytes: 4 * 1024 * 1024, entries: 2e3, entryBytes: 256 * 1024 };
358
+ const BRICK_KEYS = /* @__PURE__ */ new Set(["pos", "kind", "source", "label", "describe", "fields", "layout", "tasks", "max", "order", "route", "visible", "untranslated", "plans", "planned", "drafts", "status", "retired"]);
359
+ const FIELD_KEYS = /* @__PURE__ */ new Set(["pos", "type", "label", "hint", "unit", "optional", "default", "translate", "retired", "max", "min", "step", "format", "currency", "from", "accept", "maxBytes", "options", "of", "itemLabel", "unique", "profile", "to", "multiple", "showWhen", "askAgainWhen", "group", "after"]);
280
360
  const SCHEMA_LIMITS = { bytes: 256 * 1024, bricks: 30, fieldsPerBrick: 60 };
281
361
  function serializeSchema(site) {
282
362
  const retired = site.retired ?? {};
@@ -299,6 +379,8 @@ function serializeSchema(site) {
299
379
  currency: site.currency,
300
380
  // Computed once here; the server, the Studio and the site all read `schema.week`.
301
381
  week: weekInfoOf(site.languages?.[site.locales[0]] ?? site.locales[0], site.firstDayOfWeek),
382
+ // Like `week`: resolved once, so the server and the Studio never run the site's code to know its holidays.
383
+ ...withHolidays(site),
302
384
  pages: Object.fromEntries(Object.entries(site.pages).map(([key, page]) => [key, typeof page === "string" ? { [site.locales[0]]: page } : page])),
303
385
  bricks,
304
386
  migrations,
@@ -307,6 +389,10 @@ function serializeSchema(site) {
307
389
  ...site.agents?.review !== void 0 ? { agents: { review: site.agents.review } } : {}
308
390
  }, [], []);
309
391
  }
392
+ function withHolidays(site) {
393
+ const holidays = holidaysOf(site);
394
+ return holidays === void 0 ? {} : { holidays };
395
+ }
310
396
  function storedField(field, locales) {
311
397
  return Object.fromEntries(Object.entries(field).flatMap(([key, value]) => {
312
398
  if (key === "of" && field.type === "list") return [[key, Object.fromEntries(Object.entries(field.of).map(([name, column]) => [name, storedField(column, locales)]))]];
@@ -334,14 +420,14 @@ function storedPrefill(prefill, fields, locales) {
334
420
  function storedValue(field, value, locales) {
335
421
  if (isTranslatable(field)) {
336
422
  if (typeof value === "string") return { [locales[0]]: value };
337
- if (!isRecord$1(value) || Object.values(value).some((text) => typeof text !== "string")) return value;
423
+ if (!isRecord(value) || Object.values(value).some((text) => typeof text !== "string")) return value;
338
424
  const kept = Object.fromEntries(Object.entries(value).filter(([locale]) => locales.includes(locale)));
339
425
  return Object.hasOwn(kept, locales[0]) ? kept : void 0;
340
426
  }
341
427
  if (field.type !== "list" || !Array.isArray(value)) return value;
342
428
  const rows = [];
343
429
  for (const row of value) {
344
- if (!isRecord$1(row)) {
430
+ if (!isRecord(row)) {
345
431
  rows.push(row);
346
432
  continue;
347
433
  }
@@ -356,7 +442,7 @@ function storedValue(field, value, locales) {
356
442
  }
357
443
  return rows;
358
444
  }
359
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
445
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
360
446
  const LOST = "$lost";
361
447
  function plain(value, path, ancestors) {
362
448
  switch (typeof value) {
@@ -405,6 +491,10 @@ function platformSchema(schema) {
405
491
  function ordered(record) {
406
492
  return Object.entries(record).sort(([a, x], [b, y]) => (x.pos ?? Number.MAX_SAFE_INTEGER) - (y.pos ?? Number.MAX_SAFE_INTEGER) || a.localeCompare(b));
407
493
  }
494
+ function grantGroups(schema, grants) {
495
+ const held = new Set(grants);
496
+ return ordered(schema.bricks).filter(([, brick]) => !brick.retired).map(([key, brick]) => ({ brick: key, label: brick.label, tasks: (brick.tasks ?? []).filter((task) => held.has(`${key}.${task.id}`)).map((task) => task.label) })).filter((group) => group.tasks.length > 0);
497
+ }
408
498
  function migrationFingerprint(brick, fills, compute) {
409
499
  return sha256(`${brick}
410
500
  ${fills}
@@ -430,7 +520,9 @@ const SITE_KEYS = {
430
520
  agents: true,
431
521
  components: true,
432
522
  migrations: true,
433
- retired: true
523
+ retired: true,
524
+ country: true,
525
+ calendar: true
434
526
  };
435
527
  function checkSite(site, schema = serializeSchema(site)) {
436
528
  const problems = Object.keys(site).filter((key) => !Object.hasOwn(SITE_KEYS, key)).map((key) => `Unknown key \u201C${key}\u201D. Known keys: ${Object.keys(SITE_KEYS).join(", ")}.`);
@@ -438,6 +530,10 @@ function checkSite(site, schema = serializeSchema(site)) {
438
530
  if (!site.locales.includes(locale)) problems.push(`languages: locale \u201C${locale}\u201D is not in locales.`);
439
531
  if (!isLocale(language)) problems.push(`languages.${locale}: invalid language \u201C${String(language)}\u201D (expected a canonical language tag such as \u201Cde-AT\u201D or \u201Cen-US\u201D).`);
440
532
  }
533
+ if (site.country !== void 0 && !/^[A-Z]{2}$/.test(String(site.country))) problems.push(`country: invalid country \u201C${String(site.country)}\u201D (expected an ISO 3166-1 code such as \u201CAT\u201D or \u201CDE\u201D).`);
534
+ if (site.calendar !== void 0 && typeof site.calendar !== "function" && !CALENDAR_NAMES.includes(site.calendar)) {
535
+ problems.push(`calendar: unknown calendar \u201C${String(site.calendar)}\u201D (expected AT, DE, SG, none or a function (year) => holidays).`);
536
+ }
441
537
  const color = site.seo?.color;
442
538
  if (color !== void 0 && !/^#(?:[\da-f]{3}|[\da-f]{6})$/i.test(color)) problems.push(`seo.color: invalid colour \u201C${String(color)}\u201D (expected \u201C#rrggbb\u201D or \u201C#rgb\u201D).`);
443
539
  return [...problems, ...checkSchema(schema)];
@@ -471,6 +567,7 @@ function schemaProblems(schema) {
471
567
  if (!isSiteUrl(schema.url)) problems.push(`Invalid site url \u201C${String(schema.url)}\u201D (expected an absolute http(s) address such as \u201Chttps://linde.example\u201D).`);
472
568
  if (!isTimeZone(schema.timeZone)) problems.push(`Invalid time zone \u201C${String(schema.timeZone)}\u201D (expected for example \u201CEurope/Vienna\u201D).`);
473
569
  if (!isWeek(schema.week)) problems.push("week must be { firstDay: 1 to 7, weekend: [days 1 to 7] } (1 = Monday, 7 = Sunday).");
570
+ if (schema.holidays !== void 0 && !STORED_HOLIDAYS.includes(schema.holidays)) problems.push(`holidays must be one of ${STORED_HOLIDAYS.join(", ")}.`);
474
571
  if (!isCurrency(schema.currency)) problems.push(`Invalid currency \u201C${String(schema.currency)}\u201D (expected an ISO 4217 code such as \u201CEUR\u201D or \u201CJPY\u201D).`);
475
572
  const pageOwner = /* @__PURE__ */ new Map();
476
573
  for (const [key, paths] of Object.entries(schema.pages)) {
@@ -496,6 +593,7 @@ function schemaProblems(schema) {
496
593
  const routes = /* @__PURE__ */ new Map();
497
594
  for (const [key, brick] of Object.entries(schema.bricks)) {
498
595
  if (!/^[a-z][a-zA-Z0-9]{0,40}$/.test(key) || RESERVED_KEYS.has(key)) problems.push(`Invalid brick name \u201C${key}\u201D.`);
596
+ else if (LINK_HEADS.has(key)) problems.push(`The brick name \u201C${key}\u201D is reserved: \`$${key}.\u2026\` in a Markdown text means ${key === "page" ? "a page" : "a file of the Files library"}.`);
499
597
  unknown(key, brick, BRICK_KEYS);
500
598
  pushLabel(`${key}.label`, brick.label);
501
599
  if (brick.describe !== void 0) pushLabel(`${key}.describe`, brick.describe);
@@ -521,8 +619,11 @@ function schemaProblems(schema) {
521
619
  else if (!token) for (const p of checkValue(schema, field, value, path)) problems.push(`${p.path}: ${coreMessage("en", p.key, p.params)}`);
522
620
  else if (field.type !== "date") problems.push(`${path}: \u201C${value}\u201D needs a date field.`);
523
621
  }
622
+ if (task.kind === "create" && task.fields !== void 0) problems.push(...createFieldsProblems(`${key}.${task.id}.fields`, task, brick));
524
623
  if (task.needsApproval !== void 0 && typeof task.needsApproval !== "boolean") problems.push(`${key}.${task.id}: needsApproval must be true or false.`);
525
624
  }
625
+ if (brick.planned !== void 0) problems.push(...plannedProblems(key, brick));
626
+ if (brick.status !== void 0) problems.push(...statusProblems(key, brick, schema));
526
627
  if (brick.kind === "single" && "drafts" in brick) problems.push(`${key}: drafts are only allowed for collections.`);
527
628
  if (brick.kind === "single" && "plans" in brick) problems.push(`${key}: plans are only allowed for collections.`);
528
629
  if (brick.kind === "collection") {
@@ -634,6 +735,25 @@ function isWeek(week) {
634
735
  const { firstDay, weekend } = week;
635
736
  return isIsoDay(firstDay) && Array.isArray(weekend) && weekend.length <= 6 && weekend.every(isIsoDay);
636
737
  }
738
+ function createFieldsProblems(path, task, brick) {
739
+ if (!Array.isArray(task.fields)) return [`${path}: must be a list of field names.`];
740
+ const fields = brick.fields;
741
+ const listed = task.fields;
742
+ const problems = [];
743
+ for (const name of listed) {
744
+ const field = Object.hasOwn(fields, name) ? fields[name] : void 0;
745
+ if (!field) problems.push(`${path}: \u201C${name}\u201D is not a field of this brick.`);
746
+ else if (field.type === "slug") problems.push(`${path}: \u201C${name}\u201D is the address and is made from \u201C${field.from}\u201D, so the task cannot ask for it.`);
747
+ else if (brick.planned && (name === brick.planned.next || name === brick.planned.from)) problems.push(`${path}: \u201C${name}\u201D is the plan of a planned change, so the task cannot ask for it.`);
748
+ else if (field.retired) problems.push(`${path}: \u201C${name}\u201D is retired, so the task cannot ask for it.`);
749
+ }
750
+ const prefilled = Object.keys(task.prefill ?? {});
751
+ for (const [name, field] of Object.entries(fields)) {
752
+ if (field.optional || field.default !== void 0 || field.type === "slug" || field.retired) continue;
753
+ if (!listed.includes(name) && !prefilled.includes(name)) problems.push(`${path}: \u201C${name}\u201D is required and has no default, so the task must ask for it.`);
754
+ }
755
+ return problems;
756
+ }
637
757
  function plansProblems(key, plans, fields, schema) {
638
758
  if (typeof plans !== "object" || plans === null || Array.isArray(plans)) return [`${key}: plans must be an object with brick and field.`];
639
759
  const { brick, field } = plans;
@@ -650,6 +770,74 @@ function plansProblems(key, plans, fields, schema) {
650
770
  }
651
771
  return problems;
652
772
  }
773
+ const HOURS_FIELDS = { week: "list", exceptions: "list", closedOn: "date", holidays: "choice" };
774
+ const HOUR_PLAN_FIELDS = { week: "list", from: "date", yearly: "boolean" };
775
+ function isHourPlans(brick) {
776
+ return brick.kind === "collection" && !brick.retired && Object.entries(HOUR_PLAN_FIELDS).every(([name, type]) => Object.hasOwn(brick.fields, name) && !brick.fields[name].retired && brick.fields[name].type === type);
777
+ }
778
+ function statusProblems(key, brick, schema) {
779
+ const status = brick.status;
780
+ const kinds = brick.kind === "collection" ? ["next", "count", "missing", "age"] : ["hours"];
781
+ const shape = brick.kind === "collection" ? "{ next }, { count: true }, { missing } or { age }" : "{ hours: true } or { hours: { plans } }";
782
+ if (typeof status !== "object" || status === null || Array.isArray(status)) return [`${key}: status must be ${shape}.`];
783
+ const entries = Object.entries(status);
784
+ if (entries.length !== 1 || !kinds.includes(entries[0][0])) return [`${key}: status must be ${shape}.`];
785
+ const [kind, value] = entries[0];
786
+ if (kind === "count") {
787
+ if (value !== true) return [`${key}: status.count must be true.`];
788
+ const vis = brick.visible;
789
+ return vis?.mode === "until" ? [] : [`${key}: status { count: true } needs visible: { mode: 'until' }.`];
790
+ }
791
+ if (kind === "hours") {
792
+ const problems = [];
793
+ if (value !== true) {
794
+ const plans = typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 ? value.plans : void 0;
795
+ if (typeof plans !== "string") return [`${key}: status.hours must be true or { plans: '<key of the hour plans>' }.`];
796
+ const target = Object.hasOwn(schema.bricks, plans) ? schema.bricks[plans] : void 0;
797
+ if (!target || target.retired) problems.push(`${key}: status.hours.plans names \u201C${plans}\u201D, which is not a live brick of the site.`);
798
+ else if (!isHourPlans(target)) problems.push(`${key}: status.hours.plans names \u201C${plans}\u201D, which is not a collection of hour plans (hourPlans(): a list \u201Cweek\u201D, a date \u201Cfrom\u201D and a switch \u201Cyearly\u201D).`);
799
+ }
800
+ for (const [name, type] of Object.entries(HOURS_FIELDS)) {
801
+ const field2 = Object.hasOwn(brick.fields, name) ? brick.fields[name] : void 0;
802
+ if (!field2 || field2.retired) problems.push(`${key}: status { hours } needs the live field \u201C${name}\u201D.`);
803
+ else if (field2.type !== type) problems.push(`${key}: status { hours } needs \u201C${name}\u201D to be a ${type} field.`);
804
+ }
805
+ return problems;
806
+ }
807
+ const field = typeof value === "string" && Object.hasOwn(brick.fields, value) ? brick.fields[value] : void 0;
808
+ if (!field) return [`${key}: status.${kind} names \u201C${String(value)}\u201D, which is not a field of the brick.`];
809
+ if (field.retired) return [`${key}: status.${kind} names the retired field \u201C${value}\u201D; remove the status or name another field.`];
810
+ if (kind === "missing") return field.optional && field.default === void 0 ? [] : [`${key}: status.missing needs an optional field without a default, \u201C${value}\u201D is not.`];
811
+ return field.type === "date" || field.type === "daterange" ? [] : [`${key}: status.${kind} needs a date or daterange field, \u201C${value}\u201D is a ${field.type} field.`];
812
+ }
813
+ function plannedProblems(key, brick) {
814
+ const planned = brick.planned;
815
+ if (typeof planned !== "object" || planned === null || Array.isArray(planned)) return [`${key}: planned must be an object with field, next and from.`];
816
+ const p = planned;
817
+ const problems = [];
818
+ for (const k of Object.keys(p)) if (!["field", "next", "from"].includes(k)) problems.push(`${key}.planned: unknown property \u201C${k}\u201D.`);
819
+ const named = (role) => {
820
+ const name = p[role];
821
+ const field2 = typeof name === "string" && Object.hasOwn(brick.fields, name) ? brick.fields[name] : void 0;
822
+ if (!field2 || field2.retired) problems.push(`${key}: planned.${role} names \u201C${String(name)}\u201D, which is not a field of the brick.`);
823
+ return field2;
824
+ };
825
+ const field = named("field");
826
+ const next = named("next");
827
+ const from = named("from");
828
+ if (!field || !next || !from) return problems;
829
+ const [f, n, d] = [p.field, p.next, p.from];
830
+ if ((/* @__PURE__ */ new Set([f, n, d])).size < 3) problems.push(`${key}: planned needs three different fields.`);
831
+ if (next.type !== field.type) problems.push(`${key}: planned.next \u201C${n}\u201D needs the type of \u201C${f}\u201D (${field.type}).`);
832
+ if (from.type !== "date") problems.push(`${key}: planned.from \u201C${d}\u201D must be a date field.`);
833
+ if (!next.optional || !from.optional) problems.push(`${key}: planned.next \u201C${n}\u201D and planned.from \u201C${d}\u201D must be optional.`);
834
+ for (const task of brick.tasks ?? []) {
835
+ if (task.kind === "edit" && task.fields.includes(f) && !(task.fields.includes(n) && task.fields.includes(d))) {
836
+ problems.push(`${key}.${task.id}: the task changes \u201C${f}\u201D, so it must also show its plan \u201C${n}\u201D and \u201C${d}\u201D.`);
837
+ }
838
+ }
839
+ return problems;
840
+ }
653
841
  function mirrorProblem(target, source, siteCurrency) {
654
842
  if (source.type !== target.type) return `type ${source.type} instead of ${target.type}`;
655
843
  if (isTranslatable(source) !== isTranslatable(target)) return "different translatability";
@@ -734,6 +922,11 @@ function checkField(path, field, siblings, topLevel, schema) {
734
922
  if (keys !== "field,is" && keys !== "field,not") problems.push(`${path}.showWhen: needs \`field\` and one of \`is\` or \`not\`.`);
735
923
  else if (typeof when.field !== "string" || !Object.hasOwn(siblings, when.field) || when.field === path.split(".").pop()) problems.push(`${path}.showWhen: \u201C${String(when.field)}\u201D is not another field next to this one.`);
736
924
  }
925
+ if (field.type === "choices" && field.askAgainWhen !== void 0) {
926
+ const target = field.askAgainWhen;
927
+ const sibling = typeof target === "string" && Object.hasOwn(siblings, target) ? siblings[target] : void 0;
928
+ if (target === path.split(".").pop() || sibling?.type !== "text" && sibling?.type !== "longtext") problems.push(`${path}.askAgainWhen: must name a text field next to it.`);
929
+ }
737
930
  let numbersOk = true;
738
931
  for (const key of NUMBER_OPTIONS) {
739
932
  const value = field[key];
@@ -808,6 +1001,11 @@ function checkField(path, field, siblings, topLevel, schema) {
808
1001
  case "file":
809
1002
  if (!(field.maxBytes > 0 && field.maxBytes <= limitOf("file"))) problems.push(`${path}: maxBytes at most ${formatMb(limitOf("file"))} MB.`);
810
1003
  break;
1004
+ case "time":
1005
+ if (field.after !== void 0 && (!topLevel || siblings[field.after]?.type !== "time" || field.after === path.split(".").pop())) {
1006
+ problems.push(`${path}.after: \u201C${String(field.after)}\u201D is not another time field of the brick.`);
1007
+ }
1008
+ break;
811
1009
  case "slug": {
812
1010
  const source = siblings[field.from];
813
1011
  if (!source || !["text", "longtext"].includes(source.type)) problems.push(`${path}: from must be a text field.`);
@@ -818,258 +1016,4 @@ function checkField(path, field, siblings, topLevel, schema) {
818
1016
  return problems;
819
1017
  }
820
1018
 
821
- function trimSlash(path) {
822
- return path.length > 1 ? path.replace(/\/+$/, "") : path;
823
- }
824
- function escapeRegex(text) {
825
- return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
826
- }
827
- function compile(rule) {
828
- const wildcard = rule.from.endsWith("*");
829
- const base = trimSlash(wildcard ? rule.from.slice(0, -1).replace(/\/$/, "") : rule.from);
830
- const params = [];
831
- const source = base.split("/").map((segment) => {
832
- const param = /^:([A-Za-z_]\w*)$/.exec(segment);
833
- if (!param) return escapeRegex(segment);
834
- params.push(param[1]);
835
- return "([^/]+)";
836
- }).join("/");
837
- const tail = wildcard ? "(?:/(.*))?" : "";
838
- return { rule, pattern: new RegExp(`^${source}${tail}$`), params, wildcard };
839
- }
840
- const compiledRules = /* @__PURE__ */ new WeakMap();
841
- const entryRules = /* @__PURE__ */ new WeakMap();
842
- function compiledFor(rules) {
843
- let compiled = compiledRules.get(rules);
844
- if (!compiled) compiledRules.set(rules, compiled = rules.map(compile));
845
- return compiled;
846
- }
847
- function matchRedirect(rules, path) {
848
- const queryStart = path.indexOf("?");
849
- const pathPart = trimSlash(queryStart === -1 ? path : path.slice(0, queryStart));
850
- const query = queryStart === -1 ? "" : path.slice(queryStart + 1);
851
- for (const compiled of compiledFor(rules)) {
852
- const match = compiled.pattern.exec(pathPart);
853
- if (!match) continue;
854
- const values = {};
855
- compiled.params.forEach((name, index) => {
856
- values[name] = match[index + 1] ?? "";
857
- });
858
- values.splat = compiled.wildcard ? match[match.length - 1] ?? "" : "";
859
- let to = compiled.rule.to.replace(/:([A-Za-z_]\w*)/g, (token, name) => values[name] ?? token);
860
- if (/^[/\\]/.test(to)) to = "/" + to.replace(/^[/\\]+/, "");
861
- if (query) to += (to.includes("?") ? "&" : "?") + query;
862
- return { to, status: compiled.rule.status ?? 301 };
863
- }
864
- return void 0;
865
- }
866
- function findRedirect(rules, bundle, path) {
867
- if (!bundle) return matchRedirect(rules, path);
868
- let fromEntries = entryRules.get(bundle);
869
- if (!fromEntries) entryRules.set(bundle, fromEntries = entryRedirects(bundle));
870
- return matchRedirect(rules, path) ?? matchRedirect(fromEntries, path);
871
- }
872
- function entryRedirects(bundle) {
873
- const rules = [];
874
- for (const brick of Object.values(bundle.bricks)) {
875
- if (brick.kind !== "collection") continue;
876
- for (const item of brick.items) {
877
- const value = item.data.redirectFrom;
878
- const oldPaths = Array.isArray(value) ? value.filter((path) => typeof path === "string") : typeof value === "string" ? [value] : [];
879
- const url = item.urls?.[bundle.locale];
880
- if (!url) continue;
881
- for (const from of oldPaths) rules.push({ from, to: url, status: 301 });
882
- }
883
- }
884
- return rules;
885
- }
886
- function redirectProblems(rules) {
887
- const problems = [];
888
- const seen = /* @__PURE__ */ new Set();
889
- for (const { from, to } of rules) {
890
- if (!from.startsWith("/")) problems.push(`"${from}": path must start with "/".`);
891
- if (trimSlash(from) === trimSlash(to)) problems.push(`"${from}": redirect points to itself.`);
892
- if (/^[/\\]?:/.test(to)) problems.push(`"${from}": "to" starts with a parameter (${to}). Start it with a fixed path such as "/new/:splat" so a visitor cannot choose the target host.`);
893
- if (seen.has(from)) problems.push(`"${from}": defined more than once.`);
894
- seen.add(from);
895
- }
896
- return problems;
897
- }
898
-
899
- const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
900
- const isTree = (v) => isRecord(v) && Array.isArray(v.nodes);
901
- const isNode = (n) => Array.isArray(n) && typeof n[0] === "string" && isRecord(n[1]);
902
- function mapTrees(bundle, fn) {
903
- const walk = (value, at, field) => {
904
- if (isTree(value)) return fn(value, { ...at, field });
905
- if (Array.isArray(value)) return value.map((v, i) => walk(v, at, `${field}.${i}`));
906
- if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v, at, field ? `${field}.${k}` : k)]));
907
- return value;
908
- };
909
- const data = (d, brick, id) => walk(d, { brick, id }, "");
910
- const bricks = Object.fromEntries(Object.entries(bundle.bricks).map(([key, b]) => [key, b.kind === "single" ? { ...b, data: data(b.data, key, key) } : { ...b, items: b.items.map((item) => ({ ...item, data: data(item.data, key, item.id) })) }]));
911
- return { ...bundle, bricks };
912
- }
913
- function find(bundle, brick, ref) {
914
- const b = Object.hasOwn(bundle.bricks, brick) ? bundle.bricks[brick] : void 0;
915
- if (b?.kind !== "collection") return void 0;
916
- return b.items.find((i) => i.id === `${brick}/${ref}` || i.id === ref || i.data.slug === ref);
917
- }
918
- function routeTarget(bundle, schema, ref) {
919
- const dot = ref.indexOf(".");
920
- if (dot < 1) return void 0;
921
- const head = ref.slice(0, dot);
922
- const rest = ref.slice(dot + 1);
923
- if (head === "page") return Object.hasOwn(schema.pages, rest) ? schema.pages[rest][bundle.locale] : void 0;
924
- return find(bundle, head, rest)?.urls?.[bundle.locale];
925
- }
926
- function resolveRouteLinks(bundle, schema) {
927
- const hasSnippets = Object.hasOwn(schema.bricks, "snippets") && schema.bricks.snippets.kind === "collection";
928
- const snippetNodes = (name) => {
929
- const body = hasSnippets ? find(bundle, "snippets", name)?.data.body : void 0;
930
- return isTree(body) ? body.nodes : void 0;
931
- };
932
- const resolve = (nodes, nested) => nodes.flatMap((node) => {
933
- if (!isNode(node)) return [node];
934
- const [tag, props, ...children] = node;
935
- if (tag === "snippet" && !nested) {
936
- const inner = typeof props.name === "string" ? snippetNodes(props.name) : void 0;
937
- return inner ? resolve(inner, true) : [[tag, { ...props, "data-broken": true }, ...children]];
938
- }
939
- let next = props;
940
- const href = props.href;
941
- if (tag === "a" && typeof href === "string" && href.startsWith("$")) {
942
- const hash = href.indexOf("#");
943
- const target = routeTarget(bundle, schema, hash === -1 ? href.slice(1) : href.slice(1, hash));
944
- next = target === void 0 ? { ...props, "data-broken": true } : { ...props, href: target + (hash === -1 ? "" : href.slice(hash)) };
945
- }
946
- return [[tag, next, ...resolve(children, nested)]];
947
- });
948
- return mapTrees(bundle, (tree) => ({
949
- ...tree,
950
- nodes: resolve(tree.nodes, false),
951
- ...tree.summary ? { summary: resolve(tree.summary, false) } : {}
952
- }));
953
- }
954
- function eachNode(nodes, fn) {
955
- for (const node of nodes) if (isNode(node)) {
956
- fn(node);
957
- eachNode(node.slice(2), fn);
958
- }
959
- }
960
- const normal = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
961
- function checkLinks(bundles, schema, options = {}) {
962
- const config = options.linkCheck ?? {};
963
- const levels = { broken: config.broken ?? "error", anchors: config.anchors ?? "warn", redirected: config.redirected ?? "warn" };
964
- const rules = [...options.redirects ?? [], ...bundles.flatMap(entryRedirects)];
965
- const known = /* @__PURE__ */ new Set();
966
- const trees = /* @__PURE__ */ new Map();
967
- for (const bundle of bundles) {
968
- for (const pages of Object.values(schema.pages)) {
969
- const p = pages[bundle.locale];
970
- if (p) known.add(normal(p));
971
- }
972
- for (const b of Object.values(bundle.bricks)) {
973
- if (b.kind !== "collection") continue;
974
- for (const item of b.items) {
975
- const url = item.urls?.[bundle.locale];
976
- if (!url) continue;
977
- known.add(normal(url));
978
- const own = [];
979
- mapTrees({ ...bundle, bricks: { x: { kind: "single", data: item.data, fallback: [] } } }, (t) => {
980
- own.push(t);
981
- return t;
982
- });
983
- trees.set(normal(url), own);
984
- }
985
- }
986
- }
987
- const hasId = (list, id) => list.some((t) => {
988
- let found = false;
989
- eachNode(t.nodes, (n) => {
990
- if (/^h[1-6]$/.test(n[0]) && n[1].id === id) found = true;
991
- });
992
- return found;
993
- });
994
- const issues = [];
995
- for (const bundle of bundles) {
996
- mapTrees(bundle, (tree, at) => {
997
- const add = (severity, href, key, params) => issues.push({ severity, locale: bundle.locale, brick: at.brick, id: at.id, field: at.field, href, ...msg(`links.${key}`, params) });
998
- const broken = (href, key, params) => {
999
- if (levels.broken !== "off") add(levels.broken, href, key, params);
1000
- };
1001
- eachNode([...tree.nodes, ...tree.summary ?? []], (node) => {
1002
- const [tag, props] = node;
1003
- if (tag === "snippet" && props["data-broken"]) return broken(`snippet:${String(props.name)}`, "snippetMissing", { name: String(props.name) });
1004
- const href = props.href;
1005
- if (tag !== "a" || typeof href !== "string") return;
1006
- if (props["data-broken"]) {
1007
- const head = href.slice(1).split(".")[0];
1008
- if (href.startsWith("$") && head !== "page" && Object.hasOwn(schema.bricks, head) && !Object.hasOwn(bundle.bricks, head)) return;
1009
- return broken(href, "linkBroken", { href });
1010
- }
1011
- const hash = href.indexOf("#");
1012
- const path = hash === -1 ? href : href.slice(0, hash);
1013
- const anchor = hash === -1 ? "" : href.slice(hash + 1);
1014
- let target;
1015
- if (path === "") target = [tree];
1016
- else if (path.startsWith("/") && !path.startsWith("//")) {
1017
- const clean = normal(path.split("?")[0]);
1018
- const moved = levels.redirected === "off" ? void 0 : matchRedirect(rules, clean);
1019
- if (moved) {
1020
- add(levels.redirected, href, "linkRedirected", { href, to: moved.to });
1021
- return;
1022
- }
1023
- if (!known.has(clean)) {
1024
- if (options.files && !options.files(clean)) broken(href, "pathUnknown", { path: clean });
1025
- return;
1026
- }
1027
- target = trees.get(clean);
1028
- } else return;
1029
- if (anchor && levels.anchors !== "off" && target && !hasId(target, anchor)) add(levels.anchors, href, "anchorMissing", { anchor });
1030
- });
1031
- return tree;
1032
- });
1033
- }
1034
- return issues;
1035
- }
1036
- function externalLinks(bundles) {
1037
- const out = [];
1038
- for (const bundle of bundles) {
1039
- mapTrees(bundle, (tree, at) => {
1040
- eachNode([...tree.nodes, ...tree.summary ?? []], ([tag, props]) => {
1041
- const href = props.href;
1042
- if (tag === "a" && typeof href === "string" && /^https?:\/\//.test(href)) out.push({ href, locale: bundle.locale, ...at });
1043
- });
1044
- return tree;
1045
- });
1046
- }
1047
- return out;
1048
- }
1049
- const MD_INLINE = /\]\(\s*<?([^)\s>]+)/g;
1050
- const MD_DEFINITION = /^[ \t]{0,3}\[[^\]]+\]:[ \t]*<?(\S+?)>?(?:[ \t]+.*)?$/gm;
1051
- const MD_SNIPPET = /::snippet\{[^}]*\bname="([^"]*)"/g;
1052
- function linkTargets(fields, data) {
1053
- const out = /* @__PURE__ */ new Set();
1054
- const text = (value) => {
1055
- for (const markdown of typeof value === "string" ? [value] : isRecord(value) ? Object.values(value) : []) {
1056
- if (typeof markdown !== "string") continue;
1057
- for (const m of markdown.matchAll(MD_INLINE)) out.add(m[1]);
1058
- for (const m of markdown.matchAll(MD_DEFINITION)) out.add(m[1]);
1059
- for (const m of markdown.matchAll(MD_SNIPPET)) out.add(`snippet:${m[1]}`);
1060
- }
1061
- };
1062
- const walk = (inner, row) => {
1063
- for (const [name, field] of Object.entries(inner)) {
1064
- const value = row[name];
1065
- if (field.type === "markdown") text(value);
1066
- else if (field.type === "list" && Array.isArray(value)) {
1067
- for (const r of value) if (isRecord(r)) walk(field.of, r);
1068
- }
1069
- }
1070
- };
1071
- walk(fields, data);
1072
- return out;
1073
- }
1074
-
1075
- export { platformSchema as A, BRICK_KEYS as B, slugify as C, staleOf as D, uniqueSlug as E, FIELD_KEYS as F, validate as G, weeklyTask as H, PREFILL_TOKENS as P, SCHEMA_KEYS as S, UPLOAD_TYPES as U, checkSite as a, redirectProblems as b, checkLinks as c, entryRedirects as d, externalLinks as e, SCHEMA_LIMITS as f, SITE_BUDGET as g, SLUG_PATTERN as h, acceptOf as i, checkSchema as j, fileProblem as k, findRedirect as l, formatMb as m, isDate as n, isPrefillToken as o, isTimeZone as p, limitOf as q, resolveRouteLinks as r, serializeSchema as s, linkTargets as t, mainLocale as u, matchRedirect as v, migrationFingerprint as w, ordered as x, plannedWeeks as y, plansThisWeek as z };
1019
+ export { weekStartField as A, BRICK_KEYS as B, weeklyTask as C, FIELD_KEYS as F, PREFILL_TOKENS as P, SCHEMA_KEYS as S, checkSchema as a, createAsks as b, checkSite as c, SCHEMA_LIMITS as d, SHAPE_CODES as e, SLUG_PATTERN as f, duePlan as g, fileProblem as h, grantGroups as i, isDate as j, isHourPlans as k, isPrefillToken as l, isTimeZone as m, liveFrom as n, ordered as o, platformSchema as p, mainLocale as q, migrationFingerprint as r, serializeSchema as s, plannedWeeks as t, plansThisWeek as u, validate as v, slugBases as w, slugify as x, staleOf as y, uniqueSlug as z };