@heroiclands/package-build 23.13.0 → 24.0.1

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 (236) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -1
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +150 -1
  5. package/bin/content-commands.mjs +356 -210
  6. package/bin/package-build.mjs +3 -2
  7. package/ci/check-banners.mjs +7 -7
  8. package/config.mjs +21 -6
  9. package/content-config.mjs +199 -80
  10. package/docs/api.md +93 -72
  11. package/docs/authoring/links-and-markup.md +316 -78
  12. package/docs/commands.md +248 -80
  13. package/docs/configuration.md +210 -102
  14. package/docs/content-format.md +2 -2
  15. package/docs/diagnostics.md +2 -2
  16. package/docs/getting-started.md +1 -1
  17. package/docs/guides/icons.md +1 -1
  18. package/docs/guides/module-project.md +5 -8
  19. package/docs/guides/release-and-deploy.md +2 -2
  20. package/docs/guides/site.md +155 -23
  21. package/docs/guides/system-project.md +1 -1
  22. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  23. package/docs/index.md +1 -0
  24. package/docs/project-setup.md +28 -30
  25. package/docs/reference/format-details.md +589 -308
  26. package/docs/reference/note-types.md +40 -10
  27. package/docs/reference/theme-pages.md +127 -108
  28. package/docs/source-preview.md +15 -1
  29. package/engine/asset-shipping.mjs +22 -10
  30. package/engine/base-compiler.mjs +62 -104
  31. package/engine/changelog-lint.mjs +1 -1
  32. package/engine/content-address.mjs +1 -1
  33. package/engine/content-alerts.mjs +8 -4
  34. package/engine/content-blocks.mjs +26 -11
  35. package/engine/content-embeds.mjs +1 -1
  36. package/engine/content-expand.mjs +376 -0
  37. package/engine/content-figures.mjs +13 -6
  38. package/engine/content-format.yaml +8 -0
  39. package/engine/content-glyphs.mjs +213 -0
  40. package/engine/content-images.mjs +6 -7
  41. package/engine/content-links.mjs +3 -3
  42. package/engine/content-lint.mjs +1 -1
  43. package/engine/content-render.mjs +453 -0
  44. package/engine/content-scripts.mjs +326 -27
  45. package/engine/content-spans.mjs +4 -1
  46. package/engine/content-styles.mjs +91 -0
  47. package/engine/content-symbols.mjs +186 -0
  48. package/engine/content-verse.mjs +6 -18
  49. package/engine/core-report.mjs +169 -0
  50. package/engine/core-vocabulary.mjs +208 -0
  51. package/engine/dictionary-html.mjs +119 -0
  52. package/engine/field-reference.mjs +1 -1
  53. package/engine/frontmatter-lint.mjs +1 -1
  54. package/engine/heading-attributes.mjs +89 -0
  55. package/engine/helpers.mjs +61 -101
  56. package/engine/homepage.mjs +17 -29
  57. package/engine/html-minify.mjs +70 -0
  58. package/engine/index.mjs +8 -5
  59. package/engine/language-rows.mjs +8 -2
  60. package/engine/language-terms.mjs +115 -14
  61. package/engine/language-words.mjs +12 -0
  62. package/engine/logographic.mjs +255 -0
  63. package/engine/map-lexicon.mjs +221 -25
  64. package/engine/map-places.mjs +5 -4
  65. package/engine/markdown-expressions.mjs +0 -2
  66. package/engine/markdownlint-run.mjs +9 -3
  67. package/engine/markup-validation.mjs +21 -0
  68. package/engine/metre-declaration.mjs +134 -0
  69. package/engine/note-matter.mjs +2 -2
  70. package/engine/note-vocabulary.mjs +187 -3
  71. package/engine/pdf-build.mjs +68 -133
  72. package/engine/pdf-render.mjs +102 -44
  73. package/engine/post-fields.mjs +157 -0
  74. package/engine/preview-index.mjs +130 -0
  75. package/engine/preview-source.mjs +450 -0
  76. package/engine/project-init.mjs +5 -12
  77. package/engine/prose-config.mjs +3 -3
  78. package/engine/run-words.mjs +3 -2
  79. package/engine/script-emphasis-terms.mjs +38 -0
  80. package/engine/script-emphasis.mjs +295 -0
  81. package/engine/script-font-delivery.mjs +4 -4
  82. package/engine/site-build.mjs +265 -313
  83. package/engine/site-config.mjs +26 -238
  84. package/engine/site-files.mjs +175 -0
  85. package/engine/site-index.mjs +9 -4
  86. package/engine/site-preview.mjs +50 -523
  87. package/engine/site-reach.mjs +213 -0
  88. package/engine/site-render.mjs +668 -0
  89. package/engine/site-root.mjs +90 -39
  90. package/engine/site-search.mjs +161 -0
  91. package/engine/site-serve.mjs +274 -0
  92. package/engine/site-theme-banner.mjs +154 -0
  93. package/engine/site-theme-urls.mjs +125 -0
  94. package/engine/site-theme.mjs +939 -0
  95. package/engine/sql-tables.mjs +9 -6
  96. package/engine/text-maps.mjs +57 -10
  97. package/engine/typographer.mjs +248 -0
  98. package/engine/verse-metre.mjs +357 -0
  99. package/engine/wikilink-syntax.mjs +29 -47
  100. package/engine/wikilinks.mjs +758 -279
  101. package/engine/word-forms.mjs +39 -24
  102. package/engine/word-rows.mjs +12 -2
  103. package/engine/word-terms.mjs +29 -4
  104. package/manifest.mjs +3 -4
  105. package/package.json +10 -7
  106. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  107. package/site-theme/layouts/base.hbs +31 -0
  108. package/site-theme/layouts/blog-list.hbs +30 -0
  109. package/site-theme/layouts/blog-post.hbs +40 -0
  110. package/site-theme/layouts/home.hbs +35 -0
  111. package/site-theme/layouts/list.hbs +20 -0
  112. package/site-theme/layouts/not-found.hbs +31 -0
  113. package/site-theme/layouts/page.hbs +7 -0
  114. package/site-theme/partials/copy-buttons.hbs +37 -0
  115. package/site-theme/partials/draft-notice.hbs +5 -0
  116. package/site-theme/partials/exturl.hbs +2 -0
  117. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  118. package/site-theme/partials/header.hbs +44 -0
  119. package/site-theme/partials/hero-banner.hbs +11 -0
  120. package/site-theme/partials/infobox-entry.hbs +2 -0
  121. package/site-theme/partials/infobox-section.hbs +31 -0
  122. package/site-theme/partials/infobox-value.hbs +2 -0
  123. package/site-theme/partials/infobox.hbs +18 -0
  124. package/site-theme/partials/list-members.hbs +9 -0
  125. package/site-theme/partials/nav-links.hbs +5 -0
  126. package/site-theme/partials/page-body.hbs +48 -0
  127. package/site-theme/partials/page-list.hbs +14 -0
  128. package/site-theme/partials/related.hbs +24 -0
  129. package/site-theme/partials/search-meta.hbs +6 -0
  130. package/site-theme/partials/search-names.hbs +2 -0
  131. package/site-theme/partials/search.hbs +52 -0
  132. package/site-theme/partials/tags.hbs +4 -0
  133. package/site-theme/partials/toc.hbs +7 -0
  134. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  135. package/stage.mjs +1 -2
  136. package/types/config.d.mts +8 -4
  137. package/types/content-config.d.mts +15 -15
  138. package/types/engine/asset-shipping.d.mts +5 -2
  139. package/types/engine/base-compiler.d.mts +20 -2
  140. package/types/engine/content-address.d.mts +1 -1
  141. package/types/engine/content-alerts.d.mts +8 -2
  142. package/types/engine/content-blocks.d.mts +11 -4
  143. package/types/engine/content-expand.d.mts +288 -0
  144. package/types/engine/content-figures.d.mts +11 -3
  145. package/types/engine/content-glyphs.d.mts +85 -0
  146. package/types/engine/content-images.d.mts +6 -7
  147. package/types/engine/content-links.d.mts +1 -1
  148. package/types/engine/content-render.d.mts +199 -0
  149. package/types/engine/content-scripts.d.mts +62 -0
  150. package/types/engine/content-spans.d.mts +1 -1
  151. package/types/engine/content-styles.d.mts +33 -0
  152. package/types/engine/content-symbols.d.mts +35 -0
  153. package/types/engine/core-report.d.mts +45 -0
  154. package/types/engine/core-vocabulary.d.mts +53 -0
  155. package/types/engine/dictionary-html.d.mts +8 -0
  156. package/types/engine/field-reference.d.mts +1 -1
  157. package/types/engine/heading-attributes.d.mts +20 -0
  158. package/types/engine/helpers.d.mts +3 -10
  159. package/types/engine/homepage.d.mts +11 -24
  160. package/types/engine/html-minify.d.mts +9 -0
  161. package/types/engine/index.d.mts +2 -1
  162. package/types/engine/language-terms.d.mts +24 -2
  163. package/types/engine/logographic.d.mts +45 -0
  164. package/types/engine/map-lexicon.d.mts +48 -8
  165. package/types/engine/map-places.d.mts +8 -0
  166. package/types/engine/markdown-expressions.d.mts +0 -2
  167. package/types/engine/metre-declaration.d.mts +39 -0
  168. package/types/engine/pdf-render.d.mts +6 -2
  169. package/types/engine/post-fields.d.mts +44 -0
  170. package/types/engine/preview-index.d.mts +72 -0
  171. package/types/engine/preview-source.d.mts +51 -0
  172. package/types/engine/script-emphasis-terms.d.mts +21 -0
  173. package/types/engine/script-emphasis.d.mts +84 -0
  174. package/types/engine/script-font-delivery.d.mts +4 -4
  175. package/types/engine/site-build.d.mts +71 -113
  176. package/types/engine/site-config.d.mts +14 -134
  177. package/types/engine/site-files.d.mts +73 -0
  178. package/types/engine/site-index.d.mts +11 -3
  179. package/types/engine/site-preview.d.mts +15 -19
  180. package/types/engine/site-reach.d.mts +76 -0
  181. package/types/engine/site-render.d.mts +173 -0
  182. package/types/engine/site-root.d.mts +14 -4
  183. package/types/engine/site-search.d.mts +54 -0
  184. package/types/engine/site-serve.d.mts +44 -0
  185. package/types/engine/site-theme-banner.d.mts +51 -0
  186. package/types/engine/site-theme-urls.d.mts +60 -0
  187. package/types/engine/site-theme.d.mts +127 -0
  188. package/types/engine/text-maps.d.mts +17 -3
  189. package/types/engine/typographer.d.mts +11 -0
  190. package/types/engine/verse-metre.d.mts +131 -0
  191. package/types/engine/wikilink-syntax.d.mts +30 -48
  192. package/types/engine/wikilinks.d.mts +330 -14
  193. package/types/engine/word-forms.d.mts +3 -2
  194. package/types/manifest.d.mts +3 -4
  195. package/types/stage.d.mts +1 -2
  196. package/types/vendor/markdownlint.d.mts +8 -0
  197. package/vendor/markdownlint.mjs +20388 -0
  198. package/engine/web-wikilinks.mjs +0 -594
  199. package/hugo-theme/layouts/404.html +0 -85
  200. package/hugo-theme/layouts/_default/baseof.html +0 -67
  201. package/hugo-theme/layouts/_default/list.html +0 -139
  202. package/hugo-theme/layouts/_default/single.html +0 -4
  203. package/hugo-theme/layouts/blog/list.html +0 -54
  204. package/hugo-theme/layouts/blog-post/single.html +0 -36
  205. package/hugo-theme/layouts/index.html +0 -75
  206. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  207. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  208. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  209. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  210. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  211. package/hugo-theme/layouts/partials/header.html +0 -35
  212. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  213. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  214. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  215. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  216. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  217. package/hugo-theme/layouts/partials/infobox.html +0 -57
  218. package/hugo-theme/layouts/partials/page-body.html +0 -152
  219. package/hugo-theme/layouts/partials/related.html +0 -138
  220. package/hugo-theme/layouts/partials/search.html +0 -84
  221. package/hugo-theme/layouts/partials/site-url.html +0 -32
  222. package/hugo-theme/layouts/partials/tags.html +0 -30
  223. package/hugo-theme/layouts/partials/toc.html +0 -28
  224. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  225. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  226. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  227. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  228. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  229. package/hugo-theme/theme.toml +0 -11
  230. package/types/engine/web-wikilinks.d.mts +0 -107
  231. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  232. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  233. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  234. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  235. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  236. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -18,13 +18,13 @@
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
- * ```verse {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,6 +39,10 @@
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
@@ -50,6 +54,10 @@
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.
@@ -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 { expandVerse, expandVerseOrigins, scanVerse, sourcePosition } from "./content-verse.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,6 +133,17 @@ 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
  /**
@@ -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;
@@ -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.
@@ -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.
@@ -963,7 +1174,11 @@ function analyseRuns(source, ctx, syntax) {
963
1174
  holes: new Map(),
964
1175
  body: undefined,
965
1176
  contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
1177
+ languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
1178
+ conventions: undefined,
1179
+ emphasis: [],
966
1180
  };
1181
+ detail.conventions = conventionsOf(detail, ctx);
967
1182
  details.push(detail);
968
1183
  if (!plain) continue;
969
1184
  const marking = [
@@ -976,11 +1191,16 @@ function analyseRuns(source, ctx, syntax) {
976
1191
  if (construct.kind === "span") {
977
1192
  const span = construct.node;
978
1193
  const inline = text.slice(span.start + 1, span.contentEnd);
979
- plainInline(inline, span.start + 1, marking);
1194
+ plainInline(inline, span.start + 1, marking, detail);
980
1195
  if (named && errors.length === before) {
981
- lexical([[span.start + 1, span.contentEnd]], detail);
982
- capitals([[span.start + 1, span.contentEnd]], detail);
983
- covered([[span.start + 1, span.contentEnd]], detail);
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);
984
1204
  }
985
1205
  continue;
986
1206
  }
@@ -1014,6 +1234,7 @@ function analyseRuns(source, ctx, syntax) {
1014
1234
  body.slice(top, past).join("\n"),
1015
1235
  starts[first + top],
1016
1236
  marking,
1237
+ detail,
1017
1238
  detail.holes,
1018
1239
  );
1019
1240
  continue;
@@ -1029,12 +1250,14 @@ function analyseRuns(source, ctx, syntax) {
1029
1250
  }
1030
1251
  } else {
1031
1252
  for (let line = first; line < last; line++)
1032
- plainInline(lines[line], starts[line], marking, detail.holes);
1253
+ plainInline(lines[line], starts[line], marking, detail, detail.holes);
1033
1254
  }
1034
1255
  if (named && errors.length === before) {
1035
- lexical(outsideHoles(from, to, detail.holes), detail);
1036
- capitals(outsideHoles(from, to, detail.holes), detail);
1037
- covered(outsideHoles(from, to, detail.holes), detail);
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);
1038
1261
  }
1039
1262
  }
1040
1263
 
@@ -1066,7 +1289,25 @@ export function scanRuns(source, ctx = {}, syntax) {
1066
1289
  const text = String(source ?? "");
1067
1290
  const { markdown, origins } = expandVerseOrigins(text);
1068
1291
  const authored = analyseRuns(text, ctx, syntax);
1069
- if (markdown === text) return authored;
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
+ };
1070
1311
  // A verse in columns is checked as the spans each entry sets, which are
1071
1312
  // written only in the expansion; every finding is placed back where the
1072
1313
  // note wrote the text or the entry it comes from, once.
@@ -1086,8 +1327,8 @@ export function scanRuns(source, ctx = {}, syntax) {
1086
1327
  };
1087
1328
  return {
1088
1329
  runs: authored.runs,
1089
- errors: placed(checked.errors),
1090
- warnings: placed(checked.warnings),
1330
+ errors: merged(placed(checked.errors), metre.errors),
1331
+ warnings: merged(placed(checked.warnings), metre.warnings),
1091
1332
  };
1092
1333
  }
1093
1334
 
@@ -1156,6 +1397,57 @@ function rewrittenAttributes(detail, search) {
1156
1397
  return attributeText({ lang: run.tag, id: own.id, classes, values });
1157
1398
  }
1158
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
+
1159
1451
  /**
1160
1452
  * Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
1161
1453
  * div and verse grammar the rest of each chain renders, one line for one line.
@@ -1166,6 +1458,9 @@ function rewrittenAttributes(detail, search) {
1166
1458
  * rewrite that changes the number of lines.
1167
1459
  * - **The map, if any, is applied first.** Characters Markdown would read are
1168
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.
1169
1464
  * - **The construct carries** the language's tag as `lang`, the face's class
1170
1465
  * (with `script`, and `script-block` on a div) or the generic family's
1171
1466
  * class. On the site, a face's construct also carries
@@ -1188,6 +1483,7 @@ function rewrittenAttributes(detail, search) {
1188
1483
  */
1189
1484
  export function renderRuns(source, ctx = {}, syntax, options = {}) {
1190
1485
  const search = options.target === "site";
1486
+ const book = options.target === "book";
1191
1487
  const authored = String(source ?? "");
1192
1488
  const text = expandVerse(authored);
1193
1489
  if (text !== authored) syntax = undefined;
@@ -1219,11 +1515,14 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
1219
1515
  const contentEnd = /** @type {number} */ (detail.contentEnd);
1220
1516
  let inner = text.slice(run.start + 1, contentEnd);
1221
1517
  if (detail.plain)
1222
- inner = setText(
1223
- inner,
1224
- detail.mapping,
1518
+ inner = setRange(
1519
+ text,
1520
+ run.start + 1,
1521
+ contentEnd,
1522
+ detail,
1225
1523
  run.start === 0 || text[run.start - 1] === "\n",
1226
- ).text;
1524
+ book,
1525
+ );
1227
1526
  else if (attributes === undefined) continue;
1228
1527
  const braces = attributes ?? text.slice(contentEnd + 2, run.end - 1);
1229
1528
  edits.push({ from: run.start, to: run.end, text: `[${inner}]{${braces}}` });
@@ -1241,7 +1540,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
1241
1540
  if (detail.plain && detail.body)
1242
1541
  for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
1243
1542
  const lineStart = from === 0 || text[from - 1] === "\n";
1244
- const set = setText(text.slice(from, to), detail.mapping, lineStart).text;
1543
+ const set = setRange(text, from, to, detail, lineStart, book);
1245
1544
  if (set !== text.slice(from, to)) edits.push({ from, to, text: set });
1246
1545
  }
1247
1546
  }
@@ -18,6 +18,7 @@ import { scanVerse } from "./content-verse.mjs";
18
18
  import footnotePlugin from "markdown-it-footnote";
19
19
  import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
20
20
  import { inlineText } from "./name-markup.mjs";
21
+ import { cartoucheProblems, glyphProblems } from "./content-glyphs.mjs";
21
22
 
22
23
  /** Add bracket spans after recognized Markdown links and footnotes.
23
24
  * @param {import("markdown-it")} md - Markdown parser to extend.
@@ -91,6 +92,8 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}
91
92
  }
92
93
  }
93
94
  const problems = [...parsed.problems, ...refusedAttributes(parsed.values)];
95
+ if (parsed.classes.includes("glyph")) problems.push(...glyphProblems(parsed));
96
+ else if (parsed.classes.includes("cartouche")) problems.push(...cartoucheProblems(parsed));
94
97
  if (silent) {
95
98
  state.pos = finish;
96
99
  return true;
@@ -283,7 +286,7 @@ export function scanSpans(source) {
283
286
  };
284
287
  }
285
288
 
286
- /** Emit span tags while leaving their inline content as Markdown for Hugo.
289
+ /** Emit span tags while leaving their inline content as Markdown.
287
290
  * @param {string} source - Authored Markdown.
288
291
  */
289
292
  export function renderSpans(source) {