@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
@@ -0,0 +1,128 @@
1
+ import { B as Bundle } from './ginko.6cI8KXZP.mjs';
2
+ import { L as Locale } from './ginko.DrkUeYFP.mjs';
3
+
4
+ interface NavNode {
5
+ title: string;
6
+ path: string;
7
+ id?: string;
8
+ brick?: string;
9
+ children: NavNode[];
10
+ }
11
+ /**
12
+ * Navigation tree of the collections `bricks` in `locale`, built from item URLs. An item is a child of the
13
+ * node at its nearest existing path prefix, else a root node. A string `parent` field (item id) overrides that.
14
+ * Siblings keep the item order of the bundle (bricks in the given order).
15
+ */
16
+ declare function navTree(bundle: Bundle, bricks: string[], locale: Locale): NavNode[];
17
+ /** Nodes from the root down to the node at `path`; empty when the path is not in the tree. */
18
+ declare function breadcrumbs(tree: NavNode[], path: string): NavNode[];
19
+
20
+ interface SeoImage {
21
+ url: string;
22
+ width?: number;
23
+ height?: number;
24
+ alt?: string;
25
+ }
26
+ interface SeoInput {
27
+ siteName: string;
28
+ /** `%s` is replaced by the page title; default '%s · <siteName>'. A page without title gets siteName alone. */
29
+ titleTemplate?: string;
30
+ title?: string;
31
+ description?: string;
32
+ image?: SeoImage;
33
+ type?: 'website' | 'article';
34
+ noindex?: boolean;
35
+ }
36
+ interface SeoHead {
37
+ title: string;
38
+ meta: (({
39
+ name: string;
40
+ } | {
41
+ property: string;
42
+ }) & {
43
+ content: string;
44
+ })[];
45
+ }
46
+ declare function seoHead(input: SeoInput): SeoHead;
47
+ /** True when a request for `host` (with port, as in the Host header) is not for the site's own address: preview deployments, staging, localhost. Those must never be indexed. */
48
+ declare function isPreviewHost(host: string | undefined, siteUrl: string): boolean;
49
+ declare function robotsTxt(options: {
50
+ sitemap?: string;
51
+ noindex?: boolean;
52
+ disallow?: string[];
53
+ }): string;
54
+ declare function websiteLd(o: {
55
+ name: string;
56
+ url: string;
57
+ lang: string;
58
+ }): Record<string, unknown>;
59
+ /**
60
+ * `node` with the site's `country` (ISO 3166 code) where a builder left it out: `addressCountry` of every PostalAddress,
61
+ * and where remote applicants of a remote JobPosting must live. The Nuxt module applies it to a page's JSON-LD, so
62
+ * `siteJsonLd` and `jobJsonLd` need no `country` (an explicit one wins). An empty `country` changes nothing.
63
+ */
64
+ declare function withCountry<T>(node: T, country: string): T;
65
+ /** The schema.org types `organizationLd` writes. A page whose JSON-LD has a node of one of them is the site's organisation node. */
66
+ declare const ORGANIZATION_TYPES: readonly ["Organization", "LocalBusiness", "NGO"];
67
+ interface OrganizationLdInput {
68
+ name: string;
69
+ url: string;
70
+ /** `Organization` (default), `LocalBusiness` for a shop or practice, `NGO` for a nonprofit. */
71
+ type?: (typeof ORGANIZATION_TYPES)[number];
72
+ legalName?: string;
73
+ /** Absolute URL of the logo. */
74
+ logo?: string;
75
+ email?: string;
76
+ telephone?: string;
77
+ address?: {
78
+ street?: string;
79
+ postalCode?: string;
80
+ city?: string;
81
+ country?: string;
82
+ };
83
+ vatID?: string;
84
+ /** Absolute URLs of the organisation's profiles elsewhere. */
85
+ sameAs?: string[];
86
+ }
87
+ declare function organizationLd(o: OrganizationLdInput): Record<string, unknown>;
88
+ /**
89
+ * A schema.org BreadcrumbList from the first step (usually the home page) to the page itself. Each step has a `title`
90
+ * and a `path` of the site, as the nodes of `useGinkoBreadcrumbs` do; `siteUrl` (`useGinkoSite().url`) makes the
91
+ * paths absolute.
92
+ */
93
+ declare function breadcrumbJsonLd(steps: readonly Pick<NavNode, 'title' | 'path'>[], siteUrl: string): Record<string, unknown>;
94
+ interface ArticleLdInput {
95
+ /** `Article` (default) or `BlogPosting` for a post of a blog. */
96
+ type?: 'Article' | 'BlogPosting';
97
+ title: string;
98
+ url: string;
99
+ description?: string;
100
+ image?: string;
101
+ published?: string;
102
+ modified?: string;
103
+ /** One author by name, or a list of authors with an optional website each. */
104
+ author?: string | readonly {
105
+ name: string;
106
+ url?: string;
107
+ }[];
108
+ keywords?: readonly string[];
109
+ lang: string;
110
+ }
111
+ declare function articleLd(o: ArticleLdInput): Record<string, unknown>;
112
+ declare function faqLd(pairs: {
113
+ question: string;
114
+ answer: string;
115
+ }[]): Record<string, unknown>;
116
+ /**
117
+ * Headers every response of a Ginko site carries. Only the platform may frame `/__ginko` (the preview); nothing
118
+ * else is framed. `nosniff` stops a browser from reading user text (`.md`, `llms.txt`, JSON) as HTML, and the
119
+ * referrer policy keeps full URLs from leaving the site.
120
+ */
121
+ declare function siteHeaders(input: {
122
+ preview: boolean;
123
+ platformOrigin: string;
124
+ noindex: boolean;
125
+ }): Record<string, string>;
126
+
127
+ export { ORGANIZATION_TYPES as a, articleLd as d, breadcrumbJsonLd as e, breadcrumbs as f, faqLd as g, siteHeaders as h, isPreviewHost as i, withCountry as j, navTree as n, organizationLd as o, robotsTxt as r, seoHead as s, websiteLd as w };
128
+ export type { ArticleLdInput as A, NavNode as N, OrganizationLdInput as O, SeoHead as S, SeoImage as b, SeoInput as c };
@@ -1,7 +1,6 @@
1
1
  import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
