@heroiclands/package-build 23.7.0 → 23.8.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 (71) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +32 -0
  3. package/bin/content-commands.mjs +20 -6
  4. package/ci/check-banners.mjs +37 -19
  5. package/container.mjs +48 -10
  6. package/docs/api.md +87 -43
  7. package/docs/authoring/frontmatter.md +30 -1
  8. package/docs/authoring/links-and-markup.md +229 -36
  9. package/docs/commands.md +12 -2
  10. package/docs/content-format.md +24 -1
  11. package/docs/design.md +31 -0
  12. package/docs/guides/e2e.md +37 -1
  13. package/docs/project-setup.md +30 -15
  14. package/docs/reference/format-details.md +418 -220
  15. package/docs/reference/note-types.md +90 -31
  16. package/e2e.mjs +89 -2
  17. package/engine/base-compiler.mjs +11 -18
  18. package/engine/compile-corpus.mjs +2 -1
  19. package/engine/content-alerts.mjs +4 -2
  20. package/engine/content-blocks.mjs +18 -36
  21. package/engine/content-format.yaml +3 -0
  22. package/engine/content-index.mjs +35 -22
  23. package/engine/content-poetry.mjs +9 -0
  24. package/engine/diagnostics.mjs +24 -11
  25. package/engine/frontmatter-lint.mjs +1 -1
  26. package/engine/generate.mjs +12 -22
  27. package/engine/helpers.mjs +8 -6
  28. package/engine/infobox.mjs +3 -0
  29. package/engine/journals.mjs +11 -1
  30. package/engine/language-data.mjs +236 -1
  31. package/engine/language-rows.mjs +235 -3
  32. package/engine/language-terms.mjs +524 -9
  33. package/engine/language-words.mjs +75 -0
  34. package/engine/markdownlint-run.mjs +12 -2
  35. package/engine/markup-validation.mjs +34 -0
  36. package/engine/note-addresses.mjs +1 -0
  37. package/engine/note-claims.mjs +19 -10
  38. package/engine/note-package.mjs +3 -5
  39. package/engine/note-vocabulary.mjs +29 -1
  40. package/engine/pdf-build.mjs +8 -1
  41. package/engine/prose-lint.mjs +12 -6
  42. package/engine/related-pages.mjs +22 -3
  43. package/engine/see-also.mjs +150 -0
  44. package/engine/site-build.mjs +5 -1
  45. package/engine/site-preview.mjs +4 -2
  46. package/engine/sql-tables.mjs +37 -12
  47. package/engine/standings.mjs +14 -12
  48. package/engine/word-rows.mjs +8 -3
  49. package/engine/word-terms.mjs +60 -2
  50. package/hugo-theme/layouts/partials/related.html +25 -8
  51. package/package.json +2 -1
  52. package/types/container.d.mts +4 -3
  53. package/types/e2e.d.mts +28 -0
  54. package/types/engine/base-compiler.d.mts +2 -2
  55. package/types/engine/compile-corpus.d.mts +2 -1
  56. package/types/engine/content-index.d.mts +20 -7
  57. package/types/engine/diagnostics.d.mts +7 -6
  58. package/types/engine/helpers.d.mts +7 -4
  59. package/types/engine/language-rows.d.mts +89 -3
  60. package/types/engine/language-terms.d.mts +106 -4
  61. package/types/engine/markdownlint-run.d.mts +3 -1
  62. package/types/engine/markup-validation.d.mts +14 -0
  63. package/types/engine/note-addresses.d.mts +10 -0
  64. package/types/engine/note-claims.d.mts +7 -1
  65. package/types/engine/note-package.d.mts +3 -1
  66. package/types/engine/note-vocabulary.d.mts +1 -0
  67. package/types/engine/related-pages.d.mts +10 -1
  68. package/types/engine/see-also.d.mts +55 -0
  69. package/types/engine/sql-tables.d.mts +7 -3
  70. package/types/engine/word-rows.d.mts +2 -1
  71. package/types/engine/word-terms.d.mts +11 -0
@@ -34,6 +34,7 @@ These fields are accepted by every note type. A field's value can still be irrel
34
34
 
35
35
  | Field | Shape | Meaning |
36
36
  | ----------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
37
+ | `data.seeAlso` | list of `{ address, gloss? }` | Deliberate cross-references in reading order, with optional glosses; shown on the site, in journals and in books. |
37
38
  | `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
38
39
  | `data.pack` | string | The shared compendium route, with a system block overriding it. |
39
40
  | `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
@@ -42,10 +43,12 @@ These fields are accepted by every note type. A field's value can still be irrel
42
43
 
43
44
  **Inner keys** — a key a field's value does not declare is an error at its own line and column.
44
45
 
