@heroiclands/package-build 22.28.0 → 23.0.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 +14 -0
- package/docs/api.md +1 -1
- package/docs/authoring/first-note.md +1 -1
- package/docs/content-format.md +1 -1
- package/docs/guides/site.md +4 -4
- package/docs/reference/format-details.md +51 -23
- package/docs/reference/note-types.md +5 -1
- package/docs/reference/theme-pages.md +32 -1
- package/engine/content-format.yaml +5 -0
- package/engine/literature-notes.mjs +133 -0
- package/engine/literature-works.mjs +153 -0
- package/engine/metadata-index.mjs +8 -0
- package/engine/note-vocabulary.mjs +12 -5
- package/engine/site-build.mjs +16 -1
- package/engine/stub-lint.mjs +6 -0
- package/hugo-theme/layouts/partials/literature-works.html +29 -0
- package/hugo-theme/layouts/partials/page-body.html +1 -0
- package/package.json +1 -1
- package/sohl/default-item-art.mjs +6 -6
- package/types/engine/literature-notes.d.mts +29 -0
- package/types/engine/literature-works.d.mts +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
Every page a work of literature names in its subjects lists that work under **In song and story**, with its form beside the title, including works from a package this one depends on.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
Affiliations, skills, attributes, mysteries, mystical abilities and miscellaneous gear without their own art show SoHL's default icons again instead of a broken image.
|
|
12
|
+
|
|
13
|
+
Folder notes need only a nonempty body: a short line or placeholder no longer warns or fails, with or without `draft`. An empty folder body is still an error.
|
|
14
|
+
|
|
15
|
+
Lore notes can be typed `literature` for epics, sagas, poems, legends and other works a people tells, sings or writes. A literature note may name its people, its form, its subjects and its language, and each appears in the note's infobox, with the people, subjects and language as links.
|
|
16
|
+
|
|
3
17
|
## 22.28.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/docs/api.md
CHANGED
|
@@ -471,7 +471,7 @@ What a draft note says about itself, in one sentence, on every surface it reache
|
|
|
471
471
|
|
|
472
472
|
### `engine.stubLint`
|
|
473
473
|
|
|
474
|
-
Every typed note must have a nonempty body. Empty and whitespace-only bodies are errors regardless of description or `draft` tag, including structural types such as `folder` and `homepage`. Placeholder-only bodies also produce errors. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing. The state classification helpers still describe incomplete records for inspection; their classifications do not exempt notes from validation.
|
|
474
|
+
Every typed note must have a nonempty body. Empty and whitespace-only bodies are errors regardless of description or `draft` tag, including structural types such as `folder` and `homepage`. Placeholder-only bodies also produce errors. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. A `folder` note is held only to the nonempty rule: any body, placeholder or short, passes with or without `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing. The state classification helpers still describe incomplete records for inspection; their classifications do not exempt notes from validation.
|
|
475
475
|
|
|
476
476
|
| Export | Signature | Returns | Use it when |
|
|
477
477
|
| ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
@@ -45,4 +45,4 @@ The full Address is `<package>-note-lore-harbor`; `<package>` comes from `conten
|
|
|
45
45
|
|
|
46
46
|
Add `description` for a short page summary, `tags: [draft]` to mark unfinished prose, or `data` for fields the note's type permits. A system block such as `sohl:` or `hm3:` adds mechanics and can produce a Foundry Actor or Item. It does not replace the shared prose or make another JournalEntry. [Frontmatter](frontmatter.md) explains the allowed keys, and the [note-type reference](../reference/note-types.md) lists every type-specific field.
|
|
47
47
|
|
|
48
|
-
Every typed note must have a nonempty body after its frontmatter. An empty or whitespace-only body is an error, even with `tags: [draft]` or a description. This includes folder and homepage notes. A nonempty body with fewer than 25 prose words produces a warning unless the note has a `draft` tag. See [note states](../reference/format-details.md#three-states-of-a-note).
|
|
48
|
+
Every typed note must have a nonempty body after its frontmatter. An empty or whitespace-only body is an error, even with `tags: [draft]` or a description. This includes folder and homepage notes. A nonempty body with fewer than 25 prose words produces a warning unless the note has a `draft` tag. A folder note needs only a nonempty body: any content passes, with or without the tag. See [note states](../reference/format-details.md#three-states-of-a-note).
|
package/docs/content-format.md
CHANGED
|
@@ -9,7 +9,7 @@ subType: userguide
|
|
|
9
9
|
|
|
10
10
|
A content package begins with Markdown notes under `assets/content/`. Each note has YAML frontmatter describing its identity and data, followed by prose. Folder names beneath `assets/content/` are for authors; `type`, `subType`, and Addresses determine what the build creates.
|
|
11
11
|
|
|
12
|
-
Every typed note requires a nonempty body, including drafts, folders and homepages. An empty or whitespace-only body is an error. A nonempty body with fewer than 25 prose words warns unless tagged `draft`; the tag never exempts an empty body.
|
|
12
|
+
Every typed note requires a nonempty body, including drafts, folders and homepages. An empty or whitespace-only body is an error. A nonempty body with fewer than 25 prose words warns unless tagged `draft`; the tag never exempts an empty body. A `folder` note needs only a nonempty body: any content passes, with or without `draft`.
|
|
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
|
|
package/docs/guides/site.md
CHANGED
|
@@ -144,12 +144,12 @@ The build always writes `title`, `slug`, `url`, `kbfolder`, `package` and
|
|
|
144
144
|
note. `infoboxes` is assembled from the note's own fields, so no note authors
|
|
145
145
|
it and nothing in the note format accepts it.
|
|
146
146
|
|
|
147
|
-
The build writes `resolvedDates`, `related`, `contains`, `governed_by`, `governed_places
|
|
148
|
-
and `map` only when it has something to write. A page with no links either way
|
|
147
|
+
The build writes `resolvedDates`, `related`, `contains`, `governed_by`, `governed_places`,
|
|
148
|
+
`works` and `map` only when it has something to write. A page with no links either way
|
|
149
149
|
carries no `related`, and a place with no drawing carries no `map`.
|
|
150
150
|
|
|
151
|
-
An authored `aliases`, `related`, `contains`, `governed_by`, `governed_places
|
|
152
|
-
dropped. Each states a fact about the whole tree rather than about one note, so
|
|
151
|
+
An authored `aliases`, `related`, `contains`, `governed_by`, `governed_places`, `works` or
|
|
152
|
+
`map` is dropped. Each states a fact about the whole tree rather than about one note, so
|
|
153
153
|
the build replaces whatever a note wrote.
|
|
154
154
|
|
|
155
155
|
Artwork addresses are rewritten in place under `data`. A page's `data.icon`,
|
|
@@ -837,7 +837,7 @@ and the whole of it is the suffix.
|
|
|
837
837
|
|
|
838
838
|
```markdown
|
|
839
839
|
 <!-- this package's -->
|
|
840
|
-
 <!-- the sohl package's -->
|
|
841
841
|
```
|
|
842
842
|
|
|
843
843
|
The four forms, for a `harnensemble` note (the `harnensemble` content package
|
|
@@ -850,14 +850,14 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
|
|
|
850
850
|
| **Web** | `https://cdn.heroiclands.org/harnensemble/images/map.webp` |
|
|
851
851
|
| **Book** | `assets/images/map.webp`, staged beside the Typst source |
|
|
852
852
|
|
|
853
|
-
And for the same note writing `sohl/assets/icons/noun/
|
|
853
|
+
And for the same note writing `sohl/assets/icons/noun/shieldnn.svg`, a file the
|
|
854
854
|
system ships and this repository does not hold:
|
|
855
855
|
|
|
856
856
|
| Surface | Address |
|
|
857
857
|
| ----------- | ------------------------------------------------------------- |
|
|
858
|
-
| **Foundry** | `systems/sohl/assets/icons/noun/
|
|
859
|
-
| **Local** | `assets/icons/noun/
|
|
860
|
-
| **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/
|
|
858
|
+
| **Foundry** | `systems/sohl/assets/icons/noun/shieldnn.svg` |
|
|
859
|
+
| **Local** | `assets/icons/noun/shieldnn.svg`, in the `sohl` repository |
|
|
860
|
+
| **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shieldnn.svg` |
|
|
861
861
|
| **Book** | not carried, unless a declared replacement carries it instead |
|
|
862
862
|
|
|
863
863
|
**The content package name and Foundry id are separate values.**
|
|
@@ -2010,7 +2010,9 @@ Every typed note requires a nonempty body after its closing frontmatter fence.
|
|
|
2010
2010
|
An empty or whitespace-only body is an error, regardless of description or
|
|
2011
2011
|
`tags: [draft]`. This includes `folder` and `homepage` notes. Untyped vault
|
|
2012
2012
|
scaffolding is excluded. A nonempty body with fewer than 25 prose words warns
|
|
2013
|
-
unless tagged `draft`; the tag never exempts an empty body.
|
|
2013
|
+
unless tagged `draft`; the tag never exempts an empty body. A `folder` note is
|
|
2014
|
+
held only to the nonempty rule: any body, a placeholder or a single line
|
|
2015
|
+
included, passes with or without `draft`.
|
|
2014
2016
|
|
|
2015
2017
|
The `lint`, `content-index`, `package compile`, `site` and `pdf` commands enforce
|
|
2016
2018
|
body errors. Write a short beginning and tag it `draft` when work is unfinished.
|
|
@@ -2055,7 +2057,7 @@ same.
|
|
|
2055
2057
|
| A lone heading | no | The same |
|
|
2056
2058
|
| `_To be written._` | no | The same |
|
|
2057
2059
|
|
|
2058
|
-
An HTML comment is deliberately not empty, but an invisible marker does not supply prose. Replace placeholder-only content with written text.
|
|
2060
|
+
An HTML comment is deliberately not empty, but an invisible marker does not supply prose. Replace placeholder-only content with written text. A `folder` note is the exception: any of these bodies satisfies it, and the lint reports nothing.
|
|
2059
2061
|
|
|
2060
2062
|
#### What a stub emits
|
|
2061
2063
|
|
|
@@ -2794,6 +2796,15 @@ An Item can mark its readable description with `# ... {#description}`. That head
|
|
|
2794
2796
|
|
|
2795
2797
|
Generates a living (or undead, or spirit) being.
|
|
2796
2798
|
|
|
2799
|
+
**subType**:
|
|
2800
|
+
|
|
2801
|
+
- npc: A character meant to be encountered. It may describe a reusable template and need not be
|
|
2802
|
+
a singular individual.
|
|
2803
|
+
- character: A detailed, playable, singular individual.
|
|
2804
|
+
- creature: Any being that is neither a character nor an NPC.
|
|
2805
|
+
|
|
2806
|
+
See [a being's subtype describes the subject](#a-beings-subtype-describes-the-subject).
|
|
2807
|
+
|
|
2797
2808
|
| `data` property | Values | Description |
|
|
2798
2809
|
| --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2799
2810
|
| `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
|
|
@@ -3926,22 +3937,39 @@ the setting itself, rather than instructions or other apparatus for the GM.
|
|
|
3926
3937
|
which covers the _reckoning_ — a festival's date is calendar and the festival is not, and a
|
|
3927
3938
|
tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
|
|
3928
3939
|
of people rather than an occasion they attend.
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
| `
|
|
3944
|
-
|
|
|
3940
|
+
- literature: A work a people composes and keeps — an epic, saga, poem, legend, hymn or
|
|
3941
|
+
chronicle, told, sung or written. Held apart from `history`, which states what happened: a
|
|
3942
|
+
legend is literature whether or not its events took place, and a chronicle is literature as a
|
|
3943
|
+
work even where its events are also history.
|
|
3944
|
+
|
|
3945
|
+
Each note a work names in `data.subjects` lists that work on its own site page, under **In
|
|
3946
|
+
song and story**, with the work's `form` beside its title; the subject note writes nothing.
|
|
3947
|
+
A work in a fetched index lists on this package's pages when that index carries its
|
|
3948
|
+
`subjects`.
|
|
3949
|
+
|
|
3950
|
+
Calendar fields are written only on a `calendar` note, and `culture`, `form`, `subjects` and
|
|
3951
|
+
`language` only on a `literature` note; written on any other lore subType, each is an error at
|
|
3952
|
+
its own key.
|
|
3953
|
+
|
|
3954
|
+
| `data` property | Values | Description |
|
|
3955
|
+
| ----------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
3956
|
+
| `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
|
|
3957
|
+
| `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
|
|
3958
|
+
| `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
|
|
3959
|
+
| `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
|
|
3960
|
+
| `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
|
|
3961
|
+
| `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
|
|
3962
|
+
| `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
|
|
3963
|
+
| `culture` | `Address` | `literature` only: the people whose work it is, naming a lore note with `subType: culture` |
|
|
3964
|
+
| `form` | `string` | `literature` only: the kind of work in its people's own terms — `epic`, `saga`, `praise-song`, `elegy`. Free text |
|
|
3965
|
+
| `subjects` | `Address[]` | `literature` only: the beings, places, gods, events and other notes the work concerns |
|
|
3966
|
+
| `language` | `Address` | `literature` only: the tongue it is composed in, naming a skill note with `subType: language` |
|
|
3967
|
+
| `events` | `{ when, until?, recurs? }[]` | This note's dated occurrences, each with its own relationships and chronicle fields |
|
|
3968
|
+
| `events[].when` | a date, or `"0.<day>"` | The occurrence's anchor and first instance — required |
|
|
3969
|
+
| `events[].until` | a date | Where the occurrence ran to, or where a recurring series stopped |
|
|
3970
|
+
| `events[].recurs` | `{ every }` or `{ on }` | How further occurrences are found, absent for a one-time occurrence |
|
|
3971
|
+
| `events[].recurs.every` | whole number of years, 1 or more | A period counted on the canonical axis from `when` |
|
|
3972
|
+
| `events[].recurs.on` | list of dates, strictly increasing, each later than `when` | Recorded occurrences beyond the first, in place of a period |
|
|
3945
3973
|
|
|
3946
3974
|
`data.events` is available on every `lore` subType. Each entry's `kind`, `depth`, `summary`, `sources`, `standing`, `accounts`, `who`, `where`, `unresolved`, `follows` and `names` hold its chronicle detail; their shapes belong to whatever design governs chronicle records, and this format only asks that they sit inside an entry. The shared format checks `when`, `until` and `recurs`; a content package can check the rest. `events` itself does not appear as an infobox row, but a recurring entry's computed next occurrence does.
|
|
3947
3975
|
|
|
@@ -417,7 +417,7 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
417
417
|
|
|
418
418
|
### lore
|
|
419
419
|
|
|
420
|
-
**Subtypes:** `cosmology`, `deity`, `theology`, `arcana`, `spirit`, `economy`, `law`, `calendar`, `history`, `material`, `folk`, `culture`, `custom`, `bestiary`, `gathering`.
|
|
420
|
+
**Subtypes:** `cosmology`, `deity`, `theology`, `arcana`, `spirit`, `economy`, `law`, `calendar`, `history`, `material`, `folk`, `culture`, `custom`, `bestiary`, `gathering`, `literature`.
|
|
421
421
|
|
|
422
422
|
Lore records in-world knowledge. A `culture` describes a people; a `custom` describes how they practice a rite, observance, or usage. A `material` describes a physical constituent and its qualities, which may vary by region. See the [lore subtype definitions](format-details.md#type-lore) for the complete vocabulary.
|
|
423
423
|
|
|
@@ -430,6 +430,10 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
430
430
|
| `data.namedDays` | list of `{ name, abbreviation?, day }` | Names assigned to particular days of the year. |
|
|
431
431
|
| `data.eras` | list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }` | The year-counts kept in this calendar. A marker names one era and uses these months. |
|
|
432
432
|
| `data.formats` | map of named Calendaria format strings | Named patterns for reading and writing this calendar's dates. |
|
|
433
|
+
| `data.culture` | an Address | The people whose work this is, as a culture lore note. |
|
|
434
|
+
| `data.form` | string | The kind of work in its people's own terms — an epic, a saga, a praise-song, an elegy. Free text. |
|
|
435
|
+
| `data.subjects` | list of Addresses | The beings, places, gods, events and other notes the work concerns. |
|
|
436
|
+
| `data.language` | an Address | The tongue the work is composed in, as a language skill note. |
|
|
433
437
|
| `data.events` | list of `{ when, until?, recurs? }` | This note's dated occurrences — a founding once, an anniversary that recurs, or a list of recorded happenings. |
|
|
434
438
|
|
|
435
439
|
### map
|
|
@@ -14,7 +14,7 @@ map, search, and the markup an image or a figure renders as — and names the
|
|
|
14
14
|
classes a consumer styles against.
|
|
15
15
|
|
|
16
16
|
**None of the sections below is an authoring instruction.** `infoboxes`,
|
|
17
|
-
`related`, `contains`, `governed_by`, `governed_places`, `map`, `package`, `slug`, `url`
|
|
17
|
+
`related`, `contains`, `governed_by`, `governed_places`, `works`, `map`, `package`, `slug`, `url`
|
|
18
18
|
and `title` cannot be written by a note at all: a note's top-level region is
|
|
19
19
|
closed and does not admit them, so each arrives already resolved by the time
|
|
20
20
|
the theme sees it. Read this page when a panel is missing or a class needs
|
|
@@ -215,6 +215,37 @@ uses, so a region with forty settlements stays scannable.
|
|
|
215
215
|
A page whose front matter declares none of `contains`, `governed_by` or
|
|
216
216
|
`governed_places` renders no card at all.
|
|
217
217
|
|
|
218
|
+
## The "In song and story" card
|
|
219
|
+
|
|
220
|
+
`partials/literature-works.html` renders a second card directly below the
|
|
221
|
+
government card, listing the works of literature that name the page's subject.
|
|
222
|
+
The build derives `works` from every `literature` lore note's `data.subjects`,
|
|
223
|
+
including works a fetched index carries, so no note writes it:
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
---
|
|
227
|
+
works: [{ title: …, url: …, form: … }] # works naming this page, sorted by title
|
|
228
|
+
---
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`form` is the work's `data.form`, printed in parentheses beside its title, and is
|
|
232
|
+
absent when the work states none. A work naming one subject twice lists once, and
|
|
233
|
+
a work naming itself is not listed on its own page. An entry's `url` is absent
|
|
234
|
+
for a work that publishes no page, which renders as plain text through
|
|
235
|
+
`partials/holdings/entry.html`. A page no work names carries no `works` and
|
|
236
|
+
renders no card.
|
|
237
|
+
|
|
238
|
+
The card reuses the government card's classes, so a consumer styling one styles
|
|
239
|
+
both; `.literature-works` on the `<section>` tells them apart.
|
|
240
|
+
|
|
241
|
+
| class | drawn as |
|
|
242
|
+
| ----------------------- | --------------------------------- |
|
|
243
|
+
| `.literature-works` | the card, beside `.holdings` |
|
|
244
|
+
| `.holdings-table-label` | "In song and story" |
|
|
245
|
+
| `.holdings-table-grid` | the grid of works |
|
|
246
|
+
| `.holdings-entry-name` | a work's title, linked or plain |
|
|
247
|
+
| `.holdings-entry-kind` | the work's form, beside its title |
|
|
248
|
+
|
|
218
249
|
## The place map
|
|
219
250
|
|
|
220
251
|
A place page with `map: from-<shortcode>.svg` displays a **From here** panel
|
|
@@ -241,6 +241,10 @@ types:
|
|
|
241
241
|
- namedDays
|
|
242
242
|
- eras
|
|
243
243
|
- formats
|
|
244
|
+
- culture
|
|
245
|
+
- form
|
|
246
|
+
- subjects
|
|
247
|
+
- language
|
|
244
248
|
- events
|
|
245
249
|
subTypes:
|
|
246
250
|
- cosmology
|
|
@@ -258,6 +262,7 @@ types:
|
|
|
258
262
|
- custom
|
|
259
263
|
- bestiary
|
|
260
264
|
- gathering
|
|
265
|
+
- literature
|
|
261
266
|
map:
|
|
262
267
|
data:
|
|
263
268
|
- scene
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
* A work a people tells, sings or writes: the `literature` lore subType and the
|
|
16
|
+
* four optional facts such a note may state about itself.
|
|
17
|
+
*
|
|
18
|
+
* `DataFieldSpec` declares the keys a type accepts, not the subType that may
|
|
19
|
+
* write them, so {@link checkLiteratureNote} scopes these keys to `literature`
|
|
20
|
+
* the way the calendar check scopes its own to `calendar`.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
26
|
+
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
27
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
28
|
+
|
|
29
|
+
/** The `lore` subType whose notes are works of literature. */
|
|
30
|
+
export const LITERATURE_SUBTYPE = "literature";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Require `data.language` to address a `skill` note whose subType is
|
|
34
|
+
* `language`.
|
|
35
|
+
*
|
|
36
|
+
* An Address that does not resolve is the reference check's to report, so
|
|
37
|
+
* this speaks only about a target that exists and is the wrong kind.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} note
|
|
40
|
+
* @param {{index?: object}} [options]
|
|
41
|
+
* @returns {object[]} The findings.
|
|
42
|
+
*/
|
|
43
|
+
export function checkLanguageChoice(note, { index } = {}) {
|
|
44
|
+
const value = note.fm?.data?.language;
|
|
45
|
+
if (value === undefined || value === null || !index?.addressHit) return [];
|
|
46
|
+
const tuple = parseAddress(value, {
|
|
47
|
+
package: index.contentPackage,
|
|
48
|
+
system: "note",
|
|
49
|
+
type: "skill",
|
|
50
|
+
types: index.types,
|
|
51
|
+
packages: index.packages,
|
|
52
|
+
});
|
|
53
|
+
if (tuple.reason) return [];
|
|
54
|
+
const target = index.addressHit(renderAddress(tuple));
|
|
55
|
+
if (!target) return [];
|
|
56
|
+
const fm = target.fm ?? target;
|
|
57
|
+
if (tuple.type === "skill" && fm.subType === "language") return [];
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
file: note.file,
|
|
61
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "language"]),
|
|
62
|
+
severity: "error",
|
|
63
|
+
message: `data.language ${isAddressTuple(value) ? renderAddress(value) : value} names a note that is not a skill whose subType is language`,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The fields a `literature` note may state. Every one is optional.
|
|
70
|
+
*
|
|
71
|
+
* @type {ReadonlyArray<object>}
|
|
72
|
+
*/
|
|
73
|
+
export const LITERATURE_FIELDS = Object.freeze([
|
|
74
|
+
{
|
|
75
|
+
name: "culture",
|
|
76
|
+
shape: "an Address",
|
|
77
|
+
kind: "address",
|
|
78
|
+
ref: "lore",
|
|
79
|
+
accepts: ["lore"],
|
|
80
|
+
check: checkCultureChoice,
|
|
81
|
+
describe: "The people whose work this is, as a culture lore note.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "form",
|
|
85
|
+
shape: "string",
|
|
86
|
+
kind: "string",
|
|
87
|
+
describe:
|
|
88
|
+
"The kind of work in its people's own terms — an epic, a saga, a " +
|
|
89
|
+
"praise-song, an elegy. Free text.",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "subjects",
|
|
93
|
+
shape: "list of Addresses",
|
|
94
|
+
kind: "list",
|
|
95
|
+
entryKind: "address",
|
|
96
|
+
describe: "The beings, places, gods, events and other notes the work concerns.",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "language",
|
|
100
|
+
shape: "an Address",
|
|
101
|
+
kind: "address",
|
|
102
|
+
ref: "skill",
|
|
103
|
+
accepts: ["skill"],
|
|
104
|
+
check: checkLanguageChoice,
|
|
105
|
+
describe: "The tongue the work is composed in, as a language skill note.",
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const LITERATURE_KEYS = Object.freeze(LITERATURE_FIELDS.map((field) => field.name));
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Refuse a literature field on a lore note of any other subType.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} note
|
|
115
|
+
* @returns {object[]} One error per literature key written off `literature`.
|
|
116
|
+
*/
|
|
117
|
+
export function checkLiteratureNote(note) {
|
|
118
|
+
const type = String(note?.type ?? note?.fm?.type ?? "").toLowerCase();
|
|
119
|
+
const subType = String(note?.fm?.subType ?? "").toLowerCase();
|
|
120
|
+
if (type !== "lore" || subType === LITERATURE_SUBTYPE) return [];
|
|
121
|
+
const data = note?.fm?.data;
|
|
122
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
123
|
+
return LITERATURE_KEYS.filter((key) => data[key] !== undefined && data[key] !== null).map(
|
|
124
|
+
(key) => ({
|
|
125
|
+
file: note.file,
|
|
126
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", key], { key: true }),
|
|
127
|
+
severity: "error",
|
|
128
|
+
message:
|
|
129
|
+
`\`data.${key}\` describes a work of literature, and this note's ` +
|
|
130
|
+
`\`subType\` is not \`literature\``,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
* The works of literature that concern each page, derived from what the works
|
|
16
|
+
* say. A `literature` lore note names its subjects in `data.subjects`; the site
|
|
17
|
+
* build inverts that into a `works` list on every subject's page, so a reader on
|
|
18
|
+
* a hero's, a place's or a god's page reaches the epics and legends about it.
|
|
19
|
+
* The subject note writes nothing.
|
|
20
|
+
*
|
|
21
|
+
* A work in a fetched index lists on this package's subject pages when that
|
|
22
|
+
* index carries its `subjects`. A subject in another package has no page here,
|
|
23
|
+
* so a local work naming one lists nowhere.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { parseAddress, renderAddress } from "./address.mjs";
|
|
29
|
+
import { readCanonicalKey } from "./content-address.mjs";
|
|
30
|
+
import { LITERATURE_SUBTYPE } from "./literature-notes.mjs";
|
|
31
|
+
|
|
32
|
+
/** The page front-matter key the site build writes. */
|
|
33
|
+
export const WORKS_KEY = "works";
|
|
34
|
+
|
|
35
|
+
/** @typedef {{title: string, url?: string, form?: string}} WorkEntry */
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} WorksNode
|
|
38
|
+
* @property {string} key Own Address, normalized to the `note` system.
|
|
39
|
+
* @property {string} title
|
|
40
|
+
* @property {string} [url] Absent for a note that publishes no page.
|
|
41
|
+
* @property {string} [package] Owning content package.
|
|
42
|
+
* @property {string} [form] A work's `data.form`.
|
|
43
|
+
* @property {unknown[]} subjects A work's `data.subjects`; empty for any other note.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** The note-system key of one Address, or "" where it names nothing. */
|
|
47
|
+
function addressKey(value, pkg, types) {
|
|
48
|
+
if (value == null) return "";
|
|
49
|
+
const tuple = parseAddress(value, { package: pkg ?? "local", system: "note", types });
|
|
50
|
+
if (tuple.reason) return "";
|
|
51
|
+
return renderAddress({ ...tuple, system: "note" }).toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a note is a work of literature. */
|
|
55
|
+
function isWork(type, subType) {
|
|
56
|
+
return type === "lore" && String(subType ?? "").toLowerCase() === LITERATURE_SUBTYPE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One local page's node.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} fm - The note's frontmatter.
|
|
63
|
+
* @param {{title: string, url?: string, package?: string}} page
|
|
64
|
+
* @returns {WorksNode|null} Null for a note with no type or shortcode.
|
|
65
|
+
*/
|
|
66
|
+
export function worksNode(fm, { title, url, package: pkg }) {
|
|
67
|
+
const type = String(fm?.type ?? "");
|
|
68
|
+
const shortcode = String(fm?.shortcode ?? "").toLowerCase();
|
|
69
|
+
if (!type || !shortcode) return null;
|
|
70
|
+
const data = fm.data && typeof fm.data === "object" && !Array.isArray(fm.data) ? fm.data : {};
|
|
71
|
+
const work = isWork(type, fm.subType);
|
|
72
|
+
return {
|
|
73
|
+
key: renderAddress({ package: pkg ?? "local", system: "note", type, shortcode }),
|
|
74
|
+
title,
|
|
75
|
+
...(url ? { url } : {}),
|
|
76
|
+
...(pkg ? { package: pkg } : {}),
|
|
77
|
+
...(work && typeof data.form === "string" && data.form ? { form: data.form } : {}),
|
|
78
|
+
subjects:
|
|
79
|
+
!work ? []
|
|
80
|
+
: Array.isArray(data.subjects) ? data.subjects
|
|
81
|
+
: data.subjects ? [data.subjects]
|
|
82
|
+
: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The works a fetched index carries, as nodes. Only a work can list itself on
|
|
88
|
+
* a page, and only a page of this package can carry the list, so a foreign
|
|
89
|
+
* entry that is not a work is never needed here.
|
|
90
|
+
*
|
|
91
|
+
* @param {Map<string, object>|undefined} foreignIndex
|
|
92
|
+
* @returns {WorksNode[]}
|
|
93
|
+
*/
|
|
94
|
+
export function foreignWorksNodes(foreignIndex) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const [canonical, entry] of foreignIndex ?? []) {
|
|
97
|
+
const tuple = readCanonicalKey(canonical);
|
|
98
|
+
if (!tuple || !isWork(entry?.type, entry?.subType)) continue;
|
|
99
|
+
if (!Array.isArray(entry.subjects) || !entry.subjects.length) continue;
|
|
100
|
+
out.push({
|
|
101
|
+
key: renderAddress({ ...tuple, system: "note" }),
|
|
102
|
+
title: String(entry.name ?? tuple.shortcode),
|
|
103
|
+
...(entry.url ? { url: entry.url } : {}),
|
|
104
|
+
package: tuple.package,
|
|
105
|
+
...(typeof entry.form === "string" && entry.form ? { form: entry.form } : {}),
|
|
106
|
+
subjects: entry.subjects,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Invert every work's subjects into the list each subject's page carries.
|
|
114
|
+
*
|
|
115
|
+
* A subject is resolved in its work's own package. A work naming one subject
|
|
116
|
+
* twice lists once; a work naming itself is not listed on its own page.
|
|
117
|
+
* Entries sort by title.
|
|
118
|
+
*
|
|
119
|
+
* @param {Iterable<WorksNode|null>} nodes - Every local page, then any foreign work.
|
|
120
|
+
* @param {object} options
|
|
121
|
+
* @param {ReadonlySet<string>} options.types - Note types an Address may name.
|
|
122
|
+
* @returns {Map<string, {works: WorkEntry[]}>} Keyed by the subject page's URL.
|
|
123
|
+
*/
|
|
124
|
+
export function worksPages(nodes, { types }) {
|
|
125
|
+
const all = [...nodes].filter(Boolean);
|
|
126
|
+
/** @type {Map<string, WorksNode>} */
|
|
127
|
+
const pages = new Map();
|
|
128
|
+
for (const node of all) {
|
|
129
|
+
if (!node.url) continue;
|
|
130
|
+
const key = node.key.toLowerCase();
|
|
131
|
+
if (!pages.has(key)) pages.set(key, node);
|
|
132
|
+
}
|
|
133
|
+
/** @type {Map<string, {works: WorkEntry[]}>} */
|
|
134
|
+
const lists = new Map();
|
|
135
|
+
for (const work of all) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (const subject of work.subjects) {
|
|
138
|
+
const target = pages.get(addressKey(subject, work.package, types));
|
|
139
|
+
if (!target || target.key === work.key || seen.has(target.url)) continue;
|
|
140
|
+
seen.add(target.url);
|
|
141
|
+
let block = lists.get(target.url);
|
|
142
|
+
if (!block) lists.set(target.url, (block = { works: [] }));
|
|
143
|
+
block.works.push({
|
|
144
|
+
title: work.title,
|
|
145
|
+
...(work.url ? { url: work.url } : {}),
|
|
146
|
+
...(work.form ? { form: work.form } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const block of lists.values())
|
|
151
|
+
block.works.sort((a, b) => a.title.localeCompare(b.title, "en"));
|
|
152
|
+
return lists;
|
|
153
|
+
}
|
|
@@ -437,6 +437,14 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
437
437
|
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
438
438
|
{ government: record.data.government }
|
|
439
439
|
: {}),
|
|
440
|
+
// What a work of literature concerns and what kind of work it
|
|
441
|
+
// is, so a consumer's subject pages list a dependency's works.
|
|
442
|
+
...(parts.type === "lore" && record.subType === "literature" ?
|
|
443
|
+
{
|
|
444
|
+
subjects: record.data?.subjects ?? undefined,
|
|
445
|
+
form: record.data?.form ?? undefined,
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
440
448
|
package: pkg,
|
|
441
449
|
});
|
|
442
450
|
const entry = index.get(key);
|
|
@@ -109,6 +109,7 @@ import { checkAffiliationRankFloor, checkRankLadder } from "./rank-ladder.mjs";
|
|
|
109
109
|
import { checkCalendarChoice } from "./calendar-choice.mjs";
|
|
110
110
|
import { checkLoreEvents } from "./lore-events.mjs";
|
|
111
111
|
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
112
|
+
import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
112
113
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
113
114
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
114
115
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -1479,15 +1480,21 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1479
1480
|
"custom", // How a people practices a rite, observance, or usage.
|
|
1480
1481
|
"bestiary", // A kind of creature that is not a people.
|
|
1481
1482
|
"gathering", // A recurring public assembly, such as a fair or tournament.
|
|
1483
|
+
"literature", // A work a people tells, sings, or writes, such as an epic or legend.
|
|
1482
1484
|
]),
|
|
1483
1485
|
// A lore note is prose, and what it *is* about is its subType — with
|
|
1484
|
-
//
|
|
1485
|
-
// is data;
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1486
|
+
// two exceptions. A calendar is a division of the year, and a division
|
|
1487
|
+
// is data; a work of literature states whose it is, in what tongue and
|
|
1488
|
+
// about what. The checks scope each family to the subType that means
|
|
1489
|
+
// it, because `DataFieldSpec` declares the keys a type accepts and not
|
|
1490
|
+
// the subType that may write them.
|
|
1491
|
+
check: (note, context) => [
|
|
1492
|
+
...checkCalendarNote(note, context),
|
|
1493
|
+
...checkLiteratureNote(note),
|
|
1494
|
+
],
|
|
1489
1495
|
data: Object.freeze([
|
|
1490
1496
|
...CALENDAR_FIELDS,
|
|
1497
|
+
...LITERATURE_FIELDS,
|
|
1491
1498
|
{
|
|
1492
1499
|
name: "events",
|
|
1493
1500
|
kind: "list",
|
package/engine/site-build.mjs
CHANGED
|
@@ -95,7 +95,7 @@ import { resolvedDateFields } from "./note-dates.mjs";
|
|
|
95
95
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
96
96
|
// The one statement of what an empty body means, shared with the index.
|
|
97
97
|
import { isStubNote } from "./note-state.mjs";
|
|
98
|
-
import { isGmNote } from "./note-vocabulary.mjs";
|
|
98
|
+
import { NOTE_VOCABULARY, isGmNote } from "./note-vocabulary.mjs";
|
|
99
99
|
import { ART_SLOTS, artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
|
|
100
100
|
import { embedRole } from "./content-embeds.mjs";
|
|
101
101
|
import {
|
|
@@ -109,6 +109,7 @@ import { publishesContentPages } from "../content-config.mjs";
|
|
|
109
109
|
import { HUGO_CONTENT } from "./site-config.mjs";
|
|
110
110
|
import { homepageLinkTargets, relatedPages } from "./related-pages.mjs";
|
|
111
111
|
import { HOLDINGS_KEYS, foreignHoldingsNodes, holdingsNode, holdingsPages } from "./holdings.mjs";
|
|
112
|
+
import { WORKS_KEY, foreignWorksNodes, worksNode, worksPages } from "./literature-works.mjs";
|
|
112
113
|
import { drawSiteMaps } from "./site-maps.mjs";
|
|
113
114
|
|
|
114
115
|
const require = createRequire(import.meta.url);
|
|
@@ -607,6 +608,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
607
608
|
delete data.aliases;
|
|
608
609
|
delete data.related;
|
|
609
610
|
for (const key of HOLDINGS_KEYS) delete data[key];
|
|
611
|
+
delete data[WORKS_KEY];
|
|
610
612
|
delete data.map;
|
|
611
613
|
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
612
614
|
return data;
|
|
@@ -1089,12 +1091,25 @@ export function renderPages(pages, options) {
|
|
|
1089
1091
|
},
|
|
1090
1092
|
);
|
|
1091
1093
|
|
|
1094
|
+
// Each work of literature lists on the pages of the subjects it names,
|
|
1095
|
+
// including works a fetched index carries.
|
|
1096
|
+
const works = worksPages(
|
|
1097
|
+
[
|
|
1098
|
+
...pages.map((page) =>
|
|
1099
|
+
worksNode(page.fm, { title: pageTitle(page), url: page.url, package: page.pkg }),
|
|
1100
|
+
),
|
|
1101
|
+
...foreignWorksNodes(foreign?.index),
|
|
1102
|
+
],
|
|
1103
|
+
{ types: new Set(Object.keys(NOTE_VOCABULARY)) },
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1092
1106
|
let withMap = 0;
|
|
1093
1107
|
const outputs = capture === true ? new Map() : null;
|
|
1094
1108
|
for (const { page, body, data } of rendered) {
|
|
1095
1109
|
const block = related.get(page.url);
|
|
1096
1110
|
if (block) data.related = block;
|
|
1097
1111
|
Object.assign(data, holdings.get(page.url));
|
|
1112
|
+
Object.assign(data, works.get(page.url));
|
|
1098
1113
|
const map = maps.get(page.url);
|
|
1099
1114
|
if (map) {
|
|
1100
1115
|
data.map = map.name;
|
package/engine/stub-lint.mjs
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* a `draft` tag or a frontmatter description. Placeholder-only bodies remain
|
|
19
19
|
* errors. A nonempty body below twenty-five words receives a warning when it
|
|
20
20
|
* is not tagged `draft`; the author decides whether that short note is done.
|
|
21
|
+
* A `folder` note is held only to the first rule: any nonempty body passes,
|
|
22
|
+
* with or without `draft`.
|
|
21
23
|
*
|
|
22
24
|
* Counts and the oldest drafts remain reports. Classification and indexing
|
|
23
25
|
* still derive full, draft and stub states from the existing note records;
|
|
@@ -70,6 +72,10 @@ function checkNote({ fm, body, file, raw }) {
|
|
|
70
72
|
return findings;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// A folder's body describes a pack folder rather than prose a reader is
|
|
76
|
+
// asked to finish, so any nonempty body is complete.
|
|
77
|
+
if (fm.type === "folder") return findings;
|
|
78
|
+
|
|
73
79
|
const placeholders = placeholderBody(body);
|
|
74
80
|
if (placeholders.length) {
|
|
75
81
|
const [{ phrase }] = placeholders;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
The "In song and story" card: the works of literature that name this page's
|
|
3
|
+
subject, rendered after the body and the geography and government card.
|
|
4
|
+
|
|
5
|
+
Front matter `@heroiclands/package-build` derives from every `literature`
|
|
6
|
+
lore note's `data.subjects`, and a note never authors itself:
|
|
7
|
+
|
|
8
|
+
works: [{ title, url, form? }] works naming this page, sorted by title
|
|
9
|
+
|
|
10
|
+
`form` is the work's `data.form` — an epic, a saga, a praise-song — printed
|
|
11
|
+
beside its name. Presence of the key is the only test: a page no work names
|
|
12
|
+
carries no `works` and renders no card. An entry with no `url` is a work
|
|
13
|
+
that publishes no page, rendered as plain text by `holdings/entry.html`.
|
|
14
|
+
*/ -}}
|
|
15
|
+
{{- with .Params.works -}}
|
|
16
|
+
<section class="holdings literature-works" data-pagefind-ignore>
|
|
17
|
+
<div class="holdings-table">
|
|
18
|
+
<h3 class="holdings-table-label">In song and story</h3>
|
|
19
|
+
<ul class="holdings-table-grid">
|
|
20
|
+
{{- range . }}
|
|
21
|
+
<li>
|
|
22
|
+
{{- partial "holdings/entry.html" (dict "entry" . "class" "holdings-entry-name") -}}
|
|
23
|
+
{{- with .form }}<span class="holdings-entry-kind"> ({{ . }})</span>{{ end -}}
|
|
24
|
+
</li>
|
|
25
|
+
{{- end }}
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
{{- end -}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.0.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",
|
|
@@ -53,17 +53,17 @@
|
|
|
53
53
|
* build translates once and no second copy of those three rows exists.
|
|
54
54
|
*/
|
|
55
55
|
export const DEFAULT_ITEM_ART = {
|
|
56
|
-
affiliation: "sohl/assets/icons/noun/
|
|
56
|
+
affiliation: "sohl/assets/icons/noun/shieldnn.svg",
|
|
57
57
|
affliction: "sohl/assets/icons/other/sick.svg",
|
|
58
58
|
armorgear: "sohl/assets/icons/game-icons/lorc/breastplate.svg",
|
|
59
|
-
attribute: "sohl/assets/icons/
|
|
59
|
+
attribute: "sohl/assets/icons/game-icons/lorc/charm.svg",
|
|
60
60
|
concoctiongear: "sohl/assets/icons/game-icons/badges/flask.svg",
|
|
61
61
|
containergear: "sohl/assets/icons/other/sack.svg",
|
|
62
|
-
miscgear: "sohl/assets/icons/other/
|
|
63
|
-
mystery: "sohl/assets/icons/
|
|
64
|
-
mysticalability: "sohl/assets/icons/other/
|
|
62
|
+
miscgear: "sohl/assets/icons/other/questionmark.svg",
|
|
63
|
+
mystery: "sohl/assets/icons/game-icons/delapouite/sparkles.svg",
|
|
64
|
+
mysticalability: "sohl/assets/icons/other/handsparkles.svg",
|
|
65
65
|
projectilegear: "sohl/assets/icons/noun/arrow.svg",
|
|
66
|
-
skill: "sohl/assets/icons/other/
|
|
66
|
+
skill: "sohl/assets/icons/other/headgear.svg",
|
|
67
67
|
trauma: "sohl/assets/icons/other/injury.svg",
|
|
68
68
|
weapongear: "sohl/assets/icons/other/sword.svg",
|
|
69
69
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Require `data.language` to address a `skill` note whose subType is
|
|
3
|
+
* `language`.
|
|
4
|
+
*
|
|
5
|
+
* An Address that does not resolve is the reference check's to report, so
|
|
6
|
+
* this speaks only about a target that exists and is the wrong kind.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} note
|
|
9
|
+
* @param {{index?: object}} [options]
|
|
10
|
+
* @returns {object[]} The findings.
|
|
11
|
+
*/
|
|
12
|
+
export function checkLanguageChoice(note: object, { index }?: {
|
|
13
|
+
index?: object;
|
|
14
|
+
}): object[];
|
|
15
|
+
/**
|
|
16
|
+
* Refuse a literature field on a lore note of any other subType.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} note
|
|
19
|
+
* @returns {object[]} One error per literature key written off `literature`.
|
|
20
|
+
*/
|
|
21
|
+
export function checkLiteratureNote(note: object): object[];
|
|
22
|
+
/** The `lore` subType whose notes are works of literature. */
|
|
23
|
+
export const LITERATURE_SUBTYPE: "literature";
|
|
24
|
+
/**
|
|
25
|
+
* The fields a `literature` note may state. Every one is optional.
|
|
26
|
+
*
|
|
27
|
+
* @type {ReadonlyArray<object>}
|
|
28
|
+
*/
|
|
29
|
+
export const LITERATURE_FIELDS: ReadonlyArray<object>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One local page's node.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} fm - The note's frontmatter.
|
|
5
|
+
* @param {{title: string, url?: string, package?: string}} page
|
|
6
|
+
* @returns {WorksNode|null} Null for a note with no type or shortcode.
|
|
7
|
+
*/
|
|
8
|
+
export function worksNode(fm: object, { title, url, package: pkg }: {
|
|
9
|
+
title: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
package?: string;
|
|
12
|
+
}): WorksNode | null;
|
|
13
|
+
/**
|
|
14
|
+
* The works a fetched index carries, as nodes. Only a work can list itself on
|
|
15
|
+
* a page, and only a page of this package can carry the list, so a foreign
|
|
16
|
+
* entry that is not a work is never needed here.
|
|
17
|
+
*
|
|
18
|
+
* @param {Map<string, object>|undefined} foreignIndex
|
|
19
|
+
* @returns {WorksNode[]}
|
|
20
|
+
*/
|
|
21
|
+
export function foreignWorksNodes(foreignIndex: Map<string, object> | undefined): WorksNode[];
|
|
22
|
+
/**
|
|
23
|
+
* Invert every work's subjects into the list each subject's page carries.
|
|
24
|
+
*
|
|
25
|
+
* A subject is resolved in its work's own package. A work naming one subject
|
|
26
|
+
* twice lists once; a work naming itself is not listed on its own page.
|
|
27
|
+
* Entries sort by title.
|
|
28
|
+
*
|
|
29
|
+
* @param {Iterable<WorksNode|null>} nodes - Every local page, then any foreign work.
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @param {ReadonlySet<string>} options.types - Note types an Address may name.
|
|
32
|
+
* @returns {Map<string, {works: WorkEntry[]}>} Keyed by the subject page's URL.
|
|
33
|
+
*/
|
|
34
|
+
export function worksPages(nodes: Iterable<WorksNode | null>, { types }: {
|
|
35
|
+
types: ReadonlySet<string>;
|
|
36
|
+
}): Map<string, {
|
|
37
|
+
works: WorkEntry[];
|
|
38
|
+
}>;
|
|
39
|
+
/** The page front-matter key the site build writes. */
|
|
40
|
+
export const WORKS_KEY: "works";
|
|
41
|
+
export type WorkEntry = {
|
|
42
|
+
title: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
form?: string;
|
|
45
|
+
};
|
|
46
|
+
export type WorksNode = {
|
|
47
|
+
/**
|
|
48
|
+
* Own Address, normalized to the `note` system.
|
|
49
|
+
*/
|
|
50
|
+
key: string;
|
|
51
|
+
title: string;
|
|
52
|
+
/**
|
|
53
|
+
* Absent for a note that publishes no page.
|
|
54
|
+
*/
|
|
55
|
+
url?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Owning content package.
|
|
58
|
+
*/
|
|
59
|
+
package?: string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* A work's `data.form`.
|
|
62
|
+
*/
|
|
63
|
+
form?: string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* A work's `data.subjects`; empty for any other note.
|
|
66
|
+
*/
|
|
67
|
+
subjects: unknown[];
|
|
68
|
+
};
|