@heroiclands/package-build 23.13.0 → 24.0.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 +50 -0
- package/README.md +2 -1
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +150 -1
- package/bin/content-commands.mjs +356 -210
- package/bin/package-build.mjs +3 -2
- package/ci/check-banners.mjs +7 -7
- package/config.mjs +21 -6
- package/content-config.mjs +199 -80
- package/docs/api.md +93 -72
- package/docs/authoring/links-and-markup.md +316 -78
- package/docs/commands.md +248 -80
- package/docs/configuration.md +210 -102
- package/docs/content-format.md +2 -2
- package/docs/diagnostics.md +2 -2
- package/docs/getting-started.md +1 -1
- package/docs/guides/icons.md +1 -1
- package/docs/guides/module-project.md +5 -8
- package/docs/guides/release-and-deploy.md +2 -2
- package/docs/guides/site.md +155 -23
- package/docs/guides/system-project.md +1 -1
- package/docs/guides/writing-in-invented-scripts.md +1247 -0
- package/docs/index.md +1 -0
- package/docs/project-setup.md +28 -30
- package/docs/reference/format-details.md +589 -308
- package/docs/reference/note-types.md +40 -10
- package/docs/reference/theme-pages.md +127 -108
- package/docs/source-preview.md +15 -1
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +26 -11
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +13 -6
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- package/engine/content-links.mjs +3 -3
- package/engine/content-lint.mjs +1 -1
- package/engine/content-render.mjs +453 -0
- package/engine/content-scripts.mjs +326 -27
- package/engine/content-spans.mjs +4 -1
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +6 -18
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/heading-attributes.mjs +89 -0
- package/engine/helpers.mjs +61 -101
- package/engine/homepage.mjs +17 -29
- package/engine/html-minify.mjs +70 -0
- package/engine/index.mjs +8 -5
- package/engine/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +115 -14
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +221 -25
- package/engine/map-places.mjs +5 -4
- package/engine/markdown-expressions.mjs +0 -2
- package/engine/markdownlint-run.mjs +9 -3
- package/engine/markup-validation.mjs +21 -0
- package/engine/metre-declaration.mjs +134 -0
- package/engine/note-matter.mjs +2 -2
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +68 -133
- package/engine/pdf-render.mjs +102 -44
- package/engine/post-fields.mjs +157 -0
- package/engine/preview-index.mjs +130 -0
- package/engine/preview-source.mjs +450 -0
- package/engine/project-init.mjs +5 -12
- package/engine/prose-config.mjs +3 -3
- package/engine/run-words.mjs +3 -2
- package/engine/script-emphasis-terms.mjs +38 -0
- package/engine/script-emphasis.mjs +295 -0
- package/engine/script-font-delivery.mjs +4 -4
- package/engine/site-build.mjs +265 -313
- package/engine/site-config.mjs +26 -238
- package/engine/site-files.mjs +175 -0
- package/engine/site-index.mjs +9 -4
- package/engine/site-preview.mjs +50 -523
- package/engine/site-reach.mjs +213 -0
- package/engine/site-render.mjs +668 -0
- package/engine/site-root.mjs +90 -39
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +274 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +939 -0
- package/engine/sql-tables.mjs +9 -6
- package/engine/text-maps.mjs +57 -10
- package/engine/typographer.mjs +248 -0
- package/engine/verse-metre.mjs +357 -0
- package/engine/wikilink-syntax.mjs +29 -47
- package/engine/wikilinks.mjs +758 -279
- package/engine/word-forms.mjs +39 -24
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +29 -4
- package/manifest.mjs +3 -4
- package/package.json +10 -7
- package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
- package/site-theme/layouts/base.hbs +31 -0
- package/site-theme/layouts/blog-list.hbs +30 -0
- package/site-theme/layouts/blog-post.hbs +40 -0
- package/site-theme/layouts/home.hbs +35 -0
- package/site-theme/layouts/list.hbs +20 -0
- package/site-theme/layouts/not-found.hbs +31 -0
- package/site-theme/layouts/page.hbs +7 -0
- package/site-theme/partials/copy-buttons.hbs +37 -0
- package/site-theme/partials/draft-notice.hbs +5 -0
- package/site-theme/partials/exturl.hbs +2 -0
- package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
- package/site-theme/partials/header.hbs +44 -0
- package/site-theme/partials/hero-banner.hbs +11 -0
- package/site-theme/partials/infobox-entry.hbs +2 -0
- package/site-theme/partials/infobox-section.hbs +31 -0
- package/site-theme/partials/infobox-value.hbs +2 -0
- package/site-theme/partials/infobox.hbs +18 -0
- package/site-theme/partials/list-members.hbs +9 -0
- package/site-theme/partials/nav-links.hbs +5 -0
- package/site-theme/partials/page-body.hbs +48 -0
- package/site-theme/partials/page-list.hbs +14 -0
- package/site-theme/partials/related.hbs +24 -0
- package/site-theme/partials/search-meta.hbs +6 -0
- package/site-theme/partials/search-names.hbs +2 -0
- package/site-theme/partials/search.hbs +52 -0
- package/site-theme/partials/tags.hbs +4 -0
- package/site-theme/partials/toc.hbs +7 -0
- package/{hugo-theme → site-theme}/static/css/style.css +14 -0
- package/stage.mjs +1 -2
- package/types/config.d.mts +8 -4
- package/types/content-config.d.mts +15 -15
- package/types/engine/asset-shipping.d.mts +5 -2
- package/types/engine/base-compiler.d.mts +20 -2
- package/types/engine/content-address.d.mts +1 -1
- package/types/engine/content-alerts.d.mts +8 -2
- package/types/engine/content-blocks.d.mts +11 -4
- package/types/engine/content-expand.d.mts +288 -0
- package/types/engine/content-figures.d.mts +11 -3
- package/types/engine/content-glyphs.d.mts +85 -0
- package/types/engine/content-images.d.mts +6 -7
- package/types/engine/content-links.d.mts +1 -1
- package/types/engine/content-render.d.mts +199 -0
- package/types/engine/content-scripts.d.mts +62 -0
- package/types/engine/content-spans.d.mts +1 -1
- package/types/engine/content-styles.d.mts +33 -0
- package/types/engine/content-symbols.d.mts +35 -0
- package/types/engine/core-report.d.mts +45 -0
- package/types/engine/core-vocabulary.d.mts +53 -0
- package/types/engine/dictionary-html.d.mts +8 -0
- package/types/engine/field-reference.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +20 -0
- package/types/engine/helpers.d.mts +3 -10
- package/types/engine/homepage.d.mts +11 -24
- package/types/engine/html-minify.d.mts +9 -0
- package/types/engine/index.d.mts +2 -1
- package/types/engine/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +48 -8
- package/types/engine/map-places.d.mts +8 -0
- package/types/engine/markdown-expressions.d.mts +0 -2
- package/types/engine/metre-declaration.d.mts +39 -0
- package/types/engine/pdf-render.d.mts +6 -2
- package/types/engine/post-fields.d.mts +44 -0
- package/types/engine/preview-index.d.mts +72 -0
- package/types/engine/preview-source.d.mts +51 -0
- package/types/engine/script-emphasis-terms.d.mts +21 -0
- package/types/engine/script-emphasis.d.mts +84 -0
- package/types/engine/script-font-delivery.d.mts +4 -4
- package/types/engine/site-build.d.mts +71 -113
- package/types/engine/site-config.d.mts +14 -134
- package/types/engine/site-files.d.mts +73 -0
- package/types/engine/site-index.d.mts +11 -3
- package/types/engine/site-preview.d.mts +15 -19
- package/types/engine/site-reach.d.mts +76 -0
- package/types/engine/site-render.d.mts +173 -0
- package/types/engine/site-root.d.mts +14 -4
- package/types/engine/site-search.d.mts +54 -0
- package/types/engine/site-serve.d.mts +44 -0
- package/types/engine/site-theme-banner.d.mts +51 -0
- package/types/engine/site-theme-urls.d.mts +60 -0
- package/types/engine/site-theme.d.mts +127 -0
- package/types/engine/text-maps.d.mts +17 -3
- package/types/engine/typographer.d.mts +11 -0
- package/types/engine/verse-metre.d.mts +131 -0
- package/types/engine/wikilink-syntax.d.mts +30 -48
- package/types/engine/wikilinks.d.mts +330 -14
- package/types/engine/word-forms.d.mts +3 -2
- package/types/manifest.d.mts +3 -4
- package/types/stage.d.mts +1 -2
- package/types/vendor/markdownlint.d.mts +8 -0
- package/vendor/markdownlint.mjs +20388 -0
- package/engine/web-wikilinks.mjs +0 -594
- package/hugo-theme/layouts/404.html +0 -85
- package/hugo-theme/layouts/_default/baseof.html +0 -67
- package/hugo-theme/layouts/_default/list.html +0 -139
- package/hugo-theme/layouts/_default/single.html +0 -4
- package/hugo-theme/layouts/blog/list.html +0 -54
- package/hugo-theme/layouts/blog-post/single.html +0 -36
- package/hugo-theme/layouts/index.html +0 -75
- package/hugo-theme/layouts/partials/banner-url.html +0 -141
- package/hugo-theme/layouts/partials/cdn-url.html +0 -36
- package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
- package/hugo-theme/layouts/partials/draft-notice.html +0 -41
- package/hugo-theme/layouts/partials/draft-state.html +0 -26
- package/hugo-theme/layouts/partials/header.html +0 -35
- package/hugo-theme/layouts/partials/hero-banner.html +0 -85
- package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
- package/hugo-theme/layouts/partials/infobox/items.html +0 -26
- package/hugo-theme/layouts/partials/infobox/section.html +0 -56
- package/hugo-theme/layouts/partials/infobox/value.html +0 -43
- package/hugo-theme/layouts/partials/infobox.html +0 -57
- package/hugo-theme/layouts/partials/page-body.html +0 -152
- package/hugo-theme/layouts/partials/related.html +0 -138
- package/hugo-theme/layouts/partials/search.html +0 -84
- package/hugo-theme/layouts/partials/site-url.html +0 -32
- package/hugo-theme/layouts/partials/tags.html +0 -30
- package/hugo-theme/layouts/partials/toc.html +0 -28
- package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
- package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
- package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
- package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
- package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
- package/hugo-theme/theme.toml +0 -11
- package/types/engine/web-wikilinks.d.mts +0 -107
- /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
- /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
- /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
- /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
- /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
- /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
package/engine/site-config.mjs
CHANGED
|
@@ -12,76 +12,44 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* a copy that drifts. The navigation is neither: it is the list of packages
|
|
29
|
-
* the organisation publishes, which lives in one place, heroiclands-site's
|
|
30
|
-
* roster, and reaches every site as the `nav.json` that site publishes.
|
|
31
|
-
* package-build carries no package list; `deps fetch` caches the navigation
|
|
32
|
-
* and the site build writes `[menu.main]` from the cache.
|
|
15
|
+
* What a site build reads from the configuration: the site's address, title
|
|
16
|
+
* and parameters, and the organisation's navigation.
|
|
17
|
+
*
|
|
18
|
+
* Every value has one source. The package's identity (`baseURL`, `title`,
|
|
19
|
+
* `params.description`, `params.author`) is read from `package.json` and
|
|
20
|
+
* `package-build.config.yaml`, where it is already stated. The organisation's
|
|
21
|
+
* constants — the brand links — are stated once here, because they are the
|
|
22
|
+
* same on every site and a copy per repository is a copy that drifts. The
|
|
23
|
+
* navigation is neither: it is the list of packages the organisation
|
|
24
|
+
* publishes, which lives in one place, heroiclands-site's roster, and reaches
|
|
25
|
+
* every site as the `nav.json` that site publishes. package-build carries no
|
|
26
|
+
* package list; `deps fetch` caches the navigation and the site build reads
|
|
27
|
+
* the cache.
|
|
33
28
|
*
|
|
34
29
|
* What a repository still says for itself is the residue that is genuinely
|
|
35
|
-
* its own
|
|
36
|
-
* `site.hugo`, the one key nobody anticipated.
|
|
37
|
-
* {@link module:content-config.DERIVED_HUGO_KEYS} refuses everything the
|
|
38
|
-
* generator writes from being authored there too.
|
|
30
|
+
* its own, such as the wording of its "page not found" page.
|
|
39
31
|
*
|
|
40
32
|
* @module
|
|
41
33
|
*/
|
|
42
34
|
|
|
43
35
|
import fs from "node:fs";
|
|
44
36
|
import path from "node:path";
|
|
45
|
-
import { fileURLToPath } from "node:url";
|
|
46
|
-
import { stringify as stringifyToml } from "smol-toml";
|
|
47
37
|
|
|
48
38
|
import { checkHomepage, fail } from "../config.mjs";
|
|
49
|
-
import { slugify } from "./content-slug.mjs";
|
|
50
39
|
import { siteFontsParam } from "./script-font-delivery.mjs";
|
|
51
40
|
|
|
52
|
-
/** The Hugo source directory, relative to the repository root. */
|
|
53
|
-
export const HUGO_SOURCE = "build/hugo";
|
|
54
|
-
|
|
55
|
-
/** The content mount `package-build site` writes, relative to the repository root. */
|
|
56
|
-
export const HUGO_CONTENT = `${HUGO_SOURCE}/content`;
|
|
57
|
-
|
|
58
41
|
/**
|
|
59
42
|
* The directory that is deployed, relative to the repository root.
|
|
60
43
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
44
|
+
* `package-build site build` renders into `<DEPLOY_ROOT>/<contentPackage>/`
|
|
45
|
+
* and writes `_headers` beside it and the search index inside it.
|
|
63
46
|
*/
|
|
64
47
|
export const DEPLOY_ROOT = "build/site";
|
|
65
48
|
|
|
66
|
-
/**
|
|
67
|
-
* This package's own root, which is the directory Hugo reads themes from.
|
|
68
|
-
*
|
|
69
|
-
* The theme ships inside this package, so the directory holding it is found
|
|
70
|
-
* from this module's own location rather than searched for.
|
|
71
|
-
*/
|
|
72
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
73
|
-
|
|
74
|
-
/** The theme's directory name under `themesDir`. */
|
|
75
|
-
export const THEME = "hugo-theme";
|
|
76
|
-
|
|
77
|
-
/** The locale every site renders in. */
|
|
78
|
-
export const LOCALE = "en-us";
|
|
79
|
-
|
|
80
49
|
/**
|
|
81
50
|
* The brand chrome's own links, the same on every site.
|
|
82
51
|
*
|
|
83
|
-
* `logo` is
|
|
84
|
-
* the asset host rather than an address.
|
|
52
|
+
* `logo` is a path on the asset host rather than an address.
|
|
85
53
|
*/
|
|
86
54
|
export const BRAND = Object.freeze({
|
|
87
55
|
logo: "images/brand/sohl-icon-white.webp",
|
|
@@ -89,40 +57,6 @@ export const BRAND = Object.freeze({
|
|
|
89
57
|
discordURL: "https://discord.gg/EwMfkNd3az",
|
|
90
58
|
});
|
|
91
59
|
|
|
92
|
-
/**
|
|
93
|
-
* The kinds no site renders.
|
|
94
|
-
*
|
|
95
|
-
* A site is its homepage and its pages: `home` and `page` are the only kinds,
|
|
96
|
-
* for every package and whatever its notes carry. A `section` is a listing
|
|
97
|
-
* Hugo would generate from a content directory; `taxonomy` and `term` are
|
|
98
|
-
* listings it would generate from `tags:`; `RSS` is a feed. Every structure
|
|
99
|
-
* between the homepage and the pages is authored instead, as a `doc` note
|
|
100
|
-
* carrying a content table — a tag is a field such a table filters on, not a
|
|
101
|
-
* page of its own.
|
|
102
|
-
*/
|
|
103
|
-
export const DISABLE_KINDS = Object.freeze(["section", "taxonomy", "term", "RSS"]);
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* The markup settings the toolchain's own output requires.
|
|
107
|
-
*
|
|
108
|
-
* Pages are written with raw HTML in them — a `<figure>` for every image, a
|
|
109
|
-
* `<span>` marking an unresolved link — and Goldmark drops raw HTML unless
|
|
110
|
-
* told otherwise. A theme cannot supply this: Hugo does not merge a theme's
|
|
111
|
-
* `markup` block.
|
|
112
|
-
*
|
|
113
|
-
* `extensions.taskList` is off because the format has no checkbox: Goldmark
|
|
114
|
-
* enables it by default, which is the only reason a list item opened with
|
|
115
|
-
* `[ ]` renders a `<input type="checkbox">` here and the same two characters as
|
|
116
|
-
* plain text in the pack and the book. Off, a list item's text begins right
|
|
117
|
-
* after its marker on every surface.
|
|
118
|
-
*/
|
|
119
|
-
export const MARKUP = Object.freeze({
|
|
120
|
-
goldmark: Object.freeze({
|
|
121
|
-
renderer: Object.freeze({ unsafe: true }),
|
|
122
|
-
extensions: Object.freeze({ taskList: false }),
|
|
123
|
-
}),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
60
|
/** Where the navigation is published. */
|
|
127
61
|
export const NAVIGATION_URL = "https://www.heroiclands.org/nav.json";
|
|
128
62
|
|
|
@@ -144,17 +78,6 @@ const STAMP = ".complete";
|
|
|
144
78
|
* @property {NavigationEntry[]} [children] - A dropdown's entries.
|
|
145
79
|
*/
|
|
146
80
|
|
|
147
|
-
/**
|
|
148
|
-
* A Hugo menu entry, as `[[menu.main]]` states one.
|
|
149
|
-
*
|
|
150
|
-
* @typedef {object} MenuEntry
|
|
151
|
-
* @property {string} name
|
|
152
|
-
* @property {string} url
|
|
153
|
-
* @property {number} weight
|
|
154
|
-
* @property {string} [identifier] - Set on an entry that has children.
|
|
155
|
-
* @property {string} [parent] - Set on a child, naming its parent's identifier.
|
|
156
|
-
*/
|
|
157
|
-
|
|
158
81
|
/**
|
|
159
82
|
* @param {unknown} value - Anything.
|
|
160
83
|
* @returns {value is Record<string, unknown>} Whether it is a plain mapping.
|
|
@@ -180,7 +103,7 @@ function requireNonEmptyString(value, where) {
|
|
|
180
103
|
*
|
|
181
104
|
* `[{name, url, children?: [{name, url}]}]`, every `url` absolute. Checked
|
|
182
105
|
* on fetch and again on read: a file that is not a navigation would otherwise
|
|
183
|
-
* reach the
|
|
106
|
+
* reach the menu as `undefined` labels and links.
|
|
184
107
|
*
|
|
185
108
|
* @param {unknown} value - The parsed document.
|
|
186
109
|
* @param {string} where - What is being checked, for the error.
|
|
@@ -214,35 +137,6 @@ export function checkNavigation(value, where = "the navigation") {
|
|
|
214
137
|
return value.map((entry, i) => check(entry, `${where}[${i}]`, false));
|
|
215
138
|
}
|
|
216
139
|
|
|
217
|
-
/**
|
|
218
|
-
* The `[[menu.main]]` entries a navigation renders as.
|
|
219
|
-
*
|
|
220
|
-
* Entry for entry, in order; a dropdown is an entry with an `identifier` and
|
|
221
|
-
* its children are entries naming it as `parent`, which is how the theme's
|
|
222
|
-
* header partial draws one. Weights count from one within each level, so the
|
|
223
|
-
* order is the navigation's and not Hugo's alphabetical fallback.
|
|
224
|
-
*
|
|
225
|
-
* @param {readonly NavigationEntry[]} navigation - The navigation.
|
|
226
|
-
* @returns {MenuEntry[]} The menu entries.
|
|
227
|
-
*/
|
|
228
|
-
export function menuEntries(navigation) {
|
|
229
|
-
/** @type {MenuEntry[]} */
|
|
230
|
-
const out = [];
|
|
231
|
-
navigation.forEach((entry, index) => {
|
|
232
|
-
const children = entry.children ?? [];
|
|
233
|
-
if (!children.length) {
|
|
234
|
-
out.push({ name: entry.name, url: entry.url, weight: index + 1 });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const identifier = slugify(entry.name);
|
|
238
|
-
out.push({ name: entry.name, url: entry.url, weight: index + 1, identifier });
|
|
239
|
-
children.forEach((child, i) => {
|
|
240
|
-
out.push({ name: child.name, url: child.url, weight: i + 1, parent: identifier });
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
return out;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
140
|
/**
|
|
247
141
|
* Where the fetched navigation sits.
|
|
248
142
|
*
|
|
@@ -314,66 +208,23 @@ export async function fetchNavigation(
|
|
|
314
208
|
}
|
|
315
209
|
|
|
316
210
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* The theme ships inside this package at `hugo-theme/`, so the directory Hugo
|
|
321
|
-
* reads themes from is {@link PACKAGE_ROOT}. `theme` stays {@link THEME}, so
|
|
322
|
-
* Hugo opens `<themesDir>/hugo-theme/layouts`.
|
|
323
|
-
*
|
|
324
|
-
* Written as a path rather than assumed: `build/hugo/` and the installed
|
|
325
|
-
* package sit at whatever distance the install put them, and a worktree
|
|
326
|
-
* resolving its parent's install says so in the generated file.
|
|
327
|
-
*
|
|
328
|
-
* @param {string} rootDir - The repository root.
|
|
329
|
-
* @returns {string} The relative path, POSIX-separated.
|
|
330
|
-
*/
|
|
331
|
-
export function resolveThemesDir(rootDir) {
|
|
332
|
-
const from = path.resolve(rootDir, HUGO_SOURCE);
|
|
333
|
-
return path.relative(from, PACKAGE_ROOT).split(path.sep).join("/");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Deep-merge `overrides` over `base`, arrays replaced whole.
|
|
338
|
-
*
|
|
339
|
-
* @param {Record<string, unknown>} base - The generated configuration.
|
|
340
|
-
* @param {Record<string, unknown>} overrides - What `site.hugo` declares.
|
|
341
|
-
* @returns {Record<string, unknown>} A new object.
|
|
342
|
-
*/
|
|
343
|
-
function deepMerge(base, overrides) {
|
|
344
|
-
/** @type {Record<string, unknown>} */
|
|
345
|
-
const out = { ...base };
|
|
346
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
347
|
-
const current = out[key];
|
|
348
|
-
out[key] =
|
|
349
|
-
isPlainObject(current) && isPlainObject(value) ?
|
|
350
|
-
deepMerge(current, value)
|
|
351
|
-
: structuredClone(value);
|
|
352
|
-
}
|
|
353
|
-
return out;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* The Hugo configuration, as an object.
|
|
358
|
-
*
|
|
359
|
-
* Pure: every input is handed in, so a test can describe the generated shape
|
|
360
|
-
* without a repository on disk. {@link generateHugoConfig} is the same function
|
|
361
|
-
* with the reading put back.
|
|
211
|
+
* What a site is, read from the configuration: its address, its title and the
|
|
212
|
+
* parameters its layouts read.
|
|
362
213
|
*
|
|
363
214
|
* `checkHomepage` runs first, before any value is composed — a missing or
|
|
364
215
|
* mismatched `package.json` `homepage` is a finding on every site build.
|
|
365
216
|
*
|
|
366
217
|
* @param {object} options - The sources.
|
|
367
218
|
* @param {object} options.config - The resolved build configuration.
|
|
368
|
-
* @
|
|
369
|
-
*
|
|
370
|
-
*
|
|
219
|
+
* @returns {{baseURL: string, title: string, params: Record<string, unknown>}}
|
|
220
|
+
* The site. `params` holds `description`, `author`, `cdnBaseURL`, `brand`,
|
|
221
|
+
* `notfound` and `scriptFonts`, each where the configuration states it.
|
|
371
222
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
372
223
|
* configuration declares no `site.title` or `packageBuild.manifest.title`, no
|
|
373
224
|
* `site.description`, or no `site.assets`.
|
|
374
225
|
*/
|
|
375
|
-
export function
|
|
376
|
-
checkHomepage(config.homepage, config.contentPackage);
|
|
226
|
+
export function siteSettings({ config }) {
|
|
227
|
+
checkHomepage(config.homepage, config.contentPackage, config.site.base);
|
|
377
228
|
|
|
378
229
|
const title = config.site.title || config.packageBuild?.manifest?.title;
|
|
379
230
|
if (typeof title !== "string" || !title.trim()) {
|
|
@@ -407,68 +258,5 @@ export function hugoConfig({ config, navigation, themesDir }) {
|
|
|
407
258
|
// it does not ship.
|
|
408
259
|
const scriptFonts = siteFontsParam(config);
|
|
409
260
|
if (scriptFonts) params.scriptFonts = scriptFonts;
|
|
410
|
-
|
|
411
|
-
const generated = {
|
|
412
|
-
baseURL: config.homepage,
|
|
413
|
-
title,
|
|
414
|
-
locale: LOCALE,
|
|
415
|
-
publishDir: path.posix.relative(HUGO_SOURCE, `${DEPLOY_ROOT}/${config.contentPackage}`),
|
|
416
|
-
themesDir,
|
|
417
|
-
theme: THEME,
|
|
418
|
-
contentDir: path.posix.relative(HUGO_SOURCE, HUGO_CONTENT),
|
|
419
|
-
disableKinds: [...DISABLE_KINDS],
|
|
420
|
-
params,
|
|
421
|
-
markup: structuredClone(MARKUP),
|
|
422
|
-
menu: { main: menuEntries(navigation) },
|
|
423
|
-
};
|
|
424
|
-
return deepMerge(generated, config.site.hugo);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* The configuration as the TOML Hugo reads.
|
|
429
|
-
*
|
|
430
|
-
* @param {Record<string, unknown>} generated - From {@link hugoConfig}.
|
|
431
|
-
* @returns {string} The file's contents.
|
|
432
|
-
*/
|
|
433
|
-
export function hugoToml(generated) {
|
|
434
|
-
return (
|
|
435
|
-
"# Generated by `package-build site` from package.json and " +
|
|
436
|
-
"package-build.config.yaml.\n# Every value here has a source there; edit " +
|
|
437
|
-
"the source, not this file.\n\n" +
|
|
438
|
-
`${stringifyToml(generated)}\n`
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* The Hugo configuration, every source read from the repository.
|
|
444
|
-
*
|
|
445
|
-
* Reads the cached navigation and the installed theme's location, and
|
|
446
|
-
* composes them with {@link hugoConfig}. Nothing is written, so a caller can
|
|
447
|
-
* run this before touching the output tree and fail with it intact.
|
|
448
|
-
*
|
|
449
|
-
* @param {object} config - The resolved build configuration.
|
|
450
|
-
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
451
|
-
* @throws {Error} When any source is missing or wrong.
|
|
452
|
-
*/
|
|
453
|
-
export function generateHugoConfig(config) {
|
|
454
|
-
return hugoConfig({
|
|
455
|
-
config,
|
|
456
|
-
navigation: readCachedNavigation(config),
|
|
457
|
-
themesDir: resolveThemesDir(config.rootDir),
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Write `build/hugo/hugo.toml`.
|
|
463
|
-
*
|
|
464
|
-
* @param {object} config - The resolved build configuration.
|
|
465
|
-
* @param {Record<string, unknown>} [generated] - The configuration to write,
|
|
466
|
-
* when the caller already generated it. Generated here otherwise.
|
|
467
|
-
* @returns {{file: string}} The file written.
|
|
468
|
-
*/
|
|
469
|
-
export function writeHugoConfig(config, generated = generateHugoConfig(config)) {
|
|
470
|
-
const file = path.join(config.rootDir, HUGO_SOURCE, "hugo.toml");
|
|
471
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
472
|
-
fs.writeFileSync(file, hugoToml(generated));
|
|
473
|
-
return { file };
|
|
261
|
+
return { baseURL: config.homepage, title, params };
|
|
474
262
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **The files a site writes beside its pages**, for search engines and feed
|
|
16
|
+
* readers:
|
|
17
|
+
*
|
|
18
|
+
* | File | Holds |
|
|
19
|
+
* | ---- | ----- |
|
|
20
|
+
* | `sitemap.xml` | every page of the site, by its full address ({@link sitemapXml}) |
|
|
21
|
+
* | a sitemap index | every site's sitemap ({@link sitemapIndexXml}) |
|
|
22
|
+
* | `robots.txt` | where every sitemap is ({@link robotsTxt}) |
|
|
23
|
+
* | an Atom feed | a section's posts, newest first ({@link atomFeed}) |
|
|
24
|
+
*
|
|
25
|
+
* Each is a pure function of what it is given, and states no time of its own:
|
|
26
|
+
* a date is a page's date, so a build of the same content writes the same
|
|
27
|
+
* files.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** @param {unknown} value */
|
|
33
|
+
const text = (value) => (value === undefined || value === null ? "" : String(value));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Text escaped for XML content and attribute values.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} value - The text.
|
|
39
|
+
* @returns {string} The escaped text.
|
|
40
|
+
*/
|
|
41
|
+
export function escapeXml(value) {
|
|
42
|
+
return text(value)
|
|
43
|
+
.replace(/&/g, "&")
|
|
44
|
+
.replace(/</g, "<")
|
|
45
|
+
.replace(/>/g, ">")
|
|
46
|
+
.replace(/"/g, """)
|
|
47
|
+
.replace(/'/g, "'");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A date as an RFC 3339 timestamp, or `""` for a value that is no date.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value - A date, or a string a `Date` reads.
|
|
54
|
+
* @returns {string} The timestamp, in UTC.
|
|
55
|
+
*/
|
|
56
|
+
export function timestamp(value) {
|
|
57
|
+
if (value === undefined || value === null || value === "") return "";
|
|
58
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
59
|
+
return Number.isNaN(date.getTime()) ? "" : date.toISOString().replace(/\.000Z$/, "Z");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A site's sitemap.
|
|
64
|
+
*
|
|
65
|
+
* @param {Array<{loc: string, lastmod?: unknown}>} entries - Each page's full
|
|
66
|
+
* address, and the date it states where it states one.
|
|
67
|
+
* @returns {string} The document.
|
|
68
|
+
*/
|
|
69
|
+
export function sitemapXml(entries) {
|
|
70
|
+
const urls = entries.map((entry) => {
|
|
71
|
+
const lastmod = timestamp(entry.lastmod);
|
|
72
|
+
return [
|
|
73
|
+
" <url>",
|
|
74
|
+
` <loc>${escapeXml(entry.loc)}</loc>`,
|
|
75
|
+
...(lastmod ? [` <lastmod>${lastmod}</lastmod>`] : []),
|
|
76
|
+
" </url>",
|
|
77
|
+
].join("\n");
|
|
78
|
+
});
|
|
79
|
+
return [
|
|
80
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
81
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
82
|
+
...urls,
|
|
83
|
+
"</urlset>",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A sitemap index naming other sitemaps.
|
|
90
|
+
*
|
|
91
|
+
* @param {readonly string[]} sitemaps - Each sitemap's full address.
|
|
92
|
+
* @returns {string} The document.
|
|
93
|
+
*/
|
|
94
|
+
export function sitemapIndexXml(sitemaps) {
|
|
95
|
+
return [
|
|
96
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
97
|
+
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
98
|
+
...sitemaps.map((loc) => ` <sitemap>\n <loc>${escapeXml(loc)}</loc>\n </sitemap>`),
|
|
99
|
+
"</sitemapindex>",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The `robots.txt` of a site's root: every agent may read every page, and
|
|
106
|
+
* every sitemap is named.
|
|
107
|
+
*
|
|
108
|
+
* @param {readonly string[]} sitemaps - Each sitemap's full address, the
|
|
109
|
+
* sitemap index first.
|
|
110
|
+
* @returns {string} The file.
|
|
111
|
+
*/
|
|
112
|
+
export function robotsTxt(sitemaps) {
|
|
113
|
+
return ["User-agent: *", "Allow: /", "", ...sitemaps.map((loc) => `Sitemap: ${loc}`), ""].join(
|
|
114
|
+
"\n",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* An Atom feed.
|
|
120
|
+
*
|
|
121
|
+
* The feed is dated by its newest entry, and an entry by its own date, so the
|
|
122
|
+
* feed changes only when a post does.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} feed - The feed.
|
|
125
|
+
* @param {string} feed.title - The feed's title.
|
|
126
|
+
* @param {string} feed.id - The full address of the page the feed follows,
|
|
127
|
+
* which is also its id.
|
|
128
|
+
* @param {string} feed.self - The feed's own full address.
|
|
129
|
+
* @param {string} [feed.author] - Who writes the posts, where an entry names
|
|
130
|
+
* no author of its own.
|
|
131
|
+
* @param {Array<{title: string, url: string, date: unknown, author?: string,
|
|
132
|
+
* summary?: string, content?: string}>} feed.entries - The posts, each with
|
|
133
|
+
* its full address and its HTML `content`.
|
|
134
|
+
* @returns {string} The document.
|
|
135
|
+
*/
|
|
136
|
+
export function atomFeed({ title, id, self, author, entries }) {
|
|
137
|
+
const dated = entries
|
|
138
|
+
.map((entry) => ({ ...entry, updated: timestamp(entry.date) }))
|
|
139
|
+
.filter((entry) => entry.updated)
|
|
140
|
+
.sort((a, b) =>
|
|
141
|
+
a.updated < b.updated ? 1
|
|
142
|
+
: a.updated > b.updated ? -1
|
|
143
|
+
: a.url < b.url ? -1
|
|
144
|
+
: 1,
|
|
145
|
+
);
|
|
146
|
+
const name = (who) => `<author><name>${escapeXml(who)}</name></author>`;
|
|
147
|
+
const lines = [
|
|
148
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
149
|
+
'<feed xmlns="http://www.w3.org/2005/Atom">',
|
|
150
|
+
` <title>${escapeXml(title)}</title>`,
|
|
151
|
+
` <id>${escapeXml(id)}</id>`,
|
|
152
|
+
` <link rel="self" type="application/atom+xml" href="${escapeXml(self)}"/>`,
|
|
153
|
+
` <link rel="alternate" type="text/html" href="${escapeXml(id)}"/>`,
|
|
154
|
+
...(dated.length ? [` <updated>${dated[0].updated}</updated>`] : []),
|
|
155
|
+
...(author ? [` ${name(author)}`] : []),
|
|
156
|
+
];
|
|
157
|
+
for (const entry of dated) {
|
|
158
|
+
lines.push(
|
|
159
|
+
" <entry>",
|
|
160
|
+
` <title>${escapeXml(entry.title)}</title>`,
|
|
161
|
+
` <id>${escapeXml(entry.url)}</id>`,
|
|
162
|
+
` <link rel="alternate" type="text/html" href="${escapeXml(entry.url)}"/>`,
|
|
163
|
+
` <published>${entry.updated}</published>`,
|
|
164
|
+
` <updated>${entry.updated}</updated>`,
|
|
165
|
+
...(entry.author ? [` ${name(entry.author)}`] : []),
|
|
166
|
+
...(entry.summary ? [` <summary>${escapeXml(entry.summary)}</summary>`] : []),
|
|
167
|
+
...(entry.content ?
|
|
168
|
+
[` <content type="html">${escapeXml(entry.content)}</content>`]
|
|
169
|
+
: []),
|
|
170
|
+
" </entry>",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
lines.push("</feed>", "");
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
package/engine/site-index.mjs
CHANGED
|
@@ -82,12 +82,15 @@ import { isGmNote, isDraftNote } from "./note-vocabulary.mjs";
|
|
|
82
82
|
* through unindexed.
|
|
83
83
|
* @property {object} fm The note's frontmatter.
|
|
84
84
|
* @property {string} name Display name.
|
|
85
|
-
* @property {string} slug
|
|
86
|
-
* @property {string} base
|
|
85
|
+
* @property {string} [slug] URL segment.
|
|
86
|
+
* @property {string} [base] Source file's basename, e.g. `Climbing.md`.
|
|
87
87
|
* @property {string} url The page's published address.
|
|
88
88
|
* @property {string} [body] The note's markdown body, for its anchors. A
|
|
89
89
|
* content entry always carries one; absent, the
|
|
90
90
|
* page is indexed as declaring none.
|
|
91
|
+
* @property {object[]} [anchors] The anchors the body declares, when the
|
|
92
|
+
* caller holds them already — a content-index
|
|
93
|
+
* record's — in place of reading `body`.
|
|
91
94
|
*/
|
|
92
95
|
|
|
93
96
|
/**
|
|
@@ -163,7 +166,9 @@ function mergeForeign(index, foreignIndex) {
|
|
|
163
166
|
* Build the address index a site's wikilink resolver reads.
|
|
164
167
|
*
|
|
165
168
|
* @param {readonly SiteEntry[]} entries - Every page the site will publish,
|
|
166
|
-
* each already knowing its own `url`.
|
|
169
|
+
* each already knowing its own `url`. An entry may carry the `anchors` its
|
|
170
|
+
* body declares, from a content-index record, in place of the body they are
|
|
171
|
+
* read from.
|
|
167
172
|
* @param {object} [options] - Cross-package inputs.
|
|
168
173
|
* @param {string} [options.package] - The builder's own content package.
|
|
169
174
|
* @param {string} [options.currentDate="720.1"] - Setting-wide neutral date for recurring events.
|
|
@@ -254,7 +259,7 @@ export function buildSiteIndex(
|
|
|
254
259
|
// `subType` is: a being states a rank as a number, and what that
|
|
255
260
|
// number is called is declared on the body.
|
|
256
261
|
const standings = standingsDigest(e.fm);
|
|
257
|
-
const noteAnchors = collectAnchors(e.body ?? "", 1, undefined, syntax);
|
|
262
|
+
const noteAnchors = e.anchors ?? collectAnchors(e.body ?? "", 1, undefined, syntax);
|
|
258
263
|
const value = {
|
|
259
264
|
url: e.url,
|
|
260
265
|
name: e.name,
|