@heroiclands/package-build 17.1.0 → 18.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.md +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
|
@@ -0,0 +1,126 @@
|
|
|
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 shape of a content-index record, and the accessors every reader of one
|
|
16
|
+
* needs (#243).
|
|
17
|
+
*
|
|
18
|
+
* **Why these live apart from the index that builds them.** Deriving records
|
|
19
|
+
* reaches the pack router and the manifest emitter, and those reach the
|
|
20
|
+
* compilers — so a module the compilers load cannot import
|
|
21
|
+
* `engine/content-index.mjs` without closing a cycle, and
|
|
22
|
+
* `engine/helpers.mjs` is exactly such a module. But nothing about *reading* a
|
|
23
|
+
* record needs any of that machinery: these are pure functions over a plain
|
|
24
|
+
* object, and `node:path` is the whole of their dependency.
|
|
25
|
+
*
|
|
26
|
+
* So the accessors sit here, where every reader can reach them, and
|
|
27
|
+
* `engine/content-index.mjs` re-exports them — it is where callers have always
|
|
28
|
+
* addressed them, and the split is an implementation detail of the import
|
|
29
|
+
* graph rather than a second place to look.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The keys the content index adds to a record, which a note therefore may not
|
|
38
|
+
* carry itself.
|
|
39
|
+
*
|
|
40
|
+
* `package` is the note's distribution unit — the configured `contentPackage`,
|
|
41
|
+
* since a note declaring its own is a hard error (#56) — and it matches what
|
|
42
|
+
* the content-table expander puts on the same field, so a query reads the same
|
|
43
|
+
* value from either. `file` namespaces the note's place in the tree, again
|
|
44
|
+
* matching the expander's `file.*`.
|
|
45
|
+
*
|
|
46
|
+
* Both are checked rather than assumed: `folder` is real frontmatter on most
|
|
47
|
+
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
48
|
+
* overwrite is a plausible future rather than a hypothetical one.
|
|
49
|
+
*
|
|
50
|
+
* @type {ReadonlyArray<string>}
|
|
51
|
+
*/
|
|
52
|
+
export const DERIVED_KEYS = Object.freeze([
|
|
53
|
+
"package",
|
|
54
|
+
"file",
|
|
55
|
+
"address",
|
|
56
|
+
"anchors",
|
|
57
|
+
"nameAscii",
|
|
58
|
+
"aliasesAscii",
|
|
59
|
+
"foundry",
|
|
60
|
+
"documentation",
|
|
61
|
+
"documents",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The file a record was read from, as an absolute path.
|
|
66
|
+
*
|
|
67
|
+
* **The one composition, because there were four.** `file.path` is recorded
|
|
68
|
+
* *relative* to the content root deliberately — an absolute one is a fact about
|
|
69
|
+
* the machine that built the index, would differ between two checkouts of the
|
|
70
|
+
* same tree, and would put someone's home directory in a published artifact. So
|
|
71
|
+
* every pass that reads the index and then needs to open a note has to compose
|
|
72
|
+
* the absolute form, and each of the readers converted for #243 had written its
|
|
73
|
+
* own `path.join(base, ...record.file.path.split("/"))`. Four copies of one
|
|
74
|
+
* rule is what #243 exists to remove, so here it is once.
|
|
75
|
+
*
|
|
76
|
+
* The split is on `"/"` rather than `path.sep` because the recorded path is
|
|
77
|
+
* always POSIX — that is what makes the index identical on every platform.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} contentBase - Root of the content tree the index was built from.
|
|
80
|
+
* @param {object} record - An index record.
|
|
81
|
+
* @returns {string} The note's absolute path.
|
|
82
|
+
*/
|
|
83
|
+
export function noteFile(contentBase, record) {
|
|
84
|
+
return path.join(contentBase, ...String(record?.file?.path ?? "").split("/"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The note's own frontmatter, as authored, from an index record.
|
|
89
|
+
*
|
|
90
|
+
* The inverse of the record's spread, and exact rather than best-effort: a
|
|
91
|
+
* record is the note's frontmatter plus {@link DERIVED_KEYS}, and a note that
|
|
92
|
+
* authors one of those keys fails the walk — so removing them cannot remove
|
|
93
|
+
* anything the note wrote. That enforced pairing is what lets a pass read the
|
|
94
|
+
* corpus from the index and still lint, route or compile what the *author*
|
|
95
|
+
* typed, rather than reasoning about `address:` and `anchors:` as though
|
|
96
|
+
* someone had written them.
|
|
97
|
+
*
|
|
98
|
+
* Lives beside the list it is the inverse of, so the two cannot drift.
|
|
99
|
+
*
|
|
100
|
+
* @param {Record<string, any>} record - An index record.
|
|
101
|
+
* @returns {Record<string, any>} The frontmatter, without the derived keys.
|
|
102
|
+
*/
|
|
103
|
+
export function authoredFrontmatter(record) {
|
|
104
|
+
const fm = {};
|
|
105
|
+
for (const [key, value] of Object.entries(record ?? {})) {
|
|
106
|
+
if (!DERIVED_KEYS.includes(key)) fm[key] = value;
|
|
107
|
+
}
|
|
108
|
+
return fm;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether a record is a note's, rather than a documentation journal's.
|
|
113
|
+
*
|
|
114
|
+
* An item note yields two records — itself and the JournalEntry its prose
|
|
115
|
+
* compiles into — and the second is a document, not a note: it has no file of
|
|
116
|
+
* its own to read, no frontmatter an author wrote, and its `type` is the
|
|
117
|
+
* virtual `doc<type>` that `readQualifier` resolves rather than a type any tree
|
|
118
|
+
* declares. A reader enumerating the corpus wants the notes; one resolving an
|
|
119
|
+
* address wants both.
|
|
120
|
+
*
|
|
121
|
+
* @param {Record<string, any>} record - An index record.
|
|
122
|
+
* @returns {boolean} True for a note's own record.
|
|
123
|
+
*/
|
|
124
|
+
export function isNoteRecord(record) {
|
|
125
|
+
return !record?.documents;
|
|
126
|
+
}
|
package/engine/index.mjs
CHANGED
|
@@ -57,6 +57,9 @@ export * as packRouter from "./pack-router.mjs";
|
|
|
57
57
|
/** Which note types a configuration compiles, and the notes nothing claims (#146). */
|
|
58
58
|
export * as noteClaims from "./note-claims.mjs";
|
|
59
59
|
|
|
60
|
+
/** Folders as notes: the address index, the parent chain, and the emitted `Folder` (#256). */
|
|
61
|
+
export * as folderNotes from "./folder-notes.mjs";
|
|
62
|
+
|
|
60
63
|
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
61
64
|
export * as contentPackage from "./content-package.mjs";
|
|
62
65
|
|
|
@@ -75,6 +78,9 @@ export * as noteSchemas from "./note-schemas.mjs";
|
|
|
75
78
|
/** The closed frontmatter regions: the `data:` container and each type's `subType`. */
|
|
76
79
|
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
77
80
|
|
|
81
|
+
/** The closed registry of system ids, and the `none` that stands for no system. */
|
|
82
|
+
export * as systems from "./systems.mjs";
|
|
83
|
+
|
|
78
84
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
79
85
|
|
|
80
86
|
/** The one normalisation this build makes: prose to a URL-safe token. */
|
|
@@ -83,14 +89,20 @@ export * as contentSlug from "./content-slug.mjs";
|
|
|
83
89
|
/** Where a content note publishes: its section, and its `type-shortcode` URL. */
|
|
84
90
|
export * as contentAddress from "./content-address.mjs";
|
|
85
91
|
|
|
86
|
-
/**
|
|
87
|
-
export * as
|
|
92
|
+
/** Which note-type → document-subtype maps this toolchain ships. */
|
|
93
|
+
export * as subtypeRegistry from "./subtype-registry.mjs";
|
|
94
|
+
|
|
95
|
+
/** The id a note's document is filed under: its pin, or its address (#270). */
|
|
96
|
+
export * as noteIds from "./note-ids.mjs";
|
|
88
97
|
|
|
89
|
-
/** The
|
|
90
|
-
export * as
|
|
98
|
+
/** The shortcodes a note declares it used to be published under (#278). */
|
|
99
|
+
export * as noteRenames from "./note-renames.mjs";
|
|
100
|
+
|
|
101
|
+
/** The published content index packages exchange addresses through (#239). */
|
|
102
|
+
export * as metadataIndex from "./metadata-index.mjs";
|
|
91
103
|
|
|
92
104
|
/** Deriving this package's own link manifest from its content tree. */
|
|
93
|
-
export * as
|
|
105
|
+
export * as foundryEntries from "./foundry-entries.mjs";
|
|
94
106
|
|
|
95
107
|
/** Publishing the note tree as a queryable JSON Lines index. */
|
|
96
108
|
export * as contentIndex from "./content-index.mjs";
|
|
@@ -146,6 +158,12 @@ export * as mapNotes from "./map-notes.mjs";
|
|
|
146
158
|
/** The Scene and Adventure compiler. */
|
|
147
159
|
export * as scenes from "./scenes.mjs";
|
|
148
160
|
|
|
161
|
+
/** What a bundle note says, and how compiled documents become an Adventure. */
|
|
162
|
+
export * as bundleNotes from "./bundle-notes.mjs";
|
|
163
|
+
|
|
164
|
+
/** The Adventure compiler — the pass a `type: bundle` note compiles through. */
|
|
165
|
+
export * as bundles from "./bundles.mjs";
|
|
166
|
+
|
|
149
167
|
/** The compiled-pack Scene/Level integrity guard. */
|
|
150
168
|
export * as sceneLevels from "./scene-levels.mjs";
|
|
151
169
|
|
|
@@ -0,0 +1,349 @@
|
|
|
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 Item pass, for any system** — everything about compiling a note into a
|
|
16
|
+
* Foundry Item that is a fact about the *note format* rather than about a
|
|
17
|
+
* particular game system (#139).
|
|
18
|
+
*
|
|
19
|
+
* `sohl/items.mjs` was the whole of it, and every system-specific thing in it
|
|
20
|
+
* was reached through one module-level constant read off SoHL's map. That is
|
|
21
|
+
* exactly the shape a second system needs, so the class moved here and the two
|
|
22
|
+
* halves declare what differs:
|
|
23
|
+
*
|
|
24
|
+
* | stated by the subclass | what it decides |
|
|
25
|
+
* | --- | --- |
|
|
26
|
+
* | `static documentSubtypes` | the block its notes write, the types it claims, the subtype each becomes |
|
|
27
|
+
* | `commonSystem()` | the `system` keys this system's compiler writes on every item |
|
|
28
|
+
* | `commonFlags()` | the `flags` it writes on every item, beside the authored ones |
|
|
29
|
+
*
|
|
30
|
+
* Everything else — claiming a note, looking the subtype up, resolving the art,
|
|
31
|
+
* merging the authored `<system>.system` block, checking what was emitted
|
|
32
|
+
* against the receiving schema, writing the envelope — is one implementation
|
|
33
|
+
* serving both. That is not tidiness: it is the guarantee that a second
|
|
34
|
+
* system's items are compiled by the code the first system's are, so a fix to
|
|
35
|
+
* either is a fix to both.
|
|
36
|
+
*
|
|
37
|
+
* **The `system` block has two authors, and the split is deliberate.** The
|
|
38
|
+
* declared fields come from the consuming repository's `itemBuilders` registry,
|
|
39
|
+
* addressed by *this pass's system*, so a type both systems declare is built by
|
|
40
|
+
* the right one. On top of that sits {@link SystemItemCompiler#commonSystem} —
|
|
41
|
+
* the keys the compiler writes on its own initiative for every item of the
|
|
42
|
+
* system, which no field declaration states and which nothing else could
|
|
43
|
+
* therefore check. Both are checked against the receiving subtype's published
|
|
44
|
+
* schema before the document is written.
|
|
45
|
+
*
|
|
46
|
+
* @module
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import log from "loglevel";
|
|
50
|
+
|
|
51
|
+
import { resolveName, resolveImg } from "./helpers.mjs";
|
|
52
|
+
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
53
|
+
import { journalPageId, splitPages } from "./journals.mjs";
|
|
54
|
+
import { foundryPackageId } from "./content-package.mjs";
|
|
55
|
+
import { itemDocEntryId, itemDocPointer } from "./item-docs.mjs";
|
|
56
|
+
// The whitelist and the per-type `system` builders both come from the resolved
|
|
57
|
+
// configuration, so the types this pass claims and the builders it compiles
|
|
58
|
+
// them with are one table — the consuming repository's, not this package's
|
|
59
|
+
// (#1504/#1563).
|
|
60
|
+
import { itemTypes, itemBuilder, itemArt, itemFields } from "./item-registry.mjs";
|
|
61
|
+
import { currentType } from "./ids.mjs";
|
|
62
|
+
// Which Foundry Item subtype a note's `type` compiles into. Looked up in the
|
|
63
|
+
// system's declared map, never inferred from the type itself (#79).
|
|
64
|
+
import { documentSubtype, subtypeRow } from "./document-subtypes.mjs";
|
|
65
|
+
// The note-level `<system>:` block: `<system>.system` onto the document's
|
|
66
|
+
// `system` verbatim, and `<system>.img` / `.effects` / `.flags` overriding
|
|
67
|
+
// their shared top-level forms for this system alone (#58).
|
|
68
|
+
import { blockField, blockProperty, claimedPaths, mergeSystemData } from "./system-block.mjs";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The description an item carries: a pointer to its **item doc**, the
|
|
72
|
+
* JournalEntry the journals pass compiles this same body into (#1348).
|
|
73
|
+
*
|
|
74
|
+
* The prose is not rendered into the item at all. Carrying it would duplicate
|
|
75
|
+
* it onto every actor holding the item — 7.59 MB of copies across the actors
|
|
76
|
+
* pack, of which 133 KB was distinct — where a link is 60 bytes and always
|
|
77
|
+
* current. The two passes derive the target from the note's own id, so neither
|
|
78
|
+
* has to see the other's output; both split the *converted* markdown, so an H1
|
|
79
|
+
* carrying a wikilink names the same page on both sides.
|
|
80
|
+
*
|
|
81
|
+
* An item with no prose points at nothing, exactly as the journals pass writes
|
|
82
|
+
* no entry for it.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} markdown - The note body, tables expanded and wikilinks
|
|
85
|
+
* resolved.
|
|
86
|
+
* @param {object} fm - The note's frontmatter.
|
|
87
|
+
* @param {string} name - The item's name.
|
|
88
|
+
* @returns {string} The pointer, or "" for a note with no body.
|
|
89
|
+
*/
|
|
90
|
+
export function itemDescription(markdown, fm, name) {
|
|
91
|
+
if (!String(markdown).trim()) return "";
|
|
92
|
+
const [leadPage] = splitPages(markdown, name);
|
|
93
|
+
const pageId = journalPageId(itemDocEntryId(fm.id), leadPage);
|
|
94
|
+
return itemDocPointer(foundryPackageId(), fm.id, name, pageId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The Item compile pass of one game system.
|
|
99
|
+
*
|
|
100
|
+
* A subclass declares its {@link SystemItemCompiler.documentSubtypes} and, if
|
|
101
|
+
* its system writes any, the {@link SystemItemCompiler#commonSystem} keys. It
|
|
102
|
+
* declares nothing else: the class is abstract only in the sense that a map is
|
|
103
|
+
* required, and instantiating it without one is a programming error rather than
|
|
104
|
+
* a configuration one.
|
|
105
|
+
*/
|
|
106
|
+
export class SystemItemCompiler extends BasePackCompiler {
|
|
107
|
+
static id = "items";
|
|
108
|
+
static label = "item";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* An Item **is** a system's data, so this pack takes only notes carrying
|
|
112
|
+
* this system's block (#58).
|
|
113
|
+
*/
|
|
114
|
+
static requiresSystemBlock = true;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The note-type → document-subtype map this pass compiles against.
|
|
118
|
+
*
|
|
119
|
+
* Stated by the class rather than reached for through a module import, so
|
|
120
|
+
* every decision the pass makes — which notes it claims, which subtype each
|
|
121
|
+
* becomes, which registry builds it, which block it reads — reads one
|
|
122
|
+
* declaration. A second system replaces that declaration and nothing else.
|
|
123
|
+
*
|
|
124
|
+
* @type {import("./document-subtypes.mjs").DocumentSubtypeMap|undefined}
|
|
125
|
+
*/
|
|
126
|
+
static documentSubtypes = undefined;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The frontmatter block this pass reads, and the registry it addresses.
|
|
130
|
+
*
|
|
131
|
+
* @returns {string} The system's block name.
|
|
132
|
+
*/
|
|
133
|
+
get system() {
|
|
134
|
+
const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
|
|
135
|
+
if (!map) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${this.constructor.name} declares no \`documentSubtypes\` — an ` +
|
|
138
|
+
`Item pass reads its system's map to know which notes it ` +
|
|
139
|
+
`claims and what each becomes.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return map.block;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* How many of each item type this pass wrote, for the summary. Every type
|
|
147
|
+
* is present from the start so the tally reads as a census of the
|
|
148
|
+
* whitelist rather than of what happened to compile.
|
|
149
|
+
*
|
|
150
|
+
* @type {Record<string, number>}
|
|
151
|
+
*/
|
|
152
|
+
counts = Object.fromEntries([...itemTypes()].map((t) => [t, 0]));
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every content type that compiles into an item **for this system**.
|
|
156
|
+
*
|
|
157
|
+
* The whitelist is the consuming repository's `itemBuilders` keys (#1504),
|
|
158
|
+
* and the system's own map is a second filter on top of it: a type this
|
|
159
|
+
* system maps onto some *other* document class is not an item however a
|
|
160
|
+
* registry spells it, which is the "no wrongly-typed document" half of #79.
|
|
161
|
+
* A type the map does not name at all is left to the registry — see
|
|
162
|
+
* {@link SystemItemCompiler#itemSubtype}.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} fm - The note's frontmatter.
|
|
165
|
+
* @returns {boolean} True for a whitelisted item type.
|
|
166
|
+
*/
|
|
167
|
+
selects(fm) {
|
|
168
|
+
// Through {@link currentType}: the registry is keyed by the current
|
|
169
|
+
// spelling, and a note still on a renamed one compiles unchanged
|
|
170
|
+
// during the retirement window (#78).
|
|
171
|
+
if (!fm.type || !itemTypes().has(currentType(fm.type))) return false;
|
|
172
|
+
const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
|
|
173
|
+
const row = subtypeRow(/** @type {never} */ (map), fm.type);
|
|
174
|
+
return !row || row.document === "Item";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The Foundry Item subtype a note compiles into.
|
|
179
|
+
*
|
|
180
|
+
* **Looked up, not inferred.** For every type this system declares, the
|
|
181
|
+
* emitted subtype is the map's, so the note vocabulary and the document
|
|
182
|
+
* vocabulary are two separately-stated things rather than one string
|
|
183
|
+
* written twice (#79).
|
|
184
|
+
*
|
|
185
|
+
* **A type the map does not name belongs to the consumer**, and its
|
|
186
|
+
* registry entry is the declaration: a repository shipping an item type of
|
|
187
|
+
* its own writes it once, in the `itemBuilders` table of its
|
|
188
|
+
* `package-build.config.yaml`, and that key is what the document is a
|
|
189
|
+
* subtype of. That is an authored statement in the consumer's own
|
|
190
|
+
* configuration, not a coincidence inside this package's source — and
|
|
191
|
+
* refusing it here would silently drop every document of a type this system
|
|
192
|
+
* has no opinion about (#7/#1563).
|
|
193
|
+
*
|
|
194
|
+
* @param {object} fm - The note's frontmatter.
|
|
195
|
+
* @returns {string} The document's `type`.
|
|
196
|
+
*/
|
|
197
|
+
itemSubtype(fm) {
|
|
198
|
+
const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
|
|
199
|
+
const declared = documentSubtype(/** @type {never} */ (map), fm.type, fm, {
|
|
200
|
+
absPath: this.currentNote?.absPath,
|
|
201
|
+
});
|
|
202
|
+
return declared ?? fm.type;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** An item is named by its own type in the log, not by "item". */
|
|
206
|
+
noteLabel(fm) {
|
|
207
|
+
return fm.type;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The `system` keys this system's compiler writes on **every** item, beside
|
|
212
|
+
* whatever the type's declared fields emit.
|
|
213
|
+
*
|
|
214
|
+
* Nothing by default, which is the honest position for a system that has
|
|
215
|
+
* not said otherwise: a key written here lands on every document of every
|
|
216
|
+
* type, so inventing one that the receiving DataModel does not declare
|
|
217
|
+
* would be a finding on the whole pack (#155).
|
|
218
|
+
*
|
|
219
|
+
* @param {object} fm - The note's frontmatter.
|
|
220
|
+
* @param {object} at - What the pass already knows about this note.
|
|
221
|
+
* @param {string} at.description - The pointer to the note's item doc.
|
|
222
|
+
* @param {string} at.markdown - The note body, tables expanded and
|
|
223
|
+
* wikilinks resolved, for a system that reads an anchored section out of
|
|
224
|
+
* it.
|
|
225
|
+
* @param {string} at.label - Human-readable context for error messages.
|
|
226
|
+
* @returns {object} The shared `system` fields.
|
|
227
|
+
*/
|
|
228
|
+
// eslint-disable-next-line no-unused-vars
|
|
229
|
+
commonSystem(fm, { description, markdown, label }) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The document's `flags` — whatever the note authors, and whatever this
|
|
235
|
+
* system writes there of its own accord.
|
|
236
|
+
*
|
|
237
|
+
* The authored flags alone by default, which is the honest position for a
|
|
238
|
+
* system that has not said otherwise. A system whose data model has nowhere
|
|
239
|
+
* to record a shared fact keeps it here instead: HM3 writes the template
|
|
240
|
+
* priority as `flags.hm3.templatePriority`, because it declares no `system`
|
|
241
|
+
* field for it and an undeclared `system` key is discarded at load without
|
|
242
|
+
* a word.
|
|
243
|
+
*
|
|
244
|
+
* **This is the one emitted key nothing else can check** (#283). A `system`
|
|
245
|
+
* key this pass invents is caught by the emitted-`system` check against the
|
|
246
|
+
* receiving schema, but a flag is declared by no schema — so an omission
|
|
247
|
+
* here is silent, and was: the Actor pass wrote the priority and this one
|
|
248
|
+
* did not, for as long as there had been two passes.
|
|
249
|
+
*
|
|
250
|
+
* @param {object} fm - The note's frontmatter.
|
|
251
|
+
* @returns {object} The flags to emit.
|
|
252
|
+
*/
|
|
253
|
+
commonFlags(fm) {
|
|
254
|
+
return blockProperty(fm, this.system, "flags", {});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Construct the full compendium envelope for one item.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} fm - The note's frontmatter.
|
|
261
|
+
* @param {string} markdown - The body, tables expanded and wikilinks
|
|
262
|
+
* resolved.
|
|
263
|
+
* @returns {object} The item document, keyed for the pack.
|
|
264
|
+
*/
|
|
265
|
+
buildEntry(fm, markdown) {
|
|
266
|
+
const system = this.system;
|
|
267
|
+
const type = fm.type;
|
|
268
|
+
const name = resolveName(fm);
|
|
269
|
+
const description = itemDescription(markdown, fm, name);
|
|
270
|
+
const id = fm.id;
|
|
271
|
+
const subType = this.itemSubtype(fm);
|
|
272
|
+
const built = {
|
|
273
|
+
...this.commonSystem(fm, { description, markdown, label: `item "${name}"` }),
|
|
274
|
+
...itemBuilder(type, system)(fm),
|
|
275
|
+
};
|
|
276
|
+
// Whatever the note authors under `<system>.system`, at the DataModel's
|
|
277
|
+
// own paths. A path a declared field already writes is left to that
|
|
278
|
+
// field: its value came from the same authored place and went through
|
|
279
|
+
// the field's own coercion (#58).
|
|
280
|
+
mergeSystemData(built, fm, {
|
|
281
|
+
block: system,
|
|
282
|
+
claimed: claimedPaths(itemFields(type, system)),
|
|
283
|
+
});
|
|
284
|
+
this.reportUndeclaredSystemData(fm, system, "Item", subType);
|
|
285
|
+
// And what *this* pass wrote on its own initiative, which no field
|
|
286
|
+
// declaration states and so no other check can see (#155). Read off the
|
|
287
|
+
// assembled block, so a key added to `commonSystem` is checked without
|
|
288
|
+
// anyone remembering to list it.
|
|
289
|
+
this.reportEmittedSystemData(built, {
|
|
290
|
+
fm,
|
|
291
|
+
block: system,
|
|
292
|
+
documentType: "Item",
|
|
293
|
+
subType,
|
|
294
|
+
type,
|
|
295
|
+
fields: itemFields(type, system),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const effects = blockProperty(fm, system, "effects");
|
|
299
|
+
// Read through the system block like every other item field, so both
|
|
300
|
+
// spellings work wherever a note already writes one. `packFolder` is a
|
|
301
|
+
// folder note's address and `folder` an id; which it is comes from the
|
|
302
|
+
// field, never from the string (#251, #255).
|
|
303
|
+
const packFolderAddress = blockField(fm, system, "packFolder", null);
|
|
304
|
+
const folder =
|
|
305
|
+
packFolderAddress ?
|
|
306
|
+
this.folderResolver(packFolderAddress, { isAddress: true })
|
|
307
|
+
: this.folderResolver(blockField(fm, system, "folder", null));
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
name,
|
|
311
|
+
// The note's `type` addresses the builder and the default art —
|
|
312
|
+
// both registries are keyed by content type — while the document's
|
|
313
|
+
// own subtype comes from the system's map (#79).
|
|
314
|
+
type: subType,
|
|
315
|
+
// Nullish, not `||` (#218): `resolveImg` returns `null` for a
|
|
316
|
+
// note that names no art and `""` for one that wants none, and only
|
|
317
|
+
// the first may be replaced by the type's default.
|
|
318
|
+
img: resolveImg(blockProperty(fm, system, "img")) ?? itemArt(type, system),
|
|
319
|
+
_id: id,
|
|
320
|
+
system: built,
|
|
321
|
+
effects: Array.isArray(effects) ? [...effects] : [],
|
|
322
|
+
// Whatever the note authors, plus whatever this system records in
|
|
323
|
+
// flags because its data model has nowhere else for it (#283).
|
|
324
|
+
flags: this.commonFlags(fm),
|
|
325
|
+
_stats: this.stats,
|
|
326
|
+
ownership: { default: 0 },
|
|
327
|
+
folder,
|
|
328
|
+
_key: `!items!${id}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @inheritdoc */
|
|
333
|
+
onCompiled(fm) {
|
|
334
|
+
this.counts[currentType(fm.type)]++;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @inheritdoc */
|
|
338
|
+
reportCompiled(stats) {
|
|
339
|
+
log.info(`Compiled ${stats.compiled} items:`);
|
|
340
|
+
for (const [t, n] of Object.entries(this.counts)) {
|
|
341
|
+
if (n > 0) log.info(` ${t}: ${n}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** @inheritdoc */
|
|
346
|
+
reportDetail(stats) {
|
|
347
|
+
log.debug(`Skipped ${stats.skippedOther} non-item file(s) (no recognized type)`);
|
|
348
|
+
}
|
|
349
|
+
}
|
package/engine/item-docs.mjs
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import { compendiumUuid, makeId, pageUuid } from "./ids.mjs";
|
|
46
46
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
47
|
+
import { currentType } from "./ids.mjs";
|
|
47
48
|
import { itemTypes } from "./item-registry.mjs";
|
|
48
49
|
import { packRouter } from "./pack-router.mjs";
|
|
49
50
|
|
|
@@ -97,7 +98,12 @@ export function docEntryTypes() {
|
|
|
97
98
|
* false for `doc` and for actors.
|
|
98
99
|
*/
|
|
99
100
|
export function hasDocEntry(type) {
|
|
100
|
-
|
|
101
|
+
// Through {@link currentType}, because the set is derived from the item
|
|
102
|
+
// registry's keys and those are the *current* spelling of a note type. A
|
|
103
|
+
// note still on a renamed one carries its documentation journal exactly as
|
|
104
|
+
// before — this is the one lookup between an item compiling and its prose
|
|
105
|
+
// silently compiling into nothing (#78).
|
|
106
|
+
return docEntryTypes().has(String(currentType(type)));
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/**
|
package/engine/item-registry.mjs
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* @module
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import { currentType } from "./ids.mjs";
|
|
43
44
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
44
45
|
import { resolveImg } from "./helpers.mjs";
|
|
45
46
|
|
|
@@ -83,6 +84,11 @@ export function itemTypes() {
|
|
|
83
84
|
*/
|
|
84
85
|
function lookup(what, table, type, system) {
|
|
85
86
|
const config = loadPackConfig();
|
|
87
|
+
// The registry is keyed by the current spelling of a note type, and a note
|
|
88
|
+
// still on a renamed one resolves through it unchanged. One place, because
|
|
89
|
+
// every type-keyed table this module reaches — builders, art, fields — is
|
|
90
|
+
// indexed here (#78).
|
|
91
|
+
type = currentType(type);
|
|
86
92
|
if (system !== undefined) {
|
|
87
93
|
const perSystem = /** @type {Record<string, Record<string, unknown>>} */ (
|
|
88
94
|
config[`${table}BySystem`]
|