@heroiclands/package-build 23.3.2 → 23.4.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 +8 -0
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/commands.md +5 -2
- package/docs/content-format.md +10 -10
- package/docs/guides/release-and-deploy.md +1 -1
- package/docs/reference/format-details.md +39 -8
- package/docs/reference/note-types.md +27 -27
- package/docs/reference/theme-pages.md +5 -4
- package/engine/appearance-features.mjs +418 -0
- package/engine/data-keys.mjs +7 -2
- package/engine/docs-checks.mjs +41 -8
- package/engine/infobox.mjs +5 -5
- package/engine/note-vocabulary.mjs +29 -21
- package/engine/npc-generate.mjs +5 -4
- package/package.json +3 -2
- package/types/engine/appearance-features.d.mts +66 -0
- package/types/engine/data-keys.d.mts +2 -1
- package/types/engine/docs-checks.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `package-build lint` requires a being's `complexion` to be a list of complexion words, `[]` where no condition is established, and refuses a single word.
|
|
8
|
+
- `extra_features` admits only short plain descriptions of visible physical marks; colours, skin conditions, gear, voice, manner, looks, handedness, sterility, apparent age and punctuation are errors.
|
|
9
|
+
- `package-build npc new` writes the complexion it draws as a list.
|
|
10
|
+
|
|
3
11
|
## 23.3.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -26,7 +26,7 @@ A sheltered place where ships load and unload.
|
|
|
26
26
|
|
|
27
27
|
`shortcode` and `type` identify a HeroicLands note; `name.full` gives readers its name. The YAML ends at the second `---`. Everything after it is the shared Markdown body. Run `npm run lint` to check the note, then `npm run build:site` to build its web page. `npm run serve:site` serves the site locally; `npm run build:book` builds the book. A Foundry package's `npm run build:db` also compiles its configured packs. The generated project supplies these scripts and their configuration.
|
|
28
28
|
|
|
29
|
-
For optional writing suggestions, run `npx package-build prose lint assets/content/Places/Harbor.md`. This reports difficult sentences and simpler word choices with line and column numbers. It does not change the note or run as part of `npm run lint`. The [prose analysis command](../commands.md#package-build-prose-lint-path) explains the settings and how to read each suggestion.
|
|
29
|
+
For optional writing suggestions, run `npx package-build prose lint assets/content/Places/Harbor.md`. This reports difficult sentences and simpler word choices with line and column numbers. It does not change the note or run as part of `npm run lint`. The [prose analysis command](../commands.md#package-build-prose-lint-path-and-prose-score-path) explains the settings and how to read each suggestion.
|
|
30
30
|
|
|
31
31
|
## What the note becomes
|
|
32
32
|
|
|
@@ -35,7 +35,7 @@ The [note-type reference](../reference/note-types.md) lists every field accepted
|
|
|
35
35
|
|
|
36
36
|
A being's `data.archetypes` names the roles it can fill in an adventure. Choose every specific role that fits, such as `entertainer` for a performer or `guildsperson` for someone whose professional training or connections matter. Use `[commoner]` when no specific archetype fits. `commoner` cannot share the list with another value. The [archetype reference](../reference/format-details.md#actors) defines the available roles.
|
|
37
37
|
|
|
38
|
-
A being's look is stated in a closed vocabulary. `data.gender` and `data.frame` take one word each, as do `data.appearance.eye_color`, `hair_color` and `skin_color`; `data.appearance.complexion` takes
|
|
38
|
+
A being's look is stated in a closed vocabulary. `data.gender` and `data.frame` take one word each, as do `data.appearance.eye_color`, `hair_color` and `skin_color`; `data.appearance.complexion` takes a list of words, `[]` where no condition is established, and is required once a being states an appearance. A value outside a field's list — a variant spelling such as `gray`, a word from another field's list, or an empty string — is an error naming the words the field takes. Leave a field out when the fact is unrecorded. `data.appearance.extra_features` lists the visible physical marks no word covers, each a short plain description of one, such as `a burn scar on the left forearm`. The [look reference](../reference/format-details.md#a-beings-look) gives every word and what it means.
|
|
39
39
|
|
|
40
40
|
A `sohl:`, `hm3:` or `dnd5e:` block holds values for that Foundry system. Its `system:` child contains values sent to the system's DataModel. The system block can override shared routing and art where the declared field permits it. `data.pack` and `data.packFolder` are shared routes; a system-specific route belongs in that system block. The [detailed mapping reference](../reference/format-details.md#mappings-every-type-shares) and [system-block explanation](../reference/format-details.md#frontmatter-has-three-regions) cover precedence and emitted fields.
|
|
41
41
|
|
package/docs/commands.md
CHANGED
|
@@ -1898,7 +1898,10 @@ checks frontmatter against the declared vocabulary and each system's `sohl:`
|
|
|
1898
1898
|
/ `hm3:` block, the package's charset (so a book can pick a font that covers
|
|
1899
1899
|
it), that an icon a note writes is one the registry declares, and that
|
|
1900
1900
|
markup inside a note's body does not smuggle in a character the charset
|
|
1901
|
-
check cannot see.
|
|
1901
|
+
check cannot see. A being's appearance is held to
|
|
1902
|
+
[a being's look](reference/format-details.md#a-beings-look): `complexion` a
|
|
1903
|
+
list, and `extra_features` only short plain descriptions of physical marks.
|
|
1904
|
+
`--no-references` turns off the check that a frontmatter
|
|
1902
1905
|
shortcode reference lands, including references in schema-declared system
|
|
1903
1906
|
fields, for a tree whose cross-package references it cannot see. Address
|
|
1904
1907
|
references in system fields resolve against this package and its declared
|
|
@@ -2923,7 +2926,7 @@ data:
|
|
|
2923
2926
|
eye_color: brown
|
|
2924
2927
|
hair_color: black
|
|
2925
2928
|
skin_color: brown
|
|
2926
|
-
complexion: clear
|
|
2929
|
+
complexion: [clear]
|
|
2927
2930
|
extra_features: []
|
|
2928
2931
|
---
|
|
2929
2932
|
|
package/docs/content-format.md
CHANGED
|
@@ -13,15 +13,15 @@ Every typed note requires a nonempty body, including drafts, folders and homepag
|
|
|
13
13
|
|
|
14
14
|
Start with [your first note](authoring/first-note.md). It explains the smallest valid note and shows the JournalEntry, web page, book entry, and content-index record made from it. Then use the chapters below in the order your note needs them.
|
|
15
15
|
|
|
16
|
-
| Need | Read
|
|
17
|
-
| --------------------------------------------------------- |
|
|
18
|
-
| Frontmatter, `data`, system blocks, and infoboxes | [Frontmatter](authoring/frontmatter.md)
|
|
19
|
-
| Addresses, links, anchors, fences, and inline expressions | [Links and markup](authoring/links-and-markup.md)
|
|
20
|
-
| Images, icons, audio, provenance, and sidecars | [Assets](authoring/assets.md)
|
|
21
|
-
| Canonical dates, calendars, eras, and conversions | [Dates and calendars](authoring/dates-and-calendars.md)
|
|
22
|
-
| All accepted note types, subtypes, and fields | [Note type reference](reference/note-types.md)
|
|
23
|
-
| Exact format details and system mappings | [Detailed reference](reference/format-details.md)
|
|
24
|
-
| Optional readability and wording suggestions | [Prose analysis](commands.md#package-build-prose-lint-path) |
|
|
16
|
+
| Need | Read |
|
|
17
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
18
|
+
| Frontmatter, `data`, system blocks, and infoboxes | [Frontmatter](authoring/frontmatter.md) |
|
|
19
|
+
| Addresses, links, anchors, fences, and inline expressions | [Links and markup](authoring/links-and-markup.md) |
|
|
20
|
+
| Images, icons, audio, provenance, and sidecars | [Assets](authoring/assets.md) |
|
|
21
|
+
| Canonical dates, calendars, eras, and conversions | [Dates and calendars](authoring/dates-and-calendars.md) |
|
|
22
|
+
| All accepted note types, subtypes, and fields | [Note type reference](reference/note-types.md) |
|
|
23
|
+
| Exact format details and system mappings | [Detailed reference](reference/format-details.md) |
|
|
24
|
+
| Optional readability and wording suggestions | [Prose analysis](commands.md#package-build-prose-lint-path-and-prose-score-path) |
|
|
25
25
|
|
|
26
26
|
## The shared note
|
|
27
27
|
|
|
@@ -92,7 +92,7 @@ Place notes may declare a governing affiliation with `data.government`, or expli
|
|
|
92
92
|
|
|
93
93
|
A region may list its settlements in a table under `## Settlements` — name, market, people and holder, one row each — tagged with the holding model it is written in. A row links the settlement's note where one exists, and `package-build lint` holds the two to agreement. See [a region's settlements table](reference/format-details.md#a-regions-settlements-table).
|
|
94
94
|
|
|
95
|
-
A being states its gender, frame and appearance in words from one closed vocabulary: `data.gender`, `data.frame`, and `data.appearance.eye_color`, `hair_color`, `skin_color` and `complexion` each take only the words listed for them, and any other value is an error at the value. `data.appearance.extra_features`
|
|
95
|
+
A being states its gender, frame and appearance in words from one closed vocabulary: `data.gender`, `data.frame`, and `data.appearance.eye_color`, `hair_color`, `skin_color` and `complexion` each take only the words listed for them, and any other value is an error at the value. `complexion` is a list, required once a being states an appearance. `data.appearance.extra_features` lists the being's visible physical marks, each a short plain description of one, and an entry stating a colour, a skin condition or anything that is not a mark is an error. See [a being's look](reference/format-details.md#a-beings-look).
|
|
96
96
|
|
|
97
97
|
A culture note may state what its people look like in `data.appearanceProfile`: weighted words for frame, eyes, hair, skin and complexion, and height ranges by gender, all from the same vocabulary. A culture names its tongue in `data.language`, and that language skill note may state `data.names`: the patterns its speakers' names are built by and the lists each part is drawn from. `package-build npc new` writes a complete npc note from a spec of a culture and a gender, drawing the name from those lists and everything else the spec leaves out from the profile. See [appearance profiles](reference/format-details.md#appearance-profiles), [name lists](reference/format-details.md#name-lists) and [the command](commands.md#package-build-npc-new).
|
|
98
98
|
|
|
@@ -31,7 +31,7 @@ document ID appears under another address, and **withdrawn** when that ID
|
|
|
31
31
|
appears under none. Findings are warnings by default. `--strict` turns them
|
|
32
32
|
into errors for a release gate. The comparison reads shipped Item packs, so it
|
|
33
33
|
reports the surface consumers actually resolve rather than inferring changes
|
|
34
|
-
from filenames. Use the [Address reference](../
|
|
34
|
+
from filenames. Use the [Address reference](../reference/format-details.md#addresses)
|
|
35
35
|
when choosing a durable shortcode.
|
|
36
36
|
|
|
37
37
|
## Build the release files
|
|
@@ -1916,7 +1916,7 @@ largest print slot needs. Both exempt a vector asset, which carries no pixel
|
|
|
1916
1916
|
dimensions and no aspect that survives being drawn at a nominal size.
|
|
1917
1917
|
|
|
1918
1918
|
`role: map` also selects a captioned image's counter — see
|
|
1919
|
-
[
|
|
1919
|
+
[captions and numbered references](../authoring/links-and-markup.md#captions-and-numbered-references).
|
|
1920
1920
|
A fence whose picture draws an address with that role counts as `Map` rather
|
|
1921
1921
|
than `Figure`, on every surface that numbers it, because the role is the one
|
|
1922
1922
|
fact the fence's own markup cannot state.
|
|
@@ -3616,8 +3616,8 @@ See [a being's subtype describes the subject](#a-beings-subtype-describes-the-su
|
|
|
3616
3616
|
| `appearance.eye_color` | a word | Eye colour — see [a being's look](#a-beings-look) |
|
|
3617
3617
|
| `appearance.hair_color` | a word | Hair colour — see [a being's look](#a-beings-look) |
|
|
3618
3618
|
| `appearance.skin_color` | a word | Skin colour — see [a being's look](#a-beings-look) |
|
|
3619
|
-
| `appearance.complexion` | `word
|
|
3620
|
-
| `appearance.extra_features` | `string[]` |
|
|
3619
|
+
| `appearance.complexion` | `word[]`, required with `appearance` | The skin's condition, a list of words — see [a being's look](#a-beings-look) |
|
|
3620
|
+
| `appearance.extra_features` | `string[]` | The visible physical marks no word covers — see [a being's look](#a-beings-look) |
|
|
3621
3621
|
|
|
3622
3622
|
Author `data.height` as a string of decimal metres (`1.91m` or `1.91 m`) or
|
|
3623
3623
|
whole feet with optional inches (`6'`, `6'3"`, or `6' 3"`). Inches must be
|
|
@@ -3648,15 +3648,45 @@ vocabulary**, so every package describes a face in the same terms and a
|
|
|
3648
3648
|
misspelt or invented word is caught. `data.gender`, `data.frame`,
|
|
3649
3649
|
`data.appearance.eye_color`, `data.appearance.hair_color` and
|
|
3650
3650
|
`data.appearance.skin_color` each take one word from their own list.
|
|
3651
|
-
`data.appearance.complexion` takes one word or a list of them, since a face
|
|
3652
|
-
carries more than one condition at once; a single word means a list of one.
|
|
3653
3651
|
Any other value — a word from another field's list, a variant spelling such as
|
|
3654
3652
|
`gray` for `grey`, or an empty string — is an error at the value, naming the
|
|
3655
3653
|
words the field takes. Leave a field out when the fact is unrecorded.
|
|
3656
3654
|
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3655
|
+
**`data.appearance.complexion` is a list, and a being that states an appearance
|
|
3656
|
+
must state it.** A face carries more than one condition at once, so the field
|
|
3657
|
+
is always a list of complexion words: `[weathered, freckled]`, `[clear]` for
|
|
3658
|
+
skin with nothing remarkable about it, or `[]` where no condition is
|
|
3659
|
+
established. A single word written outside a list, such as `complexion: clear`,
|
|
3660
|
+
is an error that asks for the list, and so is an `appearance` map with no
|
|
3661
|
+
`complexion` or a `complexion` of `null`.
|
|
3662
|
+
|
|
3663
|
+
**`data.appearance.extra_features` lists the visible physical marks no word
|
|
3664
|
+
covers** — a scar and where it is, a missing finger, a tattoo, a limp. Each
|
|
3665
|
+
entry is a short plain description of one mark. The closed fields above carry
|
|
3666
|
+
everything else, so an entry is an error at that entry when it:
|
|
3667
|
+
|
|
3668
|
+
- states a hair, eye or skin colour or a skin condition, which belong in
|
|
3669
|
+
`hair_color`, `eye_color`, `skin_color` and `complexion` — `auburn hair`,
|
|
3670
|
+
`red eyes`, `milky white eyes`, `brown and black hair`, `pale skin`,
|
|
3671
|
+
`weathered complexion`, `freckled`;
|
|
3672
|
+
- names gear, clothing or a weapon — `a mail shirt kept oiled`,
|
|
3673
|
+
`carries a heavy battleaxe`;
|
|
3674
|
+
- describes a voice or speech — `speaks with a lisp`;
|
|
3675
|
+
- describes behaviour, manner or temperament — `fidgets constantly`,
|
|
3676
|
+
`an infectious grin`, `hyperactive`;
|
|
3677
|
+
- gives an impression of looks or presence — `startlingly beautiful`,
|
|
3678
|
+
`a commanding presence`, and `a beautiful tattoo of a crane` too, mark or not;
|
|
3679
|
+
- states handedness, sterility or an apparent age — `left handed`, `sterile`,
|
|
3680
|
+
`a boyish face that makes him look younger`;
|
|
3681
|
+
- carries any punctuation, apart from a hyphen or an apostrophe between two
|
|
3682
|
+
letters. `ink-stained fingers` and `a lion's mane` are marks; `Scar: left
|
|
3683
|
+
cheek` and `a scar on the cheek, from a duel` are not.
|
|
3684
|
+
|
|
3685
|
+
A mark that names the skin it sits on is still a mark: `a burn scar on the left
|
|
3686
|
+
forearm` and `a brand scarred into the skin above the heart` are features. A
|
|
3687
|
+
tattoo, scar or brand may show gear or a gesture, as in `a tattoo of a knight's
|
|
3688
|
+
helmet`, and stands; the voice, impression, handedness, sterility and age rules
|
|
3689
|
+
hold even for a mark.
|
|
3660
3690
|
|
|
3661
3691
|
```yaml
|
|
3662
3692
|
data:
|
|
@@ -3669,6 +3699,7 @@ data:
|
|
|
3669
3699
|
complexion: [weathered, freckled]
|
|
3670
3700
|
extra_features:
|
|
3671
3701
|
- a burn scar across the back of the left hand
|
|
3702
|
+
- ink-stained fingers
|
|
3672
3703
|
```
|
|
3673
3704
|
|
|
3674
3705
|
The words, and what each means:
|
|
@@ -53,33 +53,33 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
53
53
|
|
|
54
54
|
**Subtypes:** `npc`, `character`, `creature`.
|
|
55
55
|
|
|
56
|
-
| Field | Shape
|
|
57
|
-
| -------------------------------- |
|
|
58
|
-
| `data.icon` | an Address
|
|
59
|
-
| `data.tokenIcon` | an Address
|
|
60
|
-
| `data.templatePriority` | number
|
|
61
|
-
| `data.archetypes` | list of strings
|
|
62
|
-
| `data.occupation` | string
|
|
63
|
-
| `data.lore` | list of Addresses
|
|
64
|
-
| `data.culture` | an Address
|
|
65
|
-
| `data.homes` | list of Addresses
|
|
66
|
-
| `data.affiliations` | a standing map keyed by Address
|
|
67
|
-
| `data.socialTies` | a map keyed by Address
|
|
68
|
-
| `data.gender` | string
|
|
69
|
-
| `data.species` | an Address
|
|
70
|
-
| `data.born` | string
|
|
71
|
-
| `data.calendar` | an Address
|
|
72
|
-
| `data.died` | string
|
|
73
|
-
| `data.age` | string
|
|
74
|
-
| `data.ageYears` | number
|
|
75
|
-
| `data.height` | metres or feet and inches
|
|
76
|
-
| `data.weight` | kilograms or pounds
|
|
77
|
-
| `data.frame` | string
|
|
78
|
-
| `data.appearance.eye_color` | string
|
|
79
|
-
| `data.appearance.hair_color` | string
|
|
80
|
-
| `data.appearance.skin_color` | string
|
|
81
|
-
| `data.appearance.complexion` |
|
|
82
|
-
| `data.appearance.extra_features` | list of strings
|
|
56
|
+
| Field | Shape | Meaning |
|
|
57
|
+
| -------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
|
|
59
|
+
| `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
|
|
60
|
+
| `data.templatePriority` | number | Template priority; unset means the note is not a template. |
|
|
61
|
+
| `data.archetypes` | list of strings | Archetypal behaviours the being fits. |
|
|
62
|
+
| `data.occupation` | string | What the being does for a living. |
|
|
63
|
+
| `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in. |
|
|
64
|
+
| `data.culture` | an Address | The being's primary culture, as a culture lore note. |
|
|
65
|
+
| `data.homes` | list of Addresses | Places the being calls home. |
|
|
66
|
+
| `data.affiliations` | a standing map keyed by Address | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, required, a level on that body's own ladder, and `office`, optional, a post that body names. |
|
|
67
|
+
| `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
|
|
68
|
+
| `data.gender` | string | One of `female`, `male`, `nonbinary`, `none` or `other`. `none` says the being has no gender; an absent field says its gender is unrecorded. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
69
|
+
| `data.species` | an Address | The being's species, as a lore note. |
|
|
70
|
+
| `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
|
|
71
|
+
| `data.calendar` | an Address | Calendar that displays this being's dates. |
|
|
72
|
+
| `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
|
|
73
|
+
| `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
|
|
74
|
+
| `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
|
|
75
|
+
| `data.height` | metres or feet and inches | Height in metres or feet and inches. |
|
|
76
|
+
| `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
|
|
77
|
+
| `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
78
|
+
| `data.appearance.eye_color` | string | Eye colour: one of `amber`, `black`, `blue`, `brown`, `dark_brown`, `grey`, `green`, `hazel` or `violet`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
79
|
+
| `data.appearance.hair_color` | string | Hair colour: one of `auburn`, `black`, `blonde`, `brown`, `chestnut`, `dark_blonde`, `dark_brown`, `grey`, `greying_black`, `greying_brown`, `red`, `silver`, `strawberry_blond` or `white`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
80
|
+
| `data.appearance.skin_color` | string | Skin colour: one of `pale`, `fair`, `light`, `medium`, `olive`, `tawny`, `golden`, `tanned`, `swarthy`, `brown`, `dark`, `dark_brown` or `ebony`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
81
|
+
| `data.appearance.complexion` | a list of words, such as `[weathered]` or `[]` | The skin's condition, as a list of `ashen`, `blotchy`, `chapped`, `clear`, `freckled`, `jaundiced`, `leathery`, `oily`, `pasty`, `pimpled`, `pockmarked`, `rough`, `ruddy`, `sallow`, `scabrous`, `scarred`, `smooth`, `sun_kissed`, `sunburnt`, `translucent`, `vitiligo`, `wan`, `weathered` or `wrinkled`: `[weathered, freckled]`, `[clear]` for unremarked skin, or `[]` where no condition is established. Required once a being states an appearance; a single word outside a list is an error. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
82
|
+
| `data.appearance.extra_features` | list of strings | The visible physical marks no word covers, each a short plain description of one: `a burn scar on the left forearm`, `a missing finger`. An entry stating a hair, eye or skin colour or a skin condition, gear or clothing, a voice, behaviour or manner, an impression, handedness, sterility or an apparent age is an error, as is any punctuation but a hyphen or apostrophe inside a word. See [a being's look](format-details.md#a-beings-look). |
|
|
83
83
|
|
|
84
84
|
**Inner keys** — a key a field's value does not declare is an error at its own line and column.
|
|
85
85
|
|
|
@@ -127,7 +127,7 @@ of two ways, and the difference is who resolved it: a content page carries
|
|
|
127
127
|
`data.banner`, which the build resolved from the asset Address the note
|
|
128
128
|
declared into the address its asset host serves; a hand-authored page carries
|
|
129
129
|
a top-level `banner:`, which the theme resolves against `params.cdnBaseURL`
|
|
130
|
-
(see [`params.cdnBaseURL`](../configuration.md#
|
|
130
|
+
(see [`params.cdnBaseURL`](../configuration.md#the-generated-hugo-configuration)).
|
|
131
131
|
|
|
132
132
|
**Resolution order.**
|
|
133
133
|
|
|
@@ -303,7 +303,7 @@ fails this theme's own check until the slot table gives it a maximum.
|
|
|
303
303
|
wrapping. On narrow screens, figures remain in document order without text
|
|
304
304
|
wrapping. An image's label is alternative text, carried on the image itself
|
|
305
305
|
for a reader who cannot see it, and is not drawn beneath the picture. See
|
|
306
|
-
[
|
|
306
|
+
[captions and numbered references](../authoring/links-and-markup.md#captions-and-numbered-references)
|
|
307
307
|
for how an author writes `size`, `float` and a role.
|
|
308
308
|
|
|
309
309
|
## A numbered figure
|
|
@@ -479,8 +479,9 @@ The home page declares no `infoboxes:`, `related:`, `date:` or `tags:`, so
|
|
|
479
479
|
those pieces of a page stay silent.
|
|
480
480
|
|
|
481
481
|
**No `type` at all** — a site whose home page is a set of entry points into
|
|
482
|
-
everything it publishes — renders the featured grid
|
|
483
|
-
|
|
482
|
+
everything it publishes — renders the featured grid from `params.home`.
|
|
483
|
+
A package-build configuration supplies those values through
|
|
484
|
+
[`site.hugo`](../configuration.md#site).
|
|
484
485
|
|
|
485
486
|
### A section landing
|
|
486
487
|
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **What a being's `data.appearance.extra_features` admits.**
|
|
16
|
+
*
|
|
17
|
+
* Each entry is a short plain description of one visible physical mark: a
|
|
18
|
+
* scar, a missing finger, a tattoo, a limp. The closed look fields carry
|
|
19
|
+
* everything else, so an entry is refused when it states:
|
|
20
|
+
*
|
|
21
|
+
* - **a colour or skin condition** — a hair colour beside the word `hair`, an
|
|
22
|
+
* eye colour beside `eye` or `eyes`, a skin colour beside `skin` or
|
|
23
|
+
* `complexion`, or a skin condition. These belong in `hair_color`,
|
|
24
|
+
* `eye_color`, `skin_color` and `complexion`. The colour and condition words
|
|
25
|
+
* are the being-look vocabulary's own, read at run time, plus the spellings
|
|
26
|
+
* in {@link COLOUR_VARIANTS} and {@link CONDITION_PHRASES}. A localised mark
|
|
27
|
+
* that only mentions skin — a burn scar on the forearm — is a mark;
|
|
28
|
+
* - **anything that is not a physical mark** — the categories of
|
|
29
|
+
* {@link NON_PHYSICAL_FEATURES}: gear, clothing or a weapon; a voice or
|
|
30
|
+
* speech; behaviour, manner or temperament; an impression; handedness; sterility; an
|
|
31
|
+
* apparent-age reading;
|
|
32
|
+
* - **punctuation** — any punctuation or symbol character, apart from a hyphen
|
|
33
|
+
* or an apostrophe standing between two letters (`ink-stained fingers`, `a
|
|
34
|
+
* lion's mane`). A long punctuated entry is several facts, or a labelled one,
|
|
35
|
+
* where a single mark belongs.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { isPlainMap } from "./data-keys.mjs";
|
|
41
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
42
|
+
|
|
43
|
+
/** A vocabulary word as an author writes it: `dark_brown` is "dark brown" or "dark-brown". */
|
|
44
|
+
const phrase = (word) => word.replaceAll("_", "[ -]");
|
|
45
|
+
|
|
46
|
+
/** A case-insensitive whole-word alternation. */
|
|
47
|
+
const anyOf = (alternatives) => new RegExp(`\\b(?:${alternatives.join("|")})\\b`, "iu");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hair and eye colour spellings the vocabulary does not take but an author
|
|
51
|
+
* writes, as regular-expression alternatives. Beside `hair` or `eyes` any of
|
|
52
|
+
* them, or any hair or eye colour word, states a colour.
|
|
53
|
+
*/
|
|
54
|
+
export const COLOUR_VARIANTS = Object.freeze([
|
|
55
|
+
"gr[ae]y(?:ing|ed)?",
|
|
56
|
+
"blond",
|
|
57
|
+
"ginger",
|
|
58
|
+
"milky",
|
|
59
|
+
"yellow",
|
|
60
|
+
"gold(?:en)?",
|
|
61
|
+
"pink",
|
|
62
|
+
"orange",
|
|
63
|
+
"purple",
|
|
64
|
+
"copper",
|
|
65
|
+
"mixed",
|
|
66
|
+
"streaked",
|
|
67
|
+
"dyed",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Skin-condition phrasings that are not complexion words, as
|
|
72
|
+
* regular-expression alternatives. Any of them states a complexion.
|
|
73
|
+
*/
|
|
74
|
+
export const CONDITION_PHRASES = Object.freeze([
|
|
75
|
+
"freckles",
|
|
76
|
+
"pox marks?",
|
|
77
|
+
"pocked",
|
|
78
|
+
"pitted skin",
|
|
79
|
+
"acne",
|
|
80
|
+
"pimples",
|
|
81
|
+
"sunburn",
|
|
82
|
+
"wrinkles",
|
|
83
|
+
"pallid",
|
|
84
|
+
"flushed",
|
|
85
|
+
"weather-beaten",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Complexion words with an everyday sense beyond the skin: refused when the
|
|
90
|
+
* entry is the word alone, or names the skin and describes no mark.
|
|
91
|
+
*/
|
|
92
|
+
const BROAD_CONDITIONS = Object.freeze(["scarred", "rough", "clear", "smooth", "oily", "wan"]);
|
|
93
|
+
|
|
94
|
+
/** An entry describing one of these is a physical mark, whatever else it mentions. */
|
|
95
|
+
const MARK =
|
|
96
|
+
/\b(?:tattoos?|tatoo|scars?|scarred|scarring|scarification|brand(?:ed)?|birthmark|burn marks?|pox marks?)\b/iu;
|
|
97
|
+
|
|
98
|
+
const SKIN = /\b(?:skin|complexion|complected)\b/iu;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The terms that make an `extra_features` entry something other than a
|
|
102
|
+
* physical mark, by category, as case-insensitive whole-word patterns.
|
|
103
|
+
*
|
|
104
|
+
* `exemptMarks` lets an entry describing a tattoo, scar, brand or burn mark
|
|
105
|
+
* pass the category, so "a tattoo of a knight's helmet" stands while "wears a
|
|
106
|
+
* helmet" does not. The voice, impression, handedness, sterility and age
|
|
107
|
+
* categories admit no such exemption: "a beautiful tattoo of a crane" is
|
|
108
|
+
* refused.
|
|
109
|
+
*
|
|
110
|
+
* @type {ReadonlyArray<{rule: string, category: string, pattern: RegExp, exemptMarks: boolean}>}
|
|
111
|
+
*/
|
|
112
|
+
export const NON_PHYSICAL_FEATURES = Object.freeze([
|
|
113
|
+
{
|
|
114
|
+
rule: "gear",
|
|
115
|
+
category: "gear, clothing or a weapon",
|
|
116
|
+
pattern: anyOf([
|
|
117
|
+
"wears?",
|
|
118
|
+
"worn",
|
|
119
|
+
"wearing",
|
|
120
|
+
"carr(?:ies|ied|ying)(?! (?:him|her|them|it)self)",
|
|
121
|
+
"cloak(?:ed|s)?",
|
|
122
|
+
"clothing",
|
|
123
|
+
"clothes",
|
|
124
|
+
"robes?",
|
|
125
|
+
"vestments",
|
|
126
|
+
"tunic",
|
|
127
|
+
"mail",
|
|
128
|
+
"armou?r",
|
|
129
|
+
"shirt",
|
|
130
|
+
"axe",
|
|
131
|
+
"sword",
|
|
132
|
+
"dagger",
|
|
133
|
+
"staff",
|
|
134
|
+
"bag",
|
|
135
|
+
"chain",
|
|
136
|
+
"cord",
|
|
137
|
+
"thong",
|
|
138
|
+
"boots?",
|
|
139
|
+
"hat",
|
|
140
|
+
"helm(?:et)?",
|
|
141
|
+
"ring",
|
|
142
|
+
"amulet",
|
|
143
|
+
"pendant",
|
|
144
|
+
"belt",
|
|
145
|
+
"weapons?",
|
|
146
|
+
"garb",
|
|
147
|
+
"dress(?:ed|es)?",
|
|
148
|
+
"gloves?",
|
|
149
|
+
]),
|
|
150
|
+
exemptMarks: true,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
rule: "voice",
|
|
154
|
+
category: "a voice or speech",
|
|
155
|
+
pattern: anyOf([
|
|
156
|
+
"voice",
|
|
157
|
+
"speech",
|
|
158
|
+
"speaks?",
|
|
159
|
+
"spoken",
|
|
160
|
+
"accent",
|
|
161
|
+
"lisp",
|
|
162
|
+
"stutter\\w*",
|
|
163
|
+
"stammer\\w*",
|
|
164
|
+
"whisper\\w*",
|
|
165
|
+
"tone",
|
|
166
|
+
"laugh\\w*",
|
|
167
|
+
"sings?",
|
|
168
|
+
"singing",
|
|
169
|
+
]),
|
|
170
|
+
exemptMarks: false,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
rule: "behaviour",
|
|
174
|
+
category: "behaviour, manner or temperament",
|
|
175
|
+
pattern: anyOf([
|
|
176
|
+
"moves?",
|
|
177
|
+
"moving",
|
|
178
|
+
"fidgets?",
|
|
179
|
+
"smil\\w+",
|
|
180
|
+
"grin\\w*",
|
|
181
|
+
"scowl\\w*",
|
|
182
|
+
"frown\\w*",
|
|
183
|
+
"gaze",
|
|
184
|
+
"stares?",
|
|
185
|
+
"glare\\w*",
|
|
186
|
+
"poise",
|
|
187
|
+
"bearing",
|
|
188
|
+
"demeanou?r",
|
|
189
|
+
"manner",
|
|
190
|
+
"habits?",
|
|
191
|
+
"grooming",
|
|
192
|
+
"expressive",
|
|
193
|
+
"intensity",
|
|
194
|
+
"shakes?",
|
|
195
|
+
"air of",
|
|
196
|
+
"carr(?:y|ies|ied|ying) (?:him|her|them|it)self",
|
|
197
|
+
"hyperactive",
|
|
198
|
+
"restless\\w*",
|
|
199
|
+
"jitter\\w*",
|
|
200
|
+
"twitch\\w*",
|
|
201
|
+
"nervous\\w*",
|
|
202
|
+
"anxious\\w*",
|
|
203
|
+
"energetic",
|
|
204
|
+
"lively",
|
|
205
|
+
"cheerful",
|
|
206
|
+
"sullen",
|
|
207
|
+
"shy",
|
|
208
|
+
"calm",
|
|
209
|
+
"placid",
|
|
210
|
+
"fidgety",
|
|
211
|
+
]),
|
|
212
|
+
exemptMarks: true,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
rule: "impression",
|
|
216
|
+
category: "an impression rather than a mark",
|
|
217
|
+
pattern: anyOf([
|
|
218
|
+
"beautiful",
|
|
219
|
+
"handsome",
|
|
220
|
+
"attractive",
|
|
221
|
+
"ugly",
|
|
222
|
+
"imposing",
|
|
223
|
+
"menacing",
|
|
224
|
+
"unnerv\\w+",
|
|
225
|
+
"disarming",
|
|
226
|
+
"commanding",
|
|
227
|
+
"presence",
|
|
228
|
+
"authority",
|
|
229
|
+
"pretty",
|
|
230
|
+
"lovely",
|
|
231
|
+
"gorgeous",
|
|
232
|
+
"stunning",
|
|
233
|
+
"comely",
|
|
234
|
+
"homely",
|
|
235
|
+
"hideous",
|
|
236
|
+
"striking\\w*",
|
|
237
|
+
"alluring",
|
|
238
|
+
"dashing",
|
|
239
|
+
"rugged",
|
|
240
|
+
"elegant\\w*",
|
|
241
|
+
"grace\\w*",
|
|
242
|
+
"feminine",
|
|
243
|
+
"masculine",
|
|
244
|
+
"girly",
|
|
245
|
+
"good looks",
|
|
246
|
+
]),
|
|
247
|
+
exemptMarks: false,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
rule: "handedness",
|
|
251
|
+
category: "handedness",
|
|
252
|
+
pattern: /\b(?:left|right)[- ]handed\b|\bambidextrous\b|\bhandedness\b|\bsouthpaw\b/iu,
|
|
253
|
+
exemptMarks: false,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
rule: "sterility",
|
|
257
|
+
category: "sterility",
|
|
258
|
+
pattern: anyOf(["steril\\w*", "infertil\\w*", "barren", "childless"]),
|
|
259
|
+
exemptMarks: false,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
rule: "age",
|
|
263
|
+
category: "an apparent-age reading",
|
|
264
|
+
pattern: anyOf([
|
|
265
|
+
"look(?:s|ing)? (?:younger|older|young|old)",
|
|
266
|
+
"younger than",
|
|
267
|
+
"older than",
|
|
268
|
+
"appears? (?:\\w+ )?(?:years|young|old)",
|
|
269
|
+
"apparent age",
|
|
270
|
+
"youthful",
|
|
271
|
+
"boyish",
|
|
272
|
+
"girlish",
|
|
273
|
+
"childlike",
|
|
274
|
+
"ageless",
|
|
275
|
+
"elderly",
|
|
276
|
+
]),
|
|
277
|
+
exemptMarks: false,
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
/** A hyphen or an apostrophe standing between two letters, which an entry may carry. */
|
|
282
|
+
const JOINER = /(?<=\p{L})[-‐'’](?=\p{L})/gu;
|
|
283
|
+
|
|
284
|
+
/** Any punctuation or symbol character. */
|
|
285
|
+
const PUNCTUATION = /[\p{P}\p{S}]/gu;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The punctuation an entry carries, a hyphen or apostrophe between letters
|
|
289
|
+
* excepted.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} entry - The entry as written.
|
|
292
|
+
* @returns {string[]} Each distinct character, in order of appearance.
|
|
293
|
+
*/
|
|
294
|
+
export function featurePunctuation(entry) {
|
|
295
|
+
return [...new Set(entry.replace(JOINER, "").match(PUNCTUATION) ?? [])];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The colour and condition patterns, built once per vocabulary. */
|
|
299
|
+
const patterns = new WeakMap();
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The colour and condition patterns of one being-look vocabulary.
|
|
303
|
+
*
|
|
304
|
+
* @param {Readonly<Record<string, readonly {value: string}[]>>} look
|
|
305
|
+
* @returns {{colour: RegExp, skin: RegExp, condition: RegExp, broad: RegExp, bareBroad: RegExp}}
|
|
306
|
+
*/
|
|
307
|
+
function lookPatterns(look) {
|
|
308
|
+
let built = patterns.get(look);
|
|
309
|
+
if (built) return built;
|
|
310
|
+
const words = (field) => (look[field] ?? []).map(({ value }) => value);
|
|
311
|
+
const complexion = words("appearance.complexion");
|
|
312
|
+
const broad = BROAD_CONDITIONS.filter((word) => complexion.includes(word));
|
|
313
|
+
built = {
|
|
314
|
+
colour: anyOf(
|
|
315
|
+
[...new Set([...words("appearance.hair_color"), ...words("appearance.eye_color")])]
|
|
316
|
+
.map(phrase)
|
|
317
|
+
.concat(COLOUR_VARIANTS),
|
|
318
|
+
),
|
|
319
|
+
skin: anyOf(words("appearance.skin_color").map(phrase)),
|
|
320
|
+
condition: anyOf([
|
|
321
|
+
...complexion.filter((word) => !broad.includes(word)).map(phrase),
|
|
322
|
+
...CONDITION_PHRASES,
|
|
323
|
+
]),
|
|
324
|
+
broad: anyOf(broad.map(phrase)),
|
|
325
|
+
bareBroad: new RegExp(`^(?:${broad.map(phrase).join("|")})$`, "iu"),
|
|
326
|
+
};
|
|
327
|
+
patterns.set(look, built);
|
|
328
|
+
return built;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Why an `extra_features` entry is not a short plain description of one
|
|
333
|
+
* physical mark.
|
|
334
|
+
*
|
|
335
|
+
* @param {string} entry - The entry as written.
|
|
336
|
+
* @param {Readonly<Record<string, readonly {value: string}[]>>} look - The
|
|
337
|
+
* being-look vocabulary.
|
|
338
|
+
* @returns {Array<{rule: string, message: string}>} One problem per rule the
|
|
339
|
+
* entry breaks; empty when it is a mark. `rule` is `hair-colour`,
|
|
340
|
+
* `eye-colour`, `skin-colour`, `complexion`, `punctuation`, or a rule of
|
|
341
|
+
* {@link NON_PHYSICAL_FEATURES}.
|
|
342
|
+
*/
|
|
343
|
+
export function featureProblems(entry, look) {
|
|
344
|
+
const p = lookPatterns(look);
|
|
345
|
+
const has = (re) => re.test(entry);
|
|
346
|
+
const isMark = has(MARK);
|
|
347
|
+
const problems = [];
|
|
348
|
+
if (has(/\bhair\b/iu) && has(p.colour))
|
|
349
|
+
problems.push({
|
|
350
|
+
rule: "hair-colour",
|
|
351
|
+
message: "states a hair colour, which belongs in `appearance.hair_color`",
|
|
352
|
+
});
|
|
353
|
+
if (has(/\beyes?\b/iu) && has(p.colour))
|
|
354
|
+
problems.push({
|
|
355
|
+
rule: "eye-colour",
|
|
356
|
+
message: "states an eye colour, which belongs in `appearance.eye_color`",
|
|
357
|
+
});
|
|
358
|
+
if (has(SKIN) && has(p.skin))
|
|
359
|
+
problems.push({
|
|
360
|
+
rule: "skin-colour",
|
|
361
|
+
message: "states a skin colour, which belongs in `appearance.skin_color`",
|
|
362
|
+
});
|
|
363
|
+
if (
|
|
364
|
+
has(p.condition) ||
|
|
365
|
+
p.bareBroad.test(entry.trim()) ||
|
|
366
|
+
(has(p.broad) && has(SKIN) && !isMark)
|
|
367
|
+
)
|
|
368
|
+
problems.push({
|
|
369
|
+
rule: "complexion",
|
|
370
|
+
message: "states a skin condition, which belongs in `appearance.complexion`",
|
|
371
|
+
});
|
|
372
|
+
for (const { rule, category, pattern, exemptMarks } of NON_PHYSICAL_FEATURES) {
|
|
373
|
+
if (exemptMarks && isMark) continue;
|
|
374
|
+
if (has(pattern))
|
|
375
|
+
problems.push({ rule, message: `names ${category}, which is not a physical mark` });
|
|
376
|
+
}
|
|
377
|
+
const marks = featurePunctuation(entry);
|
|
378
|
+
if (marks.length)
|
|
379
|
+
problems.push({
|
|
380
|
+
rule: "punctuation",
|
|
381
|
+
message:
|
|
382
|
+
"carries punctuation " +
|
|
383
|
+
marks.map((mark) => `\`${mark}\``).join(" ") +
|
|
384
|
+
"; an extra feature is a short plain description of one visible mark, " +
|
|
385
|
+
"with no punctuation but a hyphen or apostrophe inside a word",
|
|
386
|
+
});
|
|
387
|
+
return problems;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The frontmatter lint's check of `data.appearance.extra_features`.
|
|
392
|
+
*
|
|
393
|
+
* @param {object} note - The note: `file`, `raw` and parsed `fm`.
|
|
394
|
+
* @param {Readonly<Record<string, readonly {value: string}[]>>} look - The
|
|
395
|
+
* being-look vocabulary.
|
|
396
|
+
* @returns {object[]} Error findings, each located at the entry at fault.
|
|
397
|
+
*/
|
|
398
|
+
export function checkExtraFeatures(note, look) {
|
|
399
|
+
const appearance = note?.fm?.data?.appearance;
|
|
400
|
+
if (!isPlainMap(appearance)) return [];
|
|
401
|
+
const features = appearance.extra_features;
|
|
402
|
+
if (!Array.isArray(features)) return [];
|
|
403
|
+
const raw = note.raw ?? "";
|
|
404
|
+
const findings = [];
|
|
405
|
+
features.forEach((entry, index) => {
|
|
406
|
+
if (typeof entry !== "string") return;
|
|
407
|
+
const path = ["data", "appearance", "extra_features", index];
|
|
408
|
+
for (const { message } of featureProblems(entry, look)) {
|
|
409
|
+
findings.push({
|
|
410
|
+
file: note.file,
|
|
411
|
+
...positionOfFrontmatterPath(raw, path),
|
|
412
|
+
severity: "error",
|
|
413
|
+
message: `\`data.appearance.extra_features[${index}]\` ${message}, but reads ${JSON.stringify(entry)}`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return findings;
|
|
418
|
+
}
|
package/engine/data-keys.mjs
CHANGED
|
@@ -185,7 +185,8 @@ export function mapEntriesOf(value) {
|
|
|
185
185
|
*
|
|
186
186
|
* @param {readonly object[]} fields - The type's `data:` declaration.
|
|
187
187
|
* @returns {object[]} One spec per top-level key, a dotted group as a `map`
|
|
188
|
-
* whose `fields` are its members, with `grouped: true
|
|
188
|
+
* whose `fields` are its members, with `grouped: true`, and each member
|
|
189
|
+
* marked `kindChecked`.
|
|
189
190
|
*/
|
|
190
191
|
export function topLevelSpecs(fields) {
|
|
191
192
|
const out = [];
|
|
@@ -202,7 +203,10 @@ export function topLevelSpecs(fields) {
|
|
|
202
203
|
groups.set(head, group);
|
|
203
204
|
out.push(group);
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
+
// The frontmatter lint's container check has already held a dotted
|
|
207
|
+
// field's leaf to its kind; `kindChecked` keeps the walk from saying
|
|
208
|
+
// so a second time.
|
|
209
|
+
group.members.push({ ...field, name: rest.join("."), kindChecked: true });
|
|
206
210
|
}
|
|
207
211
|
for (const group of groups.values()) {
|
|
208
212
|
group.fields = topLevelSpecs(group.members);
|
|
@@ -285,6 +289,7 @@ export function checkDataKeys(note, fields) {
|
|
|
285
289
|
const visit = (spec, value, path) => {
|
|
286
290
|
if (value === undefined || value === null) return;
|
|
287
291
|
if (!fitsKind(value, spec.kind)) {
|
|
292
|
+
if (spec.kindChecked && !spec.fields) return;
|
|
288
293
|
findings.push(
|
|
289
294
|
finding(
|
|
290
295
|
path,
|
package/engine/docs-checks.mjs
CHANGED
|
@@ -29,11 +29,39 @@ function* markdownFiles(root) {
|
|
|
29
29
|
|
|
30
30
|
/** Preserve offsets while excluding literal examples from link scanning. */
|
|
31
31
|
export function maskMarkdownCode(source, spans = true) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const mask = (value) => value.replace(/[^\n]/g, " ");
|
|
33
|
+
let fence;
|
|
34
|
+
let body = "";
|
|
35
|
+
for (const line of source.match(/[^\n]*\n|[^\n]+$/g) || []) {
|
|
36
|
+
if (fence) {
|
|
37
|
+
body += mask(line);
|
|
38
|
+
const close = line.match(/^ {0,3}(`+|~+)[ \t]*\r?\n?$/);
|
|
39
|
+
if (close && close[1][0] === fence[0] && close[1].length >= fence.length)
|
|
40
|
+
fence = undefined;
|
|
41
|
+
} else {
|
|
42
|
+
const open = line.match(/^ {0,3}(`{3,}|~{3,})(.*)/);
|
|
43
|
+
if (open && (open[1][0] !== "`" || !open[2].includes("`"))) {
|
|
44
|
+
fence = open[1];
|
|
45
|
+
body += mask(line);
|
|
46
|
+
} else body += line;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!spans) return body;
|
|
50
|
+
const delimiters = [...body.matchAll(/`+/g)];
|
|
51
|
+
let offset = 0;
|
|
52
|
+
let result = "";
|
|
53
|
+
for (let i = 0; i < delimiters.length; i++) {
|
|
54
|
+
const start = delimiters[i];
|
|
55
|
+
const end = delimiters.findIndex(
|
|
56
|
+
(candidate, index) => index > i && candidate[0].length === start[0].length,
|
|
57
|
+
);
|
|
58
|
+
if (end < 0) continue;
|
|
59
|
+
const stop = delimiters[end].index + delimiters[end][0].length;
|
|
60
|
+
result += body.slice(offset, start.index) + mask(body.slice(start.index, stop));
|
|
61
|
+
offset = stop;
|
|
62
|
+
i = end;
|
|
63
|
+
}
|
|
64
|
+
return result + body.slice(offset);
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
/** GitHub heading slug, retaining separate spaces after punctuation removal. */
|
|
@@ -50,11 +78,16 @@ export function docHeadingSlug(value) {
|
|
|
50
78
|
export function docAnchors(source) {
|
|
51
79
|
const body = maskMarkdownCode(source, false);
|
|
52
80
|
const anchors = new Set();
|
|
81
|
+
const suffixes = new Map();
|
|
53
82
|
for (const [, heading] of body.matchAll(/^#{1,6}\s+(.+?)\s*#*$/gm)) {
|
|
54
83
|
const explicit = splitHeadingAttributes(heading);
|
|
55
|
-
|
|
56
|
-
explicit.id || docHeadingSlug(explicit.text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"))
|
|
57
|
-
|
|
84
|
+
const base =
|
|
85
|
+
explicit.id || docHeadingSlug(explicit.text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"));
|
|
86
|
+
let anchor = base;
|
|
87
|
+
let suffix = suffixes.get(base) || 0;
|
|
88
|
+
while (anchors.has(anchor)) anchor = `${base}-${++suffix}`;
|
|
89
|
+
suffixes.set(base, suffix);
|
|
90
|
+
anchors.add(anchor);
|
|
58
91
|
}
|
|
59
92
|
for (const [, anchor] of body.matchAll(/<a\s[^>]*(?:id|name)="([^"]+)"/g)) {
|
|
60
93
|
anchors.add(anchor);
|
package/engine/infobox.mjs
CHANGED
|
@@ -506,15 +506,15 @@ export function humanizeValue(value) {
|
|
|
506
506
|
/**
|
|
507
507
|
* Several enumerated values as one clause — `weathered, ruddy and scarred`.
|
|
508
508
|
*
|
|
509
|
-
* A
|
|
510
|
-
*
|
|
511
|
-
*
|
|
509
|
+
* A list field reads as a single phrase, so the noun it qualifies is said
|
|
510
|
+
* once rather than once per value. Internal: the phrase that wants it is
|
|
511
|
+
* declared in this module.
|
|
512
512
|
*
|
|
513
|
-
* @param {unknown} value -
|
|
513
|
+
* @param {unknown} value - A list of values.
|
|
514
514
|
* @returns {string} The text.
|
|
515
515
|
*/
|
|
516
516
|
function humanizedList(value) {
|
|
517
|
-
const parts = (Array.isArray(value) ? value : [
|
|
517
|
+
const parts = (Array.isArray(value) ? value : [])
|
|
518
518
|
.filter((entry) => entry !== undefined && entry !== null && entry !== "")
|
|
519
519
|
.map((entry) => humanizeValue(entry));
|
|
520
520
|
if (parts.length < 2) return parts[0] ?? "";
|
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
profileLookKeys,
|
|
118
118
|
requiredHeightGenders,
|
|
119
119
|
} from "./appearance-profile.mjs";
|
|
120
|
+
import { checkExtraFeatures } from "./appearance-features.mjs";
|
|
120
121
|
import { LANGUAGE_FIELD, LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
121
122
|
import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
|
|
122
123
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
@@ -272,9 +273,9 @@ function lookWords(rows) {
|
|
|
272
273
|
*
|
|
273
274
|
* Each field is closed to its list: any other value, an empty one included, is
|
|
274
275
|
* a finding at the value naming the words it takes. `appearance.complexion`
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
276
|
+
* is a list of words, each entry checked on its own. `appearance.extra_features`
|
|
277
|
+
* is not here: it is a list of the physical marks no word covers, held to the
|
|
278
|
+
* rules of `appearance-features.mjs`.
|
|
278
279
|
*
|
|
279
280
|
* @type {Readonly<Record<string, readonly LookWord[]>>}
|
|
280
281
|
*/
|
|
@@ -596,15 +597,18 @@ const TEXT_LIST = Object.freeze({
|
|
|
596
597
|
});
|
|
597
598
|
|
|
598
599
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
* facts, so a single value means a list of one and a consumer reads one shape.
|
|
600
|
+
* A being's complexion: a list of complexion words, `[]` included. The shape
|
|
601
|
+
* names the list form, because the finding an author meets for a single word
|
|
602
|
+
* quotes it.
|
|
603
603
|
*/
|
|
604
|
-
const
|
|
605
|
-
shape: "
|
|
606
|
-
kind: "
|
|
607
|
-
entries: Object.freeze({
|
|
604
|
+
const COMPLEXION_LIST = Object.freeze({
|
|
605
|
+
shape: "a list of words, such as `[weathered]` or `[]`",
|
|
606
|
+
kind: "list",
|
|
607
|
+
entries: Object.freeze({
|
|
608
|
+
kind: "string",
|
|
609
|
+
shape: "a string",
|
|
610
|
+
allowed: lookValues("appearance.complexion"),
|
|
611
|
+
}),
|
|
608
612
|
});
|
|
609
613
|
|
|
610
614
|
/** A single Address. */
|
|
@@ -1454,22 +1458,26 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1454
1458
|
lookField("appearance.skin_color", "Skin colour"),
|
|
1455
1459
|
{
|
|
1456
1460
|
...lookField("appearance.complexion", "The skin's condition"),
|
|
1457
|
-
...
|
|
1461
|
+
...COMPLEXION_LIST,
|
|
1462
|
+
required: true,
|
|
1458
1463
|
describe:
|
|
1459
|
-
`The skin's condition
|
|
1460
|
-
"
|
|
1461
|
-
"
|
|
1464
|
+
`The skin's condition, as a list of ${wordList(lookValues("appearance.complexion"))}: ` +
|
|
1465
|
+
"`[weathered, freckled]`, `[clear]` for unremarked skin, or `[]` where no " +
|
|
1466
|
+
"condition is established. Required once a being states an appearance; a " +
|
|
1467
|
+
"single word outside a list is an error. See " +
|
|
1462
1468
|
"[a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
1463
|
-
entries: Object.freeze({
|
|
1464
|
-
kind: "string",
|
|
1465
|
-
shape: "a string",
|
|
1466
|
-
allowed: lookValues("appearance.complexion"),
|
|
1467
|
-
}),
|
|
1468
1469
|
},
|
|
1469
1470
|
{
|
|
1470
1471
|
name: "appearance.extra_features",
|
|
1471
1472
|
...TEXT_LIST,
|
|
1472
|
-
|
|
1473
|
+
check: (note) => checkExtraFeatures(note, BEING_LOOK_VOCABULARY),
|
|
1474
|
+
describe:
|
|
1475
|
+
"The visible physical marks no word covers, each a short plain description " +
|
|
1476
|
+
"of one: `a burn scar on the left forearm`, `a missing finger`. An entry " +
|
|
1477
|
+
"stating a hair, eye or skin colour or a skin condition, gear or clothing, a " +
|
|
1478
|
+
"voice, behaviour or manner, an impression, handedness, sterility or an " +
|
|
1479
|
+
"apparent age is an error, as is any punctuation but a hyphen or apostrophe " +
|
|
1480
|
+
"inside a word. See [a being's look](format-details.md#a-beings-look).",
|
|
1473
1481
|
},
|
|
1474
1482
|
]),
|
|
1475
1483
|
}),
|
package/engine/npc-generate.mjs
CHANGED
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
* being in the package already holds it.
|
|
41
41
|
* - `type: being`, `subType: npc`, `tags: [generated, draft]`.
|
|
42
42
|
* - `frame`, and `appearance.eye_color`, `hair_color`, `skin_color` and
|
|
43
|
-
* `complexion` — weighted draws from the culture's appearance profile
|
|
43
|
+
* `complexion` — weighted draws from the culture's appearance profile;
|
|
44
|
+
* `complexion` is a list of the one word drawn.
|
|
44
45
|
* - `height` — from the profile's range for the gender, in feet and inches.
|
|
45
46
|
* - `weight` — from the height and the frame's body-mass band, in pounds.
|
|
46
47
|
* - `born` — an adult age of {@link ADULT_AGE} years before the package's
|
|
@@ -346,8 +347,7 @@ export function appearanceParagraph({ name, age, gender, height, frame, appearan
|
|
|
346
347
|
const sentences = [`${first}.`];
|
|
347
348
|
|
|
348
349
|
const look = appearance ?? {};
|
|
349
|
-
const complexion =
|
|
350
|
-
.flat()
|
|
350
|
+
const complexion = (Array.isArray(look.complexion) ? look.complexion : [])
|
|
351
351
|
.filter(Boolean)
|
|
352
352
|
.map((w) => wordLabel("appearance.complexion", w));
|
|
353
353
|
const parts = [
|
|
@@ -491,7 +491,8 @@ export function generateNpc(spec, context) {
|
|
|
491
491
|
const appearance = { ...look };
|
|
492
492
|
for (const key of profileLookKeys(BEING_LOOK_VOCABULARY)) {
|
|
493
493
|
if (key === "frame" || given(look[key])) continue;
|
|
494
|
-
|
|
494
|
+
const word = weighted(profile[key], draw(`data.appearance.${key}`));
|
|
495
|
+
appearance[key] = key === "complexion" ? [word] : word;
|
|
495
496
|
}
|
|
496
497
|
if (!given(look.extra_features)) appearance.extra_features = [];
|
|
497
498
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.4.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",
|
|
@@ -191,8 +191,9 @@
|
|
|
191
191
|
"build:asset-index": "node ci/emit-asset-index.mjs",
|
|
192
192
|
"format": "prettier --write .",
|
|
193
193
|
"format:check": "prettier --check .",
|
|
194
|
-
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format",
|
|
194
|
+
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format lint:docs-links",
|
|
195
195
|
"lint:markdown": "node bin/package-build.mjs markdown",
|
|
196
|
+
"lint:docs-links": "node bin/package-build.mjs docs links",
|
|
196
197
|
"lint:yaml": "node bin/package-build.mjs yaml",
|
|
197
198
|
"lint:labels": "node bin/package-build.mjs labels check",
|
|
198
199
|
"lint:image-roles": "vitest run tests/theme-image-roles.test.ts",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The punctuation an entry carries, a hyphen or apostrophe between letters
|
|
3
|
+
* excepted.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} entry - The entry as written.
|
|
6
|
+
* @returns {string[]} Each distinct character, in order of appearance.
|
|
7
|
+
*/
|
|
8
|
+
export function featurePunctuation(entry: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Why an `extra_features` entry is not a short plain description of one
|
|
11
|
+
* physical mark.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} entry - The entry as written.
|
|
14
|
+
* @param {Readonly<Record<string, readonly {value: string}[]>>} look - The
|
|
15
|
+
* being-look vocabulary.
|
|
16
|
+
* @returns {Array<{rule: string, message: string}>} One problem per rule the
|
|
17
|
+
* entry breaks; empty when it is a mark. `rule` is `hair-colour`,
|
|
18
|
+
* `eye-colour`, `skin-colour`, `complexion`, `punctuation`, or a rule of
|
|
19
|
+
* {@link NON_PHYSICAL_FEATURES}.
|
|
20
|
+
*/
|
|
21
|
+
export function featureProblems(entry: string, look: Readonly<Record<string, readonly {
|
|
22
|
+
value: string;
|
|
23
|
+
}[]>>): Array<{
|
|
24
|
+
rule: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* The frontmatter lint's check of `data.appearance.extra_features`.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} note - The note: `file`, `raw` and parsed `fm`.
|
|
31
|
+
* @param {Readonly<Record<string, readonly {value: string}[]>>} look - The
|
|
32
|
+
* being-look vocabulary.
|
|
33
|
+
* @returns {object[]} Error findings, each located at the entry at fault.
|
|
34
|
+
*/
|
|
35
|
+
export function checkExtraFeatures(note: object, look: Readonly<Record<string, readonly {
|
|
36
|
+
value: string;
|
|
37
|
+
}[]>>): object[];
|
|
38
|
+
/**
|
|
39
|
+
* Hair and eye colour spellings the vocabulary does not take but an author
|
|
40
|
+
* writes, as regular-expression alternatives. Beside `hair` or `eyes` any of
|
|
41
|
+
* them, or any hair or eye colour word, states a colour.
|
|
42
|
+
*/
|
|
43
|
+
export const COLOUR_VARIANTS: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* Skin-condition phrasings that are not complexion words, as
|
|
46
|
+
* regular-expression alternatives. Any of them states a complexion.
|
|
47
|
+
*/
|
|
48
|
+
export const CONDITION_PHRASES: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* The terms that make an `extra_features` entry something other than a
|
|
51
|
+
* physical mark, by category, as case-insensitive whole-word patterns.
|
|
52
|
+
*
|
|
53
|
+
* `exemptMarks` lets an entry describing a tattoo, scar, brand or burn mark
|
|
54
|
+
* pass the category, so "a tattoo of a knight's helmet" stands while "wears a
|
|
55
|
+
* helmet" does not. The voice, impression, handedness, sterility and age
|
|
56
|
+
* categories admit no such exemption: "a beautiful tattoo of a crane" is
|
|
57
|
+
* refused.
|
|
58
|
+
*
|
|
59
|
+
* @type {ReadonlyArray<{rule: string, category: string, pattern: RegExp, exemptMarks: boolean}>}
|
|
60
|
+
*/
|
|
61
|
+
export const NON_PHYSICAL_FEATURES: ReadonlyArray<{
|
|
62
|
+
rule: string;
|
|
63
|
+
category: string;
|
|
64
|
+
pattern: RegExp;
|
|
65
|
+
exemptMarks: boolean;
|
|
66
|
+
}>;
|
|
@@ -60,7 +60,8 @@ export function mapEntriesOf(value: unknown): Array<[string, unknown]>;
|
|
|
60
60
|
*
|
|
61
61
|
* @param {readonly object[]} fields - The type's `data:` declaration.
|
|
62
62
|
* @returns {object[]} One spec per top-level key, a dotted group as a `map`
|
|
63
|
-
* whose `fields` are its members, with `grouped: true
|
|
63
|
+
* whose `fields` are its members, with `grouped: true`, and each member
|
|
64
|
+
* marked `kindChecked`.
|
|
64
65
|
*/
|
|
65
66
|
export function topLevelSpecs(fields: readonly object[]): object[];
|
|
66
67
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Preserve offsets while excluding literal examples from link scanning. */
|
|
2
|
-
export function maskMarkdownCode(source: any, spans?: boolean):
|
|
2
|
+
export function maskMarkdownCode(source: any, spans?: boolean): string;
|
|
3
3
|
/** GitHub heading slug, retaining separate spaces after punctuation removal. */
|
|
4
4
|
export function docHeadingSlug(value: any): any;
|
|
5
5
|
/** @param {string} source */
|
|
@@ -136,9 +136,9 @@ export function subTypes(type: string, vocabulary?: Readonly<Record<string, Type
|
|
|
136
136
|
*
|
|
137
137
|
* Each field is closed to its list: any other value, an empty one included, is
|
|
138
138
|
* a finding at the value naming the words it takes. `appearance.complexion`
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
139
|
+
* is a list of words, each entry checked on its own. `appearance.extra_features`
|
|
140
|
+
* is not here: it is a list of the physical marks no word covers, held to the
|
|
141
|
+
* rules of `appearance-features.mjs`.
|
|
142
142
|
*
|
|
143
143
|
* @type {Readonly<Record<string, readonly LookWord[]>>}
|
|
144
144
|
*/
|