@heroiclands/package-build 23.12.8 → 24.0.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 (258) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +3 -2
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +293 -24
  5. package/bin/content-commands.mjs +370 -213
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/bin/package-build.mjs +3 -2
  8. package/ci/check-banners.mjs +7 -7
  9. package/config.mjs +21 -6
  10. package/content-config.mjs +199 -80
  11. package/docs/api.md +110 -89
  12. package/docs/authoring/links-and-markup.md +469 -97
  13. package/docs/commands.md +259 -78
  14. package/docs/configuration.md +211 -103
  15. package/docs/content-format.md +4 -4
  16. package/docs/diagnostics.md +44 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/guides/icons.md +1 -1
  19. package/docs/guides/module-project.md +5 -8
  20. package/docs/guides/release-and-deploy.md +2 -2
  21. package/docs/guides/site.md +112 -23
  22. package/docs/guides/system-project.md +1 -1
  23. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  24. package/docs/index.md +1 -0
  25. package/docs/project-setup.md +28 -30
  26. package/docs/reference/format-details.md +677 -319
  27. package/docs/reference/note-types.md +47 -10
  28. package/docs/reference/theme-pages.md +178 -108
  29. package/docs/source-preview.md +16 -2
  30. package/engine/anchors.mjs +2 -2
  31. package/engine/asset-shipping.mjs +22 -10
  32. package/engine/base-compiler.mjs +62 -104
  33. package/engine/changelog-lint.mjs +1 -1
  34. package/engine/code-fences.mjs +3 -3
  35. package/engine/content-address.mjs +1 -1
  36. package/engine/content-alerts.mjs +8 -4
  37. package/engine/content-blocks.mjs +29 -14
  38. package/engine/content-embeds.mjs +1 -1
  39. package/engine/content-expand.mjs +376 -0
  40. package/engine/content-figures.mjs +18 -11
  41. package/engine/content-format.yaml +8 -0
  42. package/engine/content-glyphs.mjs +213 -0
  43. package/engine/content-images.mjs +6 -7
  44. package/engine/content-index.mjs +1 -1
  45. package/engine/content-links.mjs +3 -3
  46. package/engine/content-lint.mjs +1 -1
  47. package/engine/content-render.mjs +453 -0
  48. package/engine/content-scripts.mjs +376 -43
  49. package/engine/content-spans.mjs +6 -3
  50. package/engine/content-styles.mjs +91 -0
  51. package/engine/content-symbols.mjs +186 -0
  52. package/engine/content-verse.mjs +738 -0
  53. package/engine/core-report.mjs +169 -0
  54. package/engine/core-vocabulary.mjs +208 -0
  55. package/engine/dictionary-html.mjs +119 -0
  56. package/engine/dictionary-rows.mjs +15 -4
  57. package/engine/field-reference.mjs +5 -5
  58. package/engine/first-use.mjs +17 -27
  59. package/engine/frontmatter-lint.mjs +1 -1
  60. package/engine/heading-attributes.mjs +89 -0
  61. package/engine/helpers.mjs +61 -101
  62. package/engine/homepage.mjs +17 -29
  63. package/engine/html-minify.mjs +70 -0
  64. package/engine/index.mjs +8 -5
  65. package/engine/key-case-collisions.mjs +136 -0
  66. package/engine/language-data.mjs +203 -6
  67. package/engine/language-rows.mjs +8 -2
  68. package/engine/language-terms.mjs +186 -16
  69. package/engine/language-words.mjs +12 -0
  70. package/engine/logographic.mjs +255 -0
  71. package/engine/map-lexicon.mjs +262 -47
  72. package/engine/map-places.mjs +5 -4
  73. package/engine/markdown-expressions.mjs +0 -2
  74. package/engine/markdownlint-run.mjs +9 -3
  75. package/engine/markup-validation.mjs +21 -0
  76. package/engine/metre-declaration.mjs +134 -0
  77. package/engine/names.mjs +36 -9
  78. package/engine/note-matter.mjs +66 -0
  79. package/engine/note-vocabulary.mjs +187 -3
  80. package/engine/pdf-build.mjs +69 -133
  81. package/engine/pdf-render.mjs +205 -58
  82. package/engine/post-fields.mjs +157 -0
  83. package/engine/preview-index.mjs +130 -0
  84. package/engine/preview-source.mjs +450 -0
  85. package/engine/project-init.mjs +5 -12
  86. package/engine/prose-config.mjs +3 -3
  87. package/engine/read-stdin.mjs +29 -0
  88. package/engine/run-rows.mjs +2 -2
  89. package/engine/run-words.mjs +6 -112
  90. package/engine/script-emphasis-terms.mjs +38 -0
  91. package/engine/script-emphasis.mjs +295 -0
  92. package/engine/script-font-delivery.mjs +4 -4
  93. package/engine/site-build.mjs +260 -315
  94. package/engine/site-config.mjs +26 -238
  95. package/engine/site-files.mjs +175 -0
  96. package/engine/site-index.mjs +9 -4
  97. package/engine/site-preview.mjs +52 -525
  98. package/engine/site-render.mjs +593 -0
  99. package/engine/site-root.mjs +22 -13
  100. package/engine/site-search.mjs +161 -0
  101. package/engine/site-serve.mjs +273 -0
  102. package/engine/site-theme-banner.mjs +154 -0
  103. package/engine/site-theme-urls.mjs +125 -0
  104. package/engine/site-theme.mjs +926 -0
  105. package/engine/sql-tables.mjs +20 -3
  106. package/engine/text-maps.mjs +57 -10
  107. package/engine/typographer.mjs +248 -0
  108. package/engine/verse-metre.mjs +357 -0
  109. package/engine/wikilink-syntax.mjs +29 -47
  110. package/engine/wikilinks.mjs +758 -279
  111. package/engine/word-forms.mjs +364 -0
  112. package/engine/word-rows.mjs +12 -2
  113. package/engine/word-terms.mjs +38 -4
  114. package/manifest.mjs +3 -4
  115. package/package.json +11 -8
  116. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  117. package/site-theme/layouts/base.hbs +31 -0
  118. package/site-theme/layouts/blog-list.hbs +30 -0
  119. package/site-theme/layouts/blog-post.hbs +40 -0
  120. package/site-theme/layouts/home.hbs +35 -0
  121. package/site-theme/layouts/list.hbs +20 -0
  122. package/site-theme/layouts/not-found.hbs +31 -0
  123. package/site-theme/layouts/page.hbs +7 -0
  124. package/site-theme/partials/copy-buttons.hbs +37 -0
  125. package/site-theme/partials/draft-notice.hbs +5 -0
  126. package/site-theme/partials/exturl.hbs +2 -0
  127. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  128. package/site-theme/partials/header.hbs +44 -0
  129. package/site-theme/partials/hero-banner.hbs +11 -0
  130. package/site-theme/partials/infobox-entry.hbs +2 -0
  131. package/site-theme/partials/infobox-section.hbs +31 -0
  132. package/site-theme/partials/infobox-value.hbs +2 -0
  133. package/site-theme/partials/infobox.hbs +18 -0
  134. package/site-theme/partials/list-members.hbs +9 -0
  135. package/site-theme/partials/nav-links.hbs +5 -0
  136. package/site-theme/partials/page-body.hbs +48 -0
  137. package/site-theme/partials/page-list.hbs +14 -0
  138. package/site-theme/partials/related.hbs +24 -0
  139. package/site-theme/partials/search-meta.hbs +6 -0
  140. package/site-theme/partials/search-names.hbs +2 -0
  141. package/site-theme/partials/search.hbs +52 -0
  142. package/site-theme/partials/tags.hbs +4 -0
  143. package/site-theme/partials/toc.hbs +7 -0
  144. package/{hugo-theme → site-theme}/static/css/style.css +106 -14
  145. package/stage.mjs +1 -2
  146. package/types/config.d.mts +8 -4
  147. package/types/content-config.d.mts +15 -15
  148. package/types/engine/asset-shipping.d.mts +5 -2
  149. package/types/engine/base-compiler.d.mts +20 -2
  150. package/types/engine/code-fences.d.mts +3 -3
  151. package/types/engine/content-address.d.mts +1 -1
  152. package/types/engine/content-alerts.d.mts +8 -2
  153. package/types/engine/content-blocks.d.mts +11 -4
  154. package/types/engine/content-expand.d.mts +288 -0
  155. package/types/engine/content-figures.d.mts +11 -3
  156. package/types/engine/content-glyphs.d.mts +85 -0
  157. package/types/engine/content-images.d.mts +6 -7
  158. package/types/engine/content-links.d.mts +1 -1
  159. package/types/engine/content-render.d.mts +199 -0
  160. package/types/engine/content-scripts.d.mts +70 -4
  161. package/types/engine/content-spans.d.mts +1 -1
  162. package/types/engine/content-styles.d.mts +33 -0
  163. package/types/engine/content-symbols.d.mts +35 -0
  164. package/types/engine/content-verse.d.mts +240 -0
  165. package/types/engine/core-report.d.mts +45 -0
  166. package/types/engine/core-vocabulary.d.mts +53 -0
  167. package/types/engine/dictionary-html.d.mts +8 -0
  168. package/types/engine/dictionary-rows.d.mts +10 -0
  169. package/types/engine/field-reference.d.mts +2 -2
  170. package/types/engine/heading-attributes.d.mts +20 -0
  171. package/types/engine/helpers.d.mts +3 -10
  172. package/types/engine/homepage.d.mts +11 -24
  173. package/types/engine/html-minify.d.mts +9 -0
  174. package/types/engine/index.d.mts +2 -1
  175. package/types/engine/key-case-collisions.d.mts +28 -0
  176. package/types/engine/language-data.d.mts +0 -2
  177. package/types/engine/language-terms.d.mts +24 -2
  178. package/types/engine/logographic.d.mts +45 -0
  179. package/types/engine/map-lexicon.d.mts +66 -13
  180. package/types/engine/map-places.d.mts +8 -0
  181. package/types/engine/markdown-expressions.d.mts +0 -2
  182. package/types/engine/metre-declaration.d.mts +39 -0
  183. package/types/engine/names.d.mts +34 -3
  184. package/types/engine/note-matter.d.mts +48 -0
  185. package/types/engine/pdf-render.d.mts +6 -2
  186. package/types/engine/post-fields.d.mts +44 -0
  187. package/types/engine/preview-index.d.mts +72 -0
  188. package/types/engine/preview-source.d.mts +51 -0
  189. package/types/engine/read-stdin.d.mts +11 -0
  190. package/types/engine/run-rows.d.mts +2 -2
  191. package/types/engine/run-words.d.mts +0 -15
  192. package/types/engine/script-emphasis-terms.d.mts +21 -0
  193. package/types/engine/script-emphasis.d.mts +84 -0
  194. package/types/engine/script-font-delivery.d.mts +4 -4
  195. package/types/engine/site-build.d.mts +71 -113
  196. package/types/engine/site-config.d.mts +14 -134
  197. package/types/engine/site-files.d.mts +73 -0
  198. package/types/engine/site-index.d.mts +11 -3
  199. package/types/engine/site-preview.d.mts +15 -19
  200. package/types/engine/site-render.d.mts +171 -0
  201. package/types/engine/site-root.d.mts +12 -4
  202. package/types/engine/site-search.d.mts +54 -0
  203. package/types/engine/site-serve.d.mts +44 -0
  204. package/types/engine/site-theme-banner.d.mts +51 -0
  205. package/types/engine/site-theme-urls.d.mts +60 -0
  206. package/types/engine/site-theme.d.mts +127 -0
  207. package/types/engine/sql-tables.d.mts +4 -1
  208. package/types/engine/text-maps.d.mts +17 -3
  209. package/types/engine/typographer.d.mts +11 -0
  210. package/types/engine/verse-metre.d.mts +131 -0
  211. package/types/engine/wikilink-syntax.d.mts +30 -48
  212. package/types/engine/wikilinks.d.mts +330 -14
  213. package/types/engine/word-forms.d.mts +83 -0
  214. package/types/manifest.d.mts +3 -4
  215. package/types/stage.d.mts +1 -2
  216. package/types/vendor/markdownlint.d.mts +8 -0
  217. package/vendor/markdownlint.mjs +20388 -0
  218. package/engine/content-poetry.mjs +0 -240
  219. package/engine/web-wikilinks.mjs +0 -594
  220. package/hugo-theme/layouts/404.html +0 -85
  221. package/hugo-theme/layouts/_default/baseof.html +0 -67
  222. package/hugo-theme/layouts/_default/list.html +0 -139
  223. package/hugo-theme/layouts/_default/single.html +0 -4
  224. package/hugo-theme/layouts/blog/list.html +0 -54
  225. package/hugo-theme/layouts/blog-post/single.html +0 -36
  226. package/hugo-theme/layouts/index.html +0 -75
  227. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  228. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  229. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  230. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  231. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  232. package/hugo-theme/layouts/partials/header.html +0 -35
  233. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  234. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  235. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  236. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  237. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  238. package/hugo-theme/layouts/partials/infobox.html +0 -57
  239. package/hugo-theme/layouts/partials/page-body.html +0 -152
  240. package/hugo-theme/layouts/partials/related.html +0 -138
  241. package/hugo-theme/layouts/partials/search.html +0 -84
  242. package/hugo-theme/layouts/partials/site-url.html +0 -32
  243. package/hugo-theme/layouts/partials/tags.html +0 -30
  244. package/hugo-theme/layouts/partials/toc.html +0 -28
  245. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  246. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  247. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  248. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  249. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  250. package/hugo-theme/theme.toml +0 -11
  251. package/types/engine/content-poetry.d.mts +0 -58
  252. package/types/engine/web-wikilinks.d.mts +0 -107
  253. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  254. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  255. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  256. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  257. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  258. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -13,18 +13,18 @@
