@heroiclands/package-build 22.20.0 → 22.22.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.
Files changed (118) hide show
  1. package/CHANGELOG-content-build.md +0 -8
  2. package/CHANGELOG.md +204 -0
  3. package/assets/styles/content-base.css +162 -0
  4. package/bin/content-commands.mjs +26 -3
  5. package/bin/package-build.mjs +15 -2
  6. package/ci/content-format-reference.mjs +68 -1
  7. package/config.mjs +26 -0
  8. package/content-config.mjs +73 -4
  9. package/docs/api.md +46 -28
  10. package/docs/authoring/dates-and-calendars.md +4 -2
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +237 -28
  13. package/docs/configuration.md +27 -1
  14. package/docs/content-format.md +3 -1
  15. package/docs/guides/reference-pages.md +15 -0
  16. package/docs/reference/format-details.md +265 -42
  17. package/docs/reference/note-types.md +45 -28
  18. package/engine/actor-compiler.mjs +23 -0
  19. package/engine/anchored-sections.mjs +35 -4
  20. package/engine/anchors.mjs +10 -20
  21. package/engine/base-compiler.mjs +86 -21
  22. package/engine/base-styles.mjs +149 -0
  23. package/engine/bundles.mjs +0 -1
  24. package/engine/compendiums.mjs +8 -1
  25. package/engine/content-blocks.mjs +410 -0
  26. package/engine/content-captions.mjs +57 -8
  27. package/engine/content-footnotes.mjs +189 -0
  28. package/engine/content-format-check.mjs +15 -12
  29. package/engine/content-index.mjs +2 -2
  30. package/engine/content-links.mjs +14 -6
  31. package/engine/content-tables.mjs +45 -4
  32. package/engine/content-tasklists.mjs +189 -0
  33. package/engine/docs-checks.mjs +4 -4
  34. package/engine/extension-attributes.mjs +48 -2
  35. package/engine/frontmatter-lint.mjs +62 -66
  36. package/engine/frontmatter.mjs +3 -3
  37. package/engine/heading-attributes.mjs +308 -0
  38. package/engine/helpers.mjs +19 -13
  39. package/engine/homepage.mjs +8 -8
  40. package/engine/index.mjs +3 -0
  41. package/engine/infobox-render.mjs +21 -4
  42. package/engine/infobox.mjs +167 -7
  43. package/engine/journals.mjs +36 -17
  44. package/engine/macros.mjs +1 -1
  45. package/engine/markdown-expressions.mjs +178 -53
  46. package/engine/note-addresses.mjs +17 -8
  47. package/engine/note-dates.mjs +49 -0
  48. package/engine/note-frontmatter.mjs +7 -16
  49. package/engine/note-vocabulary.mjs +216 -42
  50. package/engine/page-lists.mjs +358 -0
  51. package/engine/pdf-build.mjs +76 -42
  52. package/engine/pdf-render.mjs +422 -49
  53. package/engine/rank-ladder.mjs +198 -0
  54. package/engine/reckoning-markers.mjs +1 -1
  55. package/engine/reference-targets.mjs +13 -1
  56. package/engine/retired-fields.mjs +8 -13
  57. package/engine/scenes.mjs +10 -3
  58. package/engine/site-build.mjs +41 -12
  59. package/engine/site-config.mjs +10 -1
  60. package/engine/site-index.mjs +39 -4
  61. package/engine/site-preview.mjs +18 -7
  62. package/engine/social-ties.mjs +52 -14
  63. package/engine/sql-tables.mjs +66 -9
  64. package/engine/standing-terms.mjs +19 -0
  65. package/engine/standings.mjs +538 -0
  66. package/engine/svg-theme.mjs +15 -4
  67. package/engine/system-block.mjs +12 -14
  68. package/engine/web-wikilinks.mjs +48 -1
  69. package/engine/wikilinks.mjs +3 -1
  70. package/hm3/actors.mjs +6 -3
  71. package/manifest.mjs +13 -0
  72. package/package.json +1 -1
  73. package/sohl/actors.mjs +82 -103
  74. package/sohl/item-fields.mjs +4 -5
  75. package/sohl/items.mjs +1 -1
  76. package/sohl/note-schemas.mjs +11 -21
  77. package/types/config.d.mts +9 -0
  78. package/types/engine/actor-compiler.d.mts +11 -0
  79. package/types/engine/anchored-sections.d.mts +24 -0
  80. package/types/engine/base-compiler.d.mts +19 -0
  81. package/types/engine/base-styles.d.mts +93 -0
  82. package/types/engine/content-blocks.d.mts +87 -0
  83. package/types/engine/content-captions.d.mts +14 -1
  84. package/types/engine/content-footnotes.d.mts +73 -0
  85. package/types/engine/content-format-check.d.mts +8 -6
  86. package/types/engine/content-tables.d.mts +12 -4
  87. package/types/engine/content-tasklists.d.mts +72 -0
  88. package/types/engine/extension-attributes.d.mts +22 -1
  89. package/types/engine/frontmatter.d.mts +1 -1
  90. package/types/engine/heading-attributes.d.mts +174 -0
  91. package/types/engine/helpers.d.mts +7 -4
  92. package/types/engine/homepage.d.mts +7 -7
  93. package/types/engine/index.d.mts +1 -0
  94. package/types/engine/infobox-render.d.mts +6 -1
  95. package/types/engine/journals.d.mts +14 -7
  96. package/types/engine/markdown-expressions.d.mts +23 -0
  97. package/types/engine/note-frontmatter.d.mts +1 -4
  98. package/types/engine/note-vocabulary.d.mts +98 -8
  99. package/types/engine/page-lists.d.mts +126 -0
  100. package/types/engine/pdf-render.d.mts +72 -2
  101. package/types/engine/rank-ladder.d.mts +32 -0
  102. package/types/engine/retired-fields.d.mts +4 -6
  103. package/types/engine/site-build.d.mts +13 -1
  104. package/types/engine/site-config.d.mts +9 -0
  105. package/types/engine/site-index.d.mts +13 -1
  106. package/types/engine/site-preview.d.mts +1 -1
  107. package/types/engine/sql-tables.d.mts +31 -7
  108. package/types/engine/standing-terms.d.mts +16 -0
  109. package/types/engine/standings.d.mts +147 -0
  110. package/types/engine/system-block.d.mts +11 -12
  111. package/types/engine/web-wikilinks.d.mts +3 -1
  112. package/types/engine/wikilinks.d.mts +2 -1
  113. package/types/hm3/actors.d.mts +3 -3
  114. package/types/sohl/actors.d.mts +25 -7
  115. package/engine/content-admonitions.mjs +0 -114
  116. package/engine/content-secrets.mjs +0 -112
  117. package/types/engine/content-admonitions.d.mts +0 -24
  118. package/types/engine/content-secrets.d.mts +0 -17
