@heroiclands/package-build 22.18.0 → 22.20.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 +19 -0
- package/README.md +1 -1
- package/ci/content-format-reference.mjs +31 -1
- package/docs/api.md +2 -1
- package/docs/authoring/frontmatter.md +6 -2
- package/docs/reference/format-details.md +69 -51
- package/engine/base-compiler.mjs +2 -2
- package/engine/frontmatter-lint.mjs +103 -1
- package/engine/infobox.mjs +6 -3
- package/engine/journals.mjs +25 -12
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-vocabulary.mjs +19 -0
- package/engine/pdf-build.mjs +3 -6
- package/engine/retired-fields.mjs +7 -18
- package/engine/site-build.mjs +0 -1
- package/package.json +2 -12
- package/types/engine/journals.d.mts +11 -10
- package/types/engine/note-frontmatter.d.mts +4 -0
- package/types/engine/note-vocabulary.d.mts +18 -0
- package/types/engine/retired-fields.d.mts +5 -11
- package/engine/documentation-links.mjs +0 -31
- package/types/engine/documentation-links.d.mts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Content checks require characters and NPCs to name an archetype and reject unrecognized archetypes; creatures may omit them.
|
|
8
|
+
- Names with unsupported fields now fail validation, and aliases appear beneath the name in infoboxes across published formats.
|
|
9
|
+
|
|
10
|
+
## 22.19.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
**Beings**
|
|
15
|
+
|
|
16
|
+
- Beings can be described as Commoners, Entertainers, or Guildspeople, and Commoner stands alone when no other archetype fits.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Books place infoboxes after each entry’s authored content, and Foundry journals place each infobox on its own page after the authored pages.
|
|
21
|
+
|
|
3
22
|
## 22.18.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The generated project includes a homepage, website and book targets, npm scripts
|
|
|
18
18
|
|
|
19
19
|
## Documentation
|
|
20
20
|
|
|
21
|
-
The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md).
|
|
21
|
+
The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md). This toolchain does not build its own site or book. A Hugo theme dependency would form a cycle because the theme uses package-build for releases.
|
|
22
22
|
|
|
23
23
|
| Topic | Guide |
|
|
24
24
|
| ------------------------------------------------- | ------------------------------------------------------------ |
|
|
@@ -7,7 +7,11 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { formatGenerated } from "../engine/format-generated.mjs";
|
|
9
9
|
import { loadContentFormat } from "../engine/content-format.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BEING_ARCHETYPES,
|
|
12
|
+
NOTE_VOCABULARY,
|
|
13
|
+
SHARED_DATA_FIELDS,
|
|
14
|
+
} from "../engine/note-vocabulary.mjs";
|
|
11
15
|
|
|
12
16
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
17
|
const output = path.join(root, "docs/reference/note-types.md");
|
|
@@ -104,3 +108,29 @@ if (process.argv.includes("--check")) {
|
|
|
104
108
|
} else {
|
|
105
109
|
process.stdout.write(result);
|
|
106
110
|
}
|
|
111
|
+
|
|
112
|
+
const detailsPath = path.join(root, "docs/reference/format-details.md");
|
|
113
|
+
const details = fs.readFileSync(detailsPath, "utf8");
|
|
114
|
+
if (!/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/.test(details)) {
|
|
115
|
+
throw new Error("The archetype reference requires its generation markers");
|
|
116
|
+
}
|
|
117
|
+
const archetypes = Object.entries(BEING_ARCHETYPES)
|
|
118
|
+
.map(([name, meaning]) => `- ${name}: ${meaning}`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
const updatedDetails = await formatGenerated(
|
|
121
|
+
details.replace(
|
|
122
|
+
/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
|
|
123
|
+
`<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
|
|
124
|
+
),
|
|
125
|
+
detailsPath,
|
|
126
|
+
);
|
|
127
|
+
if (process.argv.includes("--check")) {
|
|
128
|
+
if (details !== updatedDetails) {
|
|
129
|
+
console.error(
|
|
130
|
+
"docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype reference",
|
|
131
|
+
);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
fs.writeFileSync(detailsPath, updatedDetails);
|
|
136
|
+
}
|
package/docs/api.md
CHANGED
|
@@ -353,6 +353,7 @@ An addressed content note accepts exactly these top-level keys, in this order: `
|
|
|
353
353
|
| ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
354
354
|
| `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
|
|
355
355
|
| `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
|
|
356
|
+
| `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
|
|
356
357
|
| `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
|
|
357
358
|
| `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
|
|
358
359
|
| `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
|
|
@@ -417,7 +418,7 @@ The infobox declarations this toolchain ships, one per system, and the single ca
|
|
|
417
418
|
|
|
418
419
|
### `engine.infoboxRender`
|
|
419
420
|
|
|
420
|
-
Drawing a declared infobox: HTML for
|
|
421
|
+
Drawing a declared infobox: HTML for separate Foundry pages appended after authored pages, Typst panels after each note’s authored body in the book. The website's boxes travel in front matter and the site theme draws them, so there is no renderer for it here.
|
|
421
422
|
|
|
422
423
|
| Export | Signature | Returns | Use it when |
|
|
423
424
|
| ---------------------- | ------------------------------------- | --------- | ------------------------------------------------------------------------ |
|
|
@@ -21,7 +21,9 @@ data: {}
|
|
|
21
21
|
---
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`name
|
|
24
|
+
`name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names. The note infobox lists aliases directly beneath the full name. For a `being` with `subType: character`, `given` and `clan` are optional name components. The first alias of a character or NPC is its nickname. Omit optional keys when they do not apply; `given`, `clan`, and alias entries must be nonempty strings when present. No other keys are accepted under `name`.
|
|
25
|
+
|
|
26
|
+
`description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
|
|
25
27
|
|
|
26
28
|
Use `tags: [gm]` for a note intended only for the GM. It is absent from the public website and book. Foundry includes it only when its document routes to a pack with `private: true`. A note with `gm` that also creates a prose JournalEntry needs a private JournalEntry pack. Links from untagged notes to GM notes are errors; GM notes may link to one another.
|
|
27
29
|
|
|
@@ -31,10 +33,12 @@ The formatter keeps a collection on one line when the full line is under 100 cha
|
|
|
31
33
|
|
|
32
34
|
The [note-type reference](../reference/note-types.md) lists every field accepted under `data`, including the shared `id`, `pack`, `packFolder`, `harnworld`, `icon`, and `banner` fields. A type such as `being` also has its own fields, including `culture`, `homes`, and `affiliations`. A field absent from a type's vocabulary is a lint error; an unrelated field is not silently repurposed.
|
|
33
35
|
|
|
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
|
+
|
|
34
38
|
A `sohl:` or `hm3:` 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.
|
|
35
39
|
|
|
36
40
|
One body describes the subject for both systems. A being's `{#appearance}` and `{#dossier}` sections feed SoHL Actor `system.appearance` and `system.dossier`, and HM3 Actor `system.description` and `system.biography`. An Item's `{#description}` page becomes a shared JournalEntryPage. Its resolved `@UUID[...]` pointer is stored in SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item with no `{#description}` anchor uses its first page. Other H1 pages remain linkable.
|
|
37
41
|
|
|
38
42
|
## Publication and summaries
|
|
39
43
|
|
|
40
|
-
A note infobox draws from shared `data`. A being profile shows its top-level `subType` as `Type: Character`, `Type: NPC`, or `Type: Creature`. A system infobox draws from the system's fields, and a mapped system with no document says “Not available.” The same authored body appears in Foundry, on the website, and in the book;
|
|
44
|
+
A note infobox draws from shared `data`. A being profile shows its top-level `subType` as `Type: Character`, `Type: NPC`, or `Type: Creature`. A system infobox draws from the system's fields, and a mapped system with no document says “Not available.” The same authored body appears in Foundry, on the website, and in the book; the book places boxes after the authored body, Foundry appends one page per box, and the website places boxes in its responsive side rail. [Infobox details](../reference/format-details.md#the-infobox) describe field order and empty values.
|
|
@@ -11,10 +11,12 @@ subType: reference
|
|
|
11
11
|
|
|
12
12
|
Markdown generates a JournalEntry: content before the first H1 becomes its "Introduction" page, and each H1 begins another page. The document Address is `<package>-note-<note_type>-<shortcode>`.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Each emitted infobox becomes a separate page at the end of the JournalEntry,
|
|
15
|
+
after every authored page. The page names are **Properties Infobox**,
|
|
16
|
+
**SoHL Infobox**, and **HM3 Infobox**, in that order where emitted. Generated
|
|
17
|
+
page ids derive from the entry and box identity, separately from authored
|
|
18
|
+
heading names and anchors. An authored heading may share an infobox page name.
|
|
19
|
+
What the boxes hold is [the infobox](#the-infobox) below.
|
|
18
20
|
|
|
19
21
|
**There are two kinds of infobox.**
|
|
20
22
|
|
|
@@ -62,16 +64,13 @@ For web pages, the full Markdown body renders as HTML with its infoboxes.
|
|
|
62
64
|
|
|
63
65
|
### The infobox
|
|
64
66
|
|
|
65
|
-
**The
|
|
66
|
-
prose.** It is not a floating sidebar and it is not defined in a rendering
|
|
67
|
-
template. The toolchain settles what each box holds; each medium lays that out
|
|
68
|
-
its own way.
|
|
67
|
+
**The toolchain settles what each box holds; each medium places it.**
|
|
69
68
|
|
|
70
|
-
| medium | placement
|
|
71
|
-
| ------- |
|
|
72
|
-
| book | flows in the column measure, breaking between sections | none
|
|
73
|
-
| website | side rail on wide screens, inline on narrow
|
|
74
|
-
| Foundry |
|
|
69
|
+
| medium | placement | collapsing |
|
|
70
|
+
| ------- | ------------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
71
|
+
| book | after the note's authored body, before the next entry; flows in the column measure, breaking between sections | none |
|
|
72
|
+
| website | side rail on wide screens, inline on narrow screens | `<details>`, default open |
|
|
73
|
+
| Foundry | one page per emitted box, after all authored pages | `<details>`, default open |
|
|
75
74
|
|
|
76
75
|
Two columns are print-only. A scrolling page has no fixed viewport, so columns
|
|
77
76
|
make a reader travel down and back up, and a narrow screen collapses to one
|
|
@@ -79,8 +78,8 @@ column regardless.
|
|
|
79
78
|
|
|
80
79
|
**Six rules hold in every medium.**
|
|
81
80
|
|
|
82
|
-
1. **
|
|
83
|
-
|
|
81
|
+
1. **Placement belongs to the medium**, as described above. Authored images
|
|
82
|
+
remain in the body in their authored order.
|
|
84
83
|
2. **It contains no image.** A picture is authored in the text with its own
|
|
85
84
|
directive, and its position governs.
|
|
86
85
|
3. **A section is the unit that flows.** Sections are whole and unbreakable and
|
|
@@ -118,7 +117,8 @@ Item link.
|
|
|
118
117
|
order** — the very table each `### type:` section below states. There is no
|
|
119
118
|
second list, so a key added to a type appears in the book, on the website and
|
|
120
119
|
in a compendium journal with no further edit. A `Name` row stands first on
|
|
121
|
-
every note, because every note has one.
|
|
120
|
+
every note, because every note has one. An `Aliases` row follows it when the
|
|
121
|
+
note names any aliases, preserving their authored order.
|
|
122
122
|
|
|
123
123
|
A few keys carry no row, and the reason in each case is one of exactly two —
|
|
124
124
|
the key is **machinery**, steering a build or an interface rather than
|
|
@@ -334,6 +334,13 @@ sohl: {}
|
|
|
334
334
|
Any other top-level key is an error. To keep an author-only note beside the
|
|
335
335
|
content, comment out its YAML lines with `#`. Comments remain valid YAML text
|
|
336
336
|
and do not become data in the JSONL index, generated pages, or Foundry documents.
|
|
337
|
+
|
|
338
|
+
`name` is a map with a required nonempty `full` string when present. It may
|
|
339
|
+
also contain `aliases`, an ordered list of nonempty strings. A being with
|
|
340
|
+
`subType: character` may additionally contain nonempty `given` and `clan`
|
|
341
|
+
strings. Other keys under `name` are errors. For a character or NPC, the first
|
|
342
|
+
alias is its nickname. A being's homes belong in `data.homes`.
|
|
343
|
+
|
|
337
344
|
There is no custom site metadata. `description` supplies the page's short
|
|
338
345
|
description; `tags` carries publication and classification tags.
|
|
339
346
|
|
|
@@ -442,16 +449,16 @@ surface:
|
|
|
442
449
|
> **Draft.** This entry is unfinished. What it states may change, and nothing in
|
|
443
450
|
> it is settled.
|
|
444
451
|
|
|
445
|
-
It leads the entry
|
|
452
|
+
It leads the entry, before the authored content, because a reader
|
|
446
453
|
deciding whether to rely on an entry has to be told before they read it. Where
|
|
447
454
|
it lands:
|
|
448
455
|
|
|
449
|
-
| Surface | What carries it
|
|
450
|
-
| ------------ |
|
|
451
|
-
| JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page
|
|
452
|
-
| Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography`
|
|
453
|
-
| The book | a drawn mark and a left rule between the entry's plate and its
|
|
454
|
-
| The website | the theme's own notice, above the infobox rail and the prose
|
|
456
|
+
| Surface | What carries it |
|
|
457
|
+
| ------------ | ---------------------------------------------------------------------------- |
|
|
458
|
+
| JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page |
|
|
459
|
+
| Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography` |
|
|
460
|
+
| The book | a drawn mark and a left rule between the entry's plate and its authored body |
|
|
461
|
+
| The website | the theme's own notice, above the infobox rail and the prose |
|
|
455
462
|
|
|
456
463
|
**An Actor carries it once.** A being's prose reaches two fields, and a document
|
|
457
464
|
saying the same thing twice teaches a reader to skip it — a printed sheet draws
|
|
@@ -2433,6 +2440,8 @@ For JournalEntries, the following rules apply:
|
|
|
2433
2440
|
|
|
2434
2441
|
**Archetypes** describe characters in broad terms and help match them to an adventure. The list is:
|
|
2435
2442
|
|
|
2443
|
+
<!-- archetypes:start -->
|
|
2444
|
+
|
|
2436
2445
|
- warrior: Can hold a line and win a fight.
|
|
2437
2446
|
- skirmisher: Fights light — ambush, missile, mobility.
|
|
2438
2447
|
- infiltrator: Gets in unseen — locks, stealth, disguise.
|
|
@@ -2445,8 +2454,17 @@ For JournalEntries, the following rules apply:
|
|
|
2445
2454
|
- mariner: Handles boats and blue water.
|
|
2446
2455
|
- artisan: Builds, repairs, and appraises craft work.
|
|
2447
2456
|
- trader: Moves goods, values them, and knows markets.
|
|
2457
|
+
- commoner: Fits no more specific archetype; handles ordinary work and daily life.
|
|
2458
|
+
- entertainer: Performs for an audience through acting, music, comedy, or similar arts.
|
|
2459
|
+
- guildsperson: Has professional training, standing, or connections that open doors.
|
|
2460
|
+
|
|
2461
|
+
<!-- archetypes:end -->
|
|
2462
|
+
|
|
2463
|
+
`character` and `npc` beings require at least one archetype. `creature` beings may omit archetypes or write an empty array. Values use the exact lowercase spellings above.
|
|
2448
2464
|
|
|
2449
2465
|
Note that archetypes are descriptive, not proscriptive, and a character may be described by multiple archetypes at once.
|
|
2466
|
+
`commoner` stands alone: a being with another fitting archetype does not also carry `commoner`.
|
|
2467
|
+
Guild membership alone does not require `guildsperson`; the being's training, standing, or connections must matter to the role.
|
|
2450
2468
|
|
|
2451
2469
|
#### Items
|
|
2452
2470
|
|
|
@@ -2458,34 +2476,34 @@ An Item can mark its readable description with `# ... {#description}`. That head
|
|
|
2458
2476
|
|
|
2459
2477
|
Generates a living (or undead, or spirit) being.
|
|
2460
2478
|
|
|
2461
|
-
| `data` property | Values | Description
|
|
2462
|
-
| --------------------------- | ---------------------------------------------- |
|
|
2463
|
-
| `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon`
|
|
2464
|
-
| `templatePriority` | `number` | Template priority, _null_ = not a template
|
|
2465
|
-
| `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** —
|
|
2466
|
-
| `occupation` | `string` | Name of the character's occupation
|
|
2467
|
-
| `social` | `Map<string, unknown>` | The being's social profile
|
|
2468
|
-
| `stations` | `Address[]` | Name of the stations the character belongs to
|
|
2469
|
-
| `lore` | `Address[]` | Other lore concerning this being, such as social standing or law
|
|
2470
|
-
| `culture` | `Address` | Primary culture, naming a lore note with `subType: culture`
|
|
2471
|
-
| `homes` | `Address[]` | Place the being calls home
|
|
2472
|
-
| `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc)
|
|
2473
|
-
| `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others
|
|
2474
|
-
| `gender` | `male \| female \| other` | Gender of the character
|
|
2475
|
-
| `species` | `Address` | Being's species (lore)
|
|
2476
|
-
| `born` | `date \| unknown` | When the being was born; absent, it was never born
|
|
2477
|
-
| `calendar` | `Address` | Calendar note used to print the being's dates
|
|
2478
|
-
| `died` | `date \| unknown` | When the being died; absent, it is alive
|
|
2479
|
-
| `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate
|
|
2480
|
-
| `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this
|
|
2481
|
-
| `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches
|
|
2482
|
-
| `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds
|
|
2483
|
-
| `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size
|
|
2484
|
-
| `appearance.eye_color` | `string` | Eye color
|
|
2485
|
-
| `appearance.hair_color` | `string` | Hair color
|
|
2486
|
-
| `appearance.skin_color` | `string` | Skin color
|
|
2487
|
-
| `appearance.complexion` | `string` | Complexion
|
|
2488
|
-
| `appearance.extra_features` | `string[]` | Extra features
|
|
2479
|
+
| `data` property | Values | Description |
|
|
2480
|
+
| --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2481
|
+
| `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
|
|
2482
|
+
| `templatePriority` | `number` | Template priority, _null_ = not a template |
|
|
2483
|
+
| `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — character and npc require at least one; creature may omit it or write `[]`; `null` is an error. |
|
|
2484
|
+
| `occupation` | `string` | Name of the character's occupation |
|
|
2485
|
+
| `social` | `Map<string, unknown>` | The being's social profile |
|
|
2486
|
+
| `stations` | `Address[]` | Name of the stations the character belongs to |
|
|
2487
|
+
| `lore` | `Address[]` | Other lore concerning this being, such as social standing or law |
|
|
2488
|
+
| `culture` | `Address` | Primary culture, naming a lore note with `subType: culture` |
|
|
2489
|
+
| `homes` | `Address[]` | Place the being calls home |
|
|
2490
|
+
| `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
|
|
2491
|
+
| `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
|
|
2492
|
+
| `gender` | `male \| female \| other` | Gender of the character |
|
|
2493
|
+
| `species` | `Address` | Being's species (lore) |
|
|
2494
|
+
| `born` | `date \| unknown` | When the being was born; absent, it was never born |
|
|
2495
|
+
| `calendar` | `Address` | Calendar note used to print the being's dates |
|
|
2496
|
+
| `died` | `date \| unknown` | When the being died; absent, it is alive |
|
|
2497
|
+
| `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate |
|
|
2498
|
+
| `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
|
|
2499
|
+
| `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches |
|
|
2500
|
+
| `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds |
|
|
2501
|
+
| `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
|
|
2502
|
+
| `appearance.eye_color` | `string` | Eye color |
|
|
2503
|
+
| `appearance.hair_color` | `string` | Hair color |
|
|
2504
|
+
| `appearance.skin_color` | `string` | Skin color |
|
|
2505
|
+
| `appearance.complexion` | `string` | Complexion |
|
|
2506
|
+
| `appearance.extra_features` | `string[]` | Extra features |
|
|
2489
2507
|
|
|
2490
2508
|
Author `data.height` as a string of decimal metres (`1.91m` or `1.91 m`) or
|
|
2491
2509
|
whole feet with optional inches (`6'`, `6'3"`, or `6' 3"`). Inches must be
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -1133,8 +1133,8 @@ export class BasePackCompiler {
|
|
|
1133
1133
|
// - `aliases:`: it fed the alias index, which the bare
|
|
1134
1134
|
// `[[Alias]]` form was looked up in; the form is retired, so the
|
|
1135
1135
|
// list has no reader left. The nested `name.aliases` is a
|
|
1136
|
-
// different field
|
|
1137
|
-
//
|
|
1136
|
+
// different field: the infobox displays it, and the note name
|
|
1137
|
+
// check validates it.
|
|
1138
1138
|
//
|
|
1139
1139
|
// Both are reported and counted — never skipped, which is how a
|
|
1140
1140
|
// tree naming a package nothing answers to used to compile zero
|
|
@@ -55,7 +55,12 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { isAddressTuple } from "./address.mjs";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
authoredNoteKeys,
|
|
60
|
+
CHARACTER_NAME_KEYS,
|
|
61
|
+
NOTE_NAME_KEYS,
|
|
62
|
+
NOTE_TOP_LEVEL_KEY_SET,
|
|
63
|
+
} from "./note-frontmatter.mjs";
|
|
59
64
|
import { AddressEntries } from "./address-values.mjs";
|
|
60
65
|
import { addressPositions } from "./note-addresses.mjs";
|
|
61
66
|
import { authoredFields, readsLegacyKey } from "./field-spec.mjs";
|
|
@@ -78,6 +83,7 @@ import { isAddressSegment } from "./address-charset.mjs";
|
|
|
78
83
|
// reads is exactly the disagreement to avoid.
|
|
79
84
|
import { DEFAULT_PARENT } from "./folder-notes.mjs";
|
|
80
85
|
import {
|
|
86
|
+
BEING_ARCHETYPES,
|
|
81
87
|
dataFields,
|
|
82
88
|
declaredTags,
|
|
83
89
|
subTypeCharsetMessage,
|
|
@@ -1173,6 +1179,102 @@ export function lintNote(
|
|
|
1173
1179
|
const type = String(fm.type ?? "");
|
|
1174
1180
|
const raw = () => note.raw ?? "";
|
|
1175
1181
|
const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
|
|
1182
|
+
if (type === "being") {
|
|
1183
|
+
const archetypes = fm.data?.archetypes;
|
|
1184
|
+
if (
|
|
1185
|
+
["character", "npc"].includes(fm.subType) &&
|
|
1186
|
+
(archetypes == null || (Array.isArray(archetypes) && archetypes.length === 0))
|
|
1187
|
+
) {
|
|
1188
|
+
findings.push({
|
|
1189
|
+
file: note.file,
|
|
1190
|
+
...positionOfFrontmatterPath(
|
|
1191
|
+
raw(),
|
|
1192
|
+
archetypes == null ? ["subType"] : ["data", "archetypes"],
|
|
1193
|
+
),
|
|
1194
|
+
severity: "error",
|
|
1195
|
+
message: "A character or npc requires at least one `data.archetypes` value",
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (Array.isArray(archetypes)) {
|
|
1199
|
+
archetypes.forEach((value, i) => {
|
|
1200
|
+
if (!Object.hasOwn(BEING_ARCHETYPES, value)) {
|
|
1201
|
+
findings.push({
|
|
1202
|
+
file: note.file,
|
|
1203
|
+
...positionOfFrontmatterPath(raw(), ["data", "archetypes", i]),
|
|
1204
|
+
severity: "error",
|
|
1205
|
+
message: `Unknown archetype \`${String(value)}\`; use a case-sensitive archetype from the reference`,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
const commoner = archetypes.indexOf("commoner");
|
|
1210
|
+
if (commoner !== -1 && archetypes.length > 1) {
|
|
1211
|
+
findings.push({
|
|
1212
|
+
file: note.file,
|
|
1213
|
+
...positionOfFrontmatterPath(raw(), ["data", "archetypes", commoner]),
|
|
1214
|
+
severity: "error",
|
|
1215
|
+
message: "`data.archetypes` may contain `commoner` only by itself",
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (Object.hasOwn(fm, "name")) {
|
|
1221
|
+
const name = fm.name;
|
|
1222
|
+
const nameAt = (path) => positionOfFrontmatterPath(raw(), ["name", ...path], { key: true });
|
|
1223
|
+
if (!name || typeof name !== "object" || Array.isArray(name)) {
|
|
1224
|
+
findings.push({
|
|
1225
|
+
file: note.file,
|
|
1226
|
+
...positionOfFrontmatterPath(raw(), ["name"], { key: true }),
|
|
1227
|
+
severity: "error",
|
|
1228
|
+
message: "`name` must be a map with a nonempty `full` string",
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
const allowed = new Set(NOTE_NAME_KEYS);
|
|
1232
|
+
if (type === "being" && fm.subType === "character") {
|
|
1233
|
+
for (const key of CHARACTER_NAME_KEYS) allowed.add(key);
|
|
1234
|
+
}
|
|
1235
|
+
for (const key of Object.keys(name)) {
|
|
1236
|
+
if (!allowed.has(key)) {
|
|
1237
|
+
findings.push({
|
|
1238
|
+
file: note.file,
|
|
1239
|
+
...nameAt([key]),
|
|
1240
|
+
severity: "error",
|
|
1241
|
+
message: `\`name.${key}\` is not valid for this note`,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (typeof name.full !== "string" || !name.full.trim()) {
|
|
1246
|
+
findings.push({
|
|
1247
|
+
file: note.file,
|
|
1248
|
+
...(Object.hasOwn(name, "full") ? nameAt(["full"]) : nameAt([])),
|
|
1249
|
+
severity: "error",
|
|
1250
|
+
message: "`name.full` must be a nonempty string",
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (Object.hasOwn(name, "aliases")) {
|
|
1254
|
+
if (
|
|
1255
|
+
!Array.isArray(name.aliases) ||
|
|
1256
|
+
name.aliases.some((alias) => typeof alias !== "string" || !alias.trim())
|
|
1257
|
+
) {
|
|
1258
|
+
findings.push({
|
|
1259
|
+
file: note.file,
|
|
1260
|
+
...nameAt(["aliases"]),
|
|
1261
|
+
severity: "error",
|
|
1262
|
+
message: "`name.aliases` must be a list of nonempty strings",
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
for (const key of CHARACTER_NAME_KEYS) {
|
|
1267
|
+
if (!Object.hasOwn(name, key) || !allowed.has(key)) continue;
|
|
1268
|
+
if (typeof name[key] === "string" && name[key].trim()) continue;
|
|
1269
|
+
findings.push({
|
|
1270
|
+
file: note.file,
|
|
1271
|
+
...nameAt([key]),
|
|
1272
|
+
severity: "error",
|
|
1273
|
+
message: `\`name.${key}\` must be a nonempty string`,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1176
1278
|
/**
|
|
1177
1279
|
* The in-block keys this note's own type claims for something other than
|
|
1178
1280
|
* the note-level field of that name, which every note-level check below
|
package/engine/infobox.mjs
CHANGED
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
*
|
|
52
52
|
* ## Six rules that hold in every medium
|
|
53
53
|
*
|
|
54
|
-
* 1. **
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* 1. **Placement belongs to the medium.** The book puts boxes after the
|
|
55
|
+
* authored body, Foundry appends one page per box, and the website uses
|
|
56
|
+
* a side rail on wide screens and inline boxes on narrow screens.
|
|
57
57
|
* 2. **It contains no image.** A picture is authored in the text with its own
|
|
58
58
|
* directive, and its position governs. {@link NOTE_FIELD_PRESENTATION}
|
|
59
59
|
* withholds the art slots and `overlay` for that reason and no other.
|
|
@@ -706,6 +706,9 @@ function noteBox(
|
|
|
706
706
|
const shown = new Set();
|
|
707
707
|
const name = fm?.name?.full ?? fm?.title;
|
|
708
708
|
if (hasValue(name)) rows.push({ label: "Name", kind: "text", value: String(name) });
|
|
709
|
+
if (Array.isArray(fm?.name?.aliases) && fm.name.aliases.length) {
|
|
710
|
+
rows.push({ label: "Aliases", kind: "list", value: fm.name.aliases });
|
|
711
|
+
}
|
|
709
712
|
if (fm?.type === "being" && Object.hasOwn(BEING_SUBTYPE_LABELS, fm.subType)) {
|
|
710
713
|
rows.push({ label: "Type", kind: "text", value: BEING_SUBTYPE_LABELS[fm.subType] });
|
|
711
714
|
}
|
package/engine/journals.mjs
CHANGED
|
@@ -335,16 +335,13 @@ export function buildPages(rawPages, entryId, noteName, captions) {
|
|
|
335
335
|
* entry: a module may ship the same content for two systems, and each pack's
|
|
336
336
|
* documents record the system version they were built against. A
|
|
337
337
|
* caller with no pack in hand gets the package-wide block.
|
|
338
|
-
* @param {string} [params.
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* that reads. It is **not** a page of its own — a page is what a UUID
|
|
343
|
-
* addresses, and a summary a reader has to navigate to is a summary they do
|
|
344
|
-
* not see.
|
|
338
|
+
* @param {Array<{id: string, name: string, html: string}>} [params.infoboxes] -
|
|
339
|
+
* Emitted boxes, each rendered to HTML. Each becomes a separate page after
|
|
340
|
+
* the authored pages. Its identity uses the box id in a generated-page
|
|
341
|
+
* namespace, independent of authored headings and anchors.
|
|
345
342
|
* @param {string} [params.notice] - A statement about the entry rather than
|
|
346
343
|
* about its subject, already rendered to HTML — see {@link
|
|
347
|
-
* module:engine/draft-notice}. It leads the first page,
|
|
344
|
+
* module:engine/draft-notice}. It leads the first authored page,
|
|
348
345
|
* because a reader deciding whether to rely on the entry has to be told
|
|
349
346
|
* before they read it rather than after.
|
|
350
347
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
@@ -357,13 +354,21 @@ export function buildJournalEntry({
|
|
|
357
354
|
folder = null,
|
|
358
355
|
flags,
|
|
359
356
|
stats = defaultStats(),
|
|
360
|
-
|
|
357
|
+
infoboxes = [],
|
|
361
358
|
notice = "",
|
|
362
359
|
}) {
|
|
363
360
|
const rawPages = splitPages(markdown, leadName);
|
|
364
361
|
const pages = buildPages(rawPages, id, name, scanCaptions(markdown).captions);
|
|
365
|
-
|
|
366
|
-
|
|
362
|
+
for (const box of infoboxes) {
|
|
363
|
+
const pageId = makeId("journal-infobox-page", `${id}:${box.id}`);
|
|
364
|
+
pages.push({
|
|
365
|
+
_id: pageId,
|
|
366
|
+
name: box.name,
|
|
367
|
+
type: "text",
|
|
368
|
+
title: { show: true, level: 1 },
|
|
369
|
+
text: { format: 1, content: box.html },
|
|
370
|
+
_key: `!journal.pages!${id}.${pageId}`,
|
|
371
|
+
});
|
|
367
372
|
}
|
|
368
373
|
if (notice.trim() && pages.length) {
|
|
369
374
|
pages[0].text.content = `${notice}\n${pages[0].text.content}`;
|
|
@@ -522,7 +527,15 @@ export class Journals extends BasePackCompiler {
|
|
|
522
527
|
id,
|
|
523
528
|
name,
|
|
524
529
|
markdown,
|
|
525
|
-
|
|
530
|
+
infoboxes: boxes.map((box) => ({
|
|
531
|
+
id: box.id,
|
|
532
|
+
name: `${
|
|
533
|
+
box.id === "note" ? "Properties"
|
|
534
|
+
: box.id === "sohl" ? "SoHL"
|
|
535
|
+
: "HM3"
|
|
536
|
+
} Infobox`,
|
|
537
|
+
html: infoboxesToHtml([box], { link: linkToUuid }),
|
|
538
|
+
})),
|
|
526
539
|
// Whether the entry is settled, asked of the tag through the one
|
|
527
540
|
// reader of it. This sits here rather than in `buildJournalEntry`
|
|
528
541
|
// for the reason `skipNote` runs first: a note with no prose
|
|
@@ -24,6 +24,12 @@ export const NOTE_TOP_LEVEL_KEYS = Object.freeze([
|
|
|
24
24
|
/** Membership test for the top-level vocabulary. */
|
|
25
25
|
export const NOTE_TOP_LEVEL_KEY_SET = new Set(NOTE_TOP_LEVEL_KEYS);
|
|
26
26
|
|
|
27
|
+
/** The name properties every note accepts, in display order. */
|
|
28
|
+
export const NOTE_NAME_KEYS = Object.freeze(["full", "aliases"]);
|
|
29
|
+
|
|
30
|
+
/** Additional name components accepted by an individual character. */
|
|
31
|
+
export const CHARACTER_NAME_KEYS = Object.freeze(["given", "clan"]);
|
|
32
|
+
|
|
27
33
|
/** Return authored keys from an opening YAML frontmatter map. */
|
|
28
34
|
export function authoredNoteKeys(markdown) {
|
|
29
35
|
const source = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
@@ -666,6 +666,25 @@ export function isGmNote(fm) {
|
|
|
666
666
|
return hasTag(fm, GM_TAG);
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/** The case-sensitive archetypes and their author-facing meanings. */
|
|
670
|
+
export const BEING_ARCHETYPES = Object.freeze({
|
|
671
|
+
warrior: "Can hold a line and win a fight.",
|
|
672
|
+
skirmisher: "Fights light — ambush, missile, mobility.",
|
|
673
|
+
infiltrator: "Gets in unseen — locks, stealth, disguise.",
|
|
674
|
+
mage: "Commands arcane practice.",
|
|
675
|
+
cleric: "Commands religious practice and standing.",
|
|
676
|
+
healer: "Treats wounds and illness.",
|
|
677
|
+
scholar: "Reads, researches, and knows things.",
|
|
678
|
+
courtier: "Navigates rank, negotiation, and intrigue.",
|
|
679
|
+
woodsman: "Travels and survives wild country.",
|
|
680
|
+
mariner: "Handles boats and blue water.",
|
|
681
|
+
artisan: "Builds, repairs, and appraises craft work.",
|
|
682
|
+
trader: "Moves goods, values them, and knows markets.",
|
|
683
|
+
commoner: "Fits no more specific archetype; handles ordinary work and daily life.",
|
|
684
|
+
entertainer: "Performs for an audience through acting, music, comedy, or similar arts.",
|
|
685
|
+
guildsperson: "Has professional training, standing, or connections that open doors.",
|
|
686
|
+
});
|
|
687
|
+
|
|
669
688
|
/**
|
|
670
689
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
671
690
|
* declares.
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -639,9 +639,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
639
639
|
anchorPrefix,
|
|
640
640
|
captions: numberedCaptions,
|
|
641
641
|
});
|
|
642
|
-
//
|
|
643
|
-
// before the prose. An image the note authored ahead of it still comes
|
|
644
|
-
// first, because the image lives in the body and the body follows.
|
|
642
|
+
// Infoboxes follow the authored body within this note's entry.
|
|
645
643
|
const boxes = noteInfoboxes(page.fm, {
|
|
646
644
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
647
645
|
router: routerFor(resolved),
|
|
@@ -651,10 +649,9 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
651
649
|
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
652
650
|
});
|
|
653
651
|
// Whether the entry is settled comes before anything it says: the
|
|
654
|
-
// notice leads the leaf,
|
|
655
|
-
// holds on every other surface.
|
|
652
|
+
// notice leads the leaf, before the authored body.
|
|
656
653
|
const notice = isDraftNote(page.fm) ? draftNoticeTypst() : "";
|
|
657
|
-
const parts = [notice,
|
|
654
|
+
const parts = [notice, prose, panel].filter((part) => part);
|
|
658
655
|
return parts.join("\n\n");
|
|
659
656
|
};
|
|
660
657
|
|
|
@@ -48,13 +48,8 @@
|
|
|
48
48
|
* nothing checks unrecognized top-level keys, so left in place it would be
|
|
49
49
|
* silently ignored rather than reported.
|
|
50
50
|
*
|
|
51
|
-
* **`name.aliases`
|
|
52
|
-
*
|
|
53
|
-
* field here that is neither retired nor read. Nothing consults it: no index,
|
|
54
|
-
* no resolver, no lint rule, no emitter. A note carrying one compiles,
|
|
55
|
-
* resolves and emits exactly as if it were absent, and `tests/name-aliases-
|
|
56
|
-
* reserved.test.ts` pins that equivalence so a future reader cannot be added
|
|
57
|
-
* by accident.
|
|
51
|
+
* **`name.aliases` lists alternate names.** The note infobox shows them in
|
|
52
|
+
* authored order. They do not create addresses or change link resolution.
|
|
58
53
|
*
|
|
59
54
|
* **What `traits:` did.** It held a being's own description — gender,
|
|
60
55
|
* species, age, birth date, height, weight, frame and `appearance.*` — at the
|
|
@@ -199,10 +194,8 @@ export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
|
199
194
|
* unambiguous folded in every note's `name.full` and so dictated what a note
|
|
200
195
|
* could be named. The form is retired, so the list has no reader.
|
|
201
196
|
*
|
|
202
|
-
* **`name.aliases` is a different field
|
|
203
|
-
*
|
|
204
|
-
* and deliberately unmentioned by this message, which would otherwise tell an
|
|
205
|
-
* author to delete a field they are allowed to write. See
|
|
197
|
+
* **`name.aliases` is a different field.** It lists alternate names in the
|
|
198
|
+
* infobox and is deliberately unmentioned by this message. See
|
|
206
199
|
* {@link assertNoAliasesField}.
|
|
207
200
|
*
|
|
208
201
|
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
@@ -227,13 +220,9 @@ export function aliasesRetiredMessage(file) {
|
|
|
227
220
|
* it reads as "this note claims no other names", a statement about a namespace
|
|
228
221
|
* that no longer exists.
|
|
229
222
|
*
|
|
230
|
-
* **The nested `name.aliases` is deliberately not refused.**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* does not exist yet. So it is neither refused nor read — no index consults it,
|
|
234
|
-
* no rule validates its contents, nothing derives from it, and nothing emits
|
|
235
|
-
* it. It rides in the note as inert data, and a note carrying one compiles,
|
|
236
|
-
* resolves and emits exactly as if it were absent.
|
|
223
|
+
* **The nested `name.aliases` is deliberately not refused.** It is the note's
|
|
224
|
+
* ordered alternate names and appears in the note infobox. It does not create
|
|
225
|
+
* addresses or change how authored links resolve.
|
|
237
226
|
*
|
|
238
227
|
* That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
|
|
239
228
|
* rather than resolving a dotted key: the top-level field is the whole subject,
|
package/engine/site-build.mjs
CHANGED
|
@@ -984,7 +984,6 @@ export function renderPages(pages, options) {
|
|
|
984
984
|
*/
|
|
985
985
|
const SITE_PASSES = Object.freeze({
|
|
986
986
|
sohlKb: () => require("../sohl/kb-passes.mjs").sohlKbPass,
|
|
987
|
-
packageDocs: () => require("./documentation-links.mjs").documentationLinksPass,
|
|
988
987
|
});
|
|
989
988
|
|
|
990
989
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.20.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",
|
|
@@ -174,7 +174,6 @@
|
|
|
174
174
|
},
|
|
175
175
|
"devDependencies": {
|
|
176
176
|
"@changesets/cli": "^3.0.0",
|
|
177
|
-
"@heroiclands/hugo-theme": "^0.9.0",
|
|
178
177
|
"@types/node": "^26.2.0",
|
|
179
178
|
"npm-run-all": "^4.1.5",
|
|
180
179
|
"vitest": "^5.0.0"
|
|
@@ -198,16 +197,7 @@
|
|
|
198
197
|
"changeset": "changeset",
|
|
199
198
|
"changeset:version": "changeset version && node bin/package-build.mjs changelog group && npm install --package-lock-only",
|
|
200
199
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
201
|
-
"build:deps": "node bin/package-build.mjs deps fetch"
|
|
202
|
-
"build:content-index": "node bin/package-build.mjs content-index",
|
|
203
|
-
"build:site-content": "node bin/package-build.mjs site",
|
|
204
|
-
"build:site-html": "hugo --source build/hugo --minify --gc --cleanDestinationDir",
|
|
205
|
-
"build:site-root": "node bin/package-build.mjs site-root",
|
|
206
|
-
"build:site": "run-s build:deps build:content-index build:site-content build:site-html build:site-root",
|
|
207
|
-
"build:book": "run-s build:deps build:content-index build:book-content",
|
|
208
|
-
"build:book-content": "node bin/package-build.mjs pdf",
|
|
209
|
-
"serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
|
|
210
|
-
"serve:site-html": "hugo server --source build/hugo"
|
|
200
|
+
"build:deps": "node bin/package-build.mjs deps fetch"
|
|
211
201
|
},
|
|
212
202
|
"engines": {
|
|
213
203
|
"node": ">=24.0.0"
|
|
@@ -139,21 +139,18 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
|
|
|
139
139
|
* entry: a module may ship the same content for two systems, and each pack's
|
|
140
140
|
* documents record the system version they were built against. A
|
|
141
141
|
* caller with no pack in hand gets the package-wide block.
|
|
142
|
-
* @param {string} [params.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* that reads. It is **not** a page of its own — a page is what a UUID
|
|
147
|
-
* addresses, and a summary a reader has to navigate to is a summary they do
|
|
148
|
-
* not see.
|
|
142
|
+
* @param {Array<{id: string, name: string, html: string}>} [params.infoboxes] -
|
|
143
|
+
* Emitted boxes, each rendered to HTML. Each becomes a separate page after
|
|
144
|
+
* the authored pages. Its identity uses the box id in a generated-page
|
|
145
|
+
* namespace, independent of authored headings and anchors.
|
|
149
146
|
* @param {string} [params.notice] - A statement about the entry rather than
|
|
150
147
|
* about its subject, already rendered to HTML — see {@link
|
|
151
|
-
* module:engine/draft-notice}. It leads the first page,
|
|
148
|
+
* module:engine/draft-notice}. It leads the first authored page,
|
|
152
149
|
* because a reader deciding whether to rely on the entry has to be told
|
|
153
150
|
* before they read it rather than after.
|
|
154
151
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
155
152
|
*/
|
|
156
|
-
export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats,
|
|
153
|
+
export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infoboxes, notice, }: {
|
|
157
154
|
id: string;
|
|
158
155
|
name: string;
|
|
159
156
|
markdown: string;
|
|
@@ -161,7 +158,11 @@ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags,
|
|
|
161
158
|
folder?: string | null | undefined;
|
|
162
159
|
flags?: object | undefined;
|
|
163
160
|
stats?: object | undefined;
|
|
164
|
-
|
|
161
|
+
infoboxes?: {
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
html: string;
|
|
165
|
+
}[] | undefined;
|
|
165
166
|
notice?: string | undefined;
|
|
166
167
|
}): object;
|
|
167
168
|
/**
|
|
@@ -4,3 +4,7 @@ export function authoredNoteKeys(markdown: any): string[];
|
|
|
4
4
|
export const NOTE_TOP_LEVEL_KEYS: readonly string[];
|
|
5
5
|
/** Membership test for the top-level vocabulary. */
|
|
6
6
|
export const NOTE_TOP_LEVEL_KEY_SET: Set<string>;
|
|
7
|
+
/** The name properties every note accepts, in display order. */
|
|
8
|
+
export const NOTE_NAME_KEYS: readonly string[];
|
|
9
|
+
/** Additional name components accepted by an individual character. */
|
|
10
|
+
export const CHARACTER_NAME_KEYS: readonly string[];
|
|
@@ -244,6 +244,24 @@ export const DECLARED_TAGS: Readonly<{
|
|
|
244
244
|
tags: readonly string[];
|
|
245
245
|
}>;
|
|
246
246
|
}>;
|
|
247
|
+
/** The case-sensitive archetypes and their author-facing meanings. */
|
|
248
|
+
export const BEING_ARCHETYPES: Readonly<{
|
|
249
|
+
warrior: "Can hold a line and win a fight.";
|
|
250
|
+
skirmisher: "Fights light — ambush, missile, mobility.";
|
|
251
|
+
infiltrator: "Gets in unseen — locks, stealth, disguise.";
|
|
252
|
+
mage: "Commands arcane practice.";
|
|
253
|
+
cleric: "Commands religious practice and standing.";
|
|
254
|
+
healer: "Treats wounds and illness.";
|
|
255
|
+
scholar: "Reads, researches, and knows things.";
|
|
256
|
+
courtier: "Navigates rank, negotiation, and intrigue.";
|
|
257
|
+
woodsman: "Travels and survives wild country.";
|
|
258
|
+
mariner: "Handles boats and blue water.";
|
|
259
|
+
artisan: "Builds, repairs, and appraises craft work.";
|
|
260
|
+
trader: "Moves goods, values them, and knows markets.";
|
|
261
|
+
commoner: "Fits no more specific archetype; handles ordinary work and daily life.";
|
|
262
|
+
entertainer: "Performs for an audience through acting, music, comedy, or similar arts.";
|
|
263
|
+
guildsperson: "Has professional training, standing, or connections that open doors.";
|
|
264
|
+
}>;
|
|
247
265
|
/**
|
|
248
266
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
249
267
|
* declares.
|
|
@@ -49,10 +49,8 @@ export function assertNoDraftField(fm: object | null | undefined, { file, absPat
|
|
|
49
49
|
* unambiguous folded in every note's `name.full` and so dictated what a note
|
|
50
50
|
* could be named. The form is retired, so the list has no reader.
|
|
51
51
|
*
|
|
52
|
-
* **`name.aliases` is a different field
|
|
53
|
-
*
|
|
54
|
-
* and deliberately unmentioned by this message, which would otherwise tell an
|
|
55
|
-
* author to delete a field they are allowed to write. See
|
|
52
|
+
* **`name.aliases` is a different field.** It lists alternate names in the
|
|
53
|
+
* infobox and is deliberately unmentioned by this message. See
|
|
56
54
|
* {@link assertNoAliasesField}.
|
|
57
55
|
*
|
|
58
56
|
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
@@ -68,13 +66,9 @@ export function aliasesRetiredMessage(file?: string): string;
|
|
|
68
66
|
* it reads as "this note claims no other names", a statement about a namespace
|
|
69
67
|
* that no longer exists.
|
|
70
68
|
*
|
|
71
|
-
* **The nested `name.aliases` is deliberately not refused.**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* does not exist yet. So it is neither refused nor read — no index consults it,
|
|
75
|
-
* no rule validates its contents, nothing derives from it, and nothing emits
|
|
76
|
-
* it. It rides in the note as inert data, and a note carrying one compiles,
|
|
77
|
-
* resolves and emits exactly as if it were absent.
|
|
69
|
+
* **The nested `name.aliases` is deliberately not refused.** It is the note's
|
|
70
|
+
* ordered alternate names and appears in the note infobox. It does not create
|
|
71
|
+
* addresses or change how authored links resolve.
|
|
78
72
|
*
|
|
79
73
|
* That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
|
|
80
74
|
* rather than resolving a dotted key: the top-level field is the whole subject,
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
-
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
import { parseMarkdownFile } from "./helpers.mjs";
|
|
7
|
-
|
|
8
|
-
const LOCAL_MARKDOWN_LINK = /\[([^\]\n]+)\]\(([^\s)]+\.md(?:#[^\s)]*)?)\)/g;
|
|
9
|
-
|
|
10
|
-
/** Keep repository-relative documentation links usable on addressed pages. */
|
|
11
|
-
export function documentationLinksPass({ config, book = false }) {
|
|
12
|
-
const root = config.paths.content;
|
|
13
|
-
const cache = new Map();
|
|
14
|
-
return {
|
|
15
|
-
beforeLinks(text, page) {
|
|
16
|
-
return text.replace(LOCAL_MARKDOWN_LINK, (whole, label, target) => {
|
|
17
|
-
const [fileName, fragment] = target.split("#", 2);
|
|
18
|
-
const file = path.resolve(path.dirname(page.file), fileName);
|
|
19
|
-
if (!file.startsWith(`${root}${path.sep}`) || !fs.existsSync(file)) return whole;
|
|
20
|
-
let address = cache.get(file);
|
|
21
|
-
if (!address) {
|
|
22
|
-
const { frontmatter } = parseMarkdownFile(file);
|
|
23
|
-
if (!frontmatter?.type || !frontmatter?.shortcode) return whole;
|
|
24
|
-
address = `/${config.contentPackage}/${frontmatter.type}-${frontmatter.shortcode}/`;
|
|
25
|
-
cache.set(file, address);
|
|
26
|
-
}
|
|
27
|
-
return `[${label}](${address}${fragment && !book ? `#${fragment}` : ""})`;
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|