@heroiclands/package-build 22.5.1 → 22.7.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Draft notes say so.** A note tagged `draft` states that it is unfinished and
8
+ that nothing in it is settled, at the head of its compendium entry, on the
9
+ character sheet a referee opens, and on its page in the book. A finished note
10
+ carries nothing.
11
+
12
+ ## 22.6.0
13
+
14
+ ### Minor Changes
15
+
16
+ **Compendium content**
17
+
18
+ - A person or thing with no game-system data no longer becomes a blank
19
+ compendium entry — the note still publishes its page and its journal, so
20
+ somebody can be written without being statted.
21
+ - A package carrying data for a game system it ships no compendium for is now
22
+ told so, rather than having it quietly dropped.
23
+
3
24
  ## 22.5.1
4
25
 
5
26
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -867,7 +867,12 @@ has no art.
867
867
  A note is **system-agnostic**. The only system-specific things it carries are
868
868
  the properties named after a system, and one note may carry more than one — a
869
869
  `being` in `harn-ensemble` compiles into a SoHL `being` _and_ an HM3
870
- `character`. Within a system's block:
870
+ `character`.
871
+
872
+ **The block is what makes a game document.** A note carrying none produces no
873
+ Actor and no Item at all, in any package; its prose still compiles into a page,
874
+ a PDF page and a JournalEntry, so a person can be written without being statted.
875
+ Within a system's block:
871
876
 
872
877
  | property | maps to |
873
878
  | ------------------ | ------------------------------------------------------------- |
@@ -1006,12 +1011,19 @@ hold this package at a build that does not write it. A subtree the schema
1006
1011
  declares but describes no further — a discriminated `TypedSchemaField`, stored
1007
1012
  flat — is left alone rather than reported wholesale.
1008
1013
 
1009
- **A pack that declares a `system:` takes only notes carrying that block.** A
1010
- note that says nothing about a system has no system data, and compiling it there
1011
- would emit a hollow document — a subtype, and none of the fields the subtype
1012
- exists for. The build fails naming the note and the pack. A pack declaring no
1013
- system constrains nothing, and a pass whose document is not system data at all —
1014
- journals, macros, scenes — is not subject to the rule.
1014
+ **An Actor or Item pass takes only notes carrying the block it reads.** A note
1015
+ that says nothing about a system has no system data, and compiling it would emit
1016
+ a hollow document — a subtype, and none of the fields the subtype exists for. So
1017
+ a note carrying no block for any system produces no Actor and no Item anywhere,
1018
+ and is passed over in silence; its prose still compiles into a page, a PDF page
1019
+ and a JournalEntry. The question is asked of the pass's own system rather than
1020
+ of the pack's `system:`: a pack declaring none is compiled by the fallback pass,
1021
+ which reads one block like any other. A pass whose document is not system data
1022
+ at all — journals, macros, scenes — is not subject to the rule.
1023
+
1024
+ **A block for a system no pack of the class compiles is an error**, reported
1025
+ once per note per system with the position of the block key, because the
1026
+ authored data is discarded either way.
1015
1027
 
1016
1028
  **`(type, shortcode)` resolves inside one system's catalogue.** A being names its
1017
1029
  embedded items by address and never by pack, so the Item packs are read as one
@@ -1101,6 +1113,11 @@ page, no address and no link target. A note with a body and the `draft` tag is a
1101
1113
  **draft**, and one with a body and no tag is **full**.
1102
1114
  `docs/content-format.md` states the rule; this is what the lint does with it.
1103
1115
 
1116
+ A draft says so on every surface it reaches — its journal entry, the Actor a
1117
+ being compiles into, its page in the book — in one sentence at the head of the
1118
+ entry, above the infobox and the prose. A stub says nothing anywhere, because it
1119
+ publishes nothing to say it on.
1120
+
1104
1121
  **A document that would be empty is not created**, and the index then names no
1105
1122
  artefact — `uuid`, `path` and `address` are `null`. An unwritten note still
1106
1123
  compiles every document derived from `data:`, so an arcane talent is still its
