@lupinum/ginko-content 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @lupinum/ginko-content
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`ee8c8a9`](https://github.com/lupinum-dev/ginko/commit/ee8c8a982a64ee15a1cab906b7a3b58fe8c686e6)]:
8
+ - @lupinum/ginko@0.6.0
9
+
10
+ ## 0.5.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [#23](https://github.com/lupinum-dev/ginko/pull/23) [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Change multi-choice values in content files: `none` says there are none, and an empty list on a required field is an error, so deleting the values no longer publishes "none".
15
+
16
+ - [#23](https://github.com/lupinum-dev/ginko/pull/23) [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Add scheduled entries for file content: in a collection with drafts that lists its entries newest first by a date (such as `posts()` with `drafts: true`), an entry dated after the build day is left out of the build like a draft, and the first build on or after its date publishes it. The `blog` template feature relies on it (`templates/_features/blog`).
17
+
18
+ ### Patch Changes
19
+
20
+ - [#23](https://github.com/lupinum-dev/ginko/pull/23) [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Change the `ginko check` messages for content files: they name the key you typed, a list row by its place (`row 3`), and the format to write (euros, `YYYY-MM-DD`, `HH:MM`, a period, an entry id). A YAML error in Markdown frontmatter now names the file and the line.
21
+
22
+ - [#23](https://github.com/lupinum-dev/ginko/pull/23) [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Fix `ginko check` and the build hiding broken entries: a scheduling date that is not a valid `YYYY-MM-DD` date is now reported instead of leaving the entry out, a scheduled entry is checked like a published one, and a translation must have the same number of list rows as the main file
23
+
24
+ - [#23](https://github.com/lupinum-dev/ginko/pull/23) [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Change the message for an entry that exists twice: it names both files, so the owner sees the one they just added.
25
+
26
+ - [#21](https://github.com/lupinum-dev/ginko/pull/21) [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Fix the build keeping an entry whose required reference names a draft, such as the sessions of a draft event: it is now left out with the draft.
27
+
28
+ - [#21](https://github.com/lupinum-dev/ginko/pull/21) [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Change platform Markdown fields to `{ ref }` in page state, as file bricks already are, so a page's payload carries only the texts it shows.
29
+
30
+ Site code that reads a text's tree from an entry (`entry.data.body.nodes`) gets nothing now: load it with `useGinkoMarkdown(() => entry.value.data.body)` or, for several texts in one `useAsyncData`, `fetchGinkoMarkdown(value)`. `<GinkoMarkdown>` needs no change. A client navigation loads a text it has not shown yet from `/__ginko/platform/<locale>/<rev>/md/…`.
31
+
32
+ - [#21](https://github.com/lupinum-dev/ginko/pull/21) [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Fix the silent loss of text after a block component without its closing `::` line: it is now refused with the line it starts on (Studio, agent tools, releases, `ginko check` and the build). Close every block component with a `::` line, also one at the very end of a text; for content files, `ginko check` and the build now fail and name the file and line.
33
+ - Updated dependencies [[`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`c28d1e3`](https://github.com/lupinum-dev/ginko/commit/c28d1e3e4ba1e558f8c71d7b69961f7474a221d1), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`7b165c2`](https://github.com/lupinum-dev/ginko/commit/7b165c25e17ed2f4fb50e8054febf618a6af10d7), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991), [`80f93a4`](https://github.com/lupinum-dev/ginko/commit/80f93a4f6bd171e1146b929913254039becdd991)]:
34
+ - @lupinum/ginko@0.5.0
35
+
3
36
  ## 0.4.0
4
37
 
5
38
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { Locale, MarkdownValue, MarkdownTree } from '@lupinum/ginko';
2
- import { Schema, Bundle } from '@lupinum/ginko/internal';
1
+ import { Schema, StoredEntry, FileInfo, Bundle } from '@lupinum/ginko/internal';
2
+ import { Locale, MarkdownTree } from '@lupinum/ginko';
3
3
  import { SearchIndexJson } from '@lupinum/ginko/search';
4
4
 
5
5
  interface ContentError {
@@ -7,6 +7,8 @@ interface ContentError {
7
7
  file: string;
8
8
  /** Field path as in validate() issues, when the error belongs to one field. */
9
9
  path?: string;
10
+ /** The place as the owner reads it (`exceptions, row 3, closed`), when it differs from `path` (row ids, 0-based indexes). */
11
+ display?: string;
10
12
  message: string;
11
13
  /** 1-based line in the file, when the error belongs to one (YAML, JSON and CSV parse errors, CSV rows). */
12
14
  line?: number;
@@ -17,16 +19,48 @@ interface ContentError {
17
19
  */
18
20
  declare function errorLine(e: ContentError): string;
19
21
  interface ReadOptions {
20
- /** `false` leaves out entries marked `draft: true`. Default `true` (dev shows drafts). */
22
+ /**
23
+ * `false` leaves out entries marked `draft: true`, and scheduled entries: in a collection with drafts that lists its
24
+ * entries newest first by a date (`drafts: true`, `order: { field, dir: 'desc' }`), an entry dated after `today`.
25
+ * Default `true` (dev shows both).
26
+ */
21
27
  drafts?: boolean;
28
+ /** The build day (`YYYY-MM-DD`) for scheduled entries. Default: today in the schema's time zone. */
29
+ today?: string;
30
+ /** `text` keeps each Markdown field as its source text, the form the platform stores and the Studio edits
31
+ * (`pnpm local:files-export`). Default `tree`: the parsed tree the site's bundle carries. */
32
+ markdown?: 'tree' | 'text';
22
33
  }
23
-
24
- /**
25
- * The bundle for page state: every markdown tree replaced by `{ ref, toc }` (CONTENT.md, Phase 14).
26
- * `trees` collects the trees by their ref, for the route that serves them.
27
- * @internal Loaded by the Ginko module and CLI; sites do not import it.
34
+ interface ReadResult {
35
+ /** Stored-format entries per file brick, ready for buildBundle. */
36
+ entries: Record<string, StoredEntry[]>;
37
+ /** Images and files referenced from frontmatter, by their path under `public/` (the stored `file` id). */
38
+ files: Record<string, FileInfo>;
39
+ /** Images placed next to Markdown files: served URL (`/_ginko/img/<hash>.<ext>`) to absolute path. */
40
+ images: Map<string, string>;
41
+ errors: ContentError[];
42
+ }
43
+ /** Reads every brick with `source: 'files'` from `contentDir` (layout and rules: CONTENT.md "Dateien").
44
+ *
45
+ * - Collection `docs`: `docs/<n>.<name>.md` (main locale) and `docs/<n>.<name>.<locale>.md`.
46
+ * Id `docs/<name>`, `order` = n (files without a number come after, by name); entries sorted by order. Slug from
47
+ * frontmatter `slug`, else `<name>`; a translation may set its own `slug`.
48
+ * - Single `legal`: `legal.md` and `legal.<locale>.md`. One entry, id `legal`.
49
+ * - Frontmatter keys are field names in simple form; the body fills the brick's markdown field.
50
+ * Translatable fields collect one locale per file into `{ de, en }`. Simple forms: money `12.5` →
51
+ * `{ amount: 1250 }`; image `{ src: '/img/a.jpg', alt: '…' }` → `{ file: '/img/a.jpg', alt: { de: '…' } }`
52
+ * (alt per locale file); file `'/docs/a.pdf'` → `{ file: '/docs/a.pdf' }`; choices `none` → `[]` (an empty list on a
53
+ * required field is an error, so "none" is never left behind by deleting values); list rows get `_id`
54
+ * `r` + 9 chars of sha256(`<entry id>:<row index>`) and translations merge by row index.
55
+ * - `prints` mark every translation as fresh (printOf of the main value), so a present translation
56
+ * is never a fallback.
57
+ * - Errors, never thrown: unknown frontmatter key, a locale suffix the site does not have, a
58
+ * translation without its main file, two entries with the same slug in one locale, a missing
59
+ * image or file under `publicDir`, and every validate() error of the built entry (mode 'create').
60
+ * A file with an error is left out; the others are still read.
28
61
  */
29
- declare function withMarkdownRefs(bundle: Bundle, schema: Schema, trees: Map<string, MarkdownValue>): Bundle;
62
+ declare function readContent(contentDir: string, publicDir: string, schema: Schema, options?: ReadOptions): Promise<ReadResult>;
63
+
30
64
  /**
31
65
  * The bundle a site serves to everyone (`/__ginko/files/*`, page state, search): items whose visibility window
32
66
  * ended before `now` are left out (an expired offer is not private data to publish), and the repo path
@@ -71,4 +105,4 @@ interface BuildResult {
71
105
  */
72
106
  declare function searchIndex(bundle: Bundle, trees: Map<string, MarkdownTree>, schema: Schema): Promise<SearchIndexJson>;
73
107
 
74
- export { buildFileBundles, errorLine, publicBundle, searchIndex, withMarkdownRefs };
108
+ export { buildFileBundles, errorLine, publicBundle, readContent, searchIndex };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Locale, MarkdownValue, MarkdownTree } from '@lupinum/ginko';
2
- import { Schema, Bundle } from '@lupinum/ginko/internal';
1
+ import { Schema, StoredEntry, FileInfo, Bundle } from '@lupinum/ginko/internal';
2
+ import { Locale, MarkdownTree } from '@lupinum/ginko';
3
3
  import { SearchIndexJson } from '@lupinum/ginko/search';
4
4
 
5
5
  interface ContentError {
@@ -7,6 +7,8 @@ interface ContentError {
7
7
  file: string;
8
8
  /** Field path as in validate() issues, when the error belongs to one field. */
9
9
  path?: string;
10
+ /** The place as the owner reads it (`exceptions, row 3, closed`), when it differs from `path` (row ids, 0-based indexes). */
11
+ display?: string;
10
12
  message: string;
11
13
  /** 1-based line in the file, when the error belongs to one (YAML, JSON and CSV parse errors, CSV rows). */
12
14
  line?: number;
@@ -17,16 +19,48 @@ interface ContentError {
17
19
  */
18
20
  declare function errorLine(e: ContentError): string;
19
21
  interface ReadOptions {
20
- /** `false` leaves out entries marked `draft: true`. Default `true` (dev shows drafts). */
22
+ /**
23
+ * `false` leaves out entries marked `draft: true`, and scheduled entries: in a collection with drafts that lists its
24
+ * entries newest first by a date (`drafts: true`, `order: { field, dir: 'desc' }`), an entry dated after `today`.
25
+ * Default `true` (dev shows both).
26
+ */
21
27
  drafts?: boolean;
28
+ /** The build day (`YYYY-MM-DD`) for scheduled entries. Default: today in the schema's time zone. */
29
+ today?: string;
30
+ /** `text` keeps each Markdown field as its source text, the form the platform stores and the Studio edits
31
+ * (`pnpm local:files-export`). Default `tree`: the parsed tree the site's bundle carries. */
32
+ markdown?: 'tree' | 'text';
22
33
  }
23
-
24
- /**
25
- * The bundle for page state: every markdown tree replaced by `{ ref, toc }` (CONTENT.md, Phase 14).
26
- * `trees` collects the trees by their ref, for the route that serves them.
27
- * @internal Loaded by the Ginko module and CLI; sites do not import it.
34
+ interface ReadResult {
35
+ /** Stored-format entries per file brick, ready for buildBundle. */
36
+ entries: Record<string, StoredEntry[]>;
37
+ /** Images and files referenced from frontmatter, by their path under `public/` (the stored `file` id). */
38
+ files: Record<string, FileInfo>;
39
+ /** Images placed next to Markdown files: served URL (`/_ginko/img/<hash>.<ext>`) to absolute path. */
40
+ images: Map<string, string>;
41
+ errors: ContentError[];
42
+ }
43
+ /** Reads every brick with `source: 'files'` from `contentDir` (layout and rules: CONTENT.md "Dateien").
44
+ *
45
+ * - Collection `docs`: `docs/<n>.<name>.md` (main locale) and `docs/<n>.<name>.<locale>.md`.
46
+ * Id `docs/<name>`, `order` = n (files without a number come after, by name); entries sorted by order. Slug from
47
+ * frontmatter `slug`, else `<name>`; a translation may set its own `slug`.
48
+ * - Single `legal`: `legal.md` and `legal.<locale>.md`. One entry, id `legal`.
49
+ * - Frontmatter keys are field names in simple form; the body fills the brick's markdown field.
50
+ * Translatable fields collect one locale per file into `{ de, en }`. Simple forms: money `12.5` →
51
+ * `{ amount: 1250 }`; image `{ src: '/img/a.jpg', alt: '…' }` → `{ file: '/img/a.jpg', alt: { de: '…' } }`
52
+ * (alt per locale file); file `'/docs/a.pdf'` → `{ file: '/docs/a.pdf' }`; choices `none` → `[]` (an empty list on a
53
+ * required field is an error, so "none" is never left behind by deleting values); list rows get `_id`
54
+ * `r` + 9 chars of sha256(`<entry id>:<row index>`) and translations merge by row index.
55
+ * - `prints` mark every translation as fresh (printOf of the main value), so a present translation
56
+ * is never a fallback.
57
+ * - Errors, never thrown: unknown frontmatter key, a locale suffix the site does not have, a
58
+ * translation without its main file, two entries with the same slug in one locale, a missing
59
+ * image or file under `publicDir`, and every validate() error of the built entry (mode 'create').
60
+ * A file with an error is left out; the others are still read.
28
61
  */
29
- declare function withMarkdownRefs(bundle: Bundle, schema: Schema, trees: Map<string, MarkdownValue>): Bundle;
62
+ declare function readContent(contentDir: string, publicDir: string, schema: Schema, options?: ReadOptions): Promise<ReadResult>;
63
+
30
64
  /**
31
65
  * The bundle a site serves to everyone (`/__ginko/files/*`, page state, search): items whose visibility window
32
66
  * ended before `now` are left out (an expired offer is not private data to publish), and the repo path
@@ -71,4 +105,4 @@ interface BuildResult {
71
105
  */
72
106
  declare function searchIndex(bundle: Bundle, trees: Map<string, MarkdownTree>, schema: Schema): Promise<SearchIndexJson>;
73
107
 
74
- export { buildFileBundles, errorLine, publicBundle, searchIndex, withMarkdownRefs };
108
+ export { buildFileBundles, errorLine, publicBundle, readContent, searchIndex };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, statSync, openSync, readSync, closeSync, existsSync, renameSync, mkdirSync } from 'node:fs';
2
2
  import { readFile, readdir } from 'node:fs/promises';
3
3
  import { resolve, sep, dirname, join, extname, basename } from 'node:path';
4
- import { imageInfo, parseMarkdown, validate, coreMessage, printOf, urlsFor, urlClashes, effectiveProfile, slugify, isTranslatable, sha256, resolveRouteLinks, buildBundle, todayIn, isVisible } from '@lupinum/ginko/internal';
4
+ import { imageInfo, parseMarkdown, todayIn, validate, printOf, routeUrls, urlClashes, effectiveProfile, coreMessage, isDate, slugify, isTranslatable, sha256, resolveRouteLinks, isVisible, buildBundle } from '@lupinum/ginko/internal';
5
5
  import { load } from 'js-yaml';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { execFile } from 'node:child_process';
@@ -81,7 +81,7 @@ function cellValue(fields, column, cell) {
81
81
  case "image":
82
82
  case "choices":
83
83
  case "daterange":
84
- return { error: `Column \u201C${column}\u201D: \u201C${field.label}\u201D cannot be written in CSV. Use a YAML or Markdown file for it.` };
84
+ return { error: `Column \u201C${column}\u201D cannot be written in CSV. Use a YAML or Markdown file for it.` };
85
85
  default:
86
86
  return { value: text };
87
87
  }
@@ -261,7 +261,7 @@ async function withPreviews(nodes, parse) {
261
261
  }
262
262
 
263
263
  function errorLine(e) {
264
- return `content/${e.file}${e.line !== void 0 ? `:${e.line}` : ""}${e.path ? ` (${e.path})` : ""}: ${e.message}`;
264
+ return `content/${e.file}${e.line !== void 0 ? `:${e.line}` : ""}${e.display ?? e.path ? ` (${e.display ?? e.path})` : ""}: ${e.message}`;
265
265
  }
266
266
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
267
267
  const extensions = ["md", "yml", "yaml", "json", "csv"];
@@ -275,8 +275,10 @@ function parseFileName(file) {
275
275
  return { name: rest.slice(0, dot), number: numbered ? Number(numbered[1]) : void 0, locale: rest.slice(dot + 1) };
276
276
  }
277
277
  const rowId = (entryId, index) => `r${sha256(`${entryId}:${index}`).slice(0, 9)}`;
278
+ const displayOf = (where, prefix, path) => [...where, path.slice(prefix.length)].join(", ");
278
279
  function fail(cx, path, message) {
279
- cx.errors.push({ file: cx.file, path, message, ...cx.line !== void 0 ? { line: cx.line } : {} });
280
+ const display = cx.where.length > 0 ? displayOf(cx.where, cx.prefix, path) : void 0;
281
+ cx.errors.push({ file: cx.file, path, ...display !== void 0 && display !== path ? { display } : {}, message, ...cx.line !== void 0 ? { line: cx.line } : {} });
280
282
  }
281
283
  function redirectPaths(cx, value) {
282
284
  const list = Array.isArray(value) ? value : [value];
@@ -311,6 +313,18 @@ function jsonErrorOffset(text, reason) {
311
313
  }
312
314
  return low;
313
315
  }
316
+ const isYamlError = (error) => error instanceof Error && error.name === "YAMLException";
317
+ function yamlError(error, text, file, lineShift) {
318
+ const { reason, mark } = error;
319
+ const why = reason ?? (error instanceof Error ? error.message : String(error)).split("\n")[0];
320
+ const line = typeof mark?.line === "number" ? mark.line + 1 + lineShift : void 0;
321
+ let hint = "";
322
+ if (why.startsWith("bad indentation of a mapping entry")) {
323
+ const slip = line === void 0 ? null : /^\s*([^:\s][^:]*):\s+([^:]*:)\s/.exec(text.split("\n")[line - 1] ?? "");
324
+ hint = slip ? ` Put text with a colon in quotes: ${slip[1]}: "${slip[2]} \u2026"` : " Put text with a colon in quotes.";
325
+ }
326
+ return { file, message: `Invalid YAML: ${why}.${hint}`, ...line !== void 0 ? { line } : {} };
327
+ }
314
328
  function parseDataFile(text, ext, rel, errors) {
315
329
  let data;
316
330
  try {
@@ -320,9 +334,7 @@ function parseDataFile(text, ext, rel, errors) {
320
334
  if (ext === "json") {
321
335
  errors.push({ file: rel, message: `Invalid JSON: ${reason}`, line: lineAt(text, jsonErrorOffset(text, reason)) });
322
336
  } else {
323
- const mark = error.mark;
324
- const why = error.reason ?? reason.split("\n")[0];
325
- errors.push({ file: rel, message: `Invalid YAML: ${why}`, ...typeof mark?.line === "number" ? { line: mark.line + 1 } : {} });
337
+ errors.push(yamlError(error, text, rel, 0));
326
338
  }
327
339
  return void 0;
328
340
  }
@@ -354,11 +366,11 @@ function convertFields(fields, data, cx) {
354
366
  continue;
355
367
  }
356
368
  if (field.type === "markdown") {
357
- fail(cx, path, `\u201C${field.label}\u201D is the text of the file, not a frontmatter key.`);
369
+ fail(cx, path, `\u201C${key}\u201D is the text of the file, not a frontmatter key.`);
358
370
  continue;
359
371
  }
360
372
  if (!cx.isMain && !isTranslatable(field) && field.type !== "list" && field.type !== "image") {
361
- fail(cx, path, `\u201C${field.label}\u201D is set in the main-language file only.`);
373
+ fail(cx, path, `\u201C${key}\u201D is set in the main-language file only.`);
362
374
  continue;
363
375
  }
364
376
  switch (field.type) {
@@ -371,13 +383,27 @@ function convertFields(fields, data, cx) {
371
383
  out[key] = { file: value };
372
384
  } else out[key] = value;
373
385
  break;
386
+ case "choices": {
387
+ if (value === "none") {
388
+ out[key] = field.options.some((o) => o.value === "none") ? ["none"] : [];
389
+ break;
390
+ }
391
+ if (Array.isArray(value) && value.length === 0 && !field.optional) {
392
+ const [first, second] = field.options.map((o) => o.value);
393
+ const example = second === void 0 ? `[${first}]` : `[${first}, ${second}]`;
394
+ fail(cx, path, `\u201C${key}\u201D is empty: write the values, like ${example}, or none when there are none.`);
395
+ break;
396
+ }
397
+ out[key] = value;
398
+ break;
399
+ }
374
400
  case "image": {
375
401
  if (!isRecord(value)) {
376
- fail(cx, path, `\u201C${field.label}\u201D needs src and alt.`);
402
+ fail(cx, path, `\u201C${key}\u201D needs src and alt.`);
377
403
  break;
378
404
  }
379
405
  if (!cx.isMain && value.src !== void 0) {
380
- fail(cx, path, `\u201C${field.label}\u201D: src is set in the main-language file only.`);
406
+ fail(cx, path, `\u201C${key}\u201D: src is set in the main-language file only.`);
381
407
  break;
382
408
  }
383
409
  if (typeof value.src === "string") checkPublicFile(cx, path, value.src);
@@ -388,10 +414,10 @@ function convertFields(fields, data, cx) {
388
414
  const only = Object.keys(field.of).length === 1 ? Object.keys(field.of)[0] : void 0;
389
415
  const rows = Array.isArray(value) && only !== void 0 ? value.map((row) => isRecord(row) ? row : { [only]: row }) : value;
390
416
  if (!Array.isArray(rows) || !rows.every(isRecord)) {
391
- fail(cx, path, `\u201C${field.label}\u201D must be a list of objects.`);
417
+ fail(cx, path, `\u201C${key}\u201D must be a list of objects.`);
392
418
  break;
393
419
  }
394
- out[key] = rows.map((row, i) => convertFields(field.of, row, { ...cx, prefix: `${path}.${i}.` }));
420
+ out[key] = rows.map((row, i) => convertFields(field.of, row, { ...cx, prefix: `${path}.${i}.`, where: [...cx.where, displayOf(cx.where, cx.prefix, path), `row ${i + 1}`] }));
395
421
  break;
396
422
  }
397
423
  default:
@@ -425,8 +451,10 @@ function merge(fields, views, cx) {
425
451
  } else if (field.type === "list") {
426
452
  const mainRows = rowsOf(mainView[key]);
427
453
  for (const locale of cx.locales) {
428
- if (rowsOf(views[locale]?.[key]).length > mainRows.length) {
429
- cx.errors.push({ file: cx.rels[locale] ?? "", path, message: `\u201C${field.label}\u201D has more rows than in the main language.` });
454
+ const count = views[locale]?.[key] === void 0 ? mainRows.length : rowsOf(views[locale]?.[key]).length;
455
+ if (locale !== cx.main && count !== mainRows.length) {
456
+ const display = displayOf(cx.where, cx.prefix, path);
457
+ cx.errors.push({ file: cx.rels[locale] ?? "", path, ...display !== path ? { display } : {}, message: `\u201C${key}\u201D has ${count} rows here and ${mainRows.length} in content/${cx.rels[cx.main] ?? ""}. Each row translates the row at the same place in the main file; write - {} for a row with nothing to translate.` });
430
458
  }
431
459
  }
432
460
  out[key] = mainRows.map((_, i) => {
@@ -436,7 +464,7 @@ function merge(fields, views, cx) {
436
464
  const row = rowsOf(views[locale]?.[key])[i];
437
465
  if (row) rowViews[locale] = row;
438
466
  }
439
- return { _id: id, ...merge(field.of, rowViews, { ...cx, prefix: `${path}.${id}.` }) };
467
+ return { _id: id, ...merge(field.of, rowViews, { ...cx, prefix: `${path}.${id}.`, where: [...cx.where, displayOf(cx.where, cx.prefix, path), `row ${i + 1}`] }) };
440
468
  });
441
469
  } else if (field.type === "image") {
442
470
  const image = mainView[key];
@@ -456,6 +484,7 @@ function merge(fields, views, cx) {
456
484
  }
457
485
  async function readContent(contentDir, publicDir, schema, options = {}) {
458
486
  const drafts = options.drafts ?? true;
487
+ const today = options.today ?? todayIn(schema.timeZone);
459
488
  const errors = [];
460
489
  const entries = {};
461
490
  const files = {};
@@ -467,11 +496,15 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
467
496
  const times = await fileTimes(contentDir);
468
497
  const urlOwners = [];
469
498
  const known = /* @__PURE__ */ new Map();
499
+ const idExamples = /* @__PURE__ */ new Map();
470
500
  for (const [key, brick] of Object.entries(schema.bricks)) {
471
501
  if (brick.source !== "files" || brick.kind !== "collection") continue;
472
502
  known.set(key, new Set((await entryNames(join(contentDir, key))).map((name) => `${key}/${name}`)));
503
+ const first = (await dataNames(join(contentDir, key))).find((file) => extOf(file) !== "csv" && parseFileName(file).locale === void 0);
504
+ if (first !== void 0) idExamples.set(key, `content/${key}/${first} is ${key}/${parseFileName(first).name}`);
473
505
  }
474
506
  const refs = (brick, id) => known.get(brick)?.has(id) ?? true;
507
+ const leftOut = /* @__PURE__ */ new Set();
475
508
  for (const [key, brick] of Object.entries(schema.bricks)) {
476
509
  if (brick.source !== "files") continue;
477
510
  entries[key] = [];
@@ -495,12 +528,12 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
495
528
  const group = groups.get(parsed.name) ?? { rank, translations: {} };
496
529
  groups.set(parsed.name, group);
497
530
  if (locale === main) {
498
- if (group.main !== void 0) return report(`Entry \u201C${relOf(parsed.name)}\u201D exists twice.`);
531
+ if (group.main !== void 0) return report(`Entry \u201C${relOf(parsed.name)}\u201D is in two files: content/${group.main.rel} and content/${rel}. Keep one and delete or rename the other.`);
499
532
  group.main = src;
500
533
  group.number = parsed.number;
501
534
  group.rank = rank;
502
535
  } else {
503
- if (group.translations[locale] !== void 0) return report(`Language \u201C${locale}\u201D exists twice for this entry.`);
536
+ if (group.translations[locale] !== void 0) return report(`Language \u201C${locale}\u201D of \u201C${relOf(parsed.name)}\u201D is in two files: content/${group.translations[locale].rel} and content/${rel}. Keep one and delete or rename the other.`);
504
537
  group.translations[locale] = src;
505
538
  }
506
539
  };
@@ -550,6 +583,7 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
550
583
  });
551
584
  let next = Math.max(0, ...sorted.map(([, g]) => g.number ?? 0));
552
585
  const slugKey = slugKeyOf(brick);
586
+ const dateKey = scheduleKeyOf(brick);
553
587
  const slugOwner = /* @__PURE__ */ new Map();
554
588
  for (const [name, group] of sorted) {
555
589
  if (group.main === void 0) {
@@ -560,11 +594,13 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
560
594
  const order = folder ? group.number ?? ++next : void 0;
561
595
  const mainRel = group.main.rel;
562
596
  const mainView = await readDoc(group.main, true);
597
+ if (mainView === "draft") leftOut.add(id);
563
598
  if (mainView === void 0 || mainView === "draft") continue;
564
599
  const views = { [main]: mainView.view };
565
600
  const trees = { [main]: mainView.tree };
566
601
  const rels = { [main]: mainRel };
567
602
  const lines = { [main]: group.main.csv?.row.line };
603
+ const bodyLines = { [main]: mainView.bodyLine };
568
604
  for (const locale of schema.locales) {
569
605
  const src = group.translations[locale];
570
606
  if (src === void 0) continue;
@@ -574,10 +610,11 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
574
610
  if (doc.tree) trees[locale] = doc.tree;
575
611
  rels[locale] = src.rel;
576
612
  lines[locale] = src.csv?.row.line;
613
+ bodyLines[locale] = doc.bodyLine;
577
614
  }
578
615
  const prints = {};
579
616
  const mergeErrors = [];
580
- const data = merge(brick.fields, views, { main, locales: schema.locales, entryId: id, prefix: "", prints, rels, errors: mergeErrors });
617
+ const data = merge(brick.fields, views, { main, locales: schema.locales, entryId: id, prefix: "", prints, rels, errors: mergeErrors, where: [] });
581
618
  if (mergeErrors.length > 0) {
582
619
  errors.push(...mergeErrors);
583
620
  continue;
@@ -588,13 +625,25 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
588
625
  const last = issue.path.split(".").pop();
589
626
  const locale = schema.locales.find((l) => l === last);
590
627
  const at = locale && rels[locale] ? locale : main;
591
- errors.push({ file: rels[at] ?? mainRel, path: issue.path, message: coreMessage("en", issue.key, issue.params), ...lines[at] !== void 0 ? { line: lines[at] } : {} });
628
+ const bodyLine = bodyLines[at];
629
+ const textLine = issue.key === "issues.markdownAt" && issue.path.split(".")[0] === markdownKeyOf(brick) ? issue.params?.line : void 0;
630
+ const line = typeof textLine === "number" && bodyLine !== void 0 ? bodyLine + textLine - 1 : lines[at];
631
+ const params = line !== void 0 && textLine !== void 0 ? { ...issue.params, line } : issue.params;
632
+ const where = locate(brick.fields, data, issue.path);
633
+ const display = where.shown.join(", ");
634
+ errors.push({
635
+ file: rels[at] ?? mainRel,
636
+ path: issue.path,
637
+ ...display !== issue.path ? { display } : {},
638
+ message: filesMessage(issue.key, params, where, { currency: schema.currency, refs, idExamples }),
639
+ ...line !== void 0 ? { line } : {}
640
+ });
592
641
  }
593
642
  continue;
594
643
  }
595
644
  const markdownKey = markdownKeyOf(brick);
596
645
  const texts = markdownKey === void 0 ? void 0 : data[markdownKey];
597
- if (markdownKey !== void 0 && isRecord(texts)) {
646
+ if (options.markdown !== "text" && markdownKey !== void 0 && isRecord(texts)) {
598
647
  data[markdownKey] = Object.fromEntries(Object.entries(texts).map(([locale, text]) => [locale, trees[locale] ?? text]));
599
648
  for (const locale of Object.keys(prints[markdownKey] ?? {})) prints[markdownKey][locale] = printOf(data[markdownKey][main]);
600
649
  }
@@ -614,7 +663,7 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
614
663
  const entry = { id, data };
615
664
  if (mainView.redirectFrom?.length) entry.data.redirectFrom = mainView.redirectFrom;
616
665
  if (mainView.draft) entry.data.draft = true;
617
- const urls = brick.kind === "collection" ? urlsFor(schema, key, brick, entry, (fileId) => files[fileId]) : {};
666
+ const urls = brick.kind === "collection" ? routeUrls(schema, brick, entry) : {};
618
667
  const urlClash = urlClashes([...urlOwners, { id, urls }]).find((c) => c.second === id);
619
668
  if (urlClash) {
620
669
  errors.push({ file: rels[urlClash.locale] ?? mainRel, path: slugKey, message: `URL \u201C${urlClash.url}\u201D is already taken by \u201C${urlClash.first}\u201D.` });
@@ -622,6 +671,10 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
622
671
  }
623
672
  urlOwners.push({ id, urls });
624
673
  for (const t of taken) slugOwner.set(t.key, id);
674
+ if (mainView.scheduled) {
675
+ leftOut.add(id);
676
+ continue;
677
+ }
625
678
  if (order !== void 0) entry.order = order;
626
679
  entry.updatedAt = times.updatedAt(Object.values(rels));
627
680
  entry.sources = Object.fromEntries(Object.entries(rels).map(([locale, rel]) => [locale, times.source(rel)]));
@@ -633,9 +686,10 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
633
686
  const ext = extOf(rel);
634
687
  const markdownKey = markdownKeyOf(brick);
635
688
  const fileErrors = [];
636
- const cx = { file: rel, isMain, prefix: "", publicDir, files, errors: fileErrors, line: src.csv?.row.line };
689
+ const cx = { file: rel, isMain, prefix: "", publicDir, files, errors: fileErrors, line: src.csv?.row.line, where: [] };
637
690
  let data;
638
691
  let parsed;
692
+ let bodyLine;
639
693
  if (src.csv) {
640
694
  data = {};
641
695
  const { columns, row } = src.csv;
@@ -650,13 +704,20 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
650
704
  const text = await readFile(src.path, "utf8");
651
705
  if (ext === "md") {
652
706
  const field = markdownKey === void 0 ? void 0 : brick.fields[markdownKey];
653
- parsed = await parseFile(text, { profile: field?.type === "markdown" ? effectiveProfile(field) : void 0, components: schema.components });
707
+ try {
708
+ parsed = await parseFile(text, { profile: field?.type === "markdown" ? effectiveProfile(field) : void 0, components: schema.components });
709
+ } catch (error) {
710
+ if (!isYamlError(error)) throw error;
711
+ errors.push(yamlError(error, text, rel, 1));
712
+ return void 0;
713
+ }
654
714
  for (const problem of parsed.previewProblems) fail(cx, markdownKey ?? "body", `In a \`\`\`md preview fence: ${coreMessage("en", problem.key, problem.params)}`);
655
715
  data = parsed.frontmatter;
656
716
  const imageCx = { contentDir, publicDir, file: rel, images, renames: /* @__PURE__ */ new Map(), imageProps, sized, fail: (message) => fileErrors.push({ file: rel, message }) };
657
717
  rewriteImages(parsed.value.nodes, imageCx);
658
718
  rewriteImages(parsed.value.summary, imageCx);
659
719
  parsed.body = renameInText(parsed.body, imageCx.renames);
720
+ bodyLine = text.split("\n").length - parsed.body.split("\n").length + 1;
660
721
  } else {
661
722
  const raw = parseDataFile(text, ext, rel, errors);
662
723
  if (raw === void 0) return void 0;
@@ -667,21 +728,88 @@ async function readContent(contentDir, publicDir, schema, options = {}) {
667
728
  const redirects = redirectFrom === void 0 ? void 0 : redirectPaths(cx, redirectFrom);
668
729
  if (draft !== void 0 && typeof draft !== "boolean") fail(cx, "draft", "\u201Cdraft\u201D must be true or false.");
669
730
  if (draft === true && !drafts && fileErrors.length === 0) return "draft";
731
+ const dateValue = dateKey === void 0 ? void 0 : fields[dateKey];
732
+ const scheduled = isMain && !drafts && isDate(dateValue) && dateValue > today;
670
733
  const view = convertFields(brick.fields, fields, cx);
671
734
  const hasBody = parsed !== void 0 && parsed.value.nodes.length > 0;
672
735
  if (parsed === void 0 && markdownKey !== void 0) {
673
- const label = brick.fields[markdownKey].label;
674
- fail(cx, markdownKey, `\u201C${label}\u201D is a markdown field: it is the text of a .md file, and ${ext === "csv" ? "CSV" : ext.toUpperCase()} files have no text.`);
736
+ fail(cx, markdownKey, `\u201C${markdownKey}\u201D is a markdown field: it is the text of a .md file, and ${ext === "csv" ? "CSV" : ext.toUpperCase()} files have no text.`);
675
737
  }
676
738
  if (parsed !== void 0 && markdownKey !== void 0 && hasBody) view[markdownKey] = parsed.body;
677
739
  if (markdownKey === void 0 && hasBody) fail(cx, "body", `The text fits no field of \u201C${key}\u201D (it has no markdown field).`);
678
740
  if (slugKey !== void 0 && view[slugKey] === void 0) view[slugKey] = src.csv ? src.name : slugify(src.name);
679
741
  errors.push(...fileErrors);
680
- return fileErrors.length > 0 ? void 0 : { view, tree: markdownKey !== void 0 && hasBody ? parsed.value : void 0, redirectFrom: redirects, draft: draft === true ? true : void 0 };
742
+ return fileErrors.length > 0 ? void 0 : { view, tree: markdownKey !== void 0 && hasBody ? parsed.value : void 0, bodyLine, redirectFrom: redirects, draft: draft === true ? true : void 0, scheduled: scheduled ? true : void 0 };
743
+ }
744
+ }
745
+ for (let size = -1; size !== leftOut.size; ) {
746
+ size = leftOut.size;
747
+ for (const [key, list] of Object.entries(entries)) {
748
+ const owners = Object.entries(schema.bricks[key]?.fields ?? {}).filter(([, field]) => field.type === "ref" && !field.multiple && !field.optional).map(([name]) => name);
749
+ if (owners.length === 0) continue;
750
+ entries[key] = list.filter((entry) => {
751
+ const draft = owners.some((name) => leftOut.has(entry.data[name]));
752
+ if (draft) leftOut.add(entry.id);
753
+ return !draft;
754
+ });
681
755
  }
682
756
  }
683
757
  return { entries, files, images, errors };
684
758
  }
759
+ const labelTexts = (field) => typeof field.label === "string" ? [field.label] : Object.values(field.label);
760
+ function locate(root, data, path) {
761
+ const sets = [root];
762
+ const shown = [];
763
+ let fields = root;
764
+ let field;
765
+ let value = data;
766
+ for (const segment of path.split(".")) {
767
+ if (field?.type === "list") {
768
+ const rows = Array.isArray(value) ? value : [];
769
+ const index = rows.findIndex((row) => isRecord(row) && row._id === segment);
770
+ shown.push(index < 0 ? segment : `row ${index + 1}`);
771
+ value = rows[index];
772
+ fields = field.of;
773
+ sets.push(fields);
774
+ field = void 0;
775
+ } else if (field) {
776
+ shown[shown.length - 1] += `.${segment}`;
777
+ value = isRecord(value) ? value[segment] : void 0;
778
+ } else {
779
+ field = Object.hasOwn(fields, segment) ? fields[segment] : void 0;
780
+ shown.push(segment);
781
+ value = isRecord(value) ? value[segment] : void 0;
782
+ }
783
+ }
784
+ return { sets, field, value, shown };
785
+ }
786
+ function filesMessage(key, params, at, site) {
787
+ const keys = /* @__PURE__ */ new Map();
788
+ for (const set of at.sets) for (const [name2, field] of Object.entries(set)) for (const text of labelTexts(field)) keys.set(text, name2);
789
+ const named = Object.fromEntries(Object.entries(params ?? {}).map(([name2, value]) => [name2, keys.get(value) ?? value]));
790
+ const name = at.shown[at.shown.length - 1]?.split(".")[0] ?? "";
791
+ switch (key) {
792
+ case "issues.notMoney": {
793
+ const currency = at.field?.type === "money" && at.field.currency ? at.field.currency : site.currency;
794
+ return `\u201C${name}\u201D must be a number in ${currency}, like 65 or 12.50 (no currency sign, no thousands separator).`;
795
+ }
796
+ case "issues.badDate":
797
+ return `\u201C${name}\u201D is not a valid date: write it as YYYY-MM-DD, like 2026-10-24.`;
798
+ case "issues.badTime":
799
+ return `\u201C${name}\u201D is not a valid time: write it as HH:MM, like 09:30.`;
800
+ case "issues.badDateRange":
801
+ return `\u201C${name}\u201D needs from and to: ${name}: { from: 2026-11-14, to: 2026-11-14 } (the same date twice for one day).`;
802
+ case "issues.refGone": {
803
+ const to = at.field?.type === "ref" ? at.field.to : void 0;
804
+ const ids = Array.isArray(at.value) ? at.value : [at.value];
805
+ const missing = to === void 0 ? void 0 : ids.find((id) => typeof id === "string" && !site.refs(to, id));
806
+ const example = to === void 0 ? void 0 : site.idExamples.get(to);
807
+ return `There is no entry \u201C${String(missing ?? at.value)}\u201D. An id is the folder and the file name without number and language${example ? `: ${example}` : ""}.`;
808
+ }
809
+ default:
810
+ return coreMessage("en", key, named);
811
+ }
812
+ }
685
813
  function markdownKeyOf(brick) {
686
814
  return Object.keys(brick.fields).find((k) => brick.fields[k].type === "markdown");
687
815
  }
@@ -712,6 +840,10 @@ async function dataNames(dir) {
712
840
  function slugKeyOf(brick) {
713
841
  return brick.fields.slug?.type === "slug" ? "slug" : void 0;
714
842
  }
843
+ function scheduleKeyOf(brick) {
844
+ if (brick.kind !== "collection" || brick.drafts !== true || brick.order === "manual" || brick.order.dir !== "desc") return void 0;
845
+ return brick.fields[brick.order.field]?.type === "date" ? brick.order.field : void 0;
846
+ }
715
847
  function brickProblemsOf(brick) {
716
848
  const problems = [];
717
849
  if (Object.values(brick.fields).filter((f) => f.type === "markdown" && !f.retired).length > 1) {
@@ -743,24 +875,6 @@ async function makeVariants(path, hash, width, dir, names, counter) {
743
875
  function fileSchema(schema) {
744
876
  return { ...schema, bricks: Object.fromEntries(Object.entries(schema.bricks).filter(([, brick]) => brick.source === "files")) };
745
877
  }
746
- function markdownRef(locale, brick, id, field) {
747
- return `/__ginko/files/${locale}/md/${encodeURIComponent(brick)}/${id.split("/").map(encodeURIComponent).join("/")}/${encodeURIComponent(field)}.json`;
748
- }
749
- function withMarkdownRefs(bundle, schema, trees) {
750
- const swap = (brick, id, data) => {
751
- const out = { ...data };
752
- for (const [name, field] of Object.entries(schema.bricks[brick]?.fields ?? {})) {
753
- const value = out[name];
754
- if (field.type !== "markdown" || !value || !("nodes" in value)) continue;
755
- const ref = markdownRef(bundle.locale, brick, id, name);
756
- trees.set(ref, value);
757
- out[name] = value.toc === void 0 ? { ref } : { ref, toc: value.toc };
758
- }
759
- return out;
760
- };
761
- const bricks = Object.fromEntries(Object.entries(bundle.bricks).map(([key, b]) => [key, b.kind === "single" ? { ...b, data: swap(key, key, b.data) } : { ...b, items: b.items.map((item) => ({ ...item, data: swap(key, item.id, item.data) })) }]));
762
- return { ...bundle, bricks };
763
- }
764
878
  function publicBundle(bundle, schema, now, options = {}) {
765
879
  const today = todayIn(schema.timeZone, now);
766
880
  const bricks = Object.fromEntries(Object.entries(bundle.bricks).map(([key, brick]) => {
@@ -774,22 +888,22 @@ function publicBundle(bundle, schema, now, options = {}) {
774
888
  async function buildFileBundles(options) {
775
889
  const { contentDir, publicDir, schema, now = /* @__PURE__ */ new Date() } = options;
776
890
  const files = fileSchema(schema);
777
- const { entries, files: fileInfo, images, errors } = await readContent(contentDir, publicDir, files, options);
891
+ const { entries, files: fileInfo, images, errors } = await readContent(contentDir, publicDir, files, { today: todayIn(schema.timeZone, now), ...options });
778
892
  const imageFiles = /* @__PURE__ */ new Set();
779
893
  if (options.imageDir) await addVariants(fileInfo, images, publicDir, options.imageDir, imageFiles);
894
+ const build = (locale) => buildBundle({
895
+ schema: files,
896
+ requestedLocale: locale,
897
+ entries,
898
+ files: (id) => fileInfo[id],
899
+ schemaRev: 0,
900
+ contentRev: 0,
901
+ generatedAt: now.toISOString(),
902
+ today: todayIn(schema.timeZone, now)
903
+ });
904
+ const main = build(schema.locales[0]);
780
905
  const bundles = {};
781
- for (const locale of schema.locales) {
782
- bundles[locale] = resolveRouteLinks(buildBundle({
783
- schema: files,
784
- requestedLocale: locale,
785
- entries,
786
- files: (id) => fileInfo[id],
787
- schemaRev: 0,
788
- contentRev: 0,
789
- generatedAt: now.toISOString(),
790
- today: todayIn(schema.timeZone, now)
791
- }), files);
792
- }
906
+ for (const locale of schema.locales) bundles[locale] = resolveRouteLinks(locale === main.locale ? main : build(locale), files, main);
793
907
  return { bundles, images, imageFiles, errors };
794
908
  }
795
909
  async function addVariants(fileInfo, images, publicDir, imageDir, names) {
@@ -819,4 +933,4 @@ function searchIndex(bundle, trees, schema) {
819
933
  return buildIndex(searchSections(bundle, { schema, trees }), bundle.locale);
820
934
  }
821
935
 
822
- export { buildFileBundles, errorLine, publicBundle, searchIndex, withMarkdownRefs };
936
+ export { buildFileBundles, errorLine, publicBundle, readContent, searchIndex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Ginko bricks from Markdown files in the site's repo. Installed, not imported: the Ginko module loads it.",
6
6
  "license": "MIT",
@@ -36,12 +36,12 @@
36
36
  "shiki": "^4.3.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@lupinum/ginko": "0.4.0",
39
+ "@lupinum/ginko": "0.6.0",
40
40
  "sharp": "0.35.5",
41
41
  "unbuild": "3.6.1"
42
42
  },
43
43
  "peerDependencies": {
44
- "@lupinum/ginko": "^0.4.0",
44
+ "@lupinum/ginko": "^0.6.0",
45
45
  "sharp": "^0.33.0 || ^0.34.0 || ^0.35.0"
46
46
  },
47
47
  "peerDependenciesMeta": {