@heroiclands/package-build 11.0.0 → 13.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 +279 -0
- package/CONTENT.md +96 -49
- package/MIGRATING.md +65 -0
- package/content-config.mjs +51 -22
- package/docs/content-format.md +51 -27
- package/engine/base-compiler.mjs +6 -1
- package/engine/content-address.mjs +24 -73
- package/engine/frontmatter-lint.mjs +92 -1
- package/engine/manifest-emit.mjs +16 -18
- package/engine/note-vocabulary.mjs +159 -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 +29 -17
- package/types/engine/content-address.d.mts +22 -34
- package/types/engine/note-vocabulary.d.mts +76 -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
|
@@ -116,28 +116,45 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
|
|
|
116
116
|
]);
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* The landing-page rules a repository may route by.
|
|
119
|
+
* The landing-page rules a repository may route by. **Inert since #204.**
|
|
120
120
|
*
|
|
121
|
-
* A *landing page*
|
|
122
|
-
* within one, so it
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* published:
|
|
121
|
+
* A *landing page* was a note that addressed a whole section rather than a page
|
|
122
|
+
* within one, so it had no slug of its own. There are no sections in the note
|
|
123
|
+
* format any more — a section is a Hugo content directory, and a page's address
|
|
124
|
+
* names no directory — so there are no landings and this selects nothing.
|
|
126
125
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* The two are not disjoint and cannot simply both apply: each tree holds notes
|
|
136
|
-
* the other rule would move.
|
|
126
|
+
* The key survives its own mechanism on purpose. Both publishing consumers
|
|
127
|
+
* declare `landing: readme`, which stated something true when they wrote it;
|
|
128
|
+
* refusing it now would break them over a correct statement, and silently
|
|
129
|
+
* ignoring an unknown value would be worse. So `readme` stays accepted, the
|
|
130
|
+
* retired `collection` stays refused by name (below), and the key is deleted
|
|
131
|
+
* once no configuration writes it — `content-config.mjs` has no warning channel
|
|
132
|
+
* with which to say "accepted, and does nothing" in between.
|
|
137
133
|
*
|
|
138
134
|
* @type {readonly string[]}
|
|
139
135
|
*/
|
|
140
|
-
export const LANDING_RULES = Object.freeze(["readme"
|
|
136
|
+
export const LANDING_RULES = Object.freeze(["readme"]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What a configuration naming the retired `collection` landing rule is told.
|
|
140
|
+
*
|
|
141
|
+
* A retired *value* is refused the way a retired *field* is (see
|
|
142
|
+
* `engine/retired-fields.mjs`): left merely unrecognized it would be reported
|
|
143
|
+
* as a bad value, which names something to correct and leaves the author to
|
|
144
|
+
* work out for themselves that the mechanism is gone. The message says the rule
|
|
145
|
+
* is retired, what lands a section instead, and what to do with the key.
|
|
146
|
+
*
|
|
147
|
+
* @type {Readonly<Record<string, string>>}
|
|
148
|
+
*/
|
|
149
|
+
export const RETIRED_LANDING_RULES = Object.freeze({
|
|
150
|
+
collection:
|
|
151
|
+
"the `collection` landing rule is retired, and so is the mechanism it " +
|
|
152
|
+
"chose between: a section is a Hugo directory the note format does not " +
|
|
153
|
+
"carry, so no note lands one. Delete this key. A page that introduces " +
|
|
154
|
+
"the notes of a type is an ordinary note — `type: doc`, " +
|
|
155
|
+
"`subType: reference`, `shortcode: <type>` — addressed `doc-<type>`; " +
|
|
156
|
+
"the `section:` frontmatter key the rule read is retired with it",
|
|
157
|
+
});
|
|
141
158
|
|
|
142
159
|
/**
|
|
143
160
|
* A repository's address scheme, with the defaults an unconfigured one gets.
|
|
@@ -148,6 +165,8 @@ export const LANDING_RULES = Object.freeze(["readme", "collection"]);
|
|
|
148
165
|
* own mount point: where the package itself is served is the consuming build's
|
|
149
166
|
* knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
|
|
150
167
|
* resolve time, so it is never recorded here (#1465).
|
|
168
|
+
*
|
|
169
|
+
* `landing` is inert — see {@link LANDING_RULES}.
|
|
151
170
|
*/
|
|
152
171
|
export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
153
172
|
prefix: "",
|
|
@@ -453,7 +472,9 @@ export function publishesContentPages(config) {
|
|
|
453
472
|
/**
|
|
454
473
|
* @typedef {object} AddressSchemeInput
|
|
455
474
|
* @property {string} [prefix] Where the content tree mounts inside the package.
|
|
456
|
-
* @property {string} [landing] Which note
|
|
475
|
+
* @property {string} [landing] Which note addressed a whole section. Inert
|
|
476
|
+
* since #204 retired sections from the note format — see
|
|
477
|
+
* {@link LANDING_RULES}.
|
|
457
478
|
*/
|
|
458
479
|
|
|
459
480
|
/**
|
|
@@ -1092,10 +1113,11 @@ function normalizeDocs(value) {
|
|
|
1092
1113
|
* One section's landing metadata — what a section says about itself on the
|
|
1093
1114
|
* `_index.md` this build generates for it.
|
|
1094
1115
|
*
|
|
1095
|
-
* A generated landing is the *only* place a section can speak
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1116
|
+
* A generated landing is the *only* place a section can speak, and since #204 it
|
|
1117
|
+
* is the only place a section **exists**: a content page is addressed
|
|
1118
|
+
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1119
|
+
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
1120
|
+
* therefore the whole vocabulary, and it is deliberately a **closed** one.
|
|
1099
1121
|
*
|
|
1100
1122
|
* The alternative — passing whatever a section declared straight through, as
|
|
1101
1123
|
* `site.landing` does — was weighed and refused. `landing` is written once, for
|
|
@@ -1731,6 +1753,13 @@ function normalizePublish(value) {
|
|
|
1731
1753
|
address.landing === undefined ?
|
|
1732
1754
|
DEFAULT_ADDRESS_SCHEME.landing
|
|
1733
1755
|
: optionalString(address.landing, "publish.address.landing");
|
|
1756
|
+
// A retired rule is refused by name, before the vocabulary check: reported
|
|
1757
|
+
// as merely unrecognized it would read as a misspelling of the one that
|
|
1758
|
+
// survives, and the author would correct the value rather than learn that
|
|
1759
|
+
// the mechanism is gone (#202).
|
|
1760
|
+
if (Object.hasOwn(RETIRED_LANDING_RULES, landing)) {
|
|
1761
|
+
fail("publish.address.landing", RETIRED_LANDING_RULES[landing]);
|
|
1762
|
+
}
|
|
1734
1763
|
if (!LANDING_RULES.includes(landing)) {
|
|
1735
1764
|
fail("publish.address.landing", `must be one of ${LANDING_RULES.join(", ")}`);
|
|
1736
1765
|
}
|
package/docs/content-format.md
CHANGED
|
@@ -233,10 +233,33 @@ 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 an address segment — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a shortcode is held to. The hyphen separates the segments of an address, so a value containing one is read back as two segments and resolves to nothing
|
|
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 is accepted for one transitional
|
|
257
|
+
release and reported as a **warning** naming the replacement, so a tree that has
|
|
258
|
+
not yet swept still builds:
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
User_Guide/Actions.md:3:1: warning: `subType` "user-guide" is a retired spelling of "userguide" on a doc; write "userguide". …
|
|
262
|
+
```
|
|
240
263
|
|
|
241
264
|
That is a guarantee rather than an observation, and it holds: of **4,456 distinct
|
|
242
265
|
shortcodes** across the four content trees, not one contains a character outside
|
|
@@ -267,7 +290,7 @@ was measured before it was retired, and the namespace was empty in practice:
|
|
|
267
290
|
across 8,305 wikilinks in three content trees, **not one** bare link resolved to
|
|
268
291
|
a note. What the index behind it did do was fold every note's `name.full` into
|
|
269
292
|
itself, so two notes of one type could not share a display name — a rules page
|
|
270
|
-
and a user
|
|
293
|
+
and a user guide page both called "Gear" were a build failure whose every
|
|
271
294
|
available fix moved a published URL (#179).
|
|
272
295
|
|
|
273
296
|
The top-level `aliases:` that fed it is **retired** and refused. The nested
|
|
@@ -1356,29 +1379,30 @@ subType:
|
|
|
1356
1379
|
subType:
|
|
1357
1380
|
|
|
1358
1381
|
- rules: The rules of the game, independent of medium — valid at a table with paper and dice.
|
|
1359
|
-
-
|
|
1382
|
+
- userguide: How to operate the Foundry implementation to play by the rules.
|
|
1360
1383
|
- 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
|
-
|
|
1384
|
+
|
|
1385
|
+
A `doc` declares no properties of its own.
|
|
1386
|
+
|
|
1387
|
+
**A page that introduces a type is an ordinary note, named by convention.**
|
|
1388
|
+
Write `type: doc`, `subType: reference`, `shortcode: <type>` — so the
|
|
1389
|
+
affiliations introduction is `doc-affiliation`, addressed and linked like
|
|
1390
|
+
anything else, and typically carrying a generated table of what it introduces.
|
|
1391
|
+
It has no build path of its own; the package's own front page already works this
|
|
1392
|
+
way (`homepage-root`).
|
|
1393
|
+
|
|
1394
|
+
There is no landing page and no section. A `README.md` used to _be_ its
|
|
1395
|
+
section's landing, and a `subType: collection` note with a top-level `section:`
|
|
1396
|
+
key was a second way to say the same thing. All of it is retired — the second
|
|
1397
|
+
rule in #202, the first in #204 — because a section appears in **no address**: a
|
|
1398
|
+
page publishes at `/<package>/<type>-<shortcode>/`, which names no directory. A
|
|
1399
|
+
section is what Hugo calls a content directory, and the note format does not
|
|
1400
|
+
carry one.
|
|
1401
|
+
|
|
1402
|
+
So a `doc`'s `subType` is a **genre** and nothing else, closed to the three
|
|
1403
|
+
values above. It briefly had to accept a content type as well, because a
|
|
1404
|
+
landing's `subType` named the section it addressed; with no landings, one field
|
|
1405
|
+
has one reading again.
|
|
1382
1406
|
|
|
1383
1407
|
### type: macro
|
|
1384
1408
|
|
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++;
|
|
@@ -42,20 +42,6 @@ export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
|
|
|
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,40 @@ 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
|
-
* **The section still decides where the *file* is written**, which is why a
|
|
154
|
-
* note without one still has no address: Hugo derives a section from a page's
|
|
155
|
-
* directory rather than from its URL, so a page with nowhere to be filed is a
|
|
156
|
-
* page with no section landing, no `.CurrentSection` and no per-section layout.
|
|
157
|
-
*
|
|
158
119
|
* @param {object} fm - Parsed frontmatter.
|
|
159
120
|
* @param {object} [options] - Options.
|
|
160
|
-
* @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
|
|
161
121
|
* @param {{prefix?: string, landing?: string}} [options.scheme] - The
|
|
162
122
|
* repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
|
|
123
|
+
* `landing` is validated against {@link LANDING_RULES} and selects nothing —
|
|
124
|
+
* it is accepted so a configuration declaring the still-true `landing: readme`
|
|
125
|
+
* keeps loading, and is removed once none does.
|
|
163
126
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
164
127
|
* leading one.
|
|
165
|
-
* @throws {Error} When the note has no
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* in the manifest.
|
|
128
|
+
* @throws {Error} When the note has no type or no shortcode to be addressed by.
|
|
129
|
+
* Such a note is not published, and inventing an address for one would put a
|
|
130
|
+
* dead entry in the manifest.
|
|
169
131
|
*/
|
|
170
|
-
export function packageAddress(fm, {
|
|
171
|
-
const {
|
|
132
|
+
export function packageAddress(fm, { scheme } = {}) {
|
|
133
|
+
const { landing } = { ...DEFAULT_ADDRESS_SCHEME, ...scheme };
|
|
172
134
|
if (!LANDING_RULES.includes(landing)) {
|
|
173
135
|
throw new Error(
|
|
174
136
|
`unknown landing rule ${JSON.stringify(landing)} — expected one ` +
|
|
175
137
|
`of ${LANDING_RULES.join(", ")}`,
|
|
176
138
|
);
|
|
177
139
|
}
|
|
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
|
-
}
|
|
189
140
|
return `${addressSlug(fm)}/`;
|
|
190
141
|
}
|
|
@@ -60,7 +60,14 @@ import { resolveFieldValue, SYSTEM_BLOCK_KEYS, unknownBlockKeys } from "./system
|
|
|
60
60
|
import { positionInFrontmatter, positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
61
61
|
import { checkHomepageAddressFields } from "./homepage.mjs";
|
|
62
62
|
import { RETIRED_TYPES } from "./ids.mjs";
|
|
63
|
-
import {
|
|
63
|
+
import { isAddressSegment } from "./address-charset.mjs";
|
|
64
|
+
import {
|
|
65
|
+
declaredTags,
|
|
66
|
+
retiredSubType,
|
|
67
|
+
retiredSubTypeMessage,
|
|
68
|
+
subTypeCharsetMessage,
|
|
69
|
+
typeCharsetMessage,
|
|
70
|
+
} from "./note-vocabulary.mjs";
|
|
64
71
|
import {
|
|
65
72
|
RETIRED_FIELD_ALIASES,
|
|
66
73
|
declaresRetiredAlias,
|
|
@@ -69,6 +76,7 @@ import {
|
|
|
69
76
|
draftRetiredMessage,
|
|
70
77
|
readAliasedField,
|
|
71
78
|
retiredAliasMessage,
|
|
79
|
+
sectionRetiredMessage,
|
|
72
80
|
} from "./retired-fields.mjs";
|
|
73
81
|
|
|
74
82
|
/**
|
|
@@ -322,6 +330,24 @@ function checkDataContainer(note, { type, fields }) {
|
|
|
322
330
|
* but it is not open like the rest of that region: a type either declares a
|
|
323
331
|
* `subType` or does not, and a type that does declares its values.
|
|
324
332
|
*
|
|
333
|
+
* **It is a genre, and only a genre.** #197 gave the field a second reading: a
|
|
334
|
+
* `README.md` was its section's landing page, and the segment it landed at was
|
|
335
|
+
* its `subType`, so the value had to be checked against the sections that could
|
|
336
|
+
* exist — every content type, plus whatever a repository configured — rather
|
|
337
|
+
* than against the genres its type declares. Two vocabularies in one field is
|
|
338
|
+
* what #198, #200 and #201 were each spent on, and #204 removed the cause rather
|
|
339
|
+
* than the symptom: a section is a Hugo directory the note format does not
|
|
340
|
+
* carry, and a page introducing a type is an ordinary note addressed
|
|
341
|
+
* `doc-<type>`. So the closed list answers for every note, whatever it is
|
|
342
|
+
* called, and `rules`, `userguide`, `reference` mean three genres and nothing
|
|
343
|
+
* else.
|
|
344
|
+
*
|
|
345
|
+
* **Three checks, in this order** — retired spelling, then charset, then the
|
|
346
|
+
* closed set (#206, #204). Each is ahead of the next because it is the more
|
|
347
|
+
* specific statement about the same value: a retired spelling has a named
|
|
348
|
+
* replacement, a hyphenated value is unaddressable whatever the type declares,
|
|
349
|
+
* and only then is the type's own list the reason.
|
|
350
|
+
*
|
|
325
351
|
* @param {object} note - The note.
|
|
326
352
|
* @param {object} opts
|
|
327
353
|
* @param {string} opts.type - The note's type, for the message.
|
|
@@ -348,6 +374,41 @@ function checkSubType(note, { type, entry }) {
|
|
|
348
374
|
];
|
|
349
375
|
}
|
|
350
376
|
|
|
377
|
+
// A retired spelling is **accepted**, and said out loud (#206). Checked
|
|
378
|
+
// before the charset, because the note is not wrong about the charset in
|
|
379
|
+
// some general way — it is wrong about one value, and naming the
|
|
380
|
+
// replacement is the whole of what the author needs. A warning rather than
|
|
381
|
+
// an error for the same reason the retired field aliases below are: the
|
|
382
|
+
// note compiles to the correct page, and erroring would red every tree the
|
|
383
|
+
// moment it took this release, ahead of any chance to sweep.
|
|
384
|
+
const replacement = retiredSubType(type, value);
|
|
385
|
+
if (replacement) {
|
|
386
|
+
return [
|
|
387
|
+
{
|
|
388
|
+
file: note.file,
|
|
389
|
+
...at,
|
|
390
|
+
severity: "warning",
|
|
391
|
+
message: retiredSubTypeMessage(type, value, replacement),
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The charset, before the closed set: a hyphenated value is unaddressable
|
|
397
|
+
// whatever the type declares, and the type's list is not the reason it is
|
|
398
|
+
// refused. Reported here rather than only for an enumerated type, so a
|
|
399
|
+
// `subTypes: null` type — whose values nothing may yet check — is still
|
|
400
|
+
// held to the one rule that does not depend on knowing them.
|
|
401
|
+
if (!isAddressSegment(value)) {
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
file: note.file,
|
|
405
|
+
...at,
|
|
406
|
+
severity: "error",
|
|
407
|
+
message: subTypeCharsetMessage(value),
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
|
|
351
412
|
const values = entry.subTypes;
|
|
352
413
|
// `null` is "declared, values not yet enumerated" — presence is legal and
|
|
353
414
|
// the value is nobody's to check yet.
|
|
@@ -468,6 +529,17 @@ export function lintNote(note, { schemas, index, vocabulary, systems = DEFAULT_S
|
|
|
468
529
|
message: draftRetiredMessage(),
|
|
469
530
|
});
|
|
470
531
|
}
|
|
532
|
+
// Anchored at column 1 for the same reason `aliases` is: `section` names a
|
|
533
|
+
// configuration key too (`site.trees[].section`), and a nested one under
|
|
534
|
+
// some other block is not this field (#202).
|
|
535
|
+
if (Object.hasOwn(fm, "section")) {
|
|
536
|
+
findings.push({
|
|
537
|
+
file: note.file,
|
|
538
|
+
...positionInFrontmatter(raw(), "section", undefined, { topLevel: true }),
|
|
539
|
+
severity: "error",
|
|
540
|
+
message: sectionRetiredMessage(),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
471
543
|
// Only the top-level `aliases` is retired. `name.aliases` writes the same
|
|
472
544
|
// key indented under `name:` and is **permitted** — reserved and unread —
|
|
473
545
|
// so both the test and the locator are anchored at column 1 (#180).
|
|
@@ -499,6 +571,25 @@ export function lintNote(note, { schemas, index, vocabulary, systems = DEFAULT_S
|
|
|
499
571
|
});
|
|
500
572
|
}
|
|
501
573
|
|
|
574
|
+
// The type's charset, before anything that looks the type up (#206). A
|
|
575
|
+
// hyphenated type is unaddressable, and every lookup below would report it
|
|
576
|
+
// as a type nobody declared — true, but not the reason, and it would send
|
|
577
|
+
// the author to declare one rather than to rename it.
|
|
578
|
+
//
|
|
579
|
+
// Guarded on a non-empty type: an absent one is a missing key, not a
|
|
580
|
+
// charset violation, and is reported as the missing schema it causes.
|
|
581
|
+
// There is **no transitional path** here, deliberately: no tree authors a
|
|
582
|
+
// hyphenated type, so an acceptance would guard a case that does not exist.
|
|
583
|
+
if (type && !isAddressSegment(type)) {
|
|
584
|
+
findings.push({
|
|
585
|
+
file: note.file,
|
|
586
|
+
...at("type", type),
|
|
587
|
+
severity: "error",
|
|
588
|
+
message: typeCharsetMessage(type),
|
|
589
|
+
});
|
|
590
|
+
return findings;
|
|
591
|
+
}
|
|
592
|
+
|
|
502
593
|
const replacement = RETIRED_TYPES[type];
|
|
503
594
|
if (replacement) {
|
|
504
595
|
findings.push({
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -32,19 +32,16 @@
|
|
|
32
32
|
* **An entry's `path` is derivable from the key it is filed under** (#181).
|
|
33
33
|
* `sohl-affliction-aconite` publishes at `affliction-aconite/`, because a page's
|
|
34
34
|
* URL *is* its address; nothing in it comes from a display name, so a rename
|
|
35
|
-
* moves no URL and no uniqueness check stands between the two.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* moves no URL and no uniqueness check stands between the two. Every entry is
|
|
36
|
+
* derivable that way since #204 retired the section landing, which was the one
|
|
37
|
+
* that was not. The field is still written rather than left for a consumer to
|
|
38
|
+
* compute, because an absent `path` already means something else entirely (a
|
|
39
|
+
* package that publishes no pages).
|
|
40
40
|
*
|
|
41
|
-
* **The address
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* records `affiliation/` for its own. Reading one setting here and in the page
|
|
46
|
-
* emitter is what stops a manifest asserting an address the site does not
|
|
47
|
-
* publish, which resolves at build time and 404s for the reader.
|
|
41
|
+
* **The address is derived by one function, shared with the site build.** An
|
|
42
|
+
* address is `(type, shortcode)` in both, so a manifest cannot assert an address
|
|
43
|
+
* the site does not publish — the failure that resolves at build time and 404s
|
|
44
|
+
* for the reader.
|
|
48
45
|
*
|
|
49
46
|
* **Anchors are computed, not approximated.** The pass that splits a note into
|
|
50
47
|
* journal pages is {@link splitPages}, a pure function over the markdown body,
|
|
@@ -65,7 +62,11 @@ import { compendiumUuid, packForType, pageUuid } from "./ids.mjs";
|
|
|
65
62
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
66
63
|
import { isHomepage } from "./homepage.mjs";
|
|
67
64
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
68
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
assertNoAliasesField,
|
|
67
|
+
assertNoDraftField,
|
|
68
|
+
assertNoSectionField,
|
|
69
|
+
} from "./retired-fields.mjs";
|
|
69
70
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
70
71
|
import { routerFor } from "./pack-router.mjs";
|
|
71
72
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -233,6 +234,7 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
233
234
|
});
|
|
234
235
|
assertNoDraftField(fm, { file: rel, absPath });
|
|
235
236
|
assertNoAliasesField(fm, { file: rel, absPath });
|
|
237
|
+
assertNoSectionField(fm, { file: rel, absPath });
|
|
236
238
|
if (!fm.type || !fm.shortcode) continue;
|
|
237
239
|
// A homepage is addressed like every other note since #182, and a
|
|
238
240
|
// shortcode alone would now put it here. It stays out for the reason it
|
|
@@ -243,15 +245,11 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
243
245
|
// no index.
|
|
244
246
|
if (isHomepage(fm)) continue;
|
|
245
247
|
|
|
246
|
-
const base = path.basename(absPath);
|
|
247
248
|
const name = fm.name?.full ?? path.basename(absPath, ".md");
|
|
248
249
|
|
|
249
250
|
let address;
|
|
250
251
|
try {
|
|
251
|
-
address = packageAddress(fm, {
|
|
252
|
-
isReadme: base.toLowerCase() === "readme.md",
|
|
253
|
-
scheme: ctx.scheme,
|
|
254
|
-
});
|
|
252
|
+
address = packageAddress(fm, { scheme: ctx.scheme });
|
|
255
253
|
} catch (err) {
|
|
256
254
|
skipped.push({ file: rel, reason: err.message });
|
|
257
255
|
continue;
|