@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/engine/site-build.mjs
CHANGED
|
@@ -50,7 +50,7 @@ import { createRequire } from "node:module";
|
|
|
50
50
|
import matter from "gray-matter";
|
|
51
51
|
|
|
52
52
|
import { slugify } from "./content-slug.mjs";
|
|
53
|
-
import { addressSlug
|
|
53
|
+
import { addressSlug } from "./content-address.mjs";
|
|
54
54
|
import { protectCode } from "./code-fences.mjs";
|
|
55
55
|
import { expandContentTables } from "./content-tables.mjs";
|
|
56
56
|
import { buildSiteIndex, wikiContext } from "./site-index.mjs";
|
|
@@ -132,11 +132,10 @@ function readNote(file) {
|
|
|
132
132
|
* @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
|
|
133
133
|
*/
|
|
134
134
|
export function collectContentPages(contentBase, ctx) {
|
|
135
|
-
// Where an addressed page publishes.
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// being collected as a finding (#195).
|
|
135
|
+
// Where an addressed page publishes. A missing `base` would put *every*
|
|
136
|
+
// page at `undefined/` rather than one, and it is the caller's contract
|
|
137
|
+
// rather than a note's defect, so it throws instead of being collected as a
|
|
138
|
+
// finding (#195).
|
|
140
139
|
if (typeof ctx.base !== "string" || !ctx.base) {
|
|
141
140
|
throw new TypeError(
|
|
142
141
|
"collectContentPages: `ctx.base` must be a non-empty string — it is the package address every page's URL is built on",
|
|
@@ -178,20 +177,6 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
const base = path.basename(file);
|
|
181
|
-
const isReadme = base.toLowerCase() === "readme.md";
|
|
182
|
-
const sec = sectionOf(fm);
|
|
183
|
-
// A page's URL no longer contains its section, but the section is still
|
|
184
|
-
// what decides the directory the file is written to — and Hugo derives
|
|
185
|
-
// a page's section from that directory, not from its URL. So a note
|
|
186
|
-
// with none is still a note with nowhere to be published, and is
|
|
187
|
-
// reported rather than written to `undefined/`.
|
|
188
|
-
if (typeof sec !== "string" || !sec) {
|
|
189
|
-
addressFindings.push({
|
|
190
|
-
file,
|
|
191
|
-
reason: `type "${fm.type}" has no section, so there is nowhere to file the page`,
|
|
192
|
-
});
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
180
|
const rel = path.relative(contentBase, file);
|
|
196
181
|
pages.push({
|
|
197
182
|
kind: "content",
|
|
@@ -218,15 +203,12 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
218
203
|
// The immediate source subfolder, the only surviving record of the
|
|
219
204
|
// authoring folder, for grouped landings.
|
|
220
205
|
folder: path.basename(path.dirname(file)),
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
// so Hugo publishes it at its address rather than at its path.
|
|
228
|
-
url: isReadme ? `${ctx.mount}${sec}/` : `${ctx.base}${slug}/`,
|
|
229
|
-
isReadme,
|
|
206
|
+
// Every page is addressed by `(type, shortcode)` at the package
|
|
207
|
+
// root, which takes no content mount (#181). The file is written
|
|
208
|
+
// flat under the mount — see {@link pageDestination} — and the
|
|
209
|
+
// front matter carries this `url` so Hugo publishes it at its
|
|
210
|
+
// address rather than at its path.
|
|
211
|
+
url: `${ctx.base}${slug}/`,
|
|
230
212
|
});
|
|
231
213
|
}
|
|
232
214
|
return { pages, addressFindings, fmLinkFindings };
|
|
@@ -557,12 +539,10 @@ export function sectionFrontmatter(meta) {
|
|
|
557
539
|
* redirects of its own.
|
|
558
540
|
*
|
|
559
541
|
* A content page states its own **`url`**, which is its address rather than its
|
|
560
|
-
* path (#181).
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
* lookup. So the directory stays and the address is stated, and the two are
|
|
565
|
-
* free to differ.
|
|
542
|
+
* path (#181). It is written flat under the content mount (#204), so Hugo would
|
|
543
|
+
* otherwise publish it at `<mount><type>-<shortcode>/` rather than at the
|
|
544
|
+
* package-wide address the link manifest records — the same address, one
|
|
545
|
+
* segment too deep. So the address is stated and the mount does not reach it.
|
|
566
546
|
*
|
|
567
547
|
* A content page carries the package the build **derived** (#65). No note
|
|
568
548
|
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
@@ -574,8 +554,12 @@ export function sectionFrontmatter(meta) {
|
|
|
574
554
|
* self-describing and makes sweeping the field out of a content tree
|
|
575
555
|
* output-preserving for a site as it already is for the packs.
|
|
576
556
|
*
|
|
557
|
+
* A **tree** page is the one that still reads `readmeSections`: a `trees` entry
|
|
558
|
+
* keeps its source layout below a named section, so its own `README` is that
|
|
559
|
+
* section's landing and takes the title and hero the section declares.
|
|
560
|
+
*
|
|
577
561
|
* @param {object} page - The page.
|
|
578
|
-
* @param {object} options - `{
|
|
562
|
+
* @param {object} options - `{ readmeSections, decorate }`.
|
|
579
563
|
* @returns {object} The frontmatter to write.
|
|
580
564
|
*/
|
|
581
565
|
export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
@@ -598,14 +582,6 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
598
582
|
kbfolder: page.folder,
|
|
599
583
|
};
|
|
600
584
|
if (decorate) decorate(data, page);
|
|
601
|
-
if (isReadme) {
|
|
602
|
-
const meta = readmeSections[sec];
|
|
603
|
-
// What the section says about itself wins over what its README
|
|
604
|
-
// happens to carry — the landing has to match the card linking to
|
|
605
|
-
// it. Assigned rather than transcribed key by key, so a section's
|
|
606
|
-
// vocabulary is decided in one place (#91).
|
|
607
|
-
if (meta) Object.assign(data, sectionFrontmatter(meta));
|
|
608
|
-
}
|
|
609
585
|
} else {
|
|
610
586
|
// A tree's own landing describes the *mount*, and nothing beneath it. A
|
|
611
587
|
// nested README is a sub-section's landing, and reading the section's
|
|
@@ -623,23 +599,25 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
623
599
|
/**
|
|
624
600
|
* Where a page is written, relative to the output root.
|
|
625
601
|
*
|
|
626
|
-
* **
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
* Hugo
|
|
630
|
-
*
|
|
631
|
-
*
|
|
602
|
+
* **Flat, under the mount, named by its address** (#204). A content page's URL
|
|
603
|
+
* is its address — `/<package>/<type>-<shortcode>/` — and the file is now named
|
|
604
|
+
* the same way, so the two agree. It used to be filed into `<section>/` so that
|
|
605
|
+
* Hugo would read a section off its path; a section appears in no address, and
|
|
606
|
+
* a directory chosen only to satisfy a rendering engine's idea of what a
|
|
607
|
+
* section is has no business in the note format.
|
|
608
|
+
*
|
|
609
|
+
* The name is the *whole* address rather than a section-relative half of it, so
|
|
610
|
+
* two types cannot fight over one file: a `doc` note's `subType` may be spelled
|
|
611
|
+
* the same as another note's `type`, and `doc-gear.md` and `weapongear-gear.md`
|
|
612
|
+
* are distinct whatever the sections used to be.
|
|
632
613
|
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
614
|
+
* **A `trees` entry is the exception, and always was.** Those pages preserve
|
|
615
|
+
* their source layout below a named section — they are a book with chapters,
|
|
616
|
+
* addressed by their path — so a `README` there is still its directory's
|
|
617
|
+
* `_index.md`.
|
|
636
618
|
*/
|
|
637
619
|
export function pageDestination(page) {
|
|
638
|
-
if (page.kind === "content") {
|
|
639
|
-
return page.isReadme ?
|
|
640
|
-
path.join(page.sec, "_index.md")
|
|
641
|
-
: path.join(page.sec, `${page.slug}.md`);
|
|
642
|
-
}
|
|
620
|
+
if (page.kind === "content") return `${page.slug}.md`;
|
|
643
621
|
const rel =
|
|
644
622
|
page.isReadme ? path.posix.join(path.posix.dirname(page.rel), "_index.md") : page.rel;
|
|
645
623
|
return path.join(page.sec, rel);
|
|
@@ -681,7 +659,11 @@ export function renderPages(pages, options) {
|
|
|
681
659
|
const byKind = {};
|
|
682
660
|
|
|
683
661
|
for (const page of pages) {
|
|
684
|
-
|
|
662
|
+
// The page's path in the tree an author edits: below the content root
|
|
663
|
+
// for a content note, below the tree's own root for a `trees` page. It
|
|
664
|
+
// used to be composed as `<section>/<basename>` for a content note,
|
|
665
|
+
// which named a directory that was never the note's (#204).
|
|
666
|
+
const src = page.relPath ?? page.rel ?? page.base;
|
|
685
667
|
const ctx = wikiContext(index, {
|
|
686
668
|
src,
|
|
687
669
|
file: page.file,
|
|
@@ -724,26 +706,43 @@ export function renderPages(pages, options) {
|
|
|
724
706
|
}
|
|
725
707
|
|
|
726
708
|
/**
|
|
727
|
-
* Writes the
|
|
709
|
+
* Writes the Hugo sections a published tree declares.
|
|
710
|
+
*
|
|
711
|
+
* **This is where a section lives now, and the only place** (#204). A content
|
|
712
|
+
* note carries none: it is addressed by `(type, shortcode)` and emitted flat
|
|
713
|
+
* under the mount, so nothing a page does creates a directory. A site that wants
|
|
714
|
+
* `/<package>/<prefix><section>/` to answer — with a title, a hero, and whatever
|
|
715
|
+
* listing its layout builds — says so here, in configuration, and this writes
|
|
716
|
+
* the `_index.md` that makes Hugo agree it is a section.
|
|
728
717
|
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
718
|
+
* Three jobs, all of them Hugo's directory semantics rather than the note
|
|
719
|
+
* format's:
|
|
731
720
|
*
|
|
721
|
+
* - **The mount's own landing**, so `/<package>/<prefix>` is a page rather than
|
|
722
|
+
* a directory listing. It carries a `type` of its own: Hugo's template lookup
|
|
723
|
+
* walks up a page's path, so an untyped landing template at the mount would
|
|
724
|
+
* also serve every section below it that has none.
|
|
732
725
|
* - **Declared sections** get a titled `_index.md` with their hero, so a landing
|
|
733
726
|
* matches the card that links to it instead of showing Hugo's auto-humanised
|
|
734
|
-
* directory name. The body is empty, which lets the theme
|
|
735
|
-
*
|
|
736
|
-
* - **Every other section directly under the mount** gets a bare `_index.md`,
|
|
727
|
+
* directory name. The body is empty, which lets the theme decide what to list.
|
|
728
|
+
* - **Every other directory directly under the mount** gets a bare `_index.md`,
|
|
737
729
|
* or its own address publishes nothing. Hugo generates a section page
|
|
738
730
|
* automatically only for a *top-level* content directory; below that, a
|
|
739
|
-
* directory without an `_index.md` is not a section, so its URL 404s while
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
731
|
+
* directory without an `_index.md` is not a section, so its URL 404s while its
|
|
732
|
+
* children publish normally. With content pages flat, what that reaches is a
|
|
733
|
+
* `trees` entry's directory — the one thing left below the mount that a note
|
|
734
|
+
* creates.
|
|
735
|
+
*
|
|
736
|
+
* **A section listing is not a page listing any more.** A layout that reads
|
|
737
|
+
* `.Pages` off a section it declares here will find nothing, because no file is
|
|
738
|
+
* filed into it; one that queries `site.RegularPages` by `Params.type` — which
|
|
739
|
+
* is how `sohl`'s eleven catalog layouts already work — is unaffected. That is a
|
|
740
|
+
* consumer's layout to choose, and it is stated here because the choice is no
|
|
741
|
+
* longer free.
|
|
743
742
|
*
|
|
744
743
|
* Scoped to one level on purpose. A directory further down was not a section
|
|
745
|
-
* before
|
|
746
|
-
*
|
|
744
|
+
* before either, and giving it one here would silently re-scope the prev/next
|
|
745
|
+
* navigation of every page inside it.
|
|
747
746
|
*
|
|
748
747
|
* @param {string} outRoot - The mount directory.
|
|
749
748
|
* @param {object} options - `{ sections, landing, sectionTitle }`.
|
|
@@ -752,11 +751,6 @@ export function renderPages(pages, options) {
|
|
|
752
751
|
export function writeSectionLandings(outRoot, { sections = {}, landing, sectionTitle }) {
|
|
753
752
|
let written = 0;
|
|
754
753
|
|
|
755
|
-
// The mount's own landing carries a `type` of its own. Hugo's template
|
|
756
|
-
// lookup walks up a page's path, so a landing template at the mount would
|
|
757
|
-
// also serve every section below it that has no template of its own —
|
|
758
|
-
// each would render the mount's front page. Typing the landing moves its
|
|
759
|
-
// template out of the path where it could be inherited.
|
|
760
754
|
if (landing) {
|
|
761
755
|
fs.mkdirSync(outRoot, { recursive: true });
|
|
762
756
|
fs.writeFileSync(path.join(outRoot, "_index.md"), matter.stringify("", landing));
|
|
@@ -1024,9 +1018,7 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
1024
1018
|
name: page.fm.name?.full ?? homepageTitle(page.fm, resolved),
|
|
1025
1019
|
slug: addressSlug(page.fm),
|
|
1026
1020
|
base: path.basename(page.file),
|
|
1027
|
-
sec: sectionOf(page.fm),
|
|
1028
1021
|
url: `${base}${addressSlug(page.fm)}/`,
|
|
1029
|
-
isReadme: false,
|
|
1030
1022
|
}));
|
|
1031
1023
|
|
|
1032
1024
|
const trees = site.trees.map((t) => ({
|
package/engine/site-index.mjs
CHANGED
|
@@ -65,10 +65,15 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
65
65
|
* @property {object} fm The note's frontmatter.
|
|
66
66
|
* @property {string} name Display name.
|
|
67
67
|
* @property {string} slug URL segment.
|
|
68
|
-
* @property {string} sec
|
|
68
|
+
* @property {string} [sec] The Hugo section a **tree** page is filed under, and
|
|
69
|
+
* the first segment of the `<sec>/<slug>` address it
|
|
70
|
+
* is reachable by. A content page has none: it is
|
|
71
|
+
* addressed by `(type, shortcode)` and emitted flat
|
|
72
|
+
* (#204).
|
|
69
73
|
* @property {string} base Source file's basename, e.g. `Climbing.md`.
|
|
70
74
|
* @property {string} url The page's published address.
|
|
71
|
-
* @property {boolean} isReadme Whether
|
|
75
|
+
* @property {boolean} [isReadme] Whether a tree page is its directory's
|
|
76
|
+
* landing.
|
|
72
77
|
*/
|
|
73
78
|
|
|
74
79
|
/**
|
|
@@ -169,13 +174,20 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
169
174
|
const ownPackage = contentPackage();
|
|
170
175
|
const packages = new Set(ownPackage ? [ownPackage] : []);
|
|
171
176
|
|
|
172
|
-
// `section/slug` is unique by construction
|
|
173
|
-
//
|
|
174
|
-
// `
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
+
// `section/slug` is unique by construction, and is now a **tree** page's
|
|
178
|
+
// address: a `trees` entry keeps its source layout below a named section,
|
|
179
|
+
// so `dev-docs/testing` is how one is cited. A content page carries no
|
|
180
|
+
// section at all (#204) and is addressed by `(type, shortcode)` below —
|
|
181
|
+
// indexing it here as well would have written `weapongear/weapongear-dagger`,
|
|
182
|
+
// a key no author could reasonably write.
|
|
183
|
+
//
|
|
184
|
+
// A page's name, filename and bare slug were indexed here too, as
|
|
185
|
+
// collision-aware fallbacks the bare `[[Name]]` form looked up; that form is
|
|
186
|
+
// retired and nothing consults them, so they are gone and with them the rule
|
|
187
|
+
// that two pages of a type may not share a name (#179, #180).
|
|
177
188
|
for (const e of entries) {
|
|
178
|
-
|
|
189
|
+
if (typeof e.sec !== "string" || !e.sec) continue;
|
|
190
|
+
sections.add(e.sec.toLowerCase());
|
|
179
191
|
// `draft` rides on every key a page is addressable by, because a link
|
|
180
192
|
// into a draft note renders marked whichever of them the author wrote
|
|
181
193
|
// (#183). It decides nothing about resolution: the page is indexed and
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* pack build together.
|
|
29
29
|
*
|
|
30
30
|
* The KB *section* is not always the type: prose pages (`type: doc`) route by
|
|
31
|
-
* their `category`, so `doc/quickstart` lands on `/
|
|
31
|
+
* their `category`, so `doc/quickstart` lands on `/userguide/sohl-quickstart/`.
|
|
32
32
|
* The caller supplies that mapping already resolved, in the index it builds.
|
|
33
33
|
*
|
|
34
34
|
* Lives here rather than in a consumer so every package resolves a link the
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* Nothing narrower than `(type, shortcode)` is consulted — a note's directory
|
|
37
37
|
* and its `category` play no part in resolution — and nothing wider: a note's
|
|
38
38
|
* *name* is not an address, so two notes of a type may share a display name
|
|
39
|
-
* ("Gear" as a rules page and as a user
|
|
39
|
+
* ("Gear" as a rules page and as a user guide page) with nothing to disambiguate
|
|
40
40
|
* (#179, #180).
|
|
41
41
|
*
|
|
42
42
|
* At compile time each becomes a Foundry UUID enricher, routed to the pack that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -14,30 +14,6 @@ export function publishesContentPages(config: {
|
|
|
14
14
|
site: SiteMode;
|
|
15
15
|
};
|
|
16
16
|
}): boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Every URL section this repository names, in declaration order (#197).
|
|
19
|
-
*
|
|
20
|
-
* A section is *declared* by describing it: `site.sections` for one whose
|
|
21
|
-
* landing this build generates, `site.readmeSections` for one whose landing is
|
|
22
|
-
* a `README`. Between them they are the open set of addresses a repository
|
|
23
|
-
* says it publishes under — which is what a `README` landing's `subType` names,
|
|
24
|
-
* since `sectionOf` reads that field as the section rather than as a genre.
|
|
25
|
-
*
|
|
26
|
-
* Read from the same two maps the site build renders each landing from, so the
|
|
27
|
-
* set a note is checked against and the set a landing is written from cannot
|
|
28
|
-
* come to disagree. A repository that describes no section declares none, and
|
|
29
|
-
* the answer is empty rather than a guess assembled from the tree.
|
|
30
|
-
*
|
|
31
|
-
* @param {{site: {sections: object, readmeSections: object}}} config - A
|
|
32
|
-
* resolved configuration.
|
|
33
|
-
* @returns {readonly string[]} The section names, deduplicated.
|
|
34
|
-
*/
|
|
35
|
-
export function declaredSections(config: {
|
|
36
|
-
site: {
|
|
37
|
-
sections: object;
|
|
38
|
-
readmeSections: object;
|
|
39
|
-
};
|
|
40
|
-
}): readonly string[];
|
|
41
17
|
/**
|
|
42
18
|
* Validate and normalize a content configuration.
|
|
43
19
|
*
|
|
@@ -80,28 +56,31 @@ export namespace DEFAULT_PATHS {
|
|
|
80
56
|
*/
|
|
81
57
|
export const PACK_DOCUMENT_TYPES: readonly ["Actor", "Adventure", "Item", "JournalEntry", "Macro", "Scene"];
|
|
82
58
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* A
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
59
|
+
* Address-scheme keys a configuration may no longer declare.
|
|
60
|
+
*
|
|
61
|
+
* A retired key has exactly two possible fates, and only one of them is honest
|
|
62
|
+
* — the same reasoning `engine/retired-fields.mjs` applies to a retired
|
|
63
|
+
* frontmatter field. Left honoured, it keeps doing whatever it did, which is
|
|
64
|
+
* why it was retired. Left *ignored*, it reads to its author as though it still
|
|
65
|
+
* works: the configuration says one thing and the build does another, and
|
|
66
|
+
* nothing says so. This module has no third option, because it has no warning
|
|
67
|
+
* channel — every finding goes through `fail()`, which throws. So a retired
|
|
68
|
+
* key is **refused**, at the line it was written on, with a message that says
|
|
69
|
+
* the mechanism is gone rather than naming a value to correct.
|
|
70
|
+
*
|
|
71
|
+
* **What `landing` did (#204).** It named which note addressed a whole section
|
|
72
|
+
* rather than a page within one — a *landing page*, which therefore had no slug
|
|
73
|
+
* of its own. #203 retired the second of its two rules and #204 retired the
|
|
74
|
+
* concept both rules chose between: a section is a Hugo content directory that
|
|
75
|
+
* the note format does not carry, a page's address names no directory, and so
|
|
76
|
+
* no note lands anything. The key outlived its mechanism by one release only
|
|
77
|
+
* because both publishing consumers still declared the then-true
|
|
78
|
+
* `landing: readme`, and neither breaking them over a correct statement nor
|
|
79
|
+
* accepting the key in silence was acceptable. Neither declares it now.
|
|
80
|
+
*
|
|
81
|
+
* @type {Readonly<Record<string, string>>}
|
|
103
82
|
*/
|
|
104
|
-
export const
|
|
83
|
+
export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
|
|
105
84
|
/**
|
|
106
85
|
* A repository's address scheme, with the defaults an unconfigured one gets.
|
|
107
86
|
*
|
|
@@ -111,10 +90,13 @@ export const LANDING_RULES: readonly string[];
|
|
|
111
90
|
* own mount point: where the package itself is served is the consuming build's
|
|
112
91
|
* knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
|
|
113
92
|
* resolve time, so it is never recorded here (#1465).
|
|
93
|
+
*
|
|
94
|
+
* It is the whole scheme: `landing`, the key that named which note addressed a
|
|
95
|
+
* whole section, is retired with the sections themselves — see
|
|
96
|
+
* {@link RETIRED_ADDRESS_KEYS}.
|
|
114
97
|
*/
|
|
115
98
|
export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
116
99
|
prefix: "";
|
|
117
|
-
landing: "readme";
|
|
118
100
|
}>;
|
|
119
101
|
/**
|
|
120
102
|
* How much of a package reaches the web.
|
|
@@ -529,10 +511,6 @@ export type AddressSchemeInput = {
|
|
|
529
511
|
* Where the content tree mounts inside the package.
|
|
530
512
|
*/
|
|
531
513
|
prefix?: string | undefined;
|
|
532
|
-
/**
|
|
533
|
-
* Which note addresses a whole section.
|
|
534
|
-
*/
|
|
535
|
-
landing?: string | undefined;
|
|
536
514
|
};
|
|
537
515
|
/**
|
|
538
516
|
* One entry of a consumer's `itemBuilders` registry.
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The URL section a note routes to.
|
|
3
|
-
*
|
|
4
|
-
* A `doc` is narrative content whose only identity is its subtype label, so it
|
|
5
|
-
* routes by `subType`; every other type names its own section.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} fm - Parsed frontmatter.
|
|
8
|
-
* @returns {string|undefined} The section, or `undefined` when the note has
|
|
9
|
-
* none — a `doc` with no subtype has no address and is not published.
|
|
10
|
-
*/
|
|
11
|
-
export function sectionOf(fm: object): string | undefined;
|
|
12
1
|
/**
|
|
13
2
|
* The single path segment a note is addressed by: `type-shortcode`.
|
|
14
3
|
*
|
|
@@ -34,15 +23,16 @@ export function addressSlug(fm: object): string;
|
|
|
34
23
|
/**
|
|
35
24
|
* A note's address below the knowledgebase mount, e.g. `affliction-aconite/`.
|
|
36
25
|
*
|
|
37
|
-
* A `README.md`
|
|
38
|
-
*
|
|
26
|
+
* Every note, without exception. A `README.md` used to be its section's landing
|
|
27
|
+
* page and to address the section instead of itself; a section is a Hugo
|
|
28
|
+
* directory concept the note format no longer carries (#204), so a file's name
|
|
29
|
+
* decides nothing about where it publishes.
|
|
39
30
|
*
|
|
40
31
|
* @param {object} fm - Parsed frontmatter.
|
|
41
|
-
* @param {boolean} isReadme - Whether the file is a `README.md`.
|
|
42
32
|
* @returns {string} The mount-relative address, with a trailing slash.
|
|
43
33
|
* @throws {Error} When the note has no address.
|
|
44
34
|
*/
|
|
45
|
-
export function contentAddress(fm: object
|
|
35
|
+
export function contentAddress(fm: object): string;
|
|
46
36
|
/**
|
|
47
37
|
* A note's address relative to its **package**, e.g. `affliction-aconite/`.
|
|
48
38
|
*
|
|
@@ -51,43 +41,34 @@ export function contentAddress(fm: object, isReadme: boolean): string;
|
|
|
51
41
|
* address the site does not publish resolves at build time and 404s for the
|
|
52
42
|
* reader, which is the failure this module exists to prevent.
|
|
53
43
|
*
|
|
44
|
+
* **It is a pure function of the frontmatter.** Nothing about the file the note
|
|
45
|
+
* was read from reaches it: the `README.md` convention that made one note
|
|
46
|
+
* address a whole section is retired with the section itself (#204), so there
|
|
47
|
+
* is one rule and no branch.
|
|
48
|
+
*
|
|
54
49
|
* **The prefix does not apply to a page's own address.** `prefix` says where the
|
|
55
|
-
* content tree *mounts inside the package* —
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `/sohl/kb/affliction/`. The `type-` half is what keeps that flat namespace
|
|
61
|
-
* clear of the package's fixed mounts — `/<package>/` for the landing,
|
|
50
|
+
* content tree *mounts inside the package* — the Hugo directory its pages are
|
|
51
|
+
* written under — and an address is `(type, shortcode)`, a package-wide identity
|
|
52
|
+
* that takes no mount: `sohl` publishes `/sohl/affliction-aconite/` from a file
|
|
53
|
+
* written under `kb/`. The `type-` half is what keeps that flat namespace clear
|
|
54
|
+
* of the package's fixed mounts — `/<package>/` for the landing,
|
|
62
55
|
* `/<package>/api/` for generated API docs, neither of which contains a hyphen
|
|
63
56
|
* or names a type.
|
|
64
57
|
*
|
|
65
|
-
* **
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
58
|
+
* **It takes no address scheme.** It took one until #215, to validate the
|
|
59
|
+
* `landing` rule it then discarded; with that key retired, `prefix` was the
|
|
60
|
+
* only thing left in the scheme and the paragraph above is the reason it never
|
|
61
|
+
* applied. A parameter read by nothing is the defect this deletion is about.
|
|
69
62
|
*
|
|
70
63
|
* @param {object} fm - Parsed frontmatter.
|
|
71
|
-
* @param {object} [options] - Options.
|
|
72
|
-
* @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
|
|
73
|
-
* @param {{prefix?: string, landing?: string}} [options.scheme] - The
|
|
74
|
-
* repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
|
|
75
64
|
* @returns {string} The package-relative address, with a trailing slash and no
|
|
76
65
|
* leading one.
|
|
77
|
-
* @throws {Error} When the note has no
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* in the manifest.
|
|
66
|
+
* @throws {Error} When the note has no type or no shortcode to be addressed by.
|
|
67
|
+
* Such a note is not published, and inventing an address for one would put a
|
|
68
|
+
* dead entry in the manifest.
|
|
81
69
|
*/
|
|
82
|
-
export function packageAddress(fm: object
|
|
83
|
-
|
|
84
|
-
scheme?: {
|
|
85
|
-
prefix?: string;
|
|
86
|
-
landing?: string;
|
|
87
|
-
} | undefined;
|
|
88
|
-
}): string;
|
|
70
|
+
export function packageAddress(fm: object): string;
|
|
71
|
+
export { DEFAULT_ADDRESS_SCHEME };
|
|
89
72
|
/** The knowledgebase's mount within this package's site (#1470). */
|
|
90
73
|
export const KB_PREFIX: "kb/";
|
|
91
74
|
import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
|
|
92
|
-
import { LANDING_RULES } from "../content-config.mjs";
|
|
93
|
-
export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
|
|
@@ -29,22 +29,9 @@ export function matchesKind(value: unknown, kind: string): boolean;
|
|
|
29
29
|
* @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
|
|
30
30
|
* The system blocks to check, and what each accepts. See
|
|
31
31
|
* {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
32
|
-
* @param {string} [opts.landing] - The repository's landing rule, from
|
|
33
|
-
* `publish.address.landing`. It decides which note addresses a whole section,
|
|
34
|
-
* and so whether a `subType` is a genre or an address (#197).
|
|
35
|
-
* @param {readonly string[]} [opts.types] - The content types the format
|
|
36
|
-
* declares — the sections that exist by construction (#200). Read from
|
|
37
|
-
* `docs/content-format.md` rather than from `schemas`, because the two answer
|
|
38
|
-
* different questions: whether an address is real, and whether this build can
|
|
39
|
-
* check a note's fields. A type the specification declares and no schema
|
|
40
|
-
* covers is a real section, and its notes are reported on their own account.
|
|
41
|
-
* @param {readonly string[]} [opts.sections] - The sections the repository
|
|
42
|
-
* configures, from `declaredSections`. Supplied by the caller for the same
|
|
43
|
-
* reason `vocabulary` is: this module checks a note against what it is
|
|
44
|
-
* handed.
|
|
45
32
|
* @returns {object[]} Findings, each with a locator where one is obtainable.
|
|
46
33
|
*/
|
|
47
|
-
export function lintNote(note: object, { schemas, index, vocabulary, systems
|
|
34
|
+
export function lintNote(note: object, { schemas, index, vocabulary, systems }: {
|
|
48
35
|
schemas: Record<string, readonly object[]>;
|
|
49
36
|
index?: object | undefined;
|
|
50
37
|
vocabulary?: Record<string, object> | undefined;
|
|
@@ -52,9 +39,6 @@ export function lintNote(note: object, { schemas, index, vocabulary, systems, la
|
|
|
52
39
|
known?: readonly string[];
|
|
53
40
|
fieldVocabulary?: boolean;
|
|
54
41
|
}>> | undefined;
|
|
55
|
-
landing?: string | undefined;
|
|
56
|
-
types?: readonly string[] | undefined;
|
|
57
|
-
sections?: readonly string[] | undefined;
|
|
58
42
|
}): object[];
|
|
59
43
|
/**
|
|
60
44
|
* Check every note in a built index against its type's schema.
|
|
@@ -67,16 +51,10 @@ export function lintNote(note: object, { schemas, index, vocabulary, systems, la
|
|
|
67
51
|
* @param {boolean} [opts.references=true] - Whether to check references.
|
|
68
52
|
* @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
|
|
69
53
|
* The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
70
|
-
* @param {string} [opts.landing] - The repository's landing rule; see
|
|
71
|
-
* {@link lintNote}.
|
|
72
|
-
* @param {readonly string[]} [opts.types] - The content types the format
|
|
73
|
-
* declares; see {@link lintNote}.
|
|
74
|
-
* @param {readonly string[]} [opts.sections] - The sections the repository
|
|
75
|
-
* configures; see {@link lintNote}.
|
|
76
54
|
* @returns {{findings: object[], notes: number}} The findings, and how many
|
|
77
55
|
* notes were inspected.
|
|
78
56
|
*/
|
|
79
|
-
export function lintFrontmatter(index: object, { schemas, vocabulary, references, systems
|
|
57
|
+
export function lintFrontmatter(index: object, { schemas, vocabulary, references, systems }: {
|
|
80
58
|
schemas: Record<string, readonly object[]>;
|
|
81
59
|
vocabulary?: Record<string, object> | undefined;
|
|
82
60
|
references?: boolean | undefined;
|
|
@@ -84,9 +62,6 @@ export function lintFrontmatter(index: object, { schemas, vocabulary, references
|
|
|
84
62
|
known?: readonly string[];
|
|
85
63
|
fieldVocabulary?: boolean;
|
|
86
64
|
}>> | undefined;
|
|
87
|
-
landing?: string | undefined;
|
|
88
|
-
types?: readonly string[] | undefined;
|
|
89
|
-
sections?: readonly string[] | undefined;
|
|
90
65
|
}): {
|
|
91
66
|
findings: object[];
|
|
92
67
|
notes: number;
|
|
@@ -51,8 +51,7 @@ export function entriesForNote(fm: object, name: string, address: string, body:
|
|
|
51
51
|
* exist.
|
|
52
52
|
*
|
|
53
53
|
* @param {string} contentBase - Absolute path to the content tree.
|
|
54
|
-
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter
|
|
55
|
-
* scheme }`.
|
|
54
|
+
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter }`.
|
|
56
55
|
* @returns {{entries: Array<object>, notes: number,
|
|
57
56
|
* skipped: Array<{file: string, reason: string}>}}
|
|
58
57
|
*/
|
|
@@ -65,7 +64,7 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
|
|
|
65
64
|
}>;
|
|
66
65
|
};
|
|
67
66
|
/**
|
|
68
|
-
* The identities
|
|
67
|
+
* The identities an emission runs against, from configuration.
|
|
69
68
|
*
|
|
70
69
|
* Resolved in one place and passed down, rather than read at each use, so the
|
|
71
70
|
* pass itself is a pure function of its context and a test can drive it without
|
|
@@ -73,17 +72,12 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
|
|
|
73
72
|
*
|
|
74
73
|
* @param {object} [config] - A resolved configuration; loaded when omitted.
|
|
75
74
|
* @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
|
|
76
|
-
*
|
|
77
|
-
* skipDirectories: readonly string[]}}
|
|
75
|
+
* web: boolean, skipDirectories: readonly string[]}}
|
|
78
76
|
*/
|
|
79
77
|
export function manifestContext(config?: object): {
|
|
80
78
|
contentPackage: string;
|
|
81
79
|
foundryPackageId: string;
|
|
82
80
|
packRouter: object;
|
|
83
|
-
scheme: {
|
|
84
|
-
prefix: string;
|
|
85
|
-
landing: string;
|
|
86
|
-
};
|
|
87
81
|
web: boolean;
|
|
88
82
|
skipDirectories: readonly string[];
|
|
89
83
|
};
|