@heroiclands/package-build 23.12.7 → 23.13.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 (76) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/assets/preview/gitea.ini +2 -2
  4. package/assets/styles/content-base.css +143 -23
  5. package/bin/content-commands.mjs +15 -4
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/docs/api.md +18 -18
  8. package/docs/authoring/assets.md +1 -1
  9. package/docs/authoring/links-and-markup.md +156 -22
  10. package/docs/commands.md +32 -10
  11. package/docs/configuration.md +8 -1
  12. package/docs/content-format.md +3 -3
  13. package/docs/diagnostics.md +42 -0
  14. package/docs/reference/format-details.md +105 -22
  15. package/docs/reference/note-types.md +8 -1
  16. package/docs/reference/theme-pages.md +51 -0
  17. package/docs/source-preview.md +1 -1
  18. package/e2e.mjs +1 -1
  19. package/engine/anchors.mjs +2 -2
  20. package/engine/asset-shipping.mjs +22 -8
  21. package/engine/code-fences.mjs +3 -3
  22. package/engine/content-blocks.mjs +3 -3
  23. package/engine/content-figures.mjs +5 -5
  24. package/engine/content-index.mjs +1 -1
  25. package/engine/content-scripts.mjs +55 -21
  26. package/engine/content-spans.mjs +2 -2
  27. package/engine/content-verse.mjs +750 -0
  28. package/engine/dictionary-rows.mjs +15 -4
  29. package/engine/field-reference.mjs +4 -4
  30. package/engine/first-use.mjs +17 -27
  31. package/engine/key-case-collisions.mjs +136 -0
  32. package/engine/language-data.mjs +203 -6
  33. package/engine/language-terms.mjs +74 -5
  34. package/engine/map-lexicon.mjs +42 -23
  35. package/engine/names.mjs +36 -9
  36. package/engine/note-matter.mjs +66 -0
  37. package/engine/pdf-build.mjs +1 -0
  38. package/engine/pdf-render.mjs +107 -18
  39. package/engine/read-stdin.mjs +29 -0
  40. package/engine/run-rows.mjs +2 -2
  41. package/engine/run-words.mjs +4 -111
  42. package/engine/site-build.mjs +4 -4
  43. package/engine/site-preview.mjs +14 -14
  44. package/engine/sql-tables.mjs +20 -3
  45. package/engine/web-wikilinks.mjs +1 -1
  46. package/engine/word-forms.mjs +349 -0
  47. package/engine/word-terms.mjs +9 -0
  48. package/hugo-theme/static/css/style.css +92 -14
  49. package/package.json +3 -3
  50. package/types/e2e.d.mts +2 -2
  51. package/types/engine/address-fields.d.mts +1 -1
  52. package/types/engine/address-values.d.mts +1 -1
  53. package/types/engine/asset-shipping.d.mts +5 -0
  54. package/types/engine/code-fences.d.mts +3 -3
  55. package/types/engine/content-format-check.d.mts +1 -1
  56. package/types/engine/content-scripts.d.mts +8 -4
  57. package/types/engine/content-verse.d.mts +240 -0
  58. package/types/engine/dictionary-rows.d.mts +10 -0
  59. package/types/engine/field-reference.d.mts +1 -1
  60. package/types/engine/key-case-collisions.d.mts +28 -0
  61. package/types/engine/language-data.d.mts +0 -2
  62. package/types/engine/map-lexicon.d.mts +19 -6
  63. package/types/engine/names.d.mts +34 -3
  64. package/types/engine/note-matter.d.mts +48 -0
  65. package/types/engine/note-vocabulary.d.mts +1 -1
  66. package/types/engine/pdf-coverage.d.mts +1 -1
  67. package/types/engine/read-stdin.d.mts +11 -0
  68. package/types/engine/run-rows.d.mts +2 -2
  69. package/types/engine/run-words.d.mts +0 -15
  70. package/types/engine/sql-tables.d.mts +4 -1
  71. package/types/engine/text-maps.d.mts +1 -1
  72. package/types/engine/web-wikilinks.d.mts +1 -1
  73. package/types/engine/word-forms.d.mts +82 -0
  74. package/types/release.d.mts +1 -1
  75. package/engine/content-poetry.mjs +0 -240
  76. package/types/engine/content-poetry.d.mts +0 -58
@@ -1910,7 +1910,15 @@ authored labels supply none.
1910
1910
 
1911
1911
  1. A `.name` span whose whole text matches an indexed `name.full` or
1912
1912
  `name.aliases` should be a link. Use `[[place-arden|Arden Vale]]` or
1913
- `[[place-arden|]]` to introduce that note.
1913
+ `[[place-arden|]]` to introduce that note. A span with a `data-type`
1914
+ matches only notes of that type, and only notes of that subtype when it
1915
+ names one; an untyped span matches a note of any type. One name can
1916
+ belong to two things, so
1917
+ `[Theinuvael]{.name data-type="affiliation/lineage"}` draws no warning
1918
+ when _Theinuvael_ is an alias of a place note, while
1919
+ `[Theinuvael]{.name data-type="place"}` and `[Theinuvael]{.name}` should
1920
+ be links to it. The same match decides a span's identity for the rules
1921
+ below and for the unwritten-name advisory.
1914
1922
  2. Mark or link each name at most once per note. An indexed full name and its