@@ -354,6 +354,8 @@ order without changing the Markdown body.
354
354
 
355
355
  A canonical date is `<year>[.<day>[:HHMMSS]]`. The day is one-based within the world's year. `720` covers the whole year; `720.136` covers that day; `720.136:143005` identifies one second. Negative years are valid. Prefix either form with `~` to express uncertainty beyond its written interval. `unknown` is allowed only in fields that accept an unknown occurrence.
356
356
 
357
+ **Quote a canonical date that states a day.** `born: "667.130"` is read as written; unquoted, YAML parses the scalar as a number and a trailing zero on the day is gone before any date reader sees it, so `667.130` and `667.13` arrive as one value. A number-valued date is an error wherever appending a zero to its day would still name a day inside the world's year — the message names the file, the line and both readings — and is read as written where no such day exists, since `675.2810` is no day of a 365-day year. A bare year loses nothing and needs no quotes.
358
+
357
359
  A named date uses `datefrom <calendar> <date>`, where the final text follows the calendar's `data.formats.std` pattern, or its first named pattern when `std` is absent. For example, a calendar with `std: "MM/DD/Y GGG"` reads `datefrom commoncal 01/23/326 VR`, `datefrom commoncal 01/326 VR`, and `datefrom commoncal 326 VR`. Omitting the day or month retains month or year precision in frontmatter. The CLI `datefrom` command requires a day to produce one canonical day. The calendar argument is a shortcode or Address. A time follows the day only when specified in the pattern, or as a trailing `HH:mm:ss` when the pattern has no clock tokens. A named date with an era-relative year names its era.
358
360
 
359
361
  `data.epoch` on a calendar is a canonical `<year>.<day>`: the day on which **calendar year 1, day 1** occurs. It cannot be year-only, approximate, or expressed through its own calendar. `data.moon.newOn` also locates a particular day. The `data.eras[].start` values are `null` or positive **calendar years**. Exactly one null era covers dates before calendar year 1, and exactly one era starts at year 1. Further era starts are distinct positive integers. Numeric start order determines era boundaries, regardless of array order. The null era numbers its years backwards; later eras begin at their own displayed year 1. An era may have a unique uppercase `marker` for resolving authored dates.
@@ -2086,11 +2088,69 @@ See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-defi
2086
2088
  # Heading {#id .class1 .class2 attr="value"}