45
- | Key | Shape | Required | Meaning |
46
- | ----------------------- | --------------- | -------- | --------------------------------------------- |
47
- | `data.harnworld.realm` | a string | no | The HârnWorld realm the subject belongs to. |
48
- | `data.harnworld.ritual` | list of strings | no | The HârnWorld religions the subject observes. |
46
+ | Key | Shape | Required | Meaning |
47
+ | ------------------------ | --------------- | -------- | -------------------------------------------------- |
48
+ | `data.seeAlso[].address` | a note Address | yes | The note to read next. |
49
+ | `data.seeAlso[].gloss` | a string | no | A short phrase saying why to follow the reference. |
50
+ | `data.harnworld.realm` | a string | no | The HârnWorld realm the subject belongs to. |
51
+ | `data.harnworld.ritual` | list of strings | no | The HârnWorld religions the subject observes. |
49
52
 
50
53
  ## Type-specific fields
51
54
 
@@ -451,29 +454,32 @@ These fields are accepted by every note type. A field's value can still be irrel
451
454
 
452
455
  **Subtypes:** `social`, `nature`, `craft`, `lore`, `language`, `script`, `mystical`, `physical`, `combat`, `combattechnique`.
453
456
 
454
- | Field | Shape | Meaning |
455
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
456
- | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
457
- | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
458
- | `data.masteryLevel` | number | Mastery before any modifier. |
459
- | `data.parentSkill` | an Address | The skill this one specialises. |
460
- | `data.names` | a language's name lists | How this language's speakers are named, for `package-build npc new`: `pattern` builds a name from placeholders such as "{given} {clan}" and literal text, `given` maps a gender to a list of given names, `female` and `male` required, and every other key is a list of names a placeholder of the same name draws from. A language skill note only. See [name lists](format-details.md#name-lists). |
461
- | `data.family` | an Address | The parent language, as a language skill note: this language reads the parent's words, and its parents' in turn, as its own. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
462
- | `data.abbreviation` | a string, such as `Prov.` | The tag a dictionary prints for the language, beside an entry's class and in an etymology, as `Prov.`; the language's full name where unstated. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
463
- | `data.classModifiers` | list of word class modifiers | The class modifiers the language uses, from the master word classes. A word's class may carry only these. Empty when unstated. A language skill note only. |
464
- | `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
465
- | `data.words` | list of `{ form, class, field, gloss, … }` | The language's words, one map each, in the order they print. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
466
- | `data.affixes` | list of `{ form, gloss, attaches?, examples? }` | The affixes a word's `built` pieces may name, each with its gloss. A language skill note only. |
467
- | `data.tongues` | list of `{ tag, meaning, language? }` | The tags a register row's `tongue` may name, each with what it means. A language skill note only. |
468
- | `data.register` | list of `{ name, tongue?, kind?, built?, why? }` | Every name the setting gives in the language's reach, with its tongue and pieces; the notes carrying each name are derived. A language skill note only. |
469
- | `data.retired` | list of `{ form, instead?, why }` | The forms the setting does not write, and why. A language skill note only. |
470
- | `data.scope` | a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }` | The notes the register covers. A language skill note only. |
471
- | `data.sounds` | a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, joins?, marks?, skeleton?, frames?, readPast? }` | The letters the language writes and the rules its words keep. A language skill note only. |
472
- | `data.soundChanges` | list of `{ name, from?, to?, steps }` | Named sets of ordered changes, each making one tongue's words from another's. A language skill note only. |
473
- | `data.elements` | list of `{ form, variants?, position?, sense, examples? }` | The elements the language's names are built from. A language skill note only. |
474
- | `data.roots` | list of `{ root, sense, field? }` | The consonantal roots the language's words are set from. A language skill note only. |
475
- | `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. |
476
- | `data.callingNames` | a map — `{ closings, minSyllables }` | The short name a long given name is called by. A language skill note only. |
457
+ | Field | Shape | Meaning |
458
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
459
+ | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
460
+ | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
461
+ | `data.masteryLevel` | number | Mastery before any modifier. |
462
+ | `data.parentSkill` | an Address | The skill this one specialises. |
463
+ | `data.names` | a language's name lists | How this language's speakers are named, for `package-build npc new`: `pattern` builds a name from placeholders such as "{given} {clan}" and literal text, `given` maps a gender to a list of given names, `female` and `male` required, and every other key is a list of names a placeholder of the same name draws from. A language skill note only. See [name lists](format-details.md#name-lists). |
464
+ | `data.family` | an Address | The parent language, as a language skill note: this language reads the parent's words, and its parents' in turn, as its own. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
465
+ | `data.abbreviation` | a string, such as `Prov.` | The tag a dictionary prints for the language, beside an entry's class and in an etymology, as `Prov.`; the language's full name where unstated. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
466
+ | `data.classModifiers` | list of word class modifiers | The class modifiers the language uses, from the master word classes. A word's class may carry only these. Empty when unstated. A language skill note only. |
467
+ | `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
468
+ | `data.words` | list of `{ form, class, field, gloss, … }` | The language's words, one map each, in the order they print. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
469
+ | `data.affixes` | list of `{ form, gloss, attaches?, examples? }` | The affixes a word's `built` pieces may name, each with its gloss. A language skill note only. |
470
+ | `data.tongues` | list of `{ tag, meaning, language? }` | The tags a register row's `tongue` may name, each with what it means. A language skill note only. |
471
+ | `data.register` | list of `{ name, tongue?, kind?, built?, gloss?, why? }` | Every name the setting gives in the language's reach, with its tongue and pieces; the notes carrying each name are derived. A language skill note only. |
472
+ | `data.retired` | list of `{ form, instead?, why }` | The forms the setting does not write, and why. A language skill note only. |
473
+ | `data.scope` | a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }` | The notes the register covers. A language skill note only. |
474
+ | `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. |
475
+ | `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. |
476
+ | `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.roots` | list of `{ root, sense, field? }` | The consonantal roots the language's words are set from. A language skill note only. |
478
+ | `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. |
479
+ | `data.callingNames` | a map — `{ closings, minSyllables }` | The short name a long given name is called by. A language skill note only. |
480
+ | `data.older` | list of `{ form, gloss?, layer? }` | The words and names older than the language's rules, which no sound or name rule holds. A language skill note only. |
481
+ | `data.negation` | a map — `{ particle, mutation? }` | How a verb is negated. A language skill note only. |
482
+ | `data.naming` | a map — `{ shapes?, edges?, draws?, heldFrom?, nearName? }` | The shapes, edges and sources the language's names keep beyond its words' rules. A language skill note only. |
477
483
 
478
484
  **Inner keys** — a key a field's value does not declare is an error at its own line and column.
479
485
 
@@ -499,13 +505,18 @@ These fields are accepted by every note type. A field's value can still be irrel
499
505
  | `data.words[].tones` | a string | no | A tone pattern, such as `LHL`. |
500
506
  | `data.words[].root` | a string | no | A consonantal skeleton, such as `d-l-k`. |
501
507
  | `data.words[].frame` | a frame of the language's `sounds.frames` | no | The frame a root is set in, named in the language's `sounds.frames`. |
502
- | `data.affixes[].form` | the affix as written, such as `-rith` | yes | The affix as a word's `built` names it. |
508
+ | `data.affixes[].form` | the affix as written, such as `-rith` | no | The affix as a word's `built` names it; absent on a zero affix. |
509
+ | `data.affixes[].zero` | `true` | no | The affix is written as nothing, such as a case with no ending; it states no `form`, prints as `∅`, and no `built` piece names it. |
510
+ | `data.affixes[].kind` | one of case, derivation, tense, aspect, mood, voice, name, lineage | no | What the affix makes. |
511
+ | `data.affixes[].given` | one of man, woman, either | no | Who a given name closing on it belongs to; an affix of kind `name` only. |
512
+ | `data.affixes[].where` | a string, such as `before a stop` | no | Where this form stands, for an affix written two ways; everywhere where absent. |
503
513
  | `data.affixes[].gloss` | a string | yes | What the affix means. |
504
514
  | `data.affixes[].attaches` | a string | no | How it attaches, in words, such as `replaces a closing vowel`. |
505
515
  | `data.affixes[].replaces` | `vowel`, or the letters, such as `a` | no | What it takes the place of at the close of the stem: `vowel` for any closing vowel, or the letters it replaces; added to the whole stem where absent. |
506
516
  | `data.affixes[].lengthens` | `true` or `false` | no | Whether the stem's last vowel lengthens before the affix; `false` where unstated. An affix that `replaces` the stem's close does not also lengthen it. |
507
517
  | `data.affixes[].front` | the affix as written, such as `-ür` | no | Its form after front vowels, where the language keeps vowel harmony. |
508
518
  | `data.affixes[].examples` | a list of strings | no | The names the affix is read from. |
519
+ | `data.affixes[].barredAfter` | a list of sounds | no | The sounds a stem may not close on to take the affix. |
509
520
  | `data.affixes[].tones` | a tone pattern, such as `L` | no | The tone pattern the affix carries, written as a word's `tones` is. |
510
521
  | `data.tongues[].tag` | a tag such as `dunhari`, or `exonym:<tongue>` | yes | The tag a register row's `tongue` names: lowercase letters and digits, or a tag closing on `:<tongue>`, which declares one tag per tongue named after the colon. |
511
522
  | `data.tongues[].meaning` | a string | yes | What a name of this tongue is. |
@@ -514,6 +525,7 @@ These fields are accepted by every note type. A field's value can still be irrel
514
525
  | `data.register[].tongue` | a tag of `data.tongues`, or `translation` | no | The tongue the name is in, as a tag `data.tongues` declares, or `translation`. A row states a `tongue`, a `kind`, or both. |
515
526
  | `data.register[].kind` | one of byname, given, place | no | What kind of name it is, in a language that sorts its names by kind. |
516
527
  | `data.register[].built` | a list of pieces | no | The pieces the name is built from, in order, as a word's `built` writes them. |
528
+ | `data.register[].gloss` | a string | no | What the name means, where the pages give its sense. |
517
529
  | `data.register[].why` | a string | no | Why the name stands outside the pattern the language's names are built by. |
518
530
  | `data.retired[].form` | a string | yes | The form the setting does not write. |
519
531
  | `data.retired[].instead` | a string, or a list of them | no | What is written in its place, one form or several; absent where nothing is. |
@@ -529,13 +541,18 @@ These fields are accepted by every note type. A field's value can still be irrel
529
541
  | `data.sounds.letters` | list of `{ letters, kind, like, tongue? }` | no | The letters the language writes, and the sounds they stand for. |
530
542
  | `data.sounds.letters[].letters` | a list of letters | yes | The letters or digraphs the row describes, as written. |
531
543
  | `data.sounds.letters[].kind` | a string, such as `consonant` or `long vowel` | yes | What kind of sound they write. |
532
- | `data.sounds.letters[].like` | a string | yes | What they sound like. |
544
+ | `data.sounds.letters[].like` | a string | no | What they sound like; absent where the row states only their kind and place. |
545
+ | `data.sounds.letters[].place` | a string, such as `lips` or `tongue-tip` | no | Where in the mouth they are made. |
533
546
  | `data.sounds.letters[].tongue` | a tag, or a list of them | no | The tongues that write them, as tags `data.tongues` declares; every one where absent. |
534
547
  | `data.sounds.edges` | list of `{ edge, sounds }` | no | The sounds that may stand at each edge of a word, one entry per edge. |
535
548
  | `data.sounds.edges[].edge` | one of openConsonant, openCluster, closeConsonant, closeCluster, innerCluster, vowelPair, glide, syllabic | yes | Which edge of a word the sounds stand at. |
536
549
  | `data.sounds.edges[].sounds` | a list of sounds, or `any` | yes | The sounds, as written; `any` for every one, and an empty list for none. |
537
- | `data.sounds.clusters` | a map — `{ max }` | no | How many consonants may stand together. |
550
+ | `data.sounds.clusters` | a map — `{ max, groups? }` | no | How many consonants may stand together. |
538
551
  | `data.sounds.clusters.max` | a whole number | yes | The most consonants that stand together inside a word. |
552
+ | `data.sounds.clusters.groups` | list of `{ what, clusters, use }` | no | The clusters that may stand inside a word, in groups; a language stating them states no `innerCluster` edge. |
553
+ | `data.sounds.clusters.groups[].what` | a string, such as `a nasal and a fricative` | yes | What the clusters of the group are made of. |
554
+ | `data.sounds.clusters.groups[].clusters` | a list of clusters | yes | The clusters, as written; a digraph counts as one consonant. |
555
+ | `data.sounds.clusters.groups[].use` | one of common, sparing | yes | How often the group's clusters stand inside a word. |
539
556
  | `data.sounds.stress` | one of first syllable, penultimate or final, by weight, as marked | no | Where a word's stress falls. |
540
557
  | `data.sounds.finals` | list of `{ final, like }` | no | The syllable finals, for a language whose syllables are parsed by them. |
541
558
  | `data.sounds.finals[].final` | a final as written, such as `ang` | yes | The final. |
@@ -550,11 +567,12 @@ These fields are accepted by every note type. A field's value can still be irrel
550
567
  | `data.sounds.syllables.finalVowel` | `true` or `false` | no | Whether a word may close on a vowel; `true` where unstated. |
551
568
  | `data.sounds.syllables.neverInitial` | a list of letters | no | The letters that never open a word. |
552
569
  | `data.sounds.syllables.signature` | a list of letters | no | The letters that mark a word as the language's own. |
553
- | `data.sounds.syllables.harmony` | a map — `{ back, front, neutral?, neutralAs? }` | no | The vowel harmony a word keeps. |
570
+ | `data.sounds.syllables.harmony` | a map — `{ back, front, neutral?, neutralAs?, partners? }` | no | The vowel harmony a word keeps. |
554
571
  | `data.sounds.syllables.harmony.back` | a list of vowels | yes | The back vowels. |
555
572
  | `data.sounds.syllables.harmony.front` | a list of vowels | yes | The front vowels. |
556
573
  | `data.sounds.syllables.harmony.neutral` | a list of vowels | no | The vowels of neither set. |
557
574
  | `data.sounds.syllables.harmony.neutralAs` | one of back, front | no | The set a word of neutral vowels alone counts as. |
575
+ | `data.sounds.syllables.harmony.partners` | a map of vowels to front vowels | no | The front vowel each vowel turns to in an affix's front form, keyed by the vowel; every key is a vowel of the harmony and every value one of its `front` vowels. |
558
576
  | `data.sounds.joining` | list of `{ first, second, written, example? }` | no | How two parts are written where the first closes and the second opens on the given sounds. |
559
577
  | `data.sounds.joining[].first` | a list of sounds | yes | The sounds the first part closes on. |
560
578
  | `data.sounds.joining[].second` | a list of sounds | yes | The sounds the second part opens on. |
@@ -583,9 +601,15 @@ These fields are accepted by every note type. A field's value can still be irrel
583
601
  | `data.sounds.readPast[].written` | a string | yes | A mark or spelling the pages write. |
584
602
  | `data.sounds.readPast[].readAs` | a string | no | What a word is read with in its place; absent where it is read as nothing. |
585
603
  | `data.sounds.readPast[].what` | a string | yes | What the mark or spelling is. |
604
+ | `data.sounds.mutations` | list of `{ mutation, sound, becomes, example? }` | no | The sounds a word changes where a named mutation holds, one entry per sound, such as a stop worn to a fricative or a consonant voiced inside a compound. |
605
+ | `data.sounds.mutations[].mutation` | a string, such as `wearing` | yes | The mutation's name; its rows share it, and `data.negation` names it. |
606
+ | `data.sounds.mutations[].sound` | a sound as written, such as `p` | yes | The sound as it stands where the mutation does not hold; once per mutation. |
607
+ | `data.sounds.mutations[].becomes` | a sound as written, such as `v` | yes | What the sound becomes where the mutation holds. |
608
+ | `data.sounds.mutations[].example` | a string | no | A word that shows it. |
586
609
  | `data.soundChanges[].name` | a string, such as `From Helonic to Byzarian` | yes | The set's name, which no other set of the note shares. |
587
610
  | `data.soundChanges[].from` | a language skill Address | no | The language whose words the steps start from. |
588
611
  | `data.soundChanges[].to` | a language skill Address | no | The language whose words the steps make. |
612
+ | `data.soundChanges[].takes` | a case, such as `accusative` | no | The form of the older word the steps start from; its citation form where absent. |
589
613
  | `data.soundChanges[].steps` | list of `{ step, change, where? }` | yes | The steps, in the order they apply; no step label stands twice in a set. |
590
614
  | `data.soundChanges[].steps[].step` | a string, such as `A1` or `"1"` | yes | The step's label, in the order the changes apply. |
591
615
  | `data.soundChanges[].steps[].change` | a change in arrow notation, such as `` `ph` > `f`; `kh` > `h` `` | yes | The change, in arrow notation: one or more `` `sources` > `targets` `` rules separated by `;`, or the operation a step names in words. |
@@ -593,8 +617,11 @@ These fields are accepted by every note type. A field's value can still be irrel
593
617
  | `data.elements[].form` | the element as written, such as `arn-` | yes | The element a name is built from. |
594
618
  | `data.elements[].variants` | a list of forms | no | The element's other forms, such as `aldr-` beside `ald-`. |
595
619
  | `data.elements[].position` | one of opening, closing, either, link | no | Where in a name the element stands. |
620
+ | `data.elements[].kind` | one of byname, given, place | no | The kind of name the element builds, in a language that keeps one set of elements per kind; every kind where absent. |
596
621
  | `data.elements[].sense` | a string | yes | What the element means. |
597
622
  | `data.elements[].examples` | a list of names | no | The names the element is read from. |
623
+ | `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. |
624
+ | `data.elements[].genitive` | the element as written, such as `-borgar` | no | The element in the genitive, where a name governing another word takes it. |
598
625
  | `data.roots[].root` | a consonantal skeleton, such as `d-l-k` | yes | The root. |
599
626
  | `data.roots[].sense` | a string | yes | What the root means. |
600
627
  | `data.roots[].field` | a string, such as `stone` | no | The field the root belongs to. |
@@ -606,6 +633,38 @@ These fields are accepted by every note type. A field's value can still be irrel
606
633
  | `data.callingNames.closings[].doubled` | `true` | no | The calling name doubles the first syllable rather than taking an ending. |
607
634
  | `data.callingNames.closings[].usual` | one of man, woman, either | yes | Whose name it usually makes. |
608
635
  | `data.callingNames.minSyllables` | a whole number | yes | The fewest syllables a given name has that the pages give a calling name. |
636
+ | `data.older[].form` | a form as the setting writes it | yes | A word or name whose shape the language's rules do not make. |
637
+ | `data.older[].gloss` | a string | no | What it means. |
638
+ | `data.older[].layer` | a string, such as `Primordial` | no | The historical layer it belongs to. |
639
+ | `data.negation.particle` | a form of the language's words | yes | The particle that stands before a verb to negate it, a form of `data.words` of the language or an ancestor. |
640
+ | `data.negation.mutation` | a mutation of `data.sounds.mutations` | no | The mutation the negated verb takes, named in `data.sounds.mutations`. |
641
+ | `data.naming.shapes` | list of `{ of, syllables?, perVowel? }` | no | How long each class of name runs. |
642
+ | `data.naming.shapes[].of` | one of name, given, clan, bound, compound | yes | The class of name the shape holds for; once per class. |
643
+ | `data.naming.shapes[].syllables` | a map — `{ min, max }` | no | How many syllables a name of the class runs to. |
644
+ | `data.naming.shapes[].syllables.min` | a whole number | yes | The fewest syllables a name of the class runs to. |
645
+ | `data.naming.shapes[].syllables.max` | a whole number | yes | The most syllables a name of the class runs to; never below `min`. |
646
+ | `data.naming.shapes[].perVowel` | a map — `{ min, max }` | no | How many consonants a name of the class carries to each vowel; a shape states this, `syllables`, or both. |
647
+ | `data.naming.shapes[].perVowel.min` | a number | yes | The fewest consonants a name of the class carries to each vowel. |
648
+ | `data.naming.shapes[].perVowel.max` | a number | yes | The most consonants a name of the class carries to each vowel; never below `min`. |
649
+ | `data.naming.edges` | list of `{ edge, sounds, vowel?, of? }` | no | The sounds a name may open and close on, where names keep edges ordinary words do not; an edge stands once for each class of name. |
650
+ | `data.naming.edges[].edge` | one of openConsonant, openCluster, closeConsonant, closeCluster, innerCluster, vowelPair, glide, syllabic | yes | Which edge of a name the sounds stand at. |
651
+ | `data.naming.edges[].sounds` | a list of sounds, or `any` | yes | The sounds, as written; `any` for every one, and an empty list for none. |
652
+ | `data.naming.edges[].vowel` | `true` or `false` | no | Whether a vowel may stand at the edge too; `false` where unstated. |
653
+ | `data.naming.edges[].of` | one of name, given, clan, bound, compound | no | The class of name the edge holds for; `name`, every name, where absent. |
654
+ | `data.naming.draws` | list of `{ kind, elements }` | no | The element kinds each kind of name is built from. |
655
+ | `data.naming.draws[].kind` | one of byname, given, place | yes | The kind of name; once per kind. |
656
+ | `data.naming.draws[].elements` | a list of element kinds | yes | The kinds of element, as `data.elements` states them, the name is built from. |
657
+ | `data.naming.heldFrom` | list of `{ source, takes, clip?, genitive?, elements? }` | no | What a place name takes first, by what the place is held from. |
658
+ | `data.naming.heldFrom[].source` | a string, such as `a founder` | yes | What the place is held from. |
659
+ | `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`. |
660
+ | `data.naming.heldFrom[].clip` | one of first | no | How the name is clipped: `first`, to its first element; whole where absent. |
661
+ | `data.naming.heldFrom[].genitive` | a list of endings | no | The genitive endings, one of which stands at the seam after the name. |
662
+ | `data.naming.heldFrom[].elements` | a list of elements | no | The elements that may stand first, for a row that takes an `element`. |
663
+ | `data.naming.nearName` | a map — `{ minSyllables, examples? }` | no | The near name a long given name carries. |
664
+ | `data.naming.nearName.minSyllables` | a whole number | yes | The fewest syllables a given name has that carries a near name. |
665
+ | `data.naming.nearName.examples` | list of `{ near, given }` | no | Worked near names, each beside its given name. |
666
+ | `data.naming.nearName.examples[].near` | a near name, such as `Anlâ` | yes | The near name. |
667
+ | `data.naming.nearName.examples[].given` | a given name | yes | The given name it is the near name of. |
609
668
 
610
669
  **System mappings**
611
670
 
package/e2e.mjs CHANGED
@@ -43,6 +43,8 @@ import crypto from "node:crypto";
43
43
  import { readdirSync, statSync } from "node:fs";
44
44
  import fs from "node:fs/promises";
45
45
  import path from "node:path";
46
+ import os from "node:os";
47
+ import { Browser, BrowserPlatform, computeExecutablePath, install } from "@puppeteer/browsers";
46
48
  import process from "node:process";
47
49
  import { spawnSync } from "node:child_process";
48
50
 
@@ -796,6 +798,91 @@ export function suiteExecutables(command) {
796
798
  return [program];
797
799
  }
798
800
 
801
+ /** The Chrome for Testing build shared by local and CI Cypress runs. */
802
+ export const CHROME_BUILD_ID = "155.0.8059.84";
803
+
804
+ /**
805
+ * The browser cache survives reseeding and container teardown.
806
+ * @param {NodeJS.ProcessEnv} [env] - Environment overrides.
807
+ * @returns {string} The browser cache directory.
808
+ */
809
+ export function browserCacheDir(env = process.env) {
810
+ if (env.HL_BROWSER_CACHE) return path.resolve(env.HL_BROWSER_CACHE);
811
+ if (env.FOUNDRYVTT_CACHE) return path.resolve(env.FOUNDRYVTT_CACHE, "..", "browser-cache");
812
+ if (env.HL_E2E_ROOT) return path.resolve(env.HL_E2E_ROOT, "browser-cache");
813
+ const cache =
814
+ env.XDG_CACHE_HOME ||
815
+ (process.platform === "darwin" ?
816
+ path.join(os.homedir(), "Library", "Caches")
817
+ : path.join(os.homedir(), ".cache"));
818
+ return path.join(cache, "heroiclands", "chrome-for-testing");
819
+ }
820
+
821
+ /**
822
+ * Supply pinned Chrome only to a recognizable Cypress command without a browser.
823
+ * Explicit browser arguments remain verbatim, including Electron and paths.
824
+ * @param {object} opts
825
+ * @param {readonly string[]} opts.command - The configured suite command.
826
+ * @param {string[]} [opts.args] - Extra suite arguments.
827
+ * @param {NodeJS.ProcessEnv} [opts.env] - Cache overrides.
828
+ * @param {string} [opts.platform] - Host operating system.
829
+ * @param {string} [opts.arch] - Host architecture.
830
+ * @param {(message: string) => void} [opts.log] - Progress reporting.
831
+ * @returns {Promise<string[]>} Arguments to append to the suite command.
832
+ */
833
+ export async function prepareSuiteBrowser({
834
+ command,
835
+ args = [],
836
+ env = process.env,
837
+ platform = process.platform,
838
+ arch = process.arch,
839
+ log = () => {},
840
+ }) {
841
+ const tool = suiteExecutables(command).at(-1);
842
+ if (!tool || executableName(tool) !== "cypress") return args;
843
+ const all = [...command, ...args];
844
+ const explicit = all.findLastIndex(
845
+ (arg) => arg === "--browser" || arg.startsWith("--browser="),
846
+ );
847
+ if (explicit !== -1) {
848
+ const selected =
849
+ all[explicit] === "--browser" ?
850
+ all[explicit + 1]
851
+ : all[explicit].slice("--browser=".length);
852
+ log(
853
+ `▸ Cypress browser: ${selected ?? "(missing --browser value)"} (explicit; version reported by Cypress)`,
854
+ );
855
+ return args;
856
+ }
857
+ const browserPlatform =
858
+ platform === "darwin" && arch === "arm64" ? BrowserPlatform.MAC_ARM
859
+ : platform === "linux" && arch === "x64" ? BrowserPlatform.LINUX
860
+ : null;
861
+ if (!browserPlatform)
862
+ throw new Error(
863
+ `Chrome for Testing has no configured build for ${platform}/${arch}. Pass --browser <name-or-path> to select an installed browser.`,
864
+ );
865
+ const options = {
866
+ browser: Browser.CHROME,
867
+ buildId: CHROME_BUILD_ID,
868
+ platform: browserPlatform,
869
+ cacheDir: browserCacheDir(env),
870
+ };
871
+ let executablePath = computeExecutablePath(options);
872
+ if (!isExecutableFile(executablePath)) {
873
+ try {
874
+ executablePath = (await install(options)).executablePath;
875
+ } catch (cause) {
876
+ throw new Error(
877
+ `Cannot install Chrome for Testing ${CHROME_BUILD_ID} for ${platform}/${arch} in ${options.cacheDir}. Check network access or pass --browser <name-or-path>.`,
878
+ { cause },
879
+ );
880
+ }
881
+ }
882
+ log(`▸ Cypress browser: Chrome for Testing ${CHROME_BUILD_ID} (${executablePath})`);
883
+ return [...args, "--browser", executablePath];
884
+ }
885
+
799
886
  /**
800
887
  * @param {string} candidate - An absolute path.
801
888
  * @returns {boolean} Whether it is there and is a file.
@@ -1128,7 +1215,7 @@ export async function e2eRun({
1128
1215
  });
1129
1216
  return runSuite({
1130
1217
  command,
1131
- args: suiteArgs,
1218
+ args: await prepareSuiteBrowser({ command, args: suiteArgs, env, log }),
1132
1219
  cwd: config.rootDir,
1133
1220
  // Only a headless run makes a claim worth checking. `open` hands
1134
1221
  // the runner to a person, who decides what to execute and when to
@@ -1211,7 +1298,7 @@ export async function e2eFast({ config, argv = [], env = process.env, log = () =
1211
1298
  }
1212
1299
  return runSuite({
1213
1300
  command,
1214
- args: suiteArgs,
1301
+ args: await prepareSuiteBrowser({ command, args: suiteArgs, env, log }),
1215
1302
  cwd: config.rootDir,
1216
1303
  results: config.e2eResults,
1217
1304
  env,
@@ -1254,19 +1254,18 @@ export class BasePackCompiler {
1254
1254
  }
1255
1255
  }
1256
1256
 
1257
- // The corpus, from the index this compile derived once — not a walk of
1258
- // this pass's own. Each note is then read for its **prose**: the
1259
- // index carries what is *about* a note and deliberately not its text,
1260
- // nor the `bodyLine`/`bodyColumn` a diagnostic needs, and this pass has
1261
- // to have the body anyway. So the read is one this pass makes anyway;
1262
- // the index decides which files it covers.
1257
+ // Parsed source and body coordinates belong to the shared corpus.
1258
+ // A directly supplied corpus can fill missing entries on first use.
1259
+ this.corpus.parsed ??= new Map();
1263
1260
  const dates = reckoningContext({ notes: this.corpus.records });
1264
1261
  /** The note first filed under each document id in this pass. @type {Map<string, string>} */
1265
1262
  const idOwners = new Map();
1266
1263
  for (const record of this.corpus.records) {
1267
1264
  if (!isNoteRecord(record)) continue;
1268
1265
  const absPath = noteFile(this.contentBase, record);
1269
- const { body, bodyLine, bodyColumn } = parseMarkdownFile(absPath);
1266
+ const parsed = this.corpus.parsed.get(absPath) ?? parseMarkdownFile(absPath);
1267
+ this.corpus.parsed.set(absPath, parsed);
1268
+ const { body, bodyLine, bodyColumn, raw } = parsed;
1270
1269
  const fm = cloneAddressState(authoredFrontmatter(record));
1271
1270
  // Which note this pass is on, so anything it calls can report a
1272
1271
  // position without every method having to be handed one.
@@ -1284,7 +1283,7 @@ export class BasePackCompiler {
1284
1283
  // comes from the diagnostic locator, so the message must not
1285
1284
  // repeat it.
1286
1285
  try {
1287
- assertNoDeclaredPackage(fm, { absPath });
1286
+ assertNoDeclaredPackage(fm, { absPath, raw });
1288
1287
  } catch (err) {
1289
1288
  stats.declined++;
1290
1289
  this.errorCount++;
@@ -1375,7 +1374,7 @@ export class BasePackCompiler {
1375
1374
  `${path.relative(process.cwd(), owner)}. Give one of the two notes a ` +
1376
1375
  `different \`id\` under \`data:\`, or remove it to take the id derived ` +
1377
1376
  `from its address`,
1378
- locateFrontmatterKey(absPath, "id"),
1377
+ locateFrontmatterKey(absPath, "id", undefined, { raw }),
1379
1378
  );
1380
1379
  continue;
1381
1380
  }
@@ -1398,15 +1397,9 @@ export class BasePackCompiler {
1398
1397
  );
1399
1398
  const position =
1400
1399
  err.position ??
1401
- (err.keyPath ?
1402
- positionOfFrontmatterPath(fs.readFileSync(absPath, "utf8"), err.keyPath)
1403
- : repeatedAnchor ?
1404
- positionOfLiteral(
1405
- fs.readFileSync(absPath, "utf8"),
1406
- `{#${repeatedAnchor[1]}}`,
1407
- 2,
1408
- )
1409
- : undefined);
1400
+ (err.keyPath ? positionOfFrontmatterPath(raw, err.keyPath)
1401
+ : repeatedAnchor ? positionOfLiteral(raw, `{#${repeatedAnchor[1]}}`, 2)
1402
+ : undefined);
1410
1403
  // `position` is set by whatever failed if it knew where — an
1411
1404
  // unresolved address, a bad table directive — so the report
1412
1405
  // points at the line rather than at the note.
@@ -60,7 +60,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
60
60
  * created when none is passed, and returned on the corpus either way — a note
61
61
  * the index refuses is a note the compile must still *report*, exactly as the
62
62
  * compile loop reported it when the loop was the first to see it.
63
- * @returns {Promise<{records: object[], linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
63
+ * @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
64
64
  * its indexes, and the notes it could not record.
65
65
  */
66
66
  export async function buildCompileCorpus({
@@ -86,6 +86,7 @@ export async function buildCompileCorpus({
86
86
  const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
87
87
  return {
88
88
  records,
89
+ parsed,
89
90
  problems: collected,
90
91
  linkIndex: buildContentLinkIndex(contentBase, router, corpusOptions),
91
92
  sqlTables: await prepareTreeSqlTables(contentBase, sqlOptions),
@@ -48,7 +48,7 @@ export const ALERT_TYPES = Object.freeze({
48
48
  ),
49
49
  },
50
50
  });
51
- const parser = new MarkdownIt();
51
+ const parser = new MarkdownIt({ html: true });
52
52
  const titleParser = new MarkdownIt({ html: false });
53
53
  const marker = /^ {0,3}>[ \t]?\[!([^\]\n]+)\](.*)$/;
54
54
  /** Read alerts outside code examples, retaining inclusive source line ranges.
@@ -85,7 +85,9 @@ export function scanAlerts(source) {
85
85
  if (match[1] !== match[1].toUpperCase() || !Object.hasOwn(ALERT_TYPES, type)) {
86
86
  errors.push({
87
87
  ...at,
88
- message: `unknown alert type ${match[1]}; use NOTE, TIP, IMPORTANT or WARNING`,
88
+ message: `unknown alert type ${match[1]}; use ${Object.keys(ALERT_TYPES)
89
+ .map((name) => name.toUpperCase())
90
+ .join(", ")}`,
89
91
  });
90
92
  continue;
91
93
  }