- import { c as checkLinks, e as externalLinks } from './ginko.CrNRBkS5.mjs';
4
- import { m as msg } from './ginko.C-QjDn3O.mjs';
3
+ import { c as checkLinks, e as externalLinks, m as msg } from './ginko.BNVC-g9n.mjs';
5
4
 
6
5
  const TIMEOUT_MS = 8e3;
7
6
  const CONCURRENCY = 4;
@@ -1,9 +1,48 @@
1
- import { m as msg, b as stableJson, c as bare, e as sameLabel, i as isTranslatable, g as effectiveMax, h as effectiveMin, j as parseMarkdown, k as sha256, l as effectiveAlt } from './ginko.C-QjDn3O.mjs';
2
- import { B as BRICK_KEYS, F as FIELD_KEYS, S as SCHEMA_KEYS, r as resolveRouteLinks } from './ginko.CrNRBkS5.mjs';
3
- import { a as addDays } from './ginko.CQvxhyez.mjs';
1
+ import { i as isTranslatable, A as ALT_MAX, g as effectiveMax, m as msg, h as stableJson, j as bare, k as sameLabel, l as effectiveMin, n as sha256, r as resolveRouteLinks, o as resolveFileRefs, q as effectiveAlt } from './ginko.BNVC-g9n.mjs';
2
+ import { B as BRICK_KEYS, F as FIELD_KEYS, S as SCHEMA_KEYS } from './ginko.B1-DoOea.mjs';
3
+ import { b as addDays } from './ginko.C-oz8wwI.mjs';
4
+ import { B as BUNDLE_FORMAT, w as withMarkdownTrees } from './ginko.yKMIjJ68.mjs';
5
+
6
+ const SITE_BUDGET = { siteBytes: 4 * 1024 * 1024, entries: 2e3, entryBytes: 480 * 1024, treeBytes: 8 * 1024 * 1024, trashBytes: 4 * 1024 * 1024 };
7
+ function byteSize(value) {
8
+ return new TextEncoder().encode(JSON.stringify(value ?? null)).byteLength;
9
+ }
10
+ function entryBytes(data, prints = {}) {
11
+ return byteSize(data) + byteSize(prints);
12
+ }
13
+ function fitsBudget(after, before, limit) {
14
+ return after <= limit || after <= before;
15
+ }
16
+ const VALUE_JSON = 8;
17
+ const ID_BYTES = 40;
18
+ const SHORT_VALUE = 32;
19
+ function largestFieldBytes(field, locales) {
20
+ if (field.retired) return 0;
21
+ const values = isTranslatable(field) ? locales.length : 1;
22
+ switch (field.type) {
23
+ case "text":
24
+ case "longtext":
25
+ case "markdown":
26
+ return (effectiveMax(field) + VALUE_JSON) * values;
27
+ case "list":
28
+ return field.max * (largestEntryBytes(field.of, locales) + ID_BYTES);
29
+ case "choices":
30
+ return effectiveMax(field) * ID_BYTES;
31
+ case "ref":
32
+ return field.multiple ? (field.max ?? 0) * ID_BYTES : ID_BYTES;
33
+ case "image":
34
+ case "file":
35
+ return ID_BYTES + ALT_MAX * locales.length;
36
+ default:
37
+ return SHORT_VALUE * values;
38
+ }
39
+ }
40
+ function largestEntryBytes(fields, locales) {
41
+ return Object.values(fields).reduce((sum, field) => sum + largestFieldBytes(field, locales), 0);
42
+ }
4
43
 
5
44
  const fx = (key) => msg(`diff.fix.${key}`);