2087
2089
  ```
2088
2090
 
2089
- Any header can include curly braces. Inside the curly braces:
2091
+ Any header can end with a braced attribute block, in the same grammar a
2092
+ `:::caption` and a named block take. Inside the braces:
2093
+
2094
+ - `#id` names the anchor a link resolves to (only one allowed)
2095
+ - `.class1` names a CSS class (any number of classes allowed)
2096
+ - `attr="value"` names an HTML attribute whose value is `value` (any number of
2097
+ key/value pairs allowed)
2098
+
2099
+ The id is what every surface publishes; a class and an attribute reach only the
2100
+ surfaces that have somewhere to put them.
2101
+
2102
+ | | an `#id` | a `.class` | an `attr="value"` |
2103
+ | --------------- | ----------------------------------------------------------- | --------------------- | ---------------------- |
2104
+ | Website | the heading's `id`, and the target of `[[note#id\|…]]` | the heading's `class` | written on the heading |
2105
+ | Foundry journal | the anchor the page is addressed by, and the heading's `id` | the heading's `class` | written on the heading |
2106
+ | Book | the Typst label the heading is linked to | dropped | dropped |
2107
+
2108
+ **The book drops both**, because Typst has no stylesheet for a class to name
2109
+ and no element for an attribute to sit on. A heading whose appearance matters in
2110
+ print is set by the book's own template, not by a class an author writes.
2111
+
2112
+ An id contains letters, digits, `_` or `-`. A class and an attribute key begin
2113
+ with a letter. Braces holding nothing the grammar recognises — a set, a die
2114
+ expression, a measurement — are one finding at the heading's line, and the
2115
+ heading is set as it was written so nothing is lost while the author reads the
2116
+ message.
2117
+
2118
+ `id=` and `class=` are refused in favour of `#id` and `.class`, and an attribute
2119
+ beginning `on` is refused as an event handler, exactly as on a named block: a
2120
+ heading's attributes are written onto the element, so nothing authored in a note
2121
+ becomes code on a published page.
2122
+
2123
+ #### A section withheld from players
2124
+
2125
+ The class `secret` on the heading that opens a section withholds that section.
2126
+ The heading keeps its id, so the section stays linkable: secrecy is a property
2127
+ of the section, not of the name a link resolves to.
2128
+
2129
+ ```markdown
2130
+ # The Cellar {#cellar .secret}
2131
+
2132
+ The contraband is behind the false wall.
2133
+ ```
2134
+
2135
+ The section runs from the heading to the next heading that opens a page, which
2136
+ is the span Foundry makes one page of.
2090
2137
 
2091
- - `#id` represents an id anchor named `id` (only one allowed)
2092
- - `.class1` represents a CSS class named `class1` (any number of classes allowed)
2093
- - `attr="value"` represents an HTML attribute named `attr` whose value is `value` (any number of attr/value pairs allowed)
2138
+ | Surface | What a reader meets |
2139
+ | ------- | ----------------------------------------------------------------------------------------------------------------- |
2140
+ | Foundry | The journal page is visible to the GM alone — `ownership: { default: 0 }`, `CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE` |
2141
+ | Website | The heading and everything under it sit inside the expandable `<details>` a `:::secret` block renders as |
2142
+ | Book | The heading and everything under it are set in the labelled block a `:::secret` is set in |
2143
+
2144
+ **On the website this is a spoiler and not access control.** The withheld
2145
+ section is in the published page, so a reader who opens the element or reads the
2146
+ source reads it — exactly as a `:::secret` block is. Foundry is the only surface
2147
+ that withholds anything from anybody. Write nothing in a `.secret` section that
2148
+ would matter if a player read it on the web.
2149
+
2150
+ A `:::secret` block inside a withheld section is legal and renders as it does
2151
+ anywhere else. `.secret` on a heading that opens **no** page covers no page, so
2152
+ no surface can honour it: that is a finding at the heading's own line, answered
2153
+ by giving the heading an anchor or raising it to the top level.
2094
2154
 
2095
2155
  #### Images
2096
2156
 
@@ -2352,6 +2412,59 @@ errors. Duplicate and unknown attributes are errors.
2352
2412
  | `allow-empty=true` | Permits a query selecting no rows. Default `false`. |
2353
2413
  | `section-level=1` through `section-level=6` | Sets the heading level for `_section`. Default `2`. |
2354
2414
 
