@heroiclands/package-build 22.21.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 (110) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +26 -3
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +73 -4
  8. package/docs/api.md +36 -18
  9. package/docs/authoring/dates-and-calendars.md +4 -2
  10. package/docs/authoring/frontmatter.md +2 -2
  11. package/docs/authoring/links-and-markup.md +237 -28
  12. package/docs/configuration.md +27 -1
  13. package/docs/content-format.md +3 -1
  14. package/docs/guides/reference-pages.md +15 -0
  15. package/docs/reference/format-details.md +224 -46
  16. package/docs/reference/note-types.md +21 -4
  17. package/engine/actor-compiler.mjs +23 -0
  18. package/engine/anchored-sections.mjs +35 -4
  19. package/engine/anchors.mjs +10 -20
  20. package/engine/base-compiler.mjs +86 -21
  21. package/engine/base-styles.mjs +149 -0
  22. package/engine/bundles.mjs +0 -1
  23. package/engine/compendiums.mjs +8 -1
  24. package/engine/content-blocks.mjs +410 -0
  25. package/engine/content-captions.mjs +57 -8
  26. package/engine/content-footnotes.mjs +189 -0
  27. package/engine/content-format-check.mjs +15 -12
  28. package/engine/content-index.mjs +2 -2
  29. package/engine/content-links.mjs +14 -6
  30. package/engine/content-tables.mjs +45 -4
  31. package/engine/content-tasklists.mjs +189 -0
  32. package/engine/docs-checks.mjs +4 -4
  33. package/engine/extension-attributes.mjs +48 -2
  34. package/engine/frontmatter-lint.mjs +47 -64
  35. package/engine/frontmatter.mjs +3 -3
  36. package/engine/heading-attributes.mjs +308 -0
  37. package/engine/helpers.mjs +14 -13
  38. package/engine/homepage.mjs +8 -8
  39. package/engine/index.mjs +3 -0
  40. package/engine/infobox-render.mjs +15 -3
  41. package/engine/infobox.mjs +83 -8
  42. package/engine/journals.mjs +36 -17
  43. package/engine/macros.mjs +1 -1
  44. package/engine/markdown-expressions.mjs +178 -53
  45. package/engine/note-dates.mjs +49 -0
  46. package/engine/note-frontmatter.mjs +7 -16
  47. package/engine/note-vocabulary.mjs +172 -37
  48. package/engine/page-lists.mjs +358 -0
  49. package/engine/pdf-build.mjs +76 -42
  50. package/engine/pdf-render.mjs +422 -49
  51. package/engine/rank-ladder.mjs +198 -0
  52. package/engine/reckoning-markers.mjs +1 -1
  53. package/engine/retired-fields.mjs +8 -13
  54. package/engine/scenes.mjs +10 -3
  55. package/engine/site-build.mjs +41 -12
  56. package/engine/site-config.mjs +10 -1
  57. package/engine/site-index.mjs +18 -2
  58. package/engine/site-preview.mjs +18 -7
  59. package/engine/social-ties.mjs +52 -14
  60. package/engine/sql-tables.mjs +66 -9
  61. package/engine/standings.mjs +89 -6
  62. package/engine/system-block.mjs +12 -14
  63. package/engine/web-wikilinks.mjs +48 -1
  64. package/hm3/actors.mjs +6 -3
  65. package/manifest.mjs +13 -0
  66. package/package.json +1 -1
  67. package/sohl/actors.mjs +22 -99
  68. package/sohl/item-fields.mjs +4 -5
  69. package/sohl/items.mjs +1 -1
  70. package/sohl/note-schemas.mjs +11 -21
  71. package/types/config.d.mts +9 -0
  72. package/types/engine/actor-compiler.d.mts +11 -0
  73. package/types/engine/anchored-sections.d.mts +24 -0
  74. package/types/engine/base-compiler.d.mts +19 -0
  75. package/types/engine/base-styles.d.mts +93 -0
  76. package/types/engine/content-blocks.d.mts +87 -0
  77. package/types/engine/content-captions.d.mts +14 -1
  78. package/types/engine/content-footnotes.d.mts +73 -0
  79. package/types/engine/content-format-check.d.mts +8 -6
  80. package/types/engine/content-tables.d.mts +12 -4
  81. package/types/engine/content-tasklists.d.mts +72 -0
  82. package/types/engine/extension-attributes.d.mts +22 -1
  83. package/types/engine/frontmatter.d.mts +1 -1
  84. package/types/engine/heading-attributes.d.mts +174 -0
  85. package/types/engine/helpers.d.mts +7 -4
  86. package/types/engine/homepage.d.mts +7 -7
  87. package/types/engine/index.d.mts +1 -0
  88. package/types/engine/infobox-render.d.mts +6 -1
  89. package/types/engine/journals.d.mts +14 -7
  90. package/types/engine/markdown-expressions.d.mts +23 -0
  91. package/types/engine/note-frontmatter.d.mts +1 -4
  92. package/types/engine/note-vocabulary.d.mts +82 -6
  93. package/types/engine/page-lists.d.mts +126 -0
  94. package/types/engine/pdf-render.d.mts +72 -2
  95. package/types/engine/rank-ladder.d.mts +32 -0
  96. package/types/engine/retired-fields.d.mts +4 -6
  97. package/types/engine/site-build.d.mts +13 -1
  98. package/types/engine/site-config.d.mts +9 -0
  99. package/types/engine/site-index.d.mts +13 -1
  100. package/types/engine/site-preview.d.mts +1 -1
  101. package/types/engine/sql-tables.d.mts +31 -7
  102. package/types/engine/standings.d.mts +21 -3
  103. package/types/engine/system-block.d.mts +11 -12
  104. package/types/engine/web-wikilinks.d.mts +3 -1
  105. package/types/hm3/actors.d.mts +3 -3
  106. package/types/sohl/actors.d.mts +6 -7
  107. package/engine/content-admonitions.mjs +0 -114
  108. package/engine/content-secrets.mjs +0 -112
  109. package/types/engine/content-admonitions.d.mts +0 -24
  110. package/types/engine/content-secrets.d.mts +0 -17