13
13
 
14
14
  /**
15
15
  * **Text marked with a language, a font and a mapping.** Three keys on the
16
- * attribute grammar spans, divs and poetry fences already share, each optional
16
+ * attribute grammar spans, divs and verse fences already share, each optional
17
17
  * and independent of the others:
18
18
  *
19
19
  * ```markdown
20
20
  * [Daimela, helthos.]{lang=vyl}
21
- * [Zaron tholmeza]{lang=vyl font=semaron-kuriale map=vyl-semaron}
21
+ * [Zaron tholmeza]{lang=vyl font=semaron-kuriale map=semaron}
22
22
  *
23
- * ::: {lang=vyl font=semaron-kuriale map=vyl-semaron}
23
+ * ::: {lang=vyl font=semaron-kuriale map=semaron}
24
24
  * Zaron tholmeza.
25
25
  * :::
26
26
  *
27
- * ```poetry {form=epic lang=vyl font=semaron-kuriale map=vyl-semaron}
27
+ * ```verse {form=epic lang=vyl font=semaron-kuriale map=semaron}
28
28
  * ```
29
29
  *
30
30
  * - **`lang=`** gives the language tag and changes nothing else. It is a name
@@ -39,17 +39,25 @@
39
39
  * - **`lang=` with `map=` reads the language's lexicon first**: a word whose
40
40
  * entry states a native form for the map is written by it, and the rows
41
41
  * spell every other word; see {@link module:engine/map-lexicon}.
42
+ * - **A logographic map writes every word from the lexicon**: by the word's
43
+ * own writing or its parts', and a word with neither, or whose form stands
44
+ * for entries written differently, is a finding at the word, once per run;
45
+ * see {@link module:engine/logographic}.
42
46
  * - **Every word of a run with `lang=` and `map=` is a word of its language**
43
47
  * where the language's `note` names its language note: a lexicon form, a
44
48
  * word of a name its own register lists, or a regular form of a lexicon
45
49
  * form. Any other
46
50
  * word is a finding at its first character, once per run; see
47
51
  * {@link module:engine/run-words}.
48
- * - **A run inside a div or a poetry fence inherits its keys**, and its own
52
+ * - **A run inside a div or a verse fence inherits its keys**, and its own
49
53
  * keys override them.
50
54
  * - **Text with a font, a map or a declared language is plain.** A span or a
51
55
  * verse holds no emphasis, link, image, footnote, wikilink, code or HTML,
52
56
  * and no span but a marked one; a div holds paragraphs and line breaks only.
57
+ * - **Text set in a face takes `*…*` and `**…**`**, set by the convention its
58
+ * language's note states, else its face's script note, else plainly; see
59
+ * {@link module:engine/script-emphasis}. A language that marks emphasis
60
+ * with a word makes either a finding at the emphasis.
53
61
  * The text is read as written, so `[`, `]`, `"`, `<<`, `>>`, `!` and
54
62
  * `@#$%^&` are characters, and need no escaping. A real language's tag alone
55
63
  * marks text and leaves its Markdown alone.
@@ -59,7 +67,7 @@
59
67
  *
60
68
  * {@link scanRuns} reads the markup through {@link module:engine/content-spans.scanSpans},
61
69
  * {@link module:engine/content-blocks.scanBlocks} and
62
- * {@link module:engine/content-poetry.scanPoetry}, so what counts as a span, a
70
+ * {@link module:engine/content-verse.scanVerse}, so what counts as a span, a
63
71
  * div or a fence is theirs to say.
64
72
  *
65
73
  * @module
@@ -68,18 +76,30 @@
68
76
  import MarkdownIt from "markdown-it";
69
77
 
70
78
  import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
71
- import { scanPoetry } from "./content-poetry.mjs";
79
+ import {
80
+ COLUMN_KEYS,
81
+ COLUMNS_CLASS,
82
+ expandVerse,
83
+ expandVerseOrigins,
84
+ scanVerse,
85
+ sourcePosition,
86
+ } from "./content-verse.mjs";
72
87
  import { scanSpans } from "./content-spans.mjs";
73
88
  import { isLanguageTag } from "./language-tag.mjs";
74
89
  import { distance, nearest } from "./near-miss.mjs";
90
+ import { emphasesFromRecords, languageEmphasesOf } from "./script-emphasis.mjs";
91
+ import { EMPHASIS_CLASSES } from "./script-emphasis-terms.mjs";
75
92
  import { FONTS_PACKAGE, resolveScriptFonts } from "./script-fonts.mjs";
76
93
  import {
77
94
  fontProblem,
95
+ languageRecordsByName,
78
96
  lexiconsFromRecords,
79
97
  mappingFor,
80
98
  uncapitalisedMessage,
99
+ unwrittenMessage,
81
100
  } from "./map-lexicon.mjs";
82
101
  import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
102
+ import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
83
103
  import { applyMap, mapsFromRecords } from "./text-maps.mjs";
84
104
 
85
105
  /** The keys this module reads, which no construct passes through as written. */