2415
+ ##### Page lists
2416
+
2417
+ A fenced `pagelist` block renders a list of the pages carrying a tag. It is the
2418
+ companion of a content table and answers the other question: a table says what
2419
+ a set of notes contains, a page list says which pages belong to a group.
2420
+
2421
+ ````markdown
2422
+ ```pagelist {tag="key-concept" type=lore sort=type descriptions=true}
2423
+
2424
+ ```
2425
+ ````
2426
+
2427
+ **The group is the tag, not an enumeration.** A hand-written list of links is
2428
+ a second place the membership is recorded, and the two drift the moment a page
2429
+ is written and nobody edits the landing that should name it. A page list reads
2430
+ `tags:` instead, so tagging a page files it and untagging it removes it.
2431
+
2432
+ **The fence takes no body.** Every statement about the directive is an
2433
+ attribute in the shared braced grammar, so there is one grammar to learn; text
2434
+ written inside the fence is reported rather than printed, and a fence nobody
2435
+ closed is reported rather than swallowing the rest of the note.
2436
+
2437
+ | Attribute | What it does |
2438
+ | ------------------- | ------------------------------------------------------------------------- |
2439
+ | `tag=` | Required. The tag a page carries to join the list. |
2440
+ | `type=` | Restricts the list to one note type. Every type by default. |
2441
+ | `sort=name` | Alphabetical, case and accents folded, tiebroken on address. The default. |
2442
+ | `sort=type` | Groups the note types and orders by name within each. |
2443
+ | `descriptions=true` | Follows each link with the page's `description`. Default `false`. |
2444
+ | `allow-empty=true` | Permits a tag no page carries. Default `false`. |
2445
+
2446
+ The tag is matched the way `tags:` is read everywhere else — a leading `#`,
2447
+ surrounding space and letter case are all insignificant, while the spelling of
2448
+ the tag itself is exact. Nothing guesses at a near miss.
2449
+
2450
+ **What a list holds.** Every note of this package that carries the tag and
2451
+ publishes a page. A **stub** publishes no page, so it holds no address and joins
2452
+ no list. A documentation journal is not listed beside the note it documents,
2453
+ because they are one page. A note tagged `gm` is listed in Foundry and left out
2454
+ of the website and the book, which is the same audience rule a content table's
2455
+ relations follow.
2456
+
2457
+ **Each entry is a link, resolved by the surface that is building.** The
2458
+ directive is answered before the body is walked and spliced in where the fence
2459
+ sat, so the list reaches a reader as compendium journal links in Foundry and
2460
+ page links on the website and in the book — the same resolution an authored
2461
+ `[[…]]` gets, including the cue on a link into a `draft` note.
2462
+
2463
+ **A tag no page carries is an error at the fence's line and column**, naming
2464
+ the tag and, where the directive restricts one, the type. An empty list under
2465
+ an authored heading is how a misspelled tag survives a build, so the
2466
+ directive's silence is what is refused rather than its output.
2467
+
2355
2468
  ##### Captioned blocks
2356
2469
 
2357
2470
  `:::caption {#anchor}` labels the next Markdown block, whether it is prose,
@@ -2381,48 +2494,74 @@ block is prose. The website and Foundry number each category within a note;
2381
2494
  the PDF numbers each category through the book. Foundry gives a captioned block
2382
2495
  its own JournalEntryPage, so its anchor has a page UUID.
2383
2496
 
2384
- `:::info` and `:::warn` mark explanatory and cautionary boxes inline. Their
2385
- content is Markdown, and the boxes carry a label and icon on the web, in
2386
- Foundry, and in the book. An optional `{#id}` gives a box an HTML anchor.
2387
- Close each with a whole-line `:::`. Empty, nested, and unclosed boxes are
2388
- errors at the opening line.
2497
+ ##### Named blocks
2498
+
2499
+ `:::secret`, `:::info` and `:::warn` set a passage apart from the prose around
2500
+ it. The opener and the closing `:::` each take a whole line, the content is
2501
+ Markdown and renders normally, and the fences stay outside code blocks. An
2502
+ empty, nested or unclosed block is an error at its opening line, as is a name
2503
+ that is not one of the three.
2504
+
2505
+ A braced attribute block may follow the name. It takes an id as `#id`, classes
2506
+ as `.class`, and any other attribute as `key="value"`. `title` is reserved and
2507
+ supplies the heading; without it a block heads itself **Secret**, **Info** or
2508
+ **Warn**. A title carries its own emphasis and nothing else — a tag written
2509
+ there is escaped. `id=` and `class=` are refused in favour of `#id` and
2510
+ `.class`, and an attribute beginning `on` is refused as an event handler.
2389
2511
 
2390
2512
  ```markdown
2391
2513
  :::info
2392
2514
  The harbor remains open through winter.
2393
2515
  :::
2394
2516
 
2395
- :::warn {#flood-warning}
2517
+ :::warn {#flood-warning title="Seasonal closure"}
2396
2518
  Spring floods close the eastern road.
2397
2519
  :::
2398
- ```
2399
2520
 
2400
- ```
2401
- :::secret
2402
- This is secret text
2521
+ :::secret {#harbor .wide}
2522
+ The harbor master is working for the smugglers.
2403
2523
  :::
2404
2524
  ```
2405
2525
 
2406
- Use a whole-line `:::secret` opener and a whole-line `:::` closer to mark a GM passage. Markdown inside the block, including links and emphasis, renders normally. Keep the fences outside code blocks and close each one before starting another. An unclosed or nested block is a build error at the source line.
2526
+ The block's name is the first class on the emitted element and the author's
2527
+ classes follow it. **No block carries an inline `style`**, so the appearance is
2528
+ a stylesheet's. The base stylesheet styles `section.info` and `section.warn`;
2529
+ `section.secret` is left to Foundry, which styles it and owns the reveal control
2530
+ on it.
2407
2531
 
