@heroiclands/package-build 23.9.0 → 23.11.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 (96) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/bin/content-commands.mjs +197 -68
  4. package/docs/api.md +365 -70
  5. package/docs/authoring/first-note.md +10 -0
  6. package/docs/authoring/frontmatter.md +1 -1
  7. package/docs/authoring/links-and-markup.md +186 -49
  8. package/docs/commands.md +92 -3
  9. package/docs/content-format.md +10 -3
  10. package/docs/design.md +18 -0
  11. package/docs/guides/book.md +7 -0
  12. package/docs/guides/site.md +7 -0
  13. package/docs/reference/format-details.md +186 -15
  14. package/docs/reference/note-types.md +4 -4
  15. package/engine/anchors.mjs +43 -9
  16. package/engine/base-compiler.mjs +6 -4
  17. package/engine/changelog-group.mjs +58 -34
  18. package/engine/changelog-lint.mjs +6 -2
  19. package/engine/compendiums.mjs +15 -1
  20. package/engine/compile-corpus.mjs +14 -1
  21. package/engine/content-charset.mjs +13 -1
  22. package/engine/content-html.mjs +18 -4
  23. package/engine/content-icons.mjs +15 -1
  24. package/engine/content-images.mjs +28 -9
  25. package/engine/content-index.mjs +102 -23
  26. package/engine/content-links.mjs +68 -30
  27. package/engine/content-session.mjs +247 -0
  28. package/engine/content-sources.mjs +110 -0
  29. package/engine/content-spans.mjs +118 -14
  30. package/engine/content-tables.mjs +1 -0
  31. package/engine/content-tasklists.mjs +20 -4
  32. package/engine/diagnostics.mjs +17 -6
  33. package/engine/extension-attributes.mjs +28 -5
  34. package/engine/first-use.mjs +371 -0
  35. package/engine/foundry-entries.mjs +12 -9
  36. package/engine/generate.mjs +13 -4
  37. package/engine/helpers.mjs +47 -16
  38. package/engine/index-records.mjs +1 -0
  39. package/engine/index.mjs +3 -0
  40. package/engine/journals.mjs +9 -5
  41. package/engine/language-data.mjs +2 -2
  42. package/engine/language-rows.mjs +4 -4
  43. package/engine/language-terms.mjs +4 -4
  44. package/engine/lexical-sections.mjs +5 -5
  45. package/engine/markup-validation.mjs +7 -2
  46. package/engine/metadata-index.mjs +1 -0
  47. package/engine/name-markup.mjs +114 -0
  48. package/engine/name-rows.mjs +70 -0
  49. package/engine/names.mjs +460 -0
  50. package/engine/note-addresses.mjs +25 -4
  51. package/engine/path-charset.mjs +24 -1
  52. package/engine/pdf-render.mjs +15 -1
  53. package/engine/site-index.mjs +25 -2
  54. package/engine/sql-tables.mjs +76 -15
  55. package/engine/stub-lint.mjs +4 -3
  56. package/engine/web-wikilinks.mjs +33 -12
  57. package/engine/wikilink-syntax.mjs +65 -8
  58. package/engine/wikilinks.mjs +29 -11
  59. package/engine/yaml-lint.mjs +3 -5
  60. package/hugo-theme/layouts/partials/footer.html +2 -0
  61. package/hugo-theme/static/css/style.css +20 -0
  62. package/hugo-theme/static/js/inline-spoilers.js +23 -0
  63. package/package.json +1 -1
  64. package/types/engine/anchors.d.mts +6 -3
  65. package/types/engine/compendiums.d.mts +5 -1
  66. package/types/engine/compile-corpus.d.mts +5 -2
  67. package/types/engine/content-charset.d.mts +5 -1
  68. package/types/engine/content-html.d.mts +5 -1
  69. package/types/engine/content-icons.d.mts +5 -1
  70. package/types/engine/content-images.d.mts +5 -1
  71. package/types/engine/content-index.d.mts +39 -19
  72. package/types/engine/content-links.d.mts +22 -8
  73. package/types/engine/content-session.d.mts +108 -0
  74. package/types/engine/content-sources.d.mts +32 -0
  75. package/types/engine/content-spans.d.mts +8 -6
  76. package/types/engine/content-tasklists.d.mts +5 -1
  77. package/types/engine/diagnostics.d.mts +10 -0
  78. package/types/engine/extension-attributes.d.mts +16 -2
  79. package/types/engine/first-use.d.mts +31 -0
  80. package/types/engine/foundry-entries.d.mts +11 -4
  81. package/types/engine/generate.d.mts +5 -1
  82. package/types/engine/helpers.d.mts +30 -7
  83. package/types/engine/index.d.mts +1 -0
  84. package/types/engine/journals.d.mts +5 -2
  85. package/types/engine/lexical-sections.d.mts +3 -2
  86. package/types/engine/markup-validation.d.mts +2 -1
  87. package/types/engine/name-markup.d.mts +176 -0
  88. package/types/engine/name-rows.d.mts +35 -0
  89. package/types/engine/names.d.mts +356 -0
  90. package/types/engine/note-addresses.d.mts +3 -3
  91. package/types/engine/path-charset.d.mts +16 -1
  92. package/types/engine/site-build.d.mts +2 -2
  93. package/types/engine/site-index.d.mts +3 -1
  94. package/types/engine/sql-tables.d.mts +7 -2
  95. package/types/engine/stub-lint.d.mts +3 -1
  96. package/types/engine/wikilink-syntax.d.mts +40 -3
