@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
|
@@ -42,24 +42,25 @@
|
|
|
42
42
|
* transmission — and is true of it whichever system is reading. What each
|
|
43
43
|
* system makes of that value is declared elsewhere, in that system's own half.
|
|
44
44
|
*
|
|
45
|
-
* **The type names here are
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
53
|
-
*
|
|
54
|
-
* deserves until the type exists.
|
|
45
|
+
* **The type names here are the specification's**, since #78 renamed `armor`,
|
|
46
|
+
* `projectile` and `concoction` off the `…gear` spellings that named a SoHL
|
|
47
|
+
* document subtype rather than the thing the note is about. `weapon` is the one
|
|
48
|
+
* the specification and this registry still spell differently: both systems
|
|
49
|
+
* call that document a `weapongear`, so the name says nothing system-specific
|
|
50
|
+
* and #78's table has no row for it. A note left on a renamed spelling still
|
|
51
|
+
* reaches its entry — every type-keyed lookup normalises through
|
|
52
|
+
* `RENAMED_TYPES` — and is reported rather than refused until the content trees
|
|
53
|
+
* have swept.
|
|
55
54
|
*
|
|
56
55
|
* **A type name and a subType value are held to the address charset** (#206), so
|
|
57
56
|
* both are `^[A-Za-z0-9]+$` — the charset `engine/address-charset.mjs` states
|
|
58
|
-
* and the shortcode is already held to. For a type that is literal: it is
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* and the shortcode is already held to. For a type that is literal: it is a
|
|
58
|
+
* segment of every address — the first of the short form an author writes
|
|
59
|
+
* (`type-shortcode`), the third of the canonical
|
|
60
|
+
* `package-system-type-shortcode` — the hyphen is the separator between
|
|
61
|
+
* segments and can therefore never occur inside one, and a hyphenated name
|
|
62
|
+
* would be read back as two segments and resolve to nothing, reporting nothing
|
|
63
|
+
* about why.
|
|
63
64
|
*
|
|
64
65
|
* A subType reaches no address of its own. It did when this rule was written —
|
|
65
66
|
* a `doc`'s was its section, a path segment — and #204 retired sections from
|
|
@@ -77,6 +78,10 @@
|
|
|
77
78
|
// The one charset, read rather than restated. A second spelling of the pattern
|
|
78
79
|
// is how the three disagreements found in #202/#203 happened.
|
|
79
80
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
|
|
81
|
+
// The retirement window for a renamed type, read rather than restated: a
|
|
82
|
+
// vocabulary that answered only to the current spelling would report every key
|
|
83
|
+
// of an unswept note as unknown (#78).
|
|
84
|
+
import { currentType } from "./ids.mjs";
|
|
80
85
|
|
|
81
86
|
/**
|
|
82
87
|
* One `data:` key a note type may carry.
|
|
@@ -90,12 +95,19 @@ import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs
|
|
|
90
95
|
* @typedef {object} DataFieldSpec
|
|
91
96
|
* @property {string} name - The key under `data:`, dotted for a nested one
|
|
92
97
|
* (`charges.value`).
|
|
93
|
-
* @property {"string"|"number"|"boolean"|"list"|"map"} [kind] -
|
|
94
|
-
* shape, for the lint. Absent means no claim is made about the
|
|
95
|
-
* is the honest answer wherever the specification's stated
|
|
96
|
-
* shape notes are authored in today disagree.
|
|
98
|
+
* @property {"string"|"number"|"boolean"|"list"|"map"|"scalar-or-map"} [kind] -
|
|
99
|
+
* The value's shape, for the lint. Absent means no claim is made about the
|
|
100
|
+
* value — which is the honest answer wherever the specification's stated
|
|
101
|
+
* shape and the shape notes are authored in today disagree.
|
|
97
102
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
98
103
|
* documentation.
|
|
104
|
+
* @property {string} [entryShape] - For a `scalar-or-map` field, what one
|
|
105
|
+
* entry of the map is. A finding names the entry at fault rather than
|
|
106
|
+
* quoting the whole map back, so the string an author has to correct is the
|
|
107
|
+
* one the message holds.
|
|
108
|
+
* @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
|
|
109
|
+
* `"pack"` means each is a pack this package declares, so a key naming none
|
|
110
|
+
* is a finding of its own: it addresses a hierarchy nothing will ever read.
|
|
99
111
|
* @property {string} describe - One line, for the author-facing reference.
|
|
100
112
|
*/
|
|
101
113
|
|
|
@@ -136,6 +148,26 @@ const LINK = Object.freeze({ shape: "a wikilink", kind: "string" });
|
|
|
136
148
|
/** A list of wikilinks. */
|
|
137
149
|
const LINKS = Object.freeze({ shape: "list of wikilinks", kind: "list" });
|
|
138
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A single wikilink, or one per pack.
|
|
153
|
+
*
|
|
154
|
+
* The map form is not a convenience spelling of the scalar: it says something
|
|
155
|
+
* the scalar cannot, that the answer *differs by pack*. A folder's `parent` is
|
|
156
|
+
* the case it exists for — a folder's identity is one thing and its hierarchy
|
|
157
|
+
* another, and both large trees file the same folder under a different parent
|
|
158
|
+
* in the items pack and the journals pack (#276).
|
|
159
|
+
*
|
|
160
|
+
* Typing it as a bare {@link LINK} is what #288 was: the compiler read both
|
|
161
|
+
* forms and the lint rejected one of them, so every note using the form the
|
|
162
|
+
* specification prescribes was a finding and no note using it was not.
|
|
163
|
+
*/
|
|
164
|
+
const LINK_BY_PACK = Object.freeze({
|
|
165
|
+
shape: "a wikilink, or a map of wikilinks keyed by pack",
|
|
166
|
+
kind: "scalar-or-map",
|
|
167
|
+
entryShape: "a wikilink",
|
|
168
|
+
keys: "pack",
|
|
169
|
+
});
|
|
170
|
+
|
|
139
171
|
/** Whatever the author wrote — declared, but with no claim about its shape. */
|
|
140
172
|
const ANY = Object.freeze({ shape: "as authored" });
|
|
141
173
|
|
|
@@ -558,7 +590,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
558
590
|
]),
|
|
559
591
|
}),
|
|
560
592
|
|
|
561
|
-
|
|
593
|
+
armor: Object.freeze({
|
|
562
594
|
// Quantity is always one, so the specification refuses the key rather
|
|
563
595
|
// than defaulting it.
|
|
564
596
|
data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR]),
|
|
@@ -573,7 +605,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
573
605
|
data: Object.freeze([TEMPLATE_PRIORITY]),
|
|
574
606
|
}),
|
|
575
607
|
|
|
576
|
-
|
|
608
|
+
concoction: Object.freeze({
|
|
577
609
|
subTypes: Object.freeze(["mundane", "exotic", "elixir"]),
|
|
578
610
|
data: Object.freeze([
|
|
579
611
|
TEMPLATE_PRIORITY,
|
|
@@ -648,7 +680,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
648
680
|
]),
|
|
649
681
|
}),
|
|
650
682
|
|
|
651
|
-
|
|
683
|
+
projectile: Object.freeze({
|
|
652
684
|
subTypes: Object.freeze(["none", "arrow", "bolt", "bullet", "dart", "other"]),
|
|
653
685
|
data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR, QUANTITY]),
|
|
654
686
|
}),
|
|
@@ -711,6 +743,57 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
711
743
|
|
|
712
744
|
macro: Object.freeze({ data: Object.freeze([]) }),
|
|
713
745
|
|
|
746
|
+
// Foundry's `Folder`, and the last document this package compiled from
|
|
747
|
+
// bespoke configuration rather than from a note (#256). It declares no
|
|
748
|
+
// system-block fields, like a bundle: a `Folder` is a core Foundry
|
|
749
|
+
// document, so its address carries the `none` system segment and
|
|
750
|
+
// everything it says is a `data` property.
|
|
751
|
+
//
|
|
752
|
+
// It carries **no prose**, which is the decision #256 left open: a folder
|
|
753
|
+
// is structure, not content, so it wants no documentation journal and takes
|
|
754
|
+
// no part in `docEntryTypes`.
|
|
755
|
+
folder: Object.freeze({
|
|
756
|
+
data: Object.freeze([
|
|
757
|
+
{
|
|
758
|
+
name: "parent",
|
|
759
|
+
...LINK_BY_PACK,
|
|
760
|
+
describe:
|
|
761
|
+
"The folder this one sits in, as an address — or one " +
|
|
762
|
+
"address per pack, keyed by pack name with `default` for " +
|
|
763
|
+
"the rest. Unset at the root. A dead address is a " +
|
|
764
|
+
"dead-address finding and a cycle is refused, per pack.",
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "color",
|
|
768
|
+
...TEXT,
|
|
769
|
+
describe: "The folder's colour, as a CSS hex code. Unset for Foundry's default.",
|
|
770
|
+
},
|
|
771
|
+
]),
|
|
772
|
+
}),
|
|
773
|
+
|
|
774
|
+
// Foundry's `Adventure`, named for what it is rather than what Foundry
|
|
775
|
+
// calls it: a set of documents taken as a unit. The document is an
|
|
776
|
+
// installer — it carries copies, and importing one creates or updates each
|
|
777
|
+
// document in the world — which is what separates a bundle from a folder,
|
|
778
|
+
// a live grouping that persists in the pack (#259).
|
|
779
|
+
//
|
|
780
|
+
// How many Adventures a bundle makes is decided by its system blocks, as
|
|
781
|
+
// for every other type, and not by a property: an `Adventure` has no
|
|
782
|
+
// `system` field, so a bundle spanning two systems is two documents, and
|
|
783
|
+
// the pack each is written to is what carries the system.
|
|
784
|
+
bundle: Object.freeze({
|
|
785
|
+
data: Object.freeze([
|
|
786
|
+
{
|
|
787
|
+
name: "contents",
|
|
788
|
+
...LINKS,
|
|
789
|
+
describe:
|
|
790
|
+
"The documents the Adventure holds, as addresses. Empty " +
|
|
791
|
+
"when unstated. A document of neither `none` nor the " +
|
|
792
|
+
"system being compiled is left out rather than failing.",
|
|
793
|
+
},
|
|
794
|
+
]),
|
|
795
|
+
}),
|
|
796
|
+
|
|
714
797
|
lore: Object.freeze({
|
|
715
798
|
subTypes: Object.freeze([
|
|
716
799
|
"cosmology",
|
|
@@ -888,9 +971,9 @@ export function typeCharsetMessage(type) {
|
|
|
888
971
|
return (
|
|
889
972
|
`content type "${type}" is not an address segment — a type is letters ` +
|
|
890
973
|
`and digits only (${ADDRESS_SEGMENT_PATTERN.source}), the same charset ` +
|
|
891
|
-
`a shortcode is held to. A type is
|
|
892
|
-
`("type-shortcode"), so a
|
|
893
|
-
`and resolves to nothing`
|
|
974
|
+
`a shortcode is held to. A type is a segment of every address, and the ` +
|
|
975
|
+
`first of the short form an author writes ("type-shortcode"), so a ` +
|
|
976
|
+
`hyphenated one is read back as two segments and resolves to nothing`
|
|
894
977
|
);
|
|
895
978
|
}
|
|
896
979
|
|
|
@@ -948,7 +1031,7 @@ assertVocabularyCharset(NOTE_VOCABULARY);
|
|
|
948
1031
|
* one, and is why the lint makes no claim rather than refusing every key.
|
|
949
1032
|
*/
|
|
950
1033
|
export function dataFields(type, vocabulary = NOTE_VOCABULARY) {
|
|
951
|
-
return vocabulary?.[type]?.data;
|
|
1034
|
+
return vocabulary?.[currentType(type)]?.data;
|
|
952
1035
|
}
|
|
953
1036
|
|
|
954
1037
|
/**
|
|
@@ -962,7 +1045,7 @@ export function dataFields(type, vocabulary = NOTE_VOCABULARY) {
|
|
|
962
1045
|
* it has no `subType` at all — see {@link TypeVocabulary}.
|
|
963
1046
|
*/
|
|
964
1047
|
export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
|
|
965
|
-
const entry = vocabulary?.[type];
|
|
1048
|
+
const entry = vocabulary?.[currentType(type)];
|
|
966
1049
|
if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
|
|
967
1050
|
return entry.subTypes;
|
|
968
1051
|
}
|
package/engine/pack-config.mjs
CHANGED
|
@@ -168,6 +168,10 @@ const ITEM_BUILDER_REGISTRIES = Object.freeze({
|
|
|
168
168
|
/** @type {{ ITEM_BUILDERS: Record<string, unknown> }} */ (
|
|
169
169
|
require("../sohl/item-builders.mjs")
|
|
170
170
|
).ITEM_BUILDERS,
|
|
171
|
+
hm3: () =>
|
|
172
|
+
/** @type {{ HM3_ITEM_BUILDERS: Record<string, unknown> }} */ (
|
|
173
|
+
require("../hm3/item-builders.mjs")
|
|
174
|
+
).HM3_ITEM_BUILDERS,
|
|
171
175
|
});
|
|
172
176
|
|
|
173
177
|
/**
|
package/engine/pack-router.mjs
CHANGED
|
@@ -220,6 +220,29 @@ export function createPackRouter(packs) {
|
|
|
220
220
|
return [...(byType.get(docType) ?? [])];
|
|
221
221
|
},
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The systems declared by the packs of one document type.
|
|
225
|
+
*
|
|
226
|
+
* What a pass asks to tell a note that belongs to *another system's*
|
|
227
|
+
* pass from one that is simply missing this system's block (#139). A
|
|
228
|
+
* repository feeding two systems declares an Item pack per system, so
|
|
229
|
+
* a note carrying only `hm3:` is not an incomplete SoHL note — it is a
|
|
230
|
+
* note the HM3 pass will claim, and the SoHL pass has to pass over it
|
|
231
|
+
* rather than fail it.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} docType - The Foundry document type.
|
|
234
|
+
* @returns {string[]} The declared systems, deduplicated, in
|
|
235
|
+
* configured order. Empty where no pack of the type names one.
|
|
236
|
+
*/
|
|
237
|
+
systemsOfType(docType) {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const name of byType.get(docType) ?? []) {
|
|
240
|
+
const system = byName.get(name)?.system;
|
|
241
|
+
if (system && !out.includes(system)) out.push(system);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
},
|
|
245
|
+
|
|
223
246
|
/**
|
|
224
247
|
* The pack of a type that receives notes declaring none.
|
|
225
248
|
*
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
*
|
|
24
24
|
* `package:` is retired the same way and is refused from `note-package.mjs`,
|
|
25
25
|
* where the concept it belonged to still lives. `draft:`, the top-level
|
|
26
|
-
* `aliases:` and `
|
|
27
|
-
* any of them was part of — so they are refused here.
|
|
26
|
+
* `aliases:`, `section:` and `traits:` have no such home — there is no
|
|
27
|
+
* surviving concept any of them was part of — so they are refused here.
|
|
28
28
|
*
|
|
29
29
|
* **What `draft:` did (#69).** It excluded a note from the compiled packs, from
|
|
30
30
|
* the link manifest and from a consuming site build. Nothing reported the
|
|
@@ -56,6 +56,23 @@
|
|
|
56
56
|
* reserved.test.ts` pins that equivalence so a future reader cannot be added
|
|
57
57
|
* by accident.
|
|
58
58
|
*
|
|
59
|
+
* **What `traits:` did (#291).** It held a being's own description — gender,
|
|
60
|
+
* species, age, birthday, height, weight, frame and `appearance.*` — at the
|
|
61
|
+
* note's top level. The content format gives those a home: `data:`, the closed
|
|
62
|
+
* container for a subject's type-specific facts, which `being` declares every
|
|
63
|
+
* one of them in. #128 moved all 2,533 notes that carried one, across four
|
|
64
|
+
* repositories, and this is the third step of that retirement.
|
|
65
|
+
*
|
|
66
|
+
* Refusing it matters more than refusing an ordinary dead key, because top
|
|
67
|
+
* level is *deliberately open*: an unrecognised key there is passed through to
|
|
68
|
+
* Hugo, so a stray `traits:` would not be ignored loudly but would arrive on
|
|
69
|
+
* the page as a theme parameter, checked by nothing. The whole argument for
|
|
70
|
+
* `data:` being closed is the argument for refusing this.
|
|
71
|
+
*
|
|
72
|
+
* `sohl.traits` is a different field that shares the name — `projectilegear`
|
|
73
|
+
* declares one, and the theme's gear sidebar reads it — so the refusal is
|
|
74
|
+
* anchored at column 1 and never reaches inside a system block.
|
|
75
|
+
*
|
|
59
76
|
* **A field retired in favour of another is a third case (#142).** `draft:` and
|
|
60
77
|
* `package:` were retired outright: nothing replaced them, so no value made
|
|
61
78
|
* writing one right and refusal was the only honest answer. A *renamed* field
|
|
@@ -67,6 +84,15 @@
|
|
|
67
84
|
* the refusal come later, once no tree writes it. See
|
|
68
85
|
* {@link RETIRED_FIELD_ALIASES}.
|
|
69
86
|
*
|
|
87
|
+
* **A retired *position* is the same case, and reads the same (#305).** A field
|
|
88
|
+
* whose shared source moved under `data:` is not renamed — `data.species` and
|
|
89
|
+
* `hm3.species` are one field written in two places — but the retirement has
|
|
90
|
+
* the identical shape: both are read, the one the note carries wins, and the
|
|
91
|
+
* legacy in-block key is reported by {@link legacyKeyMessage} so the sweep has
|
|
92
|
+
* something to count down. Which position a value came from is
|
|
93
|
+
* {@link module:engine/system-block.resolveFieldValue}'s answer; this module
|
|
94
|
+
* only says what an author is told about it.
|
|
95
|
+
*
|
|
70
96
|
* @module
|
|
71
97
|
*/
|
|
72
98
|
|
|
@@ -289,6 +315,65 @@ export function assertNoSectionField(fm, { file, absPath } = {}) {
|
|
|
289
315
|
throw err;
|
|
290
316
|
}
|
|
291
317
|
|
|
318
|
+
/**
|
|
319
|
+
* What a note writing a top-level `traits:` block is told.
|
|
320
|
+
*
|
|
321
|
+
* The message states the **mapping**, not just the destination, because three
|
|
322
|
+
* of the keys reshaped as well as moved: the block nested its measurements
|
|
323
|
+
* where the format flattens them. A bare "write `data:` instead" would send an
|
|
324
|
+
* author to author `data.height: {m: 1.78}`, which is a declared key holding an
|
|
325
|
+
* undeclared shape.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
328
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
329
|
+
* line — repeating it prints the path twice.
|
|
330
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
331
|
+
*/
|
|
332
|
+
export function traitsRetiredMessage(file) {
|
|
333
|
+
return (
|
|
334
|
+
"`traits:` is a retired frontmatter block — move it into `data:`" +
|
|
335
|
+
(file ? ` — ${file}` : "") +
|
|
336
|
+
". A being's own description belongs in the closed container the " +
|
|
337
|
+
"content format declares, where `being` declares every one of its " +
|
|
338
|
+
"keys; at the top level it was passed through to the page unchecked, " +
|
|
339
|
+
"so a misspelling became a theme parameter rather than a finding. " +
|
|
340
|
+
"`gender`, `species`, `age`, `birthday` and `appearance.*` move " +
|
|
341
|
+
"unchanged; three reshape — `traits.height.m` becomes `data.height` " +
|
|
342
|
+
"(metres), `traits.weight.kg` becomes `data.weight` (kilograms), and " +
|
|
343
|
+
"`traits.build.frame` becomes `data.frame`"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Refuse a note that declares a top-level `traits:` block at all.
|
|
349
|
+
*
|
|
350
|
+
* Presence is the whole test, as it is for `draft:` and `aliases:`: an empty
|
|
351
|
+
* `traits:` is still a note claiming a block that no longer exists.
|
|
352
|
+
*
|
|
353
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
354
|
+
* could not be parsed.
|
|
355
|
+
* @param {object} [options] - Options.
|
|
356
|
+
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
357
|
+
* it where the caller emits through a diagnostic, which puts the locator at
|
|
358
|
+
* the start of the line already.
|
|
359
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
360
|
+
* failing path to locate the offending line and column. The position rides on
|
|
361
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
362
|
+
* @returns {void}
|
|
363
|
+
* @throws {Error} When the note declares the block.
|
|
364
|
+
*/
|
|
365
|
+
export function assertNoTraitsField(fm, { file, absPath } = {}) {
|
|
366
|
+
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "traits")) return;
|
|
367
|
+
|
|
368
|
+
const err = new Error(`${traitsRetiredMessage(file)}.`);
|
|
369
|
+
// Anchored at column 1. `sohl.traits` is a *different field that shares the
|
|
370
|
+
// name* — `projectilegear` declares one and the theme's gear sidebar reads
|
|
371
|
+
// it — so a finding about the top-level block must never open on it.
|
|
372
|
+
const position = locateFrontmatterKey(absPath, "traits", undefined, { topLevel: true });
|
|
373
|
+
if (position) err.position = position;
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
|
|
292
377
|
/**
|
|
293
378
|
* A frontmatter key's position in a note's file, or nothing.
|
|
294
379
|
*
|
|
@@ -335,6 +420,19 @@ export function locateFrontmatterKey(absPath, key, value = undefined, { topLevel
|
|
|
335
420
|
* `image` is retired on a map — which declares `img` — and remains an unknown
|
|
336
421
|
* key anywhere else.
|
|
337
422
|
*
|
|
423
|
+
* **`templatePriority` (#266).** The number that decides which of several
|
|
424
|
+
* competing templates the Create dialog offers was called `archetype`, and
|
|
425
|
+
* `archetypes` — one letter apart — is a list of what *sort* a character is.
|
|
426
|
+
* A priority and a taxonomy cannot be told apart by a plural `s`, so the
|
|
427
|
+
* priority takes the name that says what it is. The specification already
|
|
428
|
+
* called it `templatePriority` on two of its three sides.
|
|
429
|
+
*
|
|
430
|
+
* **`relations` (SoHL#1781).** The field holds a *map* of standings, one per
|
|
431
|
+
* affiliation — its own description says so, and `resolveRelation` has always
|
|
432
|
+
* read it that way. `relation` named the many as one, which every author had to
|
|
433
|
+
* read past. Only `affiliation` declares the field, so the alias is reported
|
|
434
|
+
* there and the old spelling stays an ordinary unknown key everywhere else.
|
|
435
|
+
*
|
|
338
436
|
* **`img` (#142).** Every note type names its artwork `img`, at the note's top
|
|
339
437
|
* level, and resolves it the same way. A map alone named its background art
|
|
340
438
|
* `image` and read it out of the `sohl:` block — two spellings for one idea,
|
|
@@ -345,7 +443,11 @@ export function locateFrontmatterKey(absPath, key, value = undefined, { topLevel
|
|
|
345
443
|
*
|
|
346
444
|
* @type {Readonly<Record<string, string>>}
|
|
347
445
|
*/
|
|
348
|
-
export const RETIRED_FIELD_ALIASES = Object.freeze({
|
|
446
|
+
export const RETIRED_FIELD_ALIASES = Object.freeze({
|
|
447
|
+
img: "image",
|
|
448
|
+
templatePriority: "archetype",
|
|
449
|
+
relations: "relation",
|
|
450
|
+
});
|
|
349
451
|
|
|
350
452
|
/**
|
|
351
453
|
* What a note writing a renamed field is told, in one place.
|
|
@@ -373,6 +475,38 @@ export function retiredAliasMessage(retired, current, file) {
|
|
|
373
475
|
);
|
|
374
476
|
}
|
|
375
477
|
|
|
478
|
+
/**
|
|
479
|
+
* What a note writing a field at its **legacy in-block position** is told.
|
|
480
|
+
*
|
|
481
|
+
* The same three-step retirement `retiredAliasMessage` announces, applied to a
|
|
482
|
+
* *position* rather than a spelling (#305). A field whose shared source moved
|
|
483
|
+
* under `data:` keeps reading the key its block still carries, that read wins,
|
|
484
|
+
* and it is reported here so a sweep has something to count down — without it,
|
|
485
|
+
* moving a field into `data:` would be a flag day across every repository that
|
|
486
|
+
* authors it.
|
|
487
|
+
*
|
|
488
|
+
* It names the block, because the key is `hm3.species` and not `species`: an
|
|
489
|
+
* author told to move "`species`" has two regions to guess between.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} block - The system block the key was written in.
|
|
492
|
+
* @param {{name?: string, legacyKey?: string}} field - The declaration, which
|
|
493
|
+
* names both positions.
|
|
494
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
495
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
496
|
+
* line — repeating it prints the path twice.
|
|
497
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
498
|
+
*/
|
|
499
|
+
export function legacyKeyMessage(block, field, file) {
|
|
500
|
+
const legacy = `${block}.${field.legacyKey}`;
|
|
501
|
+
return (
|
|
502
|
+
`\`${legacy}:\` is the legacy position of the shared \`${field.name}:\` ` +
|
|
503
|
+
`— write it under \`data:\` instead` +
|
|
504
|
+
(file ? ` — ${file}` : "") +
|
|
505
|
+
`. Both are read and \`${legacy}\` wins, so the note compiles ` +
|
|
506
|
+
`identically either way; the in-block key is removed in a later release`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
376
510
|
/**
|
|
377
511
|
* Whether a note writes the retired spelling of a field, wherever it put it.
|
|
378
512
|
*
|
package/engine/scenes.mjs
CHANGED
|
@@ -51,10 +51,24 @@ import fs from "fs";
|
|
|
51
51
|
import path from "path";
|
|
52
52
|
import log from "loglevel";
|
|
53
53
|
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
parseMarkdownFile,
|
|
56
|
+
sohlField,
|
|
57
|
+
resolveName,
|
|
58
|
+
slugify,
|
|
59
|
+
defaultStats,
|
|
60
|
+
folderField,
|
|
61
|
+
} from "./helpers.mjs";
|
|
55
62
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
63
|
+
// What an Adventure member may carry is one rule, and the module that owns the
|
|
64
|
+
// Adventure states it: the scenes pass bundles its pinned places, and the
|
|
65
|
+
// bundles pass compiles a note into one (#259).
|
|
66
|
+
import { stripAdventureKeys } from "./bundle-notes.mjs";
|
|
56
67
|
import { buildJournalEntry, splitPages, journalPageId } from "./journals.mjs";
|
|
57
68
|
import { compendiumUuid, makeId, packForType } from "./ids.mjs";
|
|
69
|
+
// The record accessors only — see `engine/index-records.mjs` for why they live
|
|
70
|
+
// apart from the index that builds them (#243).
|
|
71
|
+
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
58
72
|
import { packRouter } from "./pack-router.mjs";
|
|
59
73
|
import { foundryPackageId } from "./content-package.mjs";
|
|
60
74
|
import { itemDocEntryId } from "./item-docs.mjs";
|
|
@@ -109,28 +123,6 @@ export function collectKnownActionNames(repoRoot) {
|
|
|
109
123
|
return names;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
/**
|
|
113
|
-
* Strip the LevelDB keys from a document tree.
|
|
114
|
-
*
|
|
115
|
-
* An Adventure's members are inline source data in a `SetField`, not sublevel
|
|
116
|
-
* documents, so they carry no `_key` — the CLI's hierarchy does not recurse
|
|
117
|
-
* into an adventure, and Foundry's schema has no such field to hold it.
|
|
118
|
-
*
|
|
119
|
-
* @param {*} value - A document, array, or scalar.
|
|
120
|
-
* @returns {*} The same shape with every `_key` removed.
|
|
121
|
-
*/
|
|
122
|
-
function stripKeys(value) {
|
|
123
|
-
if (Array.isArray(value)) return value.map(stripKeys);
|
|
124
|
-
if (value && typeof value === "object") {
|
|
125
|
-
return Object.fromEntries(
|
|
126
|
-
Object.entries(value)
|
|
127
|
-
.filter(([k]) => k !== "_key")
|
|
128
|
-
.map(([k, v]) => [k, stripKeys(v)]),
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
126
|
export class Scenes extends BasePackCompiler {
|
|
135
127
|
static id = "scenes";
|
|
136
128
|
static label = "map";
|
|
@@ -148,11 +140,12 @@ export class Scenes extends BasePackCompiler {
|
|
|
148
140
|
constructor({
|
|
149
141
|
contentBase,
|
|
150
142
|
dest,
|
|
143
|
+
skipDirectories,
|
|
151
144
|
companionDests = {},
|
|
152
145
|
folderResolver = () => null,
|
|
153
146
|
repoRoot = process.cwd(),
|
|
154
147
|
}) {
|
|
155
|
-
super({ contentBase, dest, folderResolver });
|
|
148
|
+
super({ contentBase, dest, folderResolver, skipDirectories });
|
|
156
149
|
if (!companionDests.adventures) {
|
|
157
150
|
throw new Error("Scenes compiler requires an `adventures` companion destination");
|
|
158
151
|
}
|
|
@@ -179,15 +172,41 @@ export class Scenes extends BasePackCompiler {
|
|
|
179
172
|
*
|
|
180
173
|
* @returns {{maps: Array<object>, effectsByAddress: Map<string, object>}}
|
|
181
174
|
*/
|
|
175
|
+
/**
|
|
176
|
+
* The router this pass resolves pack names through.
|
|
177
|
+
*
|
|
178
|
+
* `generatePack` hands every pass the one router the compile resolved, and
|
|
179
|
+
* that is the answer whenever a real compile is running — a second router
|
|
180
|
+
* is a second answer to where a document landed, built from whichever
|
|
181
|
+
* configuration the working directory offers (#243). A compiler
|
|
182
|
+
* constructed directly, as a consumer's or a test's is, has none, and falls
|
|
183
|
+
* back exactly as `prepare` falls back to deriving its own corpus.
|
|
184
|
+
*
|
|
185
|
+
* @returns {object} The pack router.
|
|
186
|
+
*/
|
|
187
|
+
get #packRouter() {
|
|
188
|
+
return this.router ?? packRouter();
|
|
189
|
+
}
|
|
190
|
+
|
|
182
191
|
#collect() {
|
|
183
192
|
const maps = [];
|
|
184
193
|
const effectsByAddress = new Map();
|
|
185
|
-
|
|
194
|
+
// The corpus this compile derived once, not a walk of this pass's own
|
|
195
|
+
// (#243) — and a note is opened only when this pass needs its *prose*,
|
|
196
|
+
// which for a map note means three files in `sohl` rather than 1,685.
|
|
197
|
+
for (const record of this.corpus.records) {
|
|
198
|
+
if (!isNoteRecord(record)) continue;
|
|
186
199
|
// No retired-field test: this pass's own walk — the shared compile
|
|
187
200
|
// loop — is where a note still declaring `package:` (#56) or
|
|
188
201
|
// `draft:` (#69) is reported, once. Repeating either check here
|
|
189
202
|
// would double the diagnostic or throw past it. A refused note is
|
|
190
203
|
// indexed and then never compiled, so it reaches no document.
|
|
204
|
+
const fm = authoredFrontmatter(record);
|
|
205
|
+
const absPath = noteFile(this.contentBase, record);
|
|
206
|
+
// The id is the index's, derived against the configuration this
|
|
207
|
+
// build resolved (#270, #243) — it was derived here through
|
|
208
|
+
// `resolveNoteId(fm)` with no package, which falls back to the
|
|
209
|
+
// ambient one. What remains unset is a file with no address.
|
|
191
210
|
if (!fm || !fm.id) continue;
|
|
192
211
|
if (fm.shortcode && Array.isArray(fm.effects) && fm.effects.length) {
|
|
193
212
|
effectsByAddress.set(`${fm.type}-${fm.shortcode}`, {
|
|
@@ -196,12 +215,17 @@ export class Scenes extends BasePackCompiler {
|
|
|
196
215
|
// Where the owning item landed, so a region behaviour's
|
|
197
216
|
// effect reference addresses the right pack when a
|
|
198
217
|
// repository ships several of one type (#1566).
|
|
199
|
-
|
|
218
|
+
// This compile's router, not a freshly built one: a
|
|
219
|
+
// second router is a second answer to where the document
|
|
220
|
+
// landed, resolved from the working directory (#243).
|
|
221
|
+
pack: this.#packRouter.resolveOrNull(fm, packForType(fm.type).docType),
|
|
200
222
|
effects: fm.effects,
|
|
201
223
|
});
|
|
202
224
|
}
|
|
203
225
|
if (!isMapType(fm.type)) continue;
|
|
204
|
-
|
|
226
|
+
// Read here, and only here: the index carries no note body, and a
|
|
227
|
+
// map's prose is what this pass compiles into its Scene.
|
|
228
|
+
maps.push({ fm, body: parseMarkdownFile(absPath).body, absPath });
|
|
205
229
|
}
|
|
206
230
|
return { maps, effectsByAddress };
|
|
207
231
|
}
|
|
@@ -340,8 +364,8 @@ export class Scenes extends BasePackCompiler {
|
|
|
340
364
|
*/
|
|
341
365
|
#pageIds(markdown, entryId, name) {
|
|
342
366
|
const pageIds = new Map();
|
|
343
|
-
splitPages(markdown, name).forEach((page
|
|
344
|
-
const id = journalPageId(entryId, page
|
|
367
|
+
splitPages(markdown, name).forEach((page) => {
|
|
368
|
+
const id = journalPageId(entryId, page);
|
|
345
369
|
if (page.anchorSlug) pageIds.set(page.anchorSlug, id);
|
|
346
370
|
const slug = slugify(page.name);
|
|
347
371
|
if (slug && !pageIds.has(slug)) pageIds.set(slug, id);
|
|
@@ -384,7 +408,8 @@ export class Scenes extends BasePackCompiler {
|
|
|
384
408
|
// shared `docEntryTypes` arrangement (#1514) — so neither
|
|
385
409
|
// pass has to read the other's output.
|
|
386
410
|
const entryId = hasBody ? itemDocEntryId(fm.id) : undefined;
|
|
387
|
-
const
|
|
411
|
+
const { value: authoredFolder, isAddress: folderIsAddress } = folderField(fm);
|
|
412
|
+
const folder = this.folderResolver(authoredFolder, { isAddress: folderIsAddress });
|
|
388
413
|
// The retired spelling of the background art, reported where an author
|
|
389
414
|
// meets it soonest — every consumer runs the compile, and not every
|
|
390
415
|
// one runs the lint (#142). Located by reading the note back, which is
|
|
@@ -406,7 +431,8 @@ export class Scenes extends BasePackCompiler {
|
|
|
406
431
|
// A map note's prose is a derived JournalEntry: it lands in the
|
|
407
432
|
// default JournalEntry pack, not in whichever Scene pack the map
|
|
408
433
|
// itself was routed to (#1566).
|
|
409
|
-
|
|
434
|
+
// This compile's router, as everywhere else in this pass (#243).
|
|
435
|
+
journalPack: this.#packRouter.defaultOf("JournalEntry"),
|
|
410
436
|
pageIds: hasBody ? this.#pageIds(markdown, entryId, name) : new Map(),
|
|
411
437
|
knownActions: this.knownActions,
|
|
412
438
|
warnings,
|
|
@@ -431,7 +457,13 @@ export class Scenes extends BasePackCompiler {
|
|
|
431
457
|
name,
|
|
432
458
|
markdown,
|
|
433
459
|
leadName: name,
|
|
434
|
-
|
|
460
|
+
// As in the journals pass: an id crosses packs verbatim,
|
|
461
|
+
// an address resolves in the pack that emits it — which is
|
|
462
|
+
// what makes the folder materialise there too (#257).
|
|
463
|
+
folder:
|
|
464
|
+
folderIsAddress ?
|
|
465
|
+
this.folderResolver(authoredFolder, { isAddress: true })
|
|
466
|
+
: authoredFolder,
|
|
435
467
|
flags: fm.flags,
|
|
436
468
|
})
|
|
437
469
|
: null;
|
|
@@ -448,8 +480,8 @@ export class Scenes extends BasePackCompiler {
|
|
|
448
480
|
});
|
|
449
481
|
}
|
|
450
482
|
const place = this.places.get(placeKey);
|
|
451
|
-
place.scenes.push(
|
|
452
|
-
if (journal) place.journal.push(
|
|
483
|
+
place.scenes.push(stripAdventureKeys(scene));
|
|
484
|
+
if (journal) place.journal.push(stripAdventureKeys(journal));
|
|
453
485
|
if (Object.keys(fm.sohl?.locations ?? {}).length) {
|
|
454
486
|
place.pinned = true;
|
|
455
487
|
}
|