@heroiclands/package-build 23.12.6 → 23.12.7

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 (83) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/styles/content-base.css +25 -0
  3. package/bin/content-commands.mjs +264 -3
  4. package/bin/package-build.mjs +25 -0
  5. package/content-config.mjs +72 -0
  6. package/docs/api.md +10 -9
  7. package/docs/authoring/links-and-markup.md +439 -47
  8. package/docs/commands.md +127 -0
  9. package/docs/configuration.md +66 -3
  10. package/docs/content-format.md +3 -1
  11. package/docs/guides/book.md +11 -0
  12. package/docs/guides/module-project.md +16 -0
  13. package/docs/guides/site.md +19 -0
  14. package/docs/reference/format-details.md +372 -6
  15. package/docs/reference/note-types.md +36 -11
  16. package/engine/base-compiler.mjs +2 -0
  17. package/engine/bundles.mjs +7 -2
  18. package/engine/compile-corpus.mjs +3 -1
  19. package/engine/content-charset.mjs +21 -2
  20. package/engine/content-format.yaml +1 -0
  21. package/engine/content-index.mjs +12 -0
  22. package/engine/content-poetry.mjs +22 -4
  23. package/engine/content-scripts.mjs +1219 -0
  24. package/engine/content-session.mjs +2 -0
  25. package/engine/data-keys.mjs +5 -1
  26. package/engine/dictionary-layout.mjs +18 -12
  27. package/engine/dictionary-rows.mjs +11 -2
  28. package/engine/helpers.mjs +8 -2
  29. package/engine/index-records.mjs +1 -0
  30. package/engine/journals.mjs +14 -3
  31. package/engine/language-data.mjs +137 -1
  32. package/engine/language-tag.mjs +103 -0
  33. package/engine/language-terms.mjs +16 -4
  34. package/engine/map-lexicon.mjs +762 -0
  35. package/engine/markup-validation.mjs +17 -3
  36. package/engine/names.mjs +1 -1
  37. package/engine/note-vocabulary.mjs +113 -0
  38. package/engine/pdf-build.mjs +28 -10
  39. package/engine/pdf-render.mjs +116 -11
  40. package/engine/run-rows.mjs +119 -0
  41. package/engine/run-words.mjs +266 -0
  42. package/engine/scenes.mjs +1 -0
  43. package/engine/script-font-delivery.mjs +321 -0
  44. package/engine/script-fonts.mjs +172 -0
  45. package/engine/site-build.mjs +10 -3
  46. package/engine/site-config.mjs +5 -0
  47. package/engine/site-root.mjs +19 -5
  48. package/engine/sql-tables.mjs +111 -1
  49. package/engine/text-maps.mjs +607 -0
  50. package/engine/verb-pattern.mjs +99 -0
  51. package/engine/word-pattern.mjs +27 -0
  52. package/engine/word-rows.mjs +8 -2
  53. package/engine/word-terms.mjs +44 -0
  54. package/hugo-theme/layouts/_default/baseof.html +3 -0
  55. package/hugo-theme/static/css/style.css +10 -0
  56. package/manifest.mjs +9 -1
  57. package/package.json +1 -1
  58. package/types/content-config.d.mts +26 -0
  59. package/types/engine/compile-corpus.d.mts +2 -1
  60. package/types/engine/content-index.d.mts +11 -3
  61. package/types/engine/content-poetry.d.mts +7 -0
  62. package/types/engine/content-scripts.d.mts +226 -0
  63. package/types/engine/data-keys.d.mts +2 -2
  64. package/types/engine/dictionary-rows.d.mts +15 -0
  65. package/types/engine/helpers.d.mts +5 -1
  66. package/types/engine/journals.d.mts +7 -2
  67. package/types/engine/language-data.d.mts +2 -0
  68. package/types/engine/language-tag.d.mts +8 -0
  69. package/types/engine/language-terms.d.mts +2 -1
  70. package/types/engine/map-lexicon.d.mts +274 -0
  71. package/types/engine/markup-validation.d.mts +7 -2
  72. package/types/engine/pdf-build.d.mts +26 -3
  73. package/types/engine/pdf-render.d.mts +17 -2
  74. package/types/engine/run-rows.d.mts +89 -0
  75. package/types/engine/run-words.d.mts +86 -0
  76. package/types/engine/script-font-delivery.d.mts +134 -0
  77. package/types/engine/script-fonts.d.mts +168 -0
  78. package/types/engine/site-build.d.mts +2 -1
  79. package/types/engine/site-root.d.mts +11 -3
  80. package/types/engine/text-maps.d.mts +175 -0
  81. package/types/engine/verb-pattern.d.mts +65 -0
  82. package/types/engine/word-pattern.d.mts +13 -0
  83. package/types/engine/word-terms.d.mts +6 -0
@@ -1976,6 +1976,249 @@ subtypes conflict. One package advisory contains both groups and is located
1976
1976
  at the earliest reported use. Advisory findings preserve a successful lint
1977
1977
  exit status. Supplied-value errors prevent publication and compilation.
1978
1978
 