2408
- In FoundryVTT, the passage is a secret section visible to the GM and owners of the journal:
2532
+ In FoundryVTT a block is a section whose first line is its heading, and a
2533
+ `secret` is the section Foundry shows to the GM and to owners of the journal:
2409
2534
 
2410
2535
  ```html
2411
- <section class="secret" id="secret-example">
2412
- <p>This is secret text</p>
2536
+ <section class="secret wide" id="harbor">
2537
+ <strong>Secret</strong><br />
2538
+ <p>The harbor master is working for the smugglers.</p>
2413
2539
  </section>
2414
2540
  ```
2415
2541
 
2416
- On webpages, the passage appears as a collapsed spoiler that any reader can open:
2542
+ A `secret` that states no id of its own is given one derived from its body, so
2543
+ that Foundry can remember the section once a reader reveals it.
2544
+
2545
+ On webpages a block is a disclosure any reader can open, titled in its summary:
2417
2546
 
2418
2547
  ```html
2419
- <details>
2420
- <summary>Spoiler</summary>
2421
- <p>This is secret text</p>
2548
+ <details class="secret wide" id="harbor">
2549
+ <summary class="secret">Secret</summary>
2550
+ <p>The harbor master is working for the smugglers.</p>
2422
2551
  </details>
2423
2552
  ```
2424
2553
 
2425
- In books, the passage is visible under a **GM note** label. A printed page has no reader permissions or interactive disclosure control.
2554
+ In a book each block is a coloured box headed by its title. A printed page has
2555
+ no reader permissions and no interactive disclosure control, so a `secret` is
2556
+ visible there.
2557
+
2558
+ **A secret is a presentation distinction and not access control.** It is in the
2559
+ same authored note as the prose around it, and a reader who inspects source or
2560
+ generated output may see it.
2561
+
2562
+ **A malformed block does not silence the blocks around it.** It is reported at
2563
+ its own line and left as the author wrote it, and every well-formed block in the
2564
+ note still renders.
2426
2565
 
2427
2566
  #### Actors
2428
2567
 
@@ -2432,11 +2571,6 @@ The following H1 headers are treated specially:
2432
2571
 
2433
2572
  - `# ... {#appearance}`: The contents of this header become the `doc.appearance` property in sohl and `doc.description` in hm3.
2434
2573
  - `# ... {#dossier}`: The contents of this header become the `doc.dossier` property in sohl and `doc.biography` in hm3.
2435
- - `# ... {#spoilers}`: The contents of this header are not written to the actor at all.
2436
-
2437
- For JournalEntries, the following rules apply:
2438
-
2439
- - `# ... {#spoilers}`: The contents of this header go into a page which is viewable only by the GM (`CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE`).
2440
2574
 
2441
2575
  **Archetypes** describe characters in broad terms and help match them to an adventure. The list is:
2442
2576
 
@@ -2487,7 +2621,7 @@ Generates a living (or undead, or spirit) being.
2487
2621
  | `lore` | `Address[]` | Other lore concerning this being, such as social standing or law |
2488
2622
  | `culture` | `Address` | Primary culture, naming a lore note with `subType: culture` |
2489
2623
  | `homes` | `Address[]` | Place the being calls home |
2490
- | `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
2624
+ | `affiliations` | `Map<Address, Membership>` | Bodies the being belongs to, keyed by Address, each entry the standing it holds there |
2491
2625
  | `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
2492
2626
  | `gender` | `male \| female \| other` | Gender of the character |
2493
2627
  | `species` | `Address` | Being's species (lore) |
@@ -2517,15 +2651,70 @@ describe a being's body. Item gear weight remains a numeric `data.weight`.
2517
2651
  `data.culture` names the culture in which a character belongs. It is one
2518
2652
  Address to a `type: lore`, `subType: culture` note. A named character with a
2519
2653
  known culture states it here; generic beings and templates can omit it.
2520
- `data.lore` holds other lore concerning the being, such as social standing or
2521
- law. Record the primary culture in `data.culture` only.
2654
+ `data.lore` holds the law the being lives under, the customs it is subject to
2655
+ and the traditions it was raised in. Record the primary culture in
2656
+ `data.culture` only, and a standing in `data.affiliations`.
2522
2657
 
2523
2658
  ```yaml
2524
2659
  data:
2525
2660
  culture: lore-vedyariclt
2526
- lore: [lore-commonerrnk]
2661
+ lore: [lore-marchlaw]
2527
2662
  ```
2528
2663
 
