@heroiclands/package-build 22.21.0 → 22.23.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 +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- 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 +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- 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/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- 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 +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- 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 +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- 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/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/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- 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 +84 -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 +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/helpers.mjs
CHANGED
|
@@ -42,8 +42,15 @@ import footnotePlugin from "markdown-it-footnote";
|
|
|
42
42
|
import deflistPlugin from "markdown-it-deflist";
|
|
43
43
|
import { iconPlugin } from "./content-icons.mjs";
|
|
44
44
|
import { imagePlugin, imagesIn } from "./content-images.mjs";
|
|
45
|
-
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
46
|
-
import {
|
|
45
|
+
import { embedRole, resolveEmbeds } from "./content-embeds.mjs";
|
|
46
|
+
import { collectAssetRecords } from "./asset-index.mjs";
|
|
47
|
+
import { assetImageInfoByPathname } from "./art-fields.mjs";
|
|
48
|
+
import {
|
|
49
|
+
assetPathnameKey,
|
|
50
|
+
foundryAddressProblem,
|
|
51
|
+
pathnameProblem,
|
|
52
|
+
resolvePathname,
|
|
53
|
+
} from "./pathnames.mjs";
|
|
47
54
|
import log from "loglevel";
|
|
48
55
|
|
|
49
56
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -65,9 +72,9 @@ import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
|
65
72
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
66
73
|
import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
|
|
67
74
|
import { expandContentTables } from "./content-tables.mjs";
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
75
|
+
import { renderBlocks } from "./content-blocks.mjs";
|
|
76
|
+
import { headingAttributesPlugin } from "./heading-attributes.mjs";
|
|
77
|
+
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
71
78
|
import { positionInBody } from "./diagnostics.mjs";
|
|
72
79
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
73
80
|
// registry can import them without reaching back through this one.
|
|
@@ -84,6 +91,9 @@ export {
|
|
|
84
91
|
parseValueDesc,
|
|
85
92
|
} from "./frontmatter.mjs";
|
|
86
93
|
|
|
94
|
+
/** @type {Map<string, object>|undefined} */
|
|
95
|
+
let localAssetImageInfoCache;
|
|
96
|
+
|
|
87
97
|
/**
|
|
88
98
|
* The markdown renderer every surface shares.
|
|
89
99
|
*
|
|
@@ -95,6 +105,9 @@ export {
|
|
|
95
105
|
export const md = markdownit({ html: true })
|
|
96
106
|
.use(footnotePlugin)
|
|
97
107
|
.use(deflistPlugin)
|
|
108
|
+
// A heading's attribute block, written onto the heading element rather than
|
|
109
|
+
// typeset into it — the reading the website's renderer does for itself.
|
|
110
|
+
.use(headingAttributesPlugin)
|
|
98
111
|
.use(
|
|
99
112
|
// Resolved per render, not at import: this constant is built before any
|
|
100
113
|
// configuration is read, and a package's own icons live in the
|
|
@@ -114,25 +127,65 @@ export const md = markdownit({ html: true })
|
|
|
114
127
|
// serves a file from inside the install, so a body image's address is
|
|
115
128
|
// translated by the same rule `img:` follows.
|
|
116
129
|
.use(
|
|
117
|
-
imagePlugin(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
imagePlugin(
|
|
131
|
+
(src) => {
|
|
132
|
+
// **Reported upstream, never here.** A renderer has no channel to
|
|
133
|
+
// report through, and this one runs inside the very passes whose
|
|
134
|
+
// job is to collect findings — a throw would take the whole lint
|
|
135
|
+
// down and lose every other finding in the tree. So nothing reaches
|
|
136
|
+
// this point unreported: {@link convertNoteWikilinks} refuses a body
|
|
137
|
+
// image with no address inside the install before a compiler renders
|
|
138
|
+
// one, and the address passes report the same pathname with a line
|
|
139
|
+
// and a column. What is left here is a fallback for a caller with no
|
|
140
|
+
// configuration to resolve against, where the authored pathname is
|
|
141
|
+
// the most honest thing to emit.
|
|
142
|
+
try {
|
|
143
|
+
return resolveImg(src, loadPackConfig()) ?? src;
|
|
144
|
+
} catch {
|
|
145
|
+
return src;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
(src) => {
|
|
149
|
+
// A body image is free to write the bare, own-package form,
|
|
150
|
+
// so the address is normalized to the pathname the cache
|
|
151
|
+
// below keys by before it is looked up.
|
|
152
|
+
try {
|
|
153
|
+
const key = assetPathnameKey(src, loadPackConfig());
|
|
154
|
+
return key ? localAssetImageInfo().get(key) : undefined;
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
),
|
|
134
160
|
);
|
|
135
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The pictures this package's own tree ships, by the pathname their address
|
|
164
|
+
* resolves to — read lazily and cached for the process, exactly as
|
|
165
|
+
* {@link loadPackConfig} is, since one compile reads one tree once.
|
|
166
|
+
*
|
|
167
|
+
* **Local only.** A picture embedded from a dependency carries no role or
|
|
168
|
+
* pixel size here: fetching a foreign package's own asset index to size one
|
|
169
|
+
* inline Foundry image is a cost this lazy cache does not pay, and the
|
|
170
|
+
* picture simply draws at the medium's ordinary size, as it always has.
|
|
171
|
+
*
|
|
172
|
+
* @returns {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
|
|
173
|
+
*/
|
|
174
|
+
function localAssetImageInfo() {
|
|
175
|
+
if (localAssetImageInfoCache) return localAssetImageInfoCache;
|
|
176
|
+
try {
|
|
177
|
+
const config = loadPackConfig();
|
|
178
|
+
const records = collectAssetRecords(config.paths.assets, {
|
|
179
|
+
contentPackage: config.contentPackage,
|
|
180
|
+
problems: [],
|
|
181
|
+
});
|
|
182
|
+
localAssetImageInfoCache = assetImageInfoByPathname(records);
|
|
183
|
+
} catch {
|
|
184
|
+
localAssetImageInfoCache = new Map();
|
|
185
|
+
}
|
|
186
|
+
return localAssetImageInfoCache;
|
|
187
|
+
}
|
|
188
|
+
|
|
136
189
|
md.renderer.rules.footnote_block_open = () =>
|
|
137
190
|
'<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
|
|
138
191
|
md.renderer.rules.footnote_caption = (tokens, idx, _options, env) => {
|
|
@@ -152,16 +205,26 @@ md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
|
|
|
152
205
|
return `<li id="fn${id}" class="footnote-item" value="${number}">`;
|
|
153
206
|
};
|
|
154
207
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Render a note body with Foundry's named-block markup.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} body - The body, tables expanded and wikilinks resolved.
|
|
212
|
+
* @param {Array<{id: string, label: string}>} [figures] - Labels assigned by
|
|
213
|
+
* a note-wide scan, matched by id — see
|
|
214
|
+
* {@link module:engine/content-figures.renderFigureBlocks}.
|
|
215
|
+
* @param {Map<string, number>} [footnoteNumbers] - Shared across the note.
|
|
216
|
+
* @param {string} [docId] - Threaded into footnote anchors.
|
|
217
|
+
* @param {(address: string) => string|undefined} [resolveRole] - From a
|
|
218
|
+
* picture's address to the role its asset declares — see
|
|
219
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
220
|
+
* @returns {string} The rendered HTML.
|
|
221
|
+
*/
|
|
222
|
+
export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole) {
|
|
223
|
+
const blocks = renderBlocks(body, "foundry");
|
|
224
|
+
const figured = renderFigureBlocks(blocks.markdown, (block) => md.render(block), figures, {
|
|
225
|
+
resolveRole,
|
|
226
|
+
});
|
|
227
|
+
return md.render(figured.markdown, { footnoteNumbers, docId });
|
|
165
228
|
}
|
|
166
229
|
|
|
167
230
|
/**
|
|
@@ -547,11 +610,11 @@ export function makeFilename(name, id) {
|
|
|
547
610
|
* the other.
|
|
548
611
|
*
|
|
549
612
|
* **`title` does not follow this rule**, and must not be made to. On a
|
|
550
|
-
* `type: affiliation` note `title` is
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
613
|
+
* `type: affiliation` note `title` is a declared item field with its own
|
|
614
|
+
* default (`""`, in `sohl/item-fields.mjs`), authored only inside the item's
|
|
615
|
+
* own `sohl` block — the closed top-level vocabulary has no `title` entry, so
|
|
616
|
+
* there is no shared key for this function's null-versus-blank distinction to
|
|
617
|
+
* apply to.
|
|
555
618
|
*
|
|
556
619
|
* **`banner:` does not follow it either, deliberately.** It is not a file
|
|
557
620
|
* inside a Foundry install: it reaches no compiled document and no book, its
|
|
@@ -943,7 +1006,9 @@ export function convertNoteWikilinks(
|
|
|
943
1006
|
docPack,
|
|
944
1007
|
index,
|
|
945
1008
|
captionLabels: new Map(
|
|
946
|
-
|
|
1009
|
+
scanFigures(source, { resolveRole: (address) => embedRole(index, address) })
|
|
1010
|
+
.figures.filter((figure) => figure.id)
|
|
1011
|
+
.map((figure) => [figure.id, figure.label]),
|
|
947
1012
|
),
|
|
948
1013
|
});
|
|
949
1014
|
/**
|
|
@@ -1035,18 +1100,21 @@ export function convertNoteWikilinks(
|
|
|
1035
1100
|
/* ------------------------------------------------------------------------ */
|
|
1036
1101
|
|
|
1037
1102
|
/**
|
|
1038
|
-
* Expand prepared SQL tables before resolving wikilinks in
|
|
1103
|
+
* Expand prepared SQL tables and page lists before resolving wikilinks in the
|
|
1104
|
+
* links they generate.
|
|
1039
1105
|
* @param {string} body - The note body.
|
|
1040
|
-
* @param {object} ctx - Source and prepared
|
|
1106
|
+
* @param {object} ctx - Source and prepared results.
|
|
1041
1107
|
* @param {string} ctx.name - Name used in diagnostics.
|
|
1042
1108
|
* @param {number} [ctx.bodyLine] - The first body line in the source file.
|
|
1043
1109
|
* @param {object[]} [ctx.sqlTables] - Prepared SQL results in document order.
|
|
1110
|
+
* @param {object[]} [ctx.pageLists] - Prepared page lists in document order.
|
|
1044
1111
|
* @returns {{markdown: string, lineMap: Array<{line: number, generated: boolean}>}}
|
|
1045
1112
|
*/
|
|
1046
|
-
export function expandNoteTables(body, { name, bodyLine, sqlTables }) {
|
|
1113
|
+
export function expandNoteTables(body, { name, bodyLine, sqlTables, pageLists }) {
|
|
1047
1114
|
const { markdown, errors, lineMap } = expandContentTables(body ?? "", {
|
|
1048
1115
|
source: name,
|
|
1049
1116
|
sqlTables,
|
|
1117
|
+
pageLists,
|
|
1050
1118
|
});
|
|
1051
1119
|
if (errors.length) {
|
|
1052
1120
|
const err = new Error(errors.map((e) => `content table — ${e.reason}`).join("; "));
|
package/engine/homepage.mjs
CHANGED
|
@@ -131,12 +131,12 @@ export function isHomepage(fm) {
|
|
|
131
131
|
* `name` and `shortcode` are permitted: the shortcode is what a link is
|
|
132
132
|
* written with, and `name` titles the page like every other note's.
|
|
133
133
|
*
|
|
134
|
-
* **A named class, not an allow-list, and that boundary is the decision.**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
134
|
+
* **A named class, not an allow-list, and that boundary is the decision.**
|
|
135
|
+
* What this class answers is a key the top-level vocabulary *accepts* and a
|
|
136
|
+
* homepage still cannot use, which is a different question from an unknown
|
|
137
|
+
* key: the frontmatter lint refuses those for every note alike, naming the
|
|
138
|
+
* closed vocabulary, so repeating that judgement per type would be a second
|
|
139
|
+
* answer to one question.
|
|
140
140
|
*
|
|
141
141
|
* `aliases` is deliberately not in the class: it is a **retired** field, refused
|
|
142
142
|
* on every note whatever its type, so it is answered there rather than
|
|
@@ -340,7 +340,7 @@ function positionOfType(file) {
|
|
|
340
340
|
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
341
341
|
* package — so a homepage that adds nothing to it need not restate it. An
|
|
342
342
|
* authored `name.full` wins, because a front page may greet a reader
|
|
343
|
-
* differently from a package browser.
|
|
343
|
+
* differently from a package browser.
|
|
344
344
|
*
|
|
345
345
|
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
346
346
|
* own still yields a titled page rather than a blank heading.
|
|
@@ -350,7 +350,7 @@ function positionOfType(file) {
|
|
|
350
350
|
* @returns {string} The title.
|
|
351
351
|
*/
|
|
352
352
|
export function homepageTitle(fm, config) {
|
|
353
|
-
const authored = fm?.name?.full
|
|
353
|
+
const authored = fm?.name?.full;
|
|
354
354
|
if (typeof authored === "string" && authored.trim()) return authored;
|
|
355
355
|
const manifest = /** @type {Record<string, unknown>|undefined} */ (
|
|
356
356
|
config?.packageBuild?.manifest
|
package/engine/index.mjs
CHANGED
|
@@ -220,6 +220,9 @@ export * as contentHtml from "./content-html.mjs";
|
|
|
220
220
|
/** An image saying how wide it is and where it sits, in two closed vocabularies. */
|
|
221
221
|
export * as contentImages from "./content-images.mjs";
|
|
222
222
|
|
|
223
|
+
/** A list item opened with `[ ]` or `[x]`, which every surface reads as text. */
|
|
224
|
+
export * as contentTaskLists from "./content-tasklists.mjs";
|
|
225
|
+
|
|
223
226
|
/** `![[…]]` — the wikilink that renders a picture where it stands. */
|
|
224
227
|
export * as contentEmbeds from "./content-embeds.mjs";
|
|
225
228
|
|
|
@@ -112,11 +112,13 @@ export function compilesSystemDocument(fm, map, router) {
|
|
|
112
112
|
* @param {object} [options.router] - The pack router deciding which system
|
|
113
113
|
* compiles a document for this note. Defaults to the consuming repository's.
|
|
114
114
|
* @param {object} [options.dates] - Calendar conversion context.
|
|
115
|
+
* @param {string} [options.contentPackage] - This build's content package,
|
|
116
|
+
* the default a note box's short-form Address resolves against.
|
|
115
117
|
* @returns {object[]} The boxes, in the order every medium renders them.
|
|
116
118
|
* @throws {Error} When the built set disagrees with what the note's type maps
|
|
117
119
|
* to — see {@link module:engine/infobox.assertInfoboxSet}.
|
|
118
120
|
*/
|
|
119
|
-
export function noteInfoboxes(fm, { resolve, router = packRouter(), dates } = {}) {
|
|
121
|
+
export function noteInfoboxes(fm, { resolve, router = packRouter(), dates, contentPackage } = {}) {
|
|
120
122
|
const boxes = buildInfoboxes(fm, {
|
|
121
123
|
maps: KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
122
124
|
providers: KNOWN_INFOBOXES,
|
|
@@ -124,6 +126,7 @@ export function noteInfoboxes(fm, { resolve, router = packRouter(), dates } = {}
|
|
|
124
126
|
resolveField: resolveFieldValue,
|
|
125
127
|
resolve,
|
|
126
128
|
dates,
|
|
129
|
+
contentPackage,
|
|
127
130
|
});
|
|
128
131
|
return /** @type {object[]} */ (
|
|
129
132
|
assertInfoboxSet(boxes, fm, { maps: KNOWN_DOCUMENT_SUBTYPE_MAPS })
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { encodeAddresses } from "./address-values.mjs";
|
|
38
38
|
import { sectionHolds } from "./infobox.mjs";
|
|
39
|
-
import { escapeTypst, escapeTypstString } from "./pdf-render.mjs";
|
|
39
|
+
import { absolutePageUrl, escapeTypst, escapeTypstString } from "./pdf-render.mjs";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* How many cells an attribute grid packs into a row.
|
|
@@ -226,16 +226,22 @@ export function sectionHasContent(section) {
|
|
|
226
226
|
* reference, so the link works on paper as a cross-reference and in a PDF
|
|
227
227
|
* viewer as a jump. Everything else is set as its own words.
|
|
228
228
|
*
|
|
229
|
+
* A panel's rows address pages the same way prose does, so a page the book does
|
|
230
|
+
* not print is set as the absolute URL a reader can follow — see
|
|
231
|
+
* {@link module:engine/pdf-render.absolutePageUrl}.
|
|
232
|
+
*
|
|
229
233
|
* @param {object} value - A `link` value.
|
|
230
234
|
* @param {Map<string, string>} links - Address slug → the book's anchor.
|
|
231
235
|
* @param {(anchor: string) => string} labelFor - The anchor's Typst label.
|
|
236
|
+
* @param {string} [site] - The absolute address this book's pages are served at.
|
|
232
237
|
* @returns {string} Typst markup.
|
|
233
238
|
*/
|
|
234
|
-
export function linkToTypst(value, links, labelFor) {
|
|
239
|
+
export function linkToTypst(value, links, labelFor, site) {
|
|
235
240
|
const text = escapeTypst(value?.text ?? "");
|
|
236
241
|
const anchor = value?.address ? links?.get(encodeAddresses(value.address)) : undefined;
|
|
237
242
|
if (anchor) return `#link(<${labelFor(anchor)}>)[${text}]`;
|
|
238
|
-
if (value?.url)
|
|
243
|
+
if (value?.url)
|
|
244
|
+
return `#link("${escapeTypstString(absolutePageUrl(value.url, site))}")[${text}]`;
|
|
239
245
|
return text;
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -375,7 +381,13 @@ export function infoboxTypstPreamble() {
|
|
|
375
381
|
"fill: infobox-rule)[#upper(t)]; v(0.20em) }",
|
|
376
382
|
'#let infobox-head(t) = { v(0.40em); text(size: 7.6pt, weight: "bold", ' +
|
|
377
383
|
"tracking: 1.3pt, fill: infobox-rule)[#upper(t)]; v(0.16em) }",
|
|
384
|
+
// One definition-list style across the three surfaces: the term right
|
|
385
|
+
// against a fixed column so every term in the panel shares one edge,
|
|
386
|
+
// and space below the row rather than rows run together. A page has one
|
|
387
|
+
// width, so the stacked state the screen surfaces fall back to has
|
|
388
|
+
// nothing to answer here and is not drawn.
|
|
378
389
|
"#let infobox-row(k, v) = grid(columns: (2.05cm, 1fr), column-gutter: 4pt, " +
|
|
390
|
+
"inset: (bottom: 2pt), align: (right + top, left + top), " +
|
|
379
391
|
"text(size: 7pt, tracking: 0.5pt, fill: infobox-faint)[#upper(k)], text(size: 8pt)[#v])",
|
|
380
392
|
"#let infobox-cell(lab, val) = align(center)[" +
|
|
381
393
|
"#text(size: 7pt, tracking: 0.6pt, fill: infobox-faint)[#lab]#h(2.5pt)" +
|
package/engine/infobox.mjs
CHANGED
|
@@ -119,9 +119,20 @@ import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
|
|
|
119
119
|
import { readAliasedField } from "./retired-fields.mjs";
|
|
120
120
|
import { subtypeRow } from "./document-subtypes.mjs";
|
|
121
121
|
import { authoredKey } from "./system-block.mjs";
|
|
122
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
formatDateInCalendar,
|
|
124
|
+
formatNoteDate,
|
|
125
|
+
parseNoteDate,
|
|
126
|
+
resolvedDateFields,
|
|
127
|
+
} from "./note-dates.mjs";
|
|
123
128
|
import { displayBeingHeight, displayBeingWeight } from "./being-measurements.mjs";
|
|
124
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
officeAnchor,
|
|
131
|
+
officeRoster,
|
|
132
|
+
rankAnchor,
|
|
133
|
+
readStandings,
|
|
134
|
+
standingPhrase,
|
|
135
|
+
} from "./standings.mjs";
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
138
|
* How a section arranges what it holds.
|
|
@@ -284,7 +295,7 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
284
295
|
bgImage: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
285
296
|
banner: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
286
297
|
overlay: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
287
|
-
"lore.
|
|
298
|
+
"lore.events": Object.freeze({ withheld: "chronology machinery, not a summary row" }),
|
|
288
299
|
|
|
289
300
|
assocSkill: Object.freeze({ label: "Skill" }),
|
|
290
301
|
assocAffiliation: Object.freeze({ label: "Affiliation" }),
|
|
@@ -327,7 +338,8 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
327
338
|
}),
|
|
328
339
|
"being.appearance.complexion": Object.freeze({
|
|
329
340
|
group: "appearance",
|
|
330
|
-
|
|
341
|
+
whole: true,
|
|
342
|
+
phrase: (v) => `${humanizedList(v)} complexion`,
|
|
331
343
|
}),
|
|
332
344
|
"being.appearance.extra_features": Object.freeze({ group: "appearance" }),
|
|
333
345
|
});
|
|
@@ -469,6 +481,24 @@ export function humanizeValue(value) {
|
|
|
469
481
|
.trim();
|
|
470
482
|
}
|
|
471
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Several enumerated values as one clause — `weathered, ruddy and scarred`.
|
|
486
|
+
*
|
|
487
|
+
* A field that admits one value or a list of them reads as a single phrase, so
|
|
488
|
+
* the noun it qualifies is said once rather than once per value. Internal: the
|
|
489
|
+
* phrase that wants it is declared in this module.
|
|
490
|
+
*
|
|
491
|
+
* @param {unknown} value - One value, or a list of them.
|
|
492
|
+
* @returns {string} The text.
|
|
493
|
+
*/
|
|
494
|
+
function humanizedList(value) {
|
|
495
|
+
const parts = (Array.isArray(value) ? value : [value])
|
|
496
|
+
.filter((entry) => entry !== undefined && entry !== null && entry !== "")
|
|
497
|
+
.map((entry) => humanizeValue(entry));
|
|
498
|
+
if (parts.length < 2) return parts[0] ?? "";
|
|
499
|
+
return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
472
502
|
/**
|
|
473
503
|
* A value as a row shows it.
|
|
474
504
|
*
|
|
@@ -633,16 +663,71 @@ function rosterRows(raw) {
|
|
|
633
663
|
return rows;
|
|
634
664
|
}
|
|
635
665
|
|
|
636
|
-
/**
|
|
637
|
-
|
|
666
|
+
/**
|
|
667
|
+
* A ladder of named ranks, one row each, ordered by the level a body confers.
|
|
668
|
+
*
|
|
669
|
+
* The **title is the label and its description is the value**, the same
|
|
670
|
+
* convention {@link rosterRows} draws a body's offices with, so a reader
|
|
671
|
+
* meets the two lists the same way. A rung's optional `lore` — already
|
|
672
|
+
* resolved to an Address by the time this runs — reads alongside the
|
|
673
|
+
* description rather than replacing it, since the description is what the
|
|
674
|
+
* rung itself states and the lore is a pointer to more.
|
|
675
|
+
*
|
|
676
|
+
* @param {unknown} raw - The authored `governance.ranks` list.
|
|
677
|
+
* @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
|
|
678
|
+
* medium's resolver.
|
|
679
|
+
* @returns {object[]|null} One row per rung, in ascending level, or `null`
|
|
680
|
+
* where the value is not a list of them.
|
|
681
|
+
*/
|
|
682
|
+
function rankRows(raw, resolve) {
|
|
683
|
+
if (!Array.isArray(raw)) return null;
|
|
684
|
+
const rungs = raw
|
|
685
|
+
.filter((rung) => isMapping(rung) && hasValue(rung.title))
|
|
686
|
+
.toSorted((a, b) => Number(a.level ?? 0) - Number(b.level ?? 0));
|
|
687
|
+
const rows = [];
|
|
688
|
+
for (const rung of rungs) {
|
|
689
|
+
const anchor = rankAnchor(String(rung.title));
|
|
690
|
+
const description =
|
|
691
|
+
hasValue(rung.description) ? String(rung.description) : String(rung.title);
|
|
692
|
+
const lore = hasValue(rung.lore) ? linkValue(rung.lore, resolve, { type: "lore" }) : null;
|
|
693
|
+
rows.push({
|
|
694
|
+
...(anchor ? { id: anchor } : {}),
|
|
695
|
+
label: String(rung.title),
|
|
696
|
+
kind: "text",
|
|
697
|
+
value: lore?.text ? `${description} (Lore: ${lore.text})` : description,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return rows;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Expand structured references using the existing linked-row shape.
|
|
705
|
+
*
|
|
706
|
+
* @param {string} [contentPackage] - This build's content package, the
|
|
707
|
+
* default a short-form Address resolves against — the same value the site
|
|
708
|
+
* build and the pack compiler read `contentPackage()` for. A row naming a
|
|
709
|
+
* short Address with none handed to it skips rather than guesses.
|
|
710
|
+
*/
|
|
711
|
+
function structuredRows(field, raw, resolve, label, contentPackage) {
|
|
638
712
|
if (field.standings) return standingRows(field, raw, resolve, label);
|
|
639
713
|
if (field.roster) {
|
|
640
714
|
const roster = rosterRows(raw);
|
|
641
715
|
if (roster) return roster;
|
|
642
716
|
}
|
|
643
|
-
if (
|
|
644
|
-
const
|
|
645
|
-
|
|
717
|
+
if (field.ranks) {
|
|
718
|
+
const ranks = rankRows(raw, resolve);
|
|
719
|
+
if (ranks) return ranks;
|
|
720
|
+
}
|
|
721
|
+
// A relationship entry — `{to, ...}` — is a mapping that is not itself an
|
|
722
|
+
// Address: an authored plain list of addresses normalises to a list of
|
|
723
|
+
// Address tuples, which `isMapping` also reports true for, and treating
|
|
724
|
+
// one as a relationship keyed off a `to` it never carries is what emptied
|
|
725
|
+
// `lore`, `parents`, `domains` and `economy` silently.
|
|
726
|
+
const relations =
|
|
727
|
+
Array.isArray(raw) ? raw.filter((entry) => isMapping(entry) && !isAddressTuple(entry)) : [];
|
|
728
|
+
if (relations.length) {
|
|
729
|
+
const links = relations
|
|
730
|
+
.filter((entry) => hasValue(entry.to))
|
|
646
731
|
.map((entry) => {
|
|
647
732
|
const link = linkValue(entry.to, resolve, { type: field.ref ?? "place" });
|
|
648
733
|
const details = Object.entries(entry)
|
|
@@ -666,7 +751,7 @@ function structuredRows(field, raw, resolve, label, fm) {
|
|
|
666
751
|
const address = parseAddress(
|
|
667
752
|
target,
|
|
668
753
|
{
|
|
669
|
-
package:
|
|
754
|
+
package: contentPackage,
|
|
670
755
|
system: "note",
|
|
671
756
|
types: new Set(field.accepts),
|
|
672
757
|
},
|
|
@@ -759,6 +844,8 @@ export function linkValue(ref, resolve, hint) {
|
|
|
759
844
|
* @param {object} [options] - Options.
|
|
760
845
|
* @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
|
|
761
846
|
* reference to `{name, url?, uuid?, address?}`.
|
|
847
|
+
* @param {string} [options.contentPackage] - This build's content package,
|
|
848
|
+
* the default a relation row's short-form Address resolves against.
|
|
762
849
|
* @param {object} [options.vocabulary] - The note vocabulary to read.
|
|
763
850
|
* @param {object} [options.presentation] - The overlay to read.
|
|
764
851
|
* @returns {object} The box.
|
|
@@ -784,12 +871,18 @@ export function noteInfobox(fm, options = {}) {
|
|
|
784
871
|
*/
|
|
785
872
|
function noteBox(
|
|
786
873
|
fm,
|
|
787
|
-
{
|
|
874
|
+
{
|
|
875
|
+
resolve,
|
|
876
|
+
dates,
|
|
877
|
+
contentPackage,
|
|
878
|
+
vocabulary = NOTE_VOCABULARY,
|
|
879
|
+
presentation = NOTE_FIELD_PRESENTATION,
|
|
880
|
+
} = {},
|
|
788
881
|
) {
|
|
789
882
|
const rows = [];
|
|
790
883
|
/** @type {Set<string>} */
|
|
791
884
|
const shown = new Set();
|
|
792
|
-
const name = fm?.name?.full
|
|
885
|
+
const name = fm?.name?.full;
|
|
793
886
|
if (hasValue(name)) rows.push({ label: "Name", kind: "text", value: String(name) });
|
|
794
887
|
if (Array.isArray(fm?.name?.aliases) && fm.name.aliases.length) {
|
|
795
888
|
rows.push({ label: "Aliases", kind: "list", value: fm.name.aliases });
|
|
@@ -843,7 +936,7 @@ function noteBox(
|
|
|
843
936
|
raw,
|
|
844
937
|
resolve,
|
|
845
938
|
overlay.label ?? humanizeFieldName(field.name),
|
|
846
|
-
|
|
939
|
+
contentPackage,
|
|
847
940
|
);
|
|
848
941
|
if (structured) {
|
|
849
942
|
rows.push(...structured);
|
|
@@ -857,7 +950,9 @@ function noteBox(
|
|
|
857
950
|
label: GROUP_LABELS[overlay.group] ?? humanizeFieldName(overlay.group),
|
|
858
951
|
entries: [],
|
|
859
952
|
};
|
|
860
|
-
|
|
953
|
+
// A `whole` phrase reads the value entire, so several values compose
|
|
954
|
+
// into one clause rather than repeating the noun once each.
|
|
955
|
+
const parts = overlay.whole || !Array.isArray(raw) ? [raw] : raw.filter(hasValue);
|
|
861
956
|
for (const part of parts) {
|
|
862
957
|
group.entries.push(overlay.phrase ? overlay.phrase(part) : humanizeValue(part));
|
|
863
958
|
}
|
|
@@ -879,6 +974,22 @@ function noteBox(
|
|
|
879
974
|
shown.add(field.name);
|
|
880
975
|
}
|
|
881
976
|
|
|
977
|
+
// `events` itself stays withheld — the family's shape is chronology
|
|
978
|
+
// machinery, not a summary row — but a reader meets each occurrence's
|
|
979
|
+
// own next date, the way a being's computed `age` reaches the box.
|
|
980
|
+
if (fm?.type === "lore" && Array.isArray(data.events) && data.events.length && dates) {
|
|
981
|
+
const resolved = resolvedDateFields(fm, dates);
|
|
982
|
+
for (const eventEntry of resolved.events ?? []) {
|
|
983
|
+
if (!eventEntry.next) continue;
|
|
984
|
+
rows.push({
|
|
985
|
+
label: "Next occurrence",
|
|
986
|
+
kind: "text",
|
|
987
|
+
value: eventEntry.next.prose ?? eventEntry.next.text,
|
|
988
|
+
});
|
|
989
|
+
shown.add("events");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
882
993
|
return {
|
|
883
994
|
box: {
|
|
884
995
|
id: NOTE_BOX_ID,
|
|
@@ -1061,6 +1172,8 @@ export function systemRowsSection(
|
|
|
1061
1172
|
* Resolves one declared field against the note.
|
|
1062
1173
|
* @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
|
|
1063
1174
|
* reference to `{name, url?, uuid?, address?}`.
|
|
1175
|
+
* @param {string} [options.contentPackage] - This build's content package,
|
|
1176
|
+
* the default a note box's short-form Address resolves against.
|
|
1064
1177
|
* @param {object} [options.vocabulary] - The note vocabulary to read.
|
|
1065
1178
|
* @returns {object[]} The boxes.
|
|
1066
1179
|
*/
|
|
@@ -1071,11 +1184,12 @@ export function buildInfoboxes(fm, options) {
|
|
|
1071
1184
|
compilesDocument,
|
|
1072
1185
|
resolveField,
|
|
1073
1186
|
resolve,
|
|
1187
|
+
contentPackage,
|
|
1074
1188
|
vocabulary = NOTE_VOCABULARY,
|
|
1075
1189
|
dates,
|
|
1076
1190
|
} = options;
|
|
1077
1191
|
|
|
1078
|
-
const { box: note, shown: taken } = noteBox(fm, { resolve, vocabulary, dates });
|
|
1192
|
+
const { box: note, shown: taken } = noteBox(fm, { resolve, vocabulary, dates, contentPackage });
|
|
1079
1193
|
const boxes = [note];
|
|
1080
1194
|
|
|
1081
1195
|
for (const map of maps ?? []) {
|
|
@@ -15,18 +15,10 @@ import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
|
15
15
|
import { authoredFrontmatter, isNoteRecord } from "./index-records.mjs";
|
|
16
16
|
import { buildScene } from "./map-notes.mjs";
|
|
17
17
|
import { buildJournalEntry } from "./journals.mjs";
|
|
18
|
+
import { imageDimensions } from "./asset-types.mjs";
|
|
18
19
|
import { makeId } from "./ids.mjs";
|
|
19
20
|
import { stripAdventureKeys } from "./bundle-notes.mjs";
|
|
20
21
|
|
|
21
|
-
/** The PNG's dimensions, from its fixed header. */
|
|
22
|
-
function pngDimensions(file) {
|
|
23
|
-
const bytes = fs.readFileSync(file);
|
|
24
|
-
if (bytes.toString("hex", 0, 8) !== "89504e470d0a1a0a") {
|
|
25
|
-
throw new Error(`${file} is not a PNG`);
|
|
26
|
-
}
|
|
27
|
-
return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
22
|
/** GraphViz's plain coordinates, converted to pixel positions on its PNG. */
|
|
31
23
|
export function pinPositions(plain, width, height) {
|
|
32
24
|
const graph = /^graph\s+\S+\s+(\S+)\s+(\S+)/m.exec(plain);
|
|
@@ -111,7 +103,9 @@ export function buildItineraryScenes({
|
|
|
111
103
|
const plain = path.join(outDir, `from-${centre}.plain`);
|
|
112
104
|
renderDot(dot, png, { engine: "neato", format: "png", nop: 2, dpi: 150 });
|
|
113
105
|
renderDot(dot, plain, { engine: "neato", format: "plain", nop: 2 });
|
|
114
|
-
const
|
|
106
|
+
const size = imageDimensions(png, ".png");
|
|
107
|
+
if (!size) throw new Error(`${png} carries no readable PNG size`);
|
|
108
|
+
const { width, height } = size;
|
|
115
109
|
const positions = pinPositions(fs.readFileSync(plain, "utf8"), width, height);
|
|
116
110
|
const sceneId = makeId("itinerary-scene", `${config.contentPackage}:${centre}`);
|
|
117
111
|
const src = `${buildConfig.packageKind}/${buildConfig.packageId}/assets/maps/generated-itineraries/from-${centre}.png`;
|