@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
@@ -17,20 +17,14 @@
17
17
  * each word (see {@link module:engine/word-pattern.WORD}) is accepted when,
18
18
  * case folded, it is:
19
19
  *
20
- * - a `data.words[].form` of the language, or a word of one;
20
+ * - a `data.words[].form` of the language, or a word of one, or a declared
21
+ * `variants` entry of a word;
21
22
  * - a word of a name in the language note's own `data.register` whose `tongue`
22
23
  * is not `translation`, so a foreign name the run uses is listed there first,
23
24
  * as `other:<tongue>` or under another tag the note declares;
24
- * - a form one rule the language states derives from a lexicon form:
25
- * - the noun plural `data.grammar.plural` gives, or the word's own `plural`;
26
- * - a verb's citation form with one ending of `data.grammar.verbs.persons`,
27
- * which takes the place of a closing `data.grammar.verbs.replaces` where
28
- * the pattern states one (see {@link module:engine/verb-pattern.verbForm});
29
- * - a suffix of `data.affixes` that states `replaces`, or its `front` form:
30
- * `vowel` takes the place of a closing vowel (a letter whose base is `a`,
31
- * `e`, `i`, `o` or `u`), letters take the place of those letters, and a
32
- * stem closing otherwise takes the suffix whole. An affix that states no
33
- * `replaces` derives nothing.
25
+ * - a form one rule the language states derives from a lexicon form: a noun
26
+ * plural, a verb's present form, a replacing suffix, an agreeing form or a
27
+ * participle, of a lexicon form or of one of its variants (see {@link module:engine/word-forms.derivedForms}).
34
28
  *
35
29
  * A word not accepted whole is split at its hyphens and en dashes, and each
36
30
  * part is checked. An apostrophe joins a word, so `k'aralzon` is one word, and
@@ -42,11 +36,9 @@
42
36
  * @module
43
37
  */
44
38
 
45
- import { pluralOf } from "./dictionary-rows.mjs";
46
39
  import { languageRecordsByName } from "./map-lexicon.mjs";
47
40
  import { RESERVED_TONGUE } from "./language-terms.mjs";
48
- import { replaceClosing, verbForm, verbPattern } from "./verb-pattern.mjs";
49
- import { parseWordClass } from "./word-classes.mjs";
41
+ import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
50
42
  import { WORD } from "./word-pattern.mjs";
51
43
 
52
44
  /** Whether a value is a plain map. */
@@ -61,104 +53,6 @@ const JOINS = /[–-]/u;
61
53
  /** A letter, which a checked word holds at least one of. */
62
54
  const LETTER = /\p{L}/u;
63
55
 