2664
+ #### The bodies a being belongs to, and its standing in each
2665
+
2666
+ **Membership**: `{rank: number, office: string}` — `rank` required, `office` not.
2667
+
2668
+ `data.affiliations` is a **map keyed by the body's Address**, and each entry is
2669
+ the standing the being holds in that body. The key is what makes the pair
2670
+ expressible: a being in two bodies holds a standing in each, and two bodies may
2671
+ call a rung the same word.
2672
+
2673
+ An entry carries two keys and nothing else:
2674
+
2675
+ - **`rank`** — **required**, a **number**, the level on that body's own
2676
+ `governance.ranks` ladder. The body declares `{level, title, description}` for
2677
+ every rung, so a number indexes into it and the body stays the single source
2678
+ for what its level 5 is called.
2679
+ - **`office`** — optional, a **string**, matched against the keys of that body's
2680
+ `governance.offices` map.
2681
+
2682
+ **Every entry states a `rank`**, because belonging to a body is holding some
2683
+ standing in it. Where nothing more is known the rung is `1`, the ordinary
2684
+ member; `0` is the rung for someone cast out, which is a real answer rather than
2685
+ a default — the ladders carry `0 Níding`, `0 Outlaw` and `0 Struck from the
2686
+ Roll`. Between them the two ends cover every standing a member can hold, so an
2687
+ entry that states no rank is an **error** at the entry's own line, and the
2688
+ message names both numbers rather than only the gap.
2689
+
2690
+ An entry carrying an **office and no rank** is that same error and not a second
2691
+ one, reported once: an office distinguishes a person within a standing rather
2692
+ than standing in for one, so such an entry puts a person in a post in nothing.
2693
+ An entry carrying a rank and no office is complete.
2694
+
2695
+ ```yaml
2696
+ data:
2697
+ affiliations:
2698
+ affiliation-vrystwldtrbs: { rank: 5, office: War Chief }
2699
+ affiliation-greenwardens: { rank: 1 }
2700
+ affiliation-silverguild: { rank: 0 }
2701
+ ```
2702
+
2703
+ Both values are checked against the body the entry is keyed by. A `rank` that
2704
+ is not a level the body confers and an `office` that is not one of its keys are
2705
+ each an error naming the file, line and column — so a renamed rung or a renamed
2706
+ post fails on every being pointing at it. Naming one body twice, by its short
2707
+ and full Address, still names one body and is an error.
2708
+
2709
+ The infobox prints the standing closing on its body — `War Chief, Hárár, of
2710
+ Vrystwald Tribes` — and links an office to the description the body itself
2711
+ declares, so recording the post needs no note about the post.
2712
+
2713
+ For SoHL, a being embeds one affiliation item per entry: `rank` **is** that
2714
+ item's `system.level` and `office` its `system.office`. A standing is a fact
2715
+ about the setting rather than about one system, so the `sohl:` block states
2716
+ none of it.
2717
+
2529
2718
  #### The people who matter to a being
2530
2719
 
2531
2720
  `data.socialTies` records relationships that define the subject's dossier, not
@@ -2558,8 +2747,8 @@ an error.
2558
2747
  Each tie is one-sided. The target need not name the subject back, even when
2559
2748
  `patron` and `dependent` would describe opposite ends of the same support.
2560
2749
  Kinship, household membership, organizational membership, rank, and office are
2561
- distinct facts. Their relevant effects may appear as ties, while their details
2562
- belong in prose, `data.affiliations`, or governance fields. A tie has no
2750
+ distinct facts. Their relevant effects may appear as ties, while a membership
2751
+ and the standing held in it belong in `data.affiliations`. A tie has no
2563
2752
  mechanical modifier.
2564
2753
 
2565
2754
  #### When a being was born, when it died, and how old it is
@@ -2897,12 +3086,46 @@ two is the commonest way to get a non-monarchy wrong.
2897
3086
  **Rank**
2898
3087
  Definition of a level within the organization (e.g., Priest, Layperson, Member, Gang Leader, Master, etc.)
2899
3088
 
