@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
@@ -37,6 +37,9 @@
37
37
  * over one with none.
38
38
  * - **Derived forms take the rows**: a plural or a verb ending of an exception
39
39
  * word is spelled by the rows, and only the word as cited is looked up.
40
+ * - **A logographic map** spells no word by its rows: every word is written
41
+ * from the lexicon, by its own writing or its parts' (see
42
+ * {@link module:engine/logographic}), and a word with neither is a finding.
40
43
  *
41
44
  * {@link mappingFor} is the one path from a map, a face and a language to the
42
45
  * function that rewrites text: the build's runs, `package-build transliterate`
@@ -51,9 +54,12 @@ import { parseAddress, renderAddress } from "./address.mjs";
51
54
  import { positionOfFrontmatterPath, positionOfYamlPath } from "./diagnostics.mjs";
52
55
  import { isNoteRecord } from "./index-records.mjs";
53
56
  import { nearest } from "./near-miss.mjs";
57
+ import { WRITTEN_ENDINGS } from "./language-terms.mjs";
58
+ import { writingsOf } from "./logographic.mjs";
54
59
  import { FONTS_PACKAGE } from "./script-fonts.mjs";
55
60
  import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
56
61
  import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
62
+ import { derivedForms } from "./word-forms.mjs";
57
63
  import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
58
64
 
59
65
  /** The configuration file a language is declared in. */