package/docs/design.md CHANGED
@@ -21,6 +21,24 @@ An Address identifies a subject by package, system, type, and shortcode. It is i
21
21
 
22
22
  The content index uses JSON Lines. A record can retain the note's full frontmatter, anchors, and outgoing links, and streams without loading one giant JSON array. It is derived, disposable, and stable for a fixed source tree and toolchain version. SQL queries and editor navigation read that index rather than guessing from filenames.
23
23
 
24
+ ## Reference presentation and access
25
+
26
+ An indexed subject has one ordinary wikilink syntax regardless of its audience.
27
+ The renderer derives spoiler presentation from the actual destination, so an
28
+ author does not duplicate a privacy declaration on every mention. The `.show`
29
+ override reveals a particular reference without changing the destination's
30
+ audience. A separate inline `.secret` span marks authored text as a spoiler.
31
+
32
+ Concealment and access remain separate decisions. Public site and book builds
33
+ omit GM-only notes; a reference to an omitted destination carries no unusable
34
+ link. Foundry's private packs and page permissions govern destination access.
35
+ Web inline spoilers use a separate show/hide control beside their content, so
36
+ a revealed link is independently usable. Foundry and book inline spoilers are
37
+ visible labelled text; an inline Foundry span has no native secret-section
38
+ permission behavior. A spoiler in published HTML remains readable in the source. See the
39
+ [authoring contract](authoring/links-and-markup.md#references-to-secret-subjects)
40
+ for syntax and surface presentation.
41
+
24
42
  ## Build boundaries
25
43
 
26
44
  The content pass reads `assets/content/` and addressable assets. It emits the content index, page source, and Foundry documents. Site rendering and book typesetting consume those outputs; their layout decisions do not become fields of a Foundry DataModel. Packaging stages only the files a Foundry manifest declares. The build checks emitted fields against the target system schema because Foundry can otherwise discard an unknown field without an error.
@@ -522,6 +522,13 @@ Anchors on headings are scoped to their entries, so two entries can use the
522
522
  same heading anchor. The build reports an unresolved destination while
523
523
  keeping the source available for inspection.
524
524
 
525
+ A reference to a GM-only note or secret heading prints with secret styling
526
+ and a **Secret:** label. GM-only notes remain omitted, and references to them
527
+ print as text without a link. `.show` removes the automatic spoiler presentation
528
+ of that reference, without adding the destination to the book. An authored
529
+ `[text]{.secret}` uses the same static inline styling. Printed text remains
530
+ visible; see [secret references](../authoring/links-and-markup.md#references-to-secret-subjects).
531
+
525
532
  The [generated sections](../reference/format-details.md#generated-sections)
526
533
  an entry carries — **Within**, **Governed by**, **In song and story**, the
527
534
  event views and the rest — name notes the build chose rather than the author,
@@ -118,6 +118,13 @@ Wikilinks resolve through the address index. Each emitted page receives
118
118
  derived `related` entries for the links its author wrote and the backlinks
119
119
  other authors wrote to it.
120
120
 
121
+ A wikilink to a GM-only note or secret heading conceals its whole reference in
122
+ an inline show/hide spoiler. `.show` reveals the reference without changing the
123
+ destination's audience. GM-only pages remain omitted, so references to them
124
+ have no link; published secret sections retain theirs. Spoiler text remains in
125
+ the generated HTML and is readable in page source. See
126
+ [secret references](../authoring/links-and-markup.md#references-to-secret-subjects).
127
+
121
128
  What lies within a place, who governs it, what an affiliation governs, the
122
129
  works naming a note and the map from a place reach the page as
123
130
  [generated sections](../reference/format-details.md#generated-sections): H1
@@ -339,8 +339,11 @@ infoboxes:
339
339
  ### Frontmatter has three regions
340
340
 
341
341
  A HeroicLands note has opening YAML frontmatter with nonempty `shortcode` and
342
- `type` values. Other Markdown files are ordinary Markdown. A note's frontmatter
343
- has three regions:
342
+ `type` values. The opening and closing `---` delimiters accept LF or CRLF line
343
+ endings. Parsing keeps the original source text and diagnostic positions;
344
+ malformed YAML is an error with either line ending. The charset check recommends
345
+ LF and reports carriage returns as warnings. Other Markdown files are ordinary
346
+ Markdown. A note's frontmatter has three regions:
344
347
 
345
348
  | Region | Describes | Unknown keys |
346
349
  | -------------------- | ------------------------------------------------------- | ------------ |
@@ -512,7 +515,10 @@ is declared, so a near miss is a finding that names what you probably meant.
512
515
 
513
516
  `gm` marks a note for GM access. It is excluded from public site pages, SQL
514
517
  tables on the public site, and the book. Foundry compiles it only to private
515
- compendiums. A regular note cannot link to it; a GM note can.
518
+ compendiums. Any note may reference it with an ordinary wikilink; the
519
+ reference is automatically a spoiler. The `.show` override reveals that
520
+ reference without changing the destination's audience or access permissions;
521
+ see [references to secret subjects](#references-to-secret-subjects).
516
522
 
517
523
  **`draft` marks unfinished content.** It does two things and nothing
518
524
  else: a link _into_ a draft note renders marked, and the note itself states that
@@ -1805,6 +1811,120 @@ name at every citation with no link edited. `[[x|]]` is labelled; `[[x]]` is not
1805
1811
  It is the label that is required, not a target: the anchor-only form above
1806
1812
  carries its pipe like every other link.
1807
1813
 
1814
+ #### References to secret subjects
1815
+
1816
+ Use `[[doc-divineactn|The Truth of Divine Action]]` for an indexed subject,
1817
+ including one with a GM-only audience. The renderer derives spoiler presentation
1818
+ from the resolved destination and conceals the whole reference, including its
1819
+ label. A public destination has no automatic concealment. An anchor inside secret
1820
+ content is a secret destination, including a same-note anchor reference. A
1821
+ link to the whole public note does not inherit a section's secrecy.
1822
+
1823
+ The website renders an inline show/hide spoiler; an omitted GM-only target
1824
+ retains its reference as text without a dead link. A published secret section
1825
+ retains its link. Foundry renders a visible inline spoiler with a **Secret:**
1826
+ label, with access to its destination controlled by private packs and page
1827
+ permissions. Books print secret styling and a **Secret:** label, retaining text
1828
+ without a link when the GM-only destination is omitted. `.show` bypasses the
1829
+ automatic spoiler presentation on each surface; an unavailable destination
1830
+ still has no link.
1831
+
1832
+ An immediately following `.show` attribute explicitly reveals the reference:
1833
+
1834
+ ```markdown
1835
+ [[doc-divineactn|The Truth of Divine Action]]{.show}
1836
+ [[doc-magictruth|The Truth of Magic]]{.show}
1837
+ ```
1838
+
1839
+ This override changes the reference's presentation only. It does not publish a
1840
+ GM-only note, alter Foundry permissions or reveal the destination's content.
1841
+ It is redundant for public destinations. `[text]{.secret}` separately marks
1842
+ text itself as a spoiler; it declares no destination audience. See
1843
+ [inline spans](#inline-spans) for inline spoiler syntax and surface behavior.
1844
+
1845
+ #### Marking names in prose
1846
+
1847
+ `package-build lint` reports name-marking warnings in authored body prose.
1848
+ The warnings leave a valid package passing. Headings, frontmatter, tables,
1849
+ infoboxes, code, comments and embeds are outside this check.
1850
+
1851
+ A name is explicit: a `[Name]{.name}` bracket span or an authored link label.
1852
+ Use typed `.name` spans for unwritten subjects and ordinary wikilinks for
1853
+ indexed subjects, including private ones. Introduce another subject with its
1854
+ full canonical name at its first prose use; use plain text for the note's own
1855
+ subject and for later mentions and aliases.
1856
+ Plain prose is not searched for names. Wikilinks, ordinary Markdown links and
1857
+ reference links all supply names; a resolved empty wikilink label uses the
1858
+ note's full name. Images, code, footnote markers and bare autolinks without
1859
+ authored labels supply none.
1860
+
1861
+ 1. A `.name` span whose whole text matches an indexed `name.full` or
1862
+ `name.aliases` should be a link. Use `[[place-arden|Arden Vale]]` or
1863
+ `[[place-arden|]]` to introduce that note.
1864
+ 2. Mark or link each name at most once per note. An indexed full name and its
1865
+ declared aliases share one identity, including across different heading
1866
+ anchors. Other labels use their whole text as their identity: different
1867
+ labels linking to one page are separate names, and a label is not
1868
+ automatically an alias of its destination.
1869
+ 3. Introduce the full name before a short form or declared alias.
1870
+ Possessives and regular plurals count as uses; other indexed forms can be
1871
+ declared aliases.
1872
+
1873
+ Bold is emphasis. A whole bold span exactly matching an indexed full name or
1874
+ alias receives one advisory to mark or link the name. It supplies no name
1875
+ occurrence or introduction, and bold containing a marked or linked name
1876
+ receives no such advisory. Unknown bold phrases supply no names.
1877
+
1878
+ Inline Markdown remains active inside name spans, and name spans remain
1879
+ valid inside emphasis:
1880
+
1881
+ ```markdown
1882
+ **This is a phrase with [Joe Blow]{.name} in it**
1883
+ ```
1884
+
1885
+ The name is `Joe Blow`; the surrounding phrase is emphasis. Sites and books display
1886
+ `.name` text in bold.
1887
+
1888
+ Each affected occurrence produces one warning at its authored
1889
+ `file:line:column`, naming the full name or explicit label. A warning
1890
+ can describe several marking problems at that occurrence.
1891
+
1892
+ An optional `data-type` on a `.name` span states what the named thing is:
1893
+
1894
+ ```markdown
1895
+ [Ravenmoor]{.name data-type="place/settlement"}
1896
+ [Aldric Venn]{.name data-type="being/npc"}
1897
+ ```
1898
+
1899
+ Use a declared [note type](note-types.md), optionally followed by `/` and one
1900
+ of that type's subtypes. An enumerated declaration requires a listed subtype;
1901
+ an open declaration accepts a well-formed subtype, and an absent subtype
1902
+ declaration admits none. An unknown or malformed supplied value is an error
1903
+ at the `data-type` attribute's file, line and UTF-16 column. The same attribute
1904
+ on a span without the exact `name` class is an ordinary attribute. An untyped
1905
+ `.name` is valid; its type is not inferred from a matching note.
1906
+
1907
+ The index captures actual `.name` spans throughout the authored body, including
1908
+ headings, tables, verse and spans within links. The prose-only marking checks
1909
+ above keep their narrower scope. Frontmatter, literal code, escaped syntax,
1910
+ unmarked link labels, bold, plain prose and generated content add no names.
1911
+ The rendered visible text preserves its spelling and inline content.
1912
+
1913
+ Each own-note metadata record carries `names` in authored order. Every entry
1914
+ has exactly `name`, `type`, `subType`, `line` and `column`; absent
1915
+ classifications are explicit `null`, and no occurrences gives an empty array.
1916
+ This is derived metadata, not frontmatter. Documentation mirrors and assets
1917
+ do not duplicate a note's occurrences. The [`names` relation](#content-tables)
1918
+ flattens these entries with their containing NOTE Address.
1919
+
1920
+ Lint groups names without an indexed full-name or alias match by supplied
1921
+ classification and lists all their source uses. It also groups conflicting
1922
+ annotations of the same visible name. Untyped uses do not conflict. A bare
1923
+ type and its subtype agree; different supplied types or different supplied
1924
+ subtypes conflict. One package advisory contains both groups and is located
1925
+ at the earliest reported use. Advisory findings preserve a successful lint
1926
+ exit status. Supplied-value errors prevent publication and compilation.
1927
+
1808
1928
  ### Assets are types
1809
1929
 
1810
1930
  `icon`, `image` and `audio` are types in the type vocabulary, and an address
@@ -2911,7 +3031,7 @@ for the keys.
2911
3031
  | `frames` | `data.sounds.frames` | `frame`, `shape`, `class` (a list), `makes`, `examples` (a list of `{ root, form }`) |
2912
3032
  | `read_past` | `data.sounds.readPast` | `written`, `readAs`, `what` |
2913
3033
  | `sound_changes` | `data.soundChanges` | One row per step: `position` (the set's), `name`, `from_language`, `to_language`, `takes`, `step_position` (the step's in its set), `step`, `change`, `where` |
2914
- | `elements` | `data.elements` | `form`, `variants` (a list), `element_position` (the element's `position`), `kind`, `sense`, `examples` (a list), `word`, `genitive` |
3034
+ | `elements` | `data.elements` | `form`, `variants` (a list), `element_position` (the element's `position`), `kind`, `sense`, `examples` (a list), `word`, `joining` |
2915
3035
  | `roots` | `data.roots` | `root`, `sense`, `field` |
2916
3036
  | `cognates` | `data.cognates` | One row per form of a set: `position` (the set's), `gloss`, `proto`, `form_language` (the form's language note), `form` |
2917
3037
  | `calling_names` | `data.callingNames` | One row per closing: `form`, `doubled` (`true` or `false`), `usual`, and the language's `min_syllables` |
@@ -2922,7 +3042,7 @@ for the keys.
2922
3042
  | `name_shapes` | `data.naming.shapes` | `of`, `syllables_min`, `syllables_max`, `per_vowel_min`, `per_vowel_max` |
2923
3043
  | `name_edges` | `data.naming.edges` | `edge`, `sounds` (a list), `vowel` (`false` where unstated), `of` (`name` where unstated) |
2924
3044
  | `name_draws` | `data.naming.draws` | `kind`, `elements` (a list) |
2925
- | `held_from` | `data.naming.heldFrom` | `source`, `takes`, `clip`, `genitive` and `elements` (lists) |
3045
+ | `held_from` | `data.naming.heldFrom` | `source`, `takes`, `clip`, `joining` and `elements` (lists) |
2926
3046
  | `near_names` | `data.naming.nearName.examples` | `near`, `given`, and the language's `min_syllables` |
2927
3047
 
2928
3048
  A register name is carried by a note in the language's `data.scope` — any
@@ -2937,6 +3057,30 @@ office title, and by an anchor that has it as its name, which the row cites as
2937
3057
  `max(form) FILTER (WHERE form_language = 'thalorna-note-skill-helonclng') AS "Helonic"`,
2938
3058
  grouped by `position` and `gloss`.
2939
3059
 
3060
+ **`names`** is one row per actual `.name` span in an authored note body. Each
3061
+ row has these columns, including on an empty relation:
3062
+
3063
+ | Column | SQL type | Meaning |
3064
+ | --------- | --------- | -------------------------------------------------------------------------------------------- |
3065
+ | `owner` | `VARCHAR` | Containing authored page's canonical NOTE Address, or `NULL` when the record has no Address. |
3066
+ | `name` | `VARCHAR` | Rendered visible text, preserving its spelling. |
3067
+ | `type` | `VARCHAR` | Supplied note type, or `NULL`. |
3068
+ | `subType` | `VARCHAR` | Supplied subtype, or `NULL`. |
3069
+ | `line` | `BIGINT` | Original 1-based source line at the span's opening `[`. |
3070
+ | `column` | `BIGINT` | Original 1-based UTF-16 source column at that `[`. |
3071
+
3072
+ ```sql
3073
+ SELECT owner, name, type, subType, line, "column"
3074
+ FROM names
3075
+ WHERE type = 'place'
3076
+ ORDER BY owner, line, "column"
3077
+ ```
3078
+
3079
+ A native Item's source uses its NOTE page as `owner`. Declared dependency
3080
+ metadata supplies `names` in that package's SQL schema. Public queries exclude
3081
+ GM-only owners; all/native contexts retain them. Only authored occurrences
3082
+ contribute rows, so generated queries and sections add no further names.
3083
+
2940
3084
  **`language_lineage`** is one row per language note and each language it
2941
3085
  inherits words from through [`data.family`](#a-languages-words): `language`,
2942
3086
  `ancestor` and `depth`, with the language itself at depth `0`, its parent at
@@ -3374,6 +3518,28 @@ An attribute-bearing opener allows a nested div to be distinguished from the bar
3374
3518
  The watch wears [a _blue_ sash]{#watch-sash .uniform lang=en}.
3375
3519
  ```
3376
3520
 
3521
+ The exact `.name` class marks a name, with optional `data-type="type/subType"`;
3522
+ see [marking names in prose](#marking-names-in-prose) for the vocabulary and index.
3523
+ The exact `.secret` class marks the enclosed text as an inline spoiler:
3524
+
3525
+ ```markdown
3526
+ [The hidden **passage**]{.secret}
3527
+ [Aldric Venn]{.name .secret data-type="being/npc"}
3528
+ ```
3529
+
3530
+ The website provides an inline show/hide button and preserves interactive links
3531
+ inside the revealed content. Foundry prints a visible inline `secret` span
3532
+ with a **Secret:** label; the span does not withhold its text from players.
3533
+ The book prints the text with secret styling and a **Secret:** label.
3534
+ Inline emphasis and combined `.name` classification survive rendering. Published HTML retains the text, so opening
3535
+ the spoiler or inspecting the source reveals it.
3536
+
3537
+ This span has no effect on a destination's audience. Ordinary wikilinks derive
3538
+ spoilers from their resolved destinations; `.show` bypasses that automatic
3539
+ concealment without cancelling an enclosing authored `.secret`. An inline span
3540
+ does not change the scope of `.secret` headings or `:::secret` blocks, and it
3541
+ does not start or withhold a JournalEntryPage.
3542
+
3377
3543
  Inline Markdown remains active inside the span. Bracket spans have lower priority than URL links, image links, reference links, footnotes and wikilinks. A bracketed form immediately preceded by `!`, followed by `(` or another `[`, or beginning with `[[` is not a span, even when the link is unresolved. Ordinary links retain their existing syntax. Escape a bracket when it should be literal, or put examples inside a code span or fence. A span identifier addresses that point in the note; it does not start a journal page.
3378
3544
 
3379
3545
  ##### Alerts
@@ -5492,7 +5658,7 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5492
5658
  | `soundChanges[].steps[].step` | `string`, required | The step's label, as `A1` or `"1"`; no label stands twice in a set |
5493
5659
  | `soundChanges[].steps[].change` | `string`, required | The change in arrow notation, as `` `ph` > `f`; `kh` > `h` ``, or the operation it names in words |
5494
5660
  | `soundChanges[].steps[].where` | `string` | Where it holds, as `word end`; everywhere where absent |
5495
- | `elements` | `{ form, variants?, position?, kind?, sense, examples?, word?, genitive? }[]` | `language` only: the elements its names are built from |
5661
+ | `elements` | `{ form, variants?, position?, kind?, sense, examples?, word?, joining? }[]` | `language` only: the elements its names are built from |
5496
5662
  | `elements[].form` | `string`, required | The element, as `arn-` |
5497
5663
  | `elements[].variants` | `string[]` | The element's other forms, as `aldr-` beside `ald-` |
5498
5664
  | `elements[].position` | `opening`, `closing`, `either` or `link` | Where in a name it stands |
@@ -5500,7 +5666,7 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5500
5666
  | `elements[].sense` | `string`, required | What it means |
5501
5667
  | `elements[].examples` | `string[]` | The names it is read from |
5502
5668
  | `elements[].word` | `string` | The word it is the stem of, a form of `words` of the language or an ancestor |
5503
- | `elements[].genitive` | `string` | The element in the genitive, as `-borgar`, where a name governing another word takes it |
5669
+ | `elements[].joining` | `string` | The element's joining form, as `-borgar`, when it qualifies another word |
5504
5670
  | `roots` | `{ root, sense, field? }[]` | `language` only: the consonantal roots its words are set from |
5505
5671
  | `roots[].root` | `string`, required | The root, as `d-l-k` |
5506
5672
  | `roots[].sense` | `string`, required | What it means |
@@ -5540,11 +5706,11 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5540
5706
  | `naming.draws` | `{ kind, elements }[]` | The element kinds each kind of name is built from |
5541
5707
  | `naming.draws[].kind` | `byname`, `given` or `place`, required | The kind of name; once per kind |
5542
5708
  | `naming.draws[].elements` | list of `byname`, `given` or `place`, required | The element kinds, as `elements[].kind` states them |
5543
- | `naming.heldFrom` | `{ source, takes, clip?, genitive?, elements? }[]` | What a place name takes first, by what the place is held from |
5709
+ | `naming.heldFrom` | `{ source, takes, clip?, joining?, elements? }[]` | What a place name takes first, by what the place is held from |
5544
5710
  | `naming.heldFrom[].source` | `string`, required | What the place is held from, as `a founder` |
5545
5711
  | `naming.heldFrom[].takes` | `stem`, `godName`, `givenName` or `element`, required | What stands first: a name-stem in any grade, a god's name, a founder's given name, or one of `elements` |
5546
5712
  | `naming.heldFrom[].clip` | `first` | How the name is clipped: to its first element; whole where absent. A name row only |
5547
- | `naming.heldFrom[].genitive` | `string[]` | The genitive endings, one of which stands at the seam after the name. A name row only |
5713
+ | `naming.heldFrom[].joining` | `string[]` | The joining endings, one of which stands at the seam after the name. A name row only |
5548
5714
  | `naming.heldFrom[].elements` | `string[]` | The elements that may stand first; required on, and only on, a row that takes an `element` |
5549
5715
  | `naming.nearName` | `{ minSyllables, examples? }` | The near name a long given name carries |
5550
5716
  | `naming.nearName.minSyllables` | `integer`, required | The fewest syllables a given name has that carries a near name |
@@ -5856,7 +6022,7 @@ data:
5856
6022
  - { kind: byname, elements: [byname, place] }
5857
6023
  heldFrom:
5858
6024
  - { source: a god, takes: godName, clip: first }
5859
- - { source: a founder, takes: givenName, genitive: ["-s", "-a"] }
6025
+ - { source: a founder, takes: givenName, joining: ["-s", "-a"] }
5860
6026
  - { source: the ting and its law, takes: element, elements: ["ting-", "lög-"] }
5861
6027
  nearName:
5862
6028
  minSyllables: 4
@@ -5972,9 +6138,8 @@ form and its other forms, where in a name it stands (`opening`, `closing`,
5972
6138
  `either` or `link`), its sense and the names it is read from. In a language
5973
6139
  that keeps one set of elements for each kind of name, `kind` says which
5974
6140
  (`byname`, `given` or `place`). `word` names the word of `words` the element is
5975
- the stem of, the language's or an ancestor's, and `genitive` gives the element
5976
- in the genitive where a name governing another word takes it. An element that
5977
- is also a word stays a word.
6141
+ the stem of, the language's or an ancestor's, and `joining` gives its form
6142
+ when it qualifies another word. An element that is also a word stays a word.
5978
6143
 
5979
6144
  **`roots` lists consonantal roots**, each with its sense and the field it
5980
6145
  belongs to. **`cognates`** lists, on the eldest language of a set, the form one
@@ -6007,7 +6172,7 @@ there too and `of` where the edge holds for one class of name only; an edge
6007
6172
  stands once for each class. `draws` gives the element kinds each kind of name
6008
6173
  is built from. `heldFrom` gives what a place name takes first by what the
6009
6174
  place is held from: a name-stem, a god's name, a founder's given name — that
6010
- name `clip`ped to its `first` element, or followed by one of its `genitive`
6175
+ name `clip`ped to its `first` element, or followed by one of its `joining`
6011
6176
  endings — or one of the `elements` listed. `nearName` states the fewest
6012
6177
  syllables a given name has that carries a near name, with worked examples.
6013
6178
 
@@ -6056,7 +6221,7 @@ Each finding is located at the key at fault:
6056
6221
  | A naming edge stated twice for one class of name, at the second | error |
6057
6222
  | A kind of name whose draws are stated twice, at the second | error |
6058
6223
  | A held-from row that takes an `element` and names no `elements`, at the row | error |
6059
- | `elements` on a held-from row that takes no element, or `clip` or `genitive` on one that takes no name, at the key | error |
6224
+ | `elements` on a held-from row that takes no element, or `clip` or `joining` on one that takes no name, at the key | error |
6060
6225
  | A `scope` Address that does not resolve, or a type that is no note type | error |
6061
6226
  | A tongue `language` or a cognate form's language that is no language note | error |
6062
6227
  | A register row with neither a `tongue` nor a `kind`, at the row | error |
@@ -6391,6 +6556,12 @@ word.
6391
6556
  them, then its culture's `data.language`;
6392
6557
  3. on a culture, its own `data.language`.
6393
6558
 
6559
+ On lore and affiliation notes, `data.language` and
6560
+ `data.glossary[].language` name language notes. Use `skill-provnzlng` for a
6561
+ language in this package, or `<pkg>-note-skill-provnzlng` for its complete
6562
+ NOTE Address. Any other system segment, including `sohl`, is an error at the
6563
+ authored value. A SOHL skill Address names a compiled Item.
6564
+
6394
6565
  An affiliation's culture is the one it states or inherits, as
6395
6566
  [Culture in the index](#culture-in-the-index) describes, and a culture's
6396
6567
  `data.language` includes one it inherits from its
@@ -474,7 +474,7 @@ These fields are accepted by every note type. A field's value can still be irrel
474
474
  | `data.scope` | a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }` | The notes the register covers. A language skill note only. |
475
475
  | `data.sounds` | a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, joins?, marks?, skeleton?, frames?, readPast?, mutations? }` | The letters the language writes and the rules its words keep. A language skill note only. |
476
476
  | `data.soundChanges` | list of `{ name, from?, to?, takes?, steps }` | Named sets of ordered changes, each making one tongue's words from another's. A language skill note only. |
477
- | `data.elements` | list of `{ form, variants?, position?, kind?, sense, examples?, word?, genitive? }` | The elements the language's names are built from. A language skill note only. |
477
+ | `data.elements` | list of `{ form, variants?, position?, kind?, sense, examples?, word?, joining? }` | The elements the language's names are built from. A language skill note only. |
478
478
  | `data.roots` | list of `{ root, sense, field? }` | The consonantal roots the language's words are set from. A language skill note only. |
479
479
  | `data.cognates` | list of `{ forms, proto?, gloss }` | Sets of forms one older word takes in several languages, stated on the eldest. A language skill note only. |
480
480
  | `data.callingNames` | a map — `{ closings, minSyllables }` | The short name a long given name is called by. A language skill note only. |
@@ -635,7 +635,7 @@ These fields are accepted by every note type. A field's value can still be irrel
635
635
  | `data.elements[].sense` | a string | yes | What the element means. |
636
636
  | `data.elements[].examples` | a list of names | no | The names the element is read from. |
637
637
  | `data.elements[].word` | a form of the language's words | no | The word the element is the stem of, a form of `data.words` of the language or an ancestor. |
638
- | `data.elements[].genitive` | the element as written, such as `-borgar` | no | The element in the genitive, where a name governing another word takes it. |
638
+ | `data.elements[].joining` | the element as written, such as `-borgar` | no | The element's joining form, as written when it qualifies another word. |
639
639
  | `data.roots[].root` | a consonantal skeleton, such as `d-l-k` | yes | The root. |
640
640
  | `data.roots[].sense` | a string | yes | What the root means. |
641
641
  | `data.roots[].field` | a string, such as `stone` | no | The field the root belongs to. |
@@ -668,11 +668,11 @@ These fields are accepted by every note type. A field's value can still be irrel
668
668
  | `data.naming.draws` | list of `{ kind, elements }` | no | The element kinds each kind of name is built from. |
669
669
  | `data.naming.draws[].kind` | one of byname, given, place | yes | The kind of name; once per kind. |
670
670
  | `data.naming.draws[].elements` | a list of element kinds | yes | The kinds of element, as `data.elements` states them, the name is built from. |
671
- | `data.naming.heldFrom` | list of `{ source, takes, clip?, genitive?, elements? }` | no | What a place name takes first, by what the place is held from. |
671
+ | `data.naming.heldFrom` | list of `{ source, takes, clip?, joining?, elements? }` | no | What a place name takes first, by what the place is held from. |
672
672
  | `data.naming.heldFrom[].source` | a string, such as `a founder` | yes | What the place is held from. |
673
673
  | `data.naming.heldFrom[].takes` | one of stem, godName, givenName, element | yes | What stands first: a name-stem in any grade, a god's name, a founder's given name, or one of `elements`. |
674
674
  | `data.naming.heldFrom[].clip` | one of first | no | How the name is clipped: `first`, to its first element; whole where absent. |
675
- | `data.naming.heldFrom[].genitive` | a list of endings | no | The genitive endings, one of which stands at the seam after the name. |
675
+ | `data.naming.heldFrom[].joining` | a list of endings | no | The joining endings, one of which stands at the seam after the name. |
676
676
  | `data.naming.heldFrom[].elements` | a list of elements | no | The elements that may stand first, for a row that takes an `element`. |
677
677
  | `data.naming.nearName` | a map — `{ minSyllables, examples? }` | no | The near name a long given name carries. |
678
678
  | `data.naming.nearName.minSyllables` | a whole number | yes | The fewest syllables a given name has that carries a near name. |
@@ -41,7 +41,7 @@ import { scanPoetry } from "./content-poetry.mjs";
41
41
  import { scanSpans } from "./content-spans.mjs";
42
42
  import { slugify } from "./content-slug.mjs";
43
43
  import { scanFigures } from "./content-figures.mjs";
44
- import { scanHeadingAttributes } from "./heading-attributes.mjs";
44
+ import { withheldSections, scanHeadingAttributes } from "./heading-attributes.mjs";
45
45
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
46
46
 
47
47
  /**
@@ -65,12 +65,14 @@ export const ANCHOR_KINDS = Object.freeze(["prose", "event"]);
65
65
  * picture's address to the role its asset declares, so a figure anchor's
66
66
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
67
67
  * {@link module:engine/content-figures.scanFigures}.
68
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
68
69
  * @returns {Array<{slug: string, name: string, level: number, line: number,
69
- * kind: string}>} In document order.
70
+ * kind: string, secret?: boolean}>} In document order.
70
71
  */
71
- export function collectAnchors(body, bodyLine = 1, resolveRole) {
72
+ export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
72
73
  const anchors = [];
73
- for (const heading of scanHeadingAttributes(body, bodyLine).headings) {
74
+ const headings = scanHeadingAttributes(body, bodyLine).headings;
75
+ for (const heading of headings) {
74
76
  if (!heading.id) continue;
75
77
  anchors.push({
76
78
  slug: heading.id,
@@ -90,9 +92,12 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
90
92
  kind: "prose",
91
93
  });
92
94
  }
95
+ const secretBlocks = [];
93
96
  const blockAnchors = (source, firstLine) => {
94
97
  const sourceLines = source.split("\n");
95
- for (const block of scanBlocks(source).blocks) {
98
+ for (const block of (syntax ? syntax(source).blocks : scanBlocks(source)).blocks) {
99
+ if (block.name === "secret")
100
+ secretBlocks.push({ start: firstLine + block.start, end: firstLine + block.end });
96
101
  if (block.id)
97
102
  anchors.push({
98
103
  slug: slugify(block.id),
@@ -108,7 +113,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
108
113
  }
109
114
  };
110
115
  blockAnchors(String(body ?? ""), bodyLine);
111
- for (const alert of scanAlerts(body).blocks) {
116
+ for (const alert of (syntax ? syntax(body).alerts : scanAlerts(body)).blocks) {
112
117
  if (alert.id)
113
118
  anchors.push({
114
119
  slug: slugify(alert.id),
@@ -128,7 +133,8 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
128
133
  kind: "prose",
129
134
  });
130
135
  }
131
- for (const span of scanSpans(body).spans) {
136
+ const spans = (syntax ? syntax(body).spans : scanSpans(body)).spans;
137
+ for (const span of spans) {
132
138
  if (span.id)
133
139
  anchors.push({
134
140
  slug: slugify(span.id),
@@ -138,17 +144,45 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
138
144
  kind: "prose",
139
145
  });
140
146
  }
147
+ const sections =
148
+ headings.some((heading) => heading.classes.includes("secret")) ?
149
+ withheldSections(body).sections
150
+ : [];
151
+ const secretSpans = spans.filter((span) =>
152
+ span.attrs.some(([key, value]) => key === "class" && value.split(/\s+/).includes("secret")),
153
+ );
154
+ const secretSpanAnchors = new Set(
155
+ spans
156
+ .filter(
157
+ (target) =>
158
+ target.id &&
159
+ secretSpans.some(
160
+ (parent) => parent.start <= target.start && parent.end >= target.end,
161
+ ),
162
+ )
163
+ .map((span) => span.id),
164
+ );
165
+ for (const anchor of anchors) {
166
+ const line = anchor.line - bodyLine;
167
+ if (
168
+ sections.some((section) => line >= section.start && line < section.end) ||
169
+ secretBlocks.some((block) => anchor.line >= block.start && anchor.line <= block.end) ||
170
+ secretSpanAnchors.has(anchor.slug)
171
+ )
172
+ anchor.secret = true;
173
+ }
141
174
  return anchors.sort((a, b) => a.line - b.line);
142
175
  }
143
176
 
144
177
  /** Collisions involving a div, verse or inline-span anchor.
145
178
  * Heading and caption collisions are diagnosed by their existing scanners.
146
179
  * @param {string} body - Authored Markdown.
180
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
147
181
  * @returns {Array<{line: number, column: number, message: string}>} Located collisions.
148
182
  */
149
- export function markupAnchorFindings(body) {
183
+ export function markupAnchorFindings(body, syntax) {
150
184
  const groups = new Map();
151
- for (const anchor of collectAnchors(body)) {
185
+ for (const anchor of collectAnchors(body, 1, undefined, syntax)) {
152
186
  const slug = slugify(anchor.slug);
153
187
  if (!groups.has(slug)) groups.set(slug, []);
154
188
  groups.get(slug).push(anchor);
@@ -686,11 +686,13 @@ export class BasePackCompiler {
686
686
  // it made — `errors` or `findings`, never just the first — so nothing
687
687
  // here re-scans; it only stops taking `[0]`.
688
688
  const findings = [];
689
+ const syntax = this.corpus?.syntax;
690
+ const scanned = syntax?.(body);
689
691
  for (const secretError of [
690
- ...scanBlocks(body).errors,
691
- ...scanAlerts(body).errors,
692
- ...scanSpans(body).errors,
693
- ...markupAnchorFindings(body),
692
+ ...(scanned?.blocks ?? scanBlocks(body)).errors,
693
+ ...(scanned?.alerts ?? scanAlerts(body)).errors,
694
+ ...(scanned?.spans ?? scanSpans(body)).errors,
695
+ ...markupAnchorFindings(body, syntax),
694
696
  ]) {
695
697
  findings.push({
696
698
  message: secretError.message,