@heroiclands/package-build 22.20.0 → 22.22.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-content-build.md +0 -8
- package/CHANGELOG.md +204 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +26 -3
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +73 -4
- package/docs/api.md +46 -28
- package/docs/authoring/dates-and-calendars.md +4 -2
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +237 -28
- package/docs/configuration.md +27 -1
- package/docs/content-format.md +3 -1
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +265 -42
- package/docs/reference/note-types.md +45 -28
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +10 -20
- package/engine/base-compiler.mjs +86 -21
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +0 -1
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-captions.mjs +57 -8
- package/engine/content-footnotes.mjs +189 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-index.mjs +2 -2
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/frontmatter-lint.mjs +62 -66
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +308 -0
- package/engine/helpers.mjs +19 -13
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-render.mjs +21 -4
- package/engine/infobox.mjs +167 -7
- package/engine/journals.mjs +36 -17
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +178 -53
- package/engine/note-addresses.mjs +17 -8
- package/engine/note-dates.mjs +49 -0
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +216 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pdf-build.mjs +76 -42
- package/engine/pdf-render.mjs +422 -49
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/reference-targets.mjs +13 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +10 -3
- package/engine/site-build.mjs +41 -12
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +39 -4
- package/engine/site-preview.mjs +18 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standing-terms.mjs +19 -0
- package/engine/standings.mjs +538 -0
- package/engine/svg-theme.mjs +15 -4
- package/engine/system-block.mjs +12 -14
- package/engine/web-wikilinks.mjs +48 -1
- package/engine/wikilinks.mjs +3 -1
- package/hm3/actors.mjs +6 -3
- package/manifest.mjs +13 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +82 -103
- package/sohl/item-fields.mjs +4 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/base-compiler.d.mts +19 -0
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-captions.d.mts +14 -1
- package/types/engine/content-footnotes.d.mts +73 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +174 -0
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/journals.d.mts +14 -7
- package/types/engine/markdown-expressions.d.mts +23 -0
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +98 -8
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pdf-render.d.mts +72 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +13 -1
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +13 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standing-terms.d.mts +16 -0
- package/types/engine/standings.d.mts +147 -0
- package/types/engine/system-block.d.mts +11 -12
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/engine/wikilinks.d.mts +2 -1
- package/types/hm3/actors.d.mts +3 -3
- package/types/sohl/actors.d.mts +25 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-secrets.d.mts +0 -17
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
* What a **rung** of an affiliation's rank ladder has to state.
|
|
16
|
+
*
|
|
17
|
+
* A rung is a map of `level`, `title` and `description`, all three required,
|
|
18
|
+
* plus an optional `lore` Address for a standing that needs more said about it
|
|
19
|
+
* than a description can hold. Those three keys together are a complete
|
|
20
|
+
* statement of a rung; nothing further is needed for a ladder to be correct.
|
|
21
|
+
*
|
|
22
|
+
* **A level belongs to one rung.** The ladder is read by level, so two rungs
|
|
23
|
+
* claiming one level leave a member's `rank` answering to whichever is written
|
|
24
|
+
* first — a standing whose name changes when two lines swap places. A body that
|
|
25
|
+
* confers two titles at one standing writes them as one rung, or gives each its
|
|
26
|
+
* own level.
|
|
27
|
+
*
|
|
28
|
+
* **The severity is error**, because the ladder is what a being's `rank`
|
|
29
|
+
* indexes into: a rung missing its `title` makes a standing that resolves to
|
|
30
|
+
* nothing, and one missing its `description` ships a rung that says nothing on
|
|
31
|
+
* a sheet or a page.
|
|
32
|
+
*
|
|
33
|
+
* Two things are deliberately **not** checked here, because something else
|
|
34
|
+
* already does and a finding reported twice is a finding read once and fixed
|
|
35
|
+
* neither time:
|
|
36
|
+
*
|
|
37
|
+
* - **that `ranks` is a list at all** — the field declares `kind: "list"`, and
|
|
38
|
+
* the shape check names it;
|
|
39
|
+
* - **that `lore` resolves** — the reference checker reads every Address in the
|
|
40
|
+
* note, this one included.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
46
|
+
|
|
47
|
+
/** The keys a rung may carry. */
|
|
48
|
+
const RUNG_KEYS = Object.freeze(["level", "title", "description", "lore"]);
|
|
49
|
+
|
|
50
|
+
/** The keys a rung must carry a value for. */
|
|
51
|
+
const REQUIRED_TEXT = Object.freeze(["title", "description"]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a value is a YAML mapping rather than a list or a scalar.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} value - What was authored.
|
|
57
|
+
* @returns {boolean} True for a mapping.
|
|
58
|
+
*/
|
|
59
|
+
function mapping(value) {
|
|
60
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a value states a whole number.
|
|
65
|
+
*
|
|
66
|
+
* A quoted `"3"` counts, because YAML hands a quoted scalar back as a string
|
|
67
|
+
* and the ladder reads it as a number either way — `3.5` does not, because a
|
|
68
|
+
* rung sits on a rung and there is nothing between two of them.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value - The authored `level`.
|
|
71
|
+
* @returns {boolean} True when it is an integer.
|
|
72
|
+
*/
|
|
73
|
+
function integer(value) {
|
|
74
|
+
if (typeof value === "number") return Number.isInteger(value);
|
|
75
|
+
if (typeof value !== "string" || value.trim() === "") return false;
|
|
76
|
+
return Number.isInteger(Number(value));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a value states something rather than nothing.
|
|
81
|
+
*
|
|
82
|
+
* @param {unknown} value - The authored `title` or `description`.
|
|
83
|
+
* @returns {boolean} True for a non-empty string.
|
|
84
|
+
*/
|
|
85
|
+
function stated(value) {
|
|
86
|
+
return typeof value === "string" && value.trim() !== "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check one affiliation's `data.governance.ranks` against the rung contract.
|
|
91
|
+
*
|
|
92
|
+
* Each problem is its own finding, so a rung missing all three of its required
|
|
93
|
+
* keys reports three rather than the first: a reader fixing a ladder wants the
|
|
94
|
+
* whole of it in one pass.
|
|
95
|
+
*
|
|
96
|
+
* **A finding is located by the node it is about**, through the frontmatter's
|
|
97
|
+
* own YAML tree — a present-but-wrong value points at that value, and an absent
|
|
98
|
+
* key points at the rung that should have carried it, since a key that was
|
|
99
|
+
* never written has no position of its own. Where even the rung cannot be
|
|
100
|
+
* located the position is dropped rather than guessed, and the finding names
|
|
101
|
+
* the file alone.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} note - The note being checked.
|
|
104
|
+
* @param {string} note.file - Its path, as a diagnostic spells it.
|
|
105
|
+
* @param {string} [note.raw] - Its source, for the position.
|
|
106
|
+
* @param {object} [note.fm] - Its parsed frontmatter.
|
|
107
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
108
|
+
* severity: string, message: string}>} The findings, in authored order.
|
|
109
|
+
*/
|
|
110
|
+
export function checkRankLadder(note) {
|
|
111
|
+
const ranks = note.fm?.data?.governance?.ranks;
|
|
112
|
+
// Absent is the ordinary case: most bodies confer no ranks at all. A value
|
|
113
|
+
// that is not a list is the shape check's finding, not this one's.
|
|
114
|
+
if (!Array.isArray(ranks)) return [];
|
|
115
|
+
|
|
116
|
+
const findings = [];
|
|
117
|
+
const base = ["data", "governance", "ranks"];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One finding, located at the first of the given paths that resolves.
|
|
121
|
+
*
|
|
122
|
+
* @param {Array<Array<string|number>>} paths - Candidates, most specific
|
|
123
|
+
* first.
|
|
124
|
+
* @param {string} message - What is wrong.
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*/
|
|
127
|
+
const at = (paths, message) => {
|
|
128
|
+
let position = {};
|
|
129
|
+
for (const path of paths) {
|
|
130
|
+
position = positionOfFrontmatterPath(note.raw ?? "", path);
|
|
131
|
+
if (position.line !== undefined) break;
|
|
132
|
+
}
|
|
133
|
+
findings.push({ file: note.file, ...position, severity: "error", message });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Level → how the rung holding it is named, so a repeat can say which rung
|
|
137
|
+
// already holds the level rather than only that something does.
|
|
138
|
+
const held = new Map();
|
|
139
|
+
|
|
140
|
+
ranks.forEach((rung, index) => {
|
|
141
|
+
const rungPath = [...base, index];
|
|
142
|
+
// Named by its level where it states one, because a reader scanning a
|
|
143
|
+
// ladder of fifteen rungs knows them by level and not by position.
|
|
144
|
+
const which =
|
|
145
|
+
integer(rung?.level) ? `rank ${rung.level}` : `rank ${index + 1} of the ladder`;
|
|
146
|
+
|
|
147
|
+
if (!mapping(rung)) {
|
|
148
|
+
at([rungPath, base], `${which} must be a map of level, title and description`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const key of Object.keys(rung)) {
|
|
153
|
+
if (RUNG_KEYS.includes(key)) continue;
|
|
154
|
+
at(
|
|
155
|
+
[[...rungPath, key], rungPath],
|
|
156
|
+
`${which} has unknown key ${key}; a rung takes ${RUNG_KEYS.join(", ")}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!integer(rung.level)) {
|
|
161
|
+
at(
|
|
162
|
+
[[...rungPath, "level"], rungPath],
|
|
163
|
+
rung.level === undefined || rung.level === null ?
|
|
164
|
+
`${which} needs a level — the rung's position on this body's own ladder`
|
|
165
|
+
: `${which} has a level that is not a whole number`,
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
// A level belongs to one rung. Two rungs claiming it leave a
|
|
169
|
+
// member's `rank` answering to the one written first, so the
|
|
170
|
+
// ladder's own order decides what the standing is called and
|
|
171
|
+
// moving two lines renames it. Reported on the second rung,
|
|
172
|
+
// naming the first, because the first is the one a reader has to
|
|
173
|
+
// find to tell the two apart.
|
|
174
|
+
const level = Number(rung.level);
|
|
175
|
+
const named = stated(rung.title) ? JSON.stringify(rung.title) : which;
|
|
176
|
+
const first = held.get(level);
|
|
177
|
+
if (first === undefined) held.set(level, named);
|
|
178
|
+
else
|
|
179
|
+
at(
|
|
180
|
+
[[...rungPath, "level"], rungPath],
|
|
181
|
+
`rank ${level} is declared twice, as ${first} and ${named}; a level ` +
|
|
182
|
+
`is a rung's identity and a member's rank indexes into it, so ` +
|
|
183
|
+
`each rung states its own`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const key of REQUIRED_TEXT) {
|
|
188
|
+
if (stated(rung[key])) continue;
|
|
189
|
+
at(
|
|
190
|
+
[[...rungPath, key], rungPath],
|
|
191
|
+
`${which} needs a ${key}: ` +
|
|
192
|
+
(key === "title" ? "what the standing is called" : "what the standing is"),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return findings;
|
|
198
|
+
}
|
|
@@ -54,7 +54,7 @@ export function resolveReckoningMarkers(index, daysPerYear) {
|
|
|
54
54
|
1,
|
|
55
55
|
daysPerYear,
|
|
56
56
|
);
|
|
57
|
-
const pkg =
|
|
57
|
+
const pkg = note.package ?? index?.contentPackage;
|
|
58
58
|
const declared = [];
|
|
59
59
|
for (const [position, era] of rows.entries()) {
|
|
60
60
|
if (!era || typeof era.shortcode !== "string" || !era.shortcode || !fm.shortcode)
|
|
@@ -8,6 +8,7 @@ import { completeAddress, ownDocumentSystem, renderAddress } from "./address.mjs
|
|
|
8
8
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
9
9
|
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
10
10
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
11
|
+
import { standingsDigest } from "./standings.mjs";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Names and published destinations for reference presentation.
|
|
@@ -21,7 +22,18 @@ export function buildReferenceTargets(records, published = new Map()) {
|
|
|
21
22
|
const targets = new Map();
|
|
22
23
|
for (const record of records) {
|
|
23
24
|
if (!record.package || !record.type || !record.shortcode) continue;
|
|
24
|
-
|
|
25
|
+
// `subType` and the standings a body confers both ride along for the
|
|
26
|
+
// same reason: a caller resolving a reference often needs to know what
|
|
27
|
+
// it found, not only where it is. A rank is a number on the *being*
|
|
28
|
+
// and what that number is called is declared on the *body*, so the
|
|
29
|
+
// ladder travels with the reference rather than being looked up a
|
|
30
|
+
// second way.
|
|
31
|
+
const standings = standingsDigest(record);
|
|
32
|
+
const value = {
|
|
33
|
+
name: record.name?.full ?? record.name,
|
|
34
|
+
subType: record.subType,
|
|
35
|
+
...(standings ? { standings } : {}),
|
|
36
|
+
};
|
|
25
37
|
const systems = new Set([ownDocumentSystem(record.type)]);
|
|
26
38
|
if (JOURNAL_TYPES.has(record.type) || hasDocEntry(record.type)) systems.add(NOTE_SYSTEM);
|
|
27
39
|
for (const system of systems) {
|
|
@@ -58,11 +58,9 @@
|
|
|
58
58
|
* one of them in. Every note that carried one has been moved, across four
|
|
59
59
|
* repositories, and this is the third step of that retirement.
|
|
60
60
|
*
|
|
61
|
-
* Refusing it
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the page as a theme parameter, checked by nothing. The whole argument for
|
|
65
|
-
* `data:` being closed is the argument for refusing this.
|
|
61
|
+
* Refusing it by name rather than as an unknown key is what names the home the
|
|
62
|
+
* facts have: the finding says which `data:` key each one becomes, which an
|
|
63
|
+
* author cannot read off a list of the keys the region declares.
|
|
66
64
|
*
|
|
67
65
|
* `sohl.traits` is a different field that shares the name — `projectilegear`
|
|
68
66
|
* declares one, and the theme's gear sidebar reads it — so the refusal is
|
|
@@ -350,8 +348,7 @@ export function traitsRetiredMessage(file) {
|
|
|
350
348
|
(file ? ` — ${file}` : "") +
|
|
351
349
|
". A being's own description belongs in the closed container the " +
|
|
352
350
|
"content format declares, where `being` declares every one of its " +
|
|
353
|
-
"keys
|
|
354
|
-
"so a misspelling became a theme parameter rather than a finding. " +
|
|
351
|
+
"keys. " +
|
|
355
352
|
"`gender`, `species`, `age` and `appearance.*` move unchanged; " +
|
|
356
353
|
"`traits.birthday` becomes `data.born`; three reshape — " +
|
|
357
354
|
"`traits.height.m` becomes `data.height` " +
|
|
@@ -526,12 +523,10 @@ export function legacyKeyMessage(block, field, file) {
|
|
|
526
523
|
* What a note writing a field at the **top-level key `data:` gathered it off**
|
|
527
524
|
* is told.
|
|
528
525
|
*
|
|
529
|
-
* {@link legacyKeyMessage}'s counterpart for the other retiring position.
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* of `data.portrait`, and both are read for the same reason both in-block
|
|
534
|
-
* spellings are: a package moves its corpus when it is ready, not on a flag day.
|
|
526
|
+
* {@link legacyKeyMessage}'s counterpart for the other retiring position.
|
|
527
|
+
* `portrait:` beside `img:` is the bare spelling of `data.portrait`, and both
|
|
528
|
+
* are read for the same reason both in-block spellings are: a package moves its
|
|
529
|
+
* corpus when it is ready, not on a flag day.
|
|
535
530
|
*
|
|
536
531
|
* It says nothing about which value is emitted, because that is not what an
|
|
537
532
|
* author needs from it. The value is the same either way; what the finding
|
package/engine/scenes.mjs
CHANGED
|
@@ -50,6 +50,10 @@ import fs from "fs";
|
|
|
50
50
|
import path from "path";
|
|
51
51
|
import log from "loglevel";
|
|
52
52
|
|
|
53
|
+
// The one reader of a system block's own properties, so a scene's effect
|
|
54
|
+
// references resolve from the same position the item compiler emits from.
|
|
55
|
+
import { blockProperty } from "./system-block.mjs";
|
|
56
|
+
|
|
53
57
|
import {
|
|
54
58
|
parseMarkdownFile,
|
|
55
59
|
sohlField,
|
|
@@ -235,7 +239,11 @@ export class Scenes extends BasePackCompiler {
|
|
|
235
239
|
// `resolveNoteId(fm)` with no package, which falls back to the
|
|
236
240
|
// ambient one. What remains unset is a file with no address.
|
|
237
241
|
if (!fm || !fm.id) continue;
|
|
238
|
-
|
|
242
|
+
// The Active Effects a region behaviour may point at, read from
|
|
243
|
+
// the block that carries them onto the document — the one position
|
|
244
|
+
// the format accepts for them.
|
|
245
|
+
const effects = blockProperty(fm, "sohl", "effects");
|
|
246
|
+
if (fm.shortcode && Array.isArray(effects) && effects.length) {
|
|
239
247
|
effectsByAddress.set(`${fm.type}-${fm.shortcode}`, {
|
|
240
248
|
id: fm.id,
|
|
241
249
|
type: fm.type,
|
|
@@ -246,7 +254,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
246
254
|
// second router is a second answer to where the document
|
|
247
255
|
// landed, resolved from the working directory.
|
|
248
256
|
pack: this.#packRouter.resolveOrNull(fm, packForType(fm.type).docType),
|
|
249
|
-
effects
|
|
257
|
+
effects,
|
|
250
258
|
});
|
|
251
259
|
}
|
|
252
260
|
if (!isMapType(fm.type)) continue;
|
|
@@ -532,7 +540,6 @@ export class Scenes extends BasePackCompiler {
|
|
|
532
540
|
// materialise there too. The id spelling that used
|
|
533
541
|
// to cross packs verbatim is retired.
|
|
534
542
|
folder: this.folderResolver(authoredFolder, { isAddress: true }),
|
|
535
|
-
flags: fm.flags,
|
|
536
543
|
})
|
|
537
544
|
: null;
|
|
538
545
|
|
package/engine/site-build.mjs
CHANGED
|
@@ -62,9 +62,11 @@ import { addressSlug } from "./content-address.mjs";
|
|
|
62
62
|
import { protectCode } from "./code-fences.mjs";
|
|
63
63
|
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
64
64
|
import { expandContentTables } from "./content-tables.mjs";
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
65
|
+
import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
|
|
66
|
+
import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
|
|
67
67
|
import { renderCaptionBlocks, scanCaptions } from "./content-captions.mjs";
|
|
68
|
+
import { footnoteFindings } from "./content-footnotes.mjs";
|
|
69
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
68
70
|
import { renderImageFigures } from "./content-images.mjs";
|
|
69
71
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
70
72
|
import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
@@ -601,8 +603,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
601
603
|
}
|
|
602
604
|
|
|
603
605
|
/**
|
|
604
|
-
* The title a content page publishes under
|
|
605
|
-
* note's name.
|
|
606
|
+
* The title a content page publishes under — the note's name.
|
|
606
607
|
*
|
|
607
608
|
* One rule, read by the page's own front matter and by every `related` entry
|
|
608
609
|
* that names the page, so a card lists a page by exactly the title its heading
|
|
@@ -612,7 +613,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
612
613
|
* @returns {string} The title.
|
|
613
614
|
*/
|
|
614
615
|
function pageTitle(page) {
|
|
615
|
-
return page.
|
|
616
|
+
return page.name;
|
|
616
617
|
}
|
|
617
618
|
|
|
618
619
|
/**
|
|
@@ -704,6 +705,8 @@ export function renderSitePage(
|
|
|
704
705
|
const expressionErrors = [];
|
|
705
706
|
const secretErrors = [];
|
|
706
707
|
const captionErrors = [];
|
|
708
|
+
const headingErrors = [];
|
|
709
|
+
const footnoteErrors = [];
|
|
707
710
|
const wikiErrors = [];
|
|
708
711
|
const imageErrors = [];
|
|
709
712
|
const resolved = [];
|
|
@@ -753,11 +756,15 @@ export function renderSitePage(
|
|
|
753
756
|
linkable,
|
|
754
757
|
source: src,
|
|
755
758
|
sqlTables: sqlTables?.get(page.file),
|
|
759
|
+
pageLists: sqlTables?.pageLists?.get(page.file),
|
|
756
760
|
self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
|
|
757
761
|
});
|
|
758
762
|
tableErrors.push(...errors);
|
|
759
763
|
const captionScan = scanCaptions(markdown);
|
|
760
764
|
ctx.captionLabels = new Map(captionScan.captions.map((caption) => [caption.id, caption.label]));
|
|
765
|
+
// This page's own anchors, so a `[[#slug]]` self-link is checked against
|
|
766
|
+
// what the page actually declares rather than trusted unconditionally.
|
|
767
|
+
ctx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
|
|
761
768
|
const expressions = renderMarkdownExpressions(markdown, {
|
|
762
769
|
fm: page.fm,
|
|
763
770
|
dates: index.dateContext,
|
|
@@ -767,9 +774,8 @@ export function renderSitePage(
|
|
|
767
774
|
});
|
|
768
775
|
expressionErrors.push(...expressions.findings);
|
|
769
776
|
const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
|
|
770
|
-
const
|
|
771
|
-
const
|
|
772
|
-
const captioned = renderCaptionBlocks(admonitions.markdown);
|
|
777
|
+
const blocks = renderBlocks(protectCode(expressions.markdown, resolve), "web");
|
|
778
|
+
const captioned = renderCaptionBlocks(blocks.markdown);
|
|
773
779
|
for (const error of captionScan.errors)
|
|
774
780
|
captionErrors.push({
|
|
775
781
|
file: page.file,
|
|
@@ -777,15 +783,19 @@ export function renderSitePage(
|
|
|
777
783
|
column: error.column,
|
|
778
784
|
message: error.message,
|
|
779
785
|
});
|
|
780
|
-
for (const error of
|
|
786
|
+
for (const error of scanBlocks(page.body).errors)
|
|
781
787
|
secretErrors.push({
|
|
782
788
|
file: page.file,
|
|
783
789
|
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
784
790
|
column: error.column,
|
|
785
791
|
message: error.message,
|
|
786
792
|
});
|
|
787
|
-
for (const error of
|
|
788
|
-
|
|
793
|
+
for (const error of scanHeadingAttributes(page.body, page.bodyLine ?? 1).errors)
|
|
794
|
+
headingErrors.push({ file: page.file, ...error });
|
|
795
|
+
for (const error of withheldSections(page.body, page.bodyLine ?? 1).errors)
|
|
796
|
+
headingErrors.push({ file: page.file, ...error });
|
|
797
|
+
for (const error of footnoteFindings(page.body))
|
|
798
|
+
footnoteErrors.push({
|
|
789
799
|
file: page.file,
|
|
790
800
|
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
791
801
|
column: error.column,
|
|
@@ -793,13 +803,18 @@ export function renderSitePage(
|
|
|
793
803
|
});
|
|
794
804
|
return {
|
|
795
805
|
page,
|
|
796
|
-
|
|
806
|
+
// The disclosure is written last, over the Markdown the page ships: the
|
|
807
|
+
// passes before this one carry line positions into their findings, and a
|
|
808
|
+
// line inserted ahead of them would move every one of them.
|
|
809
|
+
body: renderWithheldSections(captioned.markdown),
|
|
797
810
|
data,
|
|
798
811
|
resolved,
|
|
799
812
|
tableErrors,
|
|
800
813
|
expressionErrors,
|
|
801
814
|
secretErrors,
|
|
802
815
|
captionErrors,
|
|
816
|
+
headingErrors,
|
|
817
|
+
footnoteErrors,
|
|
803
818
|
wikiErrors,
|
|
804
819
|
imageErrors,
|
|
805
820
|
};
|
|
@@ -877,6 +892,8 @@ export function renderPages(pages, options) {
|
|
|
877
892
|
const expressionErrors = [];
|
|
878
893
|
const secretErrors = [];
|
|
879
894
|
const captionErrors = [];
|
|
895
|
+
const headingErrors = [];
|
|
896
|
+
const footnoteErrors = [];
|
|
880
897
|
const wikiErrors = [];
|
|
881
898
|
const imageErrors = [];
|
|
882
899
|
const byKind = {};
|
|
@@ -920,6 +937,8 @@ export function renderPages(pages, options) {
|
|
|
920
937
|
expressionErrors.push(...result.expressionErrors);
|
|
921
938
|
secretErrors.push(...result.secretErrors);
|
|
922
939
|
captionErrors.push(...result.captionErrors);
|
|
940
|
+
headingErrors.push(...result.headingErrors);
|
|
941
|
+
footnoteErrors.push(...result.footnoteErrors);
|
|
923
942
|
wikiErrors.push(...result.wikiErrors);
|
|
924
943
|
imageErrors.push(...result.imageErrors);
|
|
925
944
|
rendered.push({ page, body: result.body, data: result.data });
|
|
@@ -963,6 +982,8 @@ export function renderPages(pages, options) {
|
|
|
963
982
|
expressionErrors,
|
|
964
983
|
secretErrors,
|
|
965
984
|
captionErrors,
|
|
985
|
+
headingErrors,
|
|
986
|
+
footnoteErrors,
|
|
966
987
|
wikiErrors,
|
|
967
988
|
imageErrors,
|
|
968
989
|
related,
|
|
@@ -1131,6 +1152,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1131
1152
|
expressionErrors: [],
|
|
1132
1153
|
secretErrors: [],
|
|
1133
1154
|
captionErrors: [],
|
|
1155
|
+
headingErrors: [],
|
|
1156
|
+
footnoteErrors: [],
|
|
1134
1157
|
wikiErrors: [],
|
|
1135
1158
|
imageErrors: [],
|
|
1136
1159
|
mapFindings: [],
|
|
@@ -1153,6 +1176,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1153
1176
|
expressionErrors: [],
|
|
1154
1177
|
secretErrors: [],
|
|
1155
1178
|
captionErrors: [],
|
|
1179
|
+
headingErrors: [],
|
|
1180
|
+
footnoteErrors: [],
|
|
1156
1181
|
wikiErrors: [],
|
|
1157
1182
|
imageErrors: [],
|
|
1158
1183
|
mapFindings: [],
|
|
@@ -1201,6 +1226,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1201
1226
|
expressionErrors: [],
|
|
1202
1227
|
secretErrors: [],
|
|
1203
1228
|
captionErrors: [],
|
|
1229
|
+
headingErrors: [],
|
|
1230
|
+
footnoteErrors: [],
|
|
1204
1231
|
wikiErrors: [],
|
|
1205
1232
|
imageErrors: [],
|
|
1206
1233
|
mapFindings: [],
|
|
@@ -1259,6 +1286,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1259
1286
|
expressionErrors: rendered.expressionErrors,
|
|
1260
1287
|
secretErrors: rendered.secretErrors,
|
|
1261
1288
|
captionErrors: rendered.captionErrors,
|
|
1289
|
+
headingErrors: rendered.headingErrors,
|
|
1290
|
+
footnoteErrors: rendered.footnoteErrors,
|
|
1262
1291
|
wikiErrors: rendered.wikiErrors,
|
|
1263
1292
|
imageErrors: rendered.imageErrors,
|
|
1264
1293
|
mapFindings: drawn.findings,
|
package/engine/site-config.mjs
CHANGED
|
@@ -102,9 +102,18 @@ export const DISABLE_KINDS = Object.freeze(["section", "taxonomy", "term", "RSS"
|
|
|
102
102
|
* `<span>` marking an unresolved link — and Goldmark drops raw HTML unless
|
|
103
103
|
* told otherwise. A theme cannot supply this: Hugo does not merge a theme's
|
|
104
104
|
* `markup` block.
|
|
105
|
+
*
|
|
106
|
+
* `extensions.taskList` is off because the format has no checkbox: Goldmark
|
|
107
|
+
* enables it by default, which is the only reason a list item opened with
|
|
108
|
+
* `[ ]` renders a `<input type="checkbox">` here and the same two characters as
|
|
109
|
+
* plain text in the pack and the book. Off, a list item's text begins right
|
|
110
|
+
* after its marker on every surface.
|
|
105
111
|
*/
|
|
106
112
|
export const MARKUP = Object.freeze({
|
|
107
|
-
goldmark: Object.freeze({
|
|
113
|
+
goldmark: Object.freeze({
|
|
114
|
+
renderer: Object.freeze({ unsafe: true }),
|
|
115
|
+
extensions: Object.freeze({ taskList: false }),
|
|
116
|
+
}),
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
/** Where the navigation is published. */
|
package/engine/site-index.mjs
CHANGED
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import { buildReferenceTargets } from "./reference-targets.mjs";
|
|
53
|
+
import { standingsDigest } from "./standings.mjs";
|
|
54
|
+
// The leaf reader, shared with the content index and the link checker, so a
|
|
55
|
+
// page's anchors are read once rather than re-derived a third way here.
|
|
56
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
53
57
|
|
|
54
58
|
import { resolveShortcodeReference } from "./shortcode-references.mjs";
|
|
55
59
|
|
|
@@ -82,16 +86,24 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
82
86
|
* @property {string} slug URL segment.
|
|
83
87
|
* @property {string} base Source file's basename, e.g. `Climbing.md`.
|
|
84
88
|
* @property {string} url The page's published address.
|
|
89
|
+
* @property {string} [body] The note's markdown body, for its anchors. A
|
|
90
|
+
* content entry always carries one; absent, the
|
|
91
|
+
* page is indexed as declaring none.
|
|
85
92
|
*/
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
95
|
* The resolved index and everything a wikilink resolver reads beside it.
|
|
89
96
|
*
|
|
90
97
|
* @typedef {object} SiteIndex
|
|
91
|
-
* @property {Map<string, {url: string, name?: string, draft?: boolean}>} index
|
|
98
|
+
* @property {Map<string, {url: string, name?: string, draft?: boolean, anchors?: Set<string>|Record<string, string>}>} index
|
|
92
99
|
* Address → page. `draft` says the page
|
|
93
100
|
* carries the `draft` tag, which marks a
|
|
94
|
-
* link *into* it.
|
|
101
|
+
* link *into* it. `anchors` names the
|
|
102
|
+
* `{#slug}` sections it declares — a
|
|
103
|
+
* `Set` for a local page, or the
|
|
104
|
+
* `{slug: uuid}` map a foreign manifest
|
|
105
|
+
* publishes — and is absent where
|
|
106
|
+
* neither build recorded one.
|
|
95
107
|
* @property {Set<string>} ambiguous Short addresses claimed by two
|
|
96
108
|
* packages, and so deliberately absent
|
|
97
109
|
* from `index`.
|
|
@@ -218,11 +230,21 @@ export function buildSiteIndex(
|
|
|
218
230
|
// needs to know what it found, not only where it is: an infobox groups
|
|
219
231
|
// a being's skills by the family each skill note declares, and that
|
|
220
232
|
// fact lives on the target rather than on the reference.
|
|
233
|
+
// The rungs and posts a body confers, carried for the same reason
|
|
234
|
+
// `subType` is: a being states a rank as a number, and what that
|
|
235
|
+
// number is called is declared on the body.
|
|
236
|
+
const standings = standingsDigest(e.fm);
|
|
221
237
|
const value = {
|
|
222
238
|
url: e.url,
|
|
223
239
|
name: e.name,
|
|
224
240
|
draft: isDraftNote(e.fm),
|
|
225
241
|
...(e.fm.subType ? { subType: e.fm.subType } : {}),
|
|
242
|
+
...(standings ? { standings } : {}),
|
|
243
|
+
// The `{#anchor}` slugs this page declares, so a `#section`
|
|
244
|
+
// written against it can be checked rather than joined onto the
|
|
245
|
+
// URL unverified. A Set even when empty: a page that declares no
|
|
246
|
+
// anchor still has a definite answer for one an author names.
|
|
247
|
+
anchors: new Set(collectAnchors(e.body ?? "").map((anchor) => anchor.slug)),
|
|
226
248
|
};
|
|
227
249
|
|
|
228
250
|
const shortcode = e.fm.shortcode;
|
|
@@ -343,7 +365,13 @@ export function resolveInfoboxRef(siteIndex, ref, hint) {
|
|
|
343
365
|
hint,
|
|
344
366
|
);
|
|
345
367
|
return found ?
|
|
346
|
-
{
|
|
368
|
+
{
|
|
369
|
+
name: found.name,
|
|
370
|
+
url: found.url,
|
|
371
|
+
subType: found.subType,
|
|
372
|
+
address: found.address,
|
|
373
|
+
...(found.standings ? { standings: found.standings } : {}),
|
|
374
|
+
}
|
|
347
375
|
: undefined;
|
|
348
376
|
}
|
|
349
377
|
const context = {
|
|
@@ -357,6 +385,13 @@ export function resolveInfoboxRef(siteIndex, ref, hint) {
|
|
|
357
385
|
const found =
|
|
358
386
|
siteIndex.referenceTargets?.get(renderAddress(tuple)) ??
|
|
359
387
|
siteIndex.index?.get(renderAddress(tuple));
|
|
360
|
-
if (found)
|
|
388
|
+
if (found)
|
|
389
|
+
return {
|
|
390
|
+
name: found.name,
|
|
391
|
+
url: found.url,
|
|
392
|
+
subType: found.subType,
|
|
393
|
+
address: tuple,
|
|
394
|
+
...(found.standings ? { standings: found.standings } : {}),
|
|
395
|
+
};
|
|
361
396
|
return undefined;
|
|
362
397
|
}
|
package/engine/site-preview.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { reckoningContext } from "./reckoning-markers.mjs";
|
|
|
18
18
|
import { cachedMetadataIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
19
19
|
import { buildSiteIndex } from "./site-index.mjs";
|
|
20
20
|
import { openNotesDatabase, prepareSqlTables, findSqlBlocks } from "./sql-tables.mjs";
|
|
21
|
+
import { findPageListBlocks } from "./page-lists.mjs";
|
|
21
22
|
import { isGmNote } from "./note-vocabulary.mjs";
|
|
22
23
|
import { relatedPages } from "./related-pages.mjs";
|
|
23
24
|
import { holdingsNode, holdingsPages, foreignHoldingsNodes } from "./holdings.mjs";
|
|
@@ -90,9 +91,13 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
90
91
|
);
|
|
91
92
|
try {
|
|
92
93
|
const sources = pages
|
|
93
|
-
.filter(
|
|
94
|
+
.filter(
|
|
95
|
+
(page) =>
|
|
96
|
+
findSqlBlocks(page.body).length || findPageListBlocks(page.body).length,
|
|
97
|
+
)
|
|
94
98
|
.map((page) => ({ source: page.file, markdown: page.body }));
|
|
95
|
-
const
|
|
99
|
+
const publicRecords = records.filter((record) => !isGmNote(record));
|
|
100
|
+
const sqlTables = await prepareSqlTables(db, sources, { records: publicRecords });
|
|
96
101
|
const rendered = renderPages(pages, {
|
|
97
102
|
index: gates.index,
|
|
98
103
|
foreign: gates.foreign,
|
|
@@ -214,9 +219,11 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
214
219
|
},
|
|
215
220
|
);
|
|
216
221
|
}
|
|
217
|
-
const sqlTables = await prepareSqlTables(
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
const sqlTables = await prepareSqlTables(
|
|
223
|
+
db,
|
|
224
|
+
[{ source: absolute, markdown: body }],
|
|
225
|
+
{ records: records.filter((record) => !isGmNote(record)) },
|
|
226
|
+
);
|
|
220
227
|
const allPages = snapshot.pages.map((item) =>
|
|
221
228
|
item.file === absolute ? page : item,
|
|
222
229
|
);
|
|
@@ -246,6 +253,10 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
246
253
|
findings.push({ ...error, severity: "error" });
|
|
247
254
|
for (const error of result.captionErrors)
|
|
248
255
|
findings.push({ ...error, severity: "error" });
|
|
256
|
+
for (const error of result.headingErrors)
|
|
257
|
+
findings.push({ ...error, severity: "error" });
|
|
258
|
+
for (const error of result.footnoteErrors)
|
|
259
|
+
findings.push({ ...error, severity: "error" });
|
|
249
260
|
for (const error of result.wikiErrors) {
|
|
250
261
|
const pos = positionOfLiteral(text, error.link, error.occurrence);
|
|
251
262
|
findings.push({
|
|
@@ -265,7 +276,7 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
265
276
|
const entries = new Map(snapshot.rendered.entries);
|
|
266
277
|
entries.delete(original.url);
|
|
267
278
|
entries.set(page.url, {
|
|
268
|
-
title:
|
|
279
|
+
title: page.name,
|
|
269
280
|
url: page.url,
|
|
270
281
|
type: String(fm.type),
|
|
271
282
|
});
|
|
@@ -273,7 +284,7 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
273
284
|
if (related) result.data.related = related;
|
|
274
285
|
const holdings = holdingsPages([
|
|
275
286
|
...allPages.map((item) =>
|
|
276
|
-
holdingsNode(item.fm, { title: item.
|
|
287
|
+
holdingsNode(item.fm, { title: item.name, url: item.url }),
|
|
277
288
|
),
|
|
278
289
|
...foreignHoldingsNodes(snapshot.gates.foreign.index),
|
|
279
290
|
]);
|