@heroiclands/package-build 23.13.0 → 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 (233) hide show
  1. package/CHANGELOG.md +36 -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 +244 -78
  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 +112 -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 +259 -314
  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-render.mjs +593 -0
  88. package/engine/site-root.mjs +22 -13
  89. package/engine/site-search.mjs +161 -0
  90. package/engine/site-serve.mjs +273 -0
  91. package/engine/site-theme-banner.mjs +154 -0
  92. package/engine/site-theme-urls.mjs +125 -0
  93. package/engine/site-theme.mjs +926 -0
  94. package/engine/text-maps.mjs +57 -10
  95. package/engine/typographer.mjs +248 -0
  96. package/engine/verse-metre.mjs +357 -0
  97. package/engine/wikilink-syntax.mjs +29 -47
  98. package/engine/wikilinks.mjs +758 -279
  99. package/engine/word-forms.mjs +39 -24
  100. package/engine/word-rows.mjs +12 -2
  101. package/engine/word-terms.mjs +29 -4
  102. package/manifest.mjs +3 -4
  103. package/package.json +10 -7
  104. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  105. package/site-theme/layouts/base.hbs +31 -0
  106. package/site-theme/layouts/blog-list.hbs +30 -0
  107. package/site-theme/layouts/blog-post.hbs +40 -0
  108. package/site-theme/layouts/home.hbs +35 -0
  109. package/site-theme/layouts/list.hbs +20 -0
  110. package/site-theme/layouts/not-found.hbs +31 -0
  111. package/site-theme/layouts/page.hbs +7 -0
  112. package/site-theme/partials/copy-buttons.hbs +37 -0
  113. package/site-theme/partials/draft-notice.hbs +5 -0
  114. package/site-theme/partials/exturl.hbs +2 -0
  115. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  116. package/site-theme/partials/header.hbs +44 -0
  117. package/site-theme/partials/hero-banner.hbs +11 -0
  118. package/site-theme/partials/infobox-entry.hbs +2 -0
  119. package/site-theme/partials/infobox-section.hbs +31 -0
  120. package/site-theme/partials/infobox-value.hbs +2 -0
  121. package/site-theme/partials/infobox.hbs +18 -0
  122. package/site-theme/partials/list-members.hbs +9 -0
  123. package/site-theme/partials/nav-links.hbs +5 -0
  124. package/site-theme/partials/page-body.hbs +48 -0
  125. package/site-theme/partials/page-list.hbs +14 -0
  126. package/site-theme/partials/related.hbs +24 -0
  127. package/site-theme/partials/search-meta.hbs +6 -0
  128. package/site-theme/partials/search-names.hbs +2 -0
  129. package/site-theme/partials/search.hbs +52 -0
  130. package/site-theme/partials/tags.hbs +4 -0
  131. package/site-theme/partials/toc.hbs +7 -0
  132. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  133. package/stage.mjs +1 -2
  134. package/types/config.d.mts +8 -4
  135. package/types/content-config.d.mts +15 -15
  136. package/types/engine/asset-shipping.d.mts +5 -2
  137. package/types/engine/base-compiler.d.mts +20 -2
  138. package/types/engine/content-address.d.mts +1 -1
  139. package/types/engine/content-alerts.d.mts +8 -2
  140. package/types/engine/content-blocks.d.mts +11 -4
  141. package/types/engine/content-expand.d.mts +288 -0
  142. package/types/engine/content-figures.d.mts +11 -3
  143. package/types/engine/content-glyphs.d.mts +85 -0
  144. package/types/engine/content-images.d.mts +6 -7
  145. package/types/engine/content-links.d.mts +1 -1
  146. package/types/engine/content-render.d.mts +199 -0
  147. package/types/engine/content-scripts.d.mts +62 -0
  148. package/types/engine/content-spans.d.mts +1 -1
  149. package/types/engine/content-styles.d.mts +33 -0
  150. package/types/engine/content-symbols.d.mts +35 -0
  151. package/types/engine/core-report.d.mts +45 -0
  152. package/types/engine/core-vocabulary.d.mts +53 -0
  153. package/types/engine/dictionary-html.d.mts +8 -0
  154. package/types/engine/field-reference.d.mts +1 -1
  155. package/types/engine/heading-attributes.d.mts +20 -0
  156. package/types/engine/helpers.d.mts +3 -10
  157. package/types/engine/homepage.d.mts +11 -24
  158. package/types/engine/html-minify.d.mts +9 -0
  159. package/types/engine/index.d.mts +2 -1
  160. package/types/engine/language-terms.d.mts +24 -2
  161. package/types/engine/logographic.d.mts +45 -0
  162. package/types/engine/map-lexicon.d.mts +48 -8
  163. package/types/engine/map-places.d.mts +8 -0
  164. package/types/engine/markdown-expressions.d.mts +0 -2
  165. package/types/engine/metre-declaration.d.mts +39 -0
  166. package/types/engine/pdf-render.d.mts +6 -2
  167. package/types/engine/post-fields.d.mts +44 -0
  168. package/types/engine/preview-index.d.mts +72 -0
  169. package/types/engine/preview-source.d.mts +51 -0
  170. package/types/engine/script-emphasis-terms.d.mts +21 -0
  171. package/types/engine/script-emphasis.d.mts +84 -0
  172. package/types/engine/script-font-delivery.d.mts +4 -4
  173. package/types/engine/site-build.d.mts +71 -113
  174. package/types/engine/site-config.d.mts +14 -134
  175. package/types/engine/site-files.d.mts +73 -0
  176. package/types/engine/site-index.d.mts +11 -3
  177. package/types/engine/site-preview.d.mts +15 -19
  178. package/types/engine/site-render.d.mts +171 -0
  179. package/types/engine/site-root.d.mts +12 -4
  180. package/types/engine/site-search.d.mts +54 -0
  181. package/types/engine/site-serve.d.mts +44 -0
  182. package/types/engine/site-theme-banner.d.mts +51 -0
  183. package/types/engine/site-theme-urls.d.mts +60 -0
  184. package/types/engine/site-theme.d.mts +127 -0
  185. package/types/engine/text-maps.d.mts +17 -3
  186. package/types/engine/typographer.d.mts +11 -0
  187. package/types/engine/verse-metre.d.mts +131 -0
  188. package/types/engine/wikilink-syntax.d.mts +30 -48
  189. package/types/engine/wikilinks.d.mts +330 -14
  190. package/types/engine/word-forms.d.mts +3 -2
  191. package/types/manifest.d.mts +3 -4
  192. package/types/stage.d.mts +1 -2
  193. package/types/vendor/markdownlint.d.mts +8 -0
  194. package/vendor/markdownlint.mjs +20388 -0
  195. package/engine/web-wikilinks.mjs +0 -594
  196. package/hugo-theme/layouts/404.html +0 -85
  197. package/hugo-theme/layouts/_default/baseof.html +0 -67
  198. package/hugo-theme/layouts/_default/list.html +0 -139
  199. package/hugo-theme/layouts/_default/single.html +0 -4
  200. package/hugo-theme/layouts/blog/list.html +0 -54
  201. package/hugo-theme/layouts/blog-post/single.html +0 -36
  202. package/hugo-theme/layouts/index.html +0 -75
  203. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  204. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  205. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  206. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  207. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  208. package/hugo-theme/layouts/partials/header.html +0 -35
  209. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  210. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  211. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  212. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  213. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  214. package/hugo-theme/layouts/partials/infobox.html +0 -57
  215. package/hugo-theme/layouts/partials/page-body.html +0 -152
  216. package/hugo-theme/layouts/partials/related.html +0 -138
  217. package/hugo-theme/layouts/partials/search.html +0 -84
  218. package/hugo-theme/layouts/partials/site-url.html +0 -32
  219. package/hugo-theme/layouts/partials/tags.html +0 -30
  220. package/hugo-theme/layouts/partials/toc.html +0 -28
  221. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  222. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  223. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  224. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  225. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  226. package/hugo-theme/theme.toml +0 -11
  227. package/types/engine/web-wikilinks.d.mts +0 -107
  228. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  229. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  230. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  231. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  232. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  233. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -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}`;