@@ -113,15 +133,26 @@ const CONFIG = "package-build.config.yaml";
113
133
  * @property {ReadonlyMap<string, import("./run-words.mjs").WordBook>} [wordBooks] -
114
134
  * What each configured language's mapped runs are checked against, from
115
135
  * {@link module:engine/run-words.wordBooksFromRecords}.
136
+ * @property {ReadonlyMap<string, import("./metre-declaration.mjs").Metre>} [metres] -
137
+ * The metres the `doc` notes declare, by name.
138
+ * @property {ReadonlyMap<string, import("./verse-metre.mjs").MetreBook>} [metreBooks] -
139
+ * What each configured language's verse is counted by, from
140
+ * {@link module:engine/verse-metre.metreBooksFromRecords}.
141
+ * @property {ReadonlyMap<string, import("./script-emphasis.mjs").Emphasis>} [emphases] -
142
+ * Each face's emphasis conventions, from the script notes, by
143
+ * {@link module:engine/script-emphasis.emphasesFromRecords}.
144
+ * @property {ReadonlyMap<string, import("./script-emphasis.mjs").LanguageEmphasis>} [languageEmphases] -
145
+ * The convention each configured language's note states, by language name,
146
+ * from {@link module:engine/script-emphasis.languageEmphasesOf}.
116
147
  */
