@heroiclands/package-build 22.28.0 → 23.1.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 +72 -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 +125 -142
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/first-note.md +1 -1
- 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 +3 -3
- 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 +17 -1
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +904 -275
- package/docs/reference/note-types.md +282 -91
- package/docs/reference/theme-pages.md +58 -66
- 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 +84 -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 +136 -0
- package/engine/literature-works.mjs +161 -0
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +34 -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 +195 -92
- 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 +74 -85
- 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/stub-lint.mjs +6 -0
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -2
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/default-item-art.mjs +6 -6
- 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-notes.d.mts +29 -0
- package/types/engine/literature-works.d.mts +67 -0
- 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/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/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,12 +104,22 @@ 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";
|
|
122
|
+
import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
112
123
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
113
124
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
114
125
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -153,6 +164,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
153
164
|
* so each takes a row labelled and anchored by the post's own name.
|
|
154
165
|
* @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
|
|
155
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.
|
|
156
171
|
* @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
|
|
157
172
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
158
173
|
* documentation.
|
|
@@ -161,8 +176,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
161
176
|
* quoting the whole map back, so the string an author has to correct is the
|
|
162
177
|
* one the message holds.
|
|
163
178
|
* @property {string} [ref] - The Address's default type, an art slot declares
|
|
164
|
-
* one — `icon` for `icon` and `tokenIcon`, `image` for `
|
|
165
|
-
* `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
|
|
166
180
|
* separator states its own address. Distinct from a `FieldSpec`'s `code`
|
|
167
181
|
* ({@link module:engine/field-spec}), which names a **Shortcode** instead.
|
|
168
182
|
* @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
|
|
@@ -175,6 +189,13 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
175
189
|
* it, and the linter calls whatever it is handed. A place's `borders` and
|
|
176
190
|
* `routes` carry one, because what they state is checked against the note
|
|
177
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.
|
|
178
199
|
* @property {string} describe - One line, for the author-facing reference.
|
|
179
200
|
*/
|
|
180
201
|
|
|
@@ -225,6 +246,13 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
|
|
|
225
246
|
/** A list. */
|
|
226
247
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
227
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
|
+
|
|
228
256
|
/**
|
|
229
257
|
* One value, or several.
|
|
230
258
|
*
|
|
@@ -233,7 +261,11 @@ const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
|
233
261
|
* That is what separates this from `list-or-map`, whose halves say different
|
|
234
262
|
* things.
|
|
235
263
|
*/
|
|
236
|
-
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
|
+
});
|
|
237
269
|
|
|
238
270
|
/** A single Address. */
|
|
239
271
|
const LINK = Object.freeze({ shape: "an Address", kind: "address" });
|
|
@@ -258,6 +290,11 @@ const STANDINGS = Object.freeze({
|
|
|
258
290
|
kind: "list-or-map",
|
|
259
291
|
entryKind: "address",
|
|
260
292
|
keyKind: "address",
|
|
293
|
+
values: Object.freeze({
|
|
294
|
+
kind: "map",
|
|
295
|
+
shape: "a map — `{ rank, office? }`",
|
|
296
|
+
fields: STANDING_FIELDS,
|
|
297
|
+
}),
|
|
261
298
|
standings: true,
|
|
262
299
|
ref: "affiliation",
|
|
263
300
|
accepts: STANDING_BODY_TYPES,
|
|
@@ -320,6 +357,29 @@ const TOKEN_ICON = Object.freeze({
|
|
|
320
357
|
describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
|
|
321
358
|
});
|
|
322
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
|
+
|
|
323
383
|
/**
|
|
324
384
|
* One top-level frontmatter key.
|
|
325
385
|
*
|
|
@@ -454,9 +514,24 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
454
514
|
}),
|
|
455
515
|
Object.freeze({
|
|
456
516
|
name: "harnworld",
|
|
457
|
-
shape: "
|
|
517
|
+
shape: "`{ realm?, ritual? }`",
|
|
458
518
|
kind: "map",
|
|
459
|
-
|
|
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`.",
|
|
460
535
|
}),
|
|
461
536
|
Object.freeze({
|
|
462
537
|
name: "icon",
|
|
@@ -477,7 +552,20 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
477
552
|
]);
|
|
478
553
|
|
|
479
554
|
/**
|
|
480
|
-
*
|
|
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.
|
|
481
569
|
*
|
|
482
570
|
* @type {readonly DataFieldSpec[]}
|
|
483
571
|
*/
|
|
@@ -486,6 +574,20 @@ const GEAR = Object.freeze([
|
|
|
486
574
|
{ name: "value", ...NUM, describe: "What the thing is worth." },
|
|
487
575
|
{ name: "quality", ...NUM, describe: "How well it is made." },
|
|
488
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
|
+
},
|
|
489
591
|
]);
|
|
490
592
|
|
|
491
593
|
/** Gear that is counted rather than carried singly. */
|
|
@@ -854,10 +956,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
854
956
|
// package's declared present compute — see `engine/being-age.mjs`.
|
|
855
957
|
check: checkBeingAge,
|
|
856
958
|
data: Object.freeze([
|
|
857
|
-
{ name: "social", shape: "map", kind: "map", describe: "The being's social profile." },
|
|
858
959
|
TOKEN_ICON,
|
|
859
960
|
TEMPLATE_PRIORITY,
|
|
860
|
-
{ name: "archetypes", ...
|
|
961
|
+
{ name: "archetypes", ...TEXT_LIST, describe: "Archetypal behaviours the being fits." },
|
|
861
962
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
862
963
|
{
|
|
863
964
|
name: "stations",
|
|
@@ -905,6 +1006,10 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
905
1006
|
accepts: SOCIAL_TIE_TARGET_TYPES,
|
|
906
1007
|
terms: SOCIAL_TIES,
|
|
907
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
|
+
}),
|
|
908
1013
|
check: checkSocialTies,
|
|
909
1014
|
describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
|
|
910
1015
|
},
|
|
@@ -998,7 +1103,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
998
1103
|
},
|
|
999
1104
|
{
|
|
1000
1105
|
name: "appearance.extra_features",
|
|
1001
|
-
...
|
|
1106
|
+
...TEXT_LIST,
|
|
1002
1107
|
describe: "Anything else a stranger would notice.",
|
|
1003
1108
|
},
|
|
1004
1109
|
]),
|
|
@@ -1030,6 +1135,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1030
1135
|
]),
|
|
1031
1136
|
data: Object.freeze([
|
|
1032
1137
|
TEMPLATE_PRIORITY,
|
|
1138
|
+
EVENTS_FIELD,
|
|
1033
1139
|
{
|
|
1034
1140
|
name: "demonym",
|
|
1035
1141
|
...TEXT,
|
|
@@ -1060,6 +1166,12 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1060
1166
|
{
|
|
1061
1167
|
name: "governance.ranks",
|
|
1062
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
|
+
}),
|
|
1063
1175
|
// A rung is `{level, title, description}` with an optional
|
|
1064
1176
|
// `lore`. The shape check sees a list and stops there, so the
|
|
1065
1177
|
// rungs inside it are checked here — a ladder is what a being's
|
|
@@ -1074,7 +1186,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1074
1186
|
},
|
|
1075
1187
|
{
|
|
1076
1188
|
name: "governance.offices",
|
|
1077
|
-
|
|
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
|
+
}),
|
|
1078
1196
|
// A map of named posts: each takes a row of its own, labelled
|
|
1079
1197
|
// by the post and anchored by it, so the description a body
|
|
1080
1198
|
// declares is a destination a being's `office` can link to.
|
|
@@ -1119,6 +1237,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1119
1237
|
ref: "affiliation",
|
|
1120
1238
|
accepts: ["affiliation"],
|
|
1121
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
|
+
}),
|
|
1122
1245
|
describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
|
|
1123
1246
|
},
|
|
1124
1247
|
]),
|
|
@@ -1248,6 +1371,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1248
1371
|
keyKind: "shortcode",
|
|
1249
1372
|
keySelector: "subType",
|
|
1250
1373
|
shape: "a map keyed by Shortcode or subType selector",
|
|
1374
|
+
values: Object.freeze({ kind: "number", shape: "a number" }),
|
|
1251
1375
|
describe:
|
|
1252
1376
|
"Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
|
|
1253
1377
|
},
|
|
@@ -1479,25 +1603,19 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1479
1603
|
"custom", // How a people practices a rite, observance, or usage.
|
|
1480
1604
|
"bestiary", // A kind of creature that is not a people.
|
|
1481
1605
|
"gathering", // A recurring public assembly, such as a fair or tournament.
|
|
1606
|
+
"literature", // A work a people tells, sings, or writes, such as an epic or legend.
|
|
1482
1607
|
]),
|
|
1483
1608
|
// A lore note is prose, and what it *is* about is its subType — with
|
|
1484
|
-
//
|
|
1485
|
-
// is data;
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
...
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
shape: "list of `{ when, until?, recurs? }`",
|
|
1495
|
-
check: checkLoreEvents,
|
|
1496
|
-
describe:
|
|
1497
|
-
"This note's dated occurrences — a founding once, an anniversary " +
|
|
1498
|
-
"that recurs, or a list of recorded happenings.",
|
|
1499
|
-
},
|
|
1500
|
-
]),
|
|
1609
|
+
// two exceptions. A calendar is a division of the year, and a division
|
|
1610
|
+
// is data; a work of literature states whose it is, in what tongue and
|
|
1611
|
+
// about what. The checks scope each family to the subType that means
|
|
1612
|
+
// it, because `DataFieldSpec` declares the keys a type accepts and not
|
|
1613
|
+
// the subType that may write them.
|
|
1614
|
+
check: (note, context) => [
|
|
1615
|
+
...checkCalendarNote(note, context),
|
|
1616
|
+
...checkLiteratureNote(note),
|
|
1617
|
+
],
|
|
1618
|
+
data: Object.freeze([...CALENDAR_FIELDS, ...LITERATURE_FIELDS, EVENTS_FIELD]),
|
|
1501
1619
|
}),
|
|
1502
1620
|
|
|
1503
1621
|
place: Object.freeze({
|
|
@@ -1513,6 +1631,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1513
1631
|
]),
|
|
1514
1632
|
check: checkPlace,
|
|
1515
1633
|
data: Object.freeze([
|
|
1634
|
+
EVENTS_FIELD,
|
|
1516
1635
|
{
|
|
1517
1636
|
name: "calendar",
|
|
1518
1637
|
...LINK,
|
|
@@ -1579,6 +1698,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1579
1698
|
name: "borders",
|
|
1580
1699
|
...LIST,
|
|
1581
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
|
+
}),
|
|
1582
1706
|
check: checkBorders,
|
|
1583
1707
|
describe:
|
|
1584
1708
|
"Places sharing a frontier with this one — each the other's shortcode " +
|
|
@@ -1588,6 +1712,11 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1588
1712
|
name: "routes",
|
|
1589
1713
|
...LIST,
|
|
1590
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
|
+
}),
|
|
1591
1720
|
check: checkRoutes,
|
|
1592
1721
|
describe:
|
|
1593
1722
|
"Journeys from this place's centre — where the destination lies, how " +
|
|
@@ -1651,7 +1780,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1651
1780
|
},
|
|
1652
1781
|
{
|
|
1653
1782
|
name: "party.archetypes",
|
|
1654
|
-
...
|
|
1783
|
+
...TEXT_LIST,
|
|
1655
1784
|
describe: "Archetypes the scenario is written for.",
|
|
1656
1785
|
},
|
|
1657
1786
|
]),
|
|
@@ -1661,66 +1790,52 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1661
1790
|
|
|
1662
1791
|
map: Object.freeze({
|
|
1663
1792
|
stubbable: true,
|
|
1664
|
-
// The subType classifies the map; `data.scene`
|
|
1793
|
+
// The subType classifies the map; `data.scene` is its Foundry Scene.
|
|
1665
1794
|
subTypes: Object.freeze(["battlemap", "localmap", "regionalmap", "totm"]),
|
|
1666
1795
|
data: Object.freeze([
|
|
1667
1796
|
{
|
|
1668
1797
|
name: "scene",
|
|
1669
|
-
|
|
1670
|
-
|
|
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.",
|
|
1671
1804
|
},
|
|
1672
1805
|
{
|
|
1673
1806
|
name: "fixup",
|
|
1674
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
|
+
}),
|
|
1675
1837
|
describe: "Asset address replacements in an exported Scene.",
|
|
1676
1838
|
},
|
|
1677
|
-
// A Scene has no `img`, so the shared art key reaches nothing here:
|
|
1678
|
-
// a map's background is its own slot, and an `image` address.
|
|
1679
|
-
{
|
|
1680
|
-
name: "bgImage",
|
|
1681
|
-
...LINK,
|
|
1682
|
-
ref: "image",
|
|
1683
|
-
accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
|
|
1684
|
-
describe: "The map's background art — an `image` address.",
|
|
1685
|
-
},
|
|
1686
|
-
{
|
|
1687
|
-
name: "scale",
|
|
1688
|
-
...ANY,
|
|
1689
|
-
describe: "Regional map distance per grid unit: {distance, unit}.",
|
|
1690
|
-
},
|
|
1691
|
-
{
|
|
1692
|
-
name: "dimensions",
|
|
1693
|
-
...LIST,
|
|
1694
|
-
describe: "`[width, height]` in whole pixels — the art's own size.",
|
|
1695
|
-
},
|
|
1696
|
-
{
|
|
1697
|
-
name: "pxPerGrid",
|
|
1698
|
-
...NUM,
|
|
1699
|
-
describe: "Whole pixels per grid square; must match the art.",
|
|
1700
|
-
},
|
|
1701
|
-
{ name: "navName", ...TEXT, describe: "Short name for the navigation bar." },
|
|
1702
|
-
{ name: "levelName", ...TEXT, describe: "Name of the embedded level." },
|
|
1703
|
-
{
|
|
1704
|
-
name: "backgroundColor",
|
|
1705
|
-
...TEXT,
|
|
1706
|
-
describe: "Colour shown where the art does not reach.",
|
|
1707
|
-
},
|
|
1708
|
-
{ name: "overlay", ...TEXT, describe: "Path to the foreground art." },
|
|
1709
|
-
// Geometry carries no `kind`. The specification lists each
|
|
1710
|
-
// as a sequence while the notes authoring them today write
|
|
1711
|
-
// a map keyed by name, and a lint has no business picking
|
|
1712
|
-
// the winner of a disagreement the format has not settled.
|
|
1713
|
-
{ name: "walls", ...ANY, describe: "Wall segments." },
|
|
1714
|
-
{ name: "doors", ...ANY, describe: "Doors." },
|
|
1715
|
-
{ name: "lights", ...ANY, describe: "Light sources." },
|
|
1716
|
-
{ name: "tiles", ...ANY, describe: "Tiles." },
|
|
1717
|
-
{ name: "sounds", ...ANY, describe: "Ambient sounds." },
|
|
1718
|
-
{ name: "regions", ...ANY, describe: "Regions and their behaviours." },
|
|
1719
|
-
{
|
|
1720
|
-
name: "notes",
|
|
1721
|
-
...ANY,
|
|
1722
|
-
describe: "Map pins, each a grid location and an anchor in this note's own body.",
|
|
1723
|
-
},
|
|
1724
1839
|
{
|
|
1725
1840
|
name: "place",
|
|
1726
1841
|
...LINK,
|
|
@@ -1860,15 +1975,3 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
|
|
|
1860
1975
|
if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
|
|
1861
1976
|
return entry.subTypes;
|
|
1862
1977
|
}
|
|
1863
|
-
|
|
1864
|
-
/**
|
|
1865
|
-
* Accepted migration input with no canonical field or emitted value.
|
|
1866
|
-
*
|
|
1867
|
-
* Legacy affiliation domains are ignored. They never supply a government.
|
|
1868
|
-
* @param {string} type - The note type.
|
|
1869
|
-
* @param {string} key - An authored data key.
|
|
1870
|
-
* @returns {boolean} Whether the obsolete key is accepted during migration.
|
|
1871
|
-
*/
|
|
1872
|
-
export function isLegacyDataField(type, key) {
|
|
1873
|
-
return currentType(type) === "affiliation" && key === "domains";
|
|
1874
|
-
}
|
|
@@ -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) };
|