2900
- | Property | Values | Description |
2901
- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
2902
- | `level` | number | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
2903
- | `title` | string | Title associated with the Rank |
2904
- | `description` | string | Description of the Rank |
2905
- | `lore` | `Address` | The standing this rank _is_ — a `lore` note of subType `law`, shared with every other body that confers the same thing |
3089
+ | Property | Required | Values | Description |
3090
+ | ------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
3091
+ | `level` | yes | integer | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
3092
+ | `title` | yes | string | Title associated with the Rank |
3093
+ | `description` | yes | string | Description of the Rank |
3094
+ | `lore` | no | `Address` | The standing this rank _is_ — a `lore` note of subType `law`, shared with every other body that confers the same thing |
3095
+
3096
+ **`level`, `title` and `description` together are a complete statement of a
3097
+ rung**, and nothing further is required for a ladder to be correct. Each is an
3098
+ **error** when it is missing or empty, and a `level` that is not a whole number
3099
+ is an error too: the ladder is what a member's `rank` indexes into, so a rung
3100
+ with no title makes a standing that resolves to nothing, and one with no
3101
+ description ships a rung that says nothing on a sheet or a page. A rung accepts
3102
+ only these four keys, and each problem on a rung is reported separately, so a
3103
+ ladder with three is fixed in one pass rather than three builds.
3104
+
3105
+ **A level belongs to one rung.** Two rungs claiming one level is an **error** on
3106
+ the second, naming the first, because the ladder is read by level: a member whose
3107
+ `rank` is that level answers to whichever rung is written higher in the file, and
3108
+ moving two lines renames the standing. A body that confers two titles at one
3109
+ standing writes them as one rung — `title: Knight or Dame` — or gives each its
3110
+ own level.
3111
+
3112
+ ```yaml
3113
+ data:
3114
+ governance:
3115
+ ranks:
3116
+ - { level: 0, title: Vrystrith, description: Kinless and owed nothing. }
3117
+ - { level: 4, title: Fródrád, description: A householder whose word carries. }
3118
+ - level: 5
3119
+ title: Hárár
3120
+ description: First among them, by the assent of the householders.
3121
+ lore: lore-harar-standing
3122
+ ```
3123
+
3124
+ **Reach for `lore` only where the standing needs more said about it than a
3125
+ description can hold** — obligations, rights, what it takes to enter it, what
3126
+ ends it. Most rungs do not: a description that states what the standing is has
3127
+ already said the whole of it, and a note that restates the rung it hangs off
3128
+ costs a reader a click and tells them nothing.
2906
3129
 
2907
3130
  **A rank's `lore` is shared; its `title` is not.** A Normen kingdom calls it
2908
3131
  `Thrall` and a Vylarian province calls it `Slave`, and they mean one standing:
@@ -3575,7 +3798,7 @@ name: { full: The Common Calendar }
3575
3798
  type: lore
3576
3799
  subType: calendar
3577
3800
  data:
3578
- epoch: 1.1
3801
+ epoch: "1.1"
3579
3802
  months:
3580
3803
  - { name: Floralis, abbreviation: Flor, days: 30 }
3581
3804
  - { name: Lusenar, abbreviation: Luse, days: 31 }
@@ -3824,7 +4047,7 @@ data:
3824
4047
  # its moon — a note of its own, because a moon is a body and not a property
3825
4048
  data:
3826
4049
  body: { diameterKm: 3800, orbitalRadiusKm: 388600, orbit: circular, inclined: true }