@@ -103,6 +109,11 @@ function said(names) {
103
109
  * @property {readonly LexiconWord[]} words - Its words, in `data.words` order.
104
110
  * @property {ReadonlyMap<string, readonly NativeForm[]>} byForm - The native
105
111
  * forms by the word's form, NFC; the first word of a form keeps it.
112
+ * @property {ReadonlyArray<{form: string, from: LexiconWord}>} derived - The
113
+ * forms the language's rules derive from its words, folded, as the mapped-run
114
+ * word check accepts them (see {@link module:engine/word-forms.derivedForms}).
115
+ * @property {Record<string, any>} data - The language note's `data`, which a
116
+ * logographic map writes words from.
106
117
  */
107
118
 
108
119
  /**
@@ -134,21 +145,31 @@ function readNatives(value) {
134
145
  function lexiconOf(language, data) {
135
146
  const words = [];
136
147
  const byForm = new Map();
148
+ const bySource = new Map();
137
149
  for (const word of listOf(data?.words)) {
138
150
  if (!isMap(word) || typeof word.form !== "string") continue;
139
151
  const natives = readNatives(word.native);
140
- words.push(
141
- Object.freeze({
142
- form: word.form,
143
- class: typeof word.class === "string" ? word.class : null,
144
- gloss: glossText(word.gloss),
145
- natives,
146
- }),
147
- );
152
+ const read = Object.freeze({
153
+ form: word.form,
154
+ class: typeof word.class === "string" ? word.class : null,
155
+ gloss: glossText(word.gloss),
156
+ natives,
157
+ });
158
+ words.push(read);
159
+ bySource.set(word, read);
148
160
  const key = word.form.normalize("NFC");
149
161
  if (!byForm.has(key)) byForm.set(key, natives);
150
162
  }
151
- return Object.freeze({ language, words: Object.freeze(words), byForm });
163
+ const derived = derivedForms(data).map(({ form, word }) =>
164
+ Object.freeze({ form, from: bySource.get(word) }),
165
+ );
166
+ return Object.freeze({
167
+ language,
168
+ words: Object.freeze(words),
169
+ byForm,
170
+ derived: Object.freeze(derived),
171
+ data: isMap(data) ? data : {},
172
+ });
152
173
  }
153
174
 
154
175
  /**
@@ -317,7 +338,10 @@ export function nativeLookup(lexicon, map, font) {
317
338
  * @property {import("./text-maps.mjs").TextMap} map - The map.
318
339
  * @property {string|undefined} font - The face, whose own rows come first.
319
340
  * @property {((word: string) => string|undefined)|undefined} native - The
320
- * language's native forms under the map in the face.
341
+ * language's native forms under the map in the face, for a map that spells
342
+ * by its rows.
343
+ * @property {((word: string) => import("./logographic.mjs").Writing)|undefined} writing -
344
+ * Each word's writing, for a logographic map.
321
345
  */
322
346
 
323
347
  /**
@@ -333,7 +357,20 @@ export function mappingFor(ctx, { map, font, lang }) {
333
357
  const textMap = ctx?.maps?.get(map);
334
358
  if (!textMap) return undefined;
335
359
  const lexicon = lang === undefined ? undefined : ctx.lexicons?.get(lang);
336
- return { map: textMap, font, native: nativeLookup(lexicon, textMap.name, font) };
360
+ if (!textMap.logographic)
361
+ return {
362
+ map: textMap,
363
+ font,
364
+ native: nativeLookup(lexicon, textMap.name, font),
365
+ writing: undefined,
366
+ };
367
+ const writings = lexicon ? writingsOf(lexicon.data, textMap.name, font) : undefined;
368
+ return {
369
+ map: textMap,
370
+ font,
371
+ native: undefined,
372
+ writing: writings ? writings.word : () => ({ missing: true, noLexicon: true }),
373
+ };
337
374
  }
338
375
 
339
376
  /**
@@ -411,6 +448,52 @@ export function uncapitalisedMessage(word, lower, lang, map) {
411
448
  );
412
449
  }
413
450
 
451
+ /**
452
+ * What is wrong with a word a logographic map gives no writing.
453
+ *
454
+ * @param {string} word - The word as written.
455
+ * @param {object} unwritten - Why it has none, as
456
+ * {@link module:engine/logographic.writingsOf} says.
457
+ * @param {string|undefined} lang - The run's language.
458
+ * @param {string} map - The map's name.
459
+ * @returns {string} The message.
460
+ */
461
+ export function unwrittenMessage(word, unwritten, lang, map) {
462
+ const why = /** @type {{noLexicon?: true, ambiguous?: {form: string, glosses: string[]}}} */ (
463
+ unwritten
464
+ );
465
+ if (why.noLexicon)
466
+ return lang === undefined ?
467
+ `\`${word}\` has no \`${map}\` writing: the map \`${map}\` is logographic, and the run ` +
468
+ "names no `lang=` whose lexicon writes it; mark the run with its language, or " +
469
+ "write the word directly in signs"
470
+ : `\`${word}\` has no \`${map}\` writing: the map \`${map}\` is logographic, and \`${lang}\` ` +
471
+ `has no lexicon (\`languages.${lang}.note\`) to write it from; write the word ` +
472
+ "directly in signs";
473
+ if (why.ambiguous) {
474
+ const { form, glosses } = why.ambiguous;
475
+ const quoted = glosses.map((gloss) => `“${gloss}”`);
476
+ const listed =
477
+ quoted.length < 2 ?
478
+ quoted.join("")
479
+ : `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
480
+ const stands =
481
+ `stands for ${glosses.length} words of the \`${lang}\` lexicon that the map ` +
482
+ `\`${map}\` writes differently, ${listed}`;
483
+ return (
484
+ (form === word ?
485
+ `\`${word}\` ${stands}`
486
+ : `\`${word}\` is written from its part \`${form}\`, which ${stands}`) +
487
+ "; write the word directly in signs, choosing by its sense in " +
488
+ `\`package-build lexicon ${lang} --map ${map}\``
489
+ );
490
+ }
491
+ return (
492
+ `\`${word}\` has no \`${map}\` writing in the \`${lang}\` lexicon, and its parts do not ` +
493
+ "give one; state one, or write the word directly in signs"
494
+ );
495
+ }
496
+
414
497
  /**
415
498
  * Map a passage exactly as the build maps a run with the same keys.
416
499
  *
@@ -423,8 +506,14 @@ export function uncapitalisedMessage(word, lower, lang, map) {
423
506
  export function transliterate(ctx, keys, text) {
424
507
  const found = commandMapping(ctx, keys);
425
508
  if ("problem" in found) return { problem: found.problem, about: found.about };
426
- const { map, font, native } = found.mapping;
427
- const mapped = applyMap(map, text, { font, native });
509
+ const { map, font, native, writing } = found.mapping;
510
+ const mapped = applyMap(map, text, { font, native, writing });
511
+ const unwritten = mapped.pieces.find((one) => one.unwritten !== undefined);
512
+ if (unwritten)
513
+ return {
514
+ about: "word",
515
+ problem: unwrittenMessage(unwritten.from, unwritten.unwritten, keys.lang, map.name),
516
+ };
428
517
  const piece = mapped.pieces.find((one) => one.uncapitalised !== undefined);
429
518
  if (piece)
430
519
  return {
@@ -442,15 +531,24 @@ export function transliterate(ctx, keys, text) {
442
531
  /**
443
532
  * Every word of a language's lexicon with its native form under a map: the
444
533
  * stated exception, else the rows' spelling, each exactly as the build gives
445
- * it.
534
+ * it. With `derived`, the forms the language's rules derive follow the words,
535
+ * each case folded and naming the word it comes from in `from`, so the list
536
+ * is what the mapped-run word check accepts of the lexicon.
537
+ *
538
+ * Under a logographic map an entry carries `source` in place of `exception`:
539
+ * `own` for a writing it states, `parts` for one its parts give, and `none`,
540
+ * with an empty `native`, where it has none. A word whose form a run cannot
541
+ * write, because the form stands for entries written differently, or a part
542
+ * does, also carries `ambiguous: true`; its `native` is still its own
543
+ * writing, the signs a run writes directly for that sense.
446
544
  *
447
545
  * @param {object} ctx - The run context.
448
- * @param {{lang: string, map: string, font?: string}} keys - The language, the
449
- * map and the face.
450
- * @returns {{entries: Array<{form: string, native: string, exception: boolean, class: string|null, gloss: string|null}>}|{problem: string, about: string}}
451
- * The words in `data.words` order, or the problem.
546
+ * @param {{lang: string, map: string, font?: string, derived?: boolean}} keys -
547
+ * The language, the map, the face, and whether to list derived forms.
548
+ * @returns {{entries: Array<{form: string, native: string, exception?: boolean, source?: "own"|"parts"|"none", ambiguous?: true, class: string|null, gloss: string|null, from?: string}>}|{problem: string, about: string}}
549
+ * The words in `data.words` order, then any derived forms, or the problem.
452
550
  */
453
- export function lexiconEntries(ctx, { lang, map, font }) {
551
+ export function lexiconEntries(ctx, { lang, map, font, derived = false }) {
454
552
  const found = commandMapping(ctx, { map, font, lang });
455
553
  if ("problem" in found) return { problem: found.problem, about: found.about };
456
554
  const lexicon = ctx.lexicons?.get(lang);
@@ -462,9 +560,14 @@ export function lexiconEntries(ctx, { lang, map, font }) {
462
560
  "in its `note`",
463
561
  };
464
562
  const { map: textMap, font: face, native } = found.mapping;
465
- const mapped = lexicon.words.map((word) =>
466
- applyMap(textMap, word.form, { font: face, native }),
467
- );
563
+ if (textMap.logographic) return { entries: writtenEntries(lexicon, textMap, face, derived) };
564
+ const listed = [
565
+ ...lexicon.words.map((word) => ({ word, form: word.form, from: undefined })),
566
+ ...(derived ?
567
+ lexicon.derived.map((one) => ({ word: one.from, form: one.form, from: one.from.form }))
568
+ : []),
569
+ ];
570
+ const mapped = listed.map((one) => applyMap(textMap, one.form, { font: face, native }));
468
571
  const unsound = mapped.flatMap((one) => one.pieces).find((one) => one.uncapitalised);
469
572
  if (unsound)
470
573
  return {
@@ -477,19 +580,64 @@ export function lexiconEntries(ctx, { lang, map, font }) {
477
580
  ),
478
581
  };
479
582
  return {
480
- entries: lexicon.words.map((word, i) => ({
481
- form: word.form,
583
+ entries: listed.map(({ word, form, from }, i) => ({
584
+ form,
482
585
  native: mapped[i].text,
483
586
  exception:
484
- native?.(word.form) !== undefined ||
485
- (word.form.toLowerCase() !== word.form &&
486
- native?.(word.form.toLowerCase()) !== undefined),
587
+ native?.(form) !== undefined ||
588
+ (form.toLowerCase() !== form && native?.(form.toLowerCase()) !== undefined),
487
589
  class: word.class,
488
590
  gloss: word.gloss,
591
+ ...(from === undefined ? {} : { from }),
489
592
  })),
490
593
  };
491
594
  }
492
595
 
596
+ /**
597
+ * Every word of a lexicon with its writing under a logographic map, as
598
+ * {@link lexiconEntries} lists them.
599
+ *
600
+ * @param {Lexicon} lexicon - The lexicon.
601
+ * @param {import("./text-maps.mjs").TextMap} map - The map.
602
+ * @param {string|undefined} font - The face.
603
+ * @param {boolean} derived - Whether to list derived forms.
604
+ * @returns {Array<Record<string, any>>} The entries.
605
+ */
606
+ function writtenEntries(lexicon, map, font, derived) {
607
+ const writings = writingsOf(lexicon.data, map.name, font);
608
+ // The `data.words` index of each lexicon word, which reads the same entries.
609
+ const indexes = [];
610
+ listOf(lexicon.data.words).forEach((word, index) => {
611
+ if (isMap(word) && typeof word.form === "string") indexes.push(index);
612
+ });
613
+ const shaped = (form, writing, word, from) => {
614
+ // Whether a run of the form, written as the build writes it, meets an
615
+ // ambiguity: another entry of the form, or of a part, written otherwise.
616
+ const run = applyMap(map, form, { font, writing: writings.word }).pieces.some(
617
+ (piece) => piece.unwritten?.ambiguous !== undefined,
618
+ );
619
+ return {
620
+ form,
621
+ native: "text" in writing ? writing.text : "",
622
+ source:
623
+ "text" in writing ? writing.source
624
+ : "ambiguous" in writing ? "parts"
625
+ : "none",
626
+ ...("ambiguous" in writing || run ? { ambiguous: true } : {}),
627
+ class: word.class,
628
+ gloss: word.gloss,
629
+ ...(from === undefined ? {} : { from }),
630
+ };
631
+ };
632
+ const entries = lexicon.words.map((word, at) =>
633
+ shaped(word.form, writings.entry(indexes[at]), word, undefined),
634
+ );
635
+ if (derived)
636
+ for (const one of lexicon.derived)
637
+ entries.push(shaped(one.form, writings.word(one.form), one.from, one.from.form));
638
+ return entries;
639
+ }
640
+
493
641
  /**
494
642
  * Whether a shared row can be reached in a face: no plain row of the face's
495
643
  * own matches at the start of its `from`, which would win there first.
@@ -588,10 +736,16 @@ export function checkMapFonts(note, ctx) {
588
736
  }
589
737
 
590
738
  /**
591
- * Check a language note's native forms against the maps and faces: an unknown
592
- * map, a face the map is not for, a second form for one map and face, a form
593
- * that states what the rows already give, and a code point a face it applies
594
- * in does not draw. The shape of each entry is the data-key check's.
739
+ * Check a language note's native forms against the maps and faces: a word's
740
+ * `native`, an affix's, and the endings' in `data.grammar.native`. Each is
741
+ * checked for an unknown map, a face the map is not for, a second form for one
742
+ * map and face (and one ending), and a code point a face it applies in does
743
+ * not draw. A word's form under a map that spells by its rows is also checked
744
+ * for stating what the rows already give; under a logographic map the rows
745
+ * spell no word, so that check does not apply. An affix's and an ending's
746
+ * writings mean something only under a logographic map, and an ending of a
747
+ * person is written only where `data.grammar.verbs` states `persons`. The
748
+ * shape of each entry is the data-key check's.
595
749
  *
596
750
  * @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
597
751
  * @param {{faces?: ReadonlyMap<string, import("./script-fonts.mjs").Face>, maps?: ReadonlyMap<string, import("./text-maps.mjs").TextMap>}} ctx -
@@ -604,35 +758,47 @@ export function checkLexiconNatives(note, ctx) {
604
758
  const raw = note.raw ?? "";
605
759
  const finding = (keyPath, message, key = false) => ({
606
760
  file: note.file,
607
- ...positionOfFrontmatterPath(raw, ["data", "words", ...keyPath], { key }),
761
+ ...positionOfFrontmatterPath(raw, ["data", ...keyPath], { key }),
608
762
  severity: "error",
609
763
  message,
610
764
  });
611
765
  const maps = ctx?.maps ?? new Map();
612
766
  const faces = ctx?.faces ?? new Map();
613
767
  const findings = [];
614
- listOf(fm.data.words).forEach((word, i) => {
615
- if (!isMap(word) || !Array.isArray(word.native)) return;
616
- const form = typeof word.form === "string" ? word.form : "";
768
+
769
+ /**
770
+ * Check one `native` list.
771
+ *
772
+ * @param {unknown[]} list - The list as authored.
773
+ * @param {Array<string|number>} keyPath - Its path under `data`.
774
+ * @param {object} as - What it belongs to.
775
+ * @param {"word"|"affix"|"ending"} as.kind - A word's, an affix's or the
776
+ * endings' list.
777
+ * @param {string} [as.form] - The word's form, for the rows' check.
778
+ */
779
+ const checkList = (list, keyPath, { kind, form = "" }) => {
780
+ const base = `data.${keyPath.join(".").replace(/\.(\d+)/g, "[$1]")}`;
617
781
  const stated = new Map();
618
- const entries = word.native.map((entry) =>
782
+ const entries = list.map((entry) =>
619
783
  isMap(entry) && typeof entry.map === "string" && typeof entry.text === "string" ?
620
784
  {
621
785
  map: entry.map,
622
786
  font: typeof entry.font === "string" ? entry.font : undefined,
623
787
  text: entry.text,
788
+ ending: typeof entry.ending === "string" ? entry.ending : undefined,
624
789
  }
625
790
  : undefined,
626
791
  );
792
+ const persons = isMap(fm.data.grammar?.verbs?.persons);
627
793
  entries.forEach((entry, j) => {
628
794
  if (!entry) return;
629
- const label = `data.words[${i}].native[${j}]`;
795
+ const label = `${base}[${j}]`;
630
796
  const map = maps.get(entry.map);
631
797
  if (!map) {
632
798
  const near = nearest(entry.map, [...maps.keys()]);
633
799
  findings.push(
634
800
  finding(
635
- [i, "native", j, "map"],
801
+ [...keyPath, j, "map"],
636
802
  `\`${label}.map\` is \`${entry.map}\`, which is not a map: a native form names ` +
637
803
  "the `data.map.name` of a mapping note" +
638
804
  (near ? `; did you mean \`${near}\`?` : ""),
@@ -640,24 +806,51 @@ export function checkLexiconNatives(note, ctx) {
640
806
  );
641
807
  return;
642
808
  }
809
+ if (kind !== "word" && !map.logographic) {
810
+ findings.push(
811
+ finding(
812
+ [...keyPath, j],
813
+ `\`${label}\` names the map \`${map.name}\`, which is not logographic; ` +
814
+ `${kind === "affix" ? "an affix" : "an ending"} is written by a native ` +
815
+ "form only under a logographic map",
816
+ ),
817
+ );
818
+ return;
819
+ }
643
820
  if (entry.font !== undefined && map.fonts.size && !map.fonts.has(entry.font)) {
644
821
  findings.push(
645
822
  finding(
646
- [i, "native", j, "font"],
823
+ [...keyPath, j, "font"],
647
824
  `\`${label}.font\` is \`${entry.font}\`, which is not a font of the map ` +
648
825
  `\`${map.name}\`; it is for ${said([...map.fonts.keys()])}`,
649
826
  ),
650
827
  );
651
828
  return;
652
829
  }
653
- const slot = `${entry.map}\u0000${entry.font ?? ""}`;
830
+ if (
831
+ kind === "ending" &&
832
+ entry.ending !== undefined &&
833
+ entry.ending !== WRITTEN_ENDINGS[0] &&
834
+ WRITTEN_ENDINGS.includes(entry.ending) &&
835
+ !persons
836
+ )
837
+ findings.push(
838
+ finding(
839
+ [...keyPath, j],
840
+ `\`${label}\` writes the ending \`${entry.ending}\`, and \`data.grammar.verbs\` ` +
841
+ "states no `persons`",
842
+ ),
843
+ );
844
+ const slot = `${entry.ending ?? ""}\u0000${entry.map}\u0000${entry.font ?? ""}`;
654
845
  if (stated.has(slot))
655
846
  findings.push(
656
847
  finding(
657
- [i, "native", j],
658
- `\`${label}\` states a second native form for the map \`${map.name}\`` +
848
+ [...keyPath, j],
849
+ `\`${label}\` states a second native form` +
850
+ (kind === "ending" ? ` of the ending \`${entry.ending}\`` : "") +
851
+ ` for the map \`${map.name}\`` +
659
852
  (entry.font === undefined ? "" : ` in the font \`${entry.font}\``) +
660
- `; the first is \`data.words[${i}].native[${stated.get(slot)}]\``,
853
+ `; the first is \`${base}[${stated.get(slot)}]\``,
661
854
  ),
662
855
  );
663
856
  else stated.set(slot, j);
@@ -665,10 +858,14 @@ export function checkLexiconNatives(note, ctx) {
665
858
  entry.font !== undefined ? [entry.font]
666
859
  : map.fonts.size ? [...map.fonts.keys()]
667
860
  : [undefined];
668
- if (fonts.every((font) => applyMap(map, form, { font }).text === entry.text))
861
+ if (
862
+ kind === "word" &&
863
+ !map.logographic &&
864
+ fonts.every((font) => applyMap(map, form, { font }).text === entry.text)
865
+ )
669
866
  findings.push(
670
867
  finding(
671
- [i, "native", j],
868
+ [...keyPath, j],
672
869
  `\`${label}\` states what the rows already give: the map \`${map.name}\` ` +
673
870
  `spells \`${form}\` so ` +
674
871
  (entry.font !== undefined ? `in the font \`${entry.font}\``
@@ -686,7 +883,11 @@ export function checkLexiconNatives(note, ctx) {
686
883
  : [...map.fonts.keys()].filter(
687
884
  (font) =>
688
885
  !entries.some(
689
- (other) => other && other.map === entry.map && other.font === font,
886
+ (other) =>
887
+ other &&
888
+ other.map === entry.map &&
889
+ other.ending === entry.ending &&
890
+ other.font === font,
690
891
  ),
691
892
  );
692
893
  for (const slug of applies) {
@@ -699,7 +900,7 @@ export function checkLexiconNatives(note, ctx) {
699
900
  seen.add(point);
700
901
  findings.push(
701
902
  finding(
702
- [i, "native", j],
903
+ [...keyPath, j],
703
904
  `\`${label}\` holds ${codeName(point)}, which the font \`${slug}\`, ` +
704
905
  `listed in the map \`${map.name}\`, does not draw`,
705
906
  ),
@@ -707,7 +908,21 @@ export function checkLexiconNatives(note, ctx) {
707
908
  }
708
909
  }
709
910
  });
911
+ };
912
+
913
+ listOf(fm.data.words).forEach((word, i) => {
914
+ if (!isMap(word) || !Array.isArray(word.native)) return;
915
+ checkList(word.native, ["words", i, "native"], {
916
+ kind: "word",
917
+ form: typeof word.form === "string" ? word.form : "",
918
+ });
919
+ });
920
+ listOf(fm.data.affixes).forEach((affix, i) => {
921
+ if (isMap(affix) && Array.isArray(affix.native))
922
+ checkList(affix.native, ["affixes", i, "native"], { kind: "affix" });
710
923
  });
924
+ if (isMap(fm.data.grammar) && Array.isArray(fm.data.grammar.native))
925
+ checkList(fm.data.grammar.native, ["grammar", "native"], { kind: "ending" });
711
926
  return findings;
712
927
  }
713
928
 
@@ -131,12 +131,13 @@ function parentShortcode(parent) {
131
131
  }
132
132
 
133
133
  /**
134
- * A `parents` value as a list of shortcodes.
134
+ * A list of places — `parents`, a being's `homes`, a scenario's `locations` —
135
+ * as their shortcodes.
135
136
  *
136
137
  * @param {unknown} value - The authored value, a list or a lone entry.
137
138
  * @returns {string[]} Shortcodes, lower case, empties dropped.
138
139
  */
139
- function parentsOf(value) {
140
+ export function placeShortcodes(value) {
140
141
  const list =
141
142
  Array.isArray(value) ? value
142
143
  : value ? [value]
@@ -198,7 +199,7 @@ export function placesFromRecords(records, { contentBase, base }) {
198
199
  name,
199
200
  type: "place",
200
201
  subType: record.subType === undefined ? undefined : String(record.subType),
201
- parents: parentsOf(data.parents),
202
+ parents: placeShortcodes(data.parents),
202
203
  borders: relationList(data.borders),
203
204
  routes: relationList(data.routes),
204
205
  address: record.address?.canonical,
@@ -251,7 +252,7 @@ function addForeignPlaces(world, foreignIndex) {
251
252
  name: String(entry.name ?? shortcode),
252
253
  type: "place",
253
254
  subType: entry.subType === undefined ? undefined : String(entry.subType),
254
- parents: parentsOf(entry.parents),
255
+ parents: placeShortcodes(entry.parents),
255
256
  borders: relationList(entry.borders),
256
257
  routes: relationList(entry.routes),
257
258
  address: canonical,
@@ -25,8 +25,6 @@ import { noteCalendar } from "./note-calendars.mjs";
25
25
  /**
26
26
  * An inline expression, as an author writes one.
27
27
  *
28
- * Hugo shortcodes begin `{{<` or `{{%` and stay in the source for Hugo.
29
- *
30
28
  * Exported because a renderer that meets one has met a body this pass never ran
31
29
  * over, and has to be able to say so.
32
30
  *
@@ -51,11 +51,16 @@ import {
51
51
  printParseErrorCode,
52
52
  } from "jsonc-parser";
53
53
  import markdownIt from "markdown-it";
54
- import { applyFixes, resolveModule } from "markdownlint";
55
- import { extendConfig, lint, readConfig } from "markdownlint/promise";
56
54
  import { minimatch } from "minimatch";
57
55
  import { LineCounter, parse as parseYaml, parseDocument } from "yaml";
58
56
 
57
+ import {
58
+ applyFixes,
59
+ extendConfig,
60
+ lint,
61
+ readConfig,
62
+ resolveModule,
63
+ } from "../vendor/markdownlint.mjs";
59
64
  import { walkFiles } from "./walk-files.mjs";
60
65
 
61
66
  /** Option-file names, in the order the first existing one is taken. */
@@ -313,7 +318,8 @@ async function validateOptions(file, text, raw, isRoot) {
313
318
  */
314
319
  async function extendRules(file, config) {
315
320
  try {
316
- return await extendConfig(config, file, [...CONFIG_PARSERS]);
321
+ // `extendConfig`, unlike `readConfig`, has no default file system.
322
+ return await extendConfig(config, file, [...CONFIG_PARSERS], fs);
317
323
  } catch (err) {
318
324
  throw new MarkdownConfigError(file, `cannot resolve \`extends\`: ${err.message}`);
319
325
  }
@@ -14,6 +14,26 @@
14
14
  import { scanBlocks } from "./content-blocks.mjs";
15
15
  import { scanAlerts } from "./content-alerts.mjs";
16
16
  import { scanRuns } from "./content-scripts.mjs";
17
+ import { matchAllOutsideCode } from "./code-fences.mjs";
18
+
19
+ /** The opening of a shortcode: `{{<` or `{{%`. */
20
+ const SHORTCODE = /\{\{[<%]/g;
21
+
22
+ /**
23
+ * Locate every shortcode outside code.
24
+ * @param {string} body - Markdown with its frontmatter masked.
25
+ * @returns {Array<{line: number, column: number, message: string}>} One error per shortcode.
26
+ */
27
+ function shortcodeErrors(body) {
28
+ return matchAllOutsideCode(body, SHORTCODE).map((match) => {
29
+ const before = body.slice(0, match.index);
30
+ return {
31
+ line: before.split("\n").length,
32
+ column: match.index - before.lastIndexOf("\n"),
33
+ message: "A shortcode is not supported; write `[x]{.glyph}` or `[signs]{.cartouche}`",
34
+ };
35
+ });
36
+ }
17
37
 
18
38
  /**
19
39
  * Validate authored block syntax with positions in the original file.
@@ -39,6 +59,7 @@ export function markupFindings(source, file, syntax, runs) {
39
59
  ...(scanned?.blocks ?? scanBlocks(body)).errors,
40
60
  ...(scanned?.alerts ?? scanAlerts(body)).errors,
41
61
  ...(marked?.errors ?? []),
62
+ ...shortcodeErrors(body),
42
63
  ].map((error) => ({
43
64
  file,
44
65
  ...error,