@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.
- package/CHANGELOG.md +191 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +26 -3
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +73 -4
- package/docs/api.md +36 -18
- package/docs/authoring/dates-and-calendars.md +4 -2
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +237 -28
- package/docs/configuration.md +27 -1
- package/docs/content-format.md +3 -1
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +224 -46
- package/docs/reference/note-types.md +21 -4
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +10 -20
- package/engine/base-compiler.mjs +86 -21
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +0 -1
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-captions.mjs +57 -8
- package/engine/content-footnotes.mjs +189 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-index.mjs +2 -2
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/frontmatter-lint.mjs +47 -64
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +308 -0
- package/engine/helpers.mjs +14 -13
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +83 -8
- package/engine/journals.mjs +36 -17
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +178 -53
- package/engine/note-dates.mjs +49 -0
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +172 -37
- package/engine/page-lists.mjs +358 -0
- package/engine/pdf-build.mjs +76 -42
- package/engine/pdf-render.mjs +422 -49
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +10 -3
- package/engine/site-build.mjs +41 -12
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +18 -2
- package/engine/site-preview.mjs +18 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +12 -14
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +6 -3
- package/manifest.mjs +13 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +4 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/base-compiler.d.mts +19 -0
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-captions.d.mts +14 -1
- package/types/engine/content-footnotes.d.mts +73 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +174 -0
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/journals.d.mts +14 -7
- package/types/engine/markdown-expressions.d.mts +23 -0
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pdf-render.d.mts +72 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +13 -1
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +13 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +11 -12
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +3 -3
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-secrets.d.mts +0 -17
package/docs/configuration.md
CHANGED
|
@@ -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
|
package/docs/content-format.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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="
|
|
2412
|
-
<
|
|
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
|
-
|
|
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>
|
|
2421
|
-
<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
|
|
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}
|
|
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
|
|
2673
|
+
An entry carries two keys and nothing else:
|
|
2540
2674
|
|
|
2541
|
-
- **`rank`** — a **number**, the level on that body's own
|
|
2542
|
-
ladder. The body declares `{level, title, description}` for
|
|
2543
|
-
number indexes into it and the body stays the single source
|
|
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
|
-
|
|
2549
|
-
|
|
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` |
|
|
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`, `
|
|
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
|
|
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
|
|
19
|
-
* `{#dossier}
|
|
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
|
|
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
|
+
}
|