@heroiclands/package-build 22.13.0 → 22.14.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 +12 -0
- package/docs/api.md +23 -22
- package/docs/content-format.md +95 -123
- package/engine/folder-notes.mjs +2 -1
- package/engine/frontmatter-lint.mjs +19 -0
- package/engine/frontmatter.mjs +2 -2
- package/engine/homepage.mjs +3 -3
- package/engine/item-compiler.mjs +7 -2
- package/engine/map-places.mjs +3 -1
- package/engine/note-format.mjs +100 -0
- package/engine/note-frontmatter.mjs +33 -0
- package/engine/note-ids.mjs +1 -1
- package/engine/note-vocabulary.mjs +24 -0
- package/engine/pack-router.mjs +7 -3
- package/engine/prose-lint.mjs +3 -2
- package/engine/system-block.mjs +19 -0
- package/hm3/actors.mjs +7 -2
- package/package.json +1 -1
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/homepage.d.mts +2 -2
- package/types/engine/note-format.d.mts +8 -0
- package/types/engine/note-frontmatter.d.mts +6 -0
- package/types/engine/system-block.d.mts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
Content notes accept the documented top-level fields, with clear errors for other keys. Formatting keeps those fields in order and writes YAML collections as short lines or full blocks.
|
|
8
|
+
|
|
9
|
+
## 22.13.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
Content authors can keep pinned document identities and compendium placement alongside shared note facts.
|
|
14
|
+
|
|
3
15
|
## 22.13.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/docs/api.md
CHANGED
|
@@ -119,28 +119,29 @@ Deterministic document ids, derived by hashing rather than stored, so compile pa
|
|
|
119
119
|
|
|
120
120
|
The per-system frontmatter block: how one note feeds more than one game system through properties named after that system (`<system>.system`, `<system>.type`, `<system>.img`, `<system>.items` on actors). Resolves a field's value through the block, the shared top level, and a retiring position in that order, and merges an authored `<system>.system` onto a compiler-built one without disturbing what the builder already wrote.
|
|
121
121
|
|
|
122
|
-
| Export | Signature
|
|
123
|
-
| --------------------------- |
|
|
124
|
-
| `SYSTEM_DATA_KEY` | `const SYSTEM_DATA_KEY`
|
|
125
|
-
| `BLOCK_DOCUMENT_PROPERTIES` | `const BLOCK_DOCUMENT_PROPERTIES`
|
|
126
|
-
| `BLOCK_DIRECTIVES` | `const BLOCK_DIRECTIVES`
|
|
127
|
-
| `SYSTEM_BLOCK_KEYS` | `const SYSTEM_BLOCK_KEYS`
|
|
128
|
-
| `setPath` | `setPath(target, dotted, value)`
|
|
129
|
-
| `systemBlock` | `systemBlock(fm, block)`
|
|
130
|
-
| `carriesSystemBlock` | `carriesSystemBlock(fm, block)`
|
|
131
|
-
| `systemData` | `systemData(fm, block)`
|
|
132
|
-
| `blockField` | `blockField(fm, block, key, defaultValue)`
|
|
133
|
-
| `sharedProperty` | `sharedProperty(fm, source, defaultValue)`
|
|
134
|
-
| `blockProperty` | `blockProperty(fm, block, key, defaultValue)`
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
122
|
+
| Export | Signature | Returns | Use it when |
|
|
123
|
+
| --------------------------- | ------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `SYSTEM_DATA_KEY` | `const SYSTEM_DATA_KEY` | — | naming the key inside a system block whose contents are the system's own vocabulary |
|
|
125
|
+
| `BLOCK_DOCUMENT_PROPERTIES` | `const BLOCK_DOCUMENT_PROPERTIES` | — | mapping a system-block key to the document property it becomes |
|
|
126
|
+
| `BLOCK_DIRECTIVES` | `const BLOCK_DIRECTIVES` | — | recognizing a build directive (e.g. `pack`) that names no document property |
|
|
127
|
+
| `SYSTEM_BLOCK_KEYS` | `const SYSTEM_BLOCK_KEYS` | — | enumerating every key any system block may carry |
|
|
128
|
+
| `setPath` | `setPath(target, dotted, value)` | `object` — `target`, for chaining | writing a value at a dotted path in a document's `system` block |
|
|
129
|
+
| `systemBlock` | `systemBlock(fm, block)` | `Record<string, unknown> \| undefined` | reading one system's whole block off a note, or nothing when it is absent or malformed |
|
|
130
|
+
| `carriesSystemBlock` | `carriesSystemBlock(fm, block)` | `boolean` | deciding whether a pack that declares a system should compile a note at all |
|
|
131
|
+
| `systemData` | `systemData(fm, block)` | `Record<string, unknown>` | reading a system block's `system` sub-block, `{}` when absent |
|
|
132
|
+
| `blockField` | `blockField(fm, block, key, defaultValue)` | the value | reading a key from one system's block, falling back to the top level |
|
|
133
|
+
| `sharedProperty` | `sharedProperty(fm, source, defaultValue)` | the value | reading a shared top-level property by a possibly-dotted path, blind to every system block |
|
|
134
|
+
| `blockProperty` | `blockProperty(fm, block, key, defaultValue)` | the value | reading a property a system block may override, else the shared top-level one |
|
|
135
|
+
| `blockDataProperty` | `blockDataProperty(fm, block, key, defaultValue)` | the value | reading a system override, then shared `data`, then a top-level value |
|
|
136
|
+
| `legacyKeyOf` | `legacyKeyOf(field)` | `string \| undefined` | finding the key a field is authored at inside a system block |
|
|
137
|
+
| `retiredTopLevelKey` | `retiredTopLevelKey(field)` | `string \| undefined` | finding the bare top-level key a `data:`-sourced field is being swept off |
|
|
138
|
+
| `authoredKey` | `authoredKey(name)` | `string` | reading a shared source as the key an author writes, without the `data:` container holding it |
|
|
139
|
+
| `resolveFieldValue` | `resolveFieldValue(field, fm, ...)` | `{value, from}` | resolving one declared field against a note, in the declared source order |
|
|
140
|
+
| `systemDataPaths` | `systemDataPaths(data, prefix)` | `string[]` | listing every path a note authors under `<system>.system`, containers included |
|
|
141
|
+
| `undeclaredPaths` | `undeclaredPaths(data, declared, prefix)` | `string[]` | finding authored paths a system's published schema does not declare |
|
|
142
|
+
| `unknownBlockKeys` | `unknownBlockKeys(fm, block, ...)` | `string[]` | finding keys directly under a system block that neither this format nor the system recognizes |
|
|
143
|
+
| `claimedPaths` | `claimedPaths(fields)` | `Set<string>` | listing the exact `system` paths a field declaration writes, for `mergeSystemData` |
|
|
144
|
+
| `mergeSystemData` | `mergeSystemData(built, fm, ...)` | `object` — `built`, for chaining | merging a note's `<system>.system` verbatim onto a built `system` block |
|
|
144
145
|
|
|
145
146
|
### `engine.codeFences`
|
|
146
147
|
|
package/docs/content-format.md
CHANGED
|
@@ -287,29 +287,57 @@ infoboxes:
|
|
|
287
287
|
|
|
288
288
|
### Frontmatter has three regions
|
|
289
289
|
|
|
290
|
-
A note
|
|
291
|
-
|
|
290
|
+
A HeroicLands note has opening YAML frontmatter with nonempty `shortcode` and
|
|
291
|
+
`type` values. Other Markdown files are ordinary Markdown. A note's frontmatter
|
|
292
|
+
has three regions:
|
|
292
293
|
|
|
293
|
-
|
|
|
294
|
-
| -------------------- |
|
|
295
|
-
| **
|
|
296
|
-
| **`data:`** |
|
|
297
|
-
| **`
|
|
294
|
+
| Region | Describes | Unknown keys |
|
|
295
|
+
| -------------------- | ------------------------------------------------------- | ------------ |
|
|
296
|
+
| **Top level** | The note's identity, name, description, and publication | An error |
|
|
297
|
+
| **`data:`** | The subject itself, shared by systems | An error |
|
|
298
|
+
| **`hm3:` / `sohl:`** | The subject as one system's documents | An error |
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
document field at all, it is the page's description.
|
|
300
|
+
The complete top-level allowlist, in its required order, is `shortcode`, `name`,
|
|
301
|
+
`type`, `subType`, `description`, `tags`, `data`, `hm3`, `sohl`. Omit fields that
|
|
302
|
+
do not apply. The first fields of a typical note look like this:
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
```yaml
|
|
305
|
+
---
|
|
306
|
+
shortcode: example
|
|
307
|
+
name: { full: Example }
|
|
308
|
+
type: lore
|
|
309
|
+
subType: concept
|
|
310
|
+
description: A short page description.
|
|
311
|
+
tags: [draft]
|
|
312
|
+
data: {}
|
|
313
|
+
sohl: {}
|
|
314
|
+
---
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Any other top-level key is an error. To keep an author-only note beside the
|
|
318
|
+
content, comment out its YAML lines with `#`. Comments remain valid YAML text
|
|
319
|
+
and do not become data in the JSONL index, generated pages, or Foundry documents.
|
|
320
|
+
There is no custom site metadata. `description` supplies the page's short
|
|
321
|
+
description; `tags` carries publication and classification tags.
|
|
322
|
+
|
|
323
|
+
Arrays and objects use one-line YAML flow form when the complete line is under
|
|
324
|
+
100 characters. At 100 characters or more, they use full block form. A multiline
|
|
325
|
+
flow collection is formatted into one of those forms; folded collections are
|
|
326
|
+
not used. `content-build format --write` applies this rule and the top-level
|
|
327
|
+
order without changing the Markdown body.
|
|
328
|
+
|
|
329
|
+
**`data:` is closed.** It holds the type-specific facts about the
|
|
305
330
|
subject — a weapon's weight, an affliction's transmission, a being's species —
|
|
306
|
-
and every note type declares which keys it may carry. A misspelled key
|
|
307
|
-
finding
|
|
308
|
-
silently become a theme parameter, which is exactly why these cannot live
|
|
309
|
-
together.
|
|
331
|
+
and every note type declares which keys it may carry. A misspelled key is a
|
|
332
|
+
finding in either region.
|
|
310
333
|
|
|
311
|
-
|
|
312
|
-
|
|
334
|
+
Every note type may put a pinned document id in `data.id`, a shared compendium
|
|
335
|
+
route in `data.pack`, a shared folder Address in `data.packFolder`, and source
|
|
336
|
+
details in `data.harnworld`. A system block's `pack` or `packFolder` overrides
|
|
337
|
+
the shared value for that system. A `being` may also describe its social profile
|
|
338
|
+
in `data.social`.
|
|
339
|
+
|
|
340
|
+
**Tags are open, except the ones that classify.** A tag naming a theme or a region — `underworld`,
|
|
313
341
|
`byzaria`, `riverlands` — is the author's own and this build has no opinion about it.
|
|
314
342
|
A tag that classifies the subject is different, because something queries it. A
|
|
315
343
|
settlement tagged `village` appears in the list of villages and an untagged one
|
|
@@ -459,14 +487,11 @@ in-block spelling, and `hm3.system.gender` is the native destination.
|
|
|
459
487
|
The native destination wins, followed by the in-block value and the shared
|
|
460
488
|
source. Field declarations name both source and destination explicitly.
|
|
461
489
|
|
|
462
|
-
**A field
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
top-level key means instead, and the top level stops being read for it — leaving
|
|
468
|
-
`<system>.system.<field>` and the legacy in-block position, which describe the
|
|
469
|
-
document rather than the note. Native species is independent of the shared lore Address in `data.species`.
|
|
490
|
+
**A field without a shared source reads its system declaration.** An
|
|
491
|
+
`affiliation` item's `system.title` is the style of address an office carries;
|
|
492
|
+
the note's heading reads `name.full`. The field reads
|
|
493
|
+
`<system>.system.<field>` and the in-block position, which describe the
|
|
494
|
+
document. Native species is independent of the shared lore Address in `data.species`.
|
|
470
495
|
`subType` has the same meaning at both levels.
|
|
471
496
|
|
|
472
497
|
**Some of a system's fields are runtime state, and a note may not write any of
|
|
@@ -569,8 +594,8 @@ type.
|
|
|
569
594
|
| ----------------------- | ------------------------- | ---------------------------- |
|
|
570
595
|
| `name.full` | `name` | `name` |
|
|
571
596
|
| `data.icon` | `img` | `img` |
|
|
572
|
-
| `id`
|
|
573
|
-
| `packFolder`
|
|
597
|
+
| `data.id` | `_id` | `_id` |
|
|
598
|
+
| `data.packFolder` | `folder` | `folder` |
|
|
574
599
|
| `shortcode` | `system.shortcode` | NA |
|
|
575
600
|
| `data.templatePriority` | `system.templatePriority` | `flags.hm3.templatePriority` |
|
|
576
601
|
| `actionDefs` | `system.actionDefs` | NA |
|
|
@@ -657,16 +682,20 @@ refused at all four slots: a sound is not art, and a value naming one — `icon:
|
|
|
657
682
|
audio-thunder`, say — is an error naming the set the slot accepts, never a
|
|
658
683
|
silent fall back to the note's default art.
|
|
659
684
|
|
|
660
|
-
**
|
|
685
|
+
**These properties are legal on _every_ note**, whatever its type. `data:` is a
|
|
661
686
|
closed container and the per-type vocabularies below are the only lists there
|
|
662
687
|
are, so a key legal everywhere is stated once here rather than repeated in
|
|
663
688
|
twenty-five tables where the one that was mistyped would be the one nobody
|
|
664
689
|
noticed:
|
|
665
690
|
|
|
666
|
-
| shared `data` property | Values
|
|
667
|
-
| ---------------------- |
|
|
668
|
-
| `
|
|
669
|
-
| `
|
|
691
|
+
| shared `data` property | Values | Description |
|
|
692
|
+
| ---------------------- | ---------------------- | -------------------------------------------------------------------- |
|
|
693
|
+
| `id` | `string` | A pinned document identity. |
|
|
694
|
+
| `pack` | `string` | The shared compendium route. |
|
|
695
|
+
| `packFolder` | `Address` or pack map | The shared compendium folder Address. |
|
|
696
|
+
| `harnworld` | `Map<string, unknown>` | HârnWorld source details. |
|
|
697
|
+
| `icon` | `Address` | The document's profile art — an `icon` address, resolved into `img`. |
|
|
698
|
+
| `banner` | `Address` | The page's hero image — an `image` address. |
|
|
670
699
|
|
|
671
700
|
`icon` is legal everywhere because most types compile into a document that
|
|
672
701
|
carries one; where a type's passes emit none, the lint says so and the value is
|
|
@@ -847,10 +876,11 @@ on any type, and the frontmatter lint never reports it.
|
|
|
847
876
|
|
|
848
877
|
#### The pack a note compiles into
|
|
849
878
|
|
|
850
|
-
`pack` names which configured compendium receives the note's document.
|
|
879
|
+
`data.pack` names which configured compendium receives the note's document.
|
|
851
880
|
|
|
852
881
|
```yaml
|
|
853
|
-
|
|
882
|
+
data:
|
|
883
|
+
pack: items-hm3
|
|
854
884
|
```
|
|
855
885
|
|
|
856
886
|
It is deliberately close to the retired `package:` and deliberately not the same
|
|
@@ -867,7 +897,8 @@ system: each block the note carries sends its own document where that block
|
|
|
867
897
|
says, and a note carrying two blocks may place them in two packs.
|
|
868
898
|
|
|
869
899
|
```yaml
|
|
870
|
-
|
|
900
|
+
data:
|
|
901
|
+
pack: items-sohl
|
|
871
902
|
hm3:
|
|
872
903
|
pack: items-hm3
|
|
873
904
|
```
|
|
@@ -898,10 +929,11 @@ and in no compendium — a page of developer documentation, a page about the
|
|
|
898
929
|
package itself:
|
|
899
930
|
|
|
900
931
|
```yaml
|
|
932
|
+
shortcode: architecture
|
|
901
933
|
type: doc
|
|
902
934
|
subType: concept
|
|
903
|
-
|
|
904
|
-
pack: none
|
|
935
|
+
data:
|
|
936
|
+
pack: none
|
|
905
937
|
```
|
|
906
938
|
|
|
907
939
|
A link to such a note resolves everywhere the note is addressable. On the web
|
|
@@ -1046,16 +1078,17 @@ none. It is the same principle that turned `folder: ONXsqZAIZr2qzxTb` into
|
|
|
1046
1078
|
`packFolder: <address>` above: an opaque derived identity does not belong in
|
|
1047
1079
|
authored content.
|
|
1048
1080
|
|
|
1049
|
-
**An authored `id`
|
|
1081
|
+
**An authored `data.id` wins**, and that is how a document's identity is
|
|
1050
1082
|
**pinned**:
|
|
1051
1083
|
|
|
1052
1084
|
```yaml
|
|
1053
|
-
type: miscgear
|
|
1054
1085
|
shortcode: bowlcer
|
|
1055
|
-
|
|
1086
|
+
type: miscgear
|
|
1087
|
+
data:
|
|
1088
|
+
id: plaiQQm2T5zVK5mO # pinned: this document keeps this id
|
|
1056
1089
|
```
|
|
1057
1090
|
|
|
1058
|
-
A blank `id:` is not a pin — it is a deleted value with the key left behind, and
|
|
1091
|
+
A blank `data.id:` is not a pin — it is a deleted value with the key left behind, and
|
|
1059
1092
|
is treated as absent.
|
|
1060
1093
|
|
|
1061
1094
|
**A rename moves the id, and that is the trade.** The address carries the
|
|
@@ -1066,85 +1099,28 @@ identity across one pins its `id`, which is what the pin is for. One diagnostic
|
|
|
1066
1099
|
narrows — `content-build` tells a **rename** from a **withdrawal** by matching
|
|
1067
1100
|
document ids across releases, and for an unpinned note both sides now move
|
|
1068
1101
|
together. It never reports a _wrong_ successor, and it stays exact for a pinned
|
|
1069
|
-
note; for the rest, the
|
|
1102
|
+
note; for the rest, the diff reports the vacated address.
|
|
1070
1103
|
|
|
1071
1104
|
**A note with no address gets no id, and no document.** `type` and `shortcode`
|
|
1072
1105
|
are what a note is addressed by, so a note missing either cannot be filed and
|
|
1073
1106
|
the build refuses it by that name rather than by a missing `id:`.
|
|
1074
1107
|
|
|
1075
|
-
####
|
|
1076
|
-
|
|
1077
|
-
A note names the shortcode it used to be published under:
|
|
1078
|
-
|
|
1079
|
-
```yaml
|
|
1080
|
-
type: weapongear
|
|
1081
|
-
shortcode: Taburi
|
|
1082
|
-
renamedFrom: Tabri
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
`(type, shortcode)` is a **published interface** — every satellite that declares
|
|
1086
|
-
`itemCatalog: true` assembles its beings out of those addresses — so renaming a
|
|
1087
|
-
shortcode breaks other repositories, and `content-build addresses diff` exists to
|
|
1088
|
-
say so before a release does. To be useful it has to name where the address
|
|
1089
|
-
_went_, and since ids are derived it can no longer work that out for an unpinned
|
|
1090
|
-
note: both sides of the match move together, and the rename reads as a
|
|
1091
|
-
withdrawal.
|
|
1092
|
-
|
|
1093
|
-
**Pinning an `id` needs foresight; a declaration needs only hindsight.** A pin
|
|
1094
|
-
has to be written _before_ the rename, by an author who does not yet know they
|
|
1095
|
-
will make one. An author who has just renamed a shortcode knows exactly what the
|
|
1096
|
-
old one was, and that is the only moment anyone does — so this is the key to
|
|
1097
|
-
reach for, and `id:` stays what it is for: keeping a document's identity across
|
|
1098
|
-
the rename, which is a different question from explaining it.
|
|
1099
|
-
|
|
1100
|
-
**It takes one shortcode or a list**, because renames chain: the diff runs
|
|
1101
|
-
against a released baseline, and a shortcode may have been renamed more than once
|
|
1102
|
-
since. List every name the baseline might still know it by.
|
|
1103
|
-
|
|
1104
|
-
```yaml
|
|
1105
|
-
shortcode: Taburin
|
|
1106
|
-
renamedFrom:
|
|
1107
|
-
- Tabri
|
|
1108
|
-
- Taburi
|
|
1109
|
-
```
|
|
1108
|
+
#### Address changes
|
|
1110
1109
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
`sohl:` and `hm3:` blocks compiles two documents that share one shortcode, and
|
|
1118
|
-
one declaration covers both.
|
|
1119
|
-
|
|
1120
|
-
**What a declaration changes is what the diagnostic can say, not what it says
|
|
1121
|
-
about you.** A finding reports which of the two joins it had, because they are
|
|
1122
|
-
not equally checkable — a matched id is a fact a reader can verify in both
|
|
1123
|
-
artefacts, while a declaration is the author's word:
|
|
1124
|
-
|
|
1125
|
-
```text
|
|
1126
|
-
since sohl@0.8.2, weapongear:Tabri is no longer published; the note now
|
|
1127
|
-
published as weapongear:Taburi declares it was renamed from Tabri. Every
|
|
1128
|
-
package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
|
|
1129
|
-
```
|
|
1130
|
-
|
|
1131
|
-
A rename that is neither pinned nor declared is still reported as a
|
|
1132
|
-
**withdrawal**. Nothing infers a successor from a similar-looking string: a wrong
|
|
1133
|
-
one sends the reader to the wrong fix, which is worse than saying nothing.
|
|
1134
|
-
|
|
1135
|
-
`content-lint` holds a declaration to the same rules a current address is held
|
|
1136
|
-
to. An entry must be a well-formed shortcode, must not be the note's own, and
|
|
1137
|
-
must name an address the package actually **vacated** — an entry naming an
|
|
1138
|
-
address some note still publishes is refused, as are two notes claiming one
|
|
1139
|
-
predecessor, since an address had one holder and so has one successor. A repeat
|
|
1140
|
-
of the same entry is a warning; the declaration still works.
|
|
1110
|
+
`(type, shortcode)` is a published interface. Other packages can link to it or
|
|
1111
|
+
use it as an Item model, so changing either part requires those references to
|
|
1112
|
+
change too. `content-build addresses diff` compares the package with a released
|
|
1113
|
+
baseline and reports addresses whose published documents are absent. A pinned
|
|
1114
|
+
`data.id` preserves document identity across a shortcode change; it does not
|
|
1115
|
+
keep the old address available for links.
|
|
1141
1116
|
|
|
1142
1117
|
#### The compendium folder
|
|
1143
1118
|
|
|
1144
1119
|
A note says which folder of its pack it lands in:
|
|
1145
1120
|
|
|
1146
1121
|
```yaml
|
|
1147
|
-
|
|
1122
|
+
data:
|
|
1123
|
+
packFolder: poisonsandtoxins # a folder note's address
|
|
1148
1124
|
```
|
|
1149
1125
|
|
|
1150
1126
|
**`packFolder` is a folder note's address** — an ordinary address, resolved the
|
|
@@ -2481,6 +2457,7 @@ Generates a living (or undead, or spirit) being.
|
|
|
2481
2457
|
| `templatePriority` | `number` | Template priority, _null_ = not a template |
|
|
2482
2458
|
| `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — `[]` where none apply; `null` is an error. |
|
|
2483
2459
|
| `occupation` | `string` | Name of the character's occupation |
|
|
2460
|
+
| `social` | `Map<string, unknown>` | The being's social profile |
|
|
2484
2461
|
| `stations` | `Address[]` | Name of the stations the character belongs to |
|
|
2485
2462
|
| `lore` | `Address[]` | Lore concerning this being — the people it is of, the standing it holds, the law it lives under |
|
|
2486
2463
|
| `homes` | `Address[]` | Place the being calls home |
|
|
@@ -2743,9 +2720,9 @@ no Foundry document: it is a page and nothing else.
|
|
|
2743
2720
|
It declares a `shortcode` — conventionally `root` — because that is what a
|
|
2744
2721
|
link is written with: `[[homepage-root|Text]]` is an ordinary wikilink, and it
|
|
2745
2722
|
resolves to `/<package>/`. The shortcode names the page in links; its address
|
|
2746
|
-
is the package root. `
|
|
2747
|
-
`
|
|
2748
|
-
|
|
2723
|
+
is the package root. The page heading reads `name.full` and defaults to
|
|
2724
|
+
`packageBuild.manifest.title`. `description` supplies the page's short
|
|
2725
|
+
description.
|
|
2749
2726
|
|
|
2750
2727
|
The page is its body, published verbatim: no wikilink is resolved on it and
|
|
2751
2728
|
no table expanded, so its links are markdown links, package-relative
|
|
@@ -2753,9 +2730,8 @@ no table expanded, so its links are markdown links, package-relative
|
|
|
2753
2730
|
written here and is not generated anywhere — it is a `doc` note carrying a
|
|
2754
2731
|
content table, linked from this page like any other.
|
|
2755
2732
|
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
a card block — the homepage is a page with a body, rendered as one.
|
|
2733
|
+
The homepage uses the same top-level allowlist as every addressed note. It has
|
|
2734
|
+
no Foundry document id; a pinned `data.id` has no role on this type.
|
|
2759
2735
|
|
|
2760
2736
|
### type: vehicle
|
|
2761
2737
|
|
|
@@ -2981,14 +2957,10 @@ mapping for a field no schema declares is the drift these tables exist to catch.
|
|
|
2981
2957
|
likewise absent here: they are filled on an embedded membership, never from a
|
|
2982
2958
|
catalogue note's `data:`.
|
|
2983
2959
|
|
|
2984
|
-
**`system.title` is
|
|
2985
|
-
|
|
2986
|
-
the
|
|
2987
|
-
|
|
2988
|
-
virtue of its rank. So the top-level key is **not** a shared source for this
|
|
2989
|
-
field, and a note that writes one is stating its own heading and nothing else
|
|
2990
|
-
. Author the style of address on the membership — the `system.title` of the
|
|
2991
|
-
entry in a being's `sohl.items` — or, on a catalogue note that genuinely carries
|
|
2960
|
+
**`system.title` is the style of address the office carries**, Ajaw or Warden,
|
|
2961
|
+
which a being holds by virtue of its rank. The note's page heading reads
|
|
2962
|
+
`name.full`. Author the style of address on the membership — the `system.title`
|
|
2963
|
+
of the entry in a being's `sohl.items` — or, on a catalogue note that carries
|
|
2992
2964
|
one, at `sohl.system.title`.
|
|
2993
2965
|
|
|
2994
2966
|
### type: affliction
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -274,7 +274,8 @@ export function collectFolderNotes(notes, pkg) {
|
|
|
274
274
|
|
|
275
275
|
const { parent, color } = folderFields(fm);
|
|
276
276
|
const address = folderAddress(pkg, shortcode);
|
|
277
|
-
const
|
|
277
|
+
const pinnedId = fm.data?.id ?? fm.id;
|
|
278
|
+
const authoredId = pinnedId == null ? "" : String(pinnedId).trim();
|
|
278
279
|
|
|
279
280
|
folders.push({
|
|
280
281
|
shortcode,
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { isAddressTuple } from "./address.mjs";
|
|
58
|
+
import { authoredNoteKeys, NOTE_TOP_LEVEL_KEY_SET } from "./note-frontmatter.mjs";
|
|
58
59
|
import { AddressEntries } from "./address-values.mjs";
|
|
59
60
|
import { authoredFields, readsLegacyKey } from "./field-spec.mjs";
|
|
60
61
|
import {
|
|
@@ -1443,6 +1444,24 @@ export function lintNote(
|
|
|
1443
1444
|
});
|
|
1444
1445
|
}
|
|
1445
1446
|
|
|
1447
|
+
for (const key of authoredNoteKeys(raw())) {
|
|
1448
|
+
if (NOTE_TOP_LEVEL_KEY_SET.has(key)) continue;
|
|
1449
|
+
const position = positionInFrontmatter(raw(), key, undefined, { topLevel: true });
|
|
1450
|
+
if (
|
|
1451
|
+
findings.some(
|
|
1452
|
+
(finding) => finding.severity === "error" && finding.line === position.line,
|
|
1453
|
+
)
|
|
1454
|
+
) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
findings.push({
|
|
1458
|
+
file: note.file,
|
|
1459
|
+
...position,
|
|
1460
|
+
severity: "error",
|
|
1461
|
+
message: `unknown top-level frontmatter key ${JSON.stringify(key)}; use shortcode, name, type, subType, description, tags, data, hm3, or sohl`,
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1446
1465
|
// The type's charset, before anything that looks the type up. A
|
|
1447
1466
|
// hyphenated type is unaddressable, and every lookup below would report it
|
|
1448
1467
|
// as a type nobody declared — true, but not the reason, and it would send
|
package/engine/frontmatter.mjs
CHANGED
|
@@ -348,7 +348,7 @@ export function parseValueDesc(raw) {
|
|
|
348
348
|
/**
|
|
349
349
|
* The compendium folder a note names.
|
|
350
350
|
*
|
|
351
|
-
*
|
|
351
|
+
* `data.packFolder` is a folder note's **address**
|
|
352
352
|
* (`folder-poisonsandtoxins`), resolved through the address index the whole
|
|
353
353
|
* build shares. The `folder:` Foundry-id spelling this function once read
|
|
354
354
|
* beside it, and the per-pack `*-folders.yaml` that id was resolved against,
|
|
@@ -372,7 +372,7 @@ export function parseValueDesc(raw) {
|
|
|
372
372
|
* two spellings, and there is only one left to be.
|
|
373
373
|
*/
|
|
374
374
|
export function folderField(fm) {
|
|
375
|
-
const asAddress =
|
|
375
|
+
const asAddress = fm?.sohl?.packFolder ?? fm?.data?.packFolder ?? fm?.packFolder ?? null;
|
|
376
376
|
if (asAddress != null && asAddress !== "") return { value: asAddress, isAddress: true };
|
|
377
377
|
return { value: null, isAddress: true };
|
|
378
378
|
}
|
package/engine/homepage.mjs
CHANGED
|
@@ -340,8 +340,8 @@ function positionOfType(file) {
|
|
|
340
340
|
* The one defaulted value on the page, and it defaults to the package's own
|
|
341
341
|
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
342
342
|
* package — so a homepage that adds nothing to it need not restate it. An
|
|
343
|
-
* authored `
|
|
344
|
-
* differently from a package browser.
|
|
343
|
+
* authored `name.full` wins, because a front page may greet a reader
|
|
344
|
+
* differently from a package browser. A top-level `title` is also readable.
|
|
345
345
|
*
|
|
346
346
|
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
347
347
|
* own still yields a titled page rather than a blank heading.
|
|
@@ -351,7 +351,7 @@ function positionOfType(file) {
|
|
|
351
351
|
* @returns {string} The title.
|
|
352
352
|
*/
|
|
353
353
|
export function homepageTitle(fm, config) {
|
|
354
|
-
const authored = fm?.title;
|
|
354
|
+
const authored = fm?.name?.full ?? fm?.title;
|
|
355
355
|
if (typeof authored === "string" && authored.trim()) return authored;
|
|
356
356
|
const manifest = /** @type {Record<string, unknown>|undefined} */ (
|
|
357
357
|
config?.packageBuild?.manifest
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -64,7 +64,12 @@ import { documentSubtype, subtypeRow } from "./document-subtypes.mjs";
|
|
|
64
64
|
// The note-level `<system>:` block: `<system>.system` onto the document's
|
|
65
65
|
// `system` verbatim, and `<system>.img` / `.effects` / `.flags` overriding
|
|
66
66
|
// their shared top-level forms for this system alone.
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
blockDataProperty,
|
|
69
|
+
blockProperty,
|
|
70
|
+
claimedPaths,
|
|
71
|
+
mergeSystemData,
|
|
72
|
+
} from "./system-block.mjs";
|
|
68
73
|
// The other direction of the same declaration: a field the *document* writes in
|
|
69
74
|
// play, which a note may not author and the builder does not emit.
|
|
70
75
|
import { assertNoDerivedFields } from "./derived-fields.mjs";
|
|
@@ -379,7 +384,7 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
379
384
|
// one spelling: `packFolder` names a folder note by its address, the
|
|
380
385
|
// `folder:` id spelling having been retired with the per-pack YAML it
|
|
381
386
|
// resolved against.
|
|
382
|
-
const folder = this.folderResolver(
|
|
387
|
+
const folder = this.folderResolver(blockDataProperty(fm, system, "packFolder", null), {
|
|
383
388
|
isAddress: true,
|
|
384
389
|
});
|
|
385
390
|
|
package/engine/map-places.mjs
CHANGED
|
@@ -207,7 +207,9 @@ export function placesFromRecords(records, { contentBase, base }) {
|
|
|
207
207
|
folder: record.file?.folder ?? "",
|
|
208
208
|
package: String(record.package ?? ""),
|
|
209
209
|
local: true,
|
|
210
|
-
hasPackFolder:
|
|
210
|
+
hasPackFolder:
|
|
211
|
+
Object.hasOwn(record.data ?? {}, "packFolder") ||
|
|
212
|
+
Object.hasOwn(record, "packFolder"),
|
|
211
213
|
});
|
|
212
214
|
} else if (record.type === "affiliation" && record.subType === "polity") {
|
|
213
215
|
polities.push({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import YAML from "yaml";
|
|
15
|
+
import { NOTE_TOP_LEVEL_KEYS } from "./note-frontmatter.mjs";
|
|
16
|
+
|
|
17
|
+
const MAX_FLOW_LINE = 99;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format one addressed note's YAML without changing its Markdown body.
|
|
21
|
+
* Invalid YAML remains the YAML linter's finding.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} markdown - The complete Markdown source.
|
|
24
|
+
* @returns {string} The source with canonical frontmatter.
|
|
25
|
+
*/
|
|
26
|
+
export function formatNoteFrontmatter(markdown) {
|
|
27
|
+
const match = markdown.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)([\s\S]*)$/);
|
|
28
|
+
if (!match) return markdown;
|
|
29
|
+
const [, opening, source, closing, body] = match;
|
|
30
|
+
const document = YAML.parseDocument(source, { keepSourceTokens: true });
|
|
31
|
+
if (document.errors.length || !YAML.isMap(document.contents)) return markdown;
|
|
32
|
+
const root = document.contents;
|
|
33
|
+
const values = Object.fromEntries(
|
|
34
|
+
root.items.map((pair) => [String(pair.key?.value), pair.value]),
|
|
35
|
+
);
|
|
36
|
+
if (!nonempty(values.shortcode) || !nonempty(values.type)) return markdown;
|
|
37
|
+
|
|
38
|
+
const order = new Map(NOTE_TOP_LEVEL_KEYS.map((key, index) => [key, index]));
|
|
39
|
+
const rank = (key) => order.get(key) ?? NOTE_TOP_LEVEL_KEYS.length;
|
|
40
|
+
root.items.sort((left, right) => {
|
|
41
|
+
const a = String(left.key?.value);
|
|
42
|
+
const b = String(right.key?.value);
|
|
43
|
+
const difference = rank(a) - rank(b);
|
|
44
|
+
return difference;
|
|
45
|
+
});
|
|
46
|
+
for (const pair of root.items) {
|
|
47
|
+
const key = String(pair.key?.value);
|
|
48
|
+
formatCollection(pair.value, key.length + 2, 0);
|
|
49
|
+
}
|
|
50
|
+
const formatted = document.toString({ lineWidth: 0, flowCollectionPadding: false }).trimEnd();
|
|
51
|
+
return `${opening}${formatted}${closing}${body}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nonempty(node) {
|
|
55
|
+
return YAML.isScalar(node) && typeof node.value === "string" && node.value.trim() !== "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Format nested collections before testing the enclosing line. */
|
|
59
|
+
function formatCollection(node, prefix, depth) {
|
|
60
|
+
if (!YAML.isMap(node) && !YAML.isSeq(node)) return;
|
|
61
|
+
if (YAML.isMap(node)) {
|
|
62
|
+
for (const pair of node.items) {
|
|
63
|
+
formatCollection(
|
|
64
|
+
pair.value,
|
|
65
|
+
2 * (depth + 1) + String(pair.key?.value).length + 2,
|
|
66
|
+
depth + 1,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
for (const item of node.items) formatCollection(item, 2 * (depth + 1) + 2, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
if (hasBlockScalar(node) || hasAlias(node)) {
|
|
73
|
+
node.flow = false;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
node.flow = true;
|
|
77
|
+
const candidate = new YAML.Document(node)
|
|
78
|
+
.toString({
|
|
79
|
+
lineWidth: 0,
|
|
80
|
+
flowCollectionPadding: false,
|
|
81
|
+
})
|
|
82
|
+
.trimEnd();
|
|
83
|
+
if (candidate.includes("\n") || Array.from(candidate).length + prefix > MAX_FLOW_LINE) {
|
|
84
|
+
node.flow = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasBlockScalar(node) {
|
|
89
|
+
if (YAML.isScalar(node)) return node.type === "BLOCK_LITERAL" || node.type === "BLOCK_FOLDED";
|
|
90
|
+
if (YAML.isMap(node)) return node.items.some((pair) => hasBlockScalar(pair.value));
|
|
91
|
+
if (YAML.isSeq(node)) return node.items.some(hasBlockScalar);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasAlias(node) {
|
|
96
|
+
if (YAML.isAlias(node)) return true;
|
|
97
|
+
if (YAML.isMap(node)) return node.items.some((pair) => hasAlias(pair.value));
|
|
98
|
+
if (YAML.isSeq(node)) return node.items.some(hasAlias);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
|
|
10
|
+
/** The complete ordered top-level vocabulary for addressed content notes. */
|
|
11
|
+
export const NOTE_TOP_LEVEL_KEYS = Object.freeze([
|
|
12
|
+
"shortcode",
|
|
13
|
+
"name",
|
|
14
|
+
"type",
|
|
15
|
+
"subType",
|
|
16
|
+
"description",
|
|
17
|
+
"tags",
|
|
18
|
+
"data",
|
|
19
|
+
"hm3",
|
|
20
|
+
"sohl",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** Membership test for the top-level vocabulary. */
|
|
24
|
+
export const NOTE_TOP_LEVEL_KEY_SET = new Set(NOTE_TOP_LEVEL_KEYS);
|
|
25
|
+
|
|
26
|
+
/** Return authored keys from an opening YAML frontmatter map. */
|
|
27
|
+
export function authoredNoteKeys(markdown) {
|
|
28
|
+
const source = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
29
|
+
if (source === undefined) return [];
|
|
30
|
+
const document = YAML.parseDocument(source);
|
|
31
|
+
if (document.errors.length || !YAML.isMap(document.contents)) return [];
|
|
32
|
+
return document.contents.items.map((pair) => String(pair.key?.value));
|
|
33
|
+
}
|
package/engine/note-ids.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function text(value) {
|
|
|
105
105
|
*/
|
|
106
106
|
export function noteDocId(fm, { pkg, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS } = {}) {
|
|
107
107
|
if (!fm || typeof fm !== "object") return undefined;
|
|
108
|
-
const pinned = text(fm.id);
|
|
108
|
+
const pinned = text(fm.data?.id) ?? text(fm.id);
|
|
109
109
|
if (pinned) return pinned;
|
|
110
110
|
const type = text(fm.type);
|
|
111
111
|
const shortcode = text(fm.shortcode);
|
|
@@ -277,6 +277,29 @@ const TOKEN_ICON = Object.freeze({
|
|
|
277
277
|
* @type {readonly DataFieldSpec[]}
|
|
278
278
|
*/
|
|
279
279
|
export const SHARED_DATA_FIELDS = Object.freeze([
|
|
280
|
+
Object.freeze({
|
|
281
|
+
name: "id",
|
|
282
|
+
...TEXT,
|
|
283
|
+
describe: "A fixed document identity when the derived identity is unsuitable.",
|
|
284
|
+
}),
|
|
285
|
+
Object.freeze({
|
|
286
|
+
name: "pack",
|
|
287
|
+
...TEXT,
|
|
288
|
+
describe: "The shared compendium route, with a system block overriding it.",
|
|
289
|
+
}),
|
|
290
|
+
Object.freeze({
|
|
291
|
+
name: "packFolder",
|
|
292
|
+
...LINK_BY_PACK,
|
|
293
|
+
ref: "folder",
|
|
294
|
+
accepts: ["folder"],
|
|
295
|
+
describe: "The shared compendium folder Address, with a system block overriding it.",
|
|
296
|
+
}),
|
|
297
|
+
Object.freeze({
|
|
298
|
+
name: "harnworld",
|
|
299
|
+
shape: "map",
|
|
300
|
+
kind: "map",
|
|
301
|
+
describe: "HârnWorld source details shared by every system.",
|
|
302
|
+
}),
|
|
280
303
|
Object.freeze({
|
|
281
304
|
name: "icon",
|
|
282
305
|
...LINK,
|
|
@@ -605,6 +628,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
605
628
|
// package's declared present compute — see `engine/being-age.mjs`.
|
|
606
629
|
check: checkBeingAge,
|
|
607
630
|
data: Object.freeze([
|
|
631
|
+
{ name: "social", shape: "map", kind: "map", describe: "The being's social profile." },
|
|
608
632
|
TOKEN_ICON,
|
|
609
633
|
TEMPLATE_PRIORITY,
|
|
610
634
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
package/engine/pack-router.mjs
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
|
|
83
83
|
import { NO_PACK, packForType } from "./ids.mjs";
|
|
84
84
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
85
|
-
import {
|
|
85
|
+
import { blockDataProperty, systemBlock } from "./system-block.mjs";
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* A note that cannot be routed to a pack. Thrown rather than returned so no
|
|
@@ -131,7 +131,9 @@ export { NO_PACK };
|
|
|
131
131
|
*/
|
|
132
132
|
export function declaresNoPack(fm, system) {
|
|
133
133
|
const declared =
|
|
134
|
-
system === undefined ?
|
|
134
|
+
system === undefined ?
|
|
135
|
+
(fm?.data?.[PACK_FIELD] ?? fm?.[PACK_FIELD])
|
|
136
|
+
: blockDataProperty(fm, system, PACK_FIELD);
|
|
135
137
|
return declared === NO_PACK;
|
|
136
138
|
}
|
|
137
139
|
|
|
@@ -270,7 +272,9 @@ export function createPackRouter(packs) {
|
|
|
270
272
|
const inBlock = system === undefined ? undefined : systemBlock(fm, system)?.[PACK_FIELD];
|
|
271
273
|
const authoredInBlock = inBlock != null && inBlock !== "";
|
|
272
274
|
let declared =
|
|
273
|
-
system === undefined ?
|
|
275
|
+
system === undefined ?
|
|
276
|
+
(fm?.data?.[PACK_FIELD] ?? fm?.[PACK_FIELD])
|
|
277
|
+
: blockDataProperty(fm, system, PACK_FIELD);
|
|
274
278
|
// The declaration names where the note's *own* document goes. A pass
|
|
275
279
|
// writing a document derived from it — an item's prose becoming a
|
|
276
280
|
// JournalEntry — is not what the author was addressing.
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
|
+
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
36
37
|
|
|
37
38
|
import {
|
|
38
39
|
MARKDOWNLINT_CONFIG,
|
|
@@ -202,7 +203,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
202
203
|
let formatted = source;
|
|
203
204
|
let converged = false;
|
|
204
205
|
for (let pass = 0; pass < MAX_FORMAT_PASSES; pass += 1) {
|
|
205
|
-
const next = await prettier.format(formatted, options);
|
|
206
|
+
const next = formatNoteFrontmatter(await prettier.format(formatted, options));
|
|
206
207
|
if (next === formatted) {
|
|
207
208
|
converged = true;
|
|
208
209
|
break;
|
|
@@ -236,7 +237,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
236
237
|
continue;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
if (
|
|
240
|
+
if (formatNoteFrontmatter(await prettier.format(source, options)) !== source) {
|
|
240
241
|
findings.push({
|
|
241
242
|
file,
|
|
242
243
|
severity: "error",
|
package/engine/system-block.mjs
CHANGED
|
@@ -361,6 +361,25 @@ export function blockProperty(fm, block, key, defaultValue = undefined) {
|
|
|
361
361
|
return shared === undefined || shared === null ? defaultValue : shared;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* A system override followed by a shared `data:` value. The top-level value
|
|
366
|
+
* remains readable while content repositories move their authored fields.
|
|
367
|
+
*
|
|
368
|
+
* @param {object} fm - The note's frontmatter.
|
|
369
|
+
* @param {string} block - The system block key.
|
|
370
|
+
* @param {string} key - The shared property.
|
|
371
|
+
* @param {any} [defaultValue] - Returned when no position declares it.
|
|
372
|
+
* @returns {any} The selected value.
|
|
373
|
+
*/
|
|
374
|
+
export function blockDataProperty(fm, block, key, defaultValue = undefined) {
|
|
375
|
+
const declared = systemBlock(fm, block)?.[key];
|
|
376
|
+
if (declared !== undefined && declared !== null) return declared;
|
|
377
|
+
const shared = isMapping(fm?.data) ? fm.data[key] : undefined;
|
|
378
|
+
if (shared !== undefined && shared !== null) return shared;
|
|
379
|
+
const legacy = isMapping(fm) ? fm[key] : undefined;
|
|
380
|
+
return legacy === undefined || legacy === null ? defaultValue : legacy;
|
|
381
|
+
}
|
|
382
|
+
|
|
364
383
|
/**
|
|
365
384
|
* The key a field is authored at **inside** a system block — step 2.
|
|
366
385
|
*
|
package/hm3/actors.mjs
CHANGED
|
@@ -63,7 +63,12 @@ import {
|
|
|
63
63
|
// The note-level `hm3:` block: `hm3.system` onto the document's `system`
|
|
64
64
|
// verbatim, and `hm3.img` / `hm3.items` / `hm3.effects` / `hm3.flags`
|
|
65
65
|
// overriding their shared top-level forms for this system alone.
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
blockDataProperty,
|
|
68
|
+
blockField,
|
|
69
|
+
blockProperty,
|
|
70
|
+
mergeSystemData,
|
|
71
|
+
} from "../engine/system-block.mjs";
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
74
|
* Native HM3 facts shared by its actor subtypes.
|
|
@@ -324,7 +329,7 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
324
329
|
// by its address. This pass once read only the Foundry id, so an HM3
|
|
325
330
|
// tree could not file an actor by address at all — which its own sweep
|
|
326
331
|
// needs; the id spelling is retired outright.
|
|
327
|
-
const folder = this.folderResolver(
|
|
332
|
+
const folder = this.folderResolver(blockDataProperty(fm, block, "packFolder", null), {
|
|
328
333
|
isAddress: true,
|
|
329
334
|
});
|
|
330
335
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.14.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -149,7 +149,7 @@ export function parseValueDesc(raw: any): {
|
|
|
149
149
|
/**
|
|
150
150
|
* The compendium folder a note names.
|
|
151
151
|
*
|
|
152
|
-
*
|
|
152
|
+
* `data.packFolder` is a folder note's **address**
|
|
153
153
|
* (`folder-poisonsandtoxins`), resolved through the address index the whole
|
|
154
154
|
* build shares. The `folder:` Foundry-id spelling this function once read
|
|
155
155
|
* beside it, and the per-pack `*-folders.yaml` that id was resolved against,
|
|
@@ -119,8 +119,8 @@ export function checkHomepageCount(found: ReadonlyArray<{
|
|
|
119
119
|
* The one defaulted value on the page, and it defaults to the package's own
|
|
120
120
|
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
121
121
|
* package — so a homepage that adds nothing to it need not restate it. An
|
|
122
|
-
* authored `
|
|
123
|
-
* differently from a package browser.
|
|
122
|
+
* authored `name.full` wins, because a front page may greet a reader
|
|
123
|
+
* differently from a package browser. A top-level `title` is also readable.
|
|
124
124
|
*
|
|
125
125
|
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
126
126
|
* own still yields a titled page rather than a blank heading.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format one addressed note's YAML without changing its Markdown body.
|
|
3
|
+
* Invalid YAML remains the YAML linter's finding.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} markdown - The complete Markdown source.
|
|
6
|
+
* @returns {string} The source with canonical frontmatter.
|
|
7
|
+
*/
|
|
8
|
+
export function formatNoteFrontmatter(markdown: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Return authored keys from an opening YAML frontmatter map. */
|
|
2
|
+
export function authoredNoteKeys(markdown: any): string[];
|
|
3
|
+
/** The complete ordered top-level vocabulary for addressed content notes. */
|
|
4
|
+
export const NOTE_TOP_LEVEL_KEYS: readonly string[];
|
|
5
|
+
/** Membership test for the top-level vocabulary. */
|
|
6
|
+
export const NOTE_TOP_LEVEL_KEY_SET: Set<string>;
|
|
@@ -93,6 +93,17 @@ export function sharedProperty(fm: object, source: string, defaultValue?: any):
|
|
|
93
93
|
* @returns {any} The value.
|
|
94
94
|
*/
|
|
95
95
|
export function blockProperty(fm: object, block: string, key: string, defaultValue?: any): any;
|
|
96
|
+
/**
|
|
97
|
+
* A system override followed by a shared `data:` value. The top-level value
|
|
98
|
+
* remains readable while content repositories move their authored fields.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} fm - The note's frontmatter.
|
|
101
|
+
* @param {string} block - The system block key.
|
|
102
|
+
* @param {string} key - The shared property.
|
|
103
|
+
* @param {any} [defaultValue] - Returned when no position declares it.
|
|
104
|
+
* @returns {any} The selected value.
|
|
105
|
+
*/
|
|
106
|
+
export function blockDataProperty(fm: object, block: string, key: string, defaultValue?: any): any;
|
|
96
107
|
/**
|
|
97
108
|
* The key a field is authored at **inside** a system block — step 2.
|
|
98
109
|
*
|