@heroiclands/package-build 14.0.0 → 16.0.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 +327 -0
- package/CONTENT.md +331 -19
- package/MIGRATING.md +185 -0
- package/bin/content-build.mjs +54 -0
- package/content-config.mjs +10 -0
- package/docs/content-format.md +21 -0
- package/engine/content-address.mjs +21 -36
- package/engine/content-index.mjs +439 -0
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +171 -1
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/index.mjs +3 -0
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- package/engine/site-build.mjs +20 -14
- package/engine/system-block.mjs +29 -3
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -3
- package/sohl/item-fields.mjs +6 -0
- package/sohl/items.mjs +4 -1
- package/types/content-config.d.mts +9 -0
- package/types/engine/content-address.d.mts +22 -34
- package/types/engine/content-index.d.mts +194 -0
- package/types/engine/field-spec.d.mts +53 -0
- package/types/engine/helpers.d.mts +34 -12
- package/types/engine/homepage.d.mts +8 -4
- package/types/engine/index.d.mts +1 -0
- package/types/engine/site-build.d.mts +11 -6
|
@@ -21,44 +21,32 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export function addressSlug(fm: object): string;
|
|
23
23
|
/**
|
|
24
|
-
* A note's address
|
|
24
|
+
* A note's address: `<type>-<shortcode>/`, e.g. `affliction-aconite/`.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* decides nothing about where it publishes.
|
|
30
|
-
*
|
|
31
|
-
* @param {object} fm - Parsed frontmatter.
|
|
32
|
-
* @returns {string} The mount-relative address, with a trailing slash.
|
|
33
|
-
* @throws {Error} When the note has no address.
|
|
34
|
-
*/
|
|
35
|
-
export function contentAddress(fm: object): string;
|
|
36
|
-
/**
|
|
37
|
-
* A note's address relative to its **package**, e.g. `affliction-aconite/`.
|
|
38
|
-
*
|
|
39
|
-
* This is the form the link manifest records and the site build emits pages at,
|
|
40
|
-
* and it is one function because those two must agree — a manifest asserting an
|
|
26
|
+
* This is the one form a note is addressed by. It is what the link manifest
|
|
27
|
+
* records as an entry's `path` and what the site build emits the page at, and
|
|
28
|
+
* it is one function because those two must agree — a manifest asserting an
|
|
41
29
|
* address the site does not publish resolves at build time and 404s for the
|
|
42
30
|
* reader, which is the failure this module exists to prevent.
|
|
43
31
|
*
|
|
44
|
-
* **
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
32
|
+
* **The address is relative to the package**, and to nothing finer. A consumer
|
|
33
|
+
* composing a URL prepends where the package is served (`/<package>/`); a
|
|
34
|
+
* consumer composing a manifest entry measures against that same base. Nothing
|
|
35
|
+
* else is prepended: `prefix` says where the content tree *mounts inside the
|
|
36
|
+
* package* — the Hugo directory its pages are written under — and an address is
|
|
37
|
+
* `(type, shortcode)`, a package-wide identity that takes no mount, so `sohl`
|
|
38
|
+
* publishes `/sohl/affliction-aconite/` from a file written under `kb/`. The
|
|
39
|
+
* `type-` half is what keeps that flat namespace clear of the package's fixed
|
|
40
|
+
* mounts — `/<package>/` for the landing, `/<package>/api/` for generated API
|
|
41
|
+
* docs, neither of which contains a hyphen or names a type.
|
|
42
|
+
*
|
|
43
|
+
* **It is a pure function of the frontmatter**, and takes no options. Nothing
|
|
44
|
+
* about the file the note was read from reaches it: the `README.md` convention
|
|
45
|
+
* that made one note address a whole section is retired with the section itself
|
|
46
|
+
* (#204), so every note is addressed alike and there is one rule and no branch.
|
|
47
|
+
* It took an address scheme until #215, to validate a `landing` rule it then
|
|
48
|
+
* discarded; with that key retired, `prefix` was the only thing left in the
|
|
49
|
+
* scheme and the paragraph above is the reason it never applied.
|
|
62
50
|
*
|
|
63
51
|
* @param {object} fm - Parsed frontmatter.
|
|
64
52
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `{#slug}` anchors a note's body declares, with where each one sits.
|
|
3
|
+
*
|
|
4
|
+
* Only headings carrying an explicit anchor are collected. A bare `#` heading
|
|
5
|
+
* also starts a journal page, but it declares no slug, so nothing can address
|
|
6
|
+
* it with `#…` — listing it would offer a link that cannot be written.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} body - The note's markdown body, frontmatter already removed.
|
|
9
|
+
* @param {number} [bodyLine] - The 1-based file line the body starts on, from
|
|
10
|
+
* `parseMarkdownFile`. Anchors are reported at their position in the **file**,
|
|
11
|
+
* so an editor can jump straight to one; passing nothing numbers from the body.
|
|
12
|
+
* @returns {Array<{slug: string, name: string, level: number, line: number}>}
|
|
13
|
+
* In document order.
|
|
14
|
+
*/
|
|
15
|
+
export function collectAnchors(body: string, bodyLine?: number): Array<{
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
level: number;
|
|
19
|
+
line: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* The address a wikilink writes to reach a note, or `null` when it has none.
|
|
23
|
+
*
|
|
24
|
+
* A wikilink target is an address: `being-aurochs` locally, or
|
|
25
|
+
* `sohl-being-aurochs` from another package (`readQualifier` also accepts
|
|
26
|
+
* `being/aurochs`, the same two fields with a different separator). Both forms
|
|
27
|
+
* are already derivable from `type` and `shortcode`, which every record
|
|
28
|
+
* carries — so this field adds no information. What it adds is the *rule*:
|
|
29
|
+
* the lowercasing and the hyphen join live in one place, and a consumer that
|
|
30
|
+
* reimplements them slightly differently gets a lookup that matches nothing and
|
|
31
|
+
* says nothing about why. That is a real failure, not a hypothetical one — it
|
|
32
|
+
* is precisely how a resolver keyed on a bare `type/shortcode` silently misses
|
|
33
|
+
* every canonical `pkg-type-shortcode` entry.
|
|
34
|
+
*
|
|
35
|
+
* Derived by the same functions the link manifest and the site build use, so an
|
|
36
|
+
* index cannot disagree with either about where a note lives.
|
|
37
|
+
*
|
|
38
|
+
* @param {Record<string, any>} frontmatter - The note's parsed frontmatter.
|
|
39
|
+
* @param {string} contentPackage - The package the tree compiles as.
|
|
40
|
+
* @returns {{slug: string, canonical: string}|null} `slug` is what goes inside
|
|
41
|
+
* `[[…]]` within this package; `canonical` is the package-qualified key the
|
|
42
|
+
* manifest files the note under. `null` for a note with no type or no
|
|
43
|
+
* shortcode, which has no address at all and is stated as such rather than
|
|
44
|
+
* left for every reader to rediscover.
|
|
45
|
+
*/
|
|
46
|
+
export function noteAddress(frontmatter: Record<string, any>, contentPackage: string): {
|
|
47
|
+
slug: string;
|
|
48
|
+
canonical: string;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* Recursively sort an object's keys, so serialization is order-independent.
|
|
52
|
+
*
|
|
53
|
+
* Arrays keep their order — it is authored — but every object inside one is
|
|
54
|
+
* sorted too. Anything that is not a plain object is returned as it is.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} value - The value to normalize.
|
|
57
|
+
* @returns {unknown} The value with every plain object's keys in sorted order.
|
|
58
|
+
*/
|
|
59
|
+
export function sortKeysDeep(value: unknown): unknown;
|
|
60
|
+
/**
|
|
61
|
+
* A note's display name reduced to printable 7-bit ASCII.
|
|
62
|
+
*
|
|
63
|
+
* Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
|
|
64
|
+
* `Kèthîra` — and nobody types them. A reader searching the index, or an editor
|
|
65
|
+
* completing a wikilink, needs a form that matches what a keyboard produces, so
|
|
66
|
+
* the record states one rather than leaving every consumer to invent it (and to
|
|
67
|
+
* invent a *different* one, which is how two searches over the same data come
|
|
68
|
+
* to disagree).
|
|
69
|
+
*
|
|
70
|
+
* **Transliterated, not stripped.** `unidecode` — the same table
|
|
71
|
+
* {@link slugify} already runs, so an ASCII name and a slug can never disagree
|
|
72
|
+
* about a character — carries a letter across rather than deleting it:
|
|
73
|
+
* diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
|
|
74
|
+
* `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
|
|
75
|
+
* even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
|
|
76
|
+
* would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
|
|
77
|
+
*
|
|
78
|
+
* Anything still outside printable ASCII after that becomes a space, and runs
|
|
79
|
+
* of whitespace collapse — a space rather than nothing, so a character that
|
|
80
|
+
* transliterates away cannot silently weld two words together.
|
|
81
|
+
*
|
|
82
|
+
* The value is emitted even when it equals the name, so a consumer matching on
|
|
83
|
+
* it never has to branch on whether the name happened to be ASCII already.
|
|
84
|
+
*
|
|
85
|
+
* @param {unknown} name - The note's `name.full`.
|
|
86
|
+
* @returns {string|null} The ASCII form, or `null` when there is no name, or
|
|
87
|
+
* nothing printable survives.
|
|
88
|
+
*/
|
|
89
|
+
export function asciiName(name: unknown): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
|
|
92
|
+
*
|
|
93
|
+
* An alias is the name a reader is at least as likely to reach for as the
|
|
94
|
+
* canonical one — `Killer Whale` for an orca, `Ice Bear` for a polar bear,
|
|
95
|
+
* `Ix'balam` for a jaguar — so anything searching or completing over the index
|
|
96
|
+
* has to match them too, and needs the same keyboard-typeable form
|
|
97
|
+
* {@link asciiName} gives the primary name.
|
|
98
|
+
*
|
|
99
|
+
* Order is the authored order, so a caller can pair an entry with the alias it
|
|
100
|
+
* came from. An alias that is not a non-empty string, or that leaves nothing
|
|
101
|
+
* printable behind, is dropped rather than left as a hole — the array is a set
|
|
102
|
+
* of names to match, and a null in it is not one.
|
|
103
|
+
*
|
|
104
|
+
* @param {unknown} aliases - The note's `name.aliases`; may be absent or null.
|
|
105
|
+
* @returns {Array<string>} Possibly empty, never null: a note with no aliases
|
|
106
|
+
* has an empty set of them, which is a fact rather than a missing value, and
|
|
107
|
+
* a consumer iterating it should not have to check first.
|
|
108
|
+
*/
|
|
109
|
+
export function asciiAliases(aliases: unknown): Array<string>;
|
|
110
|
+
/**
|
|
111
|
+
* Build one index record from a note's frontmatter and its place in the tree.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} options - Options.
|
|
114
|
+
* @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
|
|
115
|
+
* @param {string} options.relPath - Its path below the content root, POSIX-separated.
|
|
116
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
117
|
+
* @param {string} [options.body] - The note's markdown body, for its anchors.
|
|
118
|
+
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
119
|
+
* @returns {Record<string, any>} The record, keys sorted at every depth.
|
|
120
|
+
* @throws {Error} When the note carries a key this module derives, which would
|
|
121
|
+
* otherwise be overwritten without a word.
|
|
122
|
+
*/
|
|
123
|
+
export function buildIndexRecord({ frontmatter, relPath, contentPackage, body, bodyLine }: {
|
|
124
|
+
frontmatter: Record<string, any>;
|
|
125
|
+
relPath: string;
|
|
126
|
+
contentPackage: string;
|
|
127
|
+
body?: string | undefined;
|
|
128
|
+
bodyLine?: number | undefined;
|
|
129
|
+
}): Record<string, any>;
|
|
130
|
+
/**
|
|
131
|
+
* Read a content tree into index records, in the order they will be written.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} contentBase - The content tree to walk.
|
|
134
|
+
* @param {object} options - Options.
|
|
135
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
136
|
+
* @param {Array<string>} [options.skipDirectories] - Directory names to skip.
|
|
137
|
+
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
138
|
+
* not depend on directory-read order.
|
|
139
|
+
*/
|
|
140
|
+
export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories }: {
|
|
141
|
+
contentPackage: string;
|
|
142
|
+
skipDirectories?: string[] | undefined;
|
|
143
|
+
}): Array<Record<string, any>>;
|
|
144
|
+
/**
|
|
145
|
+
* Serialize records as JSON Lines.
|
|
146
|
+
*
|
|
147
|
+
* @param {Array<Record<string, any>>} records - From {@link collectContentIndex}.
|
|
148
|
+
* @returns {string} One compact JSON object per line, newline-terminated. An
|
|
149
|
+
* empty set serializes to the empty string rather than to a lone newline, so
|
|
150
|
+
* the file is exactly the lines it holds.
|
|
151
|
+
*/
|
|
152
|
+
export function serializeContentIndex(records: Array<Record<string, any>>): string;
|
|
153
|
+
/**
|
|
154
|
+
* Emit this package's content index.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [options] - Options.
|
|
157
|
+
* @param {string} [options.contentBase] - The content tree; defaults to the
|
|
158
|
+
* configured `paths.content`.
|
|
159
|
+
* @param {string} [options.outDir] - Where to write; defaults to the configured
|
|
160
|
+
* `paths.contentIndex`.
|
|
161
|
+
* @param {object} [options.config] - A resolved configuration; loaded when omitted.
|
|
162
|
+
* @returns {{file: string, notes: number, bytes: number}} Where it was written,
|
|
163
|
+
* how many notes it holds, and its size.
|
|
164
|
+
* @throws {Error} When the content tree is absent, or when it yields no note at
|
|
165
|
+
* all — an empty index is indistinguishable from a mis-pointed tree, and a
|
|
166
|
+
* reader would take it as the authoritative statement that this package has
|
|
167
|
+
* no content.
|
|
168
|
+
*/
|
|
169
|
+
export function emitContentIndex({ contentBase, outDir, config }?: {
|
|
170
|
+
contentBase?: string | undefined;
|
|
171
|
+
outDir?: string | undefined;
|
|
172
|
+
config?: object | undefined;
|
|
173
|
+
}): {
|
|
174
|
+
file: string;
|
|
175
|
+
notes: number;
|
|
176
|
+
bytes: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The keys this module adds to a record, which a note therefore may not carry
|
|
180
|
+
* itself.
|
|
181
|
+
*
|
|
182
|
+
* `package` is the note's distribution unit — the configured `contentPackage`,
|
|
183
|
+
* since a note declaring its own is a hard error (package-build#56) — and it
|
|
184
|
+
* matches what the content-table expander puts on the same field, so a query
|
|
185
|
+
* reads the same value from either. `file` namespaces the note's place in the
|
|
186
|
+
* tree, again matching the expander's `file.*`.
|
|
187
|
+
*
|
|
188
|
+
* Both are checked rather than assumed: `folder` is real frontmatter on most
|
|
189
|
+
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
190
|
+
* overwrite is a plausible future rather than a hypothetical one.
|
|
191
|
+
*
|
|
192
|
+
* @type {ReadonlyArray<string>}
|
|
193
|
+
*/
|
|
194
|
+
export const DERIVED_KEYS: ReadonlyArray<string>;
|
|
@@ -63,6 +63,31 @@ export { setPath };
|
|
|
63
63
|
* still read, second, until #126 moves the corpus off it.
|
|
64
64
|
*
|
|
65
65
|
* Absent means the value is not authored at all — see `value`.
|
|
66
|
+
* @property {string} [topLevelMeans] - **What the note's top-level key of this
|
|
67
|
+
* name means instead** — declared only where it means something else, and
|
|
68
|
+
* stating it removes the shared top-level position from this field's
|
|
69
|
+
* resolution order (#218).
|
|
70
|
+
*
|
|
71
|
+
* A field's `name` doubles as its identity and as the shared property it
|
|
72
|
+
* draws from, which is right wherever the two levels state the same quantity
|
|
73
|
+
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
74
|
+
* spelling collides across the two vocabularies. An `affiliation` item's
|
|
75
|
+
* `system.title` is the style of address an office carries; a note's
|
|
76
|
+
* top-level `title` is the note's own heading. Nothing relates them, and
|
|
77
|
+
* before this key one silently fed the other, stringifying an authored
|
|
78
|
+
* `title: null` into fifteen documents.
|
|
79
|
+
*
|
|
80
|
+
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
81
|
+
* would record the decision and lose the case for it, and the next person
|
|
82
|
+
* adding a field needs to know the question exists — this package's own rule
|
|
83
|
+
* that the declaration *is* the statement, never a description of one. The
|
|
84
|
+
* author-facing reference renders it, so an author reading the field table
|
|
85
|
+
* learns that the top-level key will not fill this field, and why.
|
|
86
|
+
*
|
|
87
|
+
* The exempted field is still authorable, at both of the positions that
|
|
88
|
+
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
89
|
+
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
90
|
+
* the top level is read, as the third step.
|
|
66
91
|
* @property {string} [shape] - Human-readable shape, for documentation. Comes
|
|
67
92
|
* paired with `read` from one of the coercion constants below.
|
|
68
93
|
* @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
|
|
@@ -170,6 +195,34 @@ export type FieldSpec = {
|
|
|
170
195
|
* Absent means the value is not authored at all — see `value`.
|
|
171
196
|
*/
|
|
172
197
|
name?: string | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* - **What the note's top-level key of this
|
|
200
|
+
* name means instead** — declared only where it means something else, and
|
|
201
|
+
* stating it removes the shared top-level position from this field's
|
|
202
|
+
* resolution order (#218).
|
|
203
|
+
*
|
|
204
|
+
* A field's `name` doubles as its identity and as the shared property it
|
|
205
|
+
* draws from, which is right wherever the two levels state the same quantity
|
|
206
|
+
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
207
|
+
* spelling collides across the two vocabularies. An `affiliation` item's
|
|
208
|
+
* `system.title` is the style of address an office carries; a note's
|
|
209
|
+
* top-level `title` is the note's own heading. Nothing relates them, and
|
|
210
|
+
* before this key one silently fed the other, stringifying an authored
|
|
211
|
+
* `title: null` into fifteen documents.
|
|
212
|
+
*
|
|
213
|
+
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
214
|
+
* would record the decision and lose the case for it, and the next person
|
|
215
|
+
* adding a field needs to know the question exists — this package's own rule
|
|
216
|
+
* that the declaration *is* the statement, never a description of one. The
|
|
217
|
+
* author-facing reference renders it, so an author reading the field table
|
|
218
|
+
* learns that the top-level key will not fill this field, and why.
|
|
219
|
+
*
|
|
220
|
+
* The exempted field is still authorable, at both of the positions that
|
|
221
|
+
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
222
|
+
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
223
|
+
* the top level is read, as the third step.
|
|
224
|
+
*/
|
|
225
|
+
topLevelMeans?: string | undefined;
|
|
173
226
|
/**
|
|
174
227
|
* - Human-readable shape, for documentation. Comes
|
|
175
228
|
* paired with `read` from one of the coercion constants below.
|
|
@@ -117,25 +117,47 @@ export function makeFilename(name: any, id: any): string;
|
|
|
117
117
|
* asset roots — `icons/...` and `images/...` — are served from the package
|
|
118
118
|
* directory, so they are rewritten to `<assetRoot>/<path>` — `systems/sohl/assets`
|
|
119
119
|
* for this repository, `modules/<id>/assets` for a module (#1508). Any other
|
|
120
|
-
* path (already package-rooted, an absolute URL) is returned unchanged
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
120
|
+
* path (already package-rooted, an absolute URL) is returned unchanged.
|
|
121
|
+
*
|
|
122
|
+
* **Two empties, and they mean opposite things (#218).** `null` — or an absent
|
|
123
|
+
* key, which reaches here as `undefined` — means _unset_: the note names no art
|
|
124
|
+
* and the caller's default applies. `""` means _blank on purpose_: the note
|
|
125
|
+
* names no art **and wants none**, so no default may replace it. Both come back
|
|
126
|
+
* distinguishable, `null` and `""` respectively, and neither is invented from
|
|
127
|
+
* the other.
|
|
128
|
+
*
|
|
129
|
+
* This used to open `if (!raw) return ""`, which made the two one case: every
|
|
130
|
+
* caller then applied its default with `||`, so a deliberate blank was
|
|
131
|
+
* unspellable and an unset key and an empty string compiled identically. That
|
|
132
|
+
* is the convention the project already rejects for an optional "not specified"
|
|
133
|
+
* DataModel string, where `nullable, initial: null` keeps "unset" a single
|
|
134
|
+
* honest value rather than two.
|
|
135
|
+
*
|
|
136
|
+
* **`title` does not follow this rule**, and must not be made to. On a
|
|
137
|
+
* `type: affiliation` note `title` is *also* a declared item field whose default
|
|
138
|
+
* is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
|
|
139
|
+
* key the site emitter reads as the page title — so `title: null` stringifies
|
|
140
|
+
* into the compiled document as the literal `"null"`. One key, two destinations
|
|
141
|
+
* that disagree about what empty means; see #218.
|
|
142
|
+
*
|
|
143
|
+
* This is translation only: the default for an unset path is domain-specific
|
|
144
|
+
* (actors default differently from items, and gear differently again), so each
|
|
145
|
+
* compiler owns its own default and applies it to the result with **nullish**
|
|
146
|
+
* coalescing — `resolveImg(fm.img) ?? <default>`. Not `||`: that would collapse
|
|
147
|
+
* a deliberate blank back into the default and undo the distinction. For items
|
|
148
|
+
* that default is the art paired with the type's builder, reached through
|
|
149
|
+
* `itemArt()`, which runs the path back through this function so a registry
|
|
150
|
+
* entry and a note's `img:` are spelled the same way (#7).
|
|
130
151
|
*
|
|
131
152
|
* @param {string | null | undefined} raw - content-relative path from frontmatter.
|
|
132
153
|
* @param {{assetRoot: string}} [config] - The resolved build configuration.
|
|
133
154
|
* Defaults to this repository's.
|
|
134
|
-
* @returns {string} the Foundry-relative path
|
|
155
|
+
* @returns {string | null} the Foundry-relative path; `""` for a deliberate
|
|
156
|
+
* blank, and `null` when the note names no art at all.
|
|
135
157
|
*/
|
|
136
158
|
export function resolveImg(raw: string | null | undefined, config?: {
|
|
137
159
|
assetRoot: string;
|
|
138
|
-
}): string;
|
|
160
|
+
}): string | null;
|
|
139
161
|
/**
|
|
140
162
|
* Resolves the display name from frontmatter, preferring `name.full`,
|
|
141
163
|
* falling back to `name` (if string), then `defaultValue`.
|
|
@@ -149,6 +149,13 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
|
|
|
149
149
|
* for one; it decides nothing while `url` is present, but a page carrying only
|
|
150
150
|
* `url` would report a slug Hugo had inferred from the filename.
|
|
151
151
|
*
|
|
152
|
+
* **Site-root relative, and so carrying no package base** (#217), exactly as
|
|
153
|
+
* `pageFrontmatter` states a content page's: Hugo resolves a `url`
|
|
154
|
+
* against `baseURL`, whose path is already where the package is served, so a
|
|
155
|
+
* stated base was written twice and published the landing at
|
|
156
|
+
* `/<package>/<package>/homepage-root/`. Where the package is served is what
|
|
157
|
+
* every *href* is composed from and it reaches this page's address not at all.
|
|
158
|
+
*
|
|
152
159
|
* An authored `aliases` is dropped for the same reason it is on every other
|
|
153
160
|
* page: Hugo reads it as URL redirects, so passing it through would publish a
|
|
154
161
|
* redirect stub at each one. The field is retired (#180) and refused before a
|
|
@@ -158,15 +165,12 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
|
|
|
158
165
|
* @param {object} options - Options.
|
|
159
166
|
* @param {string} options.contentPackage - The package this build publishes.
|
|
160
167
|
* @param {string} options.title - The resolved title.
|
|
161
|
-
* @param {string} options.base - Where the package is served, with both
|
|
162
|
-
* slashes — `/<package>/`.
|
|
163
168
|
* @returns {object} The frontmatter to write.
|
|
164
169
|
* @throws {Error} When the note declares no shortcode, and so has no address.
|
|
165
170
|
*/
|
|
166
|
-
export function homepageFrontmatter(fm: object, { contentPackage, title
|
|
171
|
+
export function homepageFrontmatter(fm: object, { contentPackage, title }: {
|
|
167
172
|
contentPackage: string;
|
|
168
173
|
title: string;
|
|
169
|
-
base: string;
|
|
170
174
|
}): object;
|
|
171
175
|
/**
|
|
172
176
|
* Every address a homepage carries, wherever it is written.
|
package/types/engine/index.d.mts
CHANGED
|
@@ -17,6 +17,7 @@ export * as contentAddress from "./content-address.mjs";
|
|
|
17
17
|
export * as foreignManifests from "./foreign-manifests.mjs";
|
|
18
18
|
export * as kbManifest from "./kb-manifest.mjs";
|
|
19
19
|
export * as manifestEmit from "./manifest-emit.mjs";
|
|
20
|
+
export * as contentIndex from "./content-index.mjs";
|
|
20
21
|
export * as siteBuild from "./site-build.mjs";
|
|
21
22
|
export * as contentLint from "./content-lint.mjs";
|
|
22
23
|
export * as contentLinks from "./content-links.mjs";
|
|
@@ -102,14 +102,9 @@ export function collectHomepages(contentBase: string, ctx: object): {
|
|
|
102
102
|
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
103
103
|
* @param {object} config - The resolved configuration, for the package name and
|
|
104
104
|
* the default title.
|
|
105
|
-
* @param {object} [options] - Options.
|
|
106
|
-
* @param {string} [options.base] - Where the package is served; defaults to the
|
|
107
|
-
* configured `site.base`, and to `/<contentPackage>/` below that.
|
|
108
105
|
* @returns {number} How many pages were written.
|
|
109
106
|
*/
|
|
110
|
-
export function writeHomepages(outRoot: string, pages: readonly object[], config: object
|
|
111
|
-
base?: string | undefined;
|
|
112
|
-
}): number;
|
|
107
|
+
export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
|
|
113
108
|
/**
|
|
114
109
|
* The integrity gates a site build runs before it writes anything.
|
|
115
110
|
*
|
|
@@ -201,6 +196,16 @@ export function sectionFrontmatter(meta: object): object;
|
|
|
201
196
|
* package-wide address the link manifest records — the same address, one
|
|
202
197
|
* segment too deep. So the address is stated and the mount does not reach it.
|
|
203
198
|
*
|
|
199
|
+
* **It is stated relative to the site root, and so carries no package base**
|
|
200
|
+
* (#217). Hugo resolves a `url` against `baseURL`, whose path is already where
|
|
201
|
+
* the package is served — a consumer's Hugo site *is* its package — so writing
|
|
202
|
+
* `page.url`, which carries the base for every href this build renders, wrote
|
|
203
|
+
* that base a second time and published every content page a segment too deep
|
|
204
|
+
* (`/sohl/sohl/doc-rulesintro/`). The two are separate quantities: the page
|
|
205
|
+
* states `/<slug>/`, and everything that points *at* the page — the address
|
|
206
|
+
* index a wikilink resolves through, and the link manifest — composes
|
|
207
|
+
* `<base><slug>/`.
|
|
208
|
+
*
|
|
204
209
|
* A content page carries the package the build **derived** (#65). No note
|
|
205
210
|
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
206
211
|
* would publish a page that does not say which package it belongs to. The
|