@heroiclands/package-build 22.23.0 → 22.25.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 +22 -0
- package/README.md +1 -0
- package/assets/styles/content-base.css +45 -0
- package/ci/check-banners.mjs +183 -0
- package/ci/ci-steps.mjs +21 -11
- package/docs/authoring/assets.md +15 -0
- package/docs/authoring/links-and-markup.md +69 -2
- package/docs/commands.md +18 -19
- package/docs/configuration.md +2 -6
- package/docs/content-format.md +9 -0
- 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 +3 -3
- package/docs/reference/theme-pages.md +496 -0
- package/engine/art-slots.mjs +2 -1
- package/engine/content-blocks.mjs +83 -14
- package/engine/content-figures.mjs +18 -3
- package/engine/content-poetry.mjs +61 -0
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/helpers.mjs +5 -3
- package/engine/note-vocabulary.mjs +2 -1
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-render.mjs +32 -2
- package/engine/rank-ladder.mjs +32 -0
- package/engine/site-config.mjs +19 -24
- 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 +2982 -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/package.json +5 -2
- package/types/engine/art-slots.d.mts +2 -1
- package/types/engine/content-figures.d.mts +1 -0
- package/types/engine/content-poetry.d.mts +19 -0
- package/types/engine/helpers.d.mts +5 -3
- package/types/engine/rank-ladder.d.mts +8 -0
- package/types/engine/site-config.d.mts +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.25.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
Write poems in a `:::poetry` fence to preserve verse lines, relative indentation, and stanza breaks across journals, websites, and books. A poem placed first in a `:::figure` uses the Poem counter and may carry a caption and reference.
|
|
8
|
+
|
|
9
|
+
The bundled Hugo theme's image-role and banner guards now run as part of package-build lint. Affiliation notes must declare a `data.governance.ranks` rung at level 1; a missing ordinary standing now produces a located error.
|
|
10
|
+
|
|
11
|
+
## 22.24.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
**Hero banners** — A page whose note names its own hero image shows that image, in place of the stock banner for its kind; a note naming none gets a band with no image.
|
|
16
|
+
|
|
17
|
+
**Figures** — A numbered figure, table, listing or map reads as one block, its number and caption set beneath it rather than running on as body text, and a passage set apart as an aside draws as a box.
|
|
18
|
+
|
|
19
|
+
**Pictures** — A portrait or an emblem draws at half the content column and a banner, plate or map at its full width, never wider than the size the picture was made at; a stated width still overrides the role's measure.
|
|
20
|
+
|
|
21
|
+
**Installing** — The shared Hugo theme arrives with this package, so a site declares this package alone and needs no separate theme dependency.
|
|
22
|
+
|
|
23
|
+
**Hero images** — The authoring guide states the three states of a note's hero image: a picture, a deliberate blank that declines one, and an absent key that takes the default for the note's type.
|
|
24
|
+
|
|
3
25
|
## 22.23.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ The documentation lives in the Markdown files in this repository. Start with [th
|
|
|
33
33
|
| Detailed format rules and examples | [Format details](docs/reference/format-details.md) |
|
|
34
34
|
| Compendium routing and document builds | [Packs](docs/guides/packs.md) |
|
|
35
35
|
| Website build and publication | [Site](docs/guides/site.md) |
|
|
36
|
+
| What the theme draws on a page | [Theme pages](docs/reference/theme-pages.md) |
|
|
36
37
|
| PDF book build | [Book](docs/guides/book.md) |
|
|
37
38
|
| Icon generation and use | [Icons](docs/guides/icons.md) |
|
|
38
39
|
| Reference pages and content indexes | [Reference pages](docs/guides/reference-pages.md) |
|
|
@@ -106,6 +106,51 @@
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/* Each verse line breaks in the markup; each paragraph is a stanza. */
|
|
110
|
+
:is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry {
|
|
111
|
+
margin: 1.5rem 0;
|
|
112
|
+
line-height: 1.7;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry p {
|
|
116
|
+
margin: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry p + p {
|
|
120
|
+
margin-top: 1em;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.poetry .poetry-line {
|
|
124
|
+
display: inline-block;
|
|
125
|
+
margin-inline-start: var(--poetry-indent, 0em);
|
|
126
|
+
white-space: pre-wrap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.poetry .poetry-line.i1 {
|
|
130
|
+
--poetry-indent: 1.25em;
|
|
131
|
+
}
|
|
132
|
+
.poetry .poetry-line.i2 {
|
|
133
|
+
--poetry-indent: 2.5em;
|
|
134
|
+
}
|
|
135
|
+
.poetry .poetry-line.i3 {
|
|
136
|
+
--poetry-indent: 3.75em;
|
|
137
|
+
}
|
|
138
|
+
.poetry .poetry-line.i4 {
|
|
139
|
+
--poetry-indent: 5em;
|
|
140
|
+
}
|
|
141
|
+
.poetry .poetry-line.i5 {
|
|
142
|
+
--poetry-indent: 6.25em;
|
|
143
|
+
}
|
|
144
|
+
.poetry .poetry-line.i6 {
|
|
145
|
+
--poetry-indent: 7.5em;
|
|
146
|
+
}
|
|
147
|
+
.poetry .poetry-line.i7 {
|
|
148
|
+
--poetry-indent: 8.75em;
|
|
149
|
+
}
|
|
150
|
+
.poetry .poetry-line.i8 {
|
|
151
|
+
--poetry-indent: 10em;
|
|
152
|
+
}
|
|
153
|
+
|
|
109
154
|
/*
|
|
110
155
|
* Named body blocks. `:::info` and `:::warn` emit a `<section>` carrying the
|
|
111
156
|
* block's name as its class, so the appearance belongs here rather than in an
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Heroic Lands shared Hugo theme.
|
|
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 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
|
+
* CI guard: every banner `data/banners.yaml` declares must actually be published.
|
|
16
|
+
*
|
|
17
|
+
* `partials/hero-banner.html` needs to know which banner images exist, and Hugo
|
|
18
|
+
* cannot ask a remote asset host that question at build time — so the answer is
|
|
19
|
+
* declared in `data/banners.yaml` instead. A declaration nothing
|
|
20
|
+
* checks is kept in step by hope, and the hole it was written to close reopens
|
|
21
|
+
* the first time it drifts. It already had: when this check was written, four
|
|
22
|
+
* names present in a local working copy of the artwork repository were not
|
|
23
|
+
* published, and three that were published appeared in no local copy at all.
|
|
24
|
+
*
|
|
25
|
+
* So this fetches every declared name and fails on any non-200. It is the test
|
|
26
|
+
* the template cannot make, moved to the one place that can make it.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* node utils/check-banners.mjs [--base <url>] [--concurrency <n>]
|
|
30
|
+
*
|
|
31
|
+
* The host defaults to the Heroic Lands CDN and can be pointed anywhere with
|
|
32
|
+
* `--base` or `BANNER_BASE_URL`, so a consumer publishing its own artwork set
|
|
33
|
+
* can run the same check against its own `data/banners.yaml`.
|
|
34
|
+
*
|
|
35
|
+
* A network failure is reported as a failure rather than skipped: a check that
|
|
36
|
+
* passes when it could not reach the host is worse than no check at all.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { dirname, resolve } from "node:path";
|
|
42
|
+
import { parse } from "yaml";
|
|
43
|
+
|
|
44
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const DATA_FILE = resolve(HERE, "..", "hugo-theme", "data", "banners.yaml");
|
|
46
|
+
const DEFAULT_BASE = "https://cdn.heroiclands.org";
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const opts = {
|
|
50
|
+
base: process.env.BANNER_BASE_URL || DEFAULT_BASE,
|
|
51
|
+
concurrency: 8,
|
|
52
|
+
};
|
|
53
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
54
|
+
const arg = argv[i];
|
|
55
|
+
if (arg === "--base") opts.base = argv[(i += 1)];
|
|
56
|
+
else if (arg === "--concurrency") opts.concurrency = Number(argv[(i += 1)]);
|
|
57
|
+
else {
|
|
58
|
+
console.error(`check-banners: unrecognised argument ${arg}`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!opts.base) {
|
|
63
|
+
console.error("check-banners: no asset host; pass --base or set BANNER_BASE_URL");
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isInteger(opts.concurrency) || opts.concurrency < 1) {
|
|
67
|
+
console.error("check-banners: --concurrency must be a positive integer");
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
return opts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Diagnostics carry the fields they can know and drop the ones they cannot. */
|
|
74
|
+
function diag(line, message) {
|
|
75
|
+
const where = line ? `${DATA_FILE}:${line}` : DATA_FILE;
|
|
76
|
+
console.error(`${where}: error: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fail(message, line) {
|
|
80
|
+
diag(line, message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Line number of a declared name, so an editor can jump straight to it. */
|
|
85
|
+
function lineOf(source, name) {
|
|
86
|
+
const index = source
|
|
87
|
+
.split("\n")
|
|
88
|
+
.findIndex((l) => l.trim() === `- ${name}` || l.trim() === `- "${name}"`);
|
|
89
|
+
return index === -1 ? 0 : index + 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Read and validate the inventory, so a malformed file fails loudly. */
|
|
93
|
+
function readInventory() {
|
|
94
|
+
const source = readFileSync(DATA_FILE, "utf8");
|
|
95
|
+
const data = parse(source);
|
|
96
|
+
const dir = data?.dir;
|
|
97
|
+
const fallback = data?.fallback;
|
|
98
|
+
const available = data?.available;
|
|
99
|
+
|
|
100
|
+
if (typeof dir !== "string" || !dir) fail("`dir` must be a non-empty string");
|
|
101
|
+
if (typeof fallback !== "string" || !fallback) fail("`fallback` must be a non-empty string");
|
|
102
|
+
if (!Array.isArray(available) || available.length === 0)
|
|
103
|
+
fail("`available` must be a non-empty list");
|
|
104
|
+
if (available.some((n) => typeof n !== "string" || !n))
|
|
105
|
+
fail("`available` must hold only non-empty strings");
|
|
106
|
+
|
|
107
|
+
const dupes = [...new Set(available.filter((n, i) => available.indexOf(n) !== i))];
|
|
108
|
+
if (dupes.length) fail(`duplicate names: ${dupes.join(", ")}`, lineOf(source, dupes[0]));
|
|
109
|
+
|
|
110
|
+
const unsorted = available.findIndex((n, i) => i > 0 && available[i - 1] > n);
|
|
111
|
+
if (unsorted !== -1)
|
|
112
|
+
fail(
|
|
113
|
+
"`available` must be sorted, so a diff shows what changed",
|
|
114
|
+
lineOf(source, available[unsorted]),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// The fallback is what every unlisted name resolves to, so its absence would
|
|
118
|
+
// turn one dead URL into every dead URL.
|
|
119
|
+
const fallbackName = fallback.replace(/\.[^./]+$/, "");
|
|
120
|
+
if (!available.includes(fallbackName))
|
|
121
|
+
fail(`the fallback ${fallback} is not itself listed in \`available\``);
|
|
122
|
+
|
|
123
|
+
return { source, dir, available };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function probe(url) {
|
|
127
|
+
try {
|
|
128
|
+
// HEAD first — it is what the question actually is. Some object stores
|
|
129
|
+
// answer 405 to it, so fall back to a ranged GET rather than reporting a
|
|
130
|
+
// published file as missing.
|
|
131
|
+
let res = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
132
|
+
if (res.status === 405 || res.status === 501) {
|
|
133
|
+
res = await fetch(url, {
|
|
134
|
+
headers: { Range: "bytes=0-0" },
|
|
135
|
+
redirect: "follow",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
ok: res.status === 200 || res.status === 206,
|
|
140
|
+
status: String(res.status),
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return { ok: false, status: `network error: ${err.message}` };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function main() {
|
|
148
|
+
const { base, concurrency } = parseArgs(process.argv.slice(2));
|
|
149
|
+
const { source, dir, available } = readInventory();
|
|
150
|
+
const root = `${base.replace(/\/$/, "")}/${dir.replace(/^\//, "")}`;
|
|
151
|
+
|
|
152
|
+
const failures = [];
|
|
153
|
+
const queue = [...available];
|
|
154
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
155
|
+
for (let name = queue.shift(); name !== undefined; name = queue.shift()) {
|
|
156
|
+
const url = `${root}/${name}.webp`;
|
|
157
|
+
const { ok, status } = await probe(url);
|
|
158
|
+
if (!ok) failures.push({ name, url, status });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
await Promise.all(workers);
|
|
162
|
+
|
|
163
|
+
if (failures.length) {
|
|
164
|
+
failures.sort((a, b) => a.name.localeCompare(b.name));
|
|
165
|
+
for (const { name, url, status } of failures) {
|
|
166
|
+
diag(
|
|
167
|
+
lineOf(source, name),
|
|
168
|
+
`declared banner ${name} is not published (${status}): ${url}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
console.error(
|
|
172
|
+
`check-banners: ${failures.length} of ${available.length} declared banners are missing from ${base}.`,
|
|
173
|
+
);
|
|
174
|
+
console.error(
|
|
175
|
+
"Publish the artwork, or drop the name — an undrawn name here is a 404 on every page that resolves to it.",
|
|
176
|
+
);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`check-banners: all ${available.length} declared banners resolve at ${base}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await main();
|
package/ci/ci-steps.mjs
CHANGED
|
@@ -139,7 +139,7 @@ function prTriggeredWorkflows() {
|
|
|
139
139
|
* @param {string} file - The workflow.
|
|
140
140
|
* @returns {{run: Array<{name: string, run: string}>, skipped: string[]}} Its steps.
|
|
141
141
|
*/
|
|
142
|
-
function stepsIn(file) {
|
|
142
|
+
export function stepsIn(file) {
|
|
143
143
|
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
144
144
|
const run = [];
|
|
145
145
|
const skipped = [];
|
|
@@ -164,25 +164,32 @@ function stepsIn(file) {
|
|
|
164
164
|
continue;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
const inline = /^-?\s*run:\s*(?!\|)(.+)$/.exec(text);
|
|
168
|
-
if (inline) {
|
|
169
|
-
run.push({ name: name ?? inline[1], run: inline[1].trim() });
|
|
170
|
-
name = null;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
167
|
// A `run: |` block scalar: every following line indented deeper.
|
|
174
|
-
if (/^-?\s*run:\s
|
|
168
|
+
if (/^-?\s*run:\s*\|[+-]?\s*$/.test(text)) {
|
|
175
169
|
const base = indentOf(line);
|
|
176
170
|
const body = [];
|
|
177
171
|
while (i + 1 < lines.length) {
|
|
178
172
|
const next = lines[i + 1];
|
|
179
173
|
if (next.trim() !== "" && indentOf(next) <= base) break;
|
|
180
|
-
body.push(next
|
|
174
|
+
body.push(next);
|
|
181
175
|
i += 1;
|
|
182
176
|
}
|
|
183
|
-
const
|
|
177
|
+
const margin = Math.min(
|
|
178
|
+
...body.filter((line) => line.trim()).map((line) => indentOf(line)),
|
|
179
|
+
);
|
|
180
|
+
const command = body
|
|
181
|
+
.map((line) => line.slice(margin))
|
|
182
|
+
.join("\n")
|
|
183
|
+
.trimEnd();
|
|
184
184
|
if (command) run.push({ name: name ?? command, run: command });
|
|
185
185
|
name = null;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const inline = /^-?\s*run:\s*(.+)$/.exec(text);
|
|
189
|
+
if (inline) {
|
|
190
|
+
run.push({ name: name ?? inline[1], run: inline[1].trim() });
|
|
191
|
+
name = null;
|
|
192
|
+
continue;
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
return { run, skipped };
|
|
@@ -223,7 +230,10 @@ function main() {
|
|
|
223
230
|
|
|
224
231
|
for (const [i, step] of run.entries()) {
|
|
225
232
|
console.log(`\nci-steps: [${i + 1}/${run.length}] ${step.name}\n $ ${step.run}`);
|
|
226
|
-
const result = spawnSync(
|
|
233
|
+
const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", step.run], {
|
|
234
|
+
cwd: ROOT,
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
});
|
|
227
237
|
if (result.status !== 0) {
|
|
228
238
|
console.error(
|
|
229
239
|
`\nci-steps: FAILED at "${step.name}" — this is what GitHub would report.\n` +
|
package/docs/authoring/assets.md
CHANGED
|
@@ -38,6 +38,21 @@ role: portrait
|
|
|
38
38
|
|
|
39
39
|
**A hero image is 1792×768.** `data.banner` fills one fixed strip wherever it is drawn, so every banner is cut to that size and a picture of any other size is a finding naming both sizes — resize the picture to match. That is the one size any art field requires. A map's `bgImage` sets its own scene's dimensions, so that asks nothing of a picture's dimensions either.
|
|
40
40
|
|
|
41
|
+
`data.banner` reads in one of three states:
|
|
42
|
+
|
|
43
|
+
| A note writes | The page draws |
|
|
44
|
+
| ------------------ | -------------------------------------------------- |
|
|
45
|
+
| an asset Address | that picture |
|
|
46
|
+
| a blank | no picture, on purpose, and no default replaces it |
|
|
47
|
+
| nothing, or `null` | a default picture chosen by the note's `type` |
|
|
48
|
+
|
|
49
|
+
Omitting the key does not mean "no hero image" — it means artwork chosen for
|
|
50
|
+
the author, which inverts the ordinary reading of an absent key. **A note
|
|
51
|
+
declines with a blank, not with `none`.** `none` parses as an ordinary asset
|
|
52
|
+
Address — a shortcode, not a keyword — and since nothing in the corpus
|
|
53
|
+
answers to it, the build drops the key outright and the page falls through to
|
|
54
|
+
its type's default: the opposite of a decline.
|
|
55
|
+
|
|
41
56
|
`role` is one of `portrait`, `emblem`, `banner`, `plate` or `map`, and belongs to the `image` type only — declaring it in a `provenance.yaml` under `assets/icons` is a finding, since an icon carries one nominal size per medium whatever the file holds. Leave the key out for an ordinary picture; there is no default role name, only its absence.
|
|
42
57
|
|
|
43
58
|
**A role decides how large a picture written in prose draws, when nothing else states a size.** Each medium maps a role to a maximum, and the drawn size is the smaller of that slot and what the file's own pixels support — see [what a role draws at](../reference/format-details.md#what-a-role-draws-at). A named `size=` beside the embed still overrides it outright; an icon draws at the medium's nominal icon size whatever role it might otherwise have had, because `role` never reaches an icon address at all.
|
|
@@ -93,7 +93,7 @@ The fence displays **Table 1: Regional trade routes**, and `{{ref "#trade"}}` di
|
|
|
93
93
|
|
|
94
94
|
**`.border` draws the figure and its label inside a border.** It is the only class the construct accepts — any other class, such as `.wide`, is an error naming the class. `key="value"` is refused outright: a figure takes an id and classes, nothing else.
|
|
95
95
|
|
|
96
|
-
**The counter is derived from the fence's contents, never authored.** A table is a table; an `sql` fence is a table, because its rendered output is one; any other fence or code block is code; pictures alone, one or several, are a figure; everything else is prose. A fence around prose with `.border` is a numbered, referable boxed aside — the `Prose` counter's whole purpose. The counters are `Code`, `Table`, `Figure`, `Map`, and `Prose`. The website and a Foundry journal number each counter within the note; a book numbers each counter across the book in reading order.
|
|
96
|
+
**The counter is derived from the fence's contents, never authored.** A table is a table; an `sql` fence is a table, because its rendered output is one; any other fence or code block is code; pictures alone, one or several, are a figure; a `:::poetry` fence written first makes it a poem; everything else is prose. A fence around prose with `.border` is a numbered, referable boxed aside — the `Prose` counter's whole purpose. The counters are `Code`, `Table`, `Figure`, `Map`, `Poem`, and `Prose`. The website and a Foundry journal number each counter within the note; a book numbers each counter across the book in reading order.
|
|
97
97
|
|
|
98
98
|
**A picture counts as `Map` instead of `Figure` when the asset it draws declares `role: map`** in its provenance — see [the asset provenance reference](../reference/format-details.md). The fence's own markup cannot say so; a picture of a map looks like any other picture, so the fact rides entirely on the asset's own record. A fence grouping several pictures counts as `Map` only when every one of them declares the role; one picture without it keeps the whole fence a `Figure`.
|
|
99
99
|
|
|
@@ -172,7 +172,7 @@ The bridge closes during the spring flood.
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
Each heads itself with its own name — **Secret**, **Info**, **Warn** — and takes
|
|
175
|
-
a closing `:::`.
|
|
175
|
+
a closing `:::`. Poetry has its own fence below; other names are reported.
|
|
176
176
|
|
|
177
177
|
**A GM-only section holds a box.** A `:::secret` is a container for whatever the
|
|
178
178
|
GM reads, boxes included, and a box written inside one stays inside it. A box
|
|
@@ -232,6 +232,73 @@ would tear the block or the figure in two, and a reader could open the second
|
|
|
232
232
|
half with none of the box around it. A lower heading with no anchor is
|
|
233
233
|
unaffected and is the ordinary way to structure a long box.
|
|
234
234
|
|
|
235
|
+
### Poetry
|
|
236
|
+
|
|
237
|
+
Use `:::poetry` to preserve verse lines and stanzas. Every nonblank source line
|
|
238
|
+
is one verse line; blank lines separate stanzas. Inline emphasis, links and
|
|
239
|
+
footnotes work as they do in prose. The fence itself adds no title, caption or
|
|
240
|
+
number.
|
|
241
|
+
|
|
242
|
+
Leading spaces indent individual verse lines. The least-indented nonblank line
|
|
243
|
+
sets level zero for the whole poem, so a common indentation on every line has no
|
|
244
|
+
visual effect. Each additional two spaces adds one level, up to `i8`; an odd
|
|
245
|
+
extra space rounds down. Each medium sets the actual width of a level, with
|
|
246
|
+
every higher level wider than the preceding one. Tabs anywhere in the poem are
|
|
247
|
+
an error. Interior spaces are preserved on a best-effort basis.
|
|
248
|
+
|
|
249
|
+
| Extra leading spaces | Indent class |
|
|
250
|
+
| -------------------- | ------------ |
|
|
251
|
+
| 0–1 | none |
|
|
252
|
+
| 2–3 | `i1` |
|
|
253
|
+
| 4–5 | `i2` |
|
|
254
|
+
| 6–7 | `i3` |
|
|
255
|
+
| 8–9 | `i4` |
|
|
256
|
+
| 10–11 | `i5` |
|
|
257
|
+
| 12–13 | `i6` |
|
|
258
|
+
| 14–15 | `i7` |
|
|
259
|
+
| 16 or more | `i8` |
|
|
260
|
+
|
|
261
|
+
For example, a line with four leading spaces and a line with six leading spaces
|
|
262
|
+
render at levels zero and `i1` respectively. This also works when the entire
|
|
263
|
+
fence is indented inside a list item. Poetry is upright by default; use normal
|
|
264
|
+
Markdown emphasis when italics are part of the poem.
|
|
265
|
+
|
|
266
|
+
```markdown
|
|
267
|
+
:::poetry {form=ballad meter="common meter" rhyme=ABCB syllables="8,6,8,6" lang=en}
|
|
268
|
+
The lantern burns beside the gate,
|
|
269
|
+
The harbor sleeps below.
|
|
270
|
+
|
|
271
|
+
The keeper guards the road till dawn,
|
|
272
|
+
And keeps a light aglow.
|
|
273
|
+
:::
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The optional `form`, `meter` and `rhyme` attributes record the author's intent;
|
|
277
|
+
the build does not infer stress, rhyme or pronunciation. `syllables` gives a
|
|
278
|
+
positive count for each verse line in order, separated by commas, with blank
|
|
279
|
+
stanza lines omitted. The build checks the number of counts, not the syllables
|
|
280
|
+
spoken. `lang` is a language tag for the rendered HTML. An optional `#id`
|
|
281
|
+
addresses the poem itself. There are no `title` or `lines` attributes, and no
|
|
282
|
+
special punctuation for stress, pauses or refrains.
|
|
283
|
+
|
|
284
|
+
To number and caption a poem, put it first inside a `:::figure`:
|
|
285
|
+
|
|
286
|
+
```markdown
|
|
287
|
+
:::figure {#watch-song}
|
|
288
|
+
:::poetry {form=ballad}
|
|
289
|
+
The watch has gone to sea,
|
|
290
|
+
The lantern marks the shore.
|
|
291
|
+
:::
|
|
292
|
+
///
|
|
293
|
+
A song sung by the harbor watch.
|
|
294
|
+
:::
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The inner `:::` closes the poem; the outer one closes the figure. That figure
|
|
298
|
+
uses the **Poem** counter, so `{{ref "#watch-song"}}` reads **Poem 1**. The
|
|
299
|
+
caption after `///` belongs to the figure. A poem outside a figure is
|
|
300
|
+
unnumbered. Prose written before the poem makes the figure a **Prose** figure.
|
|
301
|
+
|
|
235
302
|
## Footnotes and definition lists
|
|
236
303
|
|
|
237
304
|
Write a footnote reference as `[^id]` and its definition as `[^id]: text`.
|
package/docs/commands.md
CHANGED
|
@@ -885,8 +885,8 @@ Install it with `npm ci`, which resolves from the lockfile this just moved.
|
|
|
885
885
|
```
|
|
886
886
|
|
|
887
887
|
```
|
|
888
|
-
$ package-build bump @heroiclands/
|
|
889
|
-
@heroiclands/
|
|
888
|
+
$ package-build bump @heroiclands/package-build --check
|
|
889
|
+
@heroiclands/package-build 22.6.0 → 22.7.0
|
|
890
890
|
|
|
891
891
|
Run without --check to take it.
|
|
892
892
|
```
|
|
@@ -2310,9 +2310,9 @@ Three things are written, and nothing outside `build/`:
|
|
|
2310
2310
|
- `build/hugo/hugo.toml`, generated on every run from `package.json`
|
|
2311
2311
|
(`homepage`, `description`, `author`), `package-build.config.yaml`
|
|
2312
2312
|
(`packageBuild.manifest.title`, `site.assets`, `site.notfound`,
|
|
2313
|
-
`site.hugo`), the organisation's constants, the
|
|
2314
|
-
|
|
2315
|
-
|
|
2313
|
+
`site.hugo`), the organisation's constants, the shared theme's own
|
|
2314
|
+
location, and the navigation `deps fetch` cached. Every value's source is
|
|
2315
|
+
listed under
|
|
2316
2316
|
[the generated Hugo configuration](configuration.md#the-generated-hugo-configuration).
|
|
2317
2317
|
- `build/hugo/content/`, the content mount — the homepage as its `_index.md`,
|
|
2318
2318
|
and the content tree's pages flat below `publish.address.prefix`. Wiped on
|
|
@@ -2321,13 +2321,13 @@ Three things are written, and nothing outside `build/`:
|
|
|
2321
2321
|
deployment root `package-build site-root` writes beside. Nothing Hugo
|
|
2322
2322
|
reads lands in what is published.
|
|
2323
2323
|
|
|
2324
|
-
The configuration's sources are read before the output tree is touched, so
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2324
|
+
The configuration's sources are read before the output tree is touched, so a
|
|
2325
|
+
missing `homepage` or a cold navigation cache fails with the previous site
|
|
2326
|
+
intact. Every gate is then checked and reported, and the run stops at the
|
|
2327
|
+
first that fires, ordered so the report names the cause rather than its
|
|
2328
|
+
symptoms — an unusable dependency manifest, reported after the links that
|
|
2329
|
+
failed because of it, would otherwise read as a pile of broken notes. Reads
|
|
2330
|
+
the content tree named by `paths.content`.
|
|
2331
2331
|
|
|
2332
2332
|
**OPTIONS**
|
|
2333
2333
|
|
|
@@ -2336,13 +2336,12 @@ None.
|
|
|
2336
2336
|
**EXIT STATUS**
|
|
2337
2337
|
|
|
2338
2338
|
1 if `package.json` declares no `homepage`, or one that does not end
|
|
2339
|
-
`/<contentPackage>/`; if `packageBuild.manifest.title` is undeclared; if
|
|
2340
|
-
navigation has not been fetched (`package-build deps fetch` fills the
|
|
2341
|
-
and is named in the message)
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
failed to expand, or a dead wikilink. Otherwise 0.
|
|
2339
|
+
`/<contentPackage>/`; if `packageBuild.manifest.title` is undeclared; or if
|
|
2340
|
+
the navigation has not been fetched (`package-build deps fetch` fills the
|
|
2341
|
+
cache and is named in the message). 1 if any gate fires — no homepage or two
|
|
2342
|
+
competing for it, a frontmatter wikilink, an address that cannot be derived,
|
|
2343
|
+
a stale or unaddressable dependency manifest, an address published twice, a
|
|
2344
|
+
table that failed to expand, or a dead wikilink. Otherwise 0.
|
|
2346
2345
|
|
|
2347
2346
|
**EXAMPLES**
|
|
2348
2347
|
|
package/docs/configuration.md
CHANGED
|
@@ -1031,8 +1031,8 @@ in it has one source, and that source is where it is edited:
|
|
|
1031
1031
|
| `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
|
|
1032
1032
|
| `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
|
|
1033
1033
|
| `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
|
|
1034
|
-
| `themesDir` |
|
|
1035
|
-
| `theme` | the
|
|
1034
|
+
| `themesDir` | this package's own root, which holds the theme it ships, written relative to `build/hugo/` |
|
|
1035
|
+
| `theme` | the theme's directory inside this package, `hugo-theme` |
|
|
1036
1036
|
| `disableKinds` | the toolchain, which renders a site as its homepage and its pages: `["section", "taxonomy", "term", "RSS"]` on every site, whatever its notes carry |
|
|
1037
1037
|
| `taxonomies` | the toolchain — never written, because `taxonomy` and `term` are disabled kinds |
|
|
1038
1038
|
| `outputs` | the toolchain — never written, because every listing kind is disabled |
|
|
@@ -1049,10 +1049,6 @@ error naming the command that fills it:
|
|
|
1049
1049
|
|
|
1050
1050
|
> `the site navigation has not been fetched. Run `package-build deps fetch` first.`
|
|
1051
1051
|
|
|
1052
|
-
A missing theme names the package to install:
|
|
1053
|
-
|
|
1054
|
-
> `@heroiclands/hugo-theme is not installed anywhere above <rootDir> — add it to `devDependencies`and run`npm ci``
|
|
1055
|
-
|
|
1056
1052
|
The site needs a title from `site.title` or the package manifest:
|
|
1057
1053
|
|
|
1058
1054
|
> ``package-build config: declare `site.title` or `packageBuild.manifest.title` for the site's title.``
|
package/docs/content-format.md
CHANGED
|
@@ -27,6 +27,8 @@ A nonempty note body creates one system-agnostic Foundry JournalEntry. Content b
|
|
|
27
27
|
|
|
28
28
|
A `:::figure {#anchor}` fence numbers whatever it holds — prose, code, a table, or an image. Its anchor makes the fence a target `{{ref "#anchor"}}` can point to, displaying the generated kind and number, such as **Figure 12**. Foundry gives the fence its own JournalEntryPage. See [figures and numbered references](authoring/links-and-markup.md#figures-and-numbered-references) for the syntax.
|
|
29
29
|
|
|
30
|
+
A `:::poetry` fence keeps each authored line as a verse line and blank lines as stanza breaks. Inside a `:::figure`, a poem written first gives that figure the **Poem** counter and an optional caption. See [poetry](authoring/links-and-markup.md#poetry) for attributes and examples.
|
|
31
|
+
|
|
30
32
|
Body extensions use braces with space-separated `key=value` attributes:
|
|
31
33
|
`![[icon-harbor|Harbor]]{float=top-left size=medium}` and an SQL fence with
|
|
32
34
|
`{allow-empty=true}` are examples. Inline font glyphs use the package's icon
|
|
@@ -49,6 +51,13 @@ explains routing for documents and their prose journals.
|
|
|
49
51
|
|
|
50
52
|
A note can also produce a system document: an Actor, Item, Scene, or Macro according to its type and system blocks. The `sohl:` and `hm3:` blocks supply game-specific mechanics and overrides; they do not create separate prose. The JournalEntry is shared between systems.
|
|
51
53
|
|
|
54
|
+
An `affiliation` note declares `data.governance.ranks` with at least one rung at
|
|
55
|
+
`level: 1`. Each rung states `level`, `title`, and `description`. A being's
|
|
56
|
+
`data.affiliations` entry names a rank on that body's ladder; level 1 is the
|
|
57
|
+
ordinary standing when no higher rank applies. A single level-1 rung is enough
|
|
58
|
+
when the body expresses its hierarchy through `governance.offices` instead.
|
|
59
|
+
Missing ranks, an empty list, or a ladder without level 1 are build errors.
|
|
60
|
+
|
|
52
61
|
| Authored section | SoHL Actor | HM3 Actor |
|
|
53
62
|
| ---------------- | ------------------- | -------------------- |
|
|
54
63
|
| `{#appearance}` | `system.appearance` | `system.description` |
|
|
@@ -863,8 +863,8 @@ None of them is needed to build a package.
|
|
|
863
863
|
**A website.** Every package publishes one — at the least, the homepage note
|
|
864
864
|
from step 5 — at `https://www.heroiclands.org/<contentPackage>/`. Add that
|
|
865
865
|
address to `package.json` as `homepage`, with a `description` and an `author`
|
|
866
|
-
beside it
|
|
867
|
-
|
|
866
|
+
beside it, and add a `packageBuild.manifest.title`. The theme the site renders
|
|
867
|
+
through ships with this package, so nothing else is installed. Then `package-build deps fetch` caches the
|
|
868
868
|
organisation's navigation, and `package-build site` writes the whole Hugo
|
|
869
869
|
source tree under `build/hugo/` — the configuration generated from those
|
|
870
870
|
values, and the content mount — for `hugo --source build/hugo` to render
|
package/docs/guides/site.md
CHANGED
|
@@ -132,6 +132,47 @@ fenced example remains literal. `site.passOptions` supplies that named pass's
|
|
|
132
132
|
options. This is a package-specific extension point; keep the general note
|
|
133
133
|
format and link behavior in the [content format](../content-format.md).
|
|
134
134
|
|
|
135
|
+
## What an emitted page carries
|
|
136
|
+
|
|
137
|
+
A theme reads the front matter `package-build site` writes: the note's own
|
|
138
|
+
front matter, plus the values the build derives. Which is which matters,
|
|
139
|
+
because an authored value under a derived key is discarded.
|
|
140
|
+
|
|
141
|
+
The build always writes `title`, `slug`, `url`, `kbfolder`, `package` and
|
|
142
|
+
`infoboxes`. `title` is the note's name, `url` is the page's address, and
|
|
143
|
+
`slug` is that address's last segment. `package` names the package shipping the
|
|
144
|
+
note. `infoboxes` is assembled from the note's own fields, so no note authors
|
|
145
|
+
it and nothing in the note format accepts it.
|
|
146
|
+
|
|
147
|
+
The build writes `resolvedDates`, `related`, `contains`, `held_by`, `holdings`
|
|
148
|
+
and `map` only when it has something to write. A page with no links either way
|
|
149
|
+
carries no `related`, and a place with no drawing carries no `map`.
|
|
150
|
+
|
|
151
|
+
An authored `aliases`, `related`, `contains`, `held_by`, `holdings` or `map` is
|
|
152
|
+
dropped. Each states a fact about the whole tree rather than about one note, so
|
|
153
|
+
the build replaces whatever a note wrote.
|
|
154
|
+
|
|
155
|
+
Artwork addresses are rewritten in place under `data`. A page's `data.icon`,
|
|
156
|
+
`data.bgImage` and `data.banner` carry the URLs the site serves rather than the
|
|
157
|
+
addresses the note authored, and there are four cases to tell apart:
|
|
158
|
+
|
|
159
|
+
| The note writes | The page carries |
|
|
160
|
+
| ---------------------------- | ---------------- |
|
|
161
|
+
| an address something answers | the resolved URL |
|
|
162
|
+
| an address nothing answers | no key at all |
|
|
163
|
+
| `null` | `null` |
|
|
164
|
+
| `""` | `""` |
|
|
165
|
+
|
|
166
|
+
An address nothing answers is deleted rather than written through, so an absent
|
|
167
|
+
key is the ordinary case for a theme to render nothing against, and no page
|
|
168
|
+
reaches a reader carrying a broken image source. The two empties are distinct
|
|
169
|
+
and both survive: `null` is a note naming no art, where a default may apply,
|
|
170
|
+
and `""` is a note refusing art, where no default may replace it.
|
|
171
|
+
|
|
172
|
+
The build writes these keys; [what the theme draws](../reference/theme-pages.md)
|
|
173
|
+
describes the panel each one feeds — the infobox, the hero band, the holdings
|
|
174
|
+
and related cards, the place map, and search.
|
|
175
|
+
|
|
135
176
|
## Build and serve
|
|
136
177
|
|
|
137
178
|
```bash
|
package/docs/project-setup.md
CHANGED
|
@@ -309,10 +309,10 @@ the rendered pages for search into `build/site/<contentPackage>/pagefind/`;
|
|
|
309
309
|
and `serve:site` does the first and then `hugo server` for a live preview.
|
|
310
310
|
The repository carries no Hugo configuration of its own: `hugo.toml` is
|
|
311
311
|
generated on every run from `package.json`, `package-build.config.yaml`, the
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
on the developer's `PATH` and the runner's.
|
|
312
|
+
shared theme that ships with `@heroiclands/package-build`, and the navigation
|
|
313
|
+
`deps fetch` cached — a site declares this one package and needs nothing
|
|
314
|
+
further added for its theme. Hugo itself is a separate install — the
|
|
315
|
+
extended edition, on the developer's `PATH` and the runner's.
|
|
316
316
|
|
|
317
317
|
The site build reads the cached navigation, so `build:site` in a package with
|
|
318
318
|
no other dependency still runs `deps fetch` first:
|