@heroiclands/package-build 0.6.0 → 3.0.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 +965 -0
- package/CHANGELOG.md +95 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -34
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
package/engine/ids.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
* Deterministic document ids, derived by hashing rather than stored.
|
|
16
|
+
*
|
|
17
|
+
* Several pack-build passes must agree on an id without being able to see each
|
|
18
|
+
* other's output — the items pass and the journals pass on an item doc's entry
|
|
19
|
+
* id, a section link and the page it addresses on that page's id. They agree by
|
|
20
|
+
* deriving the id from the same inputs, so the derivation has to be reachable
|
|
21
|
+
* from every one of them.
|
|
22
|
+
*
|
|
23
|
+
* That is why this is its own module and not part of `helpers.mjs`: link
|
|
24
|
+
* resolution needs it, and `helpers.mjs` imports the link resolver. A leaf with
|
|
25
|
+
* no local imports can be depended on from anywhere without a cycle.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import crypto from "crypto";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stable 16-char hex id derived from `${namespace}:${value}`. Use for deriving
|
|
32
|
+
* page ids from heading text when no explicit id is supplied.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} namespace - Keeps unrelated derivations from colliding.
|
|
35
|
+
* @param {string} value - The input the id is a function of.
|
|
36
|
+
* @returns {string} A 16-character hexadecimal Foundry id.
|
|
37
|
+
*/
|
|
38
|
+
export function makeId(namespace, value) {
|
|
39
|
+
return crypto
|
|
40
|
+
.createHash("sha1")
|
|
41
|
+
.update(`${namespace}:${value}`)
|
|
42
|
+
.digest("hex")
|
|
43
|
+
.slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every content type that compiles into a Foundry `Scene` — a **map note**
|
|
48
|
+
* (#1525). The three differ only in derived canvas defaults, which is the map
|
|
49
|
+
* compiler's business; everything else treats them alike.
|
|
50
|
+
*
|
|
51
|
+
* Declared in this leaf module because several passes that must not depend on
|
|
52
|
+
* the map compiler need it: the pack router below, and the doc-carrying type
|
|
53
|
+
* set in `item-docs.mjs` (a map note's prose becomes a JournalEntry, exactly as
|
|
54
|
+
* an item's or a macro's does).
|
|
55
|
+
*
|
|
56
|
+
* @type {ReadonlySet<string>}
|
|
57
|
+
*/
|
|
58
|
+
export const MAP_TYPES = Object.freeze(
|
|
59
|
+
new Set(["battlemap", "localmap", "regionalmap"]),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Content type → the pack its documents compile into, and the document type
|
|
64
|
+
* that pack holds.
|
|
65
|
+
*
|
|
66
|
+
* These are pack **names**, not addresses. The package that owns the pack is
|
|
67
|
+
* supplied by the caller, because it is a property of the repository doing the
|
|
68
|
+
* building and not of the content: the same notes compiled by a different
|
|
69
|
+
* repository belong to a different package. Baking the package into these
|
|
70
|
+
* values is what made every link emitted by `sohl-thalorna` address the `sohl`
|
|
71
|
+
* system (#1498) — correct here only by coincidence.
|
|
72
|
+
*
|
|
73
|
+
* @type {Readonly<Record<string, {pack: string, docType: string}>>}
|
|
74
|
+
*/
|
|
75
|
+
export const PACK_BY_TYPE = Object.freeze({
|
|
76
|
+
doc: { pack: "journals", docType: "JournalEntry" },
|
|
77
|
+
macro: { pack: "macros", docType: "Macro" },
|
|
78
|
+
being: { pack: "actors", docType: "Actor" },
|
|
79
|
+
...Object.fromEntries(
|
|
80
|
+
[...MAP_TYPES].map((type) => [
|
|
81
|
+
type,
|
|
82
|
+
{ pack: "scenes", docType: "Scene" },
|
|
83
|
+
]),
|
|
84
|
+
),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Content types that no longer exist, and what replaced each one.
|
|
89
|
+
*
|
|
90
|
+
* `character` and `creature` were retired in favour of the single `being` they
|
|
91
|
+
* had always compiled into (SoHL#1580). They are recorded here rather than
|
|
92
|
+
* simply deleted because deleting them is the one change that fails *quietly*:
|
|
93
|
+
* every type not named in {@link PACK_BY_TYPE} falls through to the open item
|
|
94
|
+
* set below, so a note or a link left on the old spelling would be routed to
|
|
95
|
+
* the items pack — a wrong answer, arrived at silently, which is exactly the
|
|
96
|
+
* failure mode the open-set default exists to avoid for real item types.
|
|
97
|
+
*
|
|
98
|
+
* Keeping the names lets {@link assertTypeNotRetired} say what happened and
|
|
99
|
+
* what to write instead. Entries stay for as long as content in the wild might
|
|
100
|
+
* still carry them.
|
|
101
|
+
*
|
|
102
|
+
* @type {Readonly<Record<string, string>>}
|
|
103
|
+
*/
|
|
104
|
+
export const RETIRED_TYPES = Object.freeze({
|
|
105
|
+
character: "being",
|
|
106
|
+
creature: "being",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Throw if `type` names a retired content type.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} type - The note's declared `type`, or a link's qualifier.
|
|
113
|
+
* @param {string} [where] - What carries it — a file path, a link target —
|
|
114
|
+
* appended to the message so the reader can go straight to it.
|
|
115
|
+
* @throws {Error} Naming the replacement type.
|
|
116
|
+
*/
|
|
117
|
+
export function assertTypeNotRetired(type, where) {
|
|
118
|
+
const replacement = RETIRED_TYPES[type];
|
|
119
|
+
if (!replacement) return;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Content type "${type}" was retired in favour of "${replacement}"` +
|
|
122
|
+
(where ? ` — ${where}` : "") +
|
|
123
|
+
`. Both compiled to the same document, so the fix is mechanical: ` +
|
|
124
|
+
`write "${replacement}".`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Where every other content type compiles: the items pack. */
|
|
129
|
+
export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The pack a type's documents live in, in the conventional one-pack-per-type
|
|
133
|
+
* layout.
|
|
134
|
+
*
|
|
135
|
+
* Item types are the open set — a new one is added whenever the system grows a
|
|
136
|
+
* document type — so they are the **default** rather than an enumerated list. A
|
|
137
|
+
* hand-maintained list is what made an entire content directory silently
|
|
138
|
+
* unlinkable once (#1276); nothing to maintain, nothing to forget.
|
|
139
|
+
*
|
|
140
|
+
* The `docType` is the authority: it is a property of the *content type* and
|
|
141
|
+
* holds however a repository names or splits its packs. The `pack` is the
|
|
142
|
+
* conventional name only — a repository may rename its packs, or ship several
|
|
143
|
+
* of one type (#1566), in which case the pack a particular note's document
|
|
144
|
+
* lands in comes from `engine/pack-router.mjs` and is passed to
|
|
145
|
+
* {@link compendiumUuid} explicitly. This module stays free of the
|
|
146
|
+
* configuration so the link resolver above it can stay pure.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} type - The target note's `type`.
|
|
149
|
+
* @returns {{pack: string, docType: string}} The pack and document type.
|
|
150
|
+
* @throws {Error} If `type` names a retired content type — see
|
|
151
|
+
* {@link RETIRED_TYPES}. The open-set default would otherwise route it to the
|
|
152
|
+
* items pack and say nothing.
|
|
153
|
+
*/
|
|
154
|
+
export function packForType(type) {
|
|
155
|
+
assertTypeNotRetired(type);
|
|
156
|
+
return PACK_BY_TYPE[type] ?? ITEM_PACK;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A document's full compendium UUID.
|
|
161
|
+
*
|
|
162
|
+
* This is the one place a UUID is spelled. Every link is resolved by looking up
|
|
163
|
+
* an address computed here — never by concatenating a prefix at the point of
|
|
164
|
+
* use, which is how the package came to be hard-coded in two separate files.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} packageId - The Foundry package that ships the pack, e.g.
|
|
167
|
+
* `sohl`. A system id or a module id; Foundry addresses both the same way.
|
|
168
|
+
* @param {string} type - The note's content `type`.
|
|
169
|
+
* @param {string} id - The document's id.
|
|
170
|
+
* @param {string} [packName] - The pack the document actually landed in, from
|
|
171
|
+
* the pack router. Supplied wherever the note is known, because a repository
|
|
172
|
+
* may ship several packs of one type and a UUID carries the pack name
|
|
173
|
+
* (#1566). Omitted only where there is no note to route — the conventional
|
|
174
|
+
* name from {@link packForType} then stands in.
|
|
175
|
+
* @returns {string} `Compendium.<packageId>.<pack>.<DocumentType>.<id>`
|
|
176
|
+
*/
|
|
177
|
+
export function compendiumUuid(packageId, type, id, packName) {
|
|
178
|
+
const { pack, docType } = packForType(type);
|
|
179
|
+
return `Compendium.${packageId}.${packName || pack}.${docType}.${id}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The UUID of a JournalEntry page.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} entryUuid - The owning entry's UUID, from
|
|
186
|
+
* {@link compendiumUuid}.
|
|
187
|
+
* @param {string} pageId - The page's id.
|
|
188
|
+
* @returns {string} The page's UUID.
|
|
189
|
+
*/
|
|
190
|
+
export function pageUuid(entryUuid, pageId) {
|
|
191
|
+
return `${entryUuid}.JournalEntryPage.${pageId}`;
|
|
192
|
+
}
|
package/engine/index.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
* The package-agnostic half of the toolchain: everything that knows how a
|
|
16
|
+
* HeroicLands content tree is shaped, but nothing about any particular game
|
|
17
|
+
* system's data model.
|
|
18
|
+
*
|
|
19
|
+
* The content walk, frontmatter parsing, table generation, wikilink
|
|
20
|
+
* resolution, id and folder derivation, the link manifest and the web-address
|
|
21
|
+
* rule, `BasePackCompiler`, and the generic Foundry document compilers
|
|
22
|
+
* (journals, macros, scenes) all live here (#1512).
|
|
23
|
+
*
|
|
24
|
+
* **Each module is re-exported as its own namespace, not flattened.** Several
|
|
25
|
+
* of them deliberately re-export a neighbour's symbol so a caller keeps one
|
|
26
|
+
* import path (`helpers` re-exports the frontmatter readers and `makeId`;
|
|
27
|
+
* `wikilinks` re-exports the pack router). Flattened, every such name would
|
|
28
|
+
* become an ambiguous star export and vanish from this barrel silently. Each
|
|
29
|
+
* module is also reachable as its own entry point —
|
|
30
|
+
* `@heroiclands/package-build/engine/<module>` — which is how a build that
|
|
31
|
+
* needs one thing avoids loading the whole pipeline.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Deterministic document ids, the conventional pack map, and compendium UUIDs. */
|
|
37
|
+
export * as ids from "./ids.mjs";
|
|
38
|
+
|
|
39
|
+
/** Fenced-code detection, so a rewrite never edits a code block. */
|
|
40
|
+
export * as codeFences from "./code-fences.mjs";
|
|
41
|
+
|
|
42
|
+
/** The `sohl:` frontmatter readers, shared by every content package. */
|
|
43
|
+
export * as frontmatter from "./frontmatter.mjs";
|
|
44
|
+
|
|
45
|
+
/** The content walk's note census — the empty-tree guard's evidence. */
|
|
46
|
+
export * as contentTree from "./content-tree.mjs";
|
|
47
|
+
|
|
48
|
+
/** The consuming repository's resolved `package-build.config.yaml`. */
|
|
49
|
+
export * as packConfig from "./pack-config.mjs";
|
|
50
|
+
|
|
51
|
+
/** Which pack of a document type a note's document lands in (#1566). */
|
|
52
|
+
export * as packRouter from "./pack-router.mjs";
|
|
53
|
+
|
|
54
|
+
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
55
|
+
export * as contentPackage from "./content-package.mjs";
|
|
56
|
+
|
|
57
|
+
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
58
|
+
|
|
59
|
+
/** The URL a content note is published at — the one web-address rule. */
|
|
60
|
+
export * as contentSlug from "./content-slug.mjs";
|
|
61
|
+
|
|
62
|
+
/** Section and address derivation on top of {@link contentSlug}. */
|
|
63
|
+
export * as contentAddress from "./content-address.mjs";
|
|
64
|
+
|
|
65
|
+
/** Whether a vendored manifest can still be addressed, not merely read. */
|
|
66
|
+
export * as foreignManifests from "./foreign-manifests.mjs";
|
|
67
|
+
|
|
68
|
+
/** The cross-package link manifest: reader, writer, and canonical keys. */
|
|
69
|
+
export * as kbManifest from "./kb-manifest.mjs";
|
|
70
|
+
|
|
71
|
+
/** Deriving this package's own link manifest from its content tree. */
|
|
72
|
+
export * as manifestEmit from "./manifest-emit.mjs";
|
|
73
|
+
|
|
74
|
+
/** Publishing a content tree as a website: the pass, and its integrity gates. */
|
|
75
|
+
export * as siteBuild from "./site-build.mjs";
|
|
76
|
+
|
|
77
|
+
/** Address rules every content tree is linted against: shape, uniqueness, alias. */
|
|
78
|
+
export * as contentLint from "./content-lint.mjs";
|
|
79
|
+
|
|
80
|
+
/** Resolving every link in a tree, and the ones that land nowhere. */
|
|
81
|
+
export * as contentLinks from "./content-links.mjs";
|
|
82
|
+
|
|
83
|
+
/** Wikilinks resolved to a **web URL** — the site half of the pair below. */
|
|
84
|
+
export * as webWikilinks from "./web-wikilinks.mjs";
|
|
85
|
+
|
|
86
|
+
/** Dataview-style content tables, expanded into markdown at compile time. */
|
|
87
|
+
export * as contentTables from "./content-tables.mjs";
|
|
88
|
+
|
|
89
|
+
/** Markdown parsing, stats, folders, images, and the wikilink index. */
|
|
90
|
+
export * as helpers from "./helpers.mjs";
|
|
91
|
+
|
|
92
|
+
/** The consumer's resolved item-type registry: the whitelist and its builders. */
|
|
93
|
+
export * as itemRegistry from "./item-registry.mjs";
|
|
94
|
+
|
|
95
|
+
/** Which types carry documentation of their own, and where it is addressed. */
|
|
96
|
+
export * as itemDocs from "./item-docs.mjs";
|
|
97
|
+
|
|
98
|
+
/** Wikilinks resolved to a **Foundry UUID**: qualifiers, the index, rewriting. */
|
|
99
|
+
export * as wikilinks from "./wikilinks.mjs";
|
|
100
|
+
|
|
101
|
+
/** What a `[[…]]` is, before either resolver decides where it points. */
|
|
102
|
+
export * as wikilinkSyntax from "./wikilink-syntax.mjs";
|
|
103
|
+
|
|
104
|
+
/** The address index a site build resolves its wikilinks against. */
|
|
105
|
+
export * as siteIndex from "./site-index.mjs";
|
|
106
|
+
|
|
107
|
+
/** The shape every pack compiler shares. */
|
|
108
|
+
export * as baseCompiler from "./base-compiler.mjs";
|
|
109
|
+
|
|
110
|
+
/** The JournalEntry compiler, and the page/anchor derivation it shares. */
|
|
111
|
+
export * as journals from "./journals.mjs";
|
|
112
|
+
|
|
113
|
+
/** The Macro compiler. */
|
|
114
|
+
export * as macros from "./macros.mjs";
|
|
115
|
+
|
|
116
|
+
/** The map-note schema a Scene is authored in. */
|
|
117
|
+
export * as mapNotes from "./map-notes.mjs";
|
|
118
|
+
|
|
119
|
+
/** The Scene and Adventure compiler. */
|
|
120
|
+
export * as scenes from "./scenes.mjs";
|
|
121
|
+
|
|
122
|
+
/** The compiled-pack Scene/Level integrity guard. */
|
|
123
|
+
export * as sceneLevels from "./scene-levels.mjs";
|
|
124
|
+
|
|
125
|
+
/** Pack JSON generation — the pass that turns a content tree into documents. */
|
|
126
|
+
export * as generate from "./generate.mjs";
|
|
127
|
+
|
|
128
|
+
/** Compile, unpack, and clean the LevelDB packs. */
|
|
129
|
+
export * as compendiums from "./compendiums.mjs";
|
|
130
|
+
|
|
131
|
+
// The region-event vocabulary stays flat as well as namespaced: the Foundry
|
|
132
|
+
// runtime imports these three by name through
|
|
133
|
+
// `@heroiclands/package-build/engine/region-events`, and they were part of this
|
|
134
|
+
// barrel's surface before the pipeline arrived (#1510).
|
|
135
|
+
export {
|
|
136
|
+
CURATED_REGION_EVENTS,
|
|
137
|
+
EXCLUDED_REGION_EVENTS,
|
|
138
|
+
REGION_EVENT_TO_TRIGGER,
|
|
139
|
+
} from "./region-events.mjs";
|
|
140
|
+
|
|
141
|
+
/** The curated region-event vocabulary, shared with the Foundry runtime. */
|
|
142
|
+
export * as regionEvents from "./region-events.mjs";
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
* **Item docs** — an item's prose compiled as a JournalEntry, with the item
|
|
16
|
+
* keeping only a pointer to it.
|
|
17
|
+
*
|
|
18
|
+
* An item note's body describes what the thing *is*. That is documentation, and
|
|
19
|
+
* documentation belongs in the journals pack, so each item note compiles into a
|
|
20
|
+
* JournalEntry and the item's `system.docHtml` becomes nothing but a `@UUID`
|
|
21
|
+
* link to that entry's first page — the description-as-pointer convention
|
|
22
|
+
* (#1356), which {@link sohl.utils.descriptionLinkTarget} recognises and
|
|
23
|
+
* Display Description follows.
|
|
24
|
+
*
|
|
25
|
+
* The prose then exists once. Previously every actor carrying an item carried
|
|
26
|
+
* its own copy of that item's description: 7.59 MB across the actors pack, of
|
|
27
|
+
* which only 133 KB was distinct text (#1348). Nothing about actors changes —
|
|
28
|
+
* they embed whatever the item carries, and what the item carries is now a link.
|
|
29
|
+
*
|
|
30
|
+
* **Two passes, no shared state.** The items pass writes the pointer; the
|
|
31
|
+
* journals pass writes the entry it points at. Neither can see the other's
|
|
32
|
+
* output, so both derive the same ids from the item note's own id — the same
|
|
33
|
+
* technique {@link anchorPageId} uses to let a section link and its page agree.
|
|
34
|
+
*
|
|
35
|
+
* **The shape generalises.** A `macro` note is the same arrangement: it
|
|
36
|
+
* compiles into a Macro, and its prose into a JournalEntry addressed
|
|
37
|
+
* `docmacro/<shortcode>` (#1514). So is a **map note**, which compiles into a
|
|
38
|
+
* Scene and whose prose becomes the place description its map pins point at
|
|
39
|
+
* (#1525). {@link docEntryTypes} is the one set both the compilers and the
|
|
40
|
+
* link manifest read to know which types work this way.
|
|
41
|
+
*
|
|
42
|
+
* Plain ESM with no Foundry and no filesystem access, so it is unit-testable.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { compendiumUuid, makeId, pageUuid } from "./ids.mjs";
|
|
46
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
47
|
+
import { itemTypes } from "./item-registry.mjs";
|
|
48
|
+
import { packRouter } from "./pack-router.mjs";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every content type that compiles into an item — and therefore into an item
|
|
52
|
+
* doc. Re-exported here rather than restated because both passes need it: the
|
|
53
|
+
* items pass to know what to compile, the journals pass to know whose prose it
|
|
54
|
+
* is holding.
|
|
55
|
+
*
|
|
56
|
+
* It is the consuming repository's `itemBuilders` keys, resolved once in
|
|
57
|
+
* `item-registry.mjs` alongside the builder lookup the Item compiler dispatches
|
|
58
|
+
* through — one object, so the whitelist and the table cannot disagree
|
|
59
|
+
* (#1504/#1563).
|
|
60
|
+
*/
|
|
61
|
+
export { itemTypes };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every content type whose **prose compiles into a JournalEntry of its own**,
|
|
65
|
+
* addressed by the virtual `doc<type>` qualifier.
|
|
66
|
+
*
|
|
67
|
+
* Every item type, plus `macro` — a macro note's body documents the script the
|
|
68
|
+
* note also compiles into a Macro (#1514), which is the same shape as an item
|
|
69
|
+
* and its description: one note, two documents, the prose living in the
|
|
70
|
+
* journals pack.
|
|
71
|
+
*
|
|
72
|
+
* **One set, read by the compiler and the emitter alike.** The journals pass
|
|
73
|
+
* decides what to compile from it, and the link manifest decides what to
|
|
74
|
+
* publish a `doc<type>` entry for. Held apart, the two drift into a manifest
|
|
75
|
+
* that asserts documentation nothing compiled — or a compiled entry no
|
|
76
|
+
* consumer can address. It is composed exactly once, in `defineConfig`, and
|
|
77
|
+
* read from there — never recomposed at a call site.
|
|
78
|
+
*
|
|
79
|
+
* `doc` notes and actors are absent: each is a single document, so it has no
|
|
80
|
+
* separate documentation to address.
|
|
81
|
+
*
|
|
82
|
+
* An accessor rather than a hoisted constant, so that importing this module
|
|
83
|
+
* needs no configuration (#2).
|
|
84
|
+
*
|
|
85
|
+
* @returns {ReadonlySet<string>} The configured doc-carrying types.
|
|
86
|
+
*/
|
|
87
|
+
export function docEntryTypes() {
|
|
88
|
+
return loadPackConfig().docEntryTypes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether a content note's type is one whose prose becomes a JournalEntry of
|
|
93
|
+
* its own.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} type - The note's `type` frontmatter.
|
|
96
|
+
* @returns {boolean} True for an item type, for `macro` and for a map type;
|
|
97
|
+
* false for `doc` and for actors.
|
|
98
|
+
*/
|
|
99
|
+
export function hasDocEntry(type) {
|
|
100
|
+
return docEntryTypes().has(String(type));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The id of the JournalEntry a note's prose compiles into — an item's, or a
|
|
105
|
+
* macro's.
|
|
106
|
+
*
|
|
107
|
+
* Derived from the note's own id so that the pass writing the document and the
|
|
108
|
+
* pass writing its documentation agree without either reading the other's
|
|
109
|
+
* output. It is deliberately not the note's id itself: the two documents are
|
|
110
|
+
* distinct, live in different packs, and sharing an id would make either one's
|
|
111
|
+
* UUID ambiguous to read.
|
|
112
|
+
*
|
|
113
|
+
* The `"item-doc"` hash namespace is **frozen**: it is baked into every entry
|
|
114
|
+
* id already shipped, and every `@UUID` pointing at one. It names where the
|
|
115
|
+
* derivation started, not what may use it.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} itemId - The note's `id` frontmatter.
|
|
118
|
+
* @returns {string} A 16-character Foundry id.
|
|
119
|
+
*/
|
|
120
|
+
export function itemDocEntryId(itemId) {
|
|
121
|
+
return makeId("item-doc", itemId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The description an item carries in place of its prose: a `@UUID` link to the
|
|
126
|
+
* first page of its item doc, and nothing else.
|
|
127
|
+
*
|
|
128
|
+
* "Nothing else" is the whole convention — a description that is *only* a link
|
|
129
|
+
* is a pointer, and anything alongside it would make it ordinary prose that
|
|
130
|
+
* happens to contain a link, which the runtime would then show verbatim.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} packageId - The Foundry package shipping the journals pack.
|
|
133
|
+
* Supplied rather than assumed (#1498).
|
|
134
|
+
* @param {string} itemId - The item note's `id` frontmatter.
|
|
135
|
+
* @param {string} name - The item's name, used as the link's label. It shows
|
|
136
|
+
* only if the target ever fails to resolve, where a broken link naming the
|
|
137
|
+
* item beats a bare UUID.
|
|
138
|
+
* @param {string} firstPageId - The id of the entry's first page, from
|
|
139
|
+
* {@link journalPageId}.
|
|
140
|
+
* @returns {string} The pointer to store in `system.docHtml`.
|
|
141
|
+
*/
|
|
142
|
+
export function itemDocPointer(packageId, itemId, name, firstPageId) {
|
|
143
|
+
// An item doc is a *derived* document: it lands in the default
|
|
144
|
+
// JournalEntry pack whatever Item pack the item itself was routed to
|
|
145
|
+
// (#1566).
|
|
146
|
+
const entryUuid = compendiumUuid(
|
|
147
|
+
packageId,
|
|
148
|
+
"doc",
|
|
149
|
+
itemDocEntryId(itemId),
|
|
150
|
+
packRouter().defaultOf("JournalEntry"),
|
|
151
|
+
);
|
|
152
|
+
return `@UUID[${pageUuid(entryUuid, firstPageId)}]{${name}}`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
* **The resolved item-type registry** — the consuming repository's
|
|
16
|
+
* `itemBuilders` table, and the type whitelist derived from its keys.
|
|
17
|
+
*
|
|
18
|
+
* Both are read from the one resolved configuration, so they are literally the
|
|
19
|
+
* same object's keys and values: a type cannot be whitelisted for compilation
|
|
20
|
+
* without the builder that compiles it, which is the guarantee #1504 exists
|
|
21
|
+
* for. The Item compiler dispatches through {@link itemBuilder}, so the table a
|
|
22
|
+
* consumer configured is the table its notes compile with — the whitelist and
|
|
23
|
+
* the dispatch used to come from different places, and a consumer supplying its
|
|
24
|
+
* own registry got the types it asked for and the builders it did not (#1563).
|
|
25
|
+
*
|
|
26
|
+
* **The registry itself is a consumer's, and stays a leaf.** SoHL's lives in
|
|
27
|
+
* `@heroiclands/package-build/sohl/item-builders`; the consumer names it in
|
|
28
|
+
* `package-build.config.yaml`. That module must never read the resolved
|
|
29
|
+
* configuration — it is loaded *during* the resolution such a read would be
|
|
30
|
+
* asking for, whether the loader requires it by name for a data config or a
|
|
31
|
+
* code config imports it directly, so a read from there would close a cycle
|
|
32
|
+
* around the configuration's own evaluation. Data travels *into*
|
|
33
|
+
* configuration; only modules like this one, which nothing on that path
|
|
34
|
+
* imports, read back out of it.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
40
|
+
import { resolveImg } from "./helpers.mjs";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every content type that compiles into an item — and therefore into an item
|
|
44
|
+
* doc. Read by the Item compiler to know what to claim, and by the journals
|
|
45
|
+
* pass to know whose prose it is holding.
|
|
46
|
+
*
|
|
47
|
+
* **Derived, never authored.** These are the keys of the consuming
|
|
48
|
+
* repository's `itemBuilders` registry, so the whitelist and the builder table
|
|
49
|
+
* are the same list and cannot drift apart. They already had: `trait` was
|
|
50
|
+
* whitelisted long after the item type was retired (#651), with no builder
|
|
51
|
+
* behind it, so every `type: trait` note passed the gate and then failed to
|
|
52
|
+
* compile (#1504).
|
|
53
|
+
*
|
|
54
|
+
* An accessor rather than a hoisted constant, so that importing this module
|
|
55
|
+
* needs no configuration (#2).
|
|
56
|
+
*
|
|
57
|
+
* @returns {ReadonlySet<string>} The configured item types.
|
|
58
|
+
*/
|
|
59
|
+
export function itemTypes() {
|
|
60
|
+
return loadPackConfig().itemTypes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The builder the consuming repository registered for an item type.
|
|
65
|
+
*
|
|
66
|
+
* Unreachable through the compiler — its whitelist *is* this registry's keys —
|
|
67
|
+
* so a throw here means a caller invented a type. It names the type rather than
|
|
68
|
+
* failing as an anonymous `is not a function` (#1504).
|
|
69
|
+
*
|
|
70
|
+
* @param {string} type - The note's `type` frontmatter.
|
|
71
|
+
* @returns {(fm: object) => object} The builder for that type.
|
|
72
|
+
* @throws {Error} When the configuration registers no builder for `type`.
|
|
73
|
+
*/
|
|
74
|
+
export function itemBuilder(type) {
|
|
75
|
+
const builder = /** @type {Record<string, Function>} */ (
|
|
76
|
+
loadPackConfig().itemBuilders
|
|
77
|
+
)[type];
|
|
78
|
+
if (typeof builder !== "function") {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`No builder registered for item type "${type}" — add one to the ` +
|
|
81
|
+
`\`itemBuilders\` registry this repository declares in ` +
|
|
82
|
+
`package-build.config.yaml, or stop declaring the type.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return /** @type {(fm: object) => object} */ (builder);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The default art for an item type — the image a note of that type is given
|
|
90
|
+
* when it carries no `img:` of its own.
|
|
91
|
+
*
|
|
92
|
+
* Read from the consuming repository's `itemBuilders` registry, the same place
|
|
93
|
+
* the type itself is declared, so a consumer's own type can bring art a
|
|
94
|
+
* SoHL-owned table could never hold. Art used to be looked up in
|
|
95
|
+
* `sohl/default-item-art.mjs` instead: a type was configurable while its
|
|
96
|
+
* default art was not, so a second consumer's items compiled only if every one
|
|
97
|
+
* of its notes set `img:` (#7).
|
|
98
|
+
*
|
|
99
|
+
* **Still fail-fast.** A type with neither a note-level `img:` nor paired art
|
|
100
|
+
* aborts the pack build rather than shipping a mismatched icon — the contract
|
|
101
|
+
* `defaultItemArt` was written for. Only the error's *owner* changed: it now
|
|
102
|
+
* names the registry the consumer declares and can add to.
|
|
103
|
+
*
|
|
104
|
+
* **Resolved by the same rule a note's `img:` is.** The path goes through
|
|
105
|
+
* {@link resolveImg}, so `icons/relic.svg` means the consumer's own asset root
|
|
106
|
+
* in the registry exactly as it does on a note, and an already-served path
|
|
107
|
+
* (`systems/sohl/assets/…`, as every SoHL default is) passes through untouched.
|
|
108
|
+
* One spelling, one meaning, wherever it is written.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} type - the item type.
|
|
111
|
+
* @returns {string} The default image path for that type.
|
|
112
|
+
* @throws {Error} When the type's registry entry pairs no `img`.
|
|
113
|
+
*/
|
|
114
|
+
export function itemArt(type) {
|
|
115
|
+
const art = /** @type {Record<string, string|undefined>} */ (
|
|
116
|
+
loadPackConfig().itemArt
|
|
117
|
+
)[type];
|
|
118
|
+
if (!art) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`No default art for item type "${type}" — the note carries no ` +
|
|
121
|
+
`\`img:\`, and the \`itemBuilders\` entry for "${type}" in this ` +
|
|
122
|
+
`repository's package-build.config.yaml pairs none with its ` +
|
|
123
|
+
`builder. Write the entry as ` +
|
|
124
|
+
`\`${type}: { system: <builder>, img: "<path>" }\`, or give the ` +
|
|
125
|
+
`note an \`img:\` of its own.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return resolveImg(art);
|
|
129
|
+
}
|