@heroiclands/package-build 22.3.1 → 22.4.1
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 +65 -0
- package/CONTENT.md +189 -306
- package/bin/content-build.mjs +15 -20
- package/bin/package-build.mjs +87 -3
- package/content-config.mjs +70 -179
- package/docs/api.md +27 -32
- package/docs/commands.md +98 -19
- package/docs/configuration.md +90 -126
- package/docs/content-format.md +25 -15
- package/docs/getting-started.md +5 -4
- package/docs/project-setup.md +1 -1
- package/engine/actor-compiler.mjs +53 -14
- package/engine/changelog-lint.mjs +629 -0
- package/engine/content-links.mjs +25 -60
- package/engine/diagnostics.mjs +2 -2
- package/engine/field-reference.mjs +7 -1
- package/engine/homepage.mjs +91 -172
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-vocabulary.mjs +0 -20
- package/engine/pdf-build.mjs +2 -2
- package/engine/site-build.mjs +64 -225
- package/engine/site-config.mjs +15 -53
- package/engine/site-root.mjs +26 -70
- package/githooks/pre-commit +53 -2
- package/package.json +1 -1
- package/types/content-config.d.mts +6 -8
- package/types/engine/actor-compiler.d.mts +42 -9
- package/types/engine/changelog-lint.d.mts +50 -0
- package/types/engine/content-links.d.mts +12 -23
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/homepage.d.mts +51 -91
- package/types/engine/note-vocabulary.d.mts +0 -12
- package/types/engine/site-build.d.mts +28 -129
- package/types/engine/site-config.d.mts +10 -19
- package/types/engine/site-root.d.mts +11 -36
package/engine/content-links.mjs
CHANGED
|
@@ -621,21 +621,18 @@ export function buildLinkIndex(
|
|
|
621
621
|
const SITE_HOST = /^(?:[a-z0-9-]+\.)*heroiclands\.org$/i;
|
|
622
622
|
|
|
623
623
|
/**
|
|
624
|
-
* Every package
|
|
624
|
+
* Every package front page this build can name, as `package` → base.
|
|
625
625
|
*
|
|
626
|
-
* **A
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* tree, reads no manifest and builds no index — which is precisely why the
|
|
635
|
-
* mechanism survives `homepage` mode, where the licensing fence means none of
|
|
636
|
-
* those exist.
|
|
626
|
+
* **A front page needs no manifest, and that is what makes it work.** A
|
|
627
|
+
* homepage compiles to no document and is entered in no manifest, but its
|
|
628
|
+
* address is not a *note's* address but the **package's** — it is the mount's
|
|
629
|
+
* `_index.md`, published at `/<package>/` — and {@link PACKAGE_BASE} already
|
|
630
|
+
* records where each package is served. That is a frozen constant compiled
|
|
631
|
+
* into every build, so consulting it walks no tree, reads no manifest and
|
|
632
|
+
* builds no index — which is precisely why the mechanism survives `homepage`
|
|
633
|
+
* mode, where the licensing fence means none of those exist.
|
|
637
634
|
*
|
|
638
|
-
* The roster is consulted **for
|
|
635
|
+
* The roster is consulted **for front pages only**. Widening the package set the
|
|
639
636
|
* other rules read would make them offer manifest-based advice about packages
|
|
640
637
|
* no index has been fetched for.
|
|
641
638
|
*
|
|
@@ -738,41 +735,30 @@ function readAddress(url, packages) {
|
|
|
738
735
|
* not and cannot: it is published *verbatim* by every publishing mode, including
|
|
739
736
|
* the homepage-only mode two fan-licensed packages ship under, where the content
|
|
740
737
|
* tree is never walked and there is no index for a wikilink to resolve against.
|
|
741
|
-
* So a
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
* `being`: two 404s on the package's front page, through every build.
|
|
738
|
+
* So a homepage addresses the web the way the web does — markdown links in its
|
|
739
|
+
* body — and this is what looks at those. A dead link on the page a reader
|
|
740
|
+
* arrives at is the one nothing else would report.
|
|
745
741
|
*
|
|
746
742
|
* **What is checkable, stated plainly.** Only an address into this site is, and
|
|
747
743
|
* only against facts this build already holds:
|
|
748
744
|
*
|
|
749
745
|
* - A **retired content type** in the path. The engine knows the retired names
|
|
750
|
-
* and what replaced it, so this is a fact rather than a guess
|
|
751
|
-
* exactly the SoHL defect.
|
|
746
|
+
* and what replaced it, so this is a fact rather than a guess.
|
|
752
747
|
* - A **hardcoded absolute URL** into this package's own prefix, or into one a
|
|
753
|
-
*
|
|
748
|
+
* fetched index names. Every one of them has a better form to write, which
|
|
754
749
|
* is why every one is reported — including a bare `/<package>/`, which names
|
|
755
|
-
* another package's
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* True, and beside the point: it does not need resolving. A landing's address
|
|
760
|
-
* *is* its package prefix, so `/<package>/` is the absolute URL with the host
|
|
761
|
-
* struck off — host-free, emitted verbatim, and needing no index, which is
|
|
762
|
-
* what lets it hold in homepage-only mode where the tree is never walked. The
|
|
763
|
-
* form was already accepted here; nothing had ever named it as the one to use.
|
|
764
|
-
* - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
|
|
765
|
-
* time. `href:` means "already resolved, use verbatim", so the same leading
|
|
766
|
-
* slash is correct there and is not reported.
|
|
750
|
+
* another package's front page. A front page's address *is* its package
|
|
751
|
+
* prefix, so `/<package>/` is the absolute URL with the host struck off —
|
|
752
|
+
* host-free, emitted verbatim, and needing no index, which is what lets it
|
|
753
|
+
* hold in homepage-only mode where the tree is never walked.
|
|
767
754
|
* - A **wikilink**, which nothing on this page will ever resolve.
|
|
768
755
|
*
|
|
769
756
|
* **What is not checkable, and is not attempted.** Whether an external URL
|
|
770
757
|
* answers — there is no network at build time, and a build must not fail because
|
|
771
758
|
* a third party is down. And whether a live in-site address names a page that
|
|
772
|
-
* exists: several of the surfaces a
|
|
773
|
-
* tools entirely (generated API documentation,
|
|
774
|
-
*
|
|
775
|
-
* link as dead.
|
|
759
|
+
* exists: several of the surfaces a homepage routes to are produced by other
|
|
760
|
+
* tools entirely (generated API documentation, say), so this build does not
|
|
761
|
+
* hold the set of published pages and would report a working link as dead.
|
|
776
762
|
*
|
|
777
763
|
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
778
764
|
* @returns {Array<{note: object, field: string, url: string, text: string,
|
|
@@ -814,7 +800,7 @@ export function auditHomepageLinks(index) {
|
|
|
814
800
|
);
|
|
815
801
|
}
|
|
816
802
|
|
|
817
|
-
for (const { field, url
|
|
803
|
+
for (const { field, url } of homepageAddresses(note.body)) {
|
|
818
804
|
// Counted for every address, checked or not, so the count is
|
|
819
805
|
// the literal's nth appearance in the file rather than the nth
|
|
820
806
|
// *finding* about it — two rules can fire on one address.
|
|
@@ -853,34 +839,13 @@ export function auditHomepageLinks(index) {
|
|
|
853
839
|
prefix === index.contentPackage ?
|
|
854
840
|
`hardcoded absolute URL into this package's own ` +
|
|
855
841
|
`address — write the package-relative ` +
|
|
856
|
-
`"${rest}/", which
|
|
857
|
-
`
|
|
842
|
+
`"${rest}/", which a browser resolves against ` +
|
|
843
|
+
`the homepage's own address, the package root`
|
|
858
844
|
: `hardcoded absolute URL into package "${prefix}" ` +
|
|
859
845
|
`— resolve it through that package's link ` +
|
|
860
846
|
`manifest, whose entries carry the address, so a ` +
|
|
861
847
|
`relocation does not leave this page behind`,
|
|
862
848
|
);
|
|
863
|
-
} else if (shape === "rooted" && kind === "url") {
|
|
864
|
-
const rest = prefix ? segments.slice(1).join("/") : segments.join("/");
|
|
865
|
-
report(
|
|
866
|
-
field,
|
|
867
|
-
url,
|
|
868
|
-
url,
|
|
869
|
-
occurrence,
|
|
870
|
-
// A `url:` is package-relative by construction, so it
|
|
871
|
-
// cannot address anything outside this package at all —
|
|
872
|
-
// there is no relative spelling of another package's root.
|
|
873
|
-
// `href:` is the field for an address already resolved.
|
|
874
|
-
!rest ?
|
|
875
|
-
`url "${url}" addresses ` +
|
|
876
|
-
(prefix ? `package "${prefix}"'s landing` : `the site root`) +
|
|
877
|
-
`, but a landing's url: is package-relative and ` +
|
|
878
|
-
`cannot leave this package — write ` +
|
|
879
|
-
`href: "${url}", which is used verbatim`
|
|
880
|
-
: `url "${url}" is root-relative, but a landing's url: ` +
|
|
881
|
-
`is resolved against the site — write "${rest}/", ` +
|
|
882
|
-
`or href: for an address that is already resolved`,
|
|
883
|
-
);
|
|
884
849
|
}
|
|
885
850
|
|
|
886
851
|
// The retired-type rule reads the path *inside* the package, so an
|
package/engine/diagnostics.mjs
CHANGED
|
@@ -297,7 +297,7 @@ export function positionOfLiteral(text, needle, occurrence = 1) {
|
|
|
297
297
|
*
|
|
298
298
|
* **`key: true` addresses the declaration rather than the value.** A finding
|
|
299
299
|
* about a *value* — this pack name is not in `packs[]` — belongs on the value,
|
|
300
|
-
* which is the default. A finding that names a **field** — `\`site.
|
|
300
|
+
* which is the default. A finding that names a **field** — `\`site.notfound.x\`
|
|
301
301
|
* is not a recognized option` — sends the reader to look for that field, so the
|
|
302
302
|
* position should be the field's own, and in a flow mapping
|
|
303
303
|
* (`{ title: X, banner: Y }`) the two are different columns on one line. The
|
|
@@ -347,7 +347,7 @@ export function positionOfYamlPath(text, keyPath, { key = false } = {}) {
|
|
|
347
347
|
* The YAML key path a **dotted field path** addresses.
|
|
348
348
|
*
|
|
349
349
|
* Configuration checks report the offending key as the path a reader would
|
|
350
|
-
* write it — `packs[1].name`, `site.
|
|
350
|
+
* write it — `packs[1].name`, `site.notfound.links[0].url` — because that
|
|
351
351
|
* is what the message has to say. {@link positionOfYamlPath} addresses a node
|
|
352
352
|
* by segments instead, so this is the one translation between them: `.`
|
|
353
353
|
* separates map keys, and a bracketed suffix is a sequence index.
|
|
@@ -442,5 +442,11 @@ export function renderItemFieldsPage(body, { title, destination, contentRoot, fr
|
|
|
442
442
|
shortcode: shortcodeFromBasename(destination),
|
|
443
443
|
frontmatter,
|
|
444
444
|
});
|
|
445
|
-
|
|
445
|
+
const page = matter.stringify(body, envelope);
|
|
446
|
+
// `matter.stringify` closes the frontmatter fence directly onto the
|
|
447
|
+
// body's first line; Prettier's markdown printer requires a blank line
|
|
448
|
+
// between them, so a page written without one fails a consumer's
|
|
449
|
+
// `lint:format` the moment it is committed. Insert it here rather than
|
|
450
|
+
// let the generator and the formatter rewrite the file back and forth.
|
|
451
|
+
return page.replace(/^(---\n[\s\S]*?\n---\n)/, "$1\n");
|
|
446
452
|
}
|
package/engine/homepage.mjs
CHANGED
|
@@ -20,24 +20,19 @@
|
|
|
20
20
|
* written by a person: what the module is, which system it needs, how to install
|
|
21
21
|
* it, where its source lives. Nothing about it is derived.
|
|
22
22
|
*
|
|
23
|
-
* **Authored, not assembled.**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the package's name is not written twice.
|
|
23
|
+
* **Authored, not assembled.** The things that matter most on a front page
|
|
24
|
+
* cannot be derived: that Kethira requires buying the book from Keléstia, what
|
|
25
|
+
* Thalorna's setting *is*, which of twenty pages a reader should start with. So
|
|
26
|
+
* the only thing defaulted here is the title, from `packageBuild.manifest.title`,
|
|
27
|
+
* so that the package's name is not written twice. Everything else is the body,
|
|
28
|
+
* which is why a `landing:` card block is refused: the homepage is a page with a
|
|
29
|
+
* body, rendered as one, and an index of what the package publishes is a `doc`
|
|
30
|
+
* note carrying a content table.
|
|
32
31
|
*
|
|
33
32
|
* **Dispatched by `type`, not by filename.** A fixed `homepage.md` the walker
|
|
34
33
|
* special-cased would be the anomaly: notes are routed by frontmatter, not by
|
|
35
34
|
* location, and `NOTE_SCHEMAS` already routes `doc`, `macro`, `being` and the
|
|
36
|
-
* map types. `homepage` is one more entry whose compile step emits a page.
|
|
37
|
-
* deliberately not `README.md`: `landing: readme` already means "a `README.md`
|
|
38
|
-
* is its section's landing page", and `sohl-thalorna/assets/content/README.md`
|
|
39
|
-
* is a developer explainer about the source tree — adopting that name would make
|
|
40
|
-
* Thalorna's public front page its build documentation.
|
|
35
|
+
* map types. `homepage` is one more entry whose compile step emits a page.
|
|
41
36
|
*
|
|
42
37
|
* **Engine, not `sohl/`.** The `engine/` ÷ `sohl/` line separates *note-format*
|
|
43
38
|
* knowledge from *game-system* knowledge, and a homepage is note format: it
|
|
@@ -47,27 +42,24 @@
|
|
|
47
42
|
* type living in the SoHL registry would be unavailable to HM3 and to every HM3
|
|
48
43
|
* module, which is most of the packages that need a homepage and nothing else.
|
|
49
44
|
*
|
|
50
|
-
* **
|
|
51
|
-
*
|
|
52
|
-
* `/<package
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* to
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* compiles into no document, so it carries no compendium UUID and appears in no
|
|
63
|
-
* pack and in no link-manifest entry.
|
|
45
|
+
* **Published at the package root, addressed like every other note.** A
|
|
46
|
+
* homepage is written as the mount's `_index.md`, so Hugo's `home` kind renders
|
|
47
|
+
* it at `/<package>/` — the address a reader expects a package's front page at,
|
|
48
|
+
* and the one `package.json`'s `homepage` states. It still declares a
|
|
49
|
+
* `shortcode` — conventionally {@link HOMEPAGE_SHORTCODE} — because that is what
|
|
50
|
+
* a link is written with: `[[homepage-root|Text]]` is an ordinary wikilink, and
|
|
51
|
+
* it resolves to `/<package>/`. The shortcode names the page in links; the
|
|
52
|
+
* address is the package root.
|
|
53
|
+
*
|
|
54
|
+
* `id` is refused, on its own ground: a homepage compiles into no document, so
|
|
55
|
+
* it carries no compendium UUID and appears in no pack and in no link-manifest
|
|
56
|
+
* entry.
|
|
64
57
|
*
|
|
65
58
|
* @module
|
|
66
59
|
*/
|
|
67
60
|
|
|
68
61
|
import fs from "node:fs";
|
|
69
62
|
|
|
70
|
-
import { addressSlug } from "./content-address.mjs";
|
|
71
63
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
72
64
|
import { formatLocator, positionInFrontmatter } from "./diagnostics.mjs";
|
|
73
65
|
|
|
@@ -107,20 +99,15 @@ export const HOMEPAGE_SHORTCODE = "root";
|
|
|
107
99
|
/**
|
|
108
100
|
* The file a homepage is written to, relative to the package's site root.
|
|
109
101
|
*
|
|
110
|
-
*
|
|
111
|
-
* —
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* `/<package>/kb/homepage/` that nothing links to and nobody wrote.
|
|
102
|
+
* The mount's own `_index.md`: Hugo renders it as the `home` kind, at
|
|
103
|
+
* `baseURL` — which is `/<package>/`, the package's own address. One fixed
|
|
104
|
+
* destination rather than one derived from the note's address, because the
|
|
105
|
+
* homepage's address *is* the package root; the note's `shortcode` names the
|
|
106
|
+
* page in links and decides no file.
|
|
116
107
|
*
|
|
117
|
-
* @
|
|
118
|
-
* @returns {string} The destination filename, e.g. `homepage-root.md`.
|
|
119
|
-
* @throws {Error} When the note declares no shortcode, and so has no address.
|
|
108
|
+
* @type {string}
|
|
120
109
|
*/
|
|
121
|
-
export
|
|
122
|
-
return `${addressSlug(fm)}.md`;
|
|
123
|
-
}
|
|
110
|
+
export const HOMEPAGE_DESTINATION = "_index.md";
|
|
124
111
|
|
|
125
112
|
/**
|
|
126
113
|
* Whether a note's frontmatter declares the homepage type.
|
|
@@ -133,17 +120,16 @@ export function isHomepage(fm) {
|
|
|
133
120
|
}
|
|
134
121
|
|
|
135
122
|
/**
|
|
136
|
-
* The top-level
|
|
123
|
+
* The top-level fields a homepage refuses, and what each would decide.
|
|
137
124
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* everywhere else and are permitted.
|
|
125
|
+
* `id` is the Foundry document id a compendium UUID is built from, and a
|
|
126
|
+
* homepage compiles into no document. `landing` is a card block; the homepage
|
|
127
|
+
* is a page with a body, rendered as one, so nothing reads it — and an index
|
|
128
|
+
* of what the package publishes is a `doc` note carrying a content table,
|
|
129
|
+
* authored where every other page is.
|
|
144
130
|
*
|
|
145
|
-
* `
|
|
146
|
-
*
|
|
131
|
+
* `name` and `shortcode` are permitted: the shortcode is what a link is
|
|
132
|
+
* written with, and `name` titles the page like every other note's.
|
|
147
133
|
*
|
|
148
134
|
* **A named class, not an allow-list, and that boundary is the decision.** A
|
|
149
135
|
* homepage's frontmatter is *emitted into the published page*
|
|
@@ -167,6 +153,14 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
|
167
153
|
"homepage compiles into no document — it appears in no pack " +
|
|
168
154
|
"and in no link manifest. Delete it",
|
|
169
155
|
],
|
|
156
|
+
[
|
|
157
|
+
"landing",
|
|
158
|
+
"`landing` decides nothing on a `type: homepage` note: the homepage " +
|
|
159
|
+
"is a page with a body, rendered as one, and no card block is " +
|
|
160
|
+
"read off it. Write the page's links in its body, and author an " +
|
|
161
|
+
"index of what the package publishes as a `doc` note carrying a " +
|
|
162
|
+
"content table. Delete it",
|
|
163
|
+
],
|
|
170
164
|
]),
|
|
171
165
|
);
|
|
172
166
|
|
|
@@ -174,7 +168,7 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
|
174
168
|
* What the address rule says about one note's top-level fields.
|
|
175
169
|
*
|
|
176
170
|
* Two statements about the same thing, so they are made together: the field a
|
|
177
|
-
* homepage **owes** and the
|
|
171
|
+
* homepage **owes** and the fields it may **not** write.
|
|
178
172
|
*
|
|
179
173
|
* The missing `shortcode` comes first, and is located at `type:` rather than at
|
|
180
174
|
* a key that is not there — the `homepage` value is what makes the field
|
|
@@ -184,8 +178,9 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
|
184
178
|
* diagnostic per finding walks down the file.
|
|
185
179
|
*
|
|
186
180
|
* Presence is the whole test for a refused field, and absence-or-blank for the
|
|
187
|
-
* required one: `shortcode:` authored empty is no address, and
|
|
188
|
-
* make `id` mean something on a page that compiles to no document
|
|
181
|
+
* required one: `shortcode:` authored empty is no address, and no value can
|
|
182
|
+
* make `id` or `landing` mean something on a page that compiles to no document
|
|
183
|
+
* and renders as its body.
|
|
189
184
|
*
|
|
190
185
|
* Each finding carries the `locator` key to position it at, because the two
|
|
191
186
|
* things that would resolve one — the raw note text and the position helper —
|
|
@@ -220,10 +215,10 @@ export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
|
|
|
220
215
|
locator: { key: "type", literal: HOMEPAGE_TYPE },
|
|
221
216
|
message:
|
|
222
217
|
"a `type: homepage` note declares a `shortcode`, like every " +
|
|
223
|
-
"other note: it is addressed as `homepage-<shortcode
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
`\`shortcode: ${HOMEPAGE_SHORTCODE}\` — the
|
|
218
|
+
"other note: it is addressed as `homepage-<shortcode>`, which " +
|
|
219
|
+
"is what `[[homepage-<shortcode>|Text]]` is written with to " +
|
|
220
|
+
"reach the package's front page at `/<package>/`. Write " +
|
|
221
|
+
`\`shortcode: ${HOMEPAGE_SHORTCODE}\` — the front page is ` +
|
|
227
222
|
`\`homepage-${HOMEPAGE_SHORTCODE}\` in every package`,
|
|
228
223
|
});
|
|
229
224
|
}
|
|
@@ -247,13 +242,11 @@ export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
|
|
|
247
242
|
* this exists to prevent, and it is silent — the site build reports `wrote 0
|
|
248
243
|
* homepage(s)` and exits 0.
|
|
249
244
|
* - _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
|
|
250
|
-
* only that.**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* name is a question nothing here can answer, and both being reachable is
|
|
256
|
-
* not an answer to it.
|
|
245
|
+
* only that.** Both are written to the mount's `_index.md`, so the second
|
|
246
|
+
* silently overwrites the first; the duplicate-address check catches only
|
|
247
|
+
* the pair that happen to share a shortcode, and says nothing at all about
|
|
248
|
+
* a `homepage-root` beside a `homepage-front`. Which of the two should be
|
|
249
|
+
* the front page is a question nothing here can answer.
|
|
257
250
|
*
|
|
258
251
|
* Neither has a safe default, so neither is a warning. A warning is the right
|
|
259
252
|
* severity for something a build can proceed past correctly, and a build that
|
|
@@ -315,10 +308,9 @@ export function checkHomepageCount(found, { contentBase, contentPackage }) {
|
|
|
315
308
|
message:
|
|
316
309
|
`duplicate \`type: homepage\` note, also declared by ` +
|
|
317
310
|
`${others.join(", ")}; a package has one front page` +
|
|
318
|
-
`${contentPackage ? `, at${address}` : ""}, and
|
|
319
|
-
`
|
|
320
|
-
`
|
|
321
|
-
`the rest ordinary notes`,
|
|
311
|
+
`${contentPackage ? `, at${address}` : ""}, and nothing here ` +
|
|
312
|
+
`can say which of these it should be. Keep one, and make the ` +
|
|
313
|
+
`rest ordinary notes`,
|
|
322
314
|
};
|
|
323
315
|
});
|
|
324
316
|
}
|
|
@@ -371,24 +363,15 @@ export function homepageTitle(fm, config) {
|
|
|
371
363
|
/**
|
|
372
364
|
* The frontmatter a homepage publishes with.
|
|
373
365
|
*
|
|
374
|
-
* The note's own, plus the derived values every emitted page carries: the
|
|
375
|
-
* resolved `title`, the package the build derived — no note declares one
|
|
366
|
+
* The note's own, plus the two derived values every emitted page carries: the
|
|
367
|
+
* resolved `title`, and the package the build derived — no note declares one
|
|
376
368
|
* (`package:` is retired) and the theme's breadcrumb partial reads
|
|
377
|
-
* `.Params.package
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* for one; it decides nothing while `url` is present, but a page carrying only
|
|
384
|
-
* `url` would report a slug Hugo had inferred from the filename.
|
|
385
|
-
*
|
|
386
|
-
* **Site-root relative, and so carrying no package base**, exactly as
|
|
387
|
-
* `pageFrontmatter` states a content page's: Hugo resolves a `url`
|
|
388
|
-
* against `baseURL`, whose path is already where the package is served, so a
|
|
389
|
-
* stated base was written twice and published the landing at
|
|
390
|
-
* `/<package>/<package>/homepage-root/`. Where the package is served is what
|
|
391
|
-
* every *href* is composed from and it reaches this page's address not at all.
|
|
369
|
+
* `.Params.package`.
|
|
370
|
+
*
|
|
371
|
+
* **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
|
|
372
|
+
* whose path is already where the package is served, so the page has no
|
|
373
|
+
* address to state; a content page states one because its file sits under the
|
|
374
|
+
* mount and its address does not.
|
|
392
375
|
*
|
|
393
376
|
* An authored `aliases` is dropped for the same reason it is on every other
|
|
394
377
|
* page: Hugo reads it as URL redirects, so passing it through would publish a
|
|
@@ -400,11 +383,9 @@ export function homepageTitle(fm, config) {
|
|
|
400
383
|
* @param {string} options.contentPackage - The package this build publishes.
|
|
401
384
|
* @param {string} options.title - The resolved title.
|
|
402
385
|
* @returns {object} The frontmatter to write.
|
|
403
|
-
* @throws {Error} When the note declares no shortcode, and so has no address.
|
|
404
386
|
*/
|
|
405
387
|
export function homepageFrontmatter(fm, { contentPackage, title }) {
|
|
406
|
-
const
|
|
407
|
-
const data = { ...fm, package: contentPackage, title, slug, url: `/${slug}/` };
|
|
388
|
+
const data = { ...fm, package: contentPackage, title };
|
|
408
389
|
delete data.aliases;
|
|
409
390
|
return data;
|
|
410
391
|
}
|
|
@@ -412,103 +393,41 @@ export function homepageFrontmatter(fm, { contentPackage, title }) {
|
|
|
412
393
|
/**
|
|
413
394
|
* An inline markdown link — `[text](target)`, but not an image.
|
|
414
395
|
*
|
|
415
|
-
* Reference-style links are deliberately not matched: a
|
|
416
|
-
*
|
|
417
|
-
*
|
|
396
|
+
* Reference-style links are deliberately not matched: a homepage is published
|
|
397
|
+
* verbatim, so a `[x][y]` whose definition sits in the body reaches the reader
|
|
398
|
+
* as Hugo renders it, and one whose definition is missing is a defect the
|
|
399
|
+
* rendered page shows for itself.
|
|
418
400
|
*
|
|
419
401
|
* @type {RegExp}
|
|
420
402
|
*/
|
|
421
403
|
const MARKDOWN_LINK = /(?<!!)\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g;
|
|
422
404
|
|
|
423
405
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* They are **not** interchangeable, and a check that treated them as one would
|
|
427
|
-
* be wrong about both. The theme resolves a `url` against the site with
|
|
428
|
-
* `relURL`, so a package writes `kb/rules/` and is served `/sohl/kb/rules/`
|
|
429
|
-
* without ever naming its own prefix. An `href` is an address that is *already*
|
|
430
|
-
* resolved and is used verbatim — which is what `cards.source: sections` fills
|
|
431
|
-
* in, since a section's permalink already carries the prefix.
|
|
432
|
-
*
|
|
433
|
-
* So a leading `/` is a defect in a `url` (it is prefixed a second time) and
|
|
434
|
-
* correct in an `href`.
|
|
435
|
-
*
|
|
436
|
-
* @type {ReadonlySet<string>}
|
|
437
|
-
*/
|
|
438
|
-
export const HOMEPAGE_ADDRESS_KEYS = Object.freeze(new Set(["url", "href"]));
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Collect the markdown links in one prose value.
|
|
406
|
+
* Every address a homepage carries: the markdown links in its body.
|
|
442
407
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
|
|
449
|
-
function collectProse(text, field, kind, out, skipCode = false) {
|
|
450
|
-
const pattern = new RegExp(MARKDOWN_LINK.source, "g");
|
|
451
|
-
const matches = skipCode ? matchAllOutsideCode(text, pattern) : [...text.matchAll(pattern)];
|
|
452
|
-
for (const m of matches) out.push({ field, url: m[1], kind });
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Every address a homepage carries, wherever it is written.
|
|
457
|
-
*
|
|
458
|
-
* **Both halves of the page are in scope, and that is the finding rather than
|
|
459
|
-
* the assumption.** Of the six homepages authored today, four carry every link
|
|
460
|
-
* in the body as ordinary markdown and two carry them in `landing:` — and the
|
|
461
|
-
* one whose dead links prompted the check has an *empty body*, so a body-only
|
|
462
|
-
* reading would have found nothing at all on it. A dead link in a card is
|
|
463
|
-
* exactly as broken as one in a paragraph.
|
|
408
|
+
* Nowhere else: the frontmatter holds no address, because a homepage is a
|
|
409
|
+
* page with a body and the card block that once carried links is refused.
|
|
410
|
+
* Top-level `title` and `description` are not walked — they are set as text,
|
|
411
|
+
* never rendered as markdown — and `banner:` is not an address: it is an image
|
|
412
|
+
* path resolved through the CDN base, and `banner: none` is a sentinel rather
|
|
413
|
+
* than a target.
|
|
464
414
|
*
|
|
465
|
-
*
|
|
466
|
-
* the
|
|
415
|
+
* A body link is emitted as written and resolved by the browser against the
|
|
416
|
+
* homepage's own address, which *is* the package root, so a package-relative
|
|
417
|
+
* one (`kb/rules/`) lands where a reader expects.
|
|
467
418
|
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* - **prose and body markdown links** — emitted as written and resolved by the
|
|
471
|
-
* browser against the landing's own address, which *is* the package root, so
|
|
472
|
-
* a relative one means the same thing a `url` does.
|
|
419
|
+
* Links inside code are ignored, so an example in a fenced block is not
|
|
420
|
+
* reported as a dead address.
|
|
473
421
|
*
|
|
474
|
-
* `banner:` is not an address: it is an image path resolved through the CDN
|
|
475
|
-
* base, and `banner: none` is a sentinel rather than a target. Top-level
|
|
476
|
-
* `title` and `description` are not walked either — they are set as text, never
|
|
477
|
-
* rendered as markdown.
|
|
478
|
-
*
|
|
479
|
-
* @param {object|null|undefined} fm - The note's frontmatter.
|
|
480
422
|
* @param {string} [body] - The note's markdown body.
|
|
481
423
|
* @returns {Array<{field: string, url: string, kind: string}>} Every address,
|
|
482
|
-
*
|
|
483
|
-
* written at.
|
|
424
|
+
* in body order, each recorded at `field: "body"` with `kind: "body"`.
|
|
484
425
|
*/
|
|
485
|
-
export function homepageAddresses(
|
|
426
|
+
export function homepageAddresses(body = "") {
|
|
486
427
|
const out = [];
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
if (Array.isArray(value)) {
|
|
494
|
-
value.forEach((v, i) => walk(v, `${field}[${i}]`));
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
if (!value || typeof value !== "object") return;
|
|
498
|
-
for (const [key, v] of Object.entries(value)) {
|
|
499
|
-
const child = `${field}.${key}`;
|
|
500
|
-
// An address field holds an address, not prose: reading it for
|
|
501
|
-
// markdown links as well would report the same target twice
|
|
502
|
-
// whenever one happened to look like a link.
|
|
503
|
-
if (HOMEPAGE_ADDRESS_KEYS.has(key) && typeof v === "string") {
|
|
504
|
-
out.push({ field: child, url: v, kind: key });
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
walk(v, child);
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
walk(fm?.landing, "landing");
|
|
512
|
-
collectProse(String(body ?? ""), "body", "body", out, true);
|
|
428
|
+
const pattern = new RegExp(MARKDOWN_LINK.source, "g");
|
|
429
|
+
for (const m of matchAllOutsideCode(String(body ?? ""), pattern)) {
|
|
430
|
+
out.push({ field: "body", url: m[1], kind: "body" });
|
|
431
|
+
}
|
|
513
432
|
return out;
|
|
514
433
|
}
|
|
@@ -50,6 +50,7 @@ import { packageBuildRecords } from "./packagebuild-index.mjs";
|
|
|
50
50
|
|
|
51
51
|
export { metadataFileName };
|
|
52
52
|
import { PACKAGE_BASE, readCanonicalKey, resolvePackageUrl } from "./content-address.mjs";
|
|
53
|
+
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* Written once a fetch completes, so a half-finished cache is never used.
|
|
@@ -365,9 +366,12 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
365
366
|
// must tolerate that rather than invent an href, exactly as it
|
|
366
367
|
// already tolerates an entry with no `uuid`.
|
|
367
368
|
url:
|
|
368
|
-
web
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
!web || !record.address.slug ? undefined
|
|
370
|
+
// A package's homepage is its root: the note is the
|
|
371
|
+
// mount's `_index.md`, so `[[thalorna-homepage-root]]`
|
|
372
|
+
// lands on `/thalorna/`, not on a page below it.
|
|
373
|
+
: parts.type === HOMEPAGE_TYPE ? base
|
|
374
|
+
: resolvePackageUrl(`${record.address.slug}/`, base),
|
|
371
375
|
uuid: foundry?.uuid,
|
|
372
376
|
doc: record.documentation ?? undefined,
|
|
373
377
|
anchors: foundry?.anchors,
|
|
@@ -496,26 +496,6 @@ export function hasTag(fm, tag) {
|
|
|
496
496
|
return false;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
/**
|
|
500
|
-
* Whether a note carries any `tags:` at all, however authored.
|
|
501
|
-
*
|
|
502
|
-
* The one question the site build asks of tags in aggregate — whether the
|
|
503
|
-
* tree publishes taxonomy pages — rather than about a particular tag. Reads
|
|
504
|
-
* `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
|
|
505
|
-
* or a blank string as carrying none.
|
|
506
|
-
*
|
|
507
|
-
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
508
|
-
* @returns {boolean} Whether the note carries at least one tag.
|
|
509
|
-
*/
|
|
510
|
-
export function hasAnyTag(fm) {
|
|
511
|
-
const raw = fm?.tags ?? fm?.tag;
|
|
512
|
-
if (raw == null) return false;
|
|
513
|
-
for (const entry of Array.isArray(raw) ? raw : [raw]) {
|
|
514
|
-
if (typeof entry === "string" && entry.trim() !== "") return true;
|
|
515
|
-
}
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
499
|
/**
|
|
520
500
|
* Whether a note is tagged as an unfinished **draft**.
|
|
521
501
|
*
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
*
|
|
35
35
|
* ## `publish.site` is the switch, and it is the only switch
|
|
36
36
|
*
|
|
37
|
-
* `homepage` mode fences the content surfaces off: the tree is not walked
|
|
38
|
-
*
|
|
37
|
+
* `homepage` mode fences the content surfaces off: the tree is not walked for
|
|
38
|
+
* pages, whatever else the configuration declares. A
|
|
39
39
|
* PDF of the content tree is a content surface by any reading — arguably the
|
|
40
40
|
* most portable one there is — so it is fenced on exactly the same terms, by
|
|
41
41
|
* asking the same {@link module:content-config.publishesContentPages} the
|