117
148
 
118
149
  /**
119
- * One span, div or poetry fence carrying `lang=`, `font=` or `map=`. Its
150
+ * One span, div or verse fence carrying `lang=`, `font=` or `map=`. Its
120
151
  * `lang`, `font` and `map` are the ones it is set with: its own, else the
121
152
  * innermost div or fence around it that states one.
122
153
  *
123
154
  * @typedef {object} Run
124
- * @property {"span"|"block"|"poetry"} kind - Which construct.
155
+ * @property {"span"|"block"|"verse"} kind - Which construct.
125
156
  * @property {string|undefined} lang - The language as written.
126
157
  * @property {string|undefined} tag - The BCP 47 tag it resolves to; `undefined`
127
158
  * when it resolves to none.
@@ -181,6 +212,10 @@ export function runContext(config, records) {
181
212
  maps: mapsFromRecords(records),
182
213
  lexicons: lexiconsFromRecords(records, config),
183
214
  wordBooks: wordBooksFromRecords(records, config),
215
+ metres: metresFromRecords(records),
216
+ metreBooks: metreBooksFromRecords(records, config),
217
+ emphases: emphasesFromRecords(records),
218
+ languageEmphases: languageEmphasesOf(languageRecordsByName(records, config)),
184
219
  });
185
220
  byRecords.set(records, context);
186
221
  return context;
@@ -205,7 +240,7 @@ function withinOneEdit(a, b) {
205
240
  }
206
241
 
207
242
  /** The word each construct is called in a finding. */
208
- const CONSTRUCT = Object.freeze({ span: "span", block: "div", poetry: "poetry fence" });
243
+ const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse fence" });
209
244
 
210
245
  /** Line starts of a source, for offsets to positions. */
