@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 +33 -0
- package/dist/index.d.mts +44 -10
- package/dist/index.d.ts +44 -10
- package/dist/index.mjs +173 -59
- package/package.json +3 -3
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 {
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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,
|
|
108
|
+
export { buildFileBundles, errorLine, publicBundle, readContent, searchIndex };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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${
|
|
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${
|
|
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${
|
|
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${
|
|
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${
|
|
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
|
-
|
|
429
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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" ?
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
44
|
+
"@lupinum/ginko": "^0.6.0",
|
|
45
45
|
"sharp": "^0.33.0 || ^0.34.0 || ^0.35.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|