@heroiclands/package-build 23.0.0 → 23.1.1
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 +66 -0
- package/README.md +1 -1
- package/bin/content-commands.mjs +1 -1
- package/ci/content-format-reference.mjs +46 -0
- package/docs/api.md +124 -141
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +145 -29
- package/docs/commands.md +8 -10
- package/docs/configuration.md +13 -11
- package/docs/content-format.md +2 -2
- package/docs/guides/book.md +59 -4
- package/docs/guides/icons.md +4 -5
- package/docs/guides/packs.md +6 -0
- package/docs/guides/reference-pages.md +21 -1
- package/docs/guides/release-and-deploy.md +39 -7
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +877 -276
- package/docs/reference/note-types.md +278 -91
- package/docs/reference/theme-pages.md +58 -97
- package/engine/address-values.mjs +124 -4
- package/engine/address.mjs +44 -3
- package/engine/anchors.mjs +103 -3
- package/engine/art-slots.mjs +2 -9
- package/engine/base-compiler.mjs +25 -6
- package/engine/calendar-artifacts.mjs +4 -1
- package/engine/calendar-notes.mjs +166 -102
- package/engine/content-format-check.mjs +2 -3
- package/engine/content-format.mjs +44 -1
- package/engine/content-format.yaml +79 -16
- package/engine/content-images.mjs +10 -1
- package/engine/content-index.mjs +37 -9
- package/engine/content-links.mjs +24 -5
- package/engine/content-tables.mjs +24 -3
- package/engine/data-keys.mjs +347 -0
- package/engine/derived-sections.mjs +322 -0
- package/engine/diagnostics.mjs +28 -0
- package/engine/event-fields.mjs +187 -0
- package/engine/event-rows.mjs +126 -0
- package/engine/event-views.mjs +305 -0
- package/engine/exported-scene.mjs +29 -53
- package/engine/frontmatter-lint.mjs +103 -77
- package/engine/frontmatter.mjs +7 -2
- package/engine/generate.mjs +53 -0
- package/engine/generated-sections.mjs +223 -0
- package/engine/heading-attributes.mjs +4 -2
- package/engine/helpers.mjs +12 -1
- package/engine/holdings.mjs +12 -33
- package/engine/ids.mjs +17 -11
- package/engine/index.mjs +4 -4
- package/engine/infobox-render.mjs +21 -6
- package/engine/infobox.mjs +217 -99
- package/engine/journals.mjs +104 -47
- package/engine/literature-notes.mjs +4 -1
- package/engine/literature-works.mjs +21 -13
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +26 -0
- package/engine/near-miss.mjs +90 -0
- package/engine/note-addresses.mjs +75 -19
- package/engine/note-dates.mjs +5 -3
- package/engine/note-event-terms.mjs +364 -0
- package/engine/note-events.mjs +823 -0
- package/engine/note-vocabulary.mjs +184 -88
- package/engine/office-holders.mjs +20 -54
- package/engine/packagebuild-index.mjs +16 -1
- package/engine/pdf-build.mjs +97 -60
- package/engine/pdf-index.mjs +263 -0
- package/engine/pdf-render.mjs +47 -5
- package/engine/place-relation-terms.mjs +174 -0
- package/engine/place-relations.mjs +72 -160
- package/engine/rank-ladder.mjs +14 -41
- package/engine/reckoning-markers.mjs +2 -0
- package/engine/region-events.mjs +6 -12
- package/engine/scenes.mjs +73 -468
- package/engine/site-build.mjs +75 -101
- package/engine/site-index.mjs +3 -1
- package/engine/site-maps.mjs +43 -29
- package/engine/site-preview.mjs +40 -31
- package/engine/social-ties.mjs +2 -1
- package/engine/sql-tables.mjs +212 -9
- package/engine/standing-terms.mjs +145 -6
- package/engine/standings.mjs +37 -49
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/engine/yaml-lint.mjs +7 -4
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -3
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +62 -4
- package/sohl/note-schemas.mjs +8 -73
- package/types/engine/address-values.d.mts +50 -3
- package/types/engine/address.d.mts +21 -0
- package/types/engine/anchors.d.mts +49 -2
- package/types/engine/art-slots.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +13 -4
- package/types/engine/calendar-notes.d.mts +7 -0
- package/types/engine/content-format.d.mts +52 -0
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-tables.d.mts +9 -1
- package/types/engine/data-keys.d.mts +145 -0
- package/types/engine/derived-sections.d.mts +73 -0
- package/types/engine/diagnostics.d.mts +15 -0
- package/types/engine/event-fields.d.mts +99 -0
- package/types/engine/event-rows.d.mts +12 -0
- package/types/engine/event-views.d.mts +67 -0
- package/types/engine/exported-scene.d.mts +5 -2
- package/types/engine/frontmatter-lint.d.mts +3 -3
- package/types/engine/generated-sections.d.mts +143 -0
- package/types/engine/holdings.d.mts +6 -20
- package/types/engine/ids.d.mts +15 -12
- package/types/engine/index.d.mts +1 -1
- package/types/engine/infobox.d.mts +13 -3
- package/types/engine/journals.d.mts +24 -11
- package/types/engine/literature-works.d.mts +1 -2
- package/types/engine/map-graphviz.d.mts +2 -10
- package/types/engine/markdown-expressions.d.mts +12 -3
- package/types/engine/near-miss.d.mts +42 -0
- package/types/engine/note-addresses.d.mts +7 -1
- package/types/engine/note-event-terms.d.mts +68 -0
- package/types/engine/note-events.d.mts +71 -0
- package/types/engine/note-vocabulary.d.mts +37 -8
- package/types/engine/office-holders.d.mts +3 -0
- package/types/engine/pdf-index.d.mts +35 -0
- package/types/engine/pdf-render.d.mts +8 -2
- package/types/engine/place-relation-terms.d.mts +83 -0
- package/types/engine/place-relations.d.mts +11 -66
- package/types/engine/rank-ladder.d.mts +2 -0
- package/types/engine/region-events.d.mts +6 -12
- package/types/engine/scenes.d.mts +6 -32
- package/types/engine/site-build.d.mts +8 -22
- package/types/engine/site-maps.d.mts +15 -7
- package/types/engine/sql-tables.d.mts +77 -2
- package/types/engine/standing-terms.d.mts +37 -4
- package/types/engine/standings.d.mts +14 -9
- package/types/engine/wikilink-syntax.d.mts +20 -0
- package/types/engine/wikilinks.d.mts +3 -2
- package/types/engine/yaml-lint.d.mts +5 -4
- package/types/release.d.mts +4 -0
- package/docs/guides/government-migration.md +0 -52
- package/engine/itinerary-scenes.mjs +0 -166
- package/engine/lore-events.mjs +0 -197
- package/engine/map-notes.mjs +0 -1309
- package/engine/map-raster.mjs +0 -51
- package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
- package/hugo-theme/layouts/partials/holdings.html +0 -61
- package/hugo-theme/layouts/partials/literature-works.html +0 -29
- package/hugo-theme/layouts/partials/place-map.html +0 -9
- package/types/engine/itinerary-scenes.d.mts +0 -29
- package/types/engine/lore-events.d.mts +0 -14
- package/types/engine/map-notes.d.mts +0 -297
- package/types/engine/map-raster.d.mts +0 -8
|
@@ -91,6 +91,7 @@ import { currentType } from "./ids.mjs";
|
|
|
91
91
|
// What a place is next to and reachable from — the two relation lists and
|
|
92
92
|
// the checks that hold them to their closed sets and to each other.
|
|
93
93
|
import { checkBorders, checkRoutes } from "./place-relations.mjs";
|
|
94
|
+
import { BORDER_FIELDS, ROUTE_FIELDS } from "./place-relation-terms.mjs";
|
|
94
95
|
import {
|
|
95
96
|
CALENDAR_FIELDS,
|
|
96
97
|
INVARIANT_FIELDS,
|
|
@@ -103,11 +104,20 @@ import {
|
|
|
103
104
|
import { checkBeingAge } from "./being-age.mjs";
|
|
104
105
|
import { checkSocialTies } from "./social-ties.mjs";
|
|
105
106
|
import { checkStandings } from "./standings.mjs";
|
|
106
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
OFFICE_FIELDS,
|
|
109
|
+
RUNG_FIELDS,
|
|
110
|
+
STANDING_BODY_TYPES,
|
|
111
|
+
STANDING_FIELDS,
|
|
112
|
+
} from "./standing-terms.mjs";
|
|
107
113
|
import { checkDatedOffices } from "./office-holders.mjs";
|
|
108
114
|
import { checkAffiliationRankFloor, checkRankLadder } from "./rank-ladder.mjs";
|
|
109
115
|
import { checkCalendarChoice } from "./calendar-choice.mjs";
|
|
110
|
-
import {
|
|
116
|
+
import { checkNoteEvents } from "./note-events.mjs";
|
|
117
|
+
// The closed set of standings one body holds toward another, read from the one
|
|
118
|
+
// list the compiled Item is held to, so the lint and the compile cannot differ.
|
|
119
|
+
import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
|
|
120
|
+
import { EVENT_ENTRY } from "./note-event-terms.mjs";
|
|
111
121
|
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
112
122
|
import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
113
123
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
@@ -154,6 +164,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
154
164
|
* so each takes a row labelled and anchored by the post's own name.
|
|
155
165
|
* @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
|
|
156
166
|
* @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
|
|
167
|
+
* @property {readonly ("prose"|"event")[]} [anchors] - The anchor kinds an
|
|
168
|
+
* Address here may name with `#<anchor>`. Absent, an anchor is refused.
|
|
169
|
+
* @property {boolean} [single] - The Address names exactly one event: an
|
|
170
|
+
* anchor of kind `event`, or a note holding one event and no more.
|
|
157
171
|
* @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
|
|
158
172
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
159
173
|
* documentation.
|
|
@@ -162,8 +176,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
162
176
|
* quoting the whole map back, so the string an author has to correct is the
|
|
163
177
|
* one the message holds.
|
|
164
178
|
* @property {string} [ref] - The Address's default type, an art slot declares
|
|
165
|
-
* one — `icon` for `icon` and `tokenIcon`, `image` for `
|
|
166
|
-
* `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
|
|
179
|
+
* one — `icon` for `icon` and `tokenIcon`, `image` for `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
|
|
167
180
|
* separator states its own address. Distinct from a `FieldSpec`'s `code`
|
|
168
181
|
* ({@link module:engine/field-spec}), which names a **Shortcode** instead.
|
|
169
182
|
* @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
|
|
@@ -176,6 +189,13 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
176
189
|
* it, and the linter calls whatever it is handed. A place's `borders` and
|
|
177
190
|
* `routes` carry one, because what they state is checked against the note
|
|
178
191
|
* at the other end.
|
|
192
|
+
* @property {readonly import("./data-keys.mjs").InnerKeySpec[]} [fields] - The
|
|
193
|
+
* keys of a value that is a map, closed to them — see
|
|
194
|
+
* {@link module:engine/data-keys}.
|
|
195
|
+
* @property {object} [entries] - What one entry of a list value is, where the
|
|
196
|
+
* entries are not Addresses (`entryKind` says that).
|
|
197
|
+
* @property {object} [values] - What the value under each key of a keyed map
|
|
198
|
+
* is, where the keys are data rather than declared names.
|
|
179
199
|
* @property {string} describe - One line, for the author-facing reference.
|
|
180
200
|
*/
|
|
181
201
|
|
|
@@ -226,6 +246,13 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
|
|
|
226
246
|
/** A list. */
|
|
227
247
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
228
248
|
|
|
249
|
+
/** A list of strings. */
|
|
250
|
+
const TEXT_LIST = Object.freeze({
|
|
251
|
+
shape: "list of strings",
|
|
252
|
+
kind: "list",
|
|
253
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
254
|
+
});
|
|
255
|
+
|
|
229
256
|
/**
|
|
230
257
|
* One value, or several.
|
|
231
258
|
*
|
|
@@ -234,7 +261,11 @@ const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
|
234
261
|
* That is what separates this from `list-or-map`, whose halves say different
|
|
235
262
|
* things.
|
|
236
263
|
*/
|
|
237
|
-
const TEXT_OR_LIST = Object.freeze({
|
|
264
|
+
const TEXT_OR_LIST = Object.freeze({
|
|
265
|
+
shape: "string or list",
|
|
266
|
+
kind: "string-or-list",
|
|
267
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
268
|
+
});
|
|
238
269
|
|
|
239
270
|
/** A single Address. */
|
|
240
271
|
const LINK = Object.freeze({ shape: "an Address", kind: "address" });
|
|
@@ -259,6 +290,11 @@ const STANDINGS = Object.freeze({
|
|
|
259
290
|
kind: "list-or-map",
|
|
260
291
|
entryKind: "address",
|
|
261
292
|
keyKind: "address",
|
|
293
|
+
values: Object.freeze({
|
|
294
|
+
kind: "map",
|
|
295
|
+
shape: "a map — `{ rank, office? }`",
|
|
296
|
+
fields: STANDING_FIELDS,
|
|
297
|
+
}),
|
|
262
298
|
standings: true,
|
|
263
299
|
ref: "affiliation",
|
|
264
300
|
accepts: STANDING_BODY_TYPES,
|
|
@@ -321,6 +357,29 @@ const TOKEN_ICON = Object.freeze({
|
|
|
321
357
|
describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
|
|
322
358
|
});
|
|
323
359
|
|
|
360
|
+
/**
|
|
361
|
+
* What happened to or at a subject — the `events` key, declared once and spread
|
|
362
|
+
* into every type that carries events, so the schema cannot differ between
|
|
363
|
+
* them. The entry's own schema and its check live in
|
|
364
|
+
* {@link module:engine/note-events}.
|
|
365
|
+
*
|
|
366
|
+
* Declared here rather than in that module because its imports reach back to
|
|
367
|
+
* this one: a function declaration is bound before either module runs, and an
|
|
368
|
+
* object is not.
|
|
369
|
+
*
|
|
370
|
+
* @type {DataFieldSpec}
|
|
371
|
+
*/
|
|
372
|
+
export const EVENTS_FIELD = Object.freeze({
|
|
373
|
+
name: "events",
|
|
374
|
+
kind: "list",
|
|
375
|
+
shape: "list of event entries",
|
|
376
|
+
entries: EVENT_ENTRY,
|
|
377
|
+
check: checkNoteEvents,
|
|
378
|
+
describe:
|
|
379
|
+
"What happened to or at this subject — each entry one dated, attributed event: " +
|
|
380
|
+
"a founding, a war, a fall, or an occasion that recurs.",
|
|
381
|
+
});
|
|
382
|
+
|
|
324
383
|
/**
|
|
325
384
|
* One top-level frontmatter key.
|
|
326
385
|
*
|
|
@@ -455,9 +514,24 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
455
514
|
}),
|
|
456
515
|
Object.freeze({
|
|
457
516
|
name: "harnworld",
|
|
458
|
-
shape: "
|
|
517
|
+
shape: "`{ realm?, ritual? }`",
|
|
459
518
|
kind: "map",
|
|
460
|
-
|
|
519
|
+
fields: Object.freeze([
|
|
520
|
+
Object.freeze({
|
|
521
|
+
name: "realm",
|
|
522
|
+
kind: "string",
|
|
523
|
+
shape: "a string",
|
|
524
|
+
describe: "The HârnWorld realm the subject belongs to.",
|
|
525
|
+
}),
|
|
526
|
+
Object.freeze({
|
|
527
|
+
name: "ritual",
|
|
528
|
+
kind: "list",
|
|
529
|
+
shape: "list of strings",
|
|
530
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
531
|
+
describe: "The HârnWorld religions the subject observes.",
|
|
532
|
+
}),
|
|
533
|
+
]),
|
|
534
|
+
describe: "HârnWorld source details shared by every system — `realm` and `ritual`.",
|
|
461
535
|
}),
|
|
462
536
|
Object.freeze({
|
|
463
537
|
name: "icon",
|
|
@@ -478,7 +552,20 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
478
552
|
]);
|
|
479
553
|
|
|
480
554
|
/**
|
|
481
|
-
*
|
|
555
|
+
* An Address naming one event: `place-ironfells#sack`, or a note holding
|
|
556
|
+
* exactly one event, with a bare shortcode read as a `lore` note.
|
|
557
|
+
*/
|
|
558
|
+
const EVENT_ADDRESS = Object.freeze({
|
|
559
|
+
kind: "address",
|
|
560
|
+
ref: "lore",
|
|
561
|
+
anchors: Object.freeze(["event"]),
|
|
562
|
+
single: true,
|
|
563
|
+
shape: "an Address of one event",
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The values every carried thing declares: the four a game reads, and the two
|
|
568
|
+
* events of its history.
|
|
482
569
|
*
|
|
483
570
|
* @type {readonly DataFieldSpec[]}
|
|
484
571
|
*/
|
|
@@ -487,6 +574,20 @@ const GEAR = Object.freeze([
|
|
|
487
574
|
{ name: "value", ...NUM, describe: "What the thing is worth." },
|
|
488
575
|
{ name: "quality", ...NUM, describe: "How well it is made." },
|
|
489
576
|
{ name: "durability", ...NUM, describe: "How much wear it takes before it fails." },
|
|
577
|
+
{
|
|
578
|
+
name: "made",
|
|
579
|
+
...EVENT_ADDRESS,
|
|
580
|
+
describe:
|
|
581
|
+
"The event in which the thing was made — `lore-forging`, or one event of a " +
|
|
582
|
+
"note as `place-ironfells#raising`.",
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "lost",
|
|
586
|
+
...EVENT_ADDRESS,
|
|
587
|
+
describe:
|
|
588
|
+
"The event in which the thing was lost — `lore-flood`, or one event of a note " +
|
|
589
|
+
"as `place-ironfells#sack`.",
|
|
590
|
+
},
|
|
490
591
|
]);
|
|
491
592
|
|
|
492
593
|
/** Gear that is counted rather than carried singly. */
|
|
@@ -855,10 +956,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
855
956
|
// package's declared present compute — see `engine/being-age.mjs`.
|
|
856
957
|
check: checkBeingAge,
|
|
857
958
|
data: Object.freeze([
|
|
858
|
-
{ name: "social", shape: "map", kind: "map", describe: "The being's social profile." },
|
|
859
959
|
TOKEN_ICON,
|
|
860
960
|
TEMPLATE_PRIORITY,
|
|
861
|
-
{ name: "archetypes", ...
|
|
961
|
+
{ name: "archetypes", ...TEXT_LIST, describe: "Archetypal behaviours the being fits." },
|
|
862
962
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
863
963
|
{
|
|
864
964
|
name: "stations",
|
|
@@ -906,6 +1006,10 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
906
1006
|
accepts: SOCIAL_TIE_TARGET_TYPES,
|
|
907
1007
|
terms: SOCIAL_TIES,
|
|
908
1008
|
shape: "a map keyed by Address",
|
|
1009
|
+
values: Object.freeze({
|
|
1010
|
+
kind: "string",
|
|
1011
|
+
shape: `a tie — ${SOCIAL_TIES.map(({ term }) => term).join(", ")}`,
|
|
1012
|
+
}),
|
|
909
1013
|
check: checkSocialTies,
|
|
910
1014
|
describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
|
|
911
1015
|
},
|
|
@@ -999,7 +1103,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
999
1103
|
},
|
|
1000
1104
|
{
|
|
1001
1105
|
name: "appearance.extra_features",
|
|
1002
|
-
...
|
|
1106
|
+
...TEXT_LIST,
|
|
1003
1107
|
describe: "Anything else a stranger would notice.",
|
|
1004
1108
|
},
|
|
1005
1109
|
]),
|
|
@@ -1031,6 +1135,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1031
1135
|
]),
|
|
1032
1136
|
data: Object.freeze([
|
|
1033
1137
|
TEMPLATE_PRIORITY,
|
|
1138
|
+
EVENTS_FIELD,
|
|
1034
1139
|
{
|
|
1035
1140
|
name: "demonym",
|
|
1036
1141
|
...TEXT,
|
|
@@ -1061,6 +1166,12 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1061
1166
|
{
|
|
1062
1167
|
name: "governance.ranks",
|
|
1063
1168
|
...LIST,
|
|
1169
|
+
shape: "list of `{ level, title, description, lore? }`",
|
|
1170
|
+
entries: Object.freeze({
|
|
1171
|
+
kind: "map",
|
|
1172
|
+
shape: "a map — `{ level, title, description, lore? }`",
|
|
1173
|
+
fields: RUNG_FIELDS,
|
|
1174
|
+
}),
|
|
1064
1175
|
// A rung is `{level, title, description}` with an optional
|
|
1065
1176
|
// `lore`. The shape check sees a list and stops there, so the
|
|
1066
1177
|
// rungs inside it are checked here — a ladder is what a being's
|
|
@@ -1075,7 +1186,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1075
1186
|
},
|
|
1076
1187
|
{
|
|
1077
1188
|
name: "governance.offices",
|
|
1078
|
-
|
|
1189
|
+
kind: "map",
|
|
1190
|
+
shape: "a map of named offices",
|
|
1191
|
+
values: Object.freeze({
|
|
1192
|
+
kind: "scalar-or-map",
|
|
1193
|
+
shape: "a description, or `{ description, holders }`",
|
|
1194
|
+
fields: OFFICE_FIELDS,
|
|
1195
|
+
}),
|
|
1079
1196
|
// A map of named posts: each takes a row of its own, labelled
|
|
1080
1197
|
// by the post and anchored by it, so the description a body
|
|
1081
1198
|
// declares is a destination a being's `office` can link to.
|
|
@@ -1120,6 +1237,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1120
1237
|
ref: "affiliation",
|
|
1121
1238
|
accepts: ["affiliation"],
|
|
1122
1239
|
shape: "a map keyed by Address",
|
|
1240
|
+
values: Object.freeze({
|
|
1241
|
+
kind: "string",
|
|
1242
|
+
allowed: AFFILIATION_STANDINGS,
|
|
1243
|
+
shape: `a standing — ${AFFILIATION_STANDINGS.join(", ")}`,
|
|
1244
|
+
}),
|
|
1123
1245
|
describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
|
|
1124
1246
|
},
|
|
1125
1247
|
]),
|
|
@@ -1249,6 +1371,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1249
1371
|
keyKind: "shortcode",
|
|
1250
1372
|
keySelector: "subType",
|
|
1251
1373
|
shape: "a map keyed by Shortcode or subType selector",
|
|
1374
|
+
values: Object.freeze({ kind: "number", shape: "a number" }),
|
|
1252
1375
|
describe:
|
|
1253
1376
|
"Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
|
|
1254
1377
|
},
|
|
@@ -1492,19 +1615,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1492
1615
|
...checkCalendarNote(note, context),
|
|
1493
1616
|
...checkLiteratureNote(note),
|
|
1494
1617
|
],
|
|
1495
|
-
data: Object.freeze([
|
|
1496
|
-
...CALENDAR_FIELDS,
|
|
1497
|
-
...LITERATURE_FIELDS,
|
|
1498
|
-
{
|
|
1499
|
-
name: "events",
|
|
1500
|
-
kind: "list",
|
|
1501
|
-
shape: "list of `{ when, until?, recurs? }`",
|
|
1502
|
-
check: checkLoreEvents,
|
|
1503
|
-
describe:
|
|
1504
|
-
"This note's dated occurrences — a founding once, an anniversary " +
|
|
1505
|
-
"that recurs, or a list of recorded happenings.",
|
|
1506
|
-
},
|
|
1507
|
-
]),
|
|
1618
|
+
data: Object.freeze([...CALENDAR_FIELDS, ...LITERATURE_FIELDS, EVENTS_FIELD]),
|
|
1508
1619
|
}),
|
|
1509
1620
|
|
|
1510
1621
|
place: Object.freeze({
|
|
@@ -1520,6 +1631,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1520
1631
|
]),
|
|
1521
1632
|
check: checkPlace,
|
|
1522
1633
|
data: Object.freeze([
|
|
1634
|
+
EVENTS_FIELD,
|
|
1523
1635
|
{
|
|
1524
1636
|
name: "calendar",
|
|
1525
1637
|
...LINK,
|
|
@@ -1586,6 +1698,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1586
1698
|
name: "borders",
|
|
1587
1699
|
...LIST,
|
|
1588
1700
|
shape: "list of `{ to, bearing }` entries",
|
|
1701
|
+
entries: Object.freeze({
|
|
1702
|
+
kind: "map",
|
|
1703
|
+
shape: "a map — `{ to, bearing }`",
|
|
1704
|
+
fields: BORDER_FIELDS,
|
|
1705
|
+
}),
|
|
1589
1706
|
check: checkBorders,
|
|
1590
1707
|
describe:
|
|
1591
1708
|
"Places sharing a frontier with this one — each the other's shortcode " +
|
|
@@ -1595,6 +1712,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1595
1712
|
name: "routes",
|
|
1596
1713
|
...LIST,
|
|
1597
1714
|
shape: "list of `{ to, bearing, mode, days, terrain?, leagues? }` entries",
|
|
1715
|
+
entries: Object.freeze({
|
|
1716
|
+
kind: "map",
|
|
1717
|
+
shape: "a map — `{ to, bearing, mode, days, terrain?, leagues? }`",
|
|
1718
|
+
fields: ROUTE_FIELDS,
|
|
1719
|
+
}),
|
|
1598
1720
|
check: checkRoutes,
|
|
1599
1721
|
describe:
|
|
1600
1722
|
"Journeys from this place's centre — where the destination lies, how " +
|
|
@@ -1658,7 +1780,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1658
1780
|
},
|
|
1659
1781
|
{
|
|
1660
1782
|
name: "party.archetypes",
|
|
1661
|
-
...
|
|
1783
|
+
...TEXT_LIST,
|
|
1662
1784
|
describe: "Archetypes the scenario is written for.",
|
|
1663
1785
|
},
|
|
1664
1786
|
]),
|
|
@@ -1668,66 +1790,52 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1668
1790
|
|
|
1669
1791
|
map: Object.freeze({
|
|
1670
1792
|
stubbable: true,
|
|
1671
|
-
// The subType classifies the map; `data.scene`
|
|
1793
|
+
// The subType classifies the map; `data.scene` is its Foundry Scene.
|
|
1672
1794
|
subTypes: Object.freeze(["battlemap", "localmap", "regionalmap", "totm"]),
|
|
1673
1795
|
data: Object.freeze([
|
|
1674
1796
|
{
|
|
1675
1797
|
name: "scene",
|
|
1676
|
-
|
|
1677
|
-
|
|
1798
|
+
kind: "map",
|
|
1799
|
+
shape: "an object — a Foundry Scene document as exported",
|
|
1800
|
+
describe:
|
|
1801
|
+
"The Foundry Scene as exported, passed through unchanged apart " +
|
|
1802
|
+
"from `data.fixup` and its pins marked `#anchor`. Nothing inside it is checked; " +
|
|
1803
|
+
"the book prints its levels' background images.",
|
|
1678
1804
|
},
|
|
1679
1805
|
{
|
|
1680
1806
|
name: "fixup",
|
|
1681
1807
|
...LIST,
|
|
1808
|
+
shape: "list of `{ path, type, value }`",
|
|
1809
|
+
entries: Object.freeze({
|
|
1810
|
+
kind: "map",
|
|
1811
|
+
shape: "a map — `{ path, type, value }`",
|
|
1812
|
+
fields: Object.freeze([
|
|
1813
|
+
Object.freeze({
|
|
1814
|
+
name: "path",
|
|
1815
|
+
kind: "string",
|
|
1816
|
+
required: true,
|
|
1817
|
+
shape: "a property path into `data.scene`",
|
|
1818
|
+
describe: "The exported field to replace, from `data.scene`.",
|
|
1819
|
+
}),
|
|
1820
|
+
Object.freeze({
|
|
1821
|
+
name: "type",
|
|
1822
|
+
kind: "string",
|
|
1823
|
+
required: true,
|
|
1824
|
+
allowed: Object.freeze(["address"]),
|
|
1825
|
+
shape: "`address`",
|
|
1826
|
+
describe: "What `value` is: `address`.",
|
|
1827
|
+
}),
|
|
1828
|
+
Object.freeze({
|
|
1829
|
+
name: "value",
|
|
1830
|
+
kind: "address",
|
|
1831
|
+
required: true,
|
|
1832
|
+
shape: "an asset Address",
|
|
1833
|
+
describe: "The asset Address whose path replaces the exported one.",
|
|
1834
|
+
}),
|
|
1835
|
+
]),
|
|
1836
|
+
}),
|
|
1682
1837
|
describe: "Asset address replacements in an exported Scene.",
|
|
1683
1838
|
},
|
|
1684
|
-
// A Scene has no `img`, so the shared art key reaches nothing here:
|
|
1685
|
-
// a map's background is its own slot, and an `image` address.
|
|
1686
|
-
{
|
|
1687
|
-
name: "bgImage",
|
|
1688
|
-
...LINK,
|
|
1689
|
-
ref: "image",
|
|
1690
|
-
accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
|
|
1691
|
-
describe: "The map's background art — an `image` address.",
|
|
1692
|
-
},
|
|
1693
|
-
{
|
|
1694
|
-
name: "scale",
|
|
1695
|
-
...ANY,
|
|
1696
|
-
describe: "Regional map distance per grid unit: {distance, unit}.",
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
name: "dimensions",
|
|
1700
|
-
...LIST,
|
|
1701
|
-
describe: "`[width, height]` in whole pixels — the art's own size.",
|
|
1702
|
-
},
|
|
1703
|
-
{
|
|
1704
|
-
name: "pxPerGrid",
|
|
1705
|
-
...NUM,
|
|
1706
|
-
describe: "Whole pixels per grid square; must match the art.",
|
|
1707
|
-
},
|
|
1708
|
-
{ name: "navName", ...TEXT, describe: "Short name for the navigation bar." },
|
|
1709
|
-
{ name: "levelName", ...TEXT, describe: "Name of the embedded level." },
|
|
1710
|
-
{
|
|
1711
|
-
name: "backgroundColor",
|
|
1712
|
-
...TEXT,
|
|
1713
|
-
describe: "Colour shown where the art does not reach.",
|
|
1714
|
-
},
|
|
1715
|
-
{ name: "overlay", ...TEXT, describe: "Path to the foreground art." },
|
|
1716
|
-
// Geometry carries no `kind`. The specification lists each
|
|
1717
|
-
// as a sequence while the notes authoring them today write
|
|
1718
|
-
// a map keyed by name, and a lint has no business picking
|
|
1719
|
-
// the winner of a disagreement the format has not settled.
|
|
1720
|
-
{ name: "walls", ...ANY, describe: "Wall segments." },
|
|
1721
|
-
{ name: "doors", ...ANY, describe: "Doors." },
|
|
1722
|
-
{ name: "lights", ...ANY, describe: "Light sources." },
|
|
1723
|
-
{ name: "tiles", ...ANY, describe: "Tiles." },
|
|
1724
|
-
{ name: "sounds", ...ANY, describe: "Ambient sounds." },
|
|
1725
|
-
{ name: "regions", ...ANY, describe: "Regions and their behaviours." },
|
|
1726
|
-
{
|
|
1727
|
-
name: "notes",
|
|
1728
|
-
...ANY,
|
|
1729
|
-
describe: "Map pins, each a grid location and an anchor in this note's own body.",
|
|
1730
|
-
},
|
|
1731
1839
|
{
|
|
1732
1840
|
name: "place",
|
|
1733
1841
|
...LINK,
|
|
@@ -1867,15 +1975,3 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
|
|
|
1867
1975
|
if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
|
|
1868
1976
|
return entry.subTypes;
|
|
1869
1977
|
}
|
|
1870
|
-
|
|
1871
|
-
/**
|
|
1872
|
-
* Accepted migration input with no canonical field or emitted value.
|
|
1873
|
-
*
|
|
1874
|
-
* Legacy affiliation domains are ignored. They never supply a government.
|
|
1875
|
-
* @param {string} type - The note type.
|
|
1876
|
-
* @param {string} key - An authored data key.
|
|
1877
|
-
* @returns {boolean} Whether the obsolete key is accepted during migration.
|
|
1878
|
-
*/
|
|
1879
|
-
export function isLegacyDataField(type, key) {
|
|
1880
|
-
return currentType(type) === "affiliation" && key === "domains";
|
|
1881
|
-
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
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
|
+
*
|
|
2
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
12
|
*/
|
|
4
13
|
|
|
@@ -7,8 +16,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
7
16
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
8
17
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
import { HOLDER_FIELDS, OFFICE_FIELDS } from "./standing-terms.mjs";
|
|
20
|
+
|
|
21
|
+
export { HOLDER_FIELDS, OFFICE_FIELDS };
|
|
22
|
+
|
|
12
23
|
const mapping = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -26,65 +37,20 @@ export function checkDatedOffices(note, { index } = {}) {
|
|
|
26
37
|
severity: "error",
|
|
27
38
|
message,
|
|
28
39
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"data.governance.offices must be a map of named offices",
|
|
34
|
-
),
|
|
35
|
-
];
|
|
40
|
+
// The roster's shape — a map of offices, each a description or a map of
|
|
41
|
+
// `OFFICE_FIELDS`, each holder a map of `HOLDER_FIELDS` — is the inner-key
|
|
42
|
+
// check's. What is checked here is what the holders and their dates mean.
|
|
43
|
+
if (!mapping(offices)) return [];
|
|
36
44
|
|
|
37
45
|
const dates = reckoningContext(index);
|
|
38
46
|
for (const [office, value] of Object.entries(offices)) {
|
|
39
47
|
const base = ["data", "governance", "offices", office];
|
|
40
|
-
if (
|
|
41
|
-
if (!mapping(value)) {
|
|
42
|
-
findings.push(
|
|
43
|
-
at(base, `office ${office} must be a description or a {description, holders} map`),
|
|
44
|
-
);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
for (const key of Object.keys(value))
|
|
48
|
-
if (!OFFICE_KEYS.has(key))
|
|
49
|
-
findings.push(
|
|
50
|
-
at(
|
|
51
|
-
[...base, key],
|
|
52
|
-
`office ${office} has unknown key ${key}; use description or holders`,
|
|
53
|
-
),
|
|
54
|
-
);
|
|
55
|
-
if (typeof value.description !== "string")
|
|
56
|
-
findings.push(
|
|
57
|
-
at([...base, "description"], `office ${office} needs a string description`),
|
|
58
|
-
);
|
|
59
|
-
if (!Array.isArray(value.holders)) {
|
|
60
|
-
findings.push(at([...base, "holders"], `office ${office} needs a holders list`));
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
48
|
+
if (!mapping(value) || !Array.isArray(value.holders)) continue;
|
|
63
49
|
const terms = [];
|
|
64
50
|
value.holders.forEach((row, position) => {
|
|
65
51
|
const rowPath = [...base, "holders", position];
|
|
66
|
-
if (!mapping(row))
|
|
67
|
-
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
for (const key of Object.keys(row))
|
|
71
|
-
if (!HOLDER_KEYS.has(key))
|
|
72
|
-
findings.push(
|
|
73
|
-
at([...rowPath, key], `office ${office} holder has unknown key ${key}`),
|
|
74
|
-
);
|
|
75
|
-
if (row.contested !== undefined && typeof row.contested !== "boolean")
|
|
76
|
-
findings.push(
|
|
77
|
-
at(
|
|
78
|
-
[...rowPath, "contested"],
|
|
79
|
-
`office ${office} contested must be true or false`,
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
if (typeof row.being !== "string" || !row.being.trim()) {
|
|
84
|
-
findings.push(
|
|
85
|
-
at([...rowPath, "being"], `office ${office} holder needs a being Address`),
|
|
86
|
-
);
|
|
87
|
-
} else {
|
|
52
|
+
if (!mapping(row)) return;
|
|
53
|
+
if (typeof row.being === "string" && row.being.trim()) {
|
|
88
54
|
const tuple = parseAddress(row.being, {
|
|
89
55
|
package: index?.contentPackage,
|
|
90
56
|
system: "note",
|
|
@@ -50,6 +50,7 @@ import { fileURLToPath } from "node:url";
|
|
|
50
50
|
import { collectAssetRecords } from "./asset-index.mjs";
|
|
51
51
|
import { recordPath, sortKeysDeep } from "./index-records.mjs";
|
|
52
52
|
import { PACKAGEBUILD_PACKAGE, metadataFileName } from "./packages.mjs";
|
|
53
|
+
import { decodeIndexAddresses, publishAddresses } from "./note-addresses.mjs";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* This package's own root, wherever it is installed.
|
|
@@ -117,7 +118,21 @@ export function packageBuildRecords(assetsBase = PACKAGEBUILD_ASSETS) {
|
|
|
117
118
|
*/
|
|
118
119
|
export function emitPackageBuildIndex({ assetsBase, file = PACKAGEBUILD_INDEX_FILE } = {}) {
|
|
119
120
|
const records = packageBuildRecords(assetsBase);
|
|
120
|
-
|
|
121
|
+
// Written the way every published index is: each Address an object.
|
|
122
|
+
const text =
|
|
123
|
+
records.length ?
|
|
124
|
+
`${records
|
|
125
|
+
.map((record) =>
|
|
126
|
+
JSON.stringify(
|
|
127
|
+
sortKeysDeep(
|
|
128
|
+
publishAddresses(
|
|
129
|
+
decodeIndexAddresses(record, { package: PACKAGEBUILD_PACKAGE }),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
.join("\n")}\n`
|
|
135
|
+
: "";
|
|
121
136
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
122
137
|
fs.writeFileSync(file, text);
|
|
123
138
|
return { file, assets: records.length, bytes: Buffer.byteLength(text) };
|