1915
1923
  declared aliases share one identity, including across different heading
1916
1924
  anchors. Other labels use their whole text as their identity: different
@@ -1921,10 +1929,9 @@ authored labels supply none.
1921
1929
  note under any label. Possessives and regular plurals count as uses; other
1922
1930
  indexed forms can be declared aliases.
1923
1931
 
1924
- Bold is emphasis. A whole bold span exactly matching an indexed full name or
1925
- alias receives one advisory to mark or link the name. It supplies no name
1926
- occurrence or introduction, and bold containing a marked or linked name
1927
- receives no such advisory. Unknown bold phrases supply no names.
1932
+ Bold is emphasis and is never read as a name. A bold label such as
1933
+ `**Strength**:` is ordinary emphasis even when its text matches an indexed
1934
+ full name or alias, and it supplies no name occurrence or introduction.
1928
1935
 
1929
1936
  Inline Markdown remains active inside name spans, and name spans remain
1930
1937
  valid inside emphasis:
@@ -1968,7 +1975,8 @@ This is derived metadata, not frontmatter. Documentation mirrors and assets
1968
1975
  do not duplicate a note's occurrences. The [`names` relation](#content-tables)
1969
1976
  flattens these entries with their containing NOTE Address.
1970
1977
 
1971
- Lint groups names without an indexed full-name or alias match by supplied
1978
+ Lint groups names without an indexed full-name or alias match, among notes
1979
+ of the span's type and subtype when it supplies them, by supplied
1972
1980
  classification and lists all their source uses. It also groups conflicting
1973
1981
  annotations of the same visible name. Untyped uses do not conflict. A bare
1974
1982
  type and its subtype agree; different supplied types or different supplied
@@ -1978,7 +1986,7 @@ exit status. Supplied-value errors prevent publication and compilation.
1978
1986
 
1979
1987
  #### Text marked with a language, a font or a map
1980
1988
 
1981
- `lang=`, `font=` and `map=` on a span, a generic div or a poetry fence are read
1989
+ `lang=`, `font=` and `map=` on a span, a generic div or a verse fence are read
1982
1990
  by the build and never written through as ordinary attributes. Each is
1983
1991
  optional, and a run inside a div or a fence inherits the keys the div or fence
1984
1992
  states, its own keys overriding them. On a named block the three keys are
@@ -2015,7 +2023,7 @@ with a real language's tag alone keeps its Markdown. See
2015
2023
 
2016
2024
  Each own-note metadata record carries `runs` in source order, an empty array
2017
2025
  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
2026
+ `verse`), `lang`, `tag`, `font`, `map`, `line` and `column`: `lang`, `font` and
2019
2027
  `map` as written, its own or inherited, `tag` the BCP 47 tag `lang` resolves
2020
2028
  to, each `null` when absent, and the position of the span's `[`, the div's
2021
2029
  `:::` or the fence's opening. Every value is a string or a number and none is a
@@ -2184,10 +2192,13 @@ word's start:
2184
2192
  lists under a `tongue` other than `translation`, or a form one rule derives from a lexicon form —
2185
2193
  the `data.grammar.plural` plural of a noun or its own `plural`, a verb's form
2186
2194
  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
2195
+ `data.grammar.verbs.replaces` where one is stated), a suffix of `data.affixes`
2196
+ stating `replaces` on a word of a class its `attachesTo` names, or on any word
2197
+ where it names none (`vowel` takes the place of a closing `a`, `e`, `i`, `o` or
2189
2198
  `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
2199
+ otherwise takes the suffix whole), an agreeing form `data.grammar.agreement`
2200
+ gives a word of one of its `classes`, or a participle `data.grammar.participles`
2201
+ gives a verb (see [optional noun and verb forms](#optional-noun-and-verb-forms)). A compound not known whole is checked part by
2191
2202
  part at its hyphens and en dashes. Any other word is an error at its first
2192
2203
  character, once per run; see
2193
2204
  [a mapped run's words](../authoring/links-and-markup.md#a-mapped-runs-words):
@@ -2493,6 +2504,12 @@ layout serves Foundry (`modules/thalorna/images/thorn.webp`), the website's CDN
2493
2504
  (`<cdn base>/thalorna/images/thorn.webp`), and every location a world copies
2494
2505
  the pictures to.
2495
2506
 
2507
+ **A row's files are kept.** Staging the pictures writes them beside whatever a
2508
+ `packageBuild.assets` row copied under `icons/` or `images/`, such as a
2509
+ subdirectory of other art, and removes none of it. A picture whose flat path is
2510
+ already a file a row staged is an error, and the row's file stays. Clearing an
2511
+ earlier build's pictures is the stage reset's work.
2512
+
2496
2513
  **The package's asset set**, `asset-sets/default.json` under the package
2497
2514
  root, lists what it ships. The name is fixed, so a world always finds the
2498
2515
  package's own set there:
@@ -3308,7 +3325,7 @@ inherited words are read through `language_lineage`.
3308
3325
  **`affixes`** is one row per entry of `data.affixes` on every language note,
3309
3326
  and **`fields`** one per entry of `data.fields`, in the order the note states
3310
3327
  them. Each has `language` and `position` as `words` does, then every key of the
3311
- entry as its own column: `form`, `zero`, `kind`, `given`, `where`, `gloss`, `attaches`, `replaces`, `lengthens`, `front`, `examples`, `barredAfter`
3328
+ entry as its own column: `form`, `zero`, `kind`, `given`, `where`, `gloss`, `attaches`, `replaces`, `attachesTo`, `lengthens`, `front`, `examples`, `barredAfter`
3312
3329
  and `tones` for an affix, then `written`, its form as a table prints it (`∅` for a zero affix), `name` and `holds` for a field. A key an entry does not state is
3313
3330
  `NULL`, `examples` is a list, and a language note tagged `gm` contributes no
3314
3331
  row on a public surface. An affix's `tones` is the tone pattern it carries,
@@ -3392,14 +3409,14 @@ metadata supplies `names` in that package's SQL schema. Public queries exclude
3392
3409
  GM-only owners; all/native contexts retain them. Only authored occurrences
3393
3410
  contribute rows, so generated queries and sections add no further names.
3394
3411
 
3395
- **`runs`** is one row per span, generic div and poetry fence an authored note
3412
+ **`runs`** is one row per span, generic div and verse fence an authored note
3396
3413
  body marks with `lang=`, `font=` or `map=`. Each row has these columns,
3397
3414
  including on an empty relation:
3398
3415
 
3399
3416
  | Column | SQL type | Meaning |
3400
3417
  | -------- | --------- | -------------------------------------------------------------------------------------------- |
3401
3418
  | `owner` | `VARCHAR` | Containing authored page's canonical NOTE Address, or `NULL` when the record has no Address. |
3402
- | `kind` | `VARCHAR` | `span`, `block` or `poetry`. |
3419
+ | `kind` | `VARCHAR` | `span`, `block` or `verse`. |
3403
3420
  | `lang` | `VARCHAR` | The language as written, its own or inherited, or `NULL`. |
3404
3421
  | `tag` | `VARCHAR` | The BCP 47 tag `lang` resolves to, or `NULL` when it resolves to none. |
3405
3422
  | `font` | `VARCHAR` | The font as written, its own or inherited, or `NULL`. |
@@ -3854,11 +3871,11 @@ The caption attaches to exactly the next supported block:
3854
3871
  | Markdown table | `table` | Table |
3855
3872
  | Image, Markdown or wikilink | `figure` | Figure |
3856
3873
  | SQL fence, including backtick SQL | `table` | Table |
3857
- | Poetry fence | `poetry` | Poem |
3874
+ | Verse fence | `verse` | Poem |
3858
3875
  | Any other code fence | `code` | Code |
3859
3876
  | Paragraph, quote block or fenced div | `prose` | Prose |
3860
3877
 
3861
- The optional `type` attribute overrides inference and accepts `figure`, `table`, `poetry`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
3878
+ The optional `type` attribute overrides inference and accepts `figure`, `table`, `verse`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
3862
3879
 
3863
3880
  Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. On a captioned table or SQL fence, `.full-width` sets the table across the page in a PDF book instead of in one column (`: Regional trade routes {.full-width}`); the other surfaces ignore it. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
3864
3881
 
@@ -5876,7 +5893,7 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5876
5893
  | `names.<name>` | `string[]` | A list the placeholder of the same name draws from |
5877
5894
  | `family` | `Address` | `language` only: the parent language, naming a skill note with `subType: language`; this language reads the parent's words as its own — see [a language's words](#a-languages-words) |
5878
5895
  | `abbreviation` | `string` | `language` only: the tag a dictionary prints for it, as `Prov.`; its full name where unstated — see [dictionaries and glossaries](#dictionaries-and-glossaries) |
5879
- | `grammar` | `{ plural?, verbs? }` | Optional dictionary noun plurals and one verb pattern; supplied shapes only are checked |
5896
+ | `grammar` | `{ plural?, verbs?, agreement?, participles? }` | Optional noun plurals, one verb pattern, agreement and participles; supplied shapes only are checked |
5880
5897
  | `grammar.plural` | `{ ending, becomes }[]` | Literal suffix replacements in authored order; the first match wins |
5881
5898
  | `grammar.plural[].ending` | `string`, required, may be empty | Bare suffix to replace; empty matches every noun |
5882
5899
  | `grammar.plural[].becomes` | `string`, required, may be empty | Bare replacement; empty deletes the matching suffix |
@@ -5890,6 +5907,12 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5890
5907
  | `grammar.verbs.persons.youAll` | `string`, required, may be empty | Ending for you all |
5891
5908
  | `grammar.verbs.persons.they` | `string`, required, may be empty | Ending for they |
5892
5909
  | `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 |
5910
+ | `grammar.agreement` | `{ classes, endings }` | Adjectives, pronouns or any other named classes agree by swapping among `endings`; see [optional noun and verb forms](#optional-noun-and-verb-forms) |
5911
+ | `grammar.agreement.classes` | `string[]`, required, at least one | Word class bases from the [master word classes](#word-classes), each once |
5912
+ | `grammar.agreement.endings` | `string[]`, required, at least two | The singular endings, one per gender; the `plural` rule gives each one's plural |
5913
+ | `grammar.participles` | `{ root, affixes }` | Participle suffixes on the verb root; see [optional noun and verb forms](#optional-noun-and-verb-forms) |
5914
+ | `grammar.participles.root` | `string[]`, required, at least one, nonempty | Infinitive endings a citation form drops to give the root; the longest that fits wins |
5915
+ | `grammar.participles.affixes` | `string[]`, required, at least one | Suffixes of `affixes`, by `form`, each attaching to the root as its `replaces` says |
5893
5916
  | `classModifiers` | `string[]` | `language` only: the word class modifiers the language uses, each from the [master word classes](#word-classes); empty when unstated |
5894
5917
  | `fields` | `{ name, holds }[]` | `language` only: the fields its words are grouped under |
5895
5918
  | `fields[].name` | `string`, required | The field's name, which a word's `field` names |
@@ -5928,6 +5951,7 @@ SoHL sets `system.quantity` to `1` for each compendium item.
5928
5951
  | `affixes[].barredAfter` | `string[]` | The sounds a stem may not close on to take the affix, as `[n]` |
5929
5952
  | `affixes[].tones` | `string` | The tone pattern it carries, as `L` |
5930
5953
  | `affixes[].replaces` | `string` | What it takes the place of at the close of the stem: `vowel` for any closing vowel, or the letters, as `a`; added to the whole stem where absent |
5954
+ | `affixes[].attachesTo` | `string[]` | The word classes it attaches to, by their base, as `[n, adj]`; every class where absent |
5931
5955
  | `affixes[].lengthens` | `boolean` | Whether the stem's last vowel lengthens before it; `false` where unstated, and never beside `replaces` |
5932
5956
  | `affixes[].front` | `string` | Its form after front vowels, where the language keeps vowel harmony, as `-ür` |
5933
5957
  | `tongues` | `{ tag, meaning, language? }[]` | `language` only: the tags a register row's `tongue` names — see [a language's register, sounds and history](#a-languages-register-sounds-and-history) |
@@ -6203,6 +6227,59 @@ A language uses only its own stated grammar, including for words
6203
6227
  listed from an ancestor; an inherited word retains its explicit plural.
6204
6228
  Grammar is not inherited through `family`.
6205
6229
 
6230
+ **Agreement** states which word classes agree with a noun and the singular
6231
+ endings they agree by, one for each gender:
6232
+
6233
+ ```yaml
6234
+ grammar:
6235
+ plural:
6236
+ - { ending: os, becomes: oi }
6237
+ - { ending: on, becomes: oi }
6238
+ - { ending: a, becomes: ai }
6239
+ agreement:
6240
+ classes: [adj, pron]
6241
+ endings: [os, a, on]
6242
+ ```
6243
+
6244
+ A word whose class base is one of `classes` and whose form closes on one of
6245
+ `endings` (the longest that leaves a stem) also has that ending swapped for
6246
+ each other one, and each of those forms, and the word itself, takes the plural
6247
+ the `plural` rule gives. So the adjective _alathos_ gives _alatha_ and
6248
+ _alathon_, and the plural rule gives _alathoi_ and _alathai_. A word closing on
6249
+ no ending, such as _ethlen_, does not agree, and a word of another class, such
6250
+ as the noun _marendos_, keeps its own plural alone. `classes` names at least
6251
+ one base of the [master word classes](#word-classes), each once, and `endings`
6252
+ states at least two endings, each once. Agreement is by these endings alone:
6253
+ there are no cases or declensions.
6254
+
6255
+ **Participles** attach to the verb root, which is the citation form less its
6256
+ infinitive ending:
6257
+
6258
+ ```yaml
6259
+ affixes:
6260
+ - { form: "-andar", gloss: "doing it", replaces: vowel }
6261
+ - { form: "-thos", gloss: "done" }
6262
+ grammar:
6263
+ participles:
6264
+ root: [athen, ethen]
6265
+ affixes: ["-andar", "-thos"]
6266
+ ```
6267
+
6268
+ A verb closing on one of `root` (the longest that leaves a root) drops it, and
6269
+ each suffix of `data.affixes` that `affixes` names by its `form` attaches to
6270
+ what is left as its own `replaces` says, or whole where it states none: the
6271
+ `front` form attaches too. So _dokathen_ gives the root _dok-_, which gives
6272
+ _dokandar_ and _dokthos_. A participle closing on an agreement ending agrees,
6273
+ so _dokthos_ also gives _doktha_, _dokthon_, _dokthoi_ and _dokthai_. A verb
6274
+ closing on no `root` ending has no participle. `root` names at least one
6275
+ nonempty ending and `affixes` at least one suffix the language's `data.affixes`
6276
+ states; an affix it names that is a prefix, or that the language does not
6277
+ state, is an error at the entry.
6278
+
6279
+ Neither rule is used by the dictionary. Both are read by the
6280
+ [mapped-run word check](#mapping-notes), which accepts the forms they give,
6281
+ and `package-build lexicon --derived` lists the same forms.
6282
+
6206
6283
  All grammar is optional. Only a supplied key's shape is checked: no language
6207
6284
  must supply a pattern, no noun must supply gender or plural, and no word is
6208
6285
  checked against a rule or required to provide an exception. There are no
@@ -6453,8 +6530,8 @@ note of the package: `{ tag: vylari, language: skill-vylarilng }` makes
6453
6530
  `exonym:vylari` read the Vylari note. That note's `data.register` must hold a
6454
6531
  row of the same name, case folded and NFC, whose `tongue` is the tongue itself,
6455
6532
  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`:
6533
+ links to a language note, or that entries link to two, is an error at the
6534
+ row's `tongue`; a missing name is an error at its `name`:
6458
6535
 
6459
6536
  > `` `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
6537
 
@@ -6604,7 +6681,12 @@ to take the affix.
6604
6681
 
6605
6682
  **An affix states `replaces`** where it takes the place of the stem's close —
6606
6683
  `vowel` for any closing vowel, or the letters, as `a` — and is added to the
6607
- whole stem where it states none. An affix states `lengthens: true` where the
6684
+ whole stem where it states none. An affix states `attachesTo` where it
6685
+ attaches to some word classes and not others, naming each by its base, as
6686
+ `attachesTo: [n, adj]`: a suffix stating `replaces` then derives forms of those
6687
+ classes' words alone, so `-andi` with `attachesTo: [n]` gives _zunelandi_ from
6688
+ the noun _zunel_ and nothing from a verb. Where it states none, the suffix
6689
+ attaches to a word of every class. An affix states `lengthens: true` where the
6608
6690
  stem's last vowel lengthens before it, and never beside `replaces`. A language
6609
6691
  that keeps vowel harmony gives an affix's form after front vowels in `front`.
6610
6692
 
@@ -6639,12 +6721,13 @@ Each finding is located at the key at fault:
6639
6721
  | A `scope` Address that does not resolve, or a type that is no note type | error |
6640
6722
  | A tongue `language` or a cognate form's language that is no language note | error |
6641
6723
  | 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 |
6724
+ | A register row tagged `other:<tongue>` or `exonym:<tongue>` whose tongue no `data.tongues` entry links to one language note, at the tongue | error |
6725
+ | 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 | error |
6644
6726
  | A sound-change set's name stated twice, at the second | error |
6645
6727
  | A step label stated twice in one set, at the second | error |
6646
6728
  | A set's `from` or `to` that is no language note, at the value | error |
6647
6729
  | A frame class that is no word class base, at the class | error |
6730
+ | An affix's `attachesTo` that names no class, at the key, or a class that is no word class base or one named twice, at the class | error |
6648
6731
  | An affix stating both `replaces` and `lengthens: true`, at `lengthens` | error |
6649
6732
  | A calling-name closing with both or neither of `form` and `doubled` | error |
6650
6733
 
@@ -467,7 +467,7 @@ These fields are accepted by every note type. A field's value can still be irrel
467
467
  | `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
468
468
  | `data.words` | list of `{ form, class, field, gloss, … }` | The words this language states, one map each, in authored order; its automatic dictionary sorts them alphabetically. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
469
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.grammar` | a map — `{ plural?, verbs? }` | Optional noun plurals and one verb pattern for dictionary presentation. Only the shape of supplied keys is checked; no word must follow or state a rule. A language skill note only. |
470
+ | `data.grammar` | a map — `{ plural?, verbs?, agreement?, participles? }` | Optional noun plurals, one verb pattern, agreement and participles, for dictionary presentation and the mapped-run word check. Only the shape of supplied keys is checked; no word must follow or state a rule. A language skill note only. |
471
471
  | `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. |
472
472
  | `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. |
473
473
  | `data.retired` | list of `{ form, instead?, why }` | The forms the setting does not write, and why. A language skill note only. |
@@ -519,6 +519,7 @@ These fields are accepted by every note type. A field's value can still be irrel
519
519
  | `data.affixes[].gloss` | a string | yes | What the affix means. |
520
520
  | `data.affixes[].attaches` | a string | no | How it attaches, in words, such as `replaces a closing vowel`. |
521
521
  | `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. |
522
+ | `data.affixes[].attachesTo` | a list of word classes, such as `[n, adj]` | no | The word classes it attaches to, by their base; every class where absent. A suffix stating `replaces` derives forms of these classes' words alone. |
522
523
  | `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. |
523
524
  | `data.affixes[].front` | the affix as written, such as `-ür` | no | Its form after front vowels, where the language keeps vowel harmony. |
524
525
  | `data.affixes[].examples` | a list of strings | no | The names the affix is read from. |
@@ -537,6 +538,12 @@ These fields are accepted by every note type. A field's value can still be irrel
537
538
  | `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
539
  | `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
540
  | `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. |
541
+ | `data.grammar.agreement` | a map — `{ classes, endings }` | no | How words of the named classes agree with a noun: a word closing on one of the `endings` also takes each other one, and each form takes the `plural` rule. Read by the mapped-run word check. |
542
+ | `data.grammar.agreement.classes` | a list of word class bases, such as `[adj, pron]` | yes | The word classes that agree, each a base of the master word classes. |
543
+ | `data.grammar.agreement.endings` | a list of at least two literal endings, such as `[os, a, on]` | yes | The singular endings an agreeing word swaps among, one for each gender; the `plural` rule gives each one's plural. A word closing on none does not agree. |
544
+ | `data.grammar.participles` | a map — `{ root, affixes }` | no | Participles on the verb root: a verb closing on a `root` ending drops it, and each named suffix attaches to what is left; a participle closing on an `agreement` ending agrees. Read by the mapped-run word check. |
545
+ | `data.grammar.participles.root` | a list of closing letters, such as `[athen, ethen]` | yes | The infinitive endings a verb's citation form drops to give its root, the longest that matches first. A verb closing on none has no participle. |
546
+ | `data.grammar.participles.affixes` | a list of suffixes of `data.affixes`, such as `[-andar, -thos]` | yes | The participle suffixes, each attaching to the root as its own `replaces` says: `vowel` or letters take the place of the root's close, and none adds it whole. |
540
547
  | `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. |
541
548
  | `data.tongues[].meaning` | a string | yes | What a name of this tongue is. |
542
549
  | `data.tongues[].language` | a language skill Address | no | The language note the tag names, where it is one. |
@@ -335,6 +335,57 @@ Caption classes are unrestricted. The built-in `border` class draws the block as
335
335
  | `.content-figure-label` | the label, number and caption together |
336
336
  | `.border` | the block as a boxed aside |
337
337
 
338
+ ## Verse
339
+
340
+ A verse fence reaches the page as a `.verse` block holding a paragraph per
341
+ stanza and a `.verse-line` per line, its indent level as `i1` to `i8`. Its
342
+ keys reach the block as written, `lang` as its language and the rest as
343
+ `data-*` attributes. A numbered verse also carries `.verse-numbered`, and each
344
+ numbered line starts with its `.verse-number`.
345
+
346
+ A [verse in columns](../authoring/links-and-markup.md#verse-in-columns) also
347
+ carries `.verse-columns`, and each verse line is one `.verse-row`:
348
+
349
+ ```html
350
+ <div class="verse verse-columns verse-numbered" lang="art-x-vyl" data-numstep="5">
351
+ <p>
352
+ <span class="verse-row"
353
+ ><span class="verse-number"></span
354
+ ><span class="verse-column"
355
+ ><span class="verse-part"
356
+ ><span class="script hl-semaron-kuriale" lang="art-x-vyl">…</span></span
357
+ ></span
358
+ ><span class="verse-column"
359
+ ><span class="verse-part"><span lang="en">The hallowed boundary I sing,</span></span
360
+ ><span class="verse-part verse-secondary"
361
+ ><span lang="art-x-vyl">Thiruthriman sorimamen,</span></span
362
+ ></span
363
+ ></span
364
+ >
365
+ …
366
+ </p>
367
+ </div>
368
+ ```
369
+
370
+ A numbered verse in columns gives every row a `.verse-number`, empty on the
371
+ lines it does not number, so the columns stay aligned. A part whose text runs
372
+ right to left carries `dir="rtl"`. Each row is its own grid, so a long line
373
+ keeps its partner level with it. Below a block width of `32rem` a row stacks
374
+ its columns in order; it is a container query, so a narrow journal window
375
+ stacks as well. The Foundry stylesheet draws the same.
376
+
377
+ | class | drawn as |
378
+ | ------------------ | ------------------------------------------------------------- |
379
+ | `.verse` | the poem |
380
+ | `.verse-line` | one line of a poem without columns |
381
+ | `.verse-numbered` | a numbered poem, with a margin for its numbers |
382
+ | `.verse-number` | a line's number, muted |
383
+ | `.verse-columns` | a poem in columns, and the container its rows are measured by |
384
+ | `.verse-row` | one verse line: its number and its columns |
385
+ | `.verse-column` | one column, its entries stacked |
386
+ | `.verse-part` | one entry's text, in the body serif unless its face sets it |
387
+ | `.verse-secondary` | every entry after a column's first, muted |
388
+
338
389
  ## Search
339
390
 
340
391
  `params.search = true` renders a search box in the header; left unset (or
@@ -156,7 +156,7 @@ go run check-sanitizer.go /path/to/gitea.ini < preview.html > sanitized.html
156
156
  ```
157
157
 
158
158
  Check both light and dark fixtures containing all four alert types, a custom
159
- title, nesting, wikilinks and attributes, SQL/pagelist fences, infoboxes, poetry
159
+ title, nesting, wikilinks and attributes, SQL/pagelist fences, infoboxes, verse
160
160
  and footnotes. All wrapper/token classes and icon shapes should remain, while
161
161
  injected scripts and event-handler attributes should disappear. This replay
162
162
  checks the sanitizer primitives; also inspect the result in the actual Gitea
package/e2e.mjs CHANGED
@@ -799,7 +799,7 @@ export function suiteExecutables(command) {
799
799
  }
800
800
 
801
801
  /** The Chrome for Testing build shared by local and CI Cypress runs. */
802
- export const CHROME_BUILD_ID = "155.0.8059.84";
802
+ export const CHROME_BUILD_ID = "158.0.8110.0";
803
803
 
804
804
  /**
805
805
  * The browser cache survives reseeding and container teardown.
@@ -37,7 +37,7 @@
37
37
 
38
38
  import { scanAlerts } from "./content-alerts.mjs";
39
39
  import { scanBlocks } from "./content-blocks.mjs";
40
- import { scanPoetry } from "./content-poetry.mjs";
40
+ import { scanVerse } from "./content-verse.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";
@@ -123,7 +123,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
123
123
  kind: "prose",
124
124
  });
125
125
  }
126
- for (const poem of scanPoetry(body).blocks) {
126
+ for (const poem of scanVerse(body).blocks) {
127
127
  if (poem.id)
128
128
  anchors.push({
129
129
  slug: slugify(poem.id),
@@ -270,6 +270,11 @@ export function assetsManifest(staged, stageDir) {
270
270
  * stage before a file is copied: a package whose provenance cannot be read is
271
271
  * not one whose shipping can be decided.
272
272
  *
273
+ * The pictures are written beside whatever a `packageBuild.assets` row already
274
+ * put under `icons/` and `images/`; nothing there is removed. A picture whose
275
+ * deployed path is already a file in the stage is a finding, and no picture is
276
+ * written. Clearing an earlier run's pictures is the stage reset's work.
277
+ *
273
278
  * @param {object} opts - Options.
274
279
  * @param {string} opts.assetsBase - The package's asset directory, the roots'
275
280
  * parent.
@@ -304,15 +309,24 @@ export function stagePackageAssets({
304
309
  if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
305
310
 
306
311
  const { staged, excluded } = selectStagedAssets(records, shippingLicenses);
307
- for (const { root } of DEPLOYED_ASSET_TYPES) {
308
- fs.rmSync(path.join(stageDir, root), { recursive: true, force: true });
312
+ const planned = staged.map((record) => ({
313
+ record,
314
+ from: path.join(assetsBase, ...record.asset.path.split("/")),
315
+ to: path.join(stageDir, .../** @type {string} */ (deployedAssetPath(record)).split("/")),
316
+ }));
317
+ for (const { record, from, to } of planned) {
318
+ if (!fs.existsSync(to)) continue;
319
+ findings.push({
320
+ file: to,
321
+ severity: "error",
322
+ message:
323
+ `\`${deployedAssetPath(record)}\` is already in the stage, copied by a ` +
324
+ `\`packageBuild.assets\` row; the picture \`${path.relative(process.cwd(), from)}\` ` +
325
+ "was not staged over it",
326
+ });
309
327
  }
310
- for (const record of staged) {
311
- const from = path.join(assetsBase, ...record.asset.path.split("/"));
312
- const to = path.join(
313
- stageDir,
314
- .../** @type {string} */ (deployedAssetPath(record)).split("/"),
315
- );
328
+ if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
329
+ for (const { from, to } of planned) {
316
330
  fs.mkdirSync(path.dirname(to), { recursive: true });
317
331
  const transformed = transform ? transform(from) : null;
318
332
  if (transformed != null) fs.writeFileSync(to, transformed);
@@ -104,11 +104,11 @@ function column(space) {
104
104
  * @param {object} [options]
105
105
  * @param {boolean} [options.spans=true] - Include inline code spans. Set false
106
106
  * to consider only block-level code.
107
- * @param {boolean} [options.poetryAsProse=true] - Treat verse bodies as inline prose.
107
+ * @param {boolean} [options.verseAsProse=true] - Treat verse bodies as inline prose.
108
108
  * @returns {Array<{start: number, end: number}>} Non-overlapping regions, in
109
109
  * source order. `end` is exclusive.
110
110
  */
111
- export function codeRegions(markdown, { spans = true, poetryAsProse = true } = {}) {
111
+ export function codeRegions(markdown, { spans = true, verseAsProse = true } = {}) {
112
112
  const src = String(markdown ?? "");
113
113
  if (!src) return [];
114
114
  const lines = src.split("\n");
@@ -145,7 +145,7 @@ export function codeRegions(markdown, { spans = true, poetryAsProse = true } = {
145
145
  let close = i + 1;
146
146
  while (close < lines.length && !closer.test(lines[close])) close++;
147
147
  // An unclosed fence runs to the end of the document.
148
- if (poetryAsProse && /^poetry(?:\s|$)/.test(fence[3])) {
148
+ if (verseAsProse && /^verse(?:\s|$)/.test(fence[3])) {
149
149
  regions.push(through(i, i));
150
150
  if (close < lines.length) regions.push(through(close, close));
151
151
  } else regions.push(through(i, Math.min(close, lines.length - 1)));
@@ -53,7 +53,7 @@ import crypto from "node:crypto";
53
53
  import { slugify } from "./content-slug.mjs";
54
54
  import MarkdownIt from "markdown-it";
55
55
  import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
56
- import { scanPoetry, renderPoetry } from "./content-poetry.mjs";
56
+ import { scanVerse, renderVerse } from "./content-verse.mjs";
57
57
  import { WITHHELD_CLASS, parseHeadingLine, withheldSections } from "./heading-attributes.mjs";
58
58
 
59
59
  /**
@@ -200,7 +200,7 @@ export function scanBlocks(source) {
200
200
  });
201
201
  }
202
202
  }
203
- return { blocks, errors: [...errors, ...scanPoetry(source).errors] };
203
+ return { blocks, errors: [...errors, ...scanVerse(source).errors] };
204
204
  }
205
205
 
206
206
  /**
@@ -252,7 +252,7 @@ function attributeText(block, target, classes) {
252
252
  */
253
253
  export function renderBlocks(source, target) {
254
254
  const { errors } = scanBlocks(source);
255
- source = renderPoetry(source).markdown;
255
+ source = renderVerse(source).markdown;
256
256
  const renderedBlocks = scanBlocks(source).blocks;
257
257
  if (!renderedBlocks.length) return { markdown: String(source ?? ""), errors };
258
258
  const lines = String(source ?? "").split("\n");
@@ -154,7 +154,7 @@ function picturesAreMap(contents, resolveRole) {
154
154
  * @returns {"code"|"table"|"figure"|"map"|"poem"|"prose"} The kind.
155
155
  */
156
156
  function figureKind(first, contents, resolveRole) {
157
- if (/^(?:<div\b[^>]*\bclass="poetry(?:\s|"))/.test(contents)) return "poem";
157
+ if (/^(?:<div\b[^>]*\bclass="verse(?:\s|"))/.test(contents)) return "poem";
158
158
  if (first?.type === "table_open") return "table";
159
159
  if (first?.type === "fence" && /^\s*sql\b/i.test(first.info ?? "")) return "table";
160
160
  if (first?.type === "fence" || first?.type === "code_block") return "code";
@@ -220,7 +220,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
220
220
  const explicit = attributes.values.type;
221
221
  if (
222
222
  explicit &&
223
- !["figure", "table", "poetry", "code", "prose", "example"].includes(explicit)
223
+ !["figure", "table", "verse", "code", "prose", "example"].includes(explicit)
224
224
  )
225
225
  fault(i, `unsupported caption type "${explicit}"`);
226
226
  if (!caption) fault(i, "a caption needs text");
@@ -281,7 +281,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
281
281
  ) {
282
282
  fault(
283
283
  bodyStart,
284
- "a caption must be followed by an image, table, poetry or code fence, paragraph, quote, or fenced div",
284
+ "a caption must be followed by an image, table, verse or code fence, paragraph, quote, or fenced div",
285
285
  );
286
286
  continue;
287
287
  }
@@ -306,11 +306,11 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
306
306
  continue;
307
307
  }
308
308
  kind =
309
- first.type === "fence" && /^poetry(?:\s|$)/.test(first.info) ?
309
+ first.type === "fence" && /^verse(?:\s|$)/.test(first.info) ?
310
310
  "poem"
311
311
  : figureKind(first, contents.trim(), resolveRole);
312
312
  }
313
- if (explicit) kind = explicit === "poetry" ? "poem" : explicit;
313
+ if (explicit) kind = explicit === "verse" ? "poem" : explicit;
314
314
  const id = attributes.id;
315
315
  if (id && ids.has(slugify(id))) fault(i, `duplicate figure id "${id}"`);
316
316
  if (id) ids.add(slugify(id));
@@ -1512,7 +1512,7 @@ function dynamicPublicationBody(body) {
1512
1512
  const source = String(body ?? "");
1513
1513
  return (
1514
1514
  [...matchAllOutsideCode(source, EXPRESSION)].length > 0 ||
1515
- codeRegions(source, { spans: false, poetryAsProse: false }).some(({ start, end }) => {
1515
+ codeRegions(source, { spans: false, verseAsProse: false }).some(({ start, end }) => {
1516
1516
  const opener = FENCE_LINE.exec(source.slice(start, end).split("\n", 1)[0]);
1517
1517
  const language = opener && parseHeaderArgs(opener[3]).language;
1518
1518
  return language === "sql" || language === PAGE_LIST_LANGUAGE;