@@ -1462,6 +1462,7 @@ packageBuild:
1462
1462
  | [`packageBuild.stageDir`](#packagebuildstagedir-and-packagebuildassets) | string | no | `build/stage` |
1463
1463
  | [`packageBuild.assets`](#packagebuildstagedir-and-packagebuildassets) | array | no | `[]` |
1464
1464
  | [`packageBuild.assetTransform`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
1465
+ | [`packageBuild.baseStyles`](#packagebuildbasestyles) | boolean | no | `true` |
1465
1466
  | [`packageBuild.manifest`](#packagebuildmanifest) | object, pass-through | no | `{}` |
1466
1467
  | [`packageBuild.manifestFlags`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
1467
1468
  | [`packageBuild.schema`](#packagebuildschema) | object (`{documentType: {from, registry}}`) | no | `[]` |
@@ -1475,7 +1476,7 @@ packageBuild:
1475
1476
  | [`packageBuild.proseLint`](#packagebuildproselint) | object | no | `{age: 21, threshold: 5, minWords: 8, rules: readability}` |
1476
1477
  | [`packageBuild.proseScore`](#packagebuildprosescore) | object | no | `{minWords: 80, bands: {}}` |
1477
1478
 
1478
- > ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e, proseLint, proseScore).``
1479
+ > ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, baseStyles, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e, proseLint, proseScore).``
1479
1480
 
1480
1481
  ### `packageBuild.proseLint`
1481
1482
 
@@ -1561,6 +1562,31 @@ repository has to compute. Neither declares anything, both default to
1561
1562
 
1562
1563
  > ``package-build config: `packageBuild.manifestFlags` must be a non-empty string.``
1563
1564
 
1565
+ ### `packageBuild.baseStyles`
1566
+
1567
+ Whether the package takes the shared base stylesheet. It defaults to `true`,
1568
+ is staged to `styles/package-build-base.css` under the stage root, and is
1569
+ named first in the generated manifest's `styles`.
1570
+
1571
+ The sheet carries only what every package's compiled content relies on to be
1572
+ legible — the definition-list style an infobox and the markdown `Term` /
1573
+ `: definition` syntax both emit — and is scoped to the journal, enriched-field
1574
+ and editor containers so it cannot reach Foundry's own interface.
1575
+
1576
+ It loads into the cascade layer Foundry reserves for the package kind,
1577
+ `system` or `modules`, which is the weakest layer a package can occupy. A
1578
+ package overrides any rule in it from its own stylesheet, whether that sheet
1579
+ names a layer or none at all, with no specificity contest and no
1580
+ `!important`. The term column, the gutter and the width at which the list
1581
+ stacks are custom properties — `--pb-deflist-term-width`,
1582
+ `--pb-deflist-gutter` and `--pb-deflist-row-gap` — so a package adjusts them
1583
+ without copying the rule.
1584
+
1585
+ Declare `false` to decline it; nothing is staged and nothing is added to
1586
+ `styles`:
1587
+
1588
+ > ``package-build config: `packageBuild.baseStyles` must be true or false.``
1589
+
1564
1590
  ### `packageBuild.manifest`
1565
1591
 
1566
1592
  `packageBuild.manifest.title` is a required, non-empty string for a Foundry
@@ -31,7 +31,9 @@ Body extensions use braces with space-separated `key=value` attributes:
31
31
  `![[icon-harbor|Harbor]]{float=top-left size=medium}` and an SQL fence with
32
32
  `{allow-empty=true}` are examples. Inline font glyphs use the package's icon
33
33
  registry, as in `:icon warning:{size=lg}`; image icons use asset Addresses.
34
- `:::info` and `:::warn` add labelled boxes within the prose. The
34
+ `:::info` and `:::warn` add labelled boxes within the prose. A
35
+ `pagelist` fence renders the pages carrying a tag as a list of links, so a
36
+ landing page states a group by intent rather than enumerating it. The
35
37
  [links and markup guide](authoring/links-and-markup.md) gives the complete syntax.
36
38
  Footnotes use `[^id]` references and `[^id]:` definitions, with numeric output
37
39
  markers scoped to each note. Definition lists use a term followed by one or
@@ -45,6 +45,21 @@ WHERE type = 'affiliation'
45
45
  ```
46
46
  ````
47
47
 
48
+ Where the page introduces a _group_ rather than a type, a `pagelist` fence
49
+ states the group by its tag and lists the pages carrying it:
50
+
51
+ ````markdown
52
+ ```pagelist {tag="key-concept" descriptions=true}
53
+
54
+ ```
55
+ ````
56
+
57
+ That keeps the entry page true as notes are written: a page joins the list by
58
+ carrying the tag, so nobody has to remember to add it here. A tag no page
59
+ carries is a build error naming the tag. See
60
+ [page lists](../authoring/links-and-markup.md#page-lists) for the attributes it
61
+ takes.
62
+
48
63
  `data.pack: none` is appropriate when the page belongs on the site and in the
49
64
  index but does not need a Foundry compendium entry. A documentation package
50
65
  has no packs at all. A page with an empty body is a stub: it remains in the
@@ -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.
2090
2117
 
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)
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.
2137
+
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
 
@@ -2529,31 +2663,41 @@ data:
2529
2663
 
2530
2664
  #### The bodies a being belongs to, and its standing in each
2531
2665
 
2532
- **Membership**: `{rank: number, office: string}`, both optional.
2666
+ **Membership**: `{rank: number, office: string}` — `rank` required, `office` not.
2533
2667
 
2534
2668
  `data.affiliations` is a **map keyed by the body's Address**, and each entry is
2535
2669
  the standing the being holds in that body. The key is what makes the pair
2536
2670
  expressible: a being in two bodies holds a standing in each, and two bodies may
2537
2671
  call a rung the same word.
2538
2672
 
2539
- An entry carries two optional keys and nothing else:
2673
+ An entry carries two keys and nothing else:
2540
2674
 
2541
- - **`rank`** — a **number**, the level on that body's own `governance.ranks`
2542
- ladder. The body declares `{level, title, description}` for every rung, so a
2543
- number indexes into it and the body stays the single source for what its
2544
- level 5 is called.
2545
- - **`office`** — a **string**, matched against the keys of that body's
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
2546
2680
  `governance.offices` map.
2547
2681
 
2548
- An entry with neither is valid and is the ordinary shape: it says the being
2549
- belongs to the body and nothing more.
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.
2550
2694
 
2551
2695
  ```yaml
2552
2696
  data:
2553
2697
  affiliations:
2554
2698
  affiliation-vrystwldtrbs: { rank: 5, office: War Chief }
2555
2699
  affiliation-greenwardens: { rank: 1 }
2556
- affiliation-silverguild: {}
2700
+ affiliation-silverguild: { rank: 0 }
2557
2701
  ```
2558
2702
 
2559
2703
  Both values are checked against the body the entry is keyed by. A `rank` that
@@ -2942,12 +3086,46 @@ two is the commonest way to get a non-monarchy wrong.
2942
3086
  **Rank**
2943
3087
  Definition of a level within the organization (e.g., Priest, Layperson, Member, Gang Leader, Master, etc.)
2944
3088
 
2945
- | Property | Values | Description |
2946
- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
2947
- | `level` | number | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
2948
- | `title` | string | Title associated with the Rank |
2949
- | `description` | string | Description of the Rank |
2950
- | `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.
2951
3129
 
2952
3130
  **A rank's `lore` is shared; its `title` is not.** A Normen kingdom calls it
2953
3131
  `Thrall` and a Vylarian province calls it `Slave`, and they mean one standing:
@@ -3620,7 +3798,7 @@ name: { full: The Common Calendar }
3620
3798
  type: lore
3621
3799
  subType: calendar
3622
3800
  data:
3623
- epoch: 1.1
3801
+ epoch: "1.1"
3624
3802
  months:
3625
3803
  - { name: Floralis, abbreviation: Flor, days: 30 }
3626
3804
  - { name: Lusenar, abbreviation: Luse, days: 31 }
@@ -3869,7 +4047,7 @@ data:
3869
4047
  # its moon — a note of its own, because a moon is a body and not a property
3870
4048
  data:
3871
4049
  body: { diameterKm: 3800, orbitalRadiusKm: 388600, orbit: circular, inclined: true }
3872
- moon: { cycle: 30, newOn: 720.1, eclipses: rare }
4050
+ moon: { cycle: 30, newOn: "720.1", eclipses: rare }
3873
4051
  ```
3874
4052
 
3875
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.
@@ -41,9 +58,9 @@ These fields are accepted by every note type. A field's value can still be irrel
41
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. |
42
59
  | `data.culture` | an Address | The being's primary culture, as a culture lore note. |
43
60
  | `data.homes` | list of Addresses | Places the being calls home. |
44
- | `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`, a level on that body's own ladder, and `office`, a post that body names. |
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. |
45
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.) |
46
- | `data.gender` | string | `male`, `female` or `other`. |
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. |
47
64
  | `data.species` | an Address | The being's species, as a lore note. |
48
65
  | `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
49
66
  | `data.calendar` | an Address | Calendar used to display this being's dates. |
@@ -52,11 +69,11 @@ These fields are accepted by every note type. A field's value can still be irrel
52
69
  | `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
53
70
  | `data.height` | metres or feet and inches | Height in metres or feet and inches. |
54
71
  | `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
55
- | `data.frame` | string | Relative frame — `scant`, `light`, `medium`, `large` or `massive`. |
72
+ | `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. |
56
73
  | `data.appearance.eye_color` | string | Eye colour. |
57
74
  | `data.appearance.hair_color` | string | Hair colour. |
58
75
  | `data.appearance.skin_color` | string | Skin colour. |
59
- | `data.appearance.complexion` | string | Complexion. |
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. |
60
77
  | `data.appearance.extra_features` | list | Anything else a stranger would notice. |
61
78
 
62
79
  **System mappings**
@@ -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.
@@ -15,8 +15,8 @@
15
15
  * **Anchored body sections** — the `# Heading {#anchor}` convention, and how a
16
16
  * compiler pulls one section out of a note's prose.
17
17
  *
18
- * The content format gives three anchors a document meaning: `{#appearance}`,
19
- * `{#dossier}` and `{#spoilers}`. *Which field* each lands in is a system's
18
+ * The content format gives two anchors a document meaning: `{#appearance}` and
19
+ * `{#dossier}`. *Which field* each lands in is a system's
20
20
  * business — SoHL writes the first to an actor's `appearance`, HM3 to an
21
21
  * actor's and an item's `description` — but *finding* it is not, so the
22
22
  * extraction lives here where every compiler reaches it.
@@ -30,6 +30,8 @@
30
30
  */
31
31
 
32
32
  import { renderFoundryMarkdown } from "./helpers.mjs";
33
+ import { collectAnchors } from "./anchors.mjs";
34
+ import { splitHeadingAttributes } from "./heading-attributes.mjs";
33
35
 
34
36
  /**
35
37
  * Extract the body of an H1 section whose heading carries the explicit
@@ -39,6 +41,11 @@ import { renderFoundryMarkdown } from "./helpers.mjs";
39
41
  * heading exists. Fenced code blocks are respected so `# foo` inside
40
42
  * ``` blocks does not trigger a match.
41
43
  *
44
+ * An anchor declared on a lower heading is found by {@link collectAnchors}
45
+ * but not by the `#\s+` match below, so it extracts as absent rather than as
46
+ * wrong — {@link misplacedAnchorSection} is the check a caller runs first to
47
+ * tell the two apart.
48
+ *
42
49
  * @param {string} body - The note body.
43
50
  * @param {string} anchorId - The anchor to find.
44
51
  * @returns {string} The section's markdown, or "".
@@ -57,8 +64,7 @@ export function extractAnchorSection(body, anchorId) {
57
64
  }
58
65
  const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
59
66
  if (h1Match) {
60
- const anchor = h1Match[1].match(/\{#([^}]+)\}\s*$/);
61
- const id = anchor?.[1]?.trim().toLowerCase() || null;
67
+ const id = splitHeadingAttributes(h1Match[1]).id.toLowerCase() || null;
62
68
  if (capturing) break;
63
69
  if (id === wanted) {
64
70
  capturing = true;
@@ -81,3 +87,28 @@ export function renderSection(body, anchorId) {
81
87
  const slice = extractAnchorSection(body, anchorId);
82
88
  return slice ? renderFoundryMarkdown(slice) : "";
83
89
  }
90
+
91
+ /**
92
+ * Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
93
+ * top level, which {@link extractAnchorSection} cannot read.
94
+ *
95
+ * {@link module:engine/anchors.collectAnchors} finds the anchor at any
96
+ * level, because a lower heading legitimately starts a journal page. It does
97
+ * not feed an actor field: an H1-only extractor reads nothing from one, and a
98
+ * caller that trusts the empty result as "no such section" rather than
99
+ * "found, but unreadable" compiles a blank field with nothing to say why.
100
+ *
101
+ * @param {string} body - The note body.
102
+ * @param {string} anchorId - The anchor to check, case-insensitively.
103
+ * @param {number} [bodyLine=1] - The 1-based file line the body starts on.
104
+ * @returns {{line: number}|null} The misplaced anchor's line, or `null` when
105
+ * it is absent or already on an H1.
106
+ */
107
+ export function misplacedAnchorSection(body, anchorId, bodyLine = 1) {
108
+ const wanted = String(anchorId).toLowerCase();
109
+ const found = collectAnchors(body, bodyLine).find(
110
+ (anchor) => anchor.slug.toLowerCase() === wanted,
111
+ );
112
+ if (!found || found.level === 1) return null;
113
+ return { line: found.line };
114
+ }