3827
- moon: { cycle: 30, newOn: 720.1, eclipses: rare }
4050
+ moon: { cycle: 30, newOn: "720.1", eclipses: rare }
3828
4051
  ```
3829
4052
 
3830
4053
  **`year.days` is where the year's length lives, and every calendar in the
@@ -11,6 +11,23 @@ Choose a note's `type` for the subject it describes. Use `subType` when its type
11
11
 
12
12
  The [first-note guide](../authoring/first-note.md) teaches the shape of a note. [Frontmatter](../authoring/frontmatter.md) explains shared and system-specific values. The [detailed reference](format-details.md) gives the full behavior and examples.
13
13
 
14
+ ## Top-level keys
15
+
16
+ These are the only keys a note writes at its top level, in the order the formatter puts them. A key absent from this table is a lint error at its own line: the region is closed, so the key reaches no document and no page. Facts about the subject belong under `data`, and values for one game system belong inside that system's block.
17
+
18
+ | Key | Meaning |
19
+ | ------------- | ----------------------------------------------------------------------------------------- |
20
+ | `shortcode` | The note's own address segment, unique within its type. |
21
+ | `name` | The display names — a required `full`, and any `aliases`. |
22
+ | `type` | What the note is about, which decides its vocabulary and its document. |
23
+ | `subType` | The type's own genre, where it declares one. |
24
+ | `description` | The short page summary. |
25
+ | `tags` | Draft state, GM routing, and the descriptive labels a page list reads. |
26
+ | `data` | The facts about the subject itself, shared by every system. |
27
+ | `dnd5e` | What the `dnd5e` system makes of the subject — its document's mechanics, routing and art. |
28
+ | `hm3` | What the `hm3` system makes of the subject — its document's mechanics, routing and art. |
29
+ | `sohl` | What the `sohl` system makes of the subject — its document's mechanics, routing and art. |
30
+
14
31
  ## Shared `data` fields
15
32
 
16
33
  These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can.
@@ -30,34 +47,34 @@ These fields are accepted by every note type. A field's value can still be irrel
30
47
 
31
48
  **Subtypes:** `npc`, `character`, `creature`.
32
49
 
33
- | Field | Shape | Meaning |
34
- | -------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
- | `data.social` | map | The being's social profile. |
36
- | `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
37
- | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
38
- | `data.archetypes` | list | Archetypal behaviours the being fits. |
39
- | `data.occupation` | string | What the being does for a living. |
40
- | `data.stations` | list of Addresses | Stations the being holds. |
41
- | `data.lore` | list of Addresses | Lore concerning this being, such as the standing it holds or the law it lives under. |
42
- | `data.culture` | an Address | The being's primary culture, as a culture lore note. |
43
- | `data.homes` | list of Addresses | Places the being calls home. |
44
- | `data.affiliations` | list of Addresses | Affiliations the being belongs to — traditions, polities, and the rest. |
45
- | `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
46
- | `data.gender` | string | `male`, `female` or `other`. |
47
- | `data.species` | an Address | The being's species, as a lore note. |
48
- | `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
49
- | `data.calendar` | an Address | Calendar used to display this being's dates. |
50
- | `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
51
- | `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
52
- | `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
53
- | `data.height` | metres or feet and inches | Height in metres or feet and inches. |
54
- | `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
55
- | `data.frame` | string | Relative frame — `scant`, `light`, `medium`, `large` or `massive`. |
56
- | `data.appearance.eye_color` | string | Eye colour. |
57
- | `data.appearance.hair_color` | string | Hair colour. |
58
- | `data.appearance.skin_color` | string | Skin colour. |
59
- | `data.appearance.complexion` | string | Complexion. |
60
- | `data.appearance.extra_features` | list | Anything else a stranger would notice. |
50
+ | Field | Shape | Meaning |
51
+ | -------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `data.social` | map | The being's social profile. |
53
+ | `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
54
+ | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
55
+ | `data.archetypes` | list | Archetypal behaviours the being fits. |
56
+ | `data.occupation` | string | What the being does for a living. |
57
+ | `data.stations` | list of Addresses | Stations the being holds. |
58
+ | `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in. |
59
+ | `data.culture` | an Address | The being's primary culture, as a culture lore note. |
60
+ | `data.homes` | list of Addresses | Places the being calls home. |
61
+ | `data.affiliations` | a standing map keyed by Address, or a list of Addresses | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, required, a level on that body's own ladder, and `office`, optional, a post that body names. |
62
+ | `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
63
+ | `data.gender` | string | One of `male`, `female`, `nonbinary`, `none` or `other`. `none` says the being has no gender; an absent field says its gender is unrecorded. |
64
+ | `data.species` | an Address | The being's species, as a lore note. |
65
+ | `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
66
+ | `data.calendar` | an Address | Calendar used to display this being's dates. |
67
+ | `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
68
+ | `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
69
+ | `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
70
+ | `data.height` | metres or feet and inches | Height in metres or feet and inches. |
71
+ | `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
72
+ | `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. |
73
+ | `data.appearance.eye_color` | string | Eye colour. |
74
+ | `data.appearance.hair_color` | string | Hair colour. |
75
+ | `data.appearance.skin_color` | string | Skin colour. |
76
+ | `data.appearance.complexion` | string or list | The skin's condition — one value, or several, because a face carries more than one at once. A single value means a list of one. |
77
+ | `data.appearance.extra_features` | list | Anything else a stranger would notice. |
61
78
 
62
79
  **System mappings**
63
80
 
@@ -56,6 +56,7 @@ import { emitDiagnostic } from "./diagnostics.mjs";
56
56
  // The `{#appearance}` / `{#dossier}` convention is the note format's, so the
57
57
  // extraction is shared; which field a section lands in stays the system's.
58
58
  export { extractAnchorSection, renderSection } from "./anchored-sections.mjs";
59
+ import { misplacedAnchorSection } from "./anchored-sections.mjs";
59
60
  import { BasePackCompiler } from "./base-compiler.mjs";
60
61
  import { beingDefaultArt } from "./art-fields.mjs";
61
62
  import { contentPackage } from "./content-package.mjs";
@@ -472,6 +473,28 @@ export class SystemActorCompiler extends BasePackCompiler {
472
473
  return { img, token: this.artPath(fm, "tokenIcon") ?? img };
473
474
  }
474
475
 
476
+ /**
477
+ * Report an `{#appearance}` or `{#dossier}` anchor on a heading
478
+ * {@link extractAnchorSection} cannot read, rather than letting the field
479
+ * it feeds compile blank with nothing to say why.
480
+ *
481
+ * @param {string} body - The note body.
482
+ * @param {string} anchorId - `"appearance"` or `"dossier"`.
483
+ * @param {string} ctx - The actor's label, for the message.
484
+ * @returns {void}
485
+ */
486
+ checkAnchoredSection(body, anchorId, ctx) {
487
+ const misplaced = misplacedAnchorSection(body, anchorId, this.currentNote?.bodyLine);
488
+ if (!misplaced) return;
489
+ this.noteError(
490
+ `${ctx}: {#${anchorId}} sits on a heading below the top level, and only ` +
491
+ `an H1 feeds the ${anchorId} field — move the heading to the top level ` +
492
+ "or drop the anchor",
493
+ { line: misplaced.line },
494
+ );
495
+ this.errorCount++;
496
+ }
497
+
475
498
  /**
476
499
  * Which `(actor, subType:identity)` each resolved entry claimed, and the
477
500
  * entry that claimed it first.