64
- /**
65
- * A word as it is compared: NFC, lowercase, `’` read as `'`.
66
- *
67
- * @param {string} word - The word.
68
- * @returns {string} Its folded form.
69
- */
70
- export function foldWord(word) {
71
- return word.normalize("NFC").toLowerCase().replace(/’/gu, "'");
72
- }
73
-
74
- /** Whether a character is a vowel: its base letter is a, e, i, o or u. */
75
- const isVowel = (char) => /^[aeiou]/iu.test(char.normalize("NFD"));
76
-
77
- /**
78
- * Add a text's words to a set, each whole and each part of a compound.
79
- *
80
- * @param {Set<string>} set - The set.
81
- * @param {string} text - A form or a name.
82
- */
83
- function addWords(set, text) {
84
- for (const [word] of text.matchAll(WORD)) {
85
- const folded = foldWord(word);
86
- set.add(folded);
87
- for (const part of folded.split(JOINS)) if (part) set.add(part);
88
- }
89
- }
90
-
91
- /**
92
- * The form a suffix stating `replaces` makes of a stem.
93
- *
94
- * @param {string} stem - The stem, folded.
95
- * @param {string} suffix - The suffix, folded, with no hyphen.
96
- * @param {string} replaces - The affix's `replaces`.
97
- * @returns {string} The derived form.
98
- */
99
- function attach(stem, suffix, replaces) {
100
- if (replaces === "vowel") {
101
- const last = Array.from(stem).at(-1);
102
- return last !== undefined && isVowel(last) ?
103
- stem.slice(0, -last.length) + suffix
104
- : stem + suffix;
105
- }
106
- return replaceClosing(stem, suffix, foldWord(replaces));
107
- }
108
-
109
- /**
110
- * The suffixes a language's affixes state `replaces` for: each `form`, and
111
- * its `front` form, written `-x`.
112
- *
113
- * @param {Record<string, any>} data - The language note's `data`.
114
- * @returns {Array<{suffix: string, replaces: string}>} The suffixes, folded.
115
- */
116
- function replacingSuffixes(data) {
117
- const out = [];
118
- for (const affix of listOf(data.affixes)) {
119
- if (!isMap(affix) || typeof affix.replaces !== "string") continue;
120
- for (const written of [affix.form, affix.front]) {
121
- if (typeof written !== "string") continue;
122
- const form = foldWord(written.trim());
123
- if (!/^[-–]/u.test(form) || /[-–]$/u.test(form)) continue;
124
- const suffix = form.replace(/^[-–]+/u, "");
125
- if (suffix) out.push({ suffix, replaces: affix.replaces });
126
- }
127
- }
128
- return out;
129
- }
130
-
131
- /**
132
- * The words a language note accepts of its own: its forms, and what its
133
- * plural rows, verb pattern and replacing suffixes derive from them.
134
- *
135
- * @param {Record<string, any>} data - The language note's `data`.
136
- * @returns {Set<string>} The words, folded.
137
- */
138
- export function lexiconWords(data) {
139
- const accepted = new Set();
140
- const grammar = isMap(data?.grammar) ? data.grammar : undefined;
141
- const verbs = verbPattern(grammar?.verbs);
142
- const replaces = verbs?.replaces === undefined ? undefined : foldWord(verbs.replaces);
143
- const suffixes = replacingSuffixes(data ?? {});
144
- for (const word of listOf(data?.words)) {
145
- if (!isMap(word) || typeof word.form !== "string") continue;
146
- addWords(accepted, word.form);
147
- const plural = pluralOf(word, grammar);
148
- if (plural) addWords(accepted, plural);
149
- if (verbs && parseWordClass(word.class).base === "v")
150
- for (const { ending } of verbs.endings)
151
- addWords(accepted, verbForm(foldWord(word.form), foldWord(ending), replaces));
152
- if (suffixes.length)
153
- for (const [stem] of word.form.matchAll(WORD)) {
154
- const folded = foldWord(stem);
155
- for (const { suffix, replaces } of suffixes)
156
- accepted.add(attach(folded, suffix, replaces));
157
- }
158
- }
159
- return accepted;
160
- }
161
-
162
56
  /**
163
57
  * The words of every name a language note registers under a tongue other than
164
58
  * `translation`.
@@ -0,0 +1,38 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **The words a script note and a language note state emphasis by.** Read by
16
+ * the vocabulary as it is declared, so this module imports nothing; the
17
+ * checks and the conventions' use are in {@link module:engine/script-emphasis}.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ /** The conventions a script states its emphasis by. */
23
+ export const EMPHASIS_CONVENTIONS = Object.freeze(["rubric", "overline"]);
24
+
25
+ /** The class each convention gives an `<em>` or a `<strong>`. */
26
+ export const EMPHASIS_CLASSES = Object.freeze({
27
+ rubric: "emph-rubric",
28
+ overline: "emph-overline",
29
+ });
30
+
31
+ /**
32
+ * The conventions a language note states, which override its script's.
33
+ * `words` marks emphasis with a word, so a mark in its text is a finding.
34
+ */
35
+ export const LANGUAGE_EMPHASES = Object.freeze(["words", ...EMPHASIS_CONVENTIONS]);
36
+
37
+ /** The `subType` of a script note. */
38
+ export const SCRIPT_SUBTYPE = "script";
@@ -0,0 +1,295 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **How a script marks emphasis.** A script note — a `skill` of
16
+ * `subType: script` — names the faces it is written in and its scribal
17
+ * convention for a stressed word:
18
+ *
19
+ * ```yaml
20
+ * data:
21
+ * script:
22
+ * fonts: [semaron-kuriale]
23
+ * emphasis: rubric
24
+ * strongEmphasis: overline
25
+ * ```
26
+ *
27
+ * - **`fonts`** lists installed faces by the slug `font=` names them by. A
28
+ * face belongs to one script.
29
+ * - **`emphasis`** is `rubric` (red text) or `overline` (a line above the
30
+ * word), and sets both `*…*` and `**…**`.
31
+ * - **`strongEmphasis`**, where stated, sets `**…**` alone.
32
+ *
33
+ * Inside a run in one of those faces, `*…*` and `**…**` stay an `<em>` and a
34
+ * `<strong>` on the site and in Foundry, carrying {@link module:engine/script-emphasis-terms.EMPHASIS_CLASSES}'s
35
+ * class, and the book sets them in its rubric ink or under an overline. A face
36
+ * no script note lists sets its emphasis plain: the element with no class, in
37
+ * the face's Regular. See {@link module:engine/content-scripts.renderRuns}.
38
+ *
39
+ * @module
40
+ */
41
+
42
+ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
43
+ import { isNoteRecord } from "./index-records.mjs";
44
+ import { nearest } from "./near-miss.mjs";
45
+ import { FONTS_PACKAGE } from "./script-fonts.mjs";
46
+ import {
47
+ EMPHASIS_CONVENTIONS,
48
+ LANGUAGE_EMPHASES,
49
+ SCRIPT_SUBTYPE,
50
+ } from "./script-emphasis-terms.mjs";
51
+ import { GENERIC_FAMILIES } from "./text-maps.mjs";
52
+ import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
53
+
54
+ export {
55
+ EMPHASIS_CLASSES,
56
+ EMPHASIS_CONVENTIONS,
57
+ LANGUAGE_EMPHASES,
58
+ } from "./script-emphasis-terms.mjs";
59
+
60
+ /**
61
+ * One face's conventions, one for each level of emphasis.
62
+ *
63
+ * @typedef {object} Emphasis
64
+ * @property {"rubric"|"overline"} em - What `*…*` is set by.
65
+ * @property {"rubric"|"overline"} strong - What `**…**` is set by.
66
+ */
67
+
68
+ /**
69
+ * A convention a language note states.
70
+ *
71
+ * @typedef {"words"|"rubric"|"overline"} LanguageEmphasis
72
+ */
73
+
74
+ /** Whether a value is a plain map. */
75
+ const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
76
+
77
+ /** Whether a value is one of the conventions. */
78
+ const isConvention = (value) => EMPHASIS_CONVENTIONS.includes(value);
79
+
80
+ /**
81
+ * Each face's conventions, from the script notes among the index records. The
82
+ * first note to list a face keeps it; a second is the lint's to report, and a
83
+ * note whose convention is unknown gives none.
84
+ *
85
+ * @param {Iterable<Record<string, any>>|undefined} records - Index records.
86
+ * @returns {Map<string, Emphasis>} The conventions, by face slug.
87
+ */
88
+ export function emphasesFromRecords(records) {
89
+ const out = new Map();
90
+ for (const record of records ?? []) {
91
+ if (!record || !isNoteRecord(record)) continue;
92
+ if (
93
+ record.type !== "skill" ||
94
+ String(record.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE
95
+ )
96
+ continue;
97
+ const script = record.data?.script;
98
+ if (!isMap(script) || !Array.isArray(script.fonts) || !isConvention(script.emphasis))
99
+ continue;
100
+ const strong =
101
+ script.strongEmphasis === undefined || script.strongEmphasis === null ?
102
+ script.emphasis
103
+ : script.strongEmphasis;
104
+ if (!isConvention(strong)) continue;
105
+ const emphasis = Object.freeze({ em: script.emphasis, strong });
106
+ for (const slug of script.fonts)
107
+ if (typeof slug === "string" && !out.has(slug)) out.set(slug, emphasis);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * The convention each language's note states.
114
+ *
115
+ * @param {ReadonlyMap<string, Record<string, any>>} byName - Each configured
116
+ * language's note record, by language name, from
117
+ * {@link module:engine/map-lexicon.languageRecordsByName}.
118
+ * @returns {Map<string, LanguageEmphasis>} The conventions, by language name;
119
+ * a language whose note states none, or one the build does not know, is
120
+ * absent.
121
+ */
122
+ export function languageEmphasesOf(byName) {
123
+ const out = new Map();
124
+ for (const [name, record] of byName ?? [])
125
+ if (LANGUAGE_EMPHASES.includes(record?.data?.emphasis)) out.set(name, record.data.emphasis);
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Refuse `data.emphasis` on a skill that is not a language, at its key.
131
+ *
132
+ * @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
133
+ * @returns {object[]} The findings.
134
+ */
135
+ export function checkLanguageEmphasis(note) {
136
+ const value = note?.fm?.data?.emphasis;
137
+ if (value === undefined || value === null) return [];
138
+ if (String(note.fm?.subType ?? "").toLowerCase() === LANGUAGE_SUBTYPE) return [];
139
+ return [
140
+ {
141
+ file: note.file,
142
+ ...positionOfFrontmatterPath(note.raw ?? "", ["data", "emphasis"], { key: true }),
143
+ severity: "error",
144
+ message:
145
+ "`data.emphasis` states how a language marks emphasis, and this note's " +
146
+ `\`subType\` is not \`${LANGUAGE_SUBTYPE}\``,
147
+ },
148
+ ];
149
+ }
150
+
151
+ /** The notes listing each face, read once per lint index. */
152
+ const listedFaces = new WeakMap();
153
+
154
+ /**
155
+ * Each face the index's script notes list, and the notes listing it.
156
+ *
157
+ * @param {{notes?: Iterable<{file: string, fm?: Record<string, any>}>}} index - The lint index.
158
+ * @returns {Map<string, string[]>} Slug → the files that list it.
159
+ */
160
+ function facesIn(index) {
161
+ const known = listedFaces.get(index);
162
+ if (known) return known;
163
+ const faces = new Map();
164
+ for (const note of index.notes ?? []) {
165
+ const fm = note?.fm;
166
+ if (fm?.type !== "skill" || String(fm.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE)
167
+ continue;
168
+ const fonts = fm.data?.script?.fonts;
169
+ if (!Array.isArray(fonts)) continue;
170
+ for (const slug of new Set(fonts.filter((value) => typeof value === "string"))) {
171
+ if (!faces.has(slug)) faces.set(slug, []);
172
+ faces.get(slug).push(note.file);
173
+ }
174
+ }
175
+ listedFaces.set(index, faces);
176
+ return faces;
177
+ }
178
+
179
+ /**
180
+ * Check a note's `data.script`: that the note is a script, and that its
181
+ * `fonts` is not empty, names no generic family, lists no face twice, and
182
+ * lists no face another script note lists. The data-key check owns every key,
183
+ * its kind and the conventions; whether a face is installed is
184
+ * {@link checkScriptNoteFonts}'s.
185
+ *
186
+ * @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
187
+ * @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index.
188
+ * @returns {object[]} The findings, each at its key or entry.
189
+ */
190
+ export function checkScriptField(note, { index } = {}) {
191
+ const value = note?.fm?.data?.script;
192
+ if (value === undefined || value === null) return [];
193
+ const raw = note.raw ?? "";
194
+ /**
195
+ * @param {Array<string|number>} keyPath - Where the fault is.
196
+ * @param {string} message - What is wrong.
197
+ * @param {boolean} [key] - Locate the key rather than its value.
198
+ */
199
+ const finding = (keyPath, message, key = false) => ({
200
+ file: note.file,
201
+ ...positionOfFrontmatterPath(raw, ["data", "script", ...keyPath], { key }),
202
+ severity: "error",
203
+ message,
204
+ });
205
+ if (String(note.fm?.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE)
206
+ return [
207
+ finding(
208
+ [],
209
+ "`data.script` states a script's faces and emphasis, and this note's " +
210
+ `\`subType\` is not \`${SCRIPT_SUBTYPE}\``,
211
+ true,
212
+ ),
213
+ ];
214
+ if (!isMap(value) || !Array.isArray(value.fonts)) return [];
215
+ const { fonts } = value;
216
+ if (!fonts.length)
217
+ return [
218
+ finding(
219
+ ["fonts"],
220
+ "`data.script.fonts` is empty; a script lists each face it is written in",
221
+ ),
222
+ ];
223
+ const findings = [];
224
+ const first = new Map();
225
+ fonts.forEach((slug, at) => {
226
+ if (typeof slug !== "string") return;
227
+ if (GENERIC_FAMILIES.includes(slug)) {
228
+ findings.push(
229
+ finding(
230
+ ["fonts", at],
231
+ `\`data.script.fonts[${at}]\` is \`${slug}\`, a generic family; a script lists the faces it is written in`,
232
+ ),
233
+ );
234
+ return;
235
+ }
236
+ if (first.has(slug)) {
237
+ findings.push(
238
+ finding(
239
+ ["fonts", at],
240
+ `\`data.script.fonts[${at}]\` lists the font \`${slug}\` a second time; the first is ` +
241
+ `\`data.script.fonts[${first.get(slug)}]\``,
242
+ ),
243
+ );
244
+ return;
245
+ }
246
+ first.set(slug, at);
247
+ if (!index) return;
248
+ const others = (facesIn(index).get(slug) ?? []).filter((file) => file !== note.file);
249
+ if (others.length)
250
+ findings.push(
251
+ finding(
252
+ ["fonts", at],
253
+ `\`data.script.fonts[${at}]\` is \`${slug}\`, which ${others.join(", ")} also lists; a face belongs to one script`,
254
+ ),
255
+ );
256
+ });
257
+ return findings;
258
+ }
259
+
260
+ /**
261
+ * Each face a script note lists that is not installed, at its entry.
262
+ *
263
+ * @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
264
+ * @param {{faces?: ReadonlyMap<string, unknown>}} [ctx] - The installed faces,
265
+ * from {@link module:engine/content-scripts.runContext}.
266
+ * @returns {object[]} The findings.
267
+ */
268
+ export function checkScriptNoteFonts(note, ctx) {
269
+ const fm = note?.fm;
270
+ if (String(fm?.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE) return [];
271
+ const fonts = fm?.data?.script?.fonts;
272
+ if (!Array.isArray(fonts)) return [];
273
+ const faces = ctx?.faces ?? new Map();
274
+ const raw = note.raw ?? "";
275
+ const findings = [];
276
+ const seen = new Set();
277
+ fonts.forEach((slug, at) => {
278
+ if (typeof slug !== "string" || seen.has(slug)) return;
279
+ seen.add(slug);
280
+ if (GENERIC_FAMILIES.includes(slug) || faces.has(slug)) return;
281
+ const near = nearest(slug, [...faces.keys()]);
282
+ findings.push({
283
+ file: note.file,
284
+ ...positionOfFrontmatterPath(raw, ["data", "script", "fonts", at]),
285
+ severity: "error",
286
+ message:
287
+ `\`${slug}\` is not a font: \`data.script.fonts[${at}]\` names ` +
288
+ (faces.size ? "an installed face" : (
289
+ `a face of ${FONTS_PACKAGE}, which this package has not installed`
290
+ )) +
291
+ (near ? `; did you mean \`${near}\`?` : ""),
292
+ });
293
+ });
294
+ return findings;
295
+ }
@@ -39,8 +39,8 @@ import { FONTS_MANIFEST, resolveScriptFonts } from "./script-fonts.mjs";
39
39
  export const STAGED_FONTS_DIR = "fonts/heroiclands";
40
40
 
41
41
  /**
42
- * Where the files land in the Hugo static tree, relative to `build/hugo/static`,
43
- * beneath a directory per version.
42
+ * Where the files land in the site's static tree, beneath a directory per
43
+ * version.
44
44
  *
45
45
  * @type {string}
46
46
  */
@@ -279,10 +279,10 @@ export function siteFontsParam(config) {
279
279
  }
280
280
 
281
281
  /**
282
- * The directory under the Hugo static tree that holds this version's fonts.
282
+ * The directory under the site's static tree that holds this version's fonts.
283
283
  *
284
284
  * @param {string} version - The fonts package's version.
285
- * @returns {string} The POSIX path, relative to `build/hugo/static`.
285
+ * @returns {string} The POSIX path, relative to the static tree.
286
286
  */
287
287
  export function siteFontsDestination(version) {
288
288
  return `${SITE_FONTS_DIR}/${version}`;