@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,84 @@
|
|
|
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
|
+
* **HM3's item-type registry** — every content type that compiles into an HM3
|
|
16
|
+
* Foundry Item, keyed to the builder that produces its `system` block.
|
|
17
|
+
*
|
|
18
|
+
* The same arrangement as `sohl/item-builders.mjs`, and a repository feeding
|
|
19
|
+
* both systems names both: `itemBuilders: [sohl, hm3]`. The engine keeps them
|
|
20
|
+
* apart from there on — `itemBuilder(type, system)` takes the system that is
|
|
21
|
+
* asking, and a type both registries declare **throws** rather than resolving
|
|
22
|
+
* when nobody says which (#58). That guard existed before there was a second
|
|
23
|
+
* registry to trip it; this is the registry it was written for.
|
|
24
|
+
*
|
|
25
|
+
* **The builders are generated, not written**, from the field declarations in
|
|
26
|
+
* `item-fields.mjs`, compiled against the `hm3` block. `buildFromFields` takes
|
|
27
|
+
* the block as an argument precisely so one declaration mechanism serves any
|
|
28
|
+
* number of systems: `data.weight` reaches `system.weightBase` under `sohl:`
|
|
29
|
+
* and `system.weight` under `hm3:`, from two declarations and one engine.
|
|
30
|
+
*
|
|
31
|
+
* **No finalizers.** SoHL has one — a combat technique's strike mode, whose
|
|
32
|
+
* *presence* depends on another field's value — and HM3 has no such
|
|
33
|
+
* conditional. The absence is worth stating: a finalizer is opaque to every
|
|
34
|
+
* reader a declaration is legible to, so having none is the state to keep.
|
|
35
|
+
*
|
|
36
|
+
* **A leaf.** It imports the declaration primitives, the field table and the
|
|
37
|
+
* art map, and never the resolved configuration — the configuration file
|
|
38
|
+
* imports *this*, so a read back out would close a cycle around the
|
|
39
|
+
* configuration's own evaluation.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { buildFromFields } from "../engine/field-spec.mjs";
|
|
45
|
+
import { hm3DefaultItemArt } from "./default-item-art.mjs";
|
|
46
|
+
import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
47
|
+
import { HM3_ITEM_FIELDS } from "./item-fields.mjs";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The frontmatter block these builders read.
|
|
51
|
+
*
|
|
52
|
+
* Taken from the map rather than spelled here, so the block name, the subtype
|
|
53
|
+
* map and the registry are one statement.
|
|
54
|
+
*
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
const BLOCK = HM3_DOCUMENT_SUBTYPES.block;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build one registry entry: the declared fields, the builder they generate, and
|
|
61
|
+
* the default art for the type.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} type - The note type, and the art map's key.
|
|
64
|
+
* @returns {Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>}
|
|
65
|
+
* The registry entry.
|
|
66
|
+
*/
|
|
67
|
+
function entryFor(type) {
|
|
68
|
+
const fields = HM3_ITEM_FIELDS[type];
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
system: buildFromFields(fields, { block: BLOCK }),
|
|
71
|
+
img: hm3DefaultItemArt(type),
|
|
72
|
+
fields,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every HM3 item type, paired with the builder for its `system` block, the
|
|
78
|
+
* default art for the type, and the frontmatter fields it declares.
|
|
79
|
+
*
|
|
80
|
+
* @type {Readonly<Record<string, Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>>>}
|
|
81
|
+
*/
|
|
82
|
+
export const HM3_ITEM_BUILDERS = Object.freeze(
|
|
83
|
+
Object.fromEntries(Object.keys(HM3_ITEM_FIELDS).map((type) => [type, entryFor(type)])),
|
|
84
|
+
);
|
|
@@ -0,0 +1,180 @@
|
|
|
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 `hm3:` frontmatter vocabulary of every HM3 item type.
|
|
16
|
+
*
|
|
17
|
+
* The same arrangement `sohl/item-fields.mjs` describes: the declaration **is**
|
|
18
|
+
* the builder — {@link buildFromFields} turns each list into the function that
|
|
19
|
+
* runs — so a field that is not here is not emitted, and there is no second
|
|
20
|
+
* statement of the mapping to drift.
|
|
21
|
+
*
|
|
22
|
+
* **Deliberately shorter than SoHL's, and that is the honest answer.** The
|
|
23
|
+
* mapping tables in `docs/content-format.md` are the specification for what a
|
|
24
|
+
* shared `data:` fact becomes in each system, and HM3's column is `NA` far more
|
|
25
|
+
* often than SoHL's: a weapon's weight and value cross over, its quality and
|
|
26
|
+
* durability do not. Every row below is one the specification states. Where it
|
|
27
|
+
* says nothing — HM3's `armorlocation` has no `data:` row at all, and `trauma`
|
|
28
|
+
* and `mysticalability` have none either — this file declares nothing rather
|
|
29
|
+
* than inventing a plausible path. A guessed `to` compiles clean and is
|
|
30
|
+
* discarded by Foundry at load without a word, which is exactly the failure the
|
|
31
|
+
* schema check exists to catch and exactly the failure a guess would create.
|
|
32
|
+
*
|
|
33
|
+
* **One shared source, two destinations.** `name` is the shared property a
|
|
34
|
+
* field draws from and `to` is where it lands, so a single authored `weight`
|
|
35
|
+
* feeds `sohl.system.weightBase` *and* `hm3.system.weight` — one authored fact,
|
|
36
|
+
* two documents (#58). That is the whole reason the two are declared
|
|
37
|
+
* separately.
|
|
38
|
+
*
|
|
39
|
+
* The specification writes those sources as `data.weight`, and the sources here
|
|
40
|
+
* are written bare, exactly as SoHL's are: the `data:` region is #128's
|
|
41
|
+
* migration and neither half has moved to it. Spelling HM3's differently would
|
|
42
|
+
* make a note feeding both systems author the same fact twice for no gain, and
|
|
43
|
+
* would take these rows out of reach of `content-build content-format fields`,
|
|
44
|
+
* which pairs a `data.<key>` claim with a bare declared `<key>`. Both halves
|
|
45
|
+
* move together when #128 lands.
|
|
46
|
+
*
|
|
47
|
+
* **What a note authors under `hm3.system` is not declared here.** HM3 fields
|
|
48
|
+
* that no shared `data:` property feeds — a skill's `type`, an armour's
|
|
49
|
+
* `protection`, a character's `abilities` — are written at their own paths in
|
|
50
|
+
* the note's `hm3.system` block and reach the document through the verbatim
|
|
51
|
+
* passthrough. They are checked against HM3's published `schema.json` like
|
|
52
|
+
* everything else; they simply have no shared source to declare.
|
|
53
|
+
*
|
|
54
|
+
* **A leaf**, like its SoHL counterpart: declaration primitives only, never the
|
|
55
|
+
* resolved configuration.
|
|
56
|
+
*
|
|
57
|
+
* @module
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { AS_AUTHORED, NUMBER } from "../engine/field-spec.mjs";
|
|
61
|
+
|
|
62
|
+
/* --------------------------------------------------------------------- */
|
|
63
|
+
/* Rows shared by the four gear types */
|
|
64
|
+
/* --------------------------------------------------------------------- */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What HM3's gear template takes from the shared `data:` region.
|
|
68
|
+
*
|
|
69
|
+
* `weight` and `value` are the two rows every gear table in the content format
|
|
70
|
+
* gives an HM3 destination. `quality` and `durability` are SoHL's alone — HM3
|
|
71
|
+
* carries `weaponQuality` and `armorQuality` on the subtypes that have them,
|
|
72
|
+
* which is a different fact about a different thing, so the shared row is `NA`
|
|
73
|
+
* and nothing is emitted.
|
|
74
|
+
*
|
|
75
|
+
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
76
|
+
*/
|
|
77
|
+
const GEAR_COMMON = Object.freeze([
|
|
78
|
+
{
|
|
79
|
+
name: "weight",
|
|
80
|
+
to: "weight",
|
|
81
|
+
...AS_AUTHORED,
|
|
82
|
+
kind: "number",
|
|
83
|
+
default: 0,
|
|
84
|
+
describe: "Weight of one, in pounds.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "value",
|
|
88
|
+
to: "value",
|
|
89
|
+
...AS_AUTHORED,
|
|
90
|
+
kind: "number",
|
|
91
|
+
default: 0,
|
|
92
|
+
describe: "Worth of one, in pence.",
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The stack count, for the gear types whose content-format table gives it an
|
|
98
|
+
* HM3 destination.
|
|
99
|
+
*
|
|
100
|
+
* `armor`, `containergear` and `weapongear` are excluded on purpose: their
|
|
101
|
+
* tables state that quantity is always 1 and may not be authored, so the note
|
|
102
|
+
* has nothing to say and HM3's own `initial` is the right value to ship.
|
|
103
|
+
*
|
|
104
|
+
* @type {import("../engine/field-spec.mjs").FieldSpec}
|
|
105
|
+
*/
|
|
106
|
+
const QUANTITY = Object.freeze({
|
|
107
|
+
name: "quantity",
|
|
108
|
+
to: "quantity",
|
|
109
|
+
...NUMBER,
|
|
110
|
+
default: 1,
|
|
111
|
+
describe: "How many the stack holds.",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/* --------------------------------------------------------------------- */
|
|
115
|
+
/* Per-type declarations */
|
|
116
|
+
/* --------------------------------------------------------------------- */
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Every HM3 item type's frontmatter vocabulary, in the order the `system` block
|
|
120
|
+
* emits it.
|
|
121
|
+
*
|
|
122
|
+
* The keys are **note** types, not HM3 document subtypes — `projectile`
|
|
123
|
+
* rather than `missilegear`, `mysticalability` rather than `psionic` — because
|
|
124
|
+
* a registry is addressed by what a note calls itself. What the document is
|
|
125
|
+
* called is the map's answer, and only the map's.
|
|
126
|
+
*
|
|
127
|
+
* @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
|
|
128
|
+
*/
|
|
129
|
+
export const HM3_ITEM_FIELDS = Object.freeze({
|
|
130
|
+
armor: Object.freeze([...GEAR_COMMON]),
|
|
131
|
+
|
|
132
|
+
// The one subtype that extends the Foundry base directly, with none of the
|
|
133
|
+
// shared templates. Its content-format table has a single row, and that row
|
|
134
|
+
// is `NA` on both sides — so an armour location is authored entirely under
|
|
135
|
+
// `hm3.system` (`probWeight`, `impactType`, the per-aspect protections) and
|
|
136
|
+
// there is nothing for a shared source to feed.
|
|
137
|
+
armorlocation: Object.freeze([]),
|
|
138
|
+
|
|
139
|
+
containergear: Object.freeze([
|
|
140
|
+
...GEAR_COMMON,
|
|
141
|
+
{
|
|
142
|
+
name: "capacity",
|
|
143
|
+
to: "capacity.max",
|
|
144
|
+
...NUMBER,
|
|
145
|
+
default: 0,
|
|
146
|
+
describe: "How much the container holds, in pounds.",
|
|
147
|
+
},
|
|
148
|
+
]),
|
|
149
|
+
|
|
150
|
+
miscgear: Object.freeze([...GEAR_COMMON, QUANTITY]),
|
|
151
|
+
|
|
152
|
+
// A note's `projectile` is HM3's `missilegear`; the fields are the gear
|
|
153
|
+
// template's, and the aspect, impact and range that make it a missile are
|
|
154
|
+
// authored under `hm3.system`.
|
|
155
|
+
projectile: Object.freeze([...GEAR_COMMON, QUANTITY]),
|
|
156
|
+
|
|
157
|
+
// `hm3.system.type` — "Craft", "Physical", "Communication", "Combat",
|
|
158
|
+
// "Magic", "Ritual" — has no shared source: the content format states
|
|
159
|
+
// outright that it does not map onto the note's `subType` and must be
|
|
160
|
+
// written for each HM3 skill. So it is authored at its own path and passes
|
|
161
|
+
// through, and only the mastery level is declared here.
|
|
162
|
+
skill: Object.freeze([
|
|
163
|
+
{
|
|
164
|
+
name: "masteryLevel",
|
|
165
|
+
to: "masteryLevel",
|
|
166
|
+
...NUMBER,
|
|
167
|
+
default: 0,
|
|
168
|
+
describe: "Mastery level, as a percentage.",
|
|
169
|
+
},
|
|
170
|
+
]),
|
|
171
|
+
|
|
172
|
+
// Both one-to-many item rows: every shared row in their content-format
|
|
173
|
+
// tables is `NA`, so what an HM3 trauma or mystical ability carries — a
|
|
174
|
+
// severity and heal rate, a convocation and level, a circle and deity — is
|
|
175
|
+
// authored under `hm3.system` against whichever subtype `hm3.type` names.
|
|
176
|
+
trauma: Object.freeze([]),
|
|
177
|
+
mysticalability: Object.freeze([]),
|
|
178
|
+
|
|
179
|
+
weapongear: Object.freeze([...GEAR_COMMON]),
|
|
180
|
+
});
|
package/hm3/items.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
* **HM3's Item pass** — the two things about compiling a note into an HM3 Item
|
|
16
|
+
* that are facts about HM3 rather than about the note format (#139).
|
|
17
|
+
*
|
|
18
|
+
* Everything else is {@link module:engine/item-compiler}'s, and is the same
|
|
19
|
+
* code the SoHL pass runs: which notes are claimed, which subtype each becomes,
|
|
20
|
+
* which registry builds it, the authored `hm3.system` passthrough, the schema
|
|
21
|
+
* check, and the compendium envelope. A second system is a map and a handful of
|
|
22
|
+
* emitted keys, which is the arrangement #79 and #58 were building towards.
|
|
23
|
+
*
|
|
24
|
+
* **What HM3's compiler writes on every item: one key, and only when there is
|
|
25
|
+
* something to write.** The content format gives an item's `{#appearance}`
|
|
26
|
+
* section a home in HM3 — `description` — and SoHL none, since no SoHL Item
|
|
27
|
+
* subtype declares such a field. So the section is rendered here and nowhere
|
|
28
|
+
* else. Where a note has no such section nothing is emitted at all, which
|
|
29
|
+
* matters for `armorlocation`: it is the one HM3 subtype that extends the
|
|
30
|
+
* Foundry base directly, declaring neither `description` nor `notes`, so an
|
|
31
|
+
* unconditional key would be a finding on every armour-location document in the
|
|
32
|
+
* pack.
|
|
33
|
+
*
|
|
34
|
+
* **And one flag, for the fact HM3's `system` has no field for.** The template
|
|
35
|
+
* priority is a shared statement — a note declaring `data.templatePriority` says
|
|
36
|
+
* the same thing to both systems — but HM3 declares no `system` field for it, so
|
|
37
|
+
* it lands under this system's own flag scope as `flags.hm3.templatePriority`,
|
|
38
|
+
* exactly as the Actor pass writes it. This pass wrote no such flag until #283:
|
|
39
|
+
* an item note declaring the priority compiled into a SoHL item that knew it was
|
|
40
|
+
* a template and an HM3 item that did not.
|
|
41
|
+
*
|
|
42
|
+
* **There is no HM3 equivalent of `docHtml`.** SoHL points an item at the
|
|
43
|
+
* JournalEntry its prose compiled into, and HM3's data model has nowhere to put
|
|
44
|
+
* such a pointer; inventing one would emit a key Foundry discards at load
|
|
45
|
+
* without a word. The prose still compiles into its JournalEntry — the journals
|
|
46
|
+
* pass claims every doc-carrying type regardless of system — it simply is not
|
|
47
|
+
* addressed from the item.
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { renderSection } from "../engine/anchored-sections.mjs";
|
|
53
|
+
import { SystemItemCompiler } from "../engine/item-compiler.mjs";
|
|
54
|
+
import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
55
|
+
import { templateFlags } from "./template-priority.mjs";
|
|
56
|
+
|
|
57
|
+
export class Hm3Items extends SystemItemCompiler {
|
|
58
|
+
/**
|
|
59
|
+
* HM3's note-type → document-subtype map — the one declaration that says
|
|
60
|
+
* which block this pass reads, which notes it claims, and what each becomes
|
|
61
|
+
* (#58/#79).
|
|
62
|
+
*
|
|
63
|
+
* @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
|
|
64
|
+
*/
|
|
65
|
+
static documentSubtypes = HM3_DOCUMENT_SUBTYPES;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The `system.*` field HM3 writes on an item from the note's prose.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} fm - The note's frontmatter.
|
|
71
|
+
* @param {object} at - What the pass already knows about this note.
|
|
72
|
+
* @param {string} at.markdown - The note body, tables expanded and
|
|
73
|
+
* wikilinks resolved.
|
|
74
|
+
* @returns {object} The shared `system` fields — `description`, or nothing.
|
|
75
|
+
*/
|
|
76
|
+
commonSystem(fm, { markdown }) {
|
|
77
|
+
const description = renderSection(markdown, "appearance");
|
|
78
|
+
return description ? { description } : {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The `flags` HM3 writes on an item: whatever the note authors, plus the
|
|
83
|
+
* template priority.
|
|
84
|
+
*
|
|
85
|
+
* The same statement the Actor pass records, through the same
|
|
86
|
+
* {@link module:hm3/template-priority.templateFlags} — see that module for
|
|
87
|
+
* why the priority lives in flags at all, and for what its absence here
|
|
88
|
+
* cost (#283).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} fm - The note's frontmatter.
|
|
91
|
+
* @returns {object} The flags to emit.
|
|
92
|
+
*/
|
|
93
|
+
commonFlags(fm) {
|
|
94
|
+
return templateFlags(fm, this.system);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* **Where HM3 records the template priority** — one statement, read by both of
|
|
16
|
+
* this system's passes (#283).
|
|
17
|
+
*
|
|
18
|
+
* `data.templatePriority` is the shared fact that a note is a *starting
|
|
19
|
+
* template*, and the specification states it as a row every type maps:
|
|
20
|
+
* `system.templatePriority` in SoHL, `flags.hm3.templatePriority` here. It lands
|
|
21
|
+
* in flags because HM3's data model declares no field for it, and an undeclared
|
|
22
|
+
* `system` key is discarded at load without a word.
|
|
23
|
+
*
|
|
24
|
+
* It was written by the Actor pass alone. The Item pass emitted only whatever
|
|
25
|
+
* `flags` the note itself authored, so an item note declaring the priority
|
|
26
|
+
* compiled into a SoHL item that knew it was a template and an HM3 item that did
|
|
27
|
+
* not — silently on both sides of the build, since an omitted flag is exactly how
|
|
28
|
+
* this system says *not a template*, making a lost priority and a deliberate one
|
|
29
|
+
* the same output. Nothing could report it either: the emitted-`system` check
|
|
30
|
+
* compares against a declared schema, and a flag is declared by nothing.
|
|
31
|
+
*
|
|
32
|
+
* So the rule lives here rather than in either pass, and both call it. Two
|
|
33
|
+
* copies of it would be two chances to diverge again, which is the failure this
|
|
34
|
+
* module exists to close.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import log from "loglevel";
|
|
40
|
+
|
|
41
|
+
import { resolveName, statedTemplatePriority } from "../engine/helpers.mjs";
|
|
42
|
+
import { blockProperty } from "../engine/system-block.mjs";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A document's `flags`: whatever the note authors, plus this system's template
|
|
46
|
+
* priority.
|
|
47
|
+
*
|
|
48
|
+
* A note that is not a template — or states nothing — writes nothing, rather
|
|
49
|
+
* than a `null` nothing reads.
|
|
50
|
+
*
|
|
51
|
+
* **Read through the shared resolver, not a field declaration** (#266). A
|
|
52
|
+
* `FieldSpec`'s shared source is a single position, and this value has five:
|
|
53
|
+
* `data:`, this block, the top level, and the retiring `archetype` spelling in
|
|
54
|
+
* the latter two. The resolver is the single implementation of what a note said,
|
|
55
|
+
* so the two systems cannot disagree about it. It is read against **this**
|
|
56
|
+
* block: the `sohl:` block is not a source for an HM3 document, so a tree that
|
|
57
|
+
* still states the priority there (`harn-ensemble`, on 2,502 notes) writes no
|
|
58
|
+
* HM3 flag until it sweeps to `data:`.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} fm - The note's frontmatter.
|
|
61
|
+
* @param {string} block - This pass's system block.
|
|
62
|
+
* @returns {object} The flags to emit.
|
|
63
|
+
*/
|
|
64
|
+
export function templateFlags(fm, block) {
|
|
65
|
+
const authored = blockProperty(fm, block, "flags", {});
|
|
66
|
+
const stated = statedTemplatePriority(fm, resolveName(fm), { block });
|
|
67
|
+
if (stated == null) return authored;
|
|
68
|
+
// Coerced, as the field declaration this replaced coerced it: a note may
|
|
69
|
+
// state the priority as a YAML string, and `"3"` is a priority.
|
|
70
|
+
const value = Number(stated);
|
|
71
|
+
if (!Number.isFinite(value)) {
|
|
72
|
+
log.warn(
|
|
73
|
+
`${resolveName(fm)}: template priority ${JSON.stringify(stated)} is ` +
|
|
74
|
+
`not a number; no template flag written.`,
|
|
75
|
+
);
|
|
76
|
+
return authored;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...authored,
|
|
80
|
+
[block]: {
|
|
81
|
+
.../** @type {Record<string, unknown>} */ (authored)[block],
|
|
82
|
+
templatePriority: value,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/labels.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
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 label registry has two faces that must agree.
|
|
16
|
+
*
|
|
17
|
+
* `.github/labels.yml` is the machine source synced to GitHub, and the §3 table
|
|
18
|
+
* in `.github/ISSUE_REPORTING.md` is the documented reference a person reads.
|
|
19
|
+
* Neither is derived from the other, so either can drift — or invent a label
|
|
20
|
+
* the other has never heard of — and nothing notices until someone files an
|
|
21
|
+
* issue against a label that does not exist, or the sync pushes a label the
|
|
22
|
+
* documentation never mentions.
|
|
23
|
+
*
|
|
24
|
+
* **This lives here because every repository wants it and only the paths ever
|
|
25
|
+
* differed.** It was a `utils/check-labels.mjs` copied per repository, which is
|
|
26
|
+
* the shape a shared check takes just before it starts drifting between copies
|
|
27
|
+
* — the same argument that moved the no-attribution check to a shared action.
|
|
28
|
+
*
|
|
29
|
+
* The checks are pure and text-in: the caller reads the two files and decides
|
|
30
|
+
* what to do with the findings, so this module needs no filesystem and is
|
|
31
|
+
* testable without one.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { parse } from "yaml";
|
|
37
|
+
|
|
38
|
+
/** GitHub rejects a label description beyond this with a 422. */
|
|
39
|
+
export const MAX_DESCRIPTION = 100;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The line a label's `name:` entry sits on, 1-based.
|
|
43
|
+
*
|
|
44
|
+
* Located rather than tracked, because `yaml`'s document API would have to be
|
|
45
|
+
* threaded through the parse for a value this simple to recover: a registry is
|
|
46
|
+
* a flat list of `- name: <label>`, so the first line declaring that name is
|
|
47
|
+
* the entry. A name that cannot be found yields no position, which
|
|
48
|
+
* `reportFindings` drops rather than guessing.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text - The registry file's contents.
|
|
51
|
+
* @param {string} name - The label name.
|
|
52
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
53
|
+
*/
|
|
54
|
+
function positionOfLabel(text, name) {
|
|
55
|
+
const lines = text.split("\n");
|
|
56
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
57
|
+
const m = lines[i].match(/^\s*-?\s*name:\s*["']?(.+?)["']?\s*$/);
|
|
58
|
+
if (m && m[1] === name) return { line: i + 1, column: lines[i].indexOf(name) + 1 };
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The line a label's row sits on in the documented table, 1-based.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} text - The documentation file's contents.
|
|
67
|
+
* @param {string} name - The label name.
|
|
68
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
69
|
+
*/
|
|
70
|
+
function positionOfDocRow(text, name) {
|
|
71
|
+
const lines = text.split("\n");
|
|
72
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
73
|
+
const m = lines[i].match(/^\|\s*`([a-z][a-z-]*)`\s*\|/);
|
|
74
|
+
if (m && m[1] === name) return { line: i + 1, column: lines[i].indexOf(name) + 1 };
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The label names the documented §3 table lists.
|
|
81
|
+
*
|
|
82
|
+
* A registry row is a table row whose first cell is a backticked label name,
|
|
83
|
+
* which is narrow on purpose: §3 carries prose and other tables, and a looser
|
|
84
|
+
* match would read a heading or an example as a label.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} text - The documentation file's contents.
|
|
87
|
+
* @returns {{names: Set<string>, found: boolean}} The names, and whether §3 was
|
|
88
|
+
* located at all — an absent section is a different failure from an empty one.
|
|
89
|
+
*/
|
|
90
|
+
export function documentedLabels(text) {
|
|
91
|
+
const lines = text.split("\n");
|
|
92
|
+
const start = lines.findIndex((l) => /^##\s+3\./.test(l));
|
|
93
|
+
if (start < 0) return { names: new Set(), found: false };
|
|
94
|
+
const after = lines.findIndex((l, i) => i > start && /^##\s+\d/.test(l));
|
|
95
|
+
const section = lines.slice(start, after < 0 ? lines.length : after);
|
|
96
|
+
const names = new Set();
|
|
97
|
+
for (const line of section) {
|
|
98
|
+
const m = line.match(/^\|\s*`([a-z][a-z-]*)`\s*\|/);
|
|
99
|
+
if (m) names.add(m[1]);
|
|
100
|
+
}
|
|
101
|
+
return { names, found: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check the machine registry against the documented table.
|
|
106
|
+
*
|
|
107
|
+
* Findings are returned per file rather than merged, because each names a
|
|
108
|
+
* position in a different document and the caller reports them against the
|
|
109
|
+
* file they belong to.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} sources
|
|
112
|
+
* @param {string} sources.registryText - `.github/labels.yml`.
|
|
113
|
+
* @param {string} sources.docText - `.github/ISSUE_REPORTING.md`.
|
|
114
|
+
* @param {string} [sources.docPath] - The doc's path, for the message naming it.
|
|
115
|
+
* @returns {{registry: object[], doc: object[], count: number}} Findings for
|
|
116
|
+
* each file, and how many labels the registry declares.
|
|
117
|
+
*/
|
|
118
|
+
export function checkLabelRegistry({
|
|
119
|
+
registryText,
|
|
120
|
+
docText,
|
|
121
|
+
docPath = ".github/ISSUE_REPORTING.md",
|
|
122
|
+
}) {
|
|
123
|
+
const registryFindings = [];
|
|
124
|
+
const docFindings = [];
|
|
125
|
+
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = parse(registryText);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
registryFindings.push({
|
|
131
|
+
severity: "error",
|
|
132
|
+
message: `label registry is not valid YAML — ${error.message}`,
|
|
133
|
+
});
|
|
134
|
+
return { registry: registryFindings, doc: docFindings, count: 0 };
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(entries)) {
|
|
137
|
+
registryFindings.push({
|
|
138
|
+
severity: "error",
|
|
139
|
+
message: "label registry must be a list of `{ name, description }` entries",
|
|
140
|
+
});
|
|
141
|
+
return { registry: registryFindings, doc: docFindings, count: 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Caught here rather than mid-sync, where it surfaces as a bare 422 from
|
|
145
|
+
// the GitHub API naming neither the label nor the limit.
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const length = (entry?.description ?? "").length;
|
|
148
|
+
if (length > MAX_DESCRIPTION) {
|
|
149
|
+
registryFindings.push({
|
|
150
|
+
...positionOfLabel(registryText, entry.name),
|
|
151
|
+
severity: "error",
|
|
152
|
+
message:
|
|
153
|
+
`"${entry.name}" has a ${length}-character description; ` +
|
|
154
|
+
`GitHub rejects anything over ${MAX_DESCRIPTION}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const registryNames = new Set(entries.map((e) => e?.name).filter(Boolean));
|
|
160
|
+
const { names: docNames, found } = documentedLabels(docText);
|
|
161
|
+
if (!found) {
|
|
162
|
+
docFindings.push({
|
|
163
|
+
severity: "error",
|
|
164
|
+
message: `no §3 section found in ${docPath}, so the registry has nothing to agree with`,
|
|
165
|
+
});
|
|
166
|
+
return { registry: registryFindings, doc: docFindings, count: registryNames.size };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const name of registryNames) {
|
|
170
|
+
if (docNames.has(name)) continue;
|
|
171
|
+
registryFindings.push({
|
|
172
|
+
...positionOfLabel(registryText, name),
|
|
173
|
+
severity: "error",
|
|
174
|
+
message:
|
|
175
|
+
`"${name}" is in the registry but not in ${docPath} §3; ` +
|
|
176
|
+
`the two are edited together, so add the row`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
for (const name of docNames) {
|
|
180
|
+
if (registryNames.has(name)) continue;
|
|
181
|
+
docFindings.push({
|
|
182
|
+
...positionOfDocRow(docText, name),
|
|
183
|
+
severity: "error",
|
|
184
|
+
message:
|
|
185
|
+
`"${name}" is documented in §3 but absent from the registry, ` +
|
|
186
|
+
`so it is never synced and an issue cannot carry it`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { registry: registryFindings, doc: docFindings, count: registryNames.size };
|
|
191
|
+
}
|