@heroiclands/package-build 22.22.0 → 22.24.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 +46 -0
- package/README.md +1 -0
- package/bin/content-commands.mjs +51 -14
- package/ci/content-format-reference.mjs +1 -1
- package/content-config.mjs +80 -2
- package/docs/api.md +85 -72
- package/docs/authoring/assets.md +29 -0
- package/docs/authoring/dates-and-calendars.md +37 -1
- package/docs/authoring/links-and-markup.md +112 -31
- package/docs/commands.md +30 -29
- package/docs/configuration.md +92 -19
- package/docs/content-format.md +1 -1
- package/docs/guides/book.md +5 -4
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +41 -0
- package/docs/project-setup.md +4 -4
- package/docs/reference/format-details.md +323 -60
- package/docs/reference/note-types.md +9 -9
- package/docs/reference/theme-pages.md +496 -0
- package/engine/anchors.mjs +16 -10
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +9 -2
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +54 -2
- package/engine/bundles.mjs +13 -1
- package/engine/content-blocks.mjs +4 -4
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +10 -2
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +36 -10
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +145 -19
- package/engine/frontmatter-lint.mjs +73 -1
- package/engine/heading-attributes.mjs +31 -18
- package/engine/helpers.mjs +102 -33
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox.mjs +47 -8
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +264 -39
- package/engine/lore-events.mjs +197 -0
- package/engine/markdown-expressions.mjs +257 -7
- package/engine/note-dates.mjs +305 -12
- package/engine/note-vocabulary.mjs +11 -5
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +357 -34
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +160 -36
- package/engine/scenes.mjs +16 -4
- package/engine/site-build.mjs +127 -13
- package/engine/site-config.mjs +19 -24
- package/engine/site-index.mjs +72 -1
- package/engine/site-preview.mjs +2 -0
- package/engine/system-block.mjs +44 -16
- package/hm3/actors.mjs +2 -2
- package/hugo-theme/data/banners.yaml +69 -0
- package/hugo-theme/layouts/404.html +85 -0
- package/hugo-theme/layouts/_default/baseof.html +64 -0
- package/hugo-theme/layouts/_default/list.html +139 -0
- package/hugo-theme/layouts/_default/single.html +4 -0
- package/hugo-theme/layouts/blog/list.html +54 -0
- package/hugo-theme/layouts/blog-post/single.html +36 -0
- package/hugo-theme/layouts/index.html +75 -0
- package/hugo-theme/layouts/partials/banner-url.html +117 -0
- package/hugo-theme/layouts/partials/cdn-url.html +36 -0
- package/hugo-theme/layouts/partials/draft-notice.html +41 -0
- package/hugo-theme/layouts/partials/draft-state.html +26 -0
- package/hugo-theme/layouts/partials/footer.html +29 -0
- package/hugo-theme/layouts/partials/header.html +35 -0
- package/hugo-theme/layouts/partials/hero-banner.html +86 -0
- package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
- package/hugo-theme/layouts/partials/holdings.html +61 -0
- package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
- package/hugo-theme/layouts/partials/infobox/items.html +26 -0
- package/hugo-theme/layouts/partials/infobox/section.html +56 -0
- package/hugo-theme/layouts/partials/infobox/value.html +43 -0
- package/hugo-theme/layouts/partials/infobox.html +57 -0
- package/hugo-theme/layouts/partials/page-body.html +154 -0
- package/hugo-theme/layouts/partials/place-map.html +9 -0
- package/hugo-theme/layouts/partials/related.html +121 -0
- package/hugo-theme/layouts/partials/search.html +84 -0
- package/hugo-theme/layouts/partials/site-url.html +32 -0
- package/hugo-theme/layouts/partials/tags.html +30 -0
- package/hugo-theme/layouts/partials/toc.html +28 -0
- package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
- package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
- package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
- package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
- package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
- package/hugo-theme/static/css/sohl-icons.css +57 -0
- package/hugo-theme/static/css/style.css +2953 -0
- package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
- package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
- package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
- package/hugo-theme/theme.toml +11 -0
- package/manifest.mjs +30 -24
- package/package.json +2 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/item-fields.mjs +4 -3
- package/types/content-config.d.mts +48 -1
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/art-slots.d.mts +2 -1
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +0 -17
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +7 -0
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/heading-attributes.d.mts +13 -8
- package/types/engine/helpers.d.mts +28 -10
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +31 -9
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +23 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +12 -0
- package/types/engine/site-build.d.mts +14 -3
- package/types/engine/site-config.d.mts +9 -9
- package/types/engine/site-index.d.mts +54 -0
- package/types/engine/system-block.d.mts +26 -2
- package/types/hm3/actors.d.mts +18 -0
- package/types/manifest.d.mts +19 -23
- package/engine/content-captions.mjs +0 -230
- package/types/engine/content-captions.d.mts +0 -58
package/engine/site-config.mjs
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
import fs from "node:fs";
|
|
44
44
|
import path from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
45
46
|
import { stringify as stringifyToml } from "smol-toml";
|
|
46
47
|
|
|
47
48
|
import { checkHomepage, fail } from "../config.mjs";
|
|
@@ -61,10 +62,15 @@ export const HUGO_CONTENT = `${HUGO_SOURCE}/content`;
|
|
|
61
62
|
*/
|
|
62
63
|
export const DEPLOY_ROOT = "build/site";
|
|
63
64
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* This package's own root, which is the directory Hugo reads themes from.
|
|
67
|
+
*
|
|
68
|
+
* The theme ships inside this package, so the directory holding it is found
|
|
69
|
+
* from this module's own location rather than searched for.
|
|
70
|
+
*/
|
|
71
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
66
72
|
|
|
67
|
-
/** The theme's name under `themesDir
|
|
73
|
+
/** The theme's directory name under `themesDir`. */
|
|
68
74
|
export const THEME = "hugo-theme";
|
|
69
75
|
|
|
70
76
|
/** The locale every site renders in. */
|
|
@@ -308,33 +314,22 @@ export async function fetchNavigation(
|
|
|
308
314
|
|
|
309
315
|
/**
|
|
310
316
|
* The `themesDir` for a repository, as the path from `build/hugo/` to the
|
|
311
|
-
* directory holding the
|
|
317
|
+
* directory holding the theme.
|
|
318
|
+
*
|
|
319
|
+
* The theme ships inside this package at `hugo-theme/`, so the directory Hugo
|
|
320
|
+
* reads themes from is {@link PACKAGE_ROOT}. `theme` stays {@link THEME}, so
|
|
321
|
+
* Hugo opens `<themesDir>/hugo-theme/layouts`.
|
|
312
322
|
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* generated file.
|
|
323
|
+
* Written as a path rather than assumed: `build/hugo/` and the installed
|
|
324
|
+
* package sit at whatever distance the install put them, and a worktree
|
|
325
|
+
* resolving its parent's install says so in the generated file.
|
|
317
326
|
*
|
|
318
327
|
* @param {string} rootDir - The repository root.
|
|
319
328
|
* @returns {string} The relative path, POSIX-separated.
|
|
320
|
-
* @throws {Error} When the theme is installed nowhere above the root.
|
|
321
329
|
*/
|
|
322
330
|
export function resolveThemesDir(rootDir) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const scope = path.join(dir, "node_modules", path.dirname(THEME_PACKAGE));
|
|
326
|
-
if (fs.existsSync(path.join(scope, path.basename(THEME_PACKAGE), "theme.toml"))) {
|
|
327
|
-
const rel = path.relative(path.resolve(rootDir, HUGO_SOURCE), scope);
|
|
328
|
-
return rel.split(path.sep).join("/");
|
|
329
|
-
}
|
|
330
|
-
const parent = path.dirname(dir);
|
|
331
|
-
if (parent === dir) break;
|
|
332
|
-
dir = parent;
|
|
333
|
-
}
|
|
334
|
-
throw new Error(
|
|
335
|
-
`${THEME_PACKAGE} is not installed anywhere above ${rootDir} — add it to ` +
|
|
336
|
-
"`devDependencies` and run `npm ci`",
|
|
337
|
-
);
|
|
331
|
+
const from = path.resolve(rootDir, HUGO_SOURCE);
|
|
332
|
+
return path.relative(from, PACKAGE_ROOT).split(path.sep).join("/");
|
|
338
333
|
}
|
|
339
334
|
|
|
340
335
|
/**
|
package/engine/site-index.mjs
CHANGED
|
@@ -66,10 +66,12 @@ import {
|
|
|
66
66
|
renderAddress,
|
|
67
67
|
isAddressTuple,
|
|
68
68
|
ownDocumentSystem,
|
|
69
|
+
expandAddress,
|
|
69
70
|
} from "./address.mjs";
|
|
70
71
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
71
72
|
import { contentPackage } from "./content-package.mjs";
|
|
72
73
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
74
|
+
import { presentAmongRecords } from "./being-age.mjs";
|
|
73
75
|
// The declared tag vocabulary, which is where `draft` is stated.
|
|
74
76
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
75
77
|
|
|
@@ -277,7 +279,12 @@ export function buildSiteIndex(
|
|
|
277
279
|
|
|
278
280
|
return {
|
|
279
281
|
contentPackage: ownPackage,
|
|
280
|
-
|
|
282
|
+
// Carried on the same context `resolvedDateFields` reads: a recurring
|
|
283
|
+
// event's `next` is computed against this present.
|
|
284
|
+
dateContext: {
|
|
285
|
+
...reckoningContext({ notes: records }),
|
|
286
|
+
present: presentAmongRecords(records),
|
|
287
|
+
},
|
|
281
288
|
referenceTargets: buildReferenceTargets(records, new Map([...foreignReferences, ...index])),
|
|
282
289
|
index,
|
|
283
290
|
ambiguous,
|
|
@@ -288,6 +295,70 @@ export function buildSiteIndex(
|
|
|
288
295
|
};
|
|
289
296
|
}
|
|
290
297
|
|
|
298
|
+
/**
|
|
299
|
+
* The address keys one page's figures are reachable by — the same two forms
|
|
300
|
+
* {@link buildSiteIndex} assigns the page itself, so a `ref` crossing into it
|
|
301
|
+
* resolves by exactly the address a wikilink would use.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} pkg - The page's own content package.
|
|
304
|
+
* @param {string} type - The note's `type`.
|
|
305
|
+
* @param {string} shortcode - The note's `shortcode`.
|
|
306
|
+
* @returns {Set<string>} The short `type/shortcode` key and the canonical
|
|
307
|
+
* `package-system-type-shortcode` one.
|
|
308
|
+
*/
|
|
309
|
+
export function figureIndexKeys(pkg, type, shortcode) {
|
|
310
|
+
const system = ownDocumentSystem(type);
|
|
311
|
+
const keys = new Set([
|
|
312
|
+
`${type}/${shortcode}`.toLowerCase(),
|
|
313
|
+
canonicalKey(pkg, system, type, shortcode),
|
|
314
|
+
]);
|
|
315
|
+
if (system !== NOTE_SYSTEM) keys.add(canonicalKey(pkg, NOTE_SYSTEM, type, shortcode));
|
|
316
|
+
return keys;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolve a `ref` call's cross-note address to the target's own figures and
|
|
321
|
+
* its own address — the `figures.note` a `ref` helper's render context reads,
|
|
322
|
+
* built once and threaded into every page.
|
|
323
|
+
*
|
|
324
|
+
* Reads the written address exactly as a wikilink does
|
|
325
|
+
* ({@link readQualifier}, {@link expandAddress}), so a `ref` crossing into
|
|
326
|
+
* another note and a `[[…]]` link to the same note agree on what resolves and
|
|
327
|
+
* what does not: a target naming no known type or no fetched package's index
|
|
328
|
+
* is unresolved for both, and one address names one note for both.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} target - The address as written, anchor already removed —
|
|
331
|
+
* {@link module:engine/wikilink-syntax.ParsedWikilink.target}.
|
|
332
|
+
* @param {object} options
|
|
333
|
+
* @param {Set<string>} options.contentTypes - Every type this build reads as
|
|
334
|
+
* an address qualifier.
|
|
335
|
+
* @param {Set<string>} options.packages - Every package an address may name.
|
|
336
|
+
* @param {Set<string>} options.noIndexPackages - Packages declared
|
|
337
|
+
* `contentIndex: false`.
|
|
338
|
+
* @param {string} options.contentPackage - The package a target with no
|
|
339
|
+
* package segment defaults to.
|
|
340
|
+
* @param {Map<string, {url: string}>} options.siteIndexMap - {@link SiteIndex}'s
|
|
341
|
+
* own `index`, read for the target's existence and its URL.
|
|
342
|
+
* @param {Map<string, Map<string, object>>} options.figuresByAddress - Every
|
|
343
|
+
* note's figures, by id, keyed as {@link figureIndexKeys} keys the note.
|
|
344
|
+
* @returns {{url: string, figures: Map<string, object>}|undefined} The
|
|
345
|
+
* target's own address and figures, or `undefined` when the address names
|
|
346
|
+
* no note this build resolves.
|
|
347
|
+
*/
|
|
348
|
+
export function resolveCrossNoteFigures(
|
|
349
|
+
target,
|
|
350
|
+
{ contentTypes, packages, noIndexPackages, contentPackage, siteIndexMap, figuresByAddress },
|
|
351
|
+
) {
|
|
352
|
+
const read = readQualifier(target, contentTypes, packages, noIndexPackages);
|
|
353
|
+
if (!read || read.reason) return undefined;
|
|
354
|
+
const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
|
|
355
|
+
const shortKey = `${read.itemDoc ? "doc" : ""}${read.type}/${read.shortcode}`.toLowerCase();
|
|
356
|
+
const entry = siteIndexMap.get(key) ?? siteIndexMap.get(shortKey);
|
|
357
|
+
if (!entry) return undefined;
|
|
358
|
+
const figures = figuresByAddress.get(key) ?? figuresByAddress.get(shortKey) ?? new Map();
|
|
359
|
+
return { url: entry.url, figures };
|
|
360
|
+
}
|
|
361
|
+
|
|
291
362
|
/**
|
|
292
363
|
* The per-page context a wikilink resolver takes.
|
|
293
364
|
*
|
package/engine/site-preview.mjs
CHANGED
|
@@ -270,6 +270,8 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
270
270
|
const pos = positionOfLiteral(text, error.src, error.occurrence);
|
|
271
271
|
findings.push({ ...error, ...pos, severity: "error" });
|
|
272
272
|
}
|
|
273
|
+
for (const error of result.embedErrors)
|
|
274
|
+
findings.push({ ...error, severity: "error" });
|
|
273
275
|
if (findings.length) return { ok: false, findings };
|
|
274
276
|
const edges = snapshot.rendered.edges.filter(([source]) => source !== original.url);
|
|
275
277
|
for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
|
package/engine/system-block.mjs
CHANGED
|
@@ -104,21 +104,24 @@
|
|
|
104
104
|
*
|
|
105
105
|
* A field's `name` is both its identity and the shared property it draws from,
|
|
106
106
|
* and those coincide only while the two vocabularies agree about what the
|
|
107
|
-
* spelling means
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* spelling means — or while the top-level vocabulary admits the spelling at
|
|
108
|
+
* all. They do not always. An `affiliation` item's `system.title` is the style
|
|
109
|
+
* of address an office carries — "Ajaw", "Warden"; a note's top-level `title`
|
|
110
|
+
* is refused outright by the closed top-level vocabulary, which has no `title`
|
|
111
|
+
* entry. Nothing relates them, and step 3 fed the first from the second anyway.
|
|
111
112
|
*
|
|
112
113
|
* It was not a harmless coincidence either, because step 3 answers **without**
|
|
113
114
|
* applying `field.default` — only step 2 does — so an authored `title: null`
|
|
114
115
|
* reached the field's coercion unguarded and shipped as the literal string
|
|
115
116
|
* `"null"` in fifteen documents.
|
|
116
117
|
*
|
|
117
|
-
* So a field may declare `topLevelMeans`:
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* the
|
|
118
|
+
* So a field may declare `topLevelMeans`: why the note's top-level key of this
|
|
119
|
+
* name is not this field's source — declared wherever that key's spelling
|
|
120
|
+
* means something else at the note level, or the closed top-level vocabulary
|
|
121
|
+
* refuses it outright. Declaring it removes the whole shared level — step 3
|
|
122
|
+
* and the retiring 3b alike, since both read the note's top level and the
|
|
123
|
+
* objection is to that level, not to a spelling — leaving the two positions
|
|
124
|
+
* that describe the document rather than the note. It is deliberately
|
|
122
125
|
* a per-field opt-out rather than a change to the order — step 3 is right
|
|
123
126
|
* wherever the two levels state the same quantity, which is nearly everywhere —
|
|
124
127
|
* and its value is the reason rather than a bare flag, so the collision is
|
|
@@ -362,22 +365,47 @@ export function blockProperty(fm, block, key, defaultValue = undefined) {
|
|
|
362
365
|
}
|
|
363
366
|
|
|
364
367
|
/**
|
|
365
|
-
* A system override followed by a shared `data:` value
|
|
366
|
-
*
|
|
368
|
+
* A system override followed by a shared `data:` value followed by the
|
|
369
|
+
* retiring top-level spelling `data:` gathered it off, reporting which one
|
|
370
|
+
* answered.
|
|
371
|
+
*
|
|
372
|
+
* {@link module:engine/field-spec.resolveFieldValue}'s counterpart for a
|
|
373
|
+
* universal `data:` key — `pack` and `packFolder`, declared for every note
|
|
374
|
+
* type rather than per type, so neither is a `FieldSpec` that resolver takes.
|
|
375
|
+
* The order is the same shape: a system's own override, then the shared
|
|
376
|
+
* `data:` position, then the bare top-level key a tree authored before
|
|
377
|
+
* `data:` existed. The top-level value stays readable while a tree still on
|
|
378
|
+
* it moves its authored fields.
|
|
367
379
|
*
|
|
368
380
|
* @param {object} fm - The note's frontmatter.
|
|
369
381
|
* @param {string} block - The system block key.
|
|
370
382
|
* @param {string} key - The shared property.
|
|
371
383
|
* @param {any} [defaultValue] - Returned when no position declares it.
|
|
372
|
-
* @returns {any} The
|
|
384
|
+
* @returns {{value: any, from: FieldSource}} The value, and where it came
|
|
385
|
+
* from.
|
|
373
386
|
*/
|
|
374
|
-
export function
|
|
387
|
+
export function resolveDataProperty(fm, block, key, defaultValue = undefined) {
|
|
375
388
|
const declared = systemBlock(fm, block)?.[key];
|
|
376
|
-
if (declared !== undefined && declared !== null) return declared;
|
|
389
|
+
if (declared !== undefined && declared !== null) return { value: declared, from: "block" };
|
|
377
390
|
const shared = isMapping(fm?.data) ? fm.data[key] : undefined;
|
|
378
|
-
if (shared !== undefined && shared !== null) return shared;
|
|
391
|
+
if (shared !== undefined && shared !== null) return { value: shared, from: "shared" };
|
|
379
392
|
const legacy = isMapping(fm) ? fm[key] : undefined;
|
|
380
|
-
|
|
393
|
+
if (legacy !== undefined && legacy !== null) return { value: legacy, from: "topLevel" };
|
|
394
|
+
return { value: defaultValue, from: "default" };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* {@link resolveDataProperty}'s value alone, for a caller that only compiles
|
|
399
|
+
* a document and has no use for which position answered.
|
|
400
|
+
*
|
|
401
|
+
* @param {object} fm - The note's frontmatter.
|
|
402
|
+
* @param {string} block - The system block key.
|
|
403
|
+
* @param {string} key - The shared property.
|
|
404
|
+
* @param {any} [defaultValue] - Returned when no position declares it.
|
|
405
|
+
* @returns {any} The selected value.
|
|
406
|
+
*/
|
|
407
|
+
export function blockDataProperty(fm, block, key, defaultValue = undefined) {
|
|
408
|
+
return resolveDataProperty(fm, block, key, defaultValue).value;
|
|
381
409
|
}
|
|
382
410
|
|
|
383
411
|
/**
|
package/hm3/actors.mjs
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
*
|
|
79
79
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
80
80
|
*/
|
|
81
|
-
const ACTOR_FIELDS = Object.freeze([
|
|
81
|
+
export const ACTOR_FIELDS = Object.freeze([
|
|
82
82
|
{
|
|
83
83
|
name: "species",
|
|
84
84
|
to: "species",
|
|
@@ -97,7 +97,7 @@ const ACTOR_FIELDS = Object.freeze([
|
|
|
97
97
|
*
|
|
98
98
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
99
99
|
*/
|
|
100
|
-
const CHARACTER_FIELDS = Object.freeze([
|
|
100
|
+
export const CHARACTER_FIELDS = Object.freeze([
|
|
101
101
|
{
|
|
102
102
|
name: "data.gender",
|
|
103
103
|
legacyKey: "gender",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Which banner images exist in the shared artwork set.
|
|
2
|
+
#
|
|
3
|
+
# `partials/hero-banner.html` resolves a page's hero image to a path under
|
|
4
|
+
# `images/banners/`, and that path is emitted into a `background-image` with no
|
|
5
|
+
# way to test it: the artwork is served from the consuming site's
|
|
6
|
+
# `params.cdnBaseURL`, and Hugo cannot ask a remote host whether a URL exists.
|
|
7
|
+
# So a banner that was never drawn used to 404 in silence — the band rendered
|
|
8
|
+
# with its title, palette and gradient intact and a dead image URL behind it,
|
|
9
|
+
# and nothing failed: not the build, not Hugo, not the deploy guard
|
|
10
|
+
# (issue #36).
|
|
11
|
+
#
|
|
12
|
+
# This file is the declaration that replaces that missing test. A name listed
|
|
13
|
+
# here is asserted to exist; a resolved banner whose name is absent falls back
|
|
14
|
+
# to `default.webp` and Hugo warns once for it during the consumer's build.
|
|
15
|
+
#
|
|
16
|
+
# It speaks for the names this theme resolves itself — a hand-authored
|
|
17
|
+
# `banner:` and the subtype defaults below. A note's own `data.banner` arrives
|
|
18
|
+
# already resolved from an asset Address, against a real asset record, so it is
|
|
19
|
+
# used as given and never looked for here.
|
|
20
|
+
#
|
|
21
|
+
# Keeping it in step is not left to hope: `npm run lint:banners`
|
|
22
|
+
# (`utils/check-banners.mjs`, part of `npm run lint`) fetches every name below
|
|
23
|
+
# from a real asset host and fails on any non-200. Add a name here only after
|
|
24
|
+
# the file is published, or the check will say so.
|
|
25
|
+
#
|
|
26
|
+
# A consumer publishing its own artwork set overrides the whole list by
|
|
27
|
+
# shipping its own `data/banners.yaml` — Hugo reads the site's data directory
|
|
28
|
+
# in preference to the theme's.
|
|
29
|
+
|
|
30
|
+
# The asset root every name below is resolved under, and the name used when a
|
|
31
|
+
# resolved banner is not in `available`. Both are relative to the consuming
|
|
32
|
+
# site's `params.cdnBaseURL`, like every other image path in this theme.
|
|
33
|
+
dir: images/banners
|
|
34
|
+
fallback: default.webp
|
|
35
|
+
|
|
36
|
+
# Verified against https://cdn.heroiclands.org on 2026-08-29.
|
|
37
|
+
available:
|
|
38
|
+
- affliction
|
|
39
|
+
- armor-clothing
|
|
40
|
+
- character
|
|
41
|
+
- company
|
|
42
|
+
- containers
|
|
43
|
+
- continent
|
|
44
|
+
- creature
|
|
45
|
+
- default
|
|
46
|
+
- dev-docs
|
|
47
|
+
- faith
|
|
48
|
+
- fvtt-add-on-modules
|
|
49
|
+
- fvtt-cc-anvil
|
|
50
|
+
- misc-gear
|
|
51
|
+
- module-dnload
|
|
52
|
+
- mystical-abilities
|
|
53
|
+
- orators-chance
|
|
54
|
+
- organization
|
|
55
|
+
- pantheon
|
|
56
|
+
- people
|
|
57
|
+
- projectiles
|
|
58
|
+
- region
|
|
59
|
+
- rules
|
|
60
|
+
- settlement
|
|
61
|
+
- site
|
|
62
|
+
- skills-attributes
|
|
63
|
+
- sohl-icon-3d-1920x1080
|
|
64
|
+
- ta-kheperu-banner
|
|
65
|
+
- tamzir-banner
|
|
66
|
+
- tapestry-of-dreams
|
|
67
|
+
- trauma
|
|
68
|
+
- user-guide
|
|
69
|
+
- weapons
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
"Page not found" page.
|
|
3
|
+
|
|
4
|
+
Hugo renders this template to `public/404.html`, and a static host such as
|
|
5
|
+
Cloudflare Pages serves that file — with a genuine HTTP 404 — for any path
|
|
6
|
+
the deployment does not publish. Without it, Pages falls back to the site
|
|
7
|
+
root, so an unknown path answers 200 with the landing page: a soft-404 that
|
|
8
|
+
makes every "does this URL resolve?" check report success, and lets search
|
|
9
|
+
engines index retired content as live (issue #1416).
|
|
10
|
+
|
|
11
|
+
Every consumer needs exactly this, so it lives here rather than being copied
|
|
12
|
+
into each one (issue #1454). The theme carries layout, not addresses, so the
|
|
13
|
+
wording and the routes back are supplied by the consuming site:
|
|
14
|
+
|
|
15
|
+
[params.notfound]
|
|
16
|
+
heroimage = "images/banner.webp" # CDN-relative or absolute
|
|
17
|
+
tagline = "This site has no page at"
|
|
18
|
+
sitenoun = "site" # used in the body prose
|
|
19
|
+
[[params.notfound.links]]
|
|
20
|
+
title = "Home"
|
|
21
|
+
url = "/" # relURL'd unless already absolute
|
|
22
|
+
text = "Everything, from the top."
|
|
23
|
+
|
|
24
|
+
Each is guarded: a consumer supplying none still builds, and simply gets the
|
|
25
|
+
generic wording with no list of routes back.
|
|
26
|
+
*/ -}}
|
|
27
|
+
{{ define "main" }}
|
|
28
|
+
{{- $nf := .Site.Params.notfound | default dict -}}
|
|
29
|
+
{{- $img := $nf.heroimage | default "images/banners/default.webp" -}}
|
|
30
|
+
{{- $heroURL := partial "cdn-url.html" (dict "site" .Site "path" $img) -}}
|
|
31
|
+
{{- $tagline := $nf.tagline | default "This site has no page at" -}}
|
|
32
|
+
{{- $noun := $nf.sitenoun | default "site" -}}
|
|
33
|
+
<section class="hero hero-with-image" style="background-image: url('{{ $heroURL }}');">
|
|
34
|
+
<div class="hero-inner" data-pagefind-ignore>
|
|
35
|
+
<h1>Page not found</h1>
|
|
36
|
+
<p class="hero-tagline">{{ $tagline }} <code id="missing-path">the address you followed</code>.</p>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<article class="single">
|
|
41
|
+
<div class="single-content">
|
|
42
|
+
<div class="single-body">
|
|
43
|
+
<p>
|
|
44
|
+
The page may have been renamed, moved to another part of the
|
|
45
|
+
{{ $noun }}, or retired along with the concept it described. Links
|
|
46
|
+
from older discussions, issues, and search results can outlive the
|
|
47
|
+
pages they point at.
|
|
48
|
+
</p>
|
|
49
|
+
{{- with $nf.links }}
|
|
50
|
+
<p>Start again from one of these:</p>
|
|
51
|
+
<ul class="page-list">
|
|
52
|
+
{{- range . }}
|
|
53
|
+
{{- $url := .url -}}
|
|
54
|
+
{{- if not (or (hasPrefix $url "http://") (hasPrefix $url "https://") (hasPrefix $url "//")) -}}
|
|
55
|
+
{{- /* relURL only mounts a path under the site's base when it is
|
|
56
|
+
given relative: a leading slash means "from the origin", and
|
|
57
|
+
the base path is left off. Trim it, so a consumer writing the
|
|
58
|
+
obvious "/section/" gets a link inside its own site even when
|
|
59
|
+
that site is served under a prefix. */ -}}
|
|
60
|
+
{{- $url = strings.TrimPrefix "/" $url | relURL -}}
|
|
61
|
+
{{- end }}
|
|
62
|
+
<li class="page-list-item">
|
|
63
|
+
<a class="page-list-title" href="{{ $url }}">{{ .title }}</a>
|
|
64
|
+
{{- with .text }}
|
|
65
|
+
<span class="page-list-desc">{{ . }}</span>
|
|
66
|
+
{{- end }}
|
|
67
|
+
</li>
|
|
68
|
+
{{- end }}
|
|
69
|
+
</ul>
|
|
70
|
+
{{- end }}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</article>
|
|
74
|
+
|
|
75
|
+
{{/* Name the address that failed, so a reader can see what they followed and
|
|
76
|
+
quote it in a bug report. Static hosting cannot template it — Pages serves
|
|
77
|
+
this one file for every unmatched path — so it is filled in client-side,
|
|
78
|
+
and the markup above reads sensibly without scripting. */}}
|
|
79
|
+
<script>
|
|
80
|
+
(function () {
|
|
81
|
+
var el = document.getElementById("missing-path");
|
|
82
|
+
if (el) el.textContent = window.location.pathname;
|
|
83
|
+
})();
|
|
84
|
+
</script>
|
|
85
|
+
{{ end }}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{ if not .IsHome }}{{ .Title }} — {{ end }}{{ .Site.Title }}</title>
|
|
7
|
+
<meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
|
|
12
|
+
{{/* Icon glyphs. SoHL documentation writes icons as <i class="fa-solid …">
|
|
13
|
+
or <i class="ginf-…">, the same markup the Foundry client renders, so a
|
|
14
|
+
page reads identically in a journal and here. Font Awesome's FREE set is
|
|
15
|
+
enough — the system deliberately uses no Pro-only icons — and the
|
|
16
|
+
game-icons webfont is subset to just the glyphs the docs reference
|
|
17
|
+
(~3 KB), generated by the SoHL repo. */}}
|
|
18
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
19
|
+
<link rel="stylesheet" href="{{ "css/sohl-icons.css" | relURL }}">
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
{{ partial "header.html" . }}
|
|
23
|
+
<main>
|
|
24
|
+
{{ block "main" . }}{{ end }}
|
|
25
|
+
</main>
|
|
26
|
+
{{ partial "footer.html" . }}
|
|
27
|
+
<script>
|
|
28
|
+
// Auto-enhance <span data-copy="URL"> elements with a clipboard copy button
|
|
29
|
+
document.querySelectorAll('[data-copy]').forEach(function(el) {
|
|
30
|
+
var url = el.getAttribute('data-copy');
|
|
31
|
+
el.classList.add('exturl');
|
|
32
|
+
|
|
33
|
+
// Wrap existing text in a code element if not already
|
|
34
|
+
if (!el.querySelector('.exturl-text')) {
|
|
35
|
+
var code = document.createElement('code');
|
|
36
|
+
code.className = 'exturl-text';
|
|
37
|
+
code.textContent = el.textContent;
|
|
38
|
+
el.textContent = '';
|
|
39
|
+
el.appendChild(code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var btn = document.createElement('button');
|
|
43
|
+
btn.className = 'exturl-copy';
|
|
44
|
+
btn.title = 'Copy URL to clipboard';
|
|
45
|
+
btn.setAttribute('aria-label', 'Copy URL to clipboard');
|
|
46
|
+
btn.innerHTML =
|
|
47
|
+
'<svg class="exturl-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
48
|
+
'<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>' +
|
|
49
|
+
'<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>' +
|
|
50
|
+
'</svg>' +
|
|
51
|
+
'<svg class="exturl-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
|
|
52
|
+
'<polyline points="20 6 9 17 4 12"/>' +
|
|
53
|
+
'</svg>';
|
|
54
|
+
btn.addEventListener('click', function() {
|
|
55
|
+
navigator.clipboard.writeText(url).then(function() {
|
|
56
|
+
btn.classList.add('copied');
|
|
57
|
+
setTimeout(function() { btn.classList.remove('copied'); }, 1500);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
el.appendChild(btn);
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Default list layout: hero, the authored body when there is one,
|
|
3
|
+
and an auto-list of the section's members.
|
|
4
|
+
|
|
5
|
+
Minimal by design: a package-build consumer disables Hugo's `section`,
|
|
6
|
+
`taxonomy` and `term` kinds, so this layout never runs for one — the home
|
|
7
|
+
page and its pages are all it renders through `index.html` and
|
|
8
|
+
`_default/single.html`. A site that keeps those kinds enabled — one that
|
|
9
|
+
is not a package-build consumer, publishing a hand-curated section of its
|
|
10
|
+
own — still needs a landing for them, which is what this file is for.
|
|
11
|
+
|
|
12
|
+
Content rows render "Name + description", inline rather than through a
|
|
13
|
+
shared partial: this is the only caller left, and a description is emitted
|
|
14
|
+
only when present, so a page carrying none renders a plain title row.
|
|
15
|
+
*/ -}}
|
|
16
|
+
{{ define "main" }}
|
|
17
|
+
{{ partial "hero-banner.html" . }}
|
|
18
|
+
<article class="single">
|
|
19
|
+
<div class="single-content">
|
|
20
|
+
{{ if .Content }}
|
|
21
|
+
<div class="single-body" data-pagefind-body>
|
|
22
|
+
{{ .Content }}
|
|
23
|
+
</div>
|
|
24
|
+
{{ end }}
|
|
25
|
+
|
|
26
|
+
{{- /*
|
|
27
|
+
A curated landing is the best navigation — a rules section groups its
|
|
28
|
+
pages under editorial headings like "Key Concepts", which no hierarchy
|
|
29
|
+
can infer. But gating the auto-list on "has no body" meant authoring an
|
|
30
|
+
intro silently replaced the section's ONLY navigation, and hand-curated
|
|
31
|
+
lists drift: one guide reached 41 pages linking 1, and another had
|
|
32
|
+
quietly orphaned a page.
|
|
33
|
+
|
|
34
|
+
So the auto-list is a GAP-FILLER: a section with a body lists only the
|
|
35
|
+
pages that are not reachable from that body by following links. A landing
|
|
36
|
+
whose hierarchy covers everything shows nothing extra, and a page can
|
|
37
|
+
never become unreachable — a newly added page appears under "Orphaned
|
|
38
|
+
Pages" until someone files it into the curated index.
|
|
39
|
+
|
|
40
|
+
Taxonomy and term pages keep their own listing shapes below.
|
|
41
|
+
*/ -}}
|
|
42
|
+
{{- $showAutoList :=
|
|
43
|
+
or (eq .Kind "taxonomy")
|
|
44
|
+
(eq .Kind "term")
|
|
45
|
+
(eq .Kind "section")
|
|
46
|
+
-}}
|
|
47
|
+
{{- $members := .Pages -}}
|
|
48
|
+
{{- /*
|
|
49
|
+
Pages reachable from the authored body, matched on RelPermalink so the
|
|
50
|
+
comparison survives aliases and wikilink resolution.
|
|
51
|
+
|
|
52
|
+
The needle deliberately stops after the URL rather than closing the quote,
|
|
53
|
+
so a link that carries a fragment counts. A rules landing reaches its
|
|
54
|
+
Characters chapter as `…/characters-introduction/#body-structure`, and
|
|
55
|
+
demanding the closing quote made that chapter read as unlinked. A trailing
|
|
56
|
+
slash ends every RelPermalink, so the open-ended match cannot bleed into a
|
|
57
|
+
sibling (`/rules/fate/` never prefixes `/rules/fate-points/`).
|
|
58
|
+
|
|
59
|
+
Reachability is TRANSITIVE, because a curated corpus is a hierarchy rather
|
|
60
|
+
than a flat index: a rules landing links ten chapter introductions, and
|
|
61
|
+
each of those links its own pages — up to four hops deep (root → Trauma →
|
|
62
|
+
Body → Injury → Bleeding). Testing only what the landing links DIRECTLY
|
|
63
|
+
counted every page below the first hop as a leftover. So a page counts as
|
|
64
|
+
listed when the body links it, or when any page already counted as listed
|
|
65
|
+
links it.
|
|
66
|
+
|
|
67
|
+
The walk runs to a fixed point: each pass appends the newly reached pages'
|
|
68
|
+
content to the haystack, and the first pass that reaches nothing new stops
|
|
69
|
+
it. Twelve passes is far past the depth of any corpus here (the rules need
|
|
70
|
+
four), and the `$growing` guard makes the surplus passes free.
|
|
71
|
+
*/ -}}
|
|
72
|
+
{{- $unlisted := $members -}}
|
|
73
|
+
{{- if and (eq .Kind "section") .Content -}}
|
|
74
|
+
{{- $haystack := .Content -}}
|
|
75
|
+
{{- $reached := slice -}}
|
|
76
|
+
{{- $growing := true -}}
|
|
77
|
+
{{- range seq 12 -}}
|
|
78
|
+
{{- if $growing -}}
|
|
79
|
+
{{- $growing = false -}}
|
|
80
|
+
{{- range $members -}}
|
|
81
|
+
{{- if not (in $reached .RelPermalink) -}}
|
|
82
|
+
{{- if in $haystack (printf "href=\"%s" .RelPermalink) -}}
|
|
83
|
+
{{- $reached = $reached | append .RelPermalink -}}
|
|
84
|
+
{{- $haystack = print $haystack .Content -}}
|
|
85
|
+
{{- $growing = true -}}
|
|
86
|
+
{{- end -}}
|
|
87
|
+
{{- end -}}
|
|
88
|
+
{{- end -}}
|
|
89
|
+
{{- end -}}
|
|
90
|
+
{{- end -}}
|
|
91
|
+
{{- $unlisted = where $members "RelPermalink" "not in" $reached -}}
|
|
92
|
+
{{- end -}}
|
|
93
|
+
{{- /*
|
|
94
|
+
Label the leftovers only when a curated body precedes them. With
|
|
95
|
+
reachability transitive, anything still here is genuinely unreachable
|
|
96
|
+
by reading — an orphan — so the heading says so rather than claiming to be
|
|
97
|
+
the section's full contents.
|
|
98
|
+
*/ -}}
|
|
99
|
+
{{- $labelList := and (eq .Kind "section") .Content -}}
|
|
100
|
+
{{- if $showAutoList -}}
|
|
101
|
+
{{- with $unlisted -}}
|
|
102
|
+
{{- if eq $.Kind "taxonomy" -}}
|
|
103
|
+
<ul class="page-list">
|
|
104
|
+
{{- range . }}
|
|
105
|
+
<li class="page-list-item">
|
|
106
|
+
<a class="page-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
107
|
+
<span class="page-list-count">{{ len .Pages }}</span>
|
|
108
|
+
</li>
|
|
109
|
+
{{- end }}
|
|
110
|
+
</ul>
|
|
111
|
+
{{- else -}}
|
|
112
|
+
{{- if $labelList }}
|
|
113
|
+
<h2 class="page-list-heading">Orphaned Pages</h2>
|
|
114
|
+
{{- end }}
|
|
115
|
+
<ul class="page-list">
|
|
116
|
+
{{- range . }}
|
|
117
|
+
<li class="page-list-item">
|
|
118
|
+
<a class="page-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
119
|
+
{{- with .Description }}
|
|
120
|
+
<span class="page-list-desc">{{ . }}</span>
|
|
121
|
+
{{- end }}
|
|
122
|
+
</li>
|
|
123
|
+
{{- end }}
|
|
124
|
+
</ul>
|
|
125
|
+
{{- end -}}
|
|
126
|
+
{{- else -}}
|
|
127
|
+
{{- /*
|
|
128
|
+
"Nothing here yet" is the right answer for an empty auto-listed section,
|
|
129
|
+
and the wrong one for a curated landing — there a leftover list of zero
|
|
130
|
+
means the hierarchy covers everything, so nothing should be said at all.
|
|
131
|
+
*/ -}}
|
|
132
|
+
{{- if not $labelList -}}
|
|
133
|
+
<p class="page-list-empty">Nothing here yet.</p>
|
|
134
|
+
{{- end -}}
|
|
135
|
+
{{- end -}}
|
|
136
|
+
{{- end }}
|
|
137
|
+
</div>
|
|
138
|
+
</article>
|
|
139
|
+
{{ end }}
|