package/docs/api.md CHANGED
@@ -427,6 +427,23 @@ The three states of a note — **stub**, **draft**, **full** — and the one obs
427
427
  | `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
428
428
  | `bodyWordCount` | `bodyWordCount(body)` | `number` | counting the prose a reader meets, with a wikilink counting as the one word it renders |
429
429
 
430
+ ### `engine.draftNotice`
431
+
432
+ What a draft note says about itself, in one sentence, on every surface it reaches. The words are stated once and each renderer composes its own markup around them, so a reader meeting the note on a page, in a compendium and in the book meets one statement. The mark is a notice rather than a hazard, and the book draws it rather than setting a glyph, because a face missing one is silent in Typst.
433
+
434
+ | Export | Signature | Returns | Use it when |
435
+ | ------------------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------- |
436
+ | `DRAFT_NOTICE` | `const DRAFT_NOTICE` | `string` | reading the sentence itself, which is the floor every surface degrades to |
437
+ | `DRAFT_NOTICE_LABEL` | `const DRAFT_NOTICE_LABEL` | `string` | reading the label that opens the notice |
438
+ | `DRAFT_NOTICE_ICON` | `const DRAFT_NOTICE_ICON` | `string` | naming the Font Awesome mark the HTML surfaces draw |
439
+ | `DRAFT_NOTICE_CLASS` | `const DRAFT_NOTICE_CLASS` | `string` | styling the HTML notice from a package that wants to |
440
+ | `BOOK_DRAFT_NOTICE` | `const BOOK_DRAFT_NOTICE` | `string` | naming the Typst function the book's notice calls |
441
+ | `draftNoticeHtml` | `draftNoticeHtml()` | `string` | drawing the notice for a JournalEntry page or an Actor's prose field |
442
+ | `draftNoticeFor` | `draftNoticeFor(frontmatter)` | `string` | asking whether a note has earned a notice, which is the one place the tag is asked |
443
+ | `withDraftNotice` | `withDraftNotice(frontmatter, html)` | `string` | leading a system's own prose field with the notice where one is due |
444
+ | `draftNoticeTypst` | `draftNoticeTypst()` | `string` | drawing the notice for a leaf of the book |
445
+ | `bookDraftNoticePreamble` | `bookDraftNoticePreamble()` | `string` | emitting the Typst definition the book's notice is drawn by |
446
+
430
447
  ### `engine.stubLint`
431
448
 
432
449
  An empty body must be deliberate. Four rules hold the state honest — a stub states what it is, a stub is not tagged `draft`, a body that renders to nothing is an abandoned draft, and a short unmarked body is asked whether it forgot its marker — and two reports say what the rules cannot decide.
@@ -627,6 +644,8 @@ Which note-type → document-subtype maps this toolchain ships, and the two ques
627
644
  | `KNOWN_DOCUMENT_SUBTYPE_MAPS` | `const KNOWN_DOCUMENT_SUBTYPE_MAPS` | — | The note-type → document-subtype maps this toolchain ships. |
628
645
  | `ACTOR_TYPES` | `const ACTOR_TYPES` | — | Every note type any shipped map compiles into an **Actor**. |
629
646
  | `DEFAULT_DOCUMENT_SUBTYPES` | `const DEFAULT_DOCUMENT_SUBTYPES` | — | The map a pack declaring no `system:` compiles its documents against. |
647
+ | `SHIPPED_SYSTEMS` | `const SHIPPED_SYSTEMS` | — | Every system this toolchain ships a map for, by id. |
648
+ | `SYSTEM_DOCUMENT_CLASSES` | `const SYSTEM_DOCUMENT_CLASSES` | — | The Foundry document classes a system compiles its own data into. |
630
649
  | `subtypeMapFor` | `function subtypeMapFor(system)` | {import("./document-subtypes.mjs").DocumentSubtypeMap\|undefined} Its map, or `undefined` where this toolchain ships none for it. | The map one system ships, by its id. |
631
650
  | `schemaSubtypeOf` | `function schemaSubtypeOf(system, type)` | {string} The document subtype to look up. | The document subtype a note type compiles into for one system — the translation the _schema_ check needs, and the reason it needs one. |
632
651
 
@@ -1197,6 +1216,7 @@ Pack JSON generation — in-repo Markdown → per-entry JSON (build-only). Reads
1197
1216
 
1198
1217
  | Export | Signature | Returns | Use it when |
1199
1218
  | ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1219
+ | `systemCompilers` | `function systemCompilers(passes)` | {Readonly<Record<string, Readonly<Record<string, Function>>>>} System id → document class → pass. | The system-specific compilers, derived from the passes themselves. |
1200
1220
  | `compilerFor` | `function compilerFor(docType, system` | {Function\|undefined} The compiler class, or `undefined` for a document type nothing here compiles — which {@link generatePack} reports rather than defaulting past. | The compiler class a pack of one document type and one system gets. |
1201
1221
  | `emittedArtFor` | `function emittedArtFor(type)` | {{document: string\|null, art: readonly string[]}\|null} What the type compiles into and the art it carries there, or `null` where no claim can be made — a retired type, which is reported as retired instead. | The art fields a note of one content type reaches its document through, and the document it reaches. |
1202
1222
  | `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
@@ -197,13 +197,14 @@ _Not available_ on every affiliation page would suggest a gap in the note when
197
197
  the truth is about the system's scope.
198
198
 
199
199
  Whether a document was produced is the compile's own answer rather than a
200
- reading of the frontmatter. The note's type must map to the system; the note
201
- must route to a pack of the document class that map names; and where **that
202
- pack declares a `system:`**, the note must carry that system's block, which is
203
- the same rule the pack itself compiles under. A pack declaring **no** system
204
- compiles every note it claims from `data:` and the field defaults, so a note
205
- with no block still produces a document and its box still carries content —
206
- which is the ordinary arrangement in a package built for one system.
200
+ reading of the frontmatter. Three things must hold: the note must carry that
201
+ system's block, its type must map to the system, and it must route to a pack of
202
+ the document class that map names. The block is the first of them because a
203
+ system block is what makes a game document — see
204
+ [A system block is what makes a game document](#a-system-block-is-what-makes-a-game-document).
205
+ A pack declaring **no** system is compiled by the fallback pass, which reads one
206
+ system's block and no other, so the question is the same one whether or not the
207
+ pack answered it.
207
208
 
208
209
  #### Four section layouts
209
210
 
@@ -319,16 +320,51 @@ is declared, so a near miss is a finding that names what you probably meant.
319
320
  | **being kind** | `being` | `character`, `creature` |
320
321
  | **state** | any | `draft` |
321
322
 
322
- **`draft` is the one tag either build reads.** A note tagged `draft` exists so a
323
- link into it is not dead, and a link into it renders marked — as
323
+ **`draft` is the one tag either build reads.** It does two things and nothing
324
+ else: a link _into_ a draft note renders marked, and the note itself states that
325
+ it is unfinished wherever a reader meets it.
326
+
327
+ A link into it renders as
324
328
  `<span class="sohl-draft-link" title="Draft — not yet written">…</span>` in a
325
329
  compiled journal and on the website alike, with the appearance supplied by the
326
- consuming system's stylesheet or the site theme. **Nothing else changes**: the
327
- note compiles, validates, publishes and resolves exactly as any other, and it
328
- stays in the packs, in the link manifest and on the site. That is what separates
329
- the tag from the retired `draft:` field, whose whole effect was to move a note
330
- from published to unresolvable without saying so — and which is refused, by name,
331
- if you write it.
330
+ consuming system's stylesheet or the site theme.
331
+
332
+ The note's own statement is the **draft notice**, and it reads the same on every
333
+ surface:
334
+
335
+ > **Draft.** This entry is unfinished. What it states may change, and nothing in
336
+ > it is settled.
337
+
338
+ It leads the entry — above the infobox and above the prose — because a reader
339
+ deciding whether to rely on an entry has to be told before they read it. Where
340
+ it lands:
341
+
342
+ | Surface | What carries it |
343
+ | ------------ | -------------------------------------------------------------------- |
344
+ | JournalEntry | a `blockquote.sohl-draft-notice` at the head of the first page |
345
+ | Actor | the dossier the sheet draws — SoHL's `dossier`, HM3's `biography` |
346
+ | The book | a drawn mark and a left rule between the entry's plate and its panel |
347
+ | The website | the theme's own notice, above the infobox rail and the prose |
348
+
349
+ **An Actor carries it once.** A being's prose reaches two fields, and a document
350
+ saying the same thing twice teaches a reader to skip it — a printed sheet draws
351
+ both in turn — so the dossier carries the notice and the appearance is
352
+ untouched. The two systems pair by the authored section each field reads rather
353
+ than by its name, so `dossier` and `biography` carry it while `appearance` and
354
+ `description` do not.
355
+
356
+ **The mark is a notice, not a hazard** — `fa-circle-exclamation`, which Foundry
357
+ bundles, so a journal page carries it with no module stylesheet loaded. The book
358
+ draws its own, because a consumer declaring no icon family is the ordinary case
359
+ and a glyph a face lacks is silent in Typst. Stripped of mark and rule the
360
+ sentence still states the whole thing, which is the floor every surface is built
361
+ to.
362
+
363
+ **Nothing else changes**: the note compiles, validates, publishes and resolves
364
+ exactly as any other, and it stays in the packs, in the link manifest and on the
365
+ site. That is what separates the tag from the retired `draft:` field, whose whole
366
+ effect was to move a note from published to unresolvable without saying so — and
367
+ which is refused, by name, if you write it.
332
368
 
333
369
  **The group's scope is what makes the check work.** A group names the types it
334
370
  applies to, and a place's kinds are only ever checked on a place. Without that
@@ -788,8 +824,9 @@ _compendium_ receives its document.
788
824
  — an item's prose compiling into a `JournalEntry` of its own — is not what the
789
825
  author was addressing, and is routed by the pass that produces it.
790
826
 
791
- **`<system>.pack` overrides it for one system.** A note that compiles into two
792
- systems can send each document to its own pack:
827
+ **`<system>.pack` names the pack for one system's document.** Routing is per
828
+ system: each block the note carries sends its own document where that block
829
+ says, and a note carrying two blocks may place them in two packs.
793
830
 
794
831
  ```yaml
795
832
  pack: items-sohl
@@ -797,9 +834,22 @@ hm3:
797
834
  pack: items-hm3
798
835
  ```
799
836
 
800
- **Unstated, the document goes to the pack of its type marked `default: true`.**
801
- Where no pack of that type is the default, the build refuses rather than
802
- guessing, and names the candidates.
837
+ **Unstated, a system's document goes to that system's default pack of its
838
+ class** — the pack of the class declaring that `system:`, or, where none does,
839
+ the pack of the class marked `default: true`. Where nothing answers, the build
840
+ refuses rather than guessing, and names the candidates.
841
+
842
+ **`pack` and `packFolder` are different questions.** `pack` names _which
843
+ compendium_ receives a document; `packFolder` names the folder note whose
844
+ address the document is filed under _within_ whichever compendium that is. They
845
+ compose, and both are read per system, so a note may place its two systems'
846
+ documents in different packs and different folders:
847
+
848
+ ```yaml
849
+ sohl:
850
+ pack: characteristics
851
+ packFolder: miscgear
852
+ ```
803
853
 
804
854
  **`pack: none` compiles the note into no document.** The note is walked,
805
855
  published as a page, present in the content index with an address and no
@@ -1662,11 +1712,15 @@ convention rather than declared in a field — see
1662
1712
  A note has three states, and two of them are nothing but conventions the format
1663
1713
  already carried.
1664
1714
 
1665
- | State | Body | Marker | Page | Document | Link target |
1666
- | --------- | ------- | --------------- | ----------- | --------------------------------- | ----------- |
1667
- | **stub** | empty | none | no | yes, where one would not be empty | no |
1668
- | **draft** | written | `tags: [draft]` | yes, marked | yes | yes |
1669
- | **full** | written | none | yes | yes | yes |
1715
+ | State | Body | Marker | Page | Document | Link target |
1716
+ | --------- | ------- | --------------- | ------------ | --------------------------------- | ----------- |
1717
+ | **stub** | empty | none | no | yes, where one would not be empty | no |
1718
+ | **draft** | written | `tags: [draft]` | yes, noticed | yes, noticed | yes |
1719
+ | **full** | written | none | yes | yes | yes |
1720
+
1721
+ "Document" here is the document a note's frontmatter earns. A system's Actor or
1722
+ Item is earned by that system's block, in every state —
1723
+ [a system block is what makes a game document](#a-system-block-is-what-makes-a-game-document).
1670
1724
 
1671
1725
  **Full is the absence of everything.** It is the default and carries no marker,
1672
1726
  which is right: the overwhelming majority of a finished corpus should say
@@ -1763,8 +1817,8 @@ cannot be is the target of a wikilink, because a wikilink points at a page.
1763
1817
 
1764
1818
  Note types fall into two groups, and only the first has a mapping table.
1765
1819
 
1766
- **Types producing a system document** — a SoHL or HM3 Actor or Item, one per
1767
- system, described by the tables further down.
1820
+ **Types that _may_ produce a system document** — a SoHL or HM3 Actor or Item,
1821
+ one per system, described by the tables further down.
1768
1822
 
1769
1823
  **Types producing only a core document** — `lore`, `place`, `scenario`, `doc`,
1770
1824
  `map`, `macro` and `homepage`. A core document is one Foundry defines rather than
@@ -1780,6 +1834,54 @@ having nothing worth summarising.
1780
1834
  Every note in **both** groups still produces its JournalEntry and its web page.
1781
1835
  The difference is only whether a system Actor or Item is created as well.
1782
1836
 
1837
+ #### A system block is what makes a game document
1838
+
1839
+ > **A note produces a system's Actor or Item only where it carries that
1840
+ > system's block.** A note carrying no block for any system produces no Actor
1841
+ > and no Item anywhere, whatever its packs declare.
1842
+
1843
+ The block is not a place to put optional detail — it is the statement that this
1844
+ person or thing is meant to be played. Its absence is a statement too, and it is
1845
+ made silently: the note is passed over by the Actor and Item passes exactly as
1846
+ any note they do not own, with no finding.
1847
+
1848
+ **Prose is untouched by this.** A body with content still compiles into one
1849
+ JournalEntry, one web page and one PDF page, however many blocks the note
1850
+ carries and whether or not it carries any; an empty body still compiles none of
1851
+ the three. So a note has three states, and each reaches the next by one edit in
1852
+ one file, with its address, its id, its index record and every link into it
1853
+ unchanged:
1854
+
1855
+ | The note carries | Produces |
1856
+ | -------------------------- | ---------------------------------------------- |
1857
+ | no block, empty body | an index record — a name that can be queried |
1858
+ | no block, a written body | a page, a PDF page and a JournalEntry |
1859
+ | a block, a written body | all of those, plus that system's Actor or Item |
1860
+ | two blocks, a written body | one document per system, and one of each other |
1861
+
1862
+ Write the person; add a system block when someone will need a token. A setting's
1863
+ cast is mostly people nobody rolls dice against, and a note about one of them
1864
+ carries no block and occupies no slot in a compendium a referee browses.
1865
+
1866
+ **A note carrying no document publishes no UUID for one.** Its address, its
1867
+ page and its documentation journal are published as they are for any other note,
1868
+ and the `foundry` entry that would name an Actor or an Item is absent — the same
1869
+ statement [`pack: none`](#the-pack-a-note-compiles-into) makes about a pack,
1870
+ made about a system.
1871
+
1872
+ **A block for a system this package ships no pack for is an error.** The
1873
+ authored data would be discarded either way, so the build says so, once per note
1874
+ per system, naming the system and the document class:
1875
+
1876
+ ```
1877
+ assets/content/People/Aelric.md:7:1: error: carries a `hm3:` block, and no Actor pack in this package compiles hm3 documents, so the block is discarded — declare `system: hm3` on one of the `packs:`, or remove the block.
1878
+ ```
1879
+
1880
+ A pack declaring no `system:` is compiled by the fallback pass, which reads one
1881
+ system's block — so a systemless Actor pack serves that system and not every
1882
+ system a note might write. A package shipping for a second system declares
1883
+ `system:` on a pack of each class it compiles.
1884
+
1783
1885
  That includes actors. A being producing its Actor and nothing else would be
1784
1886
  the one system-bearing note with no address at `none`, leaving a prose link
1785
1887
  naming it nowhere to land. It carries a documentation journal like every other
@@ -501,6 +501,18 @@ export class SystemActorCompiler extends BasePackCompiler {
501
501
  */
502
502
  static requiresSystemBlock = true;
503
503
 
504
+ /**
505
+ * The Foundry document class this pass writes.
506
+ *
507
+ * Declared by the class that does the writing, so the table pairing a
508
+ * system with its passes is derived from the passes themselves rather than
509
+ * written out a second time — see
510
+ * {@link module:engine/generate.systemCompilers}.
511
+ *
512
+ * @type {string}
513
+ */
514
+ static documentClass = "Actor";
515
+
504
516
  /**
505
517
  * The note-type → document-subtype map this pass compiles against.
506
518
  *
@@ -183,13 +183,16 @@ export class BasePackCompiler {
183
183
 
184
184
  /**
185
185
  * Whether this pass's document **is** a system's data, and therefore takes
186
- * only notes that carry that system's block.
186
+ * only notes that carry the block this pass reads.
187
187
  *
188
- * A pack may declare a `system:` — `harn-ensemble` ships an `actors-hm3`
189
- * and an `actors-sohl` from one tree — and the note-side half of that is
190
- * the block named after the system. A note carrying no such block has
191
- * nothing to say about it, so compiling it there would emit a **hollow
192
- * document**: a subtype, and none of the fields the subtype exists for.
188
+ * The question is asked of {@link BasePackCompiler#system} — the pass's own
189
+ * system, which an Actor or Item pass always has — rather than of the
190
+ * pack's `system:`, which a pack need not declare. A note carrying no such
191
+ * block has nothing to say about that system, so compiling it would emit a
192
+ * **hollow document**: a subtype, and none of the fields the subtype exists
193
+ * for. `harn-ensemble` ships an `actors-hm3` and an `actors-sohl` from one
194
+ * tree, and `sohl-thalorna` ships one `actors` declaring nothing; the rule
195
+ * is the same in both, because it is the pass that reads a block.
193
196
  *
194
197
  * False by default, because most passes write documents that are not
195
198
  * system data at all. A JournalEntry of prose is the same document under
@@ -428,44 +431,56 @@ export class BasePackCompiler {
428
431
  );
429
432
  }
430
433
 
434
+ /**
435
+ * The system block this pass reads, where it reads one.
436
+ *
437
+ * `undefined` on the base class, because most passes read none: a
438
+ * JournalEntry, a Macro, a Scene and an Adventure are Foundry's documents
439
+ * rather than any system's. The Actor and Item passes answer with the
440
+ * block their {@link SystemActorCompiler.documentSubtypes} names, which is
441
+ * the same declaration that says which notes they claim and what each
442
+ * becomes.
443
+ *
444
+ * Stated here so {@link BasePackCompiler#eligibleFor} is total: a pass
445
+ * declaring `requiresSystemBlock` without a system reads no block, and a
446
+ * note can carry nothing for it.
447
+ *
448
+ * @returns {string|undefined} The block key, or `undefined`.
449
+ */
450
+ get system() {
451
+ return undefined;
452
+ }
453
+
431
454
  /**
432
455
  * Whether a claimed, routed note may become this pack's document at all.
433
456
  *
434
- * The pack-eligibility gate, and it fails rather than skipping: a note that
435
- * routed *here* and carries nothing for this pack's system is an authoring
436
- * mistake with a hollow document at the end of it, not a note that belongs
437
- * to another pass. Skipping it quietly is how a whole tree compiles to
438
- * documents nobody can use.
457
+ * **A system block is what makes a game document.** A pass whose document
458
+ * *is* a system's data compiles a note only where the note carries the
459
+ * block that pass reads, so a note saying nothing about any system becomes
460
+ * no Actor and no Item anywhere — its prose still compiles into a page, a
461
+ * PDF leaf and a JournalEntry, which are documents no system defines.
462
+ *
463
+ * Asked of this pass's own {@link BasePackCompiler#system}, never of the
464
+ * pack's `system:`. A pack declaring none is a single-system convenience
465
+ * compiled by the fallback pass for its document type, and that pass still
466
+ * reads exactly one block — so the note-side question is the same one
467
+ * whether or not the pack answered it.
468
+ *
469
+ * A note this pass has nothing to compile is **skipped**, counted like any
470
+ * other note the pack does not own. A deliberately unstatted person is the
471
+ * ordinary case, and a block deleted by accident shows in the diff that
472
+ * deleted it; what would otherwise be discarded in silence is the block
473
+ * written for a system no pack of the class serves, and that is reported by
474
+ * {@link module:engine/note-claims.unclaimedNoteFindings} rather than here,
475
+ * where a pass can see only its own half of the configuration.
439
476
  *
440
477
  * @param {object} fm - The note's frontmatter.
441
478
  * @returns {boolean} True when the note may be compiled here; `false` when
442
- * it belongs to another system's pass of the same document type, which is
443
- * skipped as quietly as any other note this pack does not own.
444
- * @throws {Error} When this pack's system is absent from the note and no
445
- * other configured system claims it. The error carries a `position` where
446
- * the note's own file can be read.
479
+ * it carries nothing for this pass's system.
447
480
  */
448
481
  eligibleFor(fm) {
449
- if (!this.constructor.requiresSystemBlock || !this.packSystem) return true;
450
- if (carriesSystemBlock(fm, this.packSystem)) return true;
451
- // Another system's pack of this document type will claim it. A
452
- // note carrying only `hm3:` routes here because this pack is the
453
- // *default* of its document type, and defaults are declared per type
454
- // rather than per system — but it is not an incomplete note, it is
455
- // another pass's. The single-system case is untouched: with no second
456
- // system declared there is nothing for this to find, and the error
457
- // below still fires.
458
- const claimant = (this.router?.systemsOfType?.(this.docType) ?? []).find(
459
- (system) => system !== this.packSystem && carriesSystemBlock(fm, system),
460
- );
461
- if (claimant) return false;
462
- const label = fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "this note";
463
- throw new Error(
464
- `${label} carries no \`${this.packSystem}:\` block, so it has no ` +
465
- `${this.packSystem} data to compile — but it routes to pack ` +
466
- `"${this.packName}", which declares \`system: ${this.packSystem}\`. ` +
467
- `Add the block, or route the note to a pack of another system.`,
468
- );
482
+ if (!this.constructor.requiresSystemBlock) return true;
483
+ return carriesSystemBlock(fm, this.system);
469
484
  }
470
485
 
471
486
  /**
@@ -0,0 +1,175 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **What a draft note says about itself**, in one sentence, on every surface it
16
+ * reaches.
17
+ *
18
+ * A note tagged `draft` compiles, validates, publishes and resolves like any
19
+ * other. What separates it from a finished note is a judgement only its author
20
+ * holds — the content is not settled — and a reader who is not told assumes the
21
+ * opposite. So each surface states it where the reader cannot miss it: at the
22
+ * head of the entry, above the infobox and above the prose.
23
+ *
24
+ * **The words are the same everywhere and are stated once here.** A reader who
25
+ * meets the same note on a page, in a compendium and in the book should meet
26
+ * one statement, not three that agree approximately. {@link DRAFT_NOTICE} is
27
+ * that statement; every renderer below composes its own markup around it and
28
+ * changes none of it.
29
+ *
30
+ * **A label alone will not do.** "Draft" names the writing, so it reads as
31
+ * rough prose; what has to land is that the facts may change. The label and the
32
+ * sentence are one unit, and the sentence carries the meaning by itself — which
33
+ * is the floor each surface is built to, because each of them can lose
34
+ * everything around it.
35
+ *
36
+ * ## What each surface can rely on
37
+ *
38
+ * - **Foundry** renders a JournalEntry's own HTML inside a sheet, so the floor
39
+ * is what a journal page carries with no module and no system stylesheet
40
+ * loaded. Foundry draws a left rule on a `blockquote` in any application
41
+ * window and bundles Font Awesome, so the rule and the mark are the host's
42
+ * and nothing here ships CSS. The class is a hook for a package that wants
43
+ * one, never a requirement.
44
+ * - **The book** sets Typst, where a glyph missing from the face is silent and
45
+ * a consumer declaring no icon family is the normal case — see {@link
46
+ * module:engine/content-icons}. So the mark is **drawn**, from two
47
+ * primitives, and cannot become a box.
48
+ * - **An Actor's prose** lands in a system's own sheet, in a field the system
49
+ * styles. It gets the same HTML the journal does; what it can rely on is the
50
+ * sentence.
51
+ *
52
+ * @module
53
+ */
54
+
55
+ import { isDraftNote } from "./note-vocabulary.mjs";
56
+
57
+ /** The word that opens the notice, and the only part a reader reads as a label. */
58
+ export const DRAFT_NOTICE_LABEL = "Draft.";
59
+
60
+ /**
61
+ * The sentence, which states the fact and stops.
62
+ *
63
+ * It says the content may change rather than that the writing is rough,
64
+ * because that is the difference between a reader who treats the entry as
65
+ * provisional and one who builds on it. It gives no instruction: a notice that
66
+ * reaches one page in seven wears better as a statement than as a direction,
67
+ * and the fact carries the consequence for anyone who can act on it.
68
+ *
69
+ * @type {string}
70
+ */
71
+ export const DRAFT_NOTICE =
72
+ "This entry is unfinished. What it states may change, and nothing in it is settled.";
73
+
74
+ /** The Font Awesome mark: a notice, not a hazard. */
75
+ export const DRAFT_NOTICE_ICON = "fa-circle-exclamation";
76
+
77
+ /** The class a package may style the HTML notice against. */
78
+ export const DRAFT_NOTICE_CLASS = "sohl-draft-notice";
79
+
80
+ /** The Typst function the book's notice calls. */
81
+ export const BOOK_DRAFT_NOTICE = "book-draft-notice";
82
+
83
+ /**
84
+ * The notice as HTML, for a JournalEntry page and for an Actor's prose field.
85
+ *
86
+ * A `blockquote` because that is the element Foundry already sets apart —
87
+ * `body.game .app blockquote` carries a left rule and an indent — so the
88
+ * treatment survives with no stylesheet of this package's own. The mark is a
89
+ * Font Awesome element, which Foundry bundles; `aria-hidden` keeps it out of a
90
+ * screen reader, where the label reads it.
91
+ *
92
+ * @returns {string} The notice's HTML.
93
+ */
94
+ export function draftNoticeHtml() {
95
+ return (
96
+ `<blockquote class="${DRAFT_NOTICE_CLASS}">` +
97
+ `<p><i class="fa-solid ${DRAFT_NOTICE_ICON}" aria-hidden="true"></i> ` +
98
+ `<strong>${DRAFT_NOTICE_LABEL}</strong> ${DRAFT_NOTICE}</p>` +
99
+ `</blockquote>`
100
+ );
101
+ }
102
+
103
+ /**
104
+ * The notice a note has earned, or nothing.
105
+ *
106
+ * The one place the tag is asked about on a compiled document, so the packs and
107
+ * the book cannot answer it differently. It reads {@link
108
+ * module:engine/note-vocabulary.isDraftNote}, which is the single reader of the
109
+ * tag itself.
110
+ *
111
+ * @param {object|null|undefined} fm - Parsed frontmatter.
112
+ * @returns {string} The notice's HTML, or `""` for a note that is settled.
113
+ */
114
+ export function draftNoticeFor(fm) {
115
+ return isDraftNote(fm) ? draftNoticeHtml() : "";
116
+ }
117
+
118
+ /**
119
+ * Prose led by the notice, where the note is a draft.
120
+ *
121
+ * **Which field a system leads is the system's decision, and it makes it
122
+ * once.** A referee at the table opens an actor sheet rather than the
123
+ * documentation beside it, so the prose a sheet draws says it too — on one
124
+ * field, since a document showing the same statement twice teaches a reader to
125
+ * skip it. Both systems put it on the dossier: SoHL's `dossier` and HM3's
126
+ * `biography` read the same authored section, so the notice lands on the same
127
+ * prose whichever system compiles it.
128
+ *
129
+ * @param {object|null|undefined} fm - Parsed frontmatter.
130
+ * @param {string} html - The rendered prose.
131
+ * @returns {string} The prose, led by the notice where one is due.
132
+ */
133
+ export function withDraftNotice(fm, html) {
134
+ const notice = draftNoticeFor(fm);
135
+ if (!notice) return html;
136
+ return html ? `${notice}\n${html}` : notice;
137
+ }
138
+
139
+ /**
140
+ * The `#let` the book's notice is drawn by.
141
+ *
142
+ * Emitted with the rest of the book's preamble. The mark is a circle with an
143
+ * exclamation set inside it: two primitives and the face already setting the
144
+ * page, so there is no font to resolve and nothing that can come out as a box.
145
+ *
146
+ * @returns {string} The Typst definition.
147
+ */
148
+ export function bookDraftNoticePreamble() {
149
+ return (
150
+ `#let ${BOOK_DRAFT_NOTICE}(label, body) = block(\n` +
151
+ " width: 100%, above: 0.55em, below: 0.75em, breakable: false,\n" +
152
+ " inset: (left: 0.7em, top: 0.35em, bottom: 0.35em),\n" +
153
+ " stroke: (left: 1.6pt + book-faint))[\n" +
154
+ " #set par(justify: false, first-line-indent: 0em)\n" +
155
+ " #set text(size: 8.6pt, fill: book-faint)\n" +
156
+ " #box(baseline: 0.18em, circle(radius: 0.42em, stroke: 0.6pt + book-faint)[\n" +
157
+ ' #align(center + horizon)[#text(size: 6.4pt, weight: "bold")[!]]\n' +
158
+ " ])\n" +
159
+ " #h(0.4em)\n" +
160
+ ' #text(weight: "bold")[#label]\n' +
161
+ " #h(0.25em)\n" +
162
+ " #body\n" +
163
+ "]"
164
+ );
165
+ }
166
+
167
+ /**
168
+ * The notice as Typst, for a leaf of the book.
169
+ *
170
+ * @returns {string} The call, ready to sit between the entry's plate and its
171
+ * infobox panel.
172
+ */
173
+ export function draftNoticeTypst() {
174
+ return `#${BOOK_DRAFT_NOTICE}[${DRAFT_NOTICE_LABEL}][${DRAFT_NOTICE}]`;
175
+ }