1979
+ #### Text marked with a language, a font or a map
1980
+
1981
+ `lang=`, `font=` and `map=` on a span, a generic div or a poetry fence are read
1982
+ by the build and never written through as ordinary attributes. Each is
1983
+ optional, and a run inside a div or a fence inherits the keys the div or fence
1984
+ states, its own keys overriding them. On a named block the three keys are
1985
+ errors.
1986
+
1987
+ - **`lang=`** is a name declared in [`languages`](../configuration.md#languages),
1988
+ and otherwise a BCP 47 tag checked for its form alone. It gives the language
1989
+ tag and changes nothing else.
1990
+ - **`font=`** is a face of the installed `@heroiclands/fonts`, named by its
1991
+ `fonts.json` class without `hl-`, or `mono`, `serif` or `sans`.
1992
+ - **`map=`** is the `data.map.name` of a [mapping note](#mapping-notes). The
1993
+ text is rewritten by the map before it is set; without `map=` it is set as
1994
+ typed. With `font=`, the face is one the map lists in `fonts`, and its own
1995
+ rows come first.
1996
+ - **`lang=` with `map=`** reads the language's lexicon first, when
1997
+ [`languages.<name>.note`](../configuration.md#languages) names its language
1998
+ note: see [a map reads the dictionary first](#a-map-reads-the-dictionary-first).
1999
+
2000
+ **A face draws the code points its `fonts.json` entry lists in `codepoints`,
2001
+ and whitespace.** After the map, any other character in a run set in a face is
2002
+ an error, located at the source character it came from; through a map, at the
2003
+ start of the matched `from`. Each character is reported once per run. The
2004
+ generic families are not checked. `input`, `layout` and `encoding` in
2005
+ `fonts.json` are not read.
2006
+
2007
+ Text marked with `font=` or `map=`, or with a declared language, is plain: a
2008
+ span or a verse holds no emphasis, link, image, footnote, wikilink, code or
2009
+ HTML, and no span but a marked one inside a div or a fence; a div holds
2010
+ paragraphs and line breaks only. The text is read as written, so `[`, `]`, `"`,
2011
+ `<<`, `>>`, `!` and `@#$%^&` are characters, and a bracket pair is markup only
2012
+ when `(`, `{` or a second `[` makes it a link, a span or a wikilink. Text marked
2013
+ with a real language's tag alone keeps its Markdown. See
2014
+ [marking language, font and mapping](../authoring/links-and-markup.md#marking-language-font-and-mapping).
2015
+
2016
+ Each own-note metadata record carries `runs` in source order, an empty array
2017
+ when the body marks nothing. Every entry has exactly `kind` (`span`, `block` or
2018
+ `poetry`), `lang`, `tag`, `font`, `map`, `line` and `column`: `lang`, `font` and
2019
+ `map` as written, its own or inherited, `tag` the BCP 47 tag `lang` resolves
2020
+ to, each `null` when absent, and the position of the span's `[`, the div's
2021
+ `:::` or the fence's opening. Every value is a string or a number and none is a
2022
+ key, so tags that differ only in case are separate values. This is derived
2023
+ metadata, not frontmatter. The [`runs` relation](#content-tables) flattens
2024
+ these entries with their containing NOTE Address.
2025
+
2026
+ #### Mapping notes
2027
+
2028
+ A `doc` note's `data.map` is a mapping: rows that rewrite sequences of
2029
+ characters into characters, which a run marked `map=` with its `name` is
2030
+ rewritten by before it is set. A mapping belongs to a script, and serves every
2031
+ language written in it.
2032
+
2033
+ ```yaml
2034
+ data:
2035
+ map:
2036
+ name: semaron
2037
+ vowels: "aeiouAEIOU"
2038
+ fonts:
2039
+ - font: semaron-kuriale
2040
+ - font: semaron-carved
2041
+ rows:
2042
+ - [th, "\U000F0251"]
2043
+ rows:
2044
+ - [a, "\U000F0230"]
2045
+ - [th, "\U000F0250"]
2046
+ - { from: y, to: "\U000F0246", before: consonant }
2047
+ ```
2048
+
2049
+ | Key | Shape | Meaning |
2050
+ | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
2051
+ | `name` | string, required | What `map=` names it by: lowercase letters, digits and `-`, unique in the index |
2052
+ | `vowels` | string | The characters `before: vowel` reads as vowels |
2053
+ | `fonts` | list of `{ font, rows? }`, required, at least one | The faces the map is drawn in, by slug, each with any rows of its own |
2054
+ | `rows` | list of `[from, to]` or `{ from, to, before?, at? }` | The shared rewrites, in authored order |
2055
+
2056
+ **A face's own rows come first.** A run in a face tries that face's `rows`,
2057
+ the longest match among them, and only where none matches the shared `rows`; a
2058
+ face listed with no `rows` uses the shared rows alone, and a run with no
2059
+ `font=` uses them too. Rows take the same shapes and conditions in both places,
2060
+ and `vowels` serves them all. A run whose `font=` is not one of the map's
2061
+ `fonts` is an error at its `map=`:
2062
+
2063
+ > `` `thuravark-carved` is not a font of the map `semaron`; it is for `semaron-kuriale` and `semaron-carved` ``
2064
+
2065
+ A row's `from` is one or more characters and its `to` any number, none
2066
+ included, each a whole code point; a code point above U+FFFF is written in YAML
2067
+ with the eight-digit escape, as `"\U000F0200"`. The longest match wins; among rows of one length, a row with a
2068
+ condition that holds comes before a row with none, and otherwise the earlier
2069
+ row wins. A row whose condition fails is skipped, and a character no row
2070
+ matches passes through unchanged.
2071
+
2072
+ | Condition | Holds when |
2073
+ | ------------------- | ---------------------------------------------------------------------------- |
2074
+ | `before: vowel` | the next character is one of `vowels` |
2075
+ | `before: consonant` | the next character is any other letter |
2076
+ | `at: start` | the match is the run's first character, or follows whitespace or punctuation |
2077
+ | `at: end` | the match ends the run, or is followed by whitespace or punctuation |
2078
+
2079
+ Neither `before` condition holds at the end of the text. The note's body is
2080
+ for a reader: nothing reads it. Each row is a row of the
2081
+ [`maps` relation](#content-tables), never a struct key, so `A` and `a` are two
2082
+ rows; a face's own rows carry its slug in `font`.
2083
+
2084
+ The data-key check holds every key of `data.map` and of each row to the table
2085
+ under [`type: doc`](#type-doc), at its own line and column:
2086
+
2087
+ > `` `data.map.rows[2]` should be `[from, to]` or `{ from, to, before?, at? }`, but reads "a" ``
2088
+
2089
+ > `` `data.map.rows[3]` must state `to` — a string, which may be empty ``
2090
+
2091
+ > `` "when" is not a key of `data.map.rows[5]`; it takes only `from`, `to`, `before`, `at` ``
2092
+
2093
+ > `` `data.map.rows[6].before` should be `vowel` or `consonant`, but reads "liquid" ``
2094
+
2095
+ The map's own check reports the rest, each at its key:
2096
+
2097
+ > `` `data.map.name` is `Vyl_Semaron`; a map's name is lowercase letters, digits and `-` ``
2098
+
2099
+ > `` `data.map.name` is `vyl-semaron`, which Lore/Other.md also declares; a map's name is unique ``
2100
+
2101
+ > `` `data.map.rows[0]` is a list, and a row written as a list is two strings, `[from, to]` ``
2102
+
2103
+ > `` `data.map.rows[1]` has an empty `from`; a row matches one or more characters ``
2104
+
2105
+ > `` `data.map.rows[4]` has `before: consonant`, and the map states no `vowels` to read it by ``
2106
+
2107
+ > `` `data.map.fonts` is empty; a map lists each face it is drawn in ``
2108
+
2109
+ > `` `data.map.fonts[0].font` is `mono`, a generic family; a map lists the faces it is drawn in ``
2110
+
2111
+ The lint checks the listed faces against the installed `@heroiclands/fonts`,
2112
+ each at the entry or the row at fault. Every listed face draws every code point
2113
+ its own rows write, every code point the shared rows it can reach write (a
2114
+ shared row is out of a face's reach where a plain row of the face's own matches
2115
+ the start of its `from`), and every code point of the native forms that apply
2116
+ to it:
2117
+
2118
+ > `` `semaron-kurale` is not a font: `data.map.fonts[1].font` names an installed face; did you mean `semaron-kuriale`? ``
2119
+
2120
+ > `` `data.map.fonts[2]` lists the font `semaron-kuriale` a second time; the first is `data.map.fonts[0]` ``
2121
+
2122
+ > `` `data.map.fonts[1].rows[0]` writes U+F0251, which the font `semaron-carved` does not draw ``
2123
+
2124
+ > `` `data.map.rows[5]` writes U+F0239, which the font `semaron-kuriale`, listed in `data.map.fonts`, does not draw ``
2125
+
2126
+ A row with a fault is left out of the map; the rest apply.
2127
+
2128
+ #### A map reads the dictionary first
2129
+
2130
+ The sound rules belong to the script, and the exceptions to the language. A
2131
+ word of a [language's words](#a-languages-words) states a `native` form for a
2132
+ map only where the map's rows would spell it differently:
2133
+
2134
+ ```yaml
2135
+ words:
2136
+ - form: zaron
2137
+ class: n
2138
+ field: "Law, the court and the written act"
2139
+ gloss: an oath
2140
+ native:
2141
+ - { map: semaron, text: "\U000F024F\U000F0230\U000F023F\U000F023C" }
2142
+ - { map: semaron, font: semaron-carved, text: "\U000F024F\U000F0230\U000F023C" }
2143
+ ```
2144
+
2145
+ | Key | Shape | Meaning |
2146
+ | ------ | ---------------- | -------------------------------------------------------------------------------------------- |
2147
+ | `map` | string, required | The `data.map.name` the native form is written for |
2148
+ | `font` | string | The one face it is for; unstated, every face the map lists, and a face's own form wins there |
2149
+ | `text` | string, required | The word as the script writes it, in the map's characters |
2150
+
2151
+ A run marked `lang=` with a declared language whose
2152
+ [`note`](../configuration.md#languages) names its language note, and `map=`,
2153
+ is written word by word:
2154
+
2155
+ 1. **A word** is a maximal run of letters, combining marks and digits, an
2156
+ apostrophe (`'` or `’`), a hyphen or an en dash joining two such runs into
2157
+ one word. Everything between words is spelled by the rows.
2158
+ 2. **The word is looked up** as written, and then in lowercase. A word found is
2159
+ written by its native form for the run's map in the run's face, else by its
2160
+ native form for the map with no `font`, as one piece.
2161
+ 3. **A word found only in lowercase and written with a capital** takes its
2162
+ capital from the rows: the rows map the word's first sound as written and in
2163
+ lowercase, and where the native form begins with what the lowercase gives,
2164
+ that beginning is replaced by what the capital gives. A native form that
2165
+ does not begin as the rows begin the word cannot be capitalised by them, and
2166
+ the word is an error at its place in the run. The remedy is to state the
2167
+ capitalised word as a word of its own with its native form, which the
2168
+ lookup as written finds first.
2169
+ 4. **Every other word is spelled by the rows**, its derived forms included: a
2170
+ plural or a verb ending of an exception word is a different word, and the
2171
+ rows spell it.
2172
+
2173
+ A run with `map=` and no `lang=`, a `lang=` that is a bare BCP 47 tag, and a
2174
+ declared language with no `note` are spelled by the rows alone. A character the
2175
+ face does not draw inside a word written by its native form is an error at the
2176
+ word's start:
2177
+
2178
+ > `` `Orlan` is capitalised, and the native form the lexicon of `vyl` states for `orlan` under the map `semaron` does not begin as the rows spell its first letter, so the rows cannot capitalise it; state `Orlan` as a word of its own with its native form ``
2179
+
2180
+ > `` `zaron` is written U+F024F by the lexicon of `vyl` for the map `semaron`, which the font `semaron-kuriale` does not draw ``
2181
+
2182
+ **Every word of such a run is a word of the language**, case folded: a
2183
+ `data.words[].form`, a word of a name the language note's own `data.register`
2184
+ lists under a `tongue` other than `translation`, or a form one rule derives from a lexicon form —
2185
+ the `data.grammar.plural` plural of a noun or its own `plural`, a verb's form
2186
+ with one ending of `data.grammar.verbs.persons` (in place of a closing
2187
+ `data.grammar.verbs.replaces` where one is stated), or a suffix of `data.affixes`
2188
+ stating `replaces` (`vowel` takes the place of a closing `a`, `e`, `i`, `o` or
2189
+ `u`, accented or not; letters take the place of those letters; a stem closing
2190
+ otherwise takes the suffix whole). A compound not known whole is checked part by
2191
+ part at its hyphens and en dashes. Any other word is an error at its first
2192
+ character, once per run; see
2193
+ [a mapped run's words](../authoring/links-and-markup.md#a-mapped-runs-words):
2194
+
2195
+ > `` `thiruthrimar` is not a word of the `vyl` lexicon, a registered name, or a regular form of one; coin it in the lexicon (or register the name) first ``
2196
+
2197
+ Each native form is a row of the [`natives` relation](#content-tables), never a
2198
+ struct key, so forms that differ only in case are two rows. The data-key check
2199
+ holds each entry to its three keys; the lint checks the rest, each at the entry
2200
+ or the key at fault:
2201
+
2202
+ > `` `data.words[0].native[0].map` is `semron`, which is not a map: a native form names the `data.map.name` of a mapping note; did you mean `semaron`? ``
2203
+
2204
+ > `` `data.words[0].native[1].font` is `thuravark-carved`, which is not a font of the map `semaron`; it is for `semaron-kuriale` and `semaron-carved` ``
2205
+
2206
+ > `` `data.words[0].native[2]` states a second native form for the map `semaron`; the first is `data.words[0].native[0]` ``
2207
+
2208
+ > `` `data.words[0].native[0]` states what the rows already give: the map `semaron` spells `zaron` so in the font `semaron-kuriale`; a native form is stated only where the rows spell the word differently ``
2209
+
2210
+ > `` `data.words[0].native[0]` holds U+F024F, which the font `semaron-kuriale`, listed in the map `semaron`, does not draw ``
2211
+
2212
+ The "states what the rows already give" finding compares a native form stated
2213
+ for a face with that face's rows, and one stated for no face with every listed
2214
+ face's rows: it is redundant only where every face's rows give it. This is what
2215
+ keeps a lexicon to its exceptions.
2216
+
2217
+ `package-build transliterate` maps a passage, and `package-build lexicon` lists
2218
+ every word with its native form, through the same code as the build: see
2219
+ [`transliterate`](../commands.md#package-build-transliterate-map-text) and
2220
+ [`lexicon`](../commands.md#package-build-lexicon-language).
2221
+
1979
2222
  ### Assets are types
1980
2223
 
1981
2224
  `icon`, `image` and `audio` are types in the type vocabulary, and an address
@@ -3149,6 +3392,69 @@ metadata supplies `names` in that package's SQL schema. Public queries exclude
3149
3392
  GM-only owners; all/native contexts retain them. Only authored occurrences
3150
3393
  contribute rows, so generated queries and sections add no further names.
3151
3394
 
3395
+ **`runs`** is one row per span, generic div and poetry fence an authored note
3396
+ body marks with `lang=`, `font=` or `map=`. Each row has these columns,
3397
+ including on an empty relation:
3398
+
3399
+ | Column | SQL type | Meaning |
3400
+ | -------- | --------- | -------------------------------------------------------------------------------------------- |
3401
+ | `owner` | `VARCHAR` | Containing authored page's canonical NOTE Address, or `NULL` when the record has no Address. |
3402
+ | `kind` | `VARCHAR` | `span`, `block` or `poetry`. |
3403
+ | `lang` | `VARCHAR` | The language as written, its own or inherited, or `NULL`. |
3404
+ | `tag` | `VARCHAR` | The BCP 47 tag `lang` resolves to, or `NULL` when it resolves to none. |
3405
+ | `font` | `VARCHAR` | The font as written, its own or inherited, or `NULL`. |
3406
+ | `map` | `VARCHAR` | The map as written, its own or inherited, or `NULL`. |
3407
+ | `line` | `BIGINT` | Original 1-based source line of the span's `[`, the div's `:::` or the fence's opening. |
3408
+ | `column` | `BIGINT` | Original 1-based UTF-16 source column there. |
3409
+
3410
+ ```sql
3411
+ SELECT owner, kind, tag, line, "column"
3412
+ FROM runs
3413
+ WHERE font = 'semaron-kuriale'
3414
+ ORDER BY owner, line, "column"
3415
+ ```
3416
+
3417
+ **`maps`** is one row per well-formed row of a [mapping note](#mapping-notes)'s
3418
+ `data.map.rows` and of each face's own `rows`. Each row has these columns,
3419
+ including on an empty relation:
3420
+
3421
+ | Column | SQL type | Meaning |
3422
+ | -------- | --------- | ----------------------------------------------------------- |
3423
+ | `owner` | `VARCHAR` | The mapping note's canonical NOTE Address. |
3424
+ | `name` | `VARCHAR` | The map's `name`. |
3425
+ | `font` | `VARCHAR` | The face whose own row it is; `NULL` for a shared row. |
3426
+ | `row` | `BIGINT` | The row's 1-based place in its `rows`. |
3427
+ | `from` | `VARCHAR` | The characters it matches. |
3428
+ | `to` | `VARCHAR` | What it writes; empty for a row that drops what it matches. |
3429
+ | `before` | `VARCHAR` | `vowel`, `consonant` or `NULL`. |
3430
+ | `at` | `VARCHAR` | `start`, `end` or `NULL`. |
3431
+
3432
+ `from`, `to` and `at` are SQL keywords, so a query quotes them:
3433
+
3434
+ ```sql
3435
+ SELECT "row", "from", "to", before FROM maps WHERE name = 'vyl-semaron' ORDER BY "row"
3436
+ ```
3437
+
3438
+ **`natives`** is one row per entry of a word's `native` on every
3439
+ [language note](#a-map-reads-the-dictionary-first). Each row has these columns,
3440
+ including on an empty relation:
3441
+
3442
+ | Column | SQL type | Meaning |
3443
+ | ---------- | --------- | ---------------------------------------------------------------- |
3444
+ | `language` | `VARCHAR` | The language note's Address, as `words.language` holds it. |
3445
+ | `word` | `BIGINT` | The word's place in `data.words`, from `0`, as `words.position`. |
3446
+ | `form` | `VARCHAR` | The word's `form`. |
3447
+ | `native` | `BIGINT` | The entry's place in the word's `native`, from `0`. |
3448
+ | `map` | `VARCHAR` | The map the form is written for. |
3449
+ | `font` | `VARCHAR` | The one face it is for; `NULL` for every face the map lists. |
3450
+ | `text` | `VARCHAR` | The word as the script writes it; a value, so case is kept. |
3451
+
3452
+ The native form of a word with no entry is computed on demand, by the map's
3453
+ rows: `package-build lexicon` lists every word with it.
3454
+
3455
+ Declared dependency metadata supplies `runs`, `maps` and `natives` in that
3456
+ package's SQL schema, and public queries exclude GM-only owners, as for `names`.
3457
+
3152
3458
  **`language_lineage`** is one row per language note and each language it
3153
3459
  inherits words from through [`data.family`](#a-languages-words): `language`,
3154
3460
  `ancestor` and `depth`, with the language itself at depth `0`, its parent at
@@ -5574,15 +5880,16 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5574
5880
  | `grammar.plural` | `{ ending, becomes }[]` | Literal suffix replacements in authored order; the first match wins |
5575
5881
  | `grammar.plural[].ending` | `string`, required, may be empty | Bare suffix to replace; empty matches every noun |
5576
5882
  | `grammar.plural[].becomes` | `string`, required, may be empty | Bare replacement; empty deletes the matching suffix |
5577
- | `grammar.verbs` | `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they } }` | One regular present pattern, printed once at the dictionary head |
5883
+ | `grammar.verbs` | `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they }, replaces? }` | One regular present pattern, printed once at the dictionary head |
5578
5884
  | `grammar.verbs.invariant` | `true` | Verbs do not change; excludes `persons` |
5579
- | `grammar.verbs.persons` | a map of all six ending strings | Each ending is added to the citation form; excludes `invariant` |
5885
+ | `grammar.verbs.persons` | a map of all six ending strings | Each ending is added to the citation form, or replaces its closing `replaces`; excludes `invariant` |
5580
5886
  | `grammar.verbs.persons.I` | `string`, required, may be empty | Ending for I |
5581
5887
  | `grammar.verbs.persons.you` | `string`, required, may be empty | Ending for you |
5582
5888
  | `grammar.verbs.persons.heShe` | `string`, required, may be empty | Ending for he/she |
5583
5889
  | `grammar.verbs.persons.we` | `string`, required, may be empty | Ending for we |
5584
5890
  | `grammar.verbs.persons.youAll` | `string`, required, may be empty | Ending for you all |
5585
5891
  | `grammar.verbs.persons.they` | `string`, required, may be empty | Ending for they |
5892
+ | `grammar.verbs.replaces` | `string`, nonempty | Closing letters each ending takes the place of; a citation form closing otherwise takes the ending whole. With `persons` only |
5586
5893
  | `classModifiers` | `string[]` | `language` only: the word class modifiers the language uses, each from the [master word classes](#word-classes); empty when unstated |
5587
5894
  | `fields` | `{ name, holds }[]` | `language` only: the fields its words are grouped under |
5588
5895
  | `fields[].name` | `string`, required | The field's name, which a word's `field` names |
@@ -5605,6 +5912,10 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5605
5912
  | `words[].tones` | `string` | A tone pattern, as `LHL` |
5606
5913
  | `words[].root` | `string` | A consonantal skeleton, as `d-l-k` |
5607
5914
  | `words[].frame` | `string` | The frame a root is set in, named in `sounds.frames` of the language or an ancestor |
5915
+ | `words[].native` | `{ map, font?, text }[]` | The word's native form under a map, stated only where the rows spell it differently — see [a map reads the dictionary first](#a-map-reads-the-dictionary-first) |
5916
+ | `words[].native[].map` | `string` | Required: the `data.map.name` the form is written for |
5917
+ | `words[].native[].font` | `string` | The one face the form is for, one the map lists; unstated, every face it lists |
5918
+ | `words[].native[].text` | `string` | Required: the word as the script writes it |
5608
5919
  | `affixes` | `{ form?, zero?, kind?, given?, where?, gloss, attaches?, examples?, barredAfter?, tones? }[]` | `language` only: the affixes a word's `built` pieces may name; each states a `form` or `zero: true` |
5609
5920
  | `affixes[].form` | `string` | The affix as a `built` piece names it, as `-rith`; absent on a zero affix |
5610
5921
  | `affixes[].zero` | `boolean` | The affix is written as nothing, as a case with no ending; it states no `form`, prints as `∅`, and no `built` piece names it |
@@ -5864,7 +6175,27 @@ The raw `words` relation retains only the authored `plural`; the
5864
6175
  the `persons` map shown above. A supplied `persons` map has all six string
5865
6176
  endings, each added to the citation form; `""` leaves that form unchanged.
5866
6177
  The dictionary prints the pattern once in a small table before its letter
5867
- headings. A custom dictionary's default or `key=true` prints that table even
6178
+ headings, headed "Ending added to citation form".
6179
+
6180
+ Where the present endings take the place of the citation form's closing
6181
+ letters, the pattern states them as `replaces`:
6182
+
6183
+ ```yaml
6184
+ grammar:
6185
+ verbs:
6186
+ replaces: then
6187
+ persons: { I: men, you: zen, heShe: zor, we: mar, youAll: zar, they: zoi }
6188
+ ```
6189
+
6190
+ A form is then the citation form with its closing `then` swapped for the
6191
+ ending: `lokathen` gives `lokamen`, `lokazor` and the rest, and an empty
6192
+ ending drops the `then`. A citation form that does not close on `replaces`
6193
+ takes the ending whole, as a suffix does when its `replaces` does not match,
6194
+ so `varos` gives `varoszor`. It is not a finding, because no word is checked
6195
+ against a grammar rule. The
6196
+ dictionary's table is then headed "Ending in place of a closing -then", and
6197
+ an empty ending prints as "-then dropped". `replaces` is a literal, nonempty
6198
+ string, and only accompanies `persons`. A custom dictionary's default or `key=true` prints that table even
5868
6199
  when no abbreviation key is needed. `key=false` fragments omit both the key
5869
6200
  and table; `key=only` prints the abbreviation key alone. Separate results
5870
6201
  with their heads enabled each print their own tables.
@@ -6115,6 +6446,24 @@ affiliation in `scope` having it as a rank or office title, and every anchor of
6115
6446
  a note in `scope` having it as its name. A folder note carries no name, and a
6116
6447
  language that states no `scope` reads every note of the package.
6117
6448
 
6449
+ **A row tagged `other:<tongue>` or `exonym:<tongue>` names a name of that
6450
+ tongue's own register.** The tongue names the language note that a
6451
+ `data.tongues` entry tagged with it names by its `language`, in any language
6452
+ note of the package: `{ tag: vylari, language: skill-vylarilng }` makes
6453
+ `exonym:vylari` read the Vylari note. That note's `data.register` must hold a
6454
+ row of the same name, case folded and NFC, whose `tongue` is the tongue itself,
6455
+ so a misspelt foreign name is caught where it is listed. A tongue that no entry
6456
+ links to a language note, or that entries link to two, is a warning at the
6457
+ row's `tongue`; a missing name is a warning at its `name`:
6458
+
6459
+ > `` `Dunharra` is registered as `other:vylari`, but the register of Vylari Language (skill-vylarilng) lists no `vylari` name `Dunharra`; correct the spelling, or register the name there first ``
6460
+
6461
+ > `` the tongue `nordmal` in `exonym:nordmal` names no language note: no language note's `data.tongues` tags `nordmal` with a `language` ``
6462
+
6463
+ Bare `other`, which names no tongue, and every other tag are not checked. A
6464
+ language note of another package is not read, since its register does not
6465
+ reach the package's index.
6466
+
6118
6467
  **`retired` lists the forms the setting does not write**, each with what is
6119
6468
  written in its place, one form or several, and why. A form nothing replaces
6120
6469
  states no `instead`.
@@ -6290,6 +6639,8 @@ Each finding is located at the key at fault:
6290
6639
  | A `scope` Address that does not resolve, or a type that is no note type | error |
6291
6640
  | A tongue `language` or a cognate form's language that is no language note | error |
6292
6641
  | A register row with neither a `tongue` nor a `kind`, at the row | error |
6642
+ | A register row tagged `other:<tongue>` or `exonym:<tongue>` whose tongue no `data.tongues` entry links to one language note, at the tongue | warning |
6643
+ | A register row tagged `other:<tongue>` or `exonym:<tongue>` whose name the tongue's own register does not list under that tongue, at the name | warning |
6293
6644
  | A sound-change set's name stated twice, at the second | error |
6294
6645
  | A step label stated twice in one set, at the second | error |
6295
6646
  | A set's `from` or `to` that is no language note, at the value | error |
@@ -7485,9 +7836,24 @@ Content prepared to be played — a situation with its cast, places, and possibl
7485
7836
  - concept: An explanation of how something works and why it is shaped that way, read to understand rather than to follow.
7486
7837
  - settingguide: A culture's guide to the setting, which names its culture in `data.culture`. It orients a reader to a setting or region, bringing its places, peoples, institutions, and daily life into one usable frame with links to detailed notes.
7487
7838
 
7488
- | `data` property | Values | Description |
7489
- | --------------- | --------- | --------------------------------------------------------------------------------------------------------------------- |
7490
- | `culture` | `Address` | `settingguide` only, and required there: the culture the guide introduces, naming a lore note with `subType: culture` |
7839
+ | `data` property | Values | Description |
7840
+ | --------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
7841
+ | `culture` | `Address` | `settingguide` only, and required there: the culture the guide introduces, naming a lore note with `subType: culture` |
7842
+ | `map` | `{ name, vowels?, fonts, rows }` | A mapping, which text marked `map=` with its `name` is rewritten by; any subType. See [mapping notes](#mapping-notes) |
7843
+ | `map.name` | string | Required: what `map=` names it by, lowercase letters, digits and `-`, unique in the index |
7844
+ | `map.vowels` | string | The characters `before: vowel` reads as vowels |
7845
+ | `map.rows` | list of `[from, to]` or `{ from, to, before?, at? }` | Required: the rewrites, in authored order |
7846
+ | `map.rows[].from` | string of one or more characters | Required in a map row: the characters it matches |
7847
+ | `map.rows[].to` | string, which may be empty | Required in a map row: what it writes in their place |
7848
+ | `map.rows[].before` | `vowel \| consonant` | What must follow the match |
7849
+ | `map.rows[].at` | `start \| end` | Where the match must sit |
7850
+ | `map.fonts` | list of `{ font, rows? }` | Required, at least one: the faces the map is drawn in |
7851
+ | `map.fonts[].font` | an installed face's slug | Required: the face, as `font=` names it; not `mono`, `serif` or `sans` |
7852
+ | `map.fonts[].rows` | list of `[from, to]` or `{ from, to, before?, at? }` | Rows for this face alone, tried before the shared `rows` |
7853
+ | `map.fonts[].rows[].from` | string of one or more characters | Required in a map row: the characters it matches |
7854
+ | `map.fonts[].rows[].to` | string, which may be empty | Required in a map row: what it writes in their place |
7855
+ | `map.fonts[].rows[].before` | `vowel \| consonant` | What must follow the match |
7856
+ | `map.fonts[].rows[].at` | `start \| end` | Where the match must sit |
7491
7857
 
7492
7858
  A setting guide introduces one culture's view of the setting, so it names that
7493
7859
  culture, and a `doc` of any other `subType` has no culture of its own to name.
@@ -507,6 +507,10 @@ These fields are accepted by every note type. A field's value can still be irrel
507
507
  | `data.words[].tones` | a string | no | A tone pattern, such as `LHL`. |
508
508
  | `data.words[].root` | a string | no | A consonantal skeleton, such as `d-l-k`. |
509
509
  | `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`. |
510
+ | `data.words[].native` | a list of `{ map, font?, text }` | no | The word's native form under a map, stated only where the map's rows spell it differently. Its derived forms are spelled by the rows. |
511
+ | `data.words[].native[].map` | a mapping note's `data.map.name` | yes | The map the native form is written for. |
512
+ | `data.words[].native[].font` | a face the map lists | no | The one face the native form is for; unstated, it is for every face the map lists, and a form stated for a face wins over it there. |
513
+ | `data.words[].native[].text` | the word in the script, as the map's characters | yes | The word as the script writes it, where the map's rows would write it otherwise. |
510
514
  | `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. |
511
515
  | `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. |
512
516
  | `data.affixes[].kind` | one of case, derivation, tense, aspect, mood, voice, name, lineage | no | What the affix makes. |
@@ -523,15 +527,16 @@ These fields are accepted by every note type. A field's value can still be irrel
523
527
  | `data.grammar.plural` | list of `{ ending, becomes }` | no | Default noun plurals, replacing the first matching literal ending in authored order. A word's explicit plural wins; no word is checked against this rule. |
524
528
  | `data.grammar.plural[].ending` | a literal ending string, which may be empty | yes | The bare suffix replaced by this rule; empty matches every noun. |
525
529
  | `data.grammar.plural[].becomes` | a literal ending string, which may be empty | yes | The bare replacement; empty removes the matching suffix. |
526
- | `data.grammar.verbs` | a map — `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they } }` | no | One optional regular present pattern, printed once at the dictionary head. |
530
+ | `data.grammar.verbs` | a map — `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they }, replaces? }` | no | One optional regular present pattern, printed once at the dictionary head. |
527
531
  | `data.grammar.verbs.invariant` | true, as the alternative to persons | no | True where verbs keep the citation form for every person; excludes `persons`. |
528
- | `data.grammar.verbs.persons` | a map — `{ I, you, heShe, we, youAll, they }` of ending strings | no | All six present endings, each added to the citation form; excludes `invariant`. |
529
- | `data.grammar.verbs.persons.I` | an ending string, empty for unchanged | yes | The ending added to the citation form for I. |
530
- | `data.grammar.verbs.persons.you` | an ending string, empty for unchanged | yes | The ending added to the citation form for you. |
531
- | `data.grammar.verbs.persons.heShe` | an ending string, empty for unchanged | yes | The ending added to the citation form for he/she. |
532
- | `data.grammar.verbs.persons.we` | an ending string, empty for unchanged | yes | The ending added to the citation form for we. |
533
- | `data.grammar.verbs.persons.youAll` | an ending string, empty for unchanged | yes | The ending added to the citation form for you all. |
534
- | `data.grammar.verbs.persons.they` | an ending string, empty for unchanged | yes | The ending added to the citation form for they. |
532
+ | `data.grammar.verbs.persons` | a map — `{ I, you, heShe, we, youAll, they }` of ending strings | no | All six present endings, each added to the citation form or in place of its closing `replaces`; excludes `invariant`. |
533
+ | `data.grammar.verbs.persons.I` | an ending string, empty for unchanged | yes | The ending for I, added to the citation form or in place of its closing `replaces`. |
534
+ | `data.grammar.verbs.persons.you` | an ending string, empty for unchanged | yes | The ending for you, added to the citation form or in place of its closing `replaces`. |
535
+ | `data.grammar.verbs.persons.heShe` | an ending string, empty for unchanged | yes | The ending for he/she, added to the citation form or in place of its closing `replaces`. |
536
+ | `data.grammar.verbs.persons.we` | an ending string, empty for unchanged | yes | The ending for we, added to the citation form or in place of its closing `replaces`. |
537
+ | `data.grammar.verbs.persons.youAll` | an ending string, empty for unchanged | yes | The ending for you all, added to the citation form or in place of its closing `replaces`. |
538
+ | `data.grammar.verbs.persons.they` | an ending string, empty for unchanged | yes | The ending for they, added to the citation form or in place of its closing `replaces`. |
539
+ | `data.grammar.verbs.replaces` | the closing letters every ending takes the place of | no | Letters a citation form closes on, swapped for each `persons` ending: with `then`, `lokathen` and `zor` give `lokazor`. A citation form closing otherwise takes the ending whole. With `persons` only. |
535
540
  | `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. |
536
541
  | `data.tongues[].meaning` | a string | yes | What a name of this tongue is. |
537
542
  | `data.tongues[].language` | a language skill Address | no | The language note the tag names, where it is one. |
@@ -962,9 +967,29 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
962
967
 
963
968
  **Subtypes:** `rules`, `userguide`, `reference`, `howto`, `concept`, `settingguide`.
964
969
 
965
- | Field | Shape | Meaning |
966
- | -------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
967
- | `data.culture` | an Address | The culture a setting guide introduces, as a culture lore note. Required on `settingguide`; no other subType states it. |
970
+ | Field | Shape | Meaning |
971
+ | -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
972
+ | `data.culture` | an Address | The culture a setting guide introduces, as a culture lore note. Required on `settingguide`; no other subType states it. |
973
+ | `data.map` | a map — `{ name, vowels?, fonts, rows }` | A mapping, which text marked `map=` with its `name` is rewritten by before it is set. See [mapping notes](format-details.md#mapping-notes). |
974
+
975
+ **Inner keys** — a key a field's value does not declare is an error at its own line and column.
976
+
977
+ | Key | Shape | Required | Meaning |
978
+ | -------------------------------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
979
+ | `data.map.name` | string | yes | What `map=` names the map by: lowercase letters, digits and `-`, unique in the package. |
980
+ | `data.map.vowels` | string | no | The characters a row's `before: vowel` reads as vowels; any other letter is a consonant. |
981
+ | `data.map.rows` | list of `[from, to]` or `{ from, to, before?, at? }` | yes | The rewrites, each `[from, to]` or `{ from, to, before?, at? }`: `before` is `vowel` or `consonant`, `at` is `start` or `end`. The longest `from` wins, and a character no row matches passes through. |
982
+ | `data.map.rows[].from` | a string of one or more characters | yes | The one or more characters the row matches. |
983
+ | `data.map.rows[].to` | a string, which may be empty | yes | What the row writes in their place; empty to drop them. |
984
+ | `data.map.rows[].before` | string | no | `vowel` or `consonant`: what must follow the match for the row to apply. |
985
+ | `data.map.rows[].at` | string | no | `start` or `end`: where the match must sit for the row to apply. |
986
+ | `data.map.fonts` | list of `{ font, rows? }` | yes | The faces the map is drawn in, at least one, each with any rows of its own. A run in another face is refused, and each face must draw every character its rows and the lexicon's native forms write. |
987
+ | `data.map.fonts[].font` | an installed face's slug | yes | A face the map is drawn in, by the slug `font=` names it by. |
988
+ | `data.map.fonts[].rows` | list of `[from, to]` or `{ from, to, before?, at? }` | no | Rows for this face alone, tried before the shared `rows`; the same shapes and conditions. |
989
+ | `data.map.fonts[].rows[].from` | a string of one or more characters | yes | The one or more characters the row matches. |
990
+ | `data.map.fonts[].rows[].to` | a string, which may be empty | yes | What the row writes in their place; empty to drop them. |
991
+ | `data.map.fonts[].rows[].before` | string | no | `vowel` or `consonant`: what must follow the match for the row to apply. |
992
+ | `data.map.fonts[].rows[].at` | string | no | `start` or `end`: where the match must sit for the row to apply. |
968
993
 
969
994
  ### macro
970
995
 
@@ -66,6 +66,7 @@ import { authoredFrontmatter } from "./index-records.mjs";
66
66
  import { markupAnchorFindings } from "./anchors.mjs";
67
67
  import { scanSpans } from "./content-spans.mjs";
68
68
  import { scanBlocks } from "./content-blocks.mjs";
69
+ import { scanRuns } from "./content-scripts.mjs";
69
70
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
70
71
  import { scanFigures } from "./content-figures.mjs";
71
72
  import { footnoteFindings } from "./content-footnotes.mjs";
@@ -691,6 +692,7 @@ export class BasePackCompiler {
691
692
  ...(scanned?.blocks ?? scanBlocks(body)).errors,
692
693
  ...(scanned?.alerts ?? scanAlerts(body)).errors,
693
694
  ...(scanned?.spans ?? scanSpans(body)).errors,
695
+ ...scanRuns(body, this.corpus?.runs, syntax).errors,
694
696
  ...markupAnchorFindings(body, syntax),
695
697
  ]) {
696
698
  findings.push({
@@ -332,8 +332,13 @@ export class Bundles extends BasePackCompiler {
332
332
  // it compiles into already has somewhere to put the prose.
333
333
  description:
334
334
  markdown.trim() ?
335
- renderFoundryMarkdown(markdown, undefined, undefined, undefined, (pathname) =>
336
- roles.get(pathname),
335
+ renderFoundryMarkdown(
336
+ markdown,
337
+ undefined,
338
+ undefined,
339
+ undefined,
340
+ (pathname) => roles.get(pathname),
341
+ this.corpus?.runs,
337
342
  )
338
343
  : "",
339
344
  folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
@@ -41,6 +41,7 @@
41
41
  import { indexRecordsFor } from "./content-index.mjs";
42
42
  import { buildContentLinkIndex } from "./helpers.mjs";
43
43
  import { loadPackConfig } from "./pack-config.mjs";
44
+ import { runContext } from "./content-scripts.mjs";
44
45
  import { prepareTreeSqlTables } from "./sql-tables.mjs";
45
46
 
46
47
  /**
@@ -61,7 +62,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
61
62
  * the index refuses is a note the compile must still *report*, exactly as the
62
63
  * compile loop reported it when the loop was the first to see it.
63
64
  * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
64
- * @returns {Promise<{currentDate: string, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
65
+ * @returns {Promise<{currentDate: string, runs: import("./content-scripts.mjs").RunContext, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
65
66
  * its indexes, and the notes it could not record.
66
67
  */
67
68
  export async function buildCompileCorpus({
@@ -100,6 +101,7 @@ export async function buildCompileCorpus({
100
101
  const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
101
102
  return {
102
103
  currentDate: resolved.currentDate,
104
+ runs: runContext(resolved, records),
103
105
  records,
104
106
  parsed,
105
107
  problems: collected,