@heroiclands/package-build 22.15.0 → 22.16.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 +103 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/itinerary-scenes.mjs +4 -2
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/map-raster.mjs +4 -2
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
package/engine/helpers.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Shared helpers for the pack compilers in `packages/
|
|
15
|
+
* Shared helpers for the pack compilers in `packages/package-build/`.
|
|
16
16
|
*
|
|
17
17
|
* The HeroicLands vault is authoritative for compendium item data. Pack
|
|
18
18
|
* compilers walk the vault, read markdown files with YAML frontmatter, and
|
|
@@ -37,6 +37,8 @@ import path from "path";
|
|
|
37
37
|
import yaml from "yaml";
|
|
38
38
|
import unidecode from "unidecode";
|
|
39
39
|
import markdownit from "markdown-it";
|
|
40
|
+
import footnotePlugin from "markdown-it-footnote";
|
|
41
|
+
import deflistPlugin from "markdown-it-deflist";
|
|
40
42
|
import { iconPlugin } from "./content-icons.mjs";
|
|
41
43
|
import { imagePlugin, imagesIn } from "./content-images.mjs";
|
|
42
44
|
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
@@ -46,7 +48,6 @@ import log from "loglevel";
|
|
|
46
48
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
47
49
|
import { declaresNoPack, packRouter } from "./pack-router.mjs";
|
|
48
50
|
import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
49
|
-
import { searchableFrontmatter } from "./note-package.mjs";
|
|
50
51
|
import { PACKAGE_BASE } from "./content-address.mjs";
|
|
51
52
|
import { resolveNoteId } from "./note-ids.mjs";
|
|
52
53
|
import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
@@ -61,8 +62,11 @@ import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
|
61
62
|
// The declared tag vocabulary, which is where `draft` is stated. Read
|
|
62
63
|
// from there rather than respelt, so the tag and its one reader cannot drift.
|
|
63
64
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
65
|
+
import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
|
|
64
66
|
import { expandContentTables } from "./content-tables.mjs";
|
|
65
67
|
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
68
|
+
import { renderAdmonitions } from "./content-admonitions.mjs";
|
|
69
|
+
import { renderCaptionBlocks, scanCaptions } from "./content-captions.mjs";
|
|
66
70
|
import { positionInBody } from "./diagnostics.mjs";
|
|
67
71
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
68
72
|
// registry can import them without reaching back through this one.
|
|
@@ -88,6 +92,8 @@ export {
|
|
|
88
92
|
* the two HTML surfaces and be silently dropped by the third.
|
|
89
93
|
*/
|
|
90
94
|
export const md = markdownit({ html: true })
|
|
95
|
+
.use(footnotePlugin)
|
|
96
|
+
.use(deflistPlugin)
|
|
91
97
|
.use(
|
|
92
98
|
// Resolved per render, not at import: this constant is built before any
|
|
93
99
|
// configuration is read, and a package's own icons live in the
|
|
@@ -126,10 +132,35 @@ export const md = markdownit({ html: true })
|
|
|
126
132
|
}),
|
|
127
133
|
);
|
|
128
134
|
|
|
135
|
+
md.renderer.rules.footnote_block_open = () =>
|
|
136
|
+
'<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
|
|
137
|
+
md.renderer.rules.footnote_caption = (tokens, idx, _options, env) => {
|
|
138
|
+
const meta = tokens[idx].meta;
|
|
139
|
+
const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
|
|
140
|
+
return `[${number}${meta.subId > 0 ? `:${meta.subId}` : ""}]`;
|
|
141
|
+
};
|
|
142
|
+
md.renderer.rules.footnote_anchor_name = (tokens, idx, _options, env) => {
|
|
143
|
+
const meta = tokens[idx].meta;
|
|
144
|
+
const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
|
|
145
|
+
return `${env.docId ? `-${env.docId}-` : ""}${number}`;
|
|
146
|
+
};
|
|
147
|
+
md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
|
|
148
|
+
const meta = tokens[idx].meta;
|
|
149
|
+
const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
|
|
150
|
+
const id = renderer.rules.footnote_anchor_name(tokens, idx, options, env, renderer);
|
|
151
|
+
return `<li id="fn${id}" class="footnote-item" value="${number}">`;
|
|
152
|
+
};
|
|
153
|
+
|
|
129
154
|
/** Render a note body with Foundry's secret section markup. */
|
|
130
|
-
export function renderFoundryMarkdown(body) {
|
|
155
|
+
export function renderFoundryMarkdown(body, captions, footnoteNumbers, docId) {
|
|
131
156
|
const { markdown } = renderSecretBlocks(body, "foundry", (inner) => md.render(inner));
|
|
132
|
-
|
|
157
|
+
const admonitions = renderAdmonitions(markdown, (inner) => md.render(inner));
|
|
158
|
+
const captioned = renderCaptionBlocks(
|
|
159
|
+
admonitions.markdown,
|
|
160
|
+
(block) => md.render(block),
|
|
161
|
+
captions,
|
|
162
|
+
);
|
|
163
|
+
return md.render(captioned.markdown, { footnoteNumbers, docId });
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
/**
|
|
@@ -143,6 +174,10 @@ export function renderFoundryMarkdown(body) {
|
|
|
143
174
|
* {@link positionInBody}. If the file has no frontmatter block, returns
|
|
144
175
|
* `{ frontmatter: null, body: "", description: "" }` with a warn log, and no
|
|
145
176
|
* position: there is no body to have one.
|
|
177
|
+
* @param {string} filePath - Markdown file to read.
|
|
178
|
+
* @param {{addressContext?: object}} [opts] - Address defaults for decoded fields.
|
|
179
|
+
* @returns {{frontmatter: object|null, body: string, description: string,
|
|
180
|
+
* bodyLine?: number, bodyColumn?: number}} Parsed note and body position.
|
|
146
181
|
*/
|
|
147
182
|
export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
148
183
|
const content = fs.readFileSync(filePath, "utf8");
|
|
@@ -257,6 +292,7 @@ export function assertSuppliedCorpus(records, who) {
|
|
|
257
292
|
* @param {readonly string[]} opts.skipDirectories - Directory names to ignore.
|
|
258
293
|
* Required: the scope is the caller's to state, so two passes cannot
|
|
259
294
|
* disagree about which files are the corpus.
|
|
295
|
+
* @param {object} [opts.addressContext] - Address resolution context.
|
|
260
296
|
* @yields {{frontmatter: object|null, body: string, description: string,
|
|
261
297
|
* file: string, absPath: string, bodyLine?: number, bodyColumn?: number}}
|
|
262
298
|
* One entry per `.md` file found.
|
|
@@ -470,6 +506,9 @@ export function systemTemplatePriority(fm, label) {
|
|
|
470
506
|
/**
|
|
471
507
|
* Generates a compendium-source filename: `Name_id.json` with non-
|
|
472
508
|
* alphanumeric runs replaced by underscores.
|
|
509
|
+
* @param {string} name - Document name.
|
|
510
|
+
* @param {string} id - Document id.
|
|
511
|
+
* @returns {string} Filename for the document.
|
|
473
512
|
*/
|
|
474
513
|
export function makeFilename(name, id) {
|
|
475
514
|
return `${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
|
|
@@ -531,6 +570,9 @@ export function resolveImg(raw, config = loadPackConfig()) {
|
|
|
531
570
|
/**
|
|
532
571
|
* Resolves the display name from frontmatter, preferring `name.full`,
|
|
533
572
|
* falling back to `name` (if string), then `defaultValue`.
|
|
573
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
574
|
+
* @param {string} [defaultValue] - Name when the note provides none.
|
|
575
|
+
* @returns {string} Display name.
|
|
534
576
|
*/
|
|
535
577
|
export function resolveName(fm, defaultValue = "Unnamed") {
|
|
536
578
|
const fullName = getFrontmatter(fm, "name.full", null);
|
|
@@ -707,20 +749,16 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
707
749
|
* the index and the compile agree about where each note landed; defaults to
|
|
708
750
|
* this repository's own.
|
|
709
751
|
* @param {object} [opts]
|
|
710
|
-
* @param {readonly string[]} [opts.skipDirectories] - Part of the options bag
|
|
711
|
-
* every corpus reader takes; the scope is already settled by `records`.
|
|
712
752
|
* @param {object} [opts.config] - The resolved build configuration; loaded when
|
|
713
753
|
* omitted.
|
|
714
754
|
* @param {readonly object[]} [opts.records] - The corpus, derived once per
|
|
715
755
|
* compile and handed in. Required: see {@link assertSuppliedCorpus}.
|
|
716
|
-
* @param {object[]} [opts.problems] - Part of the same options bag; the notes
|
|
717
|
-
* the index cannot record are collected where the corpus is derived.
|
|
718
756
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
719
757
|
*/
|
|
720
758
|
export function buildContentLinkIndex(
|
|
721
759
|
contentBase,
|
|
722
760
|
router = packRouter(),
|
|
723
|
-
{
|
|
761
|
+
{ config, records } = {},
|
|
724
762
|
) {
|
|
725
763
|
const docs = [];
|
|
726
764
|
/** The files this package ships, by canonical address. */
|
|
@@ -764,17 +802,21 @@ export function buildContentLinkIndex(
|
|
|
764
802
|
assertTypeNotRetired(fm.type, absPath);
|
|
765
803
|
const base = String(record.file.name).replace(/_/g, " ");
|
|
766
804
|
const documentation = recordsByAddress.get(encodeAddresses(record.documentation));
|
|
805
|
+
const noOwnDocument =
|
|
806
|
+
declaresNoPack(fm) ||
|
|
807
|
+
NEVER_PACKED_TYPES.has(fm.type) ||
|
|
808
|
+
DERIVED_PACKED_TYPES.has(fm.type);
|
|
767
809
|
docs.push({
|
|
768
810
|
documentationUuid: documentation?.foundry?.none?.uuid ?? null,
|
|
769
811
|
type: fm.type,
|
|
770
812
|
id: fm.id,
|
|
771
813
|
// Where this note's own document lands, and where the JournalEntry
|
|
772
814
|
// its prose compiles into lands — two documents, two packs.
|
|
773
|
-
pack: router.resolveOrNull(fm, packForType(fm.type).docType),
|
|
774
|
-
docPack: router.resolveOrNull(fm, "JournalEntry"),
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
none:
|
|
815
|
+
pack: noOwnDocument ? null : router.resolveOrNull(fm, packForType(fm.type).docType),
|
|
816
|
+
docPack: noOwnDocument ? null : router.resolveOrNull(fm, "JournalEntry"),
|
|
817
|
+
// A note with no single document still has an address, but no
|
|
818
|
+
// compendium UUID for a wikilink to open.
|
|
819
|
+
none: noOwnDocument,
|
|
778
820
|
shortcode: fm.shortcode ?? null,
|
|
779
821
|
// What the note *is*, carried so a caller resolving a reference
|
|
780
822
|
// can group by the family its target declares rather than only by
|
|
@@ -812,7 +854,7 @@ export function buildContentLinkIndex(
|
|
|
812
854
|
}
|
|
813
855
|
throw new Error(
|
|
814
856
|
"Cross-package links cannot be resolved from an unusable index; " +
|
|
815
|
-
"re-run `
|
|
857
|
+
"re-run `package-build deps fetch`.",
|
|
816
858
|
);
|
|
817
859
|
}
|
|
818
860
|
log.debug(
|
|
@@ -883,6 +925,9 @@ export function convertNoteWikilinks(
|
|
|
883
925
|
pack,
|
|
884
926
|
docPack,
|
|
885
927
|
index,
|
|
928
|
+
captionLabels: new Map(
|
|
929
|
+
scanCaptions(source).captions.map((caption) => [caption.id, caption.label]),
|
|
930
|
+
),
|
|
886
931
|
});
|
|
887
932
|
/**
|
|
888
933
|
* Where one unresolved link sits, in file coordinates.
|
|
@@ -973,115 +1018,21 @@ export function convertNoteWikilinks(
|
|
|
973
1018
|
/* ------------------------------------------------------------------------ */
|
|
974
1019
|
|
|
975
1020
|
/**
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
* @param {
|
|
981
|
-
* @param {object} [
|
|
982
|
-
* @
|
|
983
|
-
* every corpus reader takes; the scope is already settled by `records`.
|
|
984
|
-
* @param {object} [opts.config] - The resolved build configuration; loaded when
|
|
985
|
-
* omitted.
|
|
986
|
-
* @param {readonly object[]} [opts.records] - The corpus, derived once per
|
|
987
|
-
* compile and handed in. Required: see {@link assertSuppliedCorpus}.
|
|
988
|
-
* @param {object[]} [opts.problems] - Part of the same options bag; the notes
|
|
989
|
-
* the walk cannot read are collected where the corpus is derived.
|
|
990
|
-
* @returns {Array<{fm: object, path: string, tld: string, folder: string,
|
|
991
|
-
* absPath: string}>}
|
|
992
|
-
*/
|
|
993
|
-
export function collectContentDocs(
|
|
994
|
-
contentBase,
|
|
995
|
-
{ skipDirectories, config, records, problems } = {},
|
|
996
|
-
) {
|
|
997
|
-
const docs = [];
|
|
998
|
-
const resolved = config ?? loadPackConfig();
|
|
999
|
-
assertSuppliedCorpus(records, "collectContentDocs");
|
|
1000
|
-
for (const record of records) {
|
|
1001
|
-
if (!isNoteRecord(record)) continue;
|
|
1002
|
-
const fm = authoredFrontmatter(record);
|
|
1003
|
-
const absPath = noteFile(contentBase, record);
|
|
1004
|
-
const segments = String(record.file.path).split("/");
|
|
1005
|
-
docs.push({
|
|
1006
|
-
// With its package supplied for a `WHERE … package = "…"` query —
|
|
1007
|
-
// synthesised from the configuration this build resolved, since no
|
|
1008
|
-
// note declares it and the ambient one is a different
|
|
1009
|
-
// configuration in a worktree or under `PACKAGE_BUILD_CONFIG`.
|
|
1010
|
-
fm: searchableFrontmatter(fm, resolved.contentPackage),
|
|
1011
|
-
// POSIX-separated and relative to the content root — what a
|
|
1012
|
-
// `path:` search term globs, on every platform.
|
|
1013
|
-
path: segments.join("/"),
|
|
1014
|
-
tld: segments[0],
|
|
1015
|
-
folder: segments[segments.length - 2] ?? segments[0],
|
|
1016
|
-
absPath,
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
docs.sort((a, b) =>
|
|
1020
|
-
a.absPath < b.absPath ? -1
|
|
1021
|
-
: a.absPath > b.absPath ? 1
|
|
1022
|
-
: 0,
|
|
1023
|
-
);
|
|
1024
|
-
log.debug(`Content table index: ${docs.length} searchable note(s)`);
|
|
1025
|
-
return docs;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* A note is linkable from a generated table cell when it carries the identity
|
|
1030
|
-
* {@link convertWikilinks} addresses it by — a `type` and a `shortcode`. Every
|
|
1031
|
-
* type routes to a pack ({@link packForType}), so nothing else can make a note
|
|
1032
|
-
* unlinkable; a note missing either renders as plain text rather than shipping a
|
|
1033
|
-
* literal wikilink into a journal.
|
|
1034
|
-
*/
|
|
1035
|
-
const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type);
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Expand the fenced `dataview` tables in one note's markdown, before wikilinks
|
|
1039
|
-
* are resolved — so a generated cell may itself be a wikilink.
|
|
1040
|
-
*
|
|
1041
|
-
* A table searches the whole tree, which is one package's notes and nothing
|
|
1042
|
-
* else — so there is no longer a package to scope on. It used to filter, back
|
|
1043
|
-
* when a tree could hold several packages' notes and `package:` said which was
|
|
1044
|
-
* which; that field is retired and the filter with it.
|
|
1045
|
-
*
|
|
1046
|
-
* @param {string} body - The note's markdown body.
|
|
1047
|
-
* @param {object} ctx
|
|
1048
|
-
* @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
|
|
1049
|
-
* @param {string} ctx.name - The note, for the error message.
|
|
1050
|
-
* @param {object} [ctx.fm] - The source note's frontmatter, which is what a
|
|
1051
|
-
* query's `this` reads. Its entry in `docs` supplies the path as well.
|
|
1052
|
-
* @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
|
|
1053
|
-
* so a failing directive can be reported at its position in the file.
|
|
1054
|
-
* @param {object[]} [ctx.sqlTables] - This note's prepared `sql` results, in
|
|
1055
|
-
* document order, from
|
|
1056
|
-
* {@link module:engine/sql-tables.prepareSqlTables}. An `sql` directive with
|
|
1057
|
-
* no prepared result fails the note: nothing here runs a query.
|
|
1058
|
-
* @returns {{markdown: string, lineMap: Array<{line: number,
|
|
1059
|
-
* generated: boolean}>}} The body with every table expanded, and where each
|
|
1060
|
-
* emitted line came from — which is what lets a diagnostic about the
|
|
1061
|
-
* expanded body name an authored position.
|
|
1062
|
-
* @throws {Error} When a query is malformed or unsupported — the note fails to
|
|
1063
|
-
* compile rather than shipping a table-shaped hole. The error carries
|
|
1064
|
-
* `position`, the directive's own line.
|
|
1021
|
+
* Expand prepared SQL tables before resolving wikilinks in generated cells.
|
|
1022
|
+
* @param {string} body - The note body.
|
|
1023
|
+
* @param {object} ctx - Source and prepared query results.
|
|
1024
|
+
* @param {string} ctx.name - Name used in diagnostics.
|
|
1025
|
+
* @param {number} [ctx.bodyLine] - The first body line in the source file.
|
|
1026
|
+
* @param {object[]} [ctx.sqlTables] - Prepared SQL results in document order.
|
|
1027
|
+
* @returns {{markdown: string, lineMap: Array<{line: number, generated: boolean}>}}
|
|
1065
1028
|
*/
|
|
1066
|
-
export function expandNoteTables(body, {
|
|
1067
|
-
const self =
|
|
1068
|
-
fm ?
|
|
1069
|
-
(docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
|
|
1070
|
-
fm: searchableFrontmatter(fm),
|
|
1071
|
-
})
|
|
1072
|
-
: undefined;
|
|
1029
|
+
export function expandNoteTables(body, { name, bodyLine, sqlTables }) {
|
|
1073
1030
|
const { markdown, errors, lineMap } = expandContentTables(body ?? "", {
|
|
1074
|
-
docs,
|
|
1075
|
-
linkable: packLinkable,
|
|
1076
1031
|
source: name,
|
|
1077
|
-
self,
|
|
1078
1032
|
sqlTables,
|
|
1079
1033
|
});
|
|
1080
1034
|
if (errors.length) {
|
|
1081
1035
|
const err = new Error(errors.map((e) => `content table — ${e.reason}`).join("; "));
|
|
1082
|
-
// The first failing directive's line. Reporting one position for a
|
|
1083
|
-
// message that may name several is honest here: a caller opens the
|
|
1084
|
-
// file at the first thing to fix, and the message lists the rest.
|
|
1085
1036
|
if (bodyLine !== undefined && errors[0].line !== undefined) {
|
|
1086
1037
|
err.position = { line: bodyLine + errors[0].line };
|
|
1087
1038
|
}
|
|
@@ -1098,6 +1049,9 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine, sqlTables })
|
|
|
1098
1049
|
* Builds a compendium-source filename for a folder JSON document:
|
|
1099
1050
|
* `folder_Name_id.json` with non-alphanumeric runs replaced by
|
|
1100
1051
|
* underscores.
|
|
1052
|
+
* @param {string} name - Folder name.
|
|
1053
|
+
* @param {string} id - Folder id.
|
|
1054
|
+
* @returns {string} Filename for the folder.
|
|
1101
1055
|
*/
|
|
1102
1056
|
export function folderFilename(name, id) {
|
|
1103
1057
|
return `folder_${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
|
package/engine/homepage.mjs
CHANGED
|
@@ -264,9 +264,8 @@ export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
|
|
|
264
264
|
* {@link module:engine/diagnostics}. {@link lintContentTree} already reports an
|
|
265
265
|
* empty walk against the same locator.
|
|
266
266
|
*
|
|
267
|
-
* The rule
|
|
268
|
-
*
|
|
269
|
-
* published, and the homepage is the floor underneath both modes.
|
|
267
|
+
* The rule applies to every content tree, whether it contains only the
|
|
268
|
+
* homepage or other notes as well.
|
|
270
269
|
*
|
|
271
270
|
* @param {ReadonlyArray<{file: string}>} found - The homepage notes, in walk
|
|
272
271
|
* order. Paths may be absolute or relative to the working directory.
|
package/engine/index-records.mjs
CHANGED
package/engine/index.mjs
CHANGED
|
@@ -42,6 +42,9 @@ export * as systemBlock from "./system-block.mjs";
|
|
|
42
42
|
/** Fenced-code detection, so a rewrite never edits a code block. */
|
|
43
43
|
export * as codeFences from "./code-fences.mjs";
|
|
44
44
|
|
|
45
|
+
/** Format generated text using the destination's Prettier configuration. */
|
|
46
|
+
export { formatGenerated } from "./format-generated.mjs";
|
|
47
|
+
|
|
45
48
|
/** The `sohl:` frontmatter readers, shared by every content package. */
|
|
46
49
|
export * as frontmatter from "./frontmatter.mjs";
|
|
47
50
|
|
|
@@ -109,13 +112,13 @@ export * as mapPlaces from "./map-places.mjs";
|
|
|
109
112
|
/** The geometry of the map from a place: bearings to angles, days to rings, hops composed. */
|
|
110
113
|
export * as mapLayout from "./map-layout.mjs";
|
|
111
114
|
|
|
112
|
-
/** The three drawings of `
|
|
115
|
+
/** The three drawings of `package-build map`, as DOT. */
|
|
113
116
|
export * as mapDot from "./map-dot.mjs";
|
|
114
117
|
|
|
115
118
|
/** GraphViz, as the map command finds and runs it. */
|
|
116
119
|
export * as mapGraphviz from "./map-graphviz.mjs";
|
|
117
120
|
|
|
118
|
-
/** `
|
|
121
|
+
/** `package-build map`: the drawings, written under `build/map/`. */
|
|
119
122
|
export * as mapBuild from "./map-build.mjs";
|
|
120
123
|
|
|
121
124
|
/** The map from each place, drawn for the site and written into its page. */
|
|
@@ -130,6 +133,18 @@ export * as calendars from "./calendars.mjs";
|
|
|
130
133
|
/** A note's dates: one authored string, one grammar, one record shape. */
|
|
131
134
|
export * as noteDates from "./note-dates.mjs";
|
|
132
135
|
|
|
136
|
+
/** Exact day conversion between an authored calendar and the canonical axis. */
|
|
137
|
+
export * as dateConversion from "./date-conversion.mjs";
|
|
138
|
+
|
|
139
|
+
/** Inline frontmatter and date expressions in Markdown prose. */
|
|
140
|
+
export * as markdownExpressions from "./markdown-expressions.mjs";
|
|
141
|
+
|
|
142
|
+
/** A being's or place's chosen calendar note. */
|
|
143
|
+
export * as calendarChoice from "./calendar-choice.mjs";
|
|
144
|
+
|
|
145
|
+
/** A being's primary culture Address, checked against lore subtype. */
|
|
146
|
+
export * as cultureChoice from "./culture-choice.mjs";
|
|
147
|
+
|
|
133
148
|
/** Era markers resolved from calendar notes in a corpus. */
|
|
134
149
|
export * as reckoningMarkers from "./reckoning-markers.mjs";
|
|
135
150
|
|
|
@@ -220,7 +235,7 @@ export * as contentLinks from "./content-links.mjs";
|
|
|
220
235
|
/** Wikilinks resolved to a **web URL** — the site half of the pair below. */
|
|
221
236
|
export * as webWikilinks from "./web-wikilinks.mjs";
|
|
222
237
|
|
|
223
|
-
/**
|
|
238
|
+
/** Prepared SQL content tables, expanded into Markdown at compile time. */
|
|
224
239
|
export * as contentTables from "./content-tables.mjs";
|
|
225
240
|
|
|
226
241
|
/** Markdown parsing, stats, folders, images, and the wikilink index. */
|
|
@@ -111,17 +111,19 @@ export function compilesSystemDocument(fm, map, router) {
|
|
|
111
111
|
* Resolves a reference to `{name, url?, uuid?, address?, subType?}`.
|
|
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
|
+
* @param {object} [options.dates] - Calendar conversion context.
|
|
114
115
|
* @returns {object[]} The boxes, in the order every medium renders them.
|
|
115
116
|
* @throws {Error} When the built set disagrees with what the note's type maps
|
|
116
117
|
* to — see {@link module:engine/infobox.assertInfoboxSet}.
|
|
117
118
|
*/
|
|
118
|
-
export function noteInfoboxes(fm, { resolve, router = packRouter() } = {}) {
|
|
119
|
+
export function noteInfoboxes(fm, { resolve, router = packRouter(), dates } = {}) {
|
|
119
120
|
const boxes = buildInfoboxes(fm, {
|
|
120
121
|
maps: KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
121
122
|
providers: KNOWN_INFOBOXES,
|
|
122
123
|
compilesDocument: (note, map) => compilesSystemDocument(note, map, router),
|
|
123
124
|
resolveField: resolveFieldValue,
|
|
124
125
|
resolve,
|
|
126
|
+
dates,
|
|
125
127
|
});
|
|
126
128
|
return /** @type {object[]} */ (
|
|
127
129
|
assertInfoboxSet(boxes, fm, { maps: KNOWN_DOCUMENT_SUBTYPE_MAPS })
|
package/engine/infobox.mjs
CHANGED
|
@@ -119,6 +119,8 @@ 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 { formatDateInCalendar, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
123
|
+
import { displayBeingHeight, displayBeingWeight } from "./being-measurements.mjs";
|
|
122
124
|
|
|
123
125
|
/**
|
|
124
126
|
* How a section arranges what it holds.
|
|
@@ -300,8 +302,8 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
300
302
|
"being.ageYears": Object.freeze({
|
|
301
303
|
withheld: "the `age` row's own machinery, not a second fact about the subject",
|
|
302
304
|
}),
|
|
303
|
-
"being.height": Object.freeze({ group: "appearance", phrase:
|
|
304
|
-
"being.weight": Object.freeze({ group: "appearance", phrase:
|
|
305
|
+
"being.height": Object.freeze({ group: "appearance", phrase: displayBeingHeight }),
|
|
306
|
+
"being.weight": Object.freeze({ group: "appearance", phrase: displayBeingWeight }),
|
|
305
307
|
"being.frame": Object.freeze({ group: "appearance", phrase: (v) => `${v} frame` }),
|
|
306
308
|
"being.appearance.eye_color": Object.freeze({
|
|
307
309
|
group: "appearance",
|
|
@@ -348,18 +350,6 @@ export function overlayFor(presentation, type, name) {
|
|
|
348
350
|
/** What a composed group's row is called. @type {Readonly<Record<string, string>>} */
|
|
349
351
|
const GROUP_LABELS = Object.freeze({ appearance: "Appearance" });
|
|
350
352
|
|
|
351
|
-
/** Metres as feet and inches, which is how the corpus reads a height. */
|
|
352
|
-
function heightPhrase(metres) {
|
|
353
|
-
const inches = Math.round(Number(metres) * 39.3701);
|
|
354
|
-
const feet = Math.floor(inches / 12);
|
|
355
|
-
return `${feet}′ ${inches - feet * 12}″`;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** Kilograms as pounds, which is how the corpus reads a weight. */
|
|
359
|
-
function weightPhrase(kilograms) {
|
|
360
|
-
return `${Math.round(Number(kilograms) * 2.20462)} lbs`;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
353
|
/** Whether a value is a plain mapping. */
|
|
364
354
|
function isMapping(value) {
|
|
365
355
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -701,7 +691,7 @@ export function noteInfobox(fm, options = {}) {
|
|
|
701
691
|
*/
|
|
702
692
|
function noteBox(
|
|
703
693
|
fm,
|
|
704
|
-
{ resolve, vocabulary = NOTE_VOCABULARY, presentation = NOTE_FIELD_PRESENTATION } = {},
|
|
694
|
+
{ resolve, dates, vocabulary = NOTE_VOCABULARY, presentation = NOTE_FIELD_PRESENTATION } = {},
|
|
705
695
|
) {
|
|
706
696
|
const rows = [];
|
|
707
697
|
/** @type {Set<string>} */
|
|
@@ -721,7 +711,34 @@ function noteBox(
|
|
|
721
711
|
// The current name wins, which is the whole of the retirement window's
|
|
722
712
|
// behaviour and is `readAliasedField`'s answer, not a second one.
|
|
723
713
|
const own = getFrontmatter(data, field.name, undefined);
|
|
724
|
-
|
|
714
|
+
let raw = own !== undefined ? own : readAliasedField(fm, field.name, { inData: true });
|
|
715
|
+
if (
|
|
716
|
+
((fm?.type === "being" && ["born", "died"].includes(field.name)) ||
|
|
717
|
+
(fm?.type === "place" && field.name === "present")) &&
|
|
718
|
+
dates
|
|
719
|
+
) {
|
|
720
|
+
const parsed = parseNoteDate(raw, dates).date;
|
|
721
|
+
const era =
|
|
722
|
+
parsed?.marker ? dates.markers?.get(parsed.marker)
|
|
723
|
+
: parsed?.qualifier ? dates.eras?.get(parsed.qualifier)
|
|
724
|
+
: null;
|
|
725
|
+
let printable = null;
|
|
726
|
+
if (parsed?.known && data.calendar) {
|
|
727
|
+
try {
|
|
728
|
+
printable = formatDateInCalendar(
|
|
729
|
+
parsed,
|
|
730
|
+
isAddressTuple(data.calendar) ?
|
|
731
|
+
renderAddress(data.calendar)
|
|
732
|
+
: data.calendar,
|
|
733
|
+
dates,
|
|
734
|
+
);
|
|
735
|
+
} catch {
|
|
736
|
+
// The calendar reference is checked at the authored field.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
printable ??= formatNoteDate(parsed, era, dates.daysPerYear);
|
|
740
|
+
if (printable) raw = printable.prose ?? printable.text;
|
|
741
|
+
}
|
|
725
742
|
const structured = structuredRows(
|
|
726
743
|
field,
|
|
727
744
|
raw,
|
|
@@ -889,7 +906,7 @@ export function isDeclaredDefault(field, raw) {
|
|
|
889
906
|
*
|
|
890
907
|
* @param {object} fm - The note's frontmatter.
|
|
891
908
|
* @param {readonly object[]} fields - The system's field declaration.
|
|
892
|
-
* @param {
|
|
909
|
+
* @param {{block: string, resolve?: Function, resolveField?: Function, taken?: Set<string>, presentation?: object}} ctx - Rendering context.
|
|
893
910
|
* @returns {object[]} Zero or one section.
|
|
894
911
|
*/
|
|
895
912
|
export function systemRowsSection(
|
|
@@ -956,9 +973,10 @@ export function buildInfoboxes(fm, options) {
|
|
|
956
973
|
resolveField,
|
|
957
974
|
resolve,
|
|
958
975
|
vocabulary = NOTE_VOCABULARY,
|
|
976
|
+
dates,
|
|
959
977
|
} = options;
|
|
960
978
|
|
|
961
|
-
const { box: note, shown: taken } = noteBox(fm, { resolve, vocabulary });
|
|
979
|
+
const { box: note, shown: taken } = noteBox(fm, { resolve, vocabulary, dates });
|
|
962
980
|
const boxes = [note];
|
|
963
981
|
|
|
964
982
|
for (const map of maps ?? []) {
|
|
@@ -1017,7 +1035,7 @@ export function sectionHolds(section) {
|
|
|
1017
1035
|
* against a restatement of it.
|
|
1018
1036
|
*
|
|
1019
1037
|
* @param {object} fm - The note's frontmatter.
|
|
1020
|
-
* @param {object} options -
|
|
1038
|
+
* @param {{maps: readonly object[]}} options - Document subtype mappings.
|
|
1021
1039
|
* @returns {string[]} The ids.
|
|
1022
1040
|
*/
|
|
1023
1041
|
export function requiredInfoboxIds(fm, { maps }) {
|
|
@@ -1040,7 +1058,7 @@ export function requiredInfoboxIds(fm, { maps }) {
|
|
|
1040
1058
|
*
|
|
1041
1059
|
* @param {readonly object[]} boxes - What was built.
|
|
1042
1060
|
* @param {object} fm - The note's frontmatter.
|
|
1043
|
-
* @param {object} options -
|
|
1061
|
+
* @param {{maps: readonly object[], where?: string}} options - Document subtype mappings and context.
|
|
1044
1062
|
* @returns {readonly object[]} The boxes, unchanged, so a caller may assert
|
|
1045
1063
|
* inline.
|
|
1046
1064
|
* @throws {Error} Naming the note, what is missing and what is extra.
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -50,7 +50,7 @@ import log from "loglevel";
|
|
|
50
50
|
|
|
51
51
|
import { resolveName } from "./helpers.mjs";
|
|
52
52
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
53
|
-
import { journalPageId, splitPages } from "./journals.mjs";
|
|
53
|
+
import { assertUniquePages, journalPageId, splitPages } from "./journals.mjs";
|
|
54
54
|
import { foundryPackageId } from "./content-package.mjs";
|
|
55
55
|
import { itemDocEntryId, itemDocPointer } from "./item-docs.mjs";
|
|
56
56
|
// The whitelist and the per-type `system` builders both come from the resolved
|
|
@@ -76,15 +76,15 @@ import { assertNoDerivedFields } from "./derived-fields.mjs";
|
|
|
76
76
|
import { assertNoRuntimeOnlyFields } from "./runtime-only-fields.mjs";
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* The description an item carries: a pointer to its **item doc**,
|
|
80
|
-
* JournalEntry the journals pass compiles this same body into.
|
|
79
|
+
* The description an item carries: a pointer to a page of its **item doc**,
|
|
80
|
+
* the JournalEntry the journals pass compiles this same body into.
|
|
81
81
|
*
|
|
82
82
|
* The prose is not rendered into the item at all. Carrying it would duplicate
|
|
83
83
|
* it onto every actor holding the item — 7.59 MB of copies across the actors
|
|
84
84
|
* pack, of which 133 KB was distinct — where a link is 60 bytes and always
|
|
85
85
|
* current. The two passes derive the target from the note's own id, so neither
|
|
86
|
-
* has to see the other's output
|
|
87
|
-
*
|
|
86
|
+
* has to see the other's output. `{#description}` selects the named page;
|
|
87
|
+
* an item without that anchor points to its first page.
|
|
88
88
|
*
|
|
89
89
|
* An item with no prose points at nothing, exactly as the journals pass writes
|
|
90
90
|
* no entry for it.
|
|
@@ -97,9 +97,11 @@ import { assertNoRuntimeOnlyFields } from "./runtime-only-fields.mjs";
|
|
|
97
97
|
*/
|
|
98
98
|
export function itemDescription(markdown, fm, name) {
|
|
99
99
|
if (!String(markdown).trim()) return "";
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const pages = splitPages(markdown, name);
|
|
101
|
+
assertUniquePages(pages, name);
|
|
102
|
+
const page = pages.find((entry) => entry.anchorSlug === "description") ?? pages[0];
|
|
103
|
+
const pageId = journalPageId(itemDocEntryId(fm.id), page);
|
|
104
|
+
return itemDocPointer(foundryPackageId(), fm.id, name, pageId, fm);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
package/engine/item-docs.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* An item note's body describes what the thing *is*. That is documentation, and
|
|
19
19
|
* documentation belongs in the journals pack, so each item note compiles into a
|
|
20
20
|
* JournalEntry and the item's `system.docHtml` becomes nothing but a `@UUID`
|
|
21
|
-
* link to
|
|
21
|
+
* link to its `{#description}` page, or its first page when that anchor is absent — the description-as-pointer convention,
|
|
22
22
|
* which {@link sohl.utils.descriptionLinkTarget} recognises and
|
|
23
23
|
* Display Description follows.
|
|
24
24
|
*
|
|
@@ -92,7 +92,7 @@ export function itemDocEntryId(itemId) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* The description an item carries in place of its prose: a `@UUID` link to the
|
|
95
|
-
*
|
|
95
|
+
* description page of its item doc, and nothing else.
|
|
96
96
|
*
|
|
97
97
|
* "Nothing else" is the whole convention — a description that is *only* a link
|
|
98
98
|
* is a pointer, and anything alongside it would make it ordinary prose that
|
|
@@ -104,18 +104,19 @@ export function itemDocEntryId(itemId) {
|
|
|
104
104
|
* @param {string} name - The item's name, used as the link's label. It shows
|
|
105
105
|
* only if the target ever fails to resolve, where a broken link naming the
|
|
106
106
|
* item beats a bare UUID.
|
|
107
|
-
* @param {string}
|
|
107
|
+
* @param {string} pageId - The id of the selected entry page, from
|
|
108
108
|
* {@link journalPageId}.
|
|
109
|
+
* @param {object} [fm] - The source note, used to route GM-only prose.
|
|
109
110
|
* @returns {string} The pointer to store in `system.docHtml`.
|
|
110
111
|
*/
|
|
111
|
-
export function itemDocPointer(packageId, itemId, name,
|
|
112
|
+
export function itemDocPointer(packageId, itemId, name, pageId, fm) {
|
|
112
113
|
// An item doc is a *derived* document: it lands in the default
|
|
113
114
|
// JournalEntry pack whatever Item pack the item itself was routed to.
|
|
114
115
|
const entryUuid = compendiumUuid(
|
|
115
116
|
packageId,
|
|
116
117
|
"doc",
|
|
117
118
|
itemDocEntryId(itemId),
|
|
118
|
-
packRouter().defaultOf("JournalEntry"),
|
|
119
|
+
fm ? packRouter().resolve(fm, "JournalEntry") : packRouter().defaultOf("JournalEntry"),
|
|
119
120
|
);
|
|
120
|
-
return `@UUID[${pageUuid(entryUuid,
|
|
121
|
+
return `@UUID[${pageUuid(entryUuid, pageId)}]{${name}}`;
|
|
121
122
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { resolvePackageBuildConfig } from "../config.mjs";
|
|
9
10
|
|
|
10
11
|
import { buildMaps, relatedPlaces } from "./map-build.mjs";
|
|
11
12
|
import { renderDot } from "./map-graphviz.mjs";
|
|
@@ -74,6 +75,7 @@ export function buildItineraryScenes({
|
|
|
74
75
|
stats,
|
|
75
76
|
foreign = loadForeignIndexes(config, [config.contentPackage]),
|
|
76
77
|
}) {
|
|
78
|
+
const buildConfig = resolvePackageBuildConfig(config);
|
|
77
79
|
const world = mapWorld({ records, foreignIndex: foreign.index, contentBase, config });
|
|
78
80
|
const centres = relatedPlaces(world.places).filter((sc) => world.places.get(sc)?.local);
|
|
79
81
|
const findings = foreign.stale.map((stale) => ({
|
|
@@ -83,7 +85,7 @@ export function buildItineraryScenes({
|
|
|
83
85
|
}));
|
|
84
86
|
const assetDir = path.resolve(
|
|
85
87
|
config.rootDir,
|
|
86
|
-
|
|
88
|
+
buildConfig.stageDir,
|
|
87
89
|
"assets/maps/generated-itineraries",
|
|
88
90
|
);
|
|
89
91
|
fs.rmSync(assetDir, { recursive: true, force: true });
|
|
@@ -112,7 +114,7 @@ export function buildItineraryScenes({
|
|
|
112
114
|
const [width, height] = pngDimensions(png);
|
|
113
115
|
const positions = pinPositions(fs.readFileSync(plain, "utf8"), width, height);
|
|
114
116
|
const sceneId = makeId("itinerary-scene", `${config.contentPackage}:${centre}`);
|
|
115
|
-
const src = `${
|
|
117
|
+
const src = `${buildConfig.packageKind}/${buildConfig.packageId}/assets/maps/generated-itineraries/from-${centre}.png`;
|
|
116
118
|
const scene = buildScene(
|
|
117
119
|
{
|
|
118
120
|
id: sceneId,
|