211
246
  function lineStarts(text) {
@@ -298,9 +333,11 @@ function closing(text, open) {
298
333
  * @param {ReadonlyMap<number, number>} [holes] - Marked spans inside the
299
334
  * text, each its offset of `[` to the offset past its attributes. They are
300
335
  * runs of their own and checked as such.
336
+ * @param {boolean} [stars] - Whether `*…*`, `_…_`, `**…**` and `__…__` are
337
+ * emphasis the text may hold, as in a run in a script face; `~~` is not.
301
338
  * @returns {Array<{offset: number, what: string}>} Each fault, in order.
302
339
  */
303
- function inlineMarkup(text, footnotes, holes = new Map()) {
340
+ function inlineMarkup(text, footnotes, holes = new Map(), stars = false) {
304
341
  const faults = [];
305
342
  const skip = new Map();
306
343
  const runs = [];
@@ -384,6 +421,10 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
384
421
  if (char === "*" || char === "_" || (char === "~" && text[i + 1] === "~")) {
385
422
  let j = i;
386
423
  while (text[j] === char) j++;
424
+ if (stars && char !== "~") {
425
+ i = j - 1;
426
+ continue;
427
+ }
387
428
  const before = text[i - 1] ?? " ",
388
429
  after = text[j] ?? " ";
389
430
  const left =
@@ -401,7 +442,7 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
401
442
  i = j - 1;
402
443
  }
403
444
  }
404
- let outside = text;
445
+ let outside = stars ? text.replace(/[*_]/g, " ") : text;
405
446
  for (const [from, to] of holes)
406
447
  outside = outside.slice(0, from) + " ".repeat(to - from) + outside.slice(to);
407
448
  if (runs.length && holdsEmphasis(outside)) {
@@ -422,6 +463,91 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
422
463
  return faults.sort((a, b) => a.offset - b.offset);
423
464
  }
424
465
 
466
+ /** A parser that reads emphasis and nothing else in text whose other punctuation is escaped. */
467
+ const starParser = new MarkdownIt({ html: false, linkify: false, typographer: false });
468
+
469
+ /** ASCII punctuation, which a backslash escapes. */
470
+ const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
471
+
472
+ /**
473
+ * One `*`, `_`, `**` or `__` that Markdown reads as opening or closing emphasis.
474
+ *
475
+ * @typedef {object} Delimiter
476
+ * @property {number} offset - Its first character's offset.
477
+ * @property {number} length - 1 for `*` or `_`, 2 for `**` or `__`.
478
+ * @property {"em"|"strong"} tag - Which level it opens or closes.
479
+ * @property {boolean} open - Whether it opens.
480
+ */
481
+
482
+ /**
483
+ * The stars and underscores Markdown reads as emphasis in a run's text, as
484
+ * CommonMark pairs them. Every other character is read as itself: an escaped
485
+ * delimiter, or one that pairs with none, is a character of the text. A marked span inside the text
486
+ * reads as one letter, so emphasis may enclose it.
487
+ *
488
+ * @param {string} text - Inline text, as written.
489
+ * @param {ReadonlyMap<number, number>} [holes] - Marked spans inside it, by
490
+ * offset in `text`.
491
+ * @returns {Delimiter[]} Each delimiter, in order, at its offset in `text`.
492
+ */
493
+ function emphasisDelimiters(text, holes = new Map()) {
494
+ let masked = "";
495
+ const stars = [];
496
+ for (let i = 0; i < text.length; i++) {
497
+ if (holes.has(i)) {
498
+ masked += "x";
499
+ i = /** @type {number} */ (holes.get(i)) - 1;
500
+ continue;
501
+ }
502
+ const char = text[i];
503
+ if (char === "\\" && ASCII_PUNCTUATION.test(text[i + 1] ?? "")) {
504
+ masked += text[i + 1] === "*" || text[i + 1] === "_" ? "\\!" : `\\${text[i + 1]}`;
505
+ i++;
506
+ } else if (char === "*" || char === "_") {
507
+ masked += char;
508
+ stars.push(i);
509
+ } else masked += ASCII_PUNCTUATION.test(char) ? `\\${char}` : char;
510
+ }
511
+ if (!stars.length) return [];
512
+ const delimiters = [];
513
+ let at = 0;
514
+ for (const token of starParser.parseInline(masked, {})[0]?.children ?? []) {
515
+ if (token.type === "text") at += (token.content.match(/[*_]/g) ?? []).length;
516
+ else if (/^(?:em|strong)_(?:open|close)$/.test(token.type)) {
517
+ delimiters.push({
518
+ offset: stars[at],
519
+ length: token.markup.length,
520
+ tag: /** @type {"em"|"strong"} */ (token.tag),
521
+ open: token.nesting === 1,
522
+ });
523
+ at += token.markup.length;
524
+ }
525
+ }
526
+ return delimiters;
527
+ }
528
+
529
+ /**
530
+ * The parts of ranges of text that no delimiter covers.
531
+ *
532
+ * @param {Array<[number, number]>} parts - The ranges, as offsets.
533
+ * @param {readonly Delimiter[]} delimiters - Delimiters, by absolute offset.
534
+ * @returns {Array<[number, number]>} The ranges, split around each delimiter.
535
+ */
536
+ function outsideDelimiters(parts, delimiters) {
537
+ if (!delimiters.length) return parts;
538
+ const out = [];
539
+ for (const [from, to] of parts) {
540
+ let at = from;
541
+ for (const delimiter of delimiters) {
542
+ if (delimiter.offset < at || delimiter.offset >= to) continue;
543
+ if (delimiter.offset > at) out.push([at, delimiter.offset]);
544
+ at = delimiter.offset + delimiter.length;
545
+ }
546
+ if (at < to) out.push([at, to]);
547
+ }
548
+ return out;
549
+ }
550
+
425
551
  /** A line split into its indentation, its text, a trailing break and its trailing space. */
426
552
  const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
427
553
 
@@ -474,7 +600,7 @@ function unescapeRoman(core) {
474
600
  * @param {import("./map-lexicon.mjs").Mapping|undefined} mapping - The run's
475
601
  * map, face and lexicon.
476
602
  * @param {boolean} lineStart - Whether `raw` starts its line.
477
- * @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string}>}}
603
+ * @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string, unwritten?: object}>}}
478
604
  * The set text, and each piece of it with the offset in `raw` it came from.
479
605
  */