6
- const NEW_FIELD_FIX = fx("newField");
45
+ const NEW_FIELD_FIX = /* @__PURE__ */ fx("newField");
7
46
  function diffSchema(prod, next) {
8
47
  const changes = [];
9
48
  const rejected = [];
@@ -28,6 +67,7 @@ function diffSchema(prod, next) {
28
67
  }
29
68
  for (const k of ["name", "url"]) if (prod[k] !== next[k]) ok(k, k, msg("diff.valueChanged", { name: k }));
30
69
  if (stableJson(prod.week) !== stableJson(next.week)) ok("week", "week", msg("diff.valueChanged", { name: "week" }));
70
+ if ((prod.holidays ?? null) !== (next.holidays ?? null)) ok("holidays", "holidays", msg("diff.valueChanged", { name: "holidays" }));
31
71
  if (stableJson(prod.pages) !== stableJson(next.pages)) ok("pages", "pages", msg("diff.pagesChanged"));
32
72
  if (stableJson(prod.agents ?? null) !== stableJson(next.agents ?? null)) ok("agents", "agents", msg("diff.agentsChanged"));
33
73
  for (const [path, rule] of Object.entries(prod.retired)) {
@@ -92,6 +132,8 @@ function diffBrick(key, before, after, newFills, newLocales, ok, no) {
92
132
  if (!sameLabel(before.describe ?? "", after.describe ?? "")) ok(`${key}.describe`, "text", msg("diff.textChanged"));
93
133
  if ((before.layout ?? "sections") !== (after.layout ?? "sections")) ok(`${key}.layout`, "text", msg("diff.textChanged"));
94
134
  if (stableJson(before.tasks ?? []) !== stableJson(after.tasks ?? [])) ok(`${key}.tasks`, "tasks", msg("diff.tasksChanged"));
135
+ if (stableJson(before.planned ?? null) !== stableJson(after.planned ?? null)) ok(`${key}.planned`, "planned", msg("diff.valueChanged", { name: "planned" }));
136
+ if (stableJson(before.status ?? null) !== stableJson(after.status ?? null)) ok(`${key}.status`, "text", msg("diff.textChanged"));
95
137
  if (before.kind === "collection" && after.kind === "collection") {
96
138
  if (after.max < before.max) no(`${key}.max`, "maxLowered", msg("diff.maxLowered", { at: key }), fx("keepMax"));
97
139
  else if (after.max > before.max) ok(`${key}.max`, "maxRaised", msg("diff.maxRaised"));
@@ -158,7 +200,7 @@ function diffField(at, old, next, newFills, ok, no) {
158
200
  if (onGrid) ok(at, "minLowered", msg("diff.minLowered"));
159
201
  else no(at, "minOffGrid", msg("diff.minOffGrid", { at, step }), fx("minOnGrid"));
160
202
  }
161
- for (const k of ["step", "format", "currency", "from", "accept", "to", "multiple"]) {
203
+ for (const k of ["step", "format", "currency", "from", "accept", "to", "multiple", "after"]) {
162
204
  if (stableJson(o[k] ?? null) !== stableJson(n[k] ?? null)) no(at, `${k}Changed`, msg("diff.propChanged", { at, prop: k }));
163
205
  }
164
206
  if ((old.type === "choice" || old.type === "choices") && (next.type === "choice" || next.type === "choices")) {
@@ -179,7 +221,7 @@ function diffField(at, old, next, newFills, ok, no) {
179
221
  }
180
222
  }
181
223
 
182
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
224
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
183
225
  function releaseData(stored, next, data, written) {
184
226
  const out = { ...data };
185
227
  for (const [name, field] of Object.entries(next)) {
@@ -193,7 +235,7 @@ function releaseData(stored, next, data, written) {
193
235
  const columns = Object.entries(field.of).filter(([column, inner]) => !Object.hasOwn(old.of, column) && !inner.optional && !inner.retired && inner.default !== void 0);
194
236
  if (!columns.length) continue;
195
237
  out[name] = out[name].map((row) => {
196
- if (!isRecord$1(row)) return row;
238
+ if (!isRecord(row)) return row;
197
239
  const filled = { ...row };
198
240
  for (const [column, inner] of columns) if (filled[column] === void 0) filled[column] = inner.default;
199
241
  return filled;
@@ -209,81 +251,9 @@ function fieldsChanged(stored, next, brick) {
209
251
  return !!before && !!after && stableJson(before.fields) !== stableJson(after.fields);
210
252
  }
211
253
 
212
- const BUNDLE_FORMAT = 3;
213
- function isBundle(value) {
214
- if (!isRecord(value)) return false;
215
- const b = value;
216
- if (b.format !== BUNDLE_FORMAT || typeof b.site !== "string" || typeof b.locale !== "string") return false;
217
- if (!Number.isInteger(b.schemaRev) || !Number.isInteger(b.contentRev) || !isRecord(b.bricks)) return false;
218
- return Object.values(b.bricks).every(isBundleBrick);
219
- }
220
- function knownBricks(bundle, schema) {
221
- return { ...bundle, bricks: Object.fromEntries(Object.entries(bundle.bricks).filter(([key]) => Object.hasOwn(schema.bricks, key))) };
222
- }
223
- function withFileBricks(platform, files, schema) {
224
- const base = platform ?? files;
225
- if (!base) throw new Error("withFileBricks: no bundle");
226
- const fromPlatform = Object.entries(platform?.bricks ?? {}).filter(([key]) => schema.bricks[key]?.source !== "files");
227
- return { ...base, bricks: { ...Object.fromEntries(fromPlatform), ...files?.bricks } };
228
- }
229
- async function markdownTrees(fields, data) {
230
- const trees = {};
231
- const parse = async (text) => (await parseMarkdown(text)).value;
232
- const walk = async (inner, row, path) => {
233
- for (const [name, field] of Object.entries(inner)) {
234
- const value = row[name];
235
- const leaf = path ? `${path}.${name}` : name;
236
- if (field.type === "markdown" && typeof value === "string") trees[leaf] = await parse(value);
237
- else if (field.type === "markdown" && isRecord(value)) {
238
- const perLocale = {};
239
- for (const [locale, text] of Object.entries(value)) if (typeof text === "string") perLocale[locale] = await parse(text);
240
- trees[leaf] = perLocale;
241
- } else if (field.type === "list" && Array.isArray(value)) {
242
- for (const r of value) if (isRecord(r) && typeof r._id === "string") await walk(field.of, r, `${leaf}.${r._id}`);
243
- }
244
- }
245
- };
246
- await walk(fields, data, "");
247
- return trees;
248
- }
249
- async function withMarkdownTrees(bundle, schema) {
250
- const fill = async (fields, data) => {
251
- const out = { ...data };
252
- for (const [key, field] of Object.entries(fields)) {
253
- const value = out[key];
254
- if (field.type === "markdown" && typeof value === "string") out[key] = (await parseMarkdown(value)).value;
255
- else if (field.type === "list" && Array.isArray(value)) out[key] = await Promise.all(value.map((row) => isRecord(row) ? fill(field.of, row) : row));
256
- }
257
- return out;
258
- };
259
- const bricks = {};
260
- for (const [key, brick] of Object.entries(bundle.bricks)) {
261
- const fields = Object.hasOwn(schema.bricks, key) ? schema.bricks[key].fields : void 0;
262
- if (!fields) bricks[key] = brick;
263
- else if (brick.kind === "single") bricks[key] = { ...brick, data: await fill(fields, brick.data) };
264
- else bricks[key] = { ...brick, items: await Promise.all(brick.items.map(async (item) => ({ ...item, data: await fill(fields, item.data) }))) };
265
- }
266
- return { ...bundle, bricks };
254
+ function untranslatedMode(brick) {
255
+ return brick.untranslated ?? (brick.route ? "hide" : "fallback");
267
256
  }
268
- function bundleFits(bundle, schema) {
269
- return Object.entries(schema.bricks).every(([key, brick]) => {
270
- const have = Object.hasOwn(bundle.bricks, key) ? bundle.bricks[key] : void 0;
271
- return have === void 0 || have.kind === (brick.kind === "single" ? "single" : "collection");
272
- });
273
- }
274
- function isBundleBrick(brick) {
275
- if (!isRecord(brick)) return false;
276
- if (brick.kind === "single") return isRecord(brick.data) && isStrings(brick.fallback);
277
- if (brick.kind !== "collection" || !Array.isArray(brick.items)) return false;
278
- return brick.items.every((item) => isRecord(item) && typeof item.id === "string" && isRecord(item.data) && isStrings(item.fallback) && (item.urls === void 0 || isRecord(item.urls) && Object.values(item.urls).every((u) => typeof u === "string")) && (item.updatedAt === void 0 || typeof item.updatedAt === "string") && (item.source === void 0 || typeof item.source === "string"));
279
- }
280
- function isStrings(value) {
281
- return Array.isArray(value) && value.every((v) => typeof v === "string");
282
- }
283
- function isRecord(value) {
284
- return typeof value === "object" && value !== null && !Array.isArray(value);
285
- }
286
-
287
257
  function printOf(mainValue) {
288
258
  return sha256(stableJson(mainValue ?? null));
289
259
  }
@@ -291,8 +261,9 @@ function resolveEntry(schema, brickKey, entry, ctx) {
291
261
  const brick = schema.bricks[brickKey];
292
262
  const main = schema.locales[0];
293
263
  const fallback = [];
294
- const flags = { hidden: false, canHide: brick.kind === "collection" && brick.untranslated === "hide" };
295
- const state = { schema, brickKey, entry, ctx, main, fallback, flags };
264
+ const outdated = [];
265
+ const blocking = [];
266
+ const state = { schema, brickKey, entry, ctx, main, fallback, outdated, blocking };
296
267
  const data = resolveFields(brick.fields, entry.data, "", state);
297
268
  for (const [name, field] of Object.entries(brick.fields)) {
298
269
  if (!field.retired) continue;
@@ -303,12 +274,13 @@ function resolveEntry(schema, brickKey, entry, ctx) {
303
274
  if (leaf === rule.from || leaf.startsWith(`${rule.from}.`)) fallback.push(name + leaf.slice(rule.from.length));
304
275
  }
305
276
  } else if (entry.data[name] !== void 0) {
306
- data[name] = resolveValue(field, entry.data[name], name, { ...state, flags: { hidden: false, canHide: false } });
277
+ data[name] = resolveValue(field, entry.data[name], name, { ...state, blocking: [], outdated: [] });
307
278
  } else if ("value" in rule) {
308
- data[name] = resolveValue(field, rule.value, name, { ...state, flags: { hidden: false, canHide: false } });
279
+ data[name] = resolveValue(field, rule.value, name, { ...state, blocking: [], outdated: [] });
309
280
  }
310
281
  }
311
- return { data, fallback, hidden: flags.hidden };
282
+ const hidden = blocking.length > 0 && brick.kind === "collection" && untranslatedMode(brick) === "hide";
283
+ return { data, fallback, outdated, blocking, hidden };
312
284
  }
313
285
  function resolveFields(fields, stored, path, state) {
314
286
  const out = {};
@@ -362,8 +334,9 @@ function altOf(own, library, leaf, state, field) {
362
334
  const fromLibrary = (l) => effectiveAlt(map, void 0, l) ? "" : effectiveAlt(void 0, library, l);
363
335
  if (!markAllFallback && locale !== state.main && fromLibrary(locale)) return fromLibrary(locale);
364
336
  const main = fromLibrary(state.main);
365
- return localized(main ? { ...map, [state.main]: main } : map, leaf, state, field) ?? "";
337
+ return localized(main ? { ...map, [state.main]: main } : map, leaf, { ...state, blocking: [] }, field) ?? "";
366
338
  }
339
+ const isEmpty = (value) => value === void 0 || value === null || value === "";
367
340
  function localized(value, leaf, state, field) {
368
341
  const map = value ?? {};
369
342
  const mainValue = map[state.main];
@@ -374,14 +347,16 @@ function localized(value, leaf, state, field) {
374
347
  }
375
348
  if (locale === state.main) return mainValue;
376
349
  const own = map[locale];
377
- const missing = own === void 0 || own === null || own === "";
378
- const print = state.entry.prints?.[leaf]?.[locale];
379
- const stale = !missing && field.type !== "slug" && print !== printOf(mainValue);
380
- if (missing || stale) {
381
- if (mainValue !== void 0 && mainValue !== null && mainValue !== "") state.fallback.push(leaf);
382
- if (state.flags.canHide && !field.optional) state.flags.hidden = true;
350
+ if (isEmpty(own)) {
351
+ if (!isEmpty(mainValue)) {
352
+ state.fallback.push(leaf);
353
+ if (!field.optional && field.type !== "slug") state.blocking.push(leaf);
354
+ }
383
355
  return mainValue;
384
356
  }
357
+ if (field.type === "slug" || state.entry.prints?.[leaf]?.[locale] === printOf(mainValue)) return own;
358
+ if (isEmpty(mainValue)) return mainValue;
359
+ state.outdated.push(leaf);
385
360
  return own;
386
361
  }
387
362
  function defaultsFor(fields) {
@@ -401,13 +376,19 @@ function visibleUntil(brick, data) {
401
376
  return typeof last === "string" ? addDays(last, vis.days ?? 0) : void 0;
402
377
  }
403
378
  const deletesExpired = (brick) => brick.kind === "collection" && brick.visible?.mode === "until" && brick.visible.deleteExpired === true;
404
- function sortEntries(brick, entries) {
379
+ function sortEntries(brick, entries, languages) {
405
380
  const copy = [...entries];
406
381
  if (brick.order === "manual") return copy.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id));
407
382
  const { field, dir } = brick.order;
408
383
  const sign = dir === "desc" ? -1 : 1;
384
+ const def = brick.fields[field];
385
+ const translated = def !== void 0 && isTranslatable(def);
409
386
  const key = (e) => {
410
- const v = e.data[field];
387
+ let v = e.data[field];
388
+ if (translated && languages && typeof v === "object" && v !== null && !Array.isArray(v)) {
389
+ const map = v;
390
+ v = isEmpty(map[languages.locale]) ? map[languages.main] : map[languages.locale];
391
+ }
411
392
  if (v === void 0 || v === null) return [];
412
393
  if (typeof v === "number") return [v];
413
394
  if (typeof v === "object" && !Array.isArray(v) && "amount" in v) return [Number(v.amount)];
@@ -426,7 +407,10 @@ function sortEntries(brick, entries) {
426
407
  return copy.sort((a, b) => sign * compare(key(a), key(b)) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
427
408
  }
428
409
  async function servedBundle(input) {
429
- return resolveRouteLinks(await withMarkdownTrees(buildBundle(input), input.schema), input.schema);
410
+ const bundle = buildBundle(input);
411
+ const main = bundle.locale === bundle.mainLocale ? void 0 : buildBundle({ ...input, requestedLocale: bundle.mainLocale });
412
+ const trees = await withMarkdownTrees(bundle, input.schema);
413
+ return resolveRouteLinks(resolveFileRefs(trees, input.schema, input.files), input.schema, main);
430
414
  }
431
415
  function buildBundle(input) {
432
416
  const { schema } = input;
@@ -444,7 +428,7 @@ function buildBundle(input) {
444
428
  continue;
445
429
  }
446
430
  const items = [];
447
- for (const entry of sortEntries(brick, stored)) {
431
+ for (const entry of sortEntries(brick, stored, { locale, main })) {
448
432
  const r = resolveEntry(schema, key, entry, ctx);
449
433
  if (r.hidden) continue;
450
434
  const item = { id: entry.id, data: r.data, fallback: r.fallback };
@@ -453,7 +437,7 @@ function buildBundle(input) {
453
437
  if (entry.updatedAt !== void 0) item.updatedAt = entry.updatedAt;
454
438
  const source = entry.sources?.[locale] ?? entry.sources?.[main];
455
439
  if (source !== void 0) item.source = source;
456
- if (brick.route && isVisible(brick, r.data, input.today)) item.urls = urlsFor(schema, key, brick, entry, input.files);
440
+ if (brick.route && isVisible(brick, r.data, input.today)) item.urls = urlsFor(schema, key, brick, entry);
457
441
  items.push(item);
458
442
  }
459
443
  bricks[key] = { kind: "collection", items };
@@ -472,20 +456,27 @@ function buildBundle(input) {
472
456
  bricks
473
457
  };
474
458
  }
475
- function urlsFor(schema, key, brick, entry, files) {
459
+ function routeUrls(schema, brick, entry) {
476
460
  const slugField = Object.entries(brick.fields).find(([, field]) => field.type === "slug" && !field.retired)?.[0];
477
461
  if (!slugField || !brick.route) return {};
478
462
  const slugs = entry.data[slugField] ?? {};
479
463
  const main = schema.locales[0];
480
464
  const urls = {};
481
465
  for (const locale of schema.locales) {
482
- if (locale !== main && brick.untranslated === "hide" && resolveEntry(schema, key, entry, { locale, files }).hidden) continue;
483
466
  const slug = slugs[locale] || slugs[main];
484
467
  const route = brick.route[locale];
485
468
  if (slug && route) urls[locale] = route.replace("[slug]", slug);
486
469
  }
487
470
  return urls;
488
471
  }
472
+ function urlsFor(schema, key, brick, entry) {
473
+ const urls = routeUrls(schema, brick, entry);
474
+ const main = schema.locales[0];
475
+ for (const locale of schema.locales) {
476
+ if (locale !== main && urls[locale] && resolveEntry(schema, key, entry, { locale, files: () => void 0 }).blocking.length) delete urls[locale];
477
+ }
478
+ return urls;
479
+ }
489
480
  function urlClashes(entries) {
490
481
  const owner = /* @__PURE__ */ new Map();
491
482
  const clashes = [];
@@ -541,4 +532,40 @@ function applyDefaults(bundle, schema) {
541
532
  return { ...bundle, bricks };
542
533
  }
543
534
 
544
- export { BUNDLE_FORMAT as B, applyDefaults as a, blockedSlugs as b, buildBundle as c, bundleFits as d, defaultsFor as e, deletesExpired as f, diffSchema as g, fieldsChanged as h, isBundle as i, isVisible as j, knownBricks as k, resolveEntry as l, markdownTrees as m, sortEntries as n, urlsFor as o, printOf as p, releaseData as r, servedBundle as s, urlClashes as u, visibleUntil as v, withFileBricks as w };
535
+ class CliError extends Error {
536
+ code;
537
+ constructor(message, code = "INVALID") {
538
+ super(message);
539
+ this.code = code;
540
+ }
541
+ }
542
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
543
+ function parseReleaseState(value) {
544
+ if (!isObject(value)) throw new CliError("The platform did not return a valid state.", "UNAVAILABLE");
545
+ const { site, schema, schemaRev, contentRev, migrations, entries } = value;
546
+ const valid = typeof site === "string" && isObject(schema) && isObject(schema.bricks) && Array.isArray(schema.migrations) && isObject(schema.retired) && typeof schemaRev === "number" && typeof contentRev === "number" && Array.isArray(migrations) && Array.isArray(entries) && entries.every((e) => isObject(e) && typeof e.id === "string" && typeof e.brick === "string" && isObject(e.data));
547
+ if (!valid) throw new CliError("The platform returned an incomplete state.", "UNAVAILABLE");
548
+ return value;
549
+ }
550
+ const EXTENSIONS = {
551
+ "image/jpeg": ".jpg",
552
+ "image/png": ".png",
553
+ "image/webp": ".webp",
554
+ "image/avif": ".avif",
555
+ "application/pdf": ".pdf"
556
+ };
557
+ function defaultDir(site, now) {
558
+ return `ginko-export-${site}-${now.toISOString().slice(0, 10)}`;
559
+ }
560
+ function pathOf(file) {
561
+ return `files/${file.id}${EXTENSIONS[file.mime] ?? ""}`;
562
+ }
563
+ function parseExport(value, locator, problem) {
564
+ const optionalNumber = (v) => v === void 0 || typeof v === "number";
565
+ const optionalString = (v) => v === void 0 || typeof v === "string";
566
+ const valid = isObject(value) && value.format === 1 && typeof value.site === "string" && typeof value.name === "string" && typeof value.exportedAt === "string" && isObject(value.schema) && isObject(value.schema.bricks) && typeof value.schemaRev === "number" && typeof value.contentRev === "number" && Array.isArray(value.entries) && value.entries.every((e) => isObject(e) && typeof e.id === "string" && typeof e.brick === "string" && (e.kind === "single" || e.kind === "item") && typeof e.order === "number" && isObject(e.data) && isObject(e.prints) && typeof e.rev === "number" && optionalNumber(e.deletedAt)) && Array.isArray(value.files) && value.files.every((f) => isObject(f) && typeof f.id === "string" && typeof f.name === "string" && typeof f.mime === "string" && typeof f.size === "number" && typeof f[locator] === "string" && optionalNumber(f.width) && optionalNumber(f.height) && optionalString(f.folder) && optionalString(f.sha256) && (f.kept === void 0 || typeof f.kept === "boolean") && (f.original === void 0 || f.original === true) && (f.alt === void 0 || isObject(f.alt) && Object.values(f.alt).every((text) => typeof text === "string")) && (f.focus === void 0 || isObject(f.focus) && typeof f.focus.x === "number" && typeof f.focus.y === "number")) && (value.folders === void 0 || Array.isArray(value.folders) && value.folders.every((f) => isObject(f) && typeof f.id === "string" && typeof f.name === "string" && optionalString(f.parent)));
567
+ if (!valid) throw new CliError(problem, "UNAVAILABLE" );
568
+ return value;
569
+ }
570
+
571
+ export { urlClashes as A, urlsFor as B, CliError as C, SITE_BUDGET as S, fitsBudget as a, diffSchema as b, largestFieldBytes as c, defaultsFor as d, entryBytes as e, fieldsChanged as f, parseReleaseState as g, pathOf as h, isObject as i, defaultDir as j, isVisible as k, largestEntryBytes as l, printOf as m, applyDefaults as n, blockedSlugs as o, parseExport as p, buildBundle as q, releaseData as r, byteSize as s, deletesExpired as t, resolveEntry as u, visibleUntil as v, routeUrls as w, servedBundle as x, sortEntries as y, untranslatedMode as z };
@@ -0,0 +1,84 @@
1
+ import { a as parseMarkdown } from './ginko.BNVC-g9n.mjs';
2
+
3
+ const BUNDLE_FORMAT = 3;
4
+ function entryTitle(item, fields) {
5
+ if (typeof item.data.title === "string") return item.data.title;
6
+ const slug = Object.values(fields ?? {}).find((field) => field.type === "slug");
7
+ const named = slug?.type === "slug" ? item.data[slug.from] : void 0;
8
+ return typeof named === "string" && named.trim() ? named : item.id;
9
+ }
10
+ function isBundle(value) {
11
+ if (!isRecord(value)) return false;
12
+ const b = value;
13
+ if (b.format !== BUNDLE_FORMAT || typeof b.site !== "string" || typeof b.locale !== "string") return false;
14
+ if (!Number.isInteger(b.schemaRev) || !Number.isInteger(b.contentRev) || !isRecord(b.bricks)) return false;
15
+ return Object.values(b.bricks).every(isBundleBrick);
16
+ }
17
+ function knownBricks(bundle, schema) {
18
+ return { ...bundle, bricks: Object.fromEntries(Object.entries(bundle.bricks).filter(([key]) => Object.hasOwn(schema.bricks, key))) };
19
+ }
20
+ function withFileBricks(platform, files, schema) {
21
+ const base = platform ?? files;
22
+ if (!base) throw new Error("withFileBricks: no bundle");
23
+ const fromPlatform = Object.entries(platform?.bricks ?? {}).filter(([key]) => schema.bricks[key]?.source !== "files");
24
+ return { ...base, bricks: { ...Object.fromEntries(fromPlatform), ...files?.bricks } };
25
+ }
26
+ async function markdownTrees(fields, data) {
27
+ const trees = {};
28
+ const parse = async (text) => (await parseMarkdown(text)).value;
29
+ const walk = async (inner, row, path) => {
30
+ for (const [name, field] of Object.entries(inner)) {
31
+ const value = row[name];
32
+ const leaf = path ? `${path}.${name}` : name;
33
+ if (field.type === "markdown" && typeof value === "string") trees[leaf] = await parse(value);
34
+ else if (field.type === "markdown" && isRecord(value)) {
35
+ const perLocale = {};
36
+ for (const [locale, text] of Object.entries(value)) if (typeof text === "string") perLocale[locale] = await parse(text);
37
+ trees[leaf] = perLocale;
38
+ } else if (field.type === "list" && Array.isArray(value)) {
39
+ for (const r of value) if (isRecord(r) && typeof r._id === "string") await walk(field.of, r, `${leaf}.${r._id}`);
40
+ }
41
+ }
42
+ };
43
+ await walk(fields, data, "");
44
+ return trees;
45
+ }
46
+ async function withMarkdownTrees(bundle, schema) {
47
+ const fill = async (fields, data) => {
48
+ const out = { ...data };
49
+ for (const [key, field] of Object.entries(fields)) {
50
+ const value = out[key];
51
+ if (field.type === "markdown" && typeof value === "string") out[key] = (await parseMarkdown(value)).value;
52
+ else if (field.type === "list" && Array.isArray(value)) out[key] = await Promise.all(value.map((row) => isRecord(row) ? fill(field.of, row) : row));
53
+ }
54
+ return out;
55
+ };
56
+ const bricks = {};
57
+ for (const [key, brick] of Object.entries(bundle.bricks)) {
58
+ const fields = Object.hasOwn(schema.bricks, key) ? schema.bricks[key].fields : void 0;
59
+ if (!fields) bricks[key] = brick;
60
+ else if (brick.kind === "single") bricks[key] = { ...brick, data: await fill(fields, brick.data) };
61
+ else bricks[key] = { ...brick, items: await Promise.all(brick.items.map(async (item) => ({ ...item, data: await fill(fields, item.data) }))) };
62
+ }
63
+ return { ...bundle, bricks };
64
+ }
65
+ function bundleFits(bundle, schema) {
66
+ return Object.entries(schema.bricks).every(([key, brick]) => {
67
+ const have = Object.hasOwn(bundle.bricks, key) ? bundle.bricks[key] : void 0;
68
+ return have === void 0 || have.kind === (brick.kind === "single" ? "single" : "collection");
69
+ });
70
+ }
71
+ function isBundleBrick(brick) {
72
+ if (!isRecord(brick)) return false;
73
+ if (brick.kind === "single") return isRecord(brick.data) && isStrings(brick.fallback);
74
+ if (brick.kind !== "collection" || !Array.isArray(brick.items)) return false;
75
+ return brick.items.every((item) => isRecord(item) && typeof item.id === "string" && isRecord(item.data) && isStrings(item.fallback) && (item.urls === void 0 || isRecord(item.urls) && Object.values(item.urls).every((u) => typeof u === "string")) && (item.updatedAt === void 0 || typeof item.updatedAt === "string") && (item.source === void 0 || typeof item.source === "string"));
76
+ }
77
+ function isStrings(value) {
78
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
79
+ }
80
+ function isRecord(value) {
81
+ return typeof value === "object" && value !== null && !Array.isArray(value);
82
+ }
83
+
84
+ export { BUNDLE_FORMAT as B, withFileBricks as a, bundleFits as b, entryTitle as e, isBundle as i, knownBricks as k, markdownTrees as m, withMarkdownTrees as w };
package/dist/vue.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType, Component } from 'vue';
3
- import { MarkdownTree, ComponentRegistry } from './index.mjs';
3
+ import { MarkdownTree } from './index.mjs';
4
+ import { AttributeRegistry } from './internal.mjs';
4
5
  import '@lupinum/ginko-i18n/core';
5
6
  import 'comark';
6
7
 
@@ -16,7 +17,7 @@ declare const GinkoMarkdown: vue.DefineComponent<vue.ExtractPropTypes<{
16
17
  };
17
18
  /** The site's component registry (`components` in ginko.config.ts): a component keeps the props it declares there. Without it a component gets only `title`, `lang`, `dir` and `aria-*`. */
18
19
  registry: {
19
- type: PropType<ComponentRegistry>;
20
+ type: PropType<AttributeRegistry>;
20
21
  default: undefined;
21
22
  };
22
23
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
@@ -33,13 +34,13 @@ declare const GinkoMarkdown: vue.DefineComponent<vue.ExtractPropTypes<{
33
34
  };
34
35
  /** The site's component registry (`components` in ginko.config.ts): a component keeps the props it declares there. Without it a component gets only `title`, `lang`, `dir` and `aria-*`. */
35
36
  registry: {
36
- type: PropType<ComponentRegistry>;
37
+ type: PropType<AttributeRegistry>;
37
38
  default: undefined;
38
39
  };
39
40
  }>> & Readonly<{}>, {
40
41
  value: MarkdownTree | null | undefined;
41
42
  components: Record<string, string | Component>;
42
- registry: ComponentRegistry;
43
+ registry: AttributeRegistry;
43
44
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
44
45
 
45
46
  export { GinkoMarkdown };
package/dist/vue.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType, Component } from 'vue';
3
- import { MarkdownTree, ComponentRegistry } from './index.js';
3
+ import { MarkdownTree } from './index.js';
4
+ import { AttributeRegistry } from './internal.js';
4
5
  import '@lupinum/ginko-i18n/core';
5
6
  import 'comark';
6
7
 
@@ -16,7 +17,7 @@ declare const GinkoMarkdown: vue.DefineComponent<vue.ExtractPropTypes<{
16
17
  };
17
18
  /** The site's component registry (`components` in ginko.config.ts): a component keeps the props it declares there. Without it a component gets only `title`, `lang`, `dir` and `aria-*`. */
18
19
  registry: {
19
- type: PropType<ComponentRegistry>;
20
+ type: PropType<AttributeRegistry>;
20
21
  default: undefined;
21
22
  };
22
23
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
@@ -33,13 +34,13 @@ declare const GinkoMarkdown: vue.DefineComponent<vue.ExtractPropTypes<{
33
34
  };
34
35
  /** The site's component registry (`components` in ginko.config.ts): a component keeps the props it declares there. Without it a component gets only `title`, `lang`, `dir` and `aria-*`. */
35
36
  registry: {
36
- type: PropType<ComponentRegistry>;
37
+ type: PropType<AttributeRegistry>;
37
38
  default: undefined;
38
39
  };
39
40
  }>> & Readonly<{}>, {
40
41
  value: MarkdownTree | null | undefined;
41
42
  components: Record<string, string | Component>;
42
- registry: ComponentRegistry;
43
+ registry: AttributeRegistry;
43
44
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
44
45
 
45
46
  export { GinkoMarkdown };