@heroiclands/package-build 11.1.0 → 14.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 +360 -0
- package/CONTENT.md +144 -72
- package/MIGRATING.md +71 -0
- package/bin/content-build.mjs +0 -20
- package/content-config.mjs +108 -69
- package/docs/content-format.md +50 -27
- package/engine/base-compiler.mjs +6 -1
- package/engine/content-address.mjs +29 -86
- package/engine/frontmatter-lint.mjs +74 -131
- package/engine/manifest-emit.mjs +19 -24
- package/engine/note-vocabulary.mjs +117 -1
- package/engine/retired-fields.mjs +76 -3
- package/engine/site-build.mjs +70 -78
- package/engine/site-index.mjs +20 -8
- package/engine/web-wikilinks.mjs +1 -1
- package/engine/wikilinks.mjs +1 -1
- package/package.json +1 -1
- package/types/content-config.d.mts +28 -50
- package/types/engine/content-address.d.mts +24 -43
- package/types/engine/frontmatter-lint.d.mts +2 -27
- package/types/engine/manifest-emit.d.mts +3 -9
- package/types/engine/note-vocabulary.d.mts +46 -0
- package/types/engine/retired-fields.d.mts +47 -0
- package/types/engine/site-build.d.mts +56 -31
- package/types/engine/site-index.d.mts +9 -4
package/content-config.mjs
CHANGED
|
@@ -68,7 +68,7 @@ import path from "node:path";
|
|
|
68
68
|
|
|
69
69
|
// Leaves with no local imports of their own, so naming them here cannot close
|
|
70
70
|
// a cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
71
|
-
import { isAddressSegment } from "./engine/address-charset.mjs";
|
|
71
|
+
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
|
|
72
72
|
import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
|
|
73
73
|
|
|
74
74
|
/**
|
|
@@ -116,28 +116,38 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
|
|
|
116
116
|
]);
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* A
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
119
|
+
* Address-scheme keys a configuration may no longer declare.
|
|
120
|
+
*
|
|
121
|
+
* A retired key has exactly two possible fates, and only one of them is honest
|
|
122
|
+
* — the same reasoning `engine/retired-fields.mjs` applies to a retired
|
|
123
|
+
* frontmatter field. Left honoured, it keeps doing whatever it did, which is
|
|
124
|
+
* why it was retired. Left *ignored*, it reads to its author as though it still
|
|
125
|
+
* works: the configuration says one thing and the build does another, and
|
|
126
|
+
* nothing says so. This module has no third option, because it has no warning
|
|
127
|
+
* channel — every finding goes through `fail()`, which throws. So a retired
|
|
128
|
+
* key is **refused**, at the line it was written on, with a message that says
|
|
129
|
+
* the mechanism is gone rather than naming a value to correct.
|
|
130
|
+
*
|
|
131
|
+
* **What `landing` did (#204).** It named which note addressed a whole section
|
|
132
|
+
* rather than a page within one — a *landing page*, which therefore had no slug
|
|
133
|
+
* of its own. #203 retired the second of its two rules and #204 retired the
|
|
134
|
+
* concept both rules chose between: a section is a Hugo content directory that
|
|
135
|
+
* the note format does not carry, a page's address names no directory, and so
|
|
136
|
+
* no note lands anything. The key outlived its mechanism by one release only
|
|
137
|
+
* because both publishing consumers still declared the then-true
|
|
138
|
+
* `landing: readme`, and neither breaking them over a correct statement nor
|
|
139
|
+
* accepting the key in silence was acceptable. Neither declares it now.
|
|
140
|
+
*
|
|
141
|
+
* @type {Readonly<Record<string, string>>}
|
|
139
142
|
*/
|
|
140
|
-
export const
|
|
143
|
+
export const RETIRED_ADDRESS_KEYS = Object.freeze({
|
|
144
|
+
landing:
|
|
145
|
+
"is a retired option — delete it. It named which note addressed a " +
|
|
146
|
+
"whole section rather than a page within one, and there are no " +
|
|
147
|
+
"sections to address: a section is a Hugo content directory the note " +
|
|
148
|
+
"format does not carry, so no note lands one and every page is " +
|
|
149
|
+
"addressed `<type>-<shortcode>`. Nothing replaces it",
|
|
150
|
+
});
|
|
141
151
|
|
|
142
152
|
/**
|
|
143
153
|
* A repository's address scheme, with the defaults an unconfigured one gets.
|
|
@@ -148,10 +158,13 @@ export const LANDING_RULES = Object.freeze(["readme", "collection"]);
|
|
|
148
158
|
* own mount point: where the package itself is served is the consuming build's
|
|
149
159
|
* knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
|
|
150
160
|
* resolve time, so it is never recorded here (#1465).
|
|
161
|
+
*
|
|
162
|
+
* It is the whole scheme: `landing`, the key that named which note addressed a
|
|
163
|
+
* whole section, is retired with the sections themselves — see
|
|
164
|
+
* {@link RETIRED_ADDRESS_KEYS}.
|
|
151
165
|
*/
|
|
152
166
|
export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
153
167
|
prefix: "",
|
|
154
|
-
landing: "readme",
|
|
155
168
|
});
|
|
156
169
|
|
|
157
170
|
/**
|
|
@@ -210,34 +223,6 @@ export function publishesContentPages(config) {
|
|
|
210
223
|
return config.publish.site === "content";
|
|
211
224
|
}
|
|
212
225
|
|
|
213
|
-
/**
|
|
214
|
-
* Every URL section this repository names, in declaration order (#197).
|
|
215
|
-
*
|
|
216
|
-
* A section is *declared* by describing it: `site.sections` for one whose
|
|
217
|
-
* landing this build generates, `site.readmeSections` for one whose landing is
|
|
218
|
-
* a `README`. Between them they are the open set of addresses a repository
|
|
219
|
-
* says it publishes under — which is what a `README` landing's `subType` names,
|
|
220
|
-
* since `sectionOf` reads that field as the section rather than as a genre.
|
|
221
|
-
*
|
|
222
|
-
* Read from the same two maps the site build renders each landing from, so the
|
|
223
|
-
* set a note is checked against and the set a landing is written from cannot
|
|
224
|
-
* come to disagree. A repository that describes no section declares none, and
|
|
225
|
-
* the answer is empty rather than a guess assembled from the tree.
|
|
226
|
-
*
|
|
227
|
-
* @param {{site: {sections: object, readmeSections: object}}} config - A
|
|
228
|
-
* resolved configuration.
|
|
229
|
-
* @returns {readonly string[]} The section names, deduplicated.
|
|
230
|
-
*/
|
|
231
|
-
export function declaredSections(config) {
|
|
232
|
-
const site = config?.site ?? {};
|
|
233
|
-
return Object.freeze([
|
|
234
|
-
...new Set([
|
|
235
|
-
...Object.keys(site.sections ?? {}),
|
|
236
|
-
...Object.keys(site.readmeSections ?? {}),
|
|
237
|
-
]),
|
|
238
|
-
]);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
226
|
/**
|
|
242
227
|
* @typedef {"systems" | "modules"} PackageKind
|
|
243
228
|
*/
|
|
@@ -480,8 +465,7 @@ export function declaredSections(config) {
|
|
|
480
465
|
|
|
481
466
|
/**
|
|
482
467
|
* @typedef {object} AddressSchemeInput
|
|
483
|
-
* @property {string} [prefix]
|
|
484
|
-
* @property {string} [landing] Which note addresses a whole section.
|
|
468
|
+
* @property {string} [prefix] Where the content tree mounts inside the package.
|
|
485
469
|
*/
|
|
486
470
|
|
|
487
471
|
/**
|
|
@@ -665,7 +649,7 @@ const SITE_KEYS = [
|
|
|
665
649
|
"backfillSections",
|
|
666
650
|
];
|
|
667
651
|
const SITE_TREE_KEYS = ["from", "section"];
|
|
668
|
-
const SECTION_META_KEYS = ["title", "banner", "description"];
|
|
652
|
+
const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
|
|
669
653
|
const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
670
654
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
671
655
|
const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
|
|
@@ -702,7 +686,7 @@ const STATS_KEYS = ["lastModifiedBy"];
|
|
|
702
686
|
export const DERIVED_SYSTEM_VERSION = Symbol.for("package-build.derivedSystemVersion");
|
|
703
687
|
const PUBLISH_KEYS = ["site", "manifests", "address"];
|
|
704
688
|
const MANIFEST_KEYS = ["publish", "consume"];
|
|
705
|
-
const ADDRESS_KEYS = ["prefix"
|
|
689
|
+
const ADDRESS_KEYS = ["prefix"];
|
|
706
690
|
|
|
707
691
|
/** @param {unknown} value */
|
|
708
692
|
function isPlainObject(value) {
|
|
@@ -1120,10 +1104,11 @@ function normalizeDocs(value) {
|
|
|
1120
1104
|
* One section's landing metadata — what a section says about itself on the
|
|
1121
1105
|
* `_index.md` this build generates for it.
|
|
1122
1106
|
*
|
|
1123
|
-
* A generated landing is the *only* place a section can speak
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1107
|
+
* A generated landing is the *only* place a section can speak, and since #204 it
|
|
1108
|
+
* is the only place a section **exists**: a content page is addressed
|
|
1109
|
+
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1110
|
+
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
1111
|
+
* therefore the whole vocabulary, and it is deliberately a **closed** one.
|
|
1127
1112
|
*
|
|
1128
1113
|
* The alternative — passing whatever a section declared straight through, as
|
|
1129
1114
|
* `site.landing` does — was weighed and refused. `landing` is written once, for
|
|
@@ -1140,9 +1125,36 @@ function normalizeDocs(value) {
|
|
|
1140
1125
|
* add to its title. Each is left off entirely rather than written as
|
|
1141
1126
|
* `undefined`, which is not a value YAML can carry.
|
|
1142
1127
|
*
|
|
1128
|
+
* **`listType` / `listSubType` say what the section lists**
|
|
1129
|
+
* (heroiclands-hugo-theme#50). Since #204 a section's directory holds nothing
|
|
1130
|
+
* but the `_index.md` written here, so a layout reading Hugo's `.Pages` finds
|
|
1131
|
+
* no members and renders an empty landing. The membership survives in this map
|
|
1132
|
+
* and nowhere a theme can reach it, so the landing states it and a layout
|
|
1133
|
+
* substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
|
|
1134
|
+
* catalog layouts already run, which is why `sohl`'s landings never broke.
|
|
1135
|
+
*
|
|
1136
|
+
* They are two keys of their own rather than `type` / `subType` because `type`
|
|
1137
|
+
* on an `_index.md` is **Hugo's own layout selector**: verified against Hugo
|
|
1138
|
+
* 0.165, a section landing carrying `type: doc` renders through
|
|
1139
|
+
* `layouts/doc/list.html` rather than the default list template, so spelling
|
|
1140
|
+
* the content type there would silently change which template serves the
|
|
1141
|
+
* landing. (This build already uses that behaviour deliberately, for the
|
|
1142
|
+
* mount's own landing.)
|
|
1143
|
+
*
|
|
1144
|
+
* Both are checked as **address segments**, which is the trap this came from:
|
|
1145
|
+
* a section is named for the URL a consumer chose and a subType is an address
|
|
1146
|
+
* segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
|
|
1147
|
+
* `userguide` the subType (#207). Copying the section's name into the
|
|
1148
|
+
* declaration would select no page at all, and an empty landing reported by
|
|
1149
|
+
* nobody is the failure being fixed. A `listSubType` with no `listType` is
|
|
1150
|
+
* refused for the same reason: a subType is only distinguishing *within* a
|
|
1151
|
+
* type — `rules`, `userguide` and `reference` are all `doc` — so alone it names
|
|
1152
|
+
* no query.
|
|
1153
|
+
*
|
|
1143
1154
|
* @param {unknown} value - The declared entry.
|
|
1144
1155
|
* @param {string} where - Dotted path, for the error.
|
|
1145
|
-
* @returns {Readonly<{title: string, banner?: string, description?: string
|
|
1156
|
+
* @returns {Readonly<{title: string, banner?: string, description?: string,
|
|
1157
|
+
* listType?: string, listSubType?: string}>}
|
|
1146
1158
|
*/
|
|
1147
1159
|
function normalizeSectionMeta(value, where) {
|
|
1148
1160
|
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
@@ -1155,6 +1167,32 @@ function normalizeSectionMeta(value, where) {
|
|
|
1155
1167
|
if (input.description !== undefined) {
|
|
1156
1168
|
out.description = requireNonEmptyString(input.description, `${where}.description`);
|
|
1157
1169
|
}
|
|
1170
|
+
for (const key of ["listType", "listSubType"]) {
|
|
1171
|
+
if (input[key] === undefined) continue;
|
|
1172
|
+
const segment = requireNonEmptyString(input[key], `${where}.${key}`);
|
|
1173
|
+
if (!isAddressSegment(segment)) {
|
|
1174
|
+
fail(
|
|
1175
|
+
`${where}.${key}`,
|
|
1176
|
+
`is \`${segment}\`, which is not alphanumeric. It names a ` +
|
|
1177
|
+
"content type or subType, and those are address segments " +
|
|
1178
|
+
`(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
|
|
1179
|
+
"own name, which is a URL this site chose and need not " +
|
|
1180
|
+
"match (`user-guide` is the section, `userguide` the " +
|
|
1181
|
+
"subType). A value no page carries selects nothing and " +
|
|
1182
|
+
"leaves the landing empty",
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
out[key] = segment;
|
|
1186
|
+
}
|
|
1187
|
+
if (out.listSubType !== undefined && out.listType === undefined) {
|
|
1188
|
+
fail(
|
|
1189
|
+
`${where}.listSubType`,
|
|
1190
|
+
"is declared without a `listType`. A subType tells pages apart " +
|
|
1191
|
+
"only within a type — `rules`, `userguide` and `reference` " +
|
|
1192
|
+
"are all `doc` — so on its own it names no query for a layout " +
|
|
1193
|
+
"to run",
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1158
1196
|
return Object.freeze(out);
|
|
1159
1197
|
}
|
|
1160
1198
|
|
|
@@ -1737,6 +1775,15 @@ function normalizePublish(value) {
|
|
|
1737
1775
|
fail("publish.address", "must be an object");
|
|
1738
1776
|
}
|
|
1739
1777
|
const address = /** @type {Record<string, unknown>} */ (addressInput ?? {});
|
|
1778
|
+
// A retired key is refused by name, ahead of the vocabulary check: reported
|
|
1779
|
+
// as merely unrecognized it would read as a misspelling of the one key that
|
|
1780
|
+
// survives, and the author would correct the spelling rather than learn
|
|
1781
|
+
// that the mechanism is gone (#215).
|
|
1782
|
+
for (const key of Object.keys(address)) {
|
|
1783
|
+
if (Object.hasOwn(RETIRED_ADDRESS_KEYS, key)) {
|
|
1784
|
+
fail(`publish.address.${key}`, RETIRED_ADDRESS_KEYS[key]);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1740
1787
|
rejectUnknownKeys(address, ADDRESS_KEYS, "publish.address.");
|
|
1741
1788
|
|
|
1742
1789
|
const prefix =
|
|
@@ -1755,17 +1802,9 @@ function normalizePublish(value) {
|
|
|
1755
1802
|
fail("publish.address.prefix", "must not begin with a slash");
|
|
1756
1803
|
}
|
|
1757
1804
|
|
|
1758
|
-
const landing =
|
|
1759
|
-
address.landing === undefined ?
|
|
1760
|
-
DEFAULT_ADDRESS_SCHEME.landing
|
|
1761
|
-
: optionalString(address.landing, "publish.address.landing");
|
|
1762
|
-
if (!LANDING_RULES.includes(landing)) {
|
|
1763
|
-
fail("publish.address.landing", `must be one of ${LANDING_RULES.join(", ")}`);
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
1805
|
return Object.freeze({
|
|
1767
1806
|
site: normalizeSiteMode(publish.site),
|
|
1768
|
-
address: Object.freeze({ prefix
|
|
1807
|
+
address: Object.freeze({ prefix }),
|
|
1769
1808
|
manifests: Object.freeze({
|
|
1770
1809
|
publish: optionalBoolean(manifests.publish, "publish.manifests.publish", false),
|
|
1771
1810
|
consume: optionalBoolean(manifests.consume, "publish.manifests.consume", false),
|
package/docs/content-format.md
CHANGED
|
@@ -233,10 +233,32 @@ field to supply it, so a bare shortcode is resolved across types and must match
|
|
|
233
233
|
exactly one; an ambiguity is an error naming the candidates.
|
|
234
234
|
|
|
235
235
|
**Parsing is positional counting from the right, and nothing else.** Every
|
|
236
|
-
segment is alphanumeric — shortcodes
|
|
237
|
-
systems come from a closed registry, and `contentPackage` is
|
|
238
|
-
the hyphen is purely a separator. There is no longest-match
|
|
239
|
-
no vocabulary check before splitting.
|
|
236
|
+
segment is alphanumeric — shortcodes, **types** and **subTypes** are all
|
|
237
|
+
`^[A-Za-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
|
|
238
|
+
alphanumeric — so the hyphen is purely a separator. There is no longest-match
|
|
239
|
+
against a roster and no vocabulary check before splitting.
|
|
240
|
+
|
|
241
|
+
**`type` and `subType` are held to that charset, not merely expected to meet
|
|
242
|
+
it** (#206). A type is the first segment of every address, so a hyphen in one is
|
|
243
|
+
read back as a segment boundary that was never meant as one. A `subType` reaches
|
|
244
|
+
no address since #204 retired sections, but it is held to the same rule all the
|
|
245
|
+
same: it is a vocabulary term the whole toolchain keys on, one closed set away
|
|
246
|
+
from being an address again, and a charset that holds for two of the three
|
|
247
|
+
segments and half of a fourth is a rule nobody can state. Both are checked
|
|
248
|
+
against the same constant a shortcode is checked against, and a note carrying a
|
|
249
|
+
hyphenated value is reported where it wrote it:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
Trauma/Blood_Loss.md:3:1: error: `subType` "blood-loss" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. It is a vocabulary term the whole toolchain keys on, and one closed set away from being an address segment again, so a charset that held for every term but this one would be a rule nobody could state in a sentence
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
One declared value broke the rule and has been renamed: a `doc`'s `user-guide`
|
|
256
|
+
is now **`userguide`**. The old spelling was accepted for one transitional
|
|
257
|
+
release, as a warning naming the replacement, so the 43 `sohl` notes authoring
|
|
258
|
+
it were not invalidated by the release that renamed them. Every consumer tree
|
|
259
|
+
has swept, so the acceptance is gone (#210) and `user-guide` is refused by the
|
|
260
|
+
charset check — it contains a hyphen, which is the reason that always applied.
|
|
261
|
+
No retirement-specific code outlived the sweep.
|
|
240
262
|
|
|
241
263
|
That is a guarantee rather than an observation, and it holds: of **4,456 distinct
|
|
242
264
|
shortcodes** across the four content trees, not one contains a character outside
|
|
@@ -267,7 +289,7 @@ was measured before it was retired, and the namespace was empty in practice:
|
|
|
267
289
|
across 8,305 wikilinks in three content trees, **not one** bare link resolved to
|
|
268
290
|
a note. What the index behind it did do was fold every note's `name.full` into
|
|
269
291
|
itself, so two notes of one type could not share a display name — a rules page
|
|
270
|
-
and a user
|
|
292
|
+
and a user guide page both called "Gear" were a build failure whose every
|
|
271
293
|
available fix moved a published URL (#179).
|
|
272
294
|
|
|
273
295
|
The top-level `aliases:` that fed it is **retired** and refused. The nested
|
|
@@ -1356,29 +1378,30 @@ subType:
|
|
|
1356
1378
|
subType:
|
|
1357
1379
|
|
|
1358
1380
|
- rules: The rules of the game, independent of medium — valid at a table with paper and dice.
|
|
1359
|
-
-
|
|
1381
|
+
- userguide: How to operate the Foundry implementation to play by the rules.
|
|
1360
1382
|
- reference: Out-of-world lookup material about the setting or system — correspondences, conversions, glossaries.
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1383
|
+
|
|
1384
|
+
A `doc` declares no properties of its own.
|
|
1385
|
+
|
|
1386
|
+
**A page that introduces a type is an ordinary note, named by convention.**
|
|
1387
|
+
Write `type: doc`, `subType: reference`, `shortcode: <type>` — so the
|
|
1388
|
+
affiliations introduction is `doc-affiliation`, addressed and linked like
|
|
1389
|
+
anything else, and typically carrying a generated table of what it introduces.
|
|
1390
|
+
It has no build path of its own; the package's own front page already works this
|
|
1391
|
+
way (`homepage-root`).
|
|
1392
|
+
|
|
1393
|
+
There is no landing page and no section. A `README.md` used to _be_ its
|
|
1394
|
+
section's landing, and a `subType: collection` note with a top-level `section:`
|
|
1395
|
+
key was a second way to say the same thing. All of it is retired — the second
|
|
1396
|
+
rule in #202, the first in #204 — because a section appears in **no address**: a
|
|
1397
|
+
page publishes at `/<package>/<type>-<shortcode>/`, which names no directory. A
|
|
1398
|
+
section is what Hugo calls a content directory, and the note format does not
|
|
1399
|
+
carry one.
|
|
1400
|
+
|
|
1401
|
+
So a `doc`'s `subType` is a **genre** and nothing else, closed to the three
|
|
1402
|
+
values above. It briefly had to accept a content type as well, because a
|
|
1403
|
+
landing's `subType` named the section it addressed; with no landings, one field
|
|
1404
|
+
has one reading again.
|
|
1382
1405
|
|
|
1383
1406
|
### type: macro
|
|
1384
1407
|
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -77,7 +77,11 @@ import {
|
|
|
77
77
|
} from "./helpers.mjs";
|
|
78
78
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
79
79
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
assertNoAliasesField,
|
|
82
|
+
assertNoDraftField,
|
|
83
|
+
assertNoSectionField,
|
|
84
|
+
} from "./retired-fields.mjs";
|
|
81
85
|
import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
82
86
|
import { carriesSystemBlock } from "./system-block.mjs";
|
|
83
87
|
import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
|
|
@@ -784,6 +788,7 @@ export class BasePackCompiler {
|
|
|
784
788
|
assertNoDeclaredPackage(fm, { absPath });
|
|
785
789
|
assertNoDraftField(fm, { absPath });
|
|
786
790
|
assertNoAliasesField(fm, { absPath });
|
|
791
|
+
assertNoSectionField(fm, { absPath });
|
|
787
792
|
} catch (err) {
|
|
788
793
|
stats.declined++;
|
|
789
794
|
this.errorCount++;
|
|
@@ -32,30 +32,16 @@
|
|
|
32
32
|
// The scheme vocabulary is part of the configuration contract — a
|
|
33
33
|
// repository names its scheme in `package-build.config.yaml` — so it is
|
|
34
34
|
// declared beside the rest of that vocabulary rather than here, and this
|
|
35
|
-
// module
|
|
36
|
-
// but `node:path` and `engine/ids.mjs`,
|
|
37
|
-
// cycle (see `engine/pack-config.mjs`).
|
|
38
|
-
import { DEFAULT_ADDRESS_SCHEME
|
|
35
|
+
// module re-exports it beside the addresses it derives. `config.mjs` is the
|
|
36
|
+
// leaf entry point and imports nothing but `node:path` and `engine/ids.mjs`,
|
|
37
|
+
// so the direction cannot close a cycle (see `engine/pack-config.mjs`).
|
|
38
|
+
import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
|
|
39
39
|
|
|
40
|
-
export { DEFAULT_ADDRESS_SCHEME
|
|
40
|
+
export { DEFAULT_ADDRESS_SCHEME };
|
|
41
41
|
|
|
42
42
|
/** The knowledgebase's mount within this package's site (#1470). */
|
|
43
43
|
export const KB_PREFIX = "kb/";
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* The URL section a note routes to.
|
|
47
|
-
*
|
|
48
|
-
* A `doc` is narrative content whose only identity is its subtype label, so it
|
|
49
|
-
* routes by `subType`; every other type names its own section.
|
|
50
|
-
*
|
|
51
|
-
* @param {object} fm - Parsed frontmatter.
|
|
52
|
-
* @returns {string|undefined} The section, or `undefined` when the note has
|
|
53
|
-
* none — a `doc` with no subtype has no address and is not published.
|
|
54
|
-
*/
|
|
55
|
-
export function sectionOf(fm) {
|
|
56
|
-
return fm.type === "doc" ? fm.subType : fm.type;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
45
|
/**
|
|
60
46
|
* The single path segment a note is addressed by: `type-shortcode`.
|
|
61
47
|
*
|
|
@@ -95,40 +81,17 @@ export function addressSlug(fm) {
|
|
|
95
81
|
/**
|
|
96
82
|
* A note's address below the knowledgebase mount, e.g. `affliction-aconite/`.
|
|
97
83
|
*
|
|
98
|
-
* A `README.md`
|
|
99
|
-
*
|
|
84
|
+
* Every note, without exception. A `README.md` used to be its section's landing
|
|
85
|
+
* page and to address the section instead of itself; a section is a Hugo
|
|
86
|
+
* directory concept the note format no longer carries (#204), so a file's name
|
|
87
|
+
* decides nothing about where it publishes.
|
|
100
88
|
*
|
|
101
89
|
* @param {object} fm - Parsed frontmatter.
|
|
102
|
-
* @param {boolean} isReadme - Whether the file is a `README.md`.
|
|
103
90
|
* @returns {string} The mount-relative address, with a trailing slash.
|
|
104
91
|
* @throws {Error} When the note has no address.
|
|
105
92
|
*/
|
|
106
|
-
export function contentAddress(fm
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Whether a note is a landing page under a scheme, and what it lands at.
|
|
112
|
-
*
|
|
113
|
-
* @param {object} fm - Parsed frontmatter.
|
|
114
|
-
* @param {boolean} isReadme - Whether the file is a `README.md`.
|
|
115
|
-
* @param {string} landing - The landing rule, one of {@link LANDING_RULES}.
|
|
116
|
-
* @returns {{landing: true, segment: string}|{landing: false}|{landing: true, segment: undefined}}
|
|
117
|
-
* `segment` is the single path segment the note addresses. A `collection`
|
|
118
|
-
* note that declares no `section` is a landing page with no segment — an
|
|
119
|
-
* error rather than a page, since it names nowhere to land.
|
|
120
|
-
*/
|
|
121
|
-
function landingOf(fm, isReadme, landing) {
|
|
122
|
-
if (landing === "readme") {
|
|
123
|
-
return isReadme ? { landing: true, segment: sectionOf(fm) } : { landing: false };
|
|
124
|
-
}
|
|
125
|
-
// `collection`. The section is authored rather than derived: it is the
|
|
126
|
-
// identity of the section being introduced, and the note's own title
|
|
127
|
-
// ("Creatures") is presentation, which would slug to something else.
|
|
128
|
-
if (fm.type === "doc" && fm.subType === "collection") {
|
|
129
|
-
return { landing: true, segment: fm.section || fm.slug };
|
|
130
|
-
}
|
|
131
|
-
return { landing: false };
|
|
93
|
+
export function contentAddress(fm) {
|
|
94
|
+
return `${addressSlug(fm)}/`;
|
|
132
95
|
}
|
|
133
96
|
|
|
134
97
|
/**
|
|
@@ -139,52 +102,32 @@ function landingOf(fm, isReadme, landing) {
|
|
|
139
102
|
* address the site does not publish resolves at build time and 404s for the
|
|
140
103
|
* reader, which is the failure this module exists to prevent.
|
|
141
104
|
*
|
|
105
|
+
* **It is a pure function of the frontmatter.** Nothing about the file the note
|
|
106
|
+
* was read from reaches it: the `README.md` convention that made one note
|
|
107
|
+
* address a whole section is retired with the section itself (#204), so there
|
|
108
|
+
* is one rule and no branch.
|
|
109
|
+
*
|
|
142
110
|
* **The prefix does not apply to a page's own address.** `prefix` says where the
|
|
143
|
-
* content tree *mounts inside the package* —
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `/sohl/kb/affliction/`. The `type-` half is what keeps that flat namespace
|
|
149
|
-
* clear of the package's fixed mounts — `/<package>/` for the landing,
|
|
111
|
+
* content tree *mounts inside the package* — the Hugo directory its pages are
|
|
112
|
+
* written under — and an address is `(type, shortcode)`, a package-wide identity
|
|
113
|
+
* that takes no mount: `sohl` publishes `/sohl/affliction-aconite/` from a file
|
|
114
|
+
* written under `kb/`. The `type-` half is what keeps that flat namespace clear
|
|
115
|
+
* of the package's fixed mounts — `/<package>/` for the landing,
|
|
150
116
|
* `/<package>/api/` for generated API docs, neither of which contains a hyphen
|
|
151
117
|
* or names a type.
|
|
152
118
|
*
|
|
153
|
-
* **
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
119
|
+
* **It takes no address scheme.** It took one until #215, to validate the
|
|
120
|
+
* `landing` rule it then discarded; with that key retired, `prefix` was the
|
|
121
|
+
* only thing left in the scheme and the paragraph above is the reason it never
|
|
122
|
+
* applied. A parameter read by nothing is the defect this deletion is about.
|
|
157
123
|
*
|
|
158
124
|
* @param {object} fm - Parsed frontmatter.
|
|
159
|
-
* @param {object} [options] - Options.
|
|
160
|
-
* @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
|
|
161
|
-
* @param {{prefix?: string, landing?: string}} [options.scheme] - The
|
|
162
|
-
* repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
|
|
163
125
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
164
126
|
* leading one.
|
|
165
|
-
* @throws {Error} When the note has no
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* in the manifest.
|
|
127
|
+
* @throws {Error} When the note has no type or no shortcode to be addressed by.
|
|
128
|
+
* Such a note is not published, and inventing an address for one would put a
|
|
129
|
+
* dead entry in the manifest.
|
|
169
130
|
*/
|
|
170
|
-
export function packageAddress(fm
|
|
171
|
-
const { prefix, landing } = { ...DEFAULT_ADDRESS_SCHEME, ...scheme };
|
|
172
|
-
if (!LANDING_RULES.includes(landing)) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`unknown landing rule ${JSON.stringify(landing)} — expected one ` +
|
|
175
|
-
`of ${LANDING_RULES.join(", ")}`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
const land = landingOf(fm, isReadme, landing);
|
|
179
|
-
if (land.landing) {
|
|
180
|
-
if (typeof land.segment !== "string" || !land.segment) {
|
|
181
|
-
throw new Error(`landing note declares no section, so it lands nowhere`);
|
|
182
|
-
}
|
|
183
|
-
return `${prefix}${land.segment}/`;
|
|
184
|
-
}
|
|
185
|
-
const sec = sectionOf(fm);
|
|
186
|
-
if (typeof sec !== "string" || !sec) {
|
|
187
|
-
throw new Error(`type "${fm.type}" has no section`);
|
|
188
|
-
}
|
|
131
|
+
export function packageAddress(fm) {
|
|
189
132
|
return `${addressSlug(fm)}/`;
|
|
190
133
|
}
|