480
606
  function setText(raw, mapping, lineStart) {
@@ -491,6 +617,7 @@ function setText(raw, mapping, lineStart) {
491
617
  const mapped = applyMap(mapping?.map, plain.text, {
492
618
  font: mapping?.font,
493
619
  native: mapping?.native,
620
+ writing: mapping?.writing,
494
621
  });
495
622
  for (const piece of mapped.pieces)
496
623
  pieces.push({
@@ -502,6 +629,7 @@ function setText(raw, mapping, lineStart) {
502
629
  ...(piece.uncapitalised === undefined ?
503
630
  {}
504
631
  : { uncapitalised: piece.uncapitalised }),
632
+ ...(piece.unwritten === undefined ? {} : { unwritten: piece.unwritten }),
505
633
  });
506
634
  out.push(
507
635
  lead +
@@ -574,7 +702,40 @@ const BLOCK_WHAT = Object.freeze({
574
702
  * @property {[number, number]|undefined} body - A div's or fence's body, as
575
703
  * offsets: the first body line's start to the end of the last.
576
704
  * @property {number|undefined} contentEnd - A span's offset of its closing `]`.
705
+ * @property {import("./script-emphasis.mjs").LanguageEmphasis|undefined} languageEmphasis -
706
+ * The convention its language's note states.
707
+ * @property {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
708
+ * What its emphasis is set by: its language's convention, else its face's
709
+ * script's; `undefined` sets it plain.
710
+ * @property {Delimiter[]} emphasis - The emphasis it holds, by absolute offset.
711
+ */
712
+
713
+ /**
714
+ * The finding for emphasis in a language that marks it with a word.
715
+ *
716
+ * @param {string} lang - The language, as `lang=` names it.
717
+ * @returns {string} The message.
718
+ */
719
+ function wordsMessage(lang) {
720
+ return `\`${lang}\` marks emphasis with a word, not a mark; write the word`;
721
+ }
722
+
723
+ /**
724
+ * What a run's emphasis is set by: the convention its language's note states,
725
+ * else the one its face's script note states, else none. A language that
726
+ * marks emphasis with a word sets none, and its emphasis is a finding.
727
+ *
728
+ * @param {{face: import("./script-fonts.mjs").Face|undefined, languageEmphasis: string|undefined}} detail -
729
+ * The run.
730
+ * @param {RunContext} ctx - The conventions.
731
+ * @returns {import("./script-emphasis.mjs").Emphasis|undefined} The conventions.
577
732
  */
733
+ function conventionsOf({ face, languageEmphasis }, ctx) {
734
+ if (!face || languageEmphasis === "words") return undefined;
735
+ if (languageEmphasis === "rubric" || languageEmphasis === "overline")
736
+ return { em: languageEmphasis, strong: languageEmphasis };
737
+ return ctx.emphases?.get(face.slug);
738
+ }
578
739
 
579
740
  /**
580
741
  * Read every marked construct in a source, and every fault in it.
@@ -645,7 +806,7 @@ function analyseRuns(source, ctx, syntax) {
645
806
  for (const key of Object.keys(keys))
646
807
  report(
647
808
  at(key, "key"),
648
- `\`${key}=\` is read on a span, a div or a poetry fence, not on a ${block.name} block`,
809
+ `\`${key}=\` is read on a span, a div or a verse fence, not on a ${block.name} block`,
649
810
  );
650
811
  } else {
651
812
  divs.set(first, block);
@@ -679,13 +840,13 @@ function analyseRuns(source, ctx, syntax) {
679
840
  collect(0, text);
680
841
 
681
842
  const poems = new Map();
682
- for (const poem of scanPoetry(text).blocks) {
843
+ for (const poem of scanVerse(text).blocks) {
683
844
  poems.set(poem.start, poem);
684
845
  const keys = runKeys(poem.attributes);
685
846
  if (!Object.keys(keys).length) continue;
686
847
  const opening = lines[poem.start];
687
848
  constructs.push({
688
- kind: "poetry",
849
+ kind: "verse",
689
850
  keys,
690
851
  node: poem,
691
852
  line0: poem.start,
@@ -790,25 +951,44 @@ function analyseRuns(source, ctx, syntax) {
790
951
  );
791
952
 
792
953
  /**
793
- * Report the markup in inline text that must be plain.
954
+ * Report the markup in inline text that must be plain, and collect the
955
+ * emphasis a run in a script face holds.
794
956
  *
795
957
  * @param {string} inline - The text.
796
958
  * @param {number} offset - Its offset in the source.
797
959
  * @param {string} marking - How its construct is marked.
960
+ * @param {RunDetail} detail - The run, whose `emphasis` collects each
961
+ * delimiter by absolute offset.
798
962
  * @param {ReadonlyMap<number, number>} [holes] - Marked spans in it, by
799
963
  * absolute offset.
800
964
  */
801
- const plainInline = (inline, offset, marking, holes = new Map()) => {
965
+ const plainInline = (inline, offset, marking, detail, holes = new Map()) => {
802
966
  const relative = new Map(
803
967
  [...holes]
804
968
  .filter(([start]) => start >= offset && start < offset + inline.length)
805
969
  .map(([start, end]) => [start - offset, end - offset]),
806
970
  );
807
- for (const fault of inlineMarkup(inline, footnotes, relative))
971
+ const { lang } = detail.run;
972
+ const words = detail.languageEmphasis === "words";
973
+ const stars = words || detail.face !== undefined;
974
+ for (const fault of inlineMarkup(inline, footnotes, relative, stars))
808
975
  report(
809
976
  positionOf(starts, offset + fault.offset),
810
- `text marked \`${marking}\` is plain text and holds no ${fault.what}`,
977
+ fault.what !== "emphasis" || !stars ?
978
+ `text marked \`${marking}\` is plain text and holds no ${fault.what}`
979
+ : words ? wordsMessage(/** @type {string} */ (lang))
980
+ : `text marked \`${marking}\` takes emphasis only as \`*…*\`, \`_…_\`, \`**…**\` or \`__…__\``,
811
981
  );
982
+ if (!stars) return;
983
+ for (const delimiter of emphasisDelimiters(inline, relative)) {
984
+ if (words) {
985
+ if (delimiter.open)
986
+ report(
987
+ positionOf(starts, offset + delimiter.offset),
988
+ wordsMessage(/** @type {string} */ (lang)),
989
+ );
990
+ } else detail.emphasis.push({ ...delimiter, offset: offset + delimiter.offset });
991
+ }
812
992
  };
813
993
 
814
994
  /**
@@ -825,6 +1005,8 @@ function analyseRuns(source, ctx, syntax) {
825
1005
  for (const [from, to] of parts) {
826
1006
  const lineStart = from === 0 || text[from - 1] === "\n";
827
1007
  for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
1008
+ // A word with no writing is its own finding, not a face's.
1009
+ if (piece.unwritten) continue;
828
1010
  for (const char of piece.to) {
829
1011
  const point = /** @type {number} */ (char.codePointAt(0));
830
1012
  if (/\s/u.test(char) || face.draws.has(point)) continue;
@@ -884,6 +1066,35 @@ function analyseRuns(source, ctx, syntax) {
884
1066
  }
885
1067
  };
886
1068
 
1069
+ /**
1070
+ * Report each word a logographic map gives no writing, once per run, at
1071
+ * the word.
1072
+ *
1073
+ * @param {Array<[number, number]>} parts - The text to check, as offsets.
1074
+ * @param {RunDetail} detail - The run.
1075
+ */
1076
+ const writings = (parts, detail) => {
1077
+ const { mapping } = detail;
1078
+ if (!mapping?.map.logographic) return;
1079
+ const said = new Set();
1080
+ for (const [from, to] of parts) {
1081
+ const lineStart = from === 0 || text[from - 1] === "\n";
1082
+ for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
1083
+ if (piece.unwritten === undefined || said.has(piece.from)) continue;
1084
+ said.add(piece.from);
1085
+ report(
1086
+ positionOf(starts, from + piece.offset),
1087
+ unwrittenMessage(
1088
+ piece.from,
1089
+ piece.unwritten,
1090
+ detail.run.lang,
1091
+ mapping.map.name,
1092
+ ),
1093
+ );
1094
+ }
1095
+ }
1096
+ };
1097
+
887
1098
  /**
888
1099
  * Report each word of a mapped run that is not a word of its language, once
889
1100
  * per run, at its first character.
@@ -914,7 +1125,11 @@ function analyseRuns(source, ctx, syntax) {
914
1125
  const runs = [];
915
1126
  const details = [];
916
1127
  for (const construct of constructs) {
1128
+ const unchecked = errors.length;
917
1129
  checkOwn(construct);
1130
+ // A key that names nothing is the finding; the text it would have set
1131
+ // is not then checked against a face or a language it cannot reach.
1132
+ const named = errors.length === unchecked;
918
1133
  const keys = Object.assign({}, ...around(construct).map((c) => c.keys), construct.keys);
919
1134
  const { lang, font, map } = keys;
920
1135
  // A face the run's map is not for, once the face and the map are each
@@ -959,7 +1174,11 @@ function analyseRuns(source, ctx, syntax) {
959
1174
  holes: new Map(),
960
1175
  body: undefined,
961
1176
  contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
1177
+ languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
1178
+ conventions: undefined,
1179
+ emphasis: [],
962
1180
  };
1181
+ detail.conventions = conventionsOf(detail, ctx);
963
1182
  details.push(detail);
964
1183
  if (!plain) continue;
965
1184
  const marking = [
@@ -972,11 +1191,16 @@ function analyseRuns(source, ctx, syntax) {
972
1191
  if (construct.kind === "span") {
973
1192
  const span = construct.node;
974
1193
  const inline = text.slice(span.start + 1, span.contentEnd);
975
- plainInline(inline, span.start + 1, marking);
976
- if (errors.length === before) {
977
- lexical([[span.start + 1, span.contentEnd]], detail);
978
- capitals([[span.start + 1, span.contentEnd]], detail);
979
- covered([[span.start + 1, span.contentEnd]], detail);
1194
+ plainInline(inline, span.start + 1, marking, detail);
1195
+ if (named && errors.length === before) {
1196
+ const parts = outsideDelimiters(
1197
+ [[span.start + 1, span.contentEnd]],
1198
+ detail.emphasis,
1199
+ );
1200
+ lexical(parts, detail);
1201
+ capitals(parts, detail);
1202
+ writings(parts, detail);
1203
+ covered(parts, detail);
980
1204
  }
981
1205
  continue;
982
1206
  }
@@ -1010,6 +1234,7 @@ function analyseRuns(source, ctx, syntax) {
1010
1234
  body.slice(top, past).join("\n"),
1011
1235
  starts[first + top],
1012
1236
  marking,
1237
+ detail,
1013
1238
  detail.holes,
1014
1239
  );
1015
1240
  continue;
@@ -1017,20 +1242,22 @@ function analyseRuns(source, ctx, syntax) {
1017
1242
  if (token.type === "fence")
1018
1243
  said(
1019
1244
  top,
1020
- token.info.trim().split(/\s/)[0] === "poetry" ?
1021
- "a poetry fence"
1245
+ token.info.trim().split(/\s/)[0] === "verse" ?
1246
+ "a verse fence"
1022
1247
  : "a code fence",
1023
1248
  );
1024
1249
  else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
1025
1250
  }
1026
1251
  } else {
1027
1252
  for (let line = first; line < last; line++)
1028
- plainInline(lines[line], starts[line], marking, detail.holes);
1253
+ plainInline(lines[line], starts[line], marking, detail, detail.holes);
1029
1254
  }
1030
- if (errors.length === before) {
1031
- lexical(outsideHoles(from, to, detail.holes), detail);
1032
- capitals(outsideHoles(from, to, detail.holes), detail);
1033
- covered(outsideHoles(from, to, detail.holes), detail);
1255
+ if (named && errors.length === before) {
1256
+ const parts = outsideDelimiters(outsideHoles(from, to, detail.holes), detail.emphasis);
1257
+ lexical(parts, detail);
1258
+ capitals(parts, detail);
1259
+ writings(parts, detail);
1260
+ covered(parts, detail);
1034
1261
  }
1035
1262
  }
1036
1263
 
@@ -1043,7 +1270,7 @@ function analyseRuns(source, ctx, syntax) {
1043
1270
  }
1044
1271
 
1045
1272
  /**
1046
- * Find the spans, divs and poetry fences marked with `lang=`, `font=` or
1273
+ * Find the spans, divs and verse fences marked with `lang=`, `font=` or
1047
1274
  * `map=`, and every fault in their markup and their text.
1048
1275
  *
1049
1276
  * @param {string} source - Authored Markdown: a note body, or a whole file
@@ -1059,8 +1286,50 @@ function analyseRuns(source, ctx, syntax) {
1059
1286
  * declared name.
1060
1287
  */
1061
1288
  export function scanRuns(source, ctx = {}, syntax) {
1062
- const { runs, errors, warnings } = analyseRuns(source, ctx, syntax);
1063
- return { runs, errors, warnings };
1289
+ const text = String(source ?? "");
1290
+ const { markdown, origins } = expandVerseOrigins(text);
1291
+ const authored = analyseRuns(text, ctx, syntax);
1292
+ const metre = metreFindings(scanVerse(text).blocks, text.split("\n"), {
1293
+ metres: ctx.metres,
1294
+ metreBooks: ctx.metreBooks,
1295
+ lexicons: ctx.lexicons,
1296
+ columnsClass: COLUMNS_CLASS,
1297
+ columnKeys: COLUMN_KEYS,
1298
+ attributeColumns,
1299
+ });
1300
+ /** @param {object[]} found @param {object[]} extra */
1301
+ const merged = (found, extra) =>
1302
+ extra.length ?
1303
+ [...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column)
1304
+ : found;
1305
+ if (markdown === text)
1306
+ return {
1307
+ ...authored,
1308
+ errors: merged(authored.errors, metre.errors),
1309
+ warnings: merged(authored.warnings, metre.warnings),
1310
+ };
1311
+ // A verse in columns is checked as the spans each entry sets, which are
1312
+ // written only in the expansion; every finding is placed back where the
1313
+ // note wrote the text or the entry it comes from, once.
1314
+ const expanded = markdown.split("\n");
1315
+ const checked = analyseRuns(markdown, ctx);
1316
+ const placed = (findings) => {
1317
+ const seen = new Set();
1318
+ return findings
1319
+ .map((finding) => ({ ...finding, ...sourcePosition(finding, expanded, origins) }))
1320
+ .filter((finding) => {
1321
+ const key = `${finding.line}:${finding.column}:${finding.message}`;
1322
+ if (seen.has(key)) return false;
1323
+ seen.add(key);
1324
+ return true;
1325
+ })
1326
+ .sort((a, b) => a.line - b.line || a.column - b.column);
1327
+ };
1328
+ return {
1329
+ runs: authored.runs,
1330
+ errors: merged(placed(checked.errors), metre.errors),
1331
+ warnings: merged(placed(checked.warnings), metre.warnings),
1332
+ };
1064
1333
  }
1065
1334
 
1066
1335
  /**
@@ -1113,7 +1382,7 @@ function rewrittenAttributes(detail, search) {
1113
1382
  const changes =
1114
1383
  run.font !== undefined ||
1115
1384
  run.map !== undefined ||
1116
- (run.tag !== undefined && (keys.lang !== run.tag || run.kind === "poetry"));
1385
+ (run.tag !== undefined && (keys.lang !== run.tag || run.kind === "verse"));
1117
1386
  if (!changes) return undefined;
1118
1387
  const classes = [...own.classes];
1119
1388
  const values = [...own.values];
@@ -1128,12 +1397,70 @@ function rewrittenAttributes(detail, search) {
1128
1397
  return attributeText({ lang: run.tag, id: own.id, classes, values });
1129
1398
  }
1130
1399
 
1400
+ /**
1401
+ * The tag a delimiter is written as: an `<em>` or a `<strong>` carrying its
1402
+ * convention's class. The book's parser reads only the classed form, so a
1403
+ * delimiter with no convention writes nothing there, and the text is set in
1404
+ * the face's Regular.
1405
+ *
1406
+ * @param {Delimiter} delimiter - The delimiter.
1407
+ * @param {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
1408
+ * The run's conventions.
1409
+ * @param {boolean} book - Whether the target is the book.
1410
+ * @returns {string} The tag.
1411
+ */
1412
+ function emphasisTag(delimiter, conventions, book) {
1413
+ const convention = conventions?.[delimiter.tag];
1414
+ if (!convention && book) return "";
1415
+ if (!delimiter.open) return `</${delimiter.tag}>`;
1416
+ return convention ?
1417
+ `<${delimiter.tag} class="${EMPHASIS_CLASSES[convention]}">`
1418
+ : `<${delimiter.tag}>`;
1419
+ }
1420
+
1421
+ /**
1422
+ * Set a range of a run's text, each piece between its emphasis delimiters by
1423
+ * {@link setText}, and each delimiter as its tag.
1424
+ *
1425
+ * @param {string} text - The source.
1426
+ * @param {number} from - The range's first offset.
1427
+ * @param {number} to - The offset past it.
1428
+ * @param {RunDetail} detail - The run.
1429
+ * @param {boolean} lineStart - Whether `from` starts its line.
1430
+ * @param {boolean} book - Whether the target is the book.
1431
+ * @returns {string} The set text.
1432
+ */
1433
+ function setRange(text, from, to, detail, lineStart, book) {
1434
+ let out = "";
1435
+ let at = from;
1436
+ for (const delimiter of detail.emphasis) {
1437
+ if (delimiter.offset < from || delimiter.offset >= to) continue;
1438
+ if (delimiter.offset > at)
1439
+ out += setText(
1440
+ text.slice(at, delimiter.offset),
1441
+ detail.mapping,
1442
+ at === from && lineStart,
1443
+ ).text;
1444
+ out += emphasisTag(delimiter, detail.conventions, book);
1445
+ at = delimiter.offset + delimiter.length;
1446
+ }
1447
+ if (at < to) out += setText(text.slice(at, to), detail.mapping, at === from && lineStart).text;
1448
+ return out;
1449
+ }
1450
+
1131
1451
  /**
1132
1452
  * Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
1133
- * div and poetry grammar the rest of each chain renders, one line for one line.
1453
+ * div and verse grammar the rest of each chain renders, one line for one line.
1134
1454
  *
1455
+ * - **A verse in columns is expanded first** into one span per column entry
1456
+ * per verse line, by {@link module:engine/content-verse.expandVerse}, so
1457
+ * each entry is mapped and set as a span with its keys is. That is the one
1458
+ * rewrite that changes the number of lines.
1135
1459
  * - **The map, if any, is applied first.** Characters Markdown would read are
1136
1460
  * then backslash-escaped, so they reach every later pass as characters.
1461
+ * - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
1462
+ * convention's class, or no class where none is stated. The book reads
1463
+ * only the classed form, so a classless one writes nothing there.
1137
1464
  * - **The construct carries** the language's tag as `lang`, the face's class
1138
1465
  * (with `script`, and `script-block` on a div) or the generic family's
1139
1466
  * class. On the site, a face's construct also carries
@@ -1156,7 +1483,10 @@ function rewrittenAttributes(detail, search) {
1156
1483
  */
1157
1484
  export function renderRuns(source, ctx = {}, syntax, options = {}) {
1158
1485
  const search = options.target === "site";
1159
- const text = String(source ?? "");
1486
+ const book = options.target === "book";
1487
+ const authored = String(source ?? "");
1488
+ const text = expandVerse(authored);
1489
+ if (text !== authored) syntax = undefined;
1160
1490
  if (!/\b(?:lang|font|map)=/.test(text)) return { markdown: text, errors: [] };
1161
1491
  const { details, errors } = analyseRuns(text, ctx, syntax);
1162
1492
  if (!details.length) return { markdown: text, errors };
@@ -1185,11 +1515,14 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
1185
1515
  const contentEnd = /** @type {number} */ (detail.contentEnd);
1186
1516
  let inner = text.slice(run.start + 1, contentEnd);
1187
1517
  if (detail.plain)
1188
- inner = setText(
1189
- inner,
1190
- detail.mapping,
1518
+ inner = setRange(
1519
+ text,
1520
+ run.start + 1,
1521
+ contentEnd,
1522
+ detail,
1191
1523
  run.start === 0 || text[run.start - 1] === "\n",
1192
- ).text;
1524
+ book,
1525
+ );
1193
1526
  else if (attributes === undefined) continue;
1194
1527
  const braces = attributes ?? text.slice(contentEnd + 2, run.end - 1);
1195
1528
  edits.push({ from: run.start, to: run.end, text: `[${inner}]{${braces}}` });
@@ -1207,7 +1540,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
1207
1540
  if (detail.plain && detail.body)
1208
1541
  for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
1209
1542
  const lineStart = from === 0 || text[from - 1] === "\n";
1210
- const set = setText(text.slice(from, to), detail.mapping, lineStart).text;
1543
+ const set = setRange(text, from, to, detail, lineStart, book);
1211
1544
  if (set !== text.slice(from, to)) edits.push({ from, to, text: set });
1212
1545
  }
1213
1546
  }