@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
@@ -1,594 +0,0 @@
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
- * Wikilink resolution for the knowledgebase build.
16
- *
17
- * The same authored links the pack compilers turn into Foundry `@UUID` enrichers
18
- * (see `./wikilinks.mjs`) become site-local hrefs here:
19
- *
20
- * `[[type-shortcode|Text]]` → `[Text](/section/slug/)`
21
- * `[[type-shortcode|]]` → the same, showing the target's own name
22
- * `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
23
- * `[[#slug|Text]]` → `[Text](#slug)`
24
- *
25
- * **Every link is an address and carries a label**. One written without
26
- * a label addresses nothing and is reported — see
27
- * {@link unlabelledLinkMessage}, which states the rule for this build and the
28
- * pack build together.
29
- *
30
- * The KB *section* is not always the type: prose pages (`type: doc`) route by
31
- * their `category`, so `doc/quickstart` lands on `/userguide/sohl-quickstart/`.
32
- * The caller supplies that mapping already resolved, in the index it builds.
33
- *
34
- * Lives here rather than in a consumer so every package resolves a link the
35
- * same way. `sohl-thalorna` carried a forked copy of this as
36
- * `utils/site-wikilinks.mjs`, already 3 KB adrift from the original, which is
37
- * exactly the drift one rule with two implementations produces.
38
- */
39
-
40
- // How an address *parses* is the pack build's own rule, so the two builds
41
- // cannot drift apart on it: they disagreed once over the unlabelled hyphen
42
- // form, which the packs showed as a raw shortcode and the knowledgebase as a
43
- // name.
44
- import { collectAnchors } from "./anchors.mjs";
45
- import { presentWikilink, readWikilink } from "./wikilink-syntax.mjs";
46
- import { readQualifier } from "./wikilinks.mjs";
47
- import { replaceOutsideCode } from "./code-fences.mjs";
48
- // The canonical `package-system-type-shortcode` key, so a package-qualified
49
- // address is matched the way a vendored manifest publishes it — by the
50
- // segments the target supplies, with the system wildcarded unless stated.
51
- import { canonicalKey, expandAddress, readCanonicalKey } from "./content-address.mjs";
52
- import { NOTE_SYSTEM } from "./systems.mjs";
53
- // The one rule about a link's shape both builds share: it carries a label, and
54
- // {@link unlabelledLinkMessage} is the one place that says so.
55
- import { unlabelledLinkMessage } from "./wikilink-syntax.mjs";
56
- // One slug rule for the whole build — see `./content-slug.mjs`. This module
57
- // carried a copy that dropped non-ASCII letters rather than transliterating
58
- // them, so a link to a heading named `Kûrbúl Helm` pointed at `#k-rb-l-helm`.
59
- import { slugify } from "./content-slug.mjs";
60
-
61
- // Re-exported so a site build keeps one import path for the whole of link
62
- // resolution: the same rule that names a page also names an anchor within it.
63
- /** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
64
- export { slugify };
65
- import {
66
- authoredLabel,
67
- WIKILINK,
68
- isEventAnchor,
69
- isSamePage,
70
- parseWikilink,
71
- } from "./wikilink-syntax.mjs";
72
- import { resolveEmbeds } from "./content-embeds.mjs";
73
-
74
- /**
75
- * Whether a page's declared anchors include the slug a link names.
76
- *
77
- * A local page carries a `Set` built by {@link module:engine/site-index}.
78
- * A foreign page declares prose anchors in its note metadata, independently
79
- * of whether it has a Foundry target map. An empty collection declares none.
80
- * An absent collection leaves the target unchecked.
81
- *
82
- * @param {Set<string>|Record<string, string>|Array<{slug: string, kind: string}>|undefined} anchors - The page's
83
- * declared anchors, or `undefined` where none were recorded.
84
- * @param {string} slug - The anchor a link names.
85
- * @returns {boolean} Whether the anchor is declared, or `true` when `anchors`
86
- * is `undefined` — which reads as "nothing to check against" rather than
87
- * "declares none".
88
- */
89
- function hasAnchor(anchors, slug) {
90
- if (!anchors) return true;
91
- if (Array.isArray(anchors))
92
- return anchors.some((anchor) => anchor.kind === "prose" && anchor.slug === slug);
93
- return anchors instanceof Set ? anchors.has(slug) : Object.hasOwn(anchors, slug);
94
- }
95
-
96
- /**
97
- * The index key a **piped** target resolves to, or `null` when it does not
98
- * parse as an address at all.
99
- *
100
- * The KB index is keyed by the canonical `type/shortcode`, so a target written
101
- * in the hyphen separator — which is what the content tree authors —
102
- * has to be rewritten to it before lookup. The target is read by the pack
103
- * build's own {@link readQualifier}, so recognising an address and resolving
104
- * one can never disagree: the two separators and the optional leading package
105
- * segment are stated once, there.
106
- *
107
- * It takes the **parsed** qualifier rather than the raw target because the
108
- * caller needs the parse for a second purpose: `unknown-type` and
109
- * `not-an-address` are different findings with different fixes, and only the
110
- * `reason` tells them apart. Reading the target twice would let the two
111
- * readings drift.
112
- *
113
- * The build indexes an item note under both `skill/climb` and `docskill/climb`,
114
- * and `contentTypes` carries both qualifiers, so either form finds the page.
115
- *
116
- * @param {Map<string, object>} index - The page index to look in.
117
- * @param {object|null} read - From {@link readQualifier}.
118
- * @param {string} contentPackage - The citing package, which an omitted package
119
- * segment defaults from.
120
- * @returns {object|undefined} The indexed page, or `undefined` when the target
121
- * is not an address or nothing publishes it.
122
- */
123
- function lookupRead(index, read, contentPackage) {
124
- if (!read || read.reason) return undefined;
125
- // Every omitted segment defaults from where the link is written —
126
- // package from the citing package, system from the block, which on a page
127
- // body is `none` — so the target expands to exactly one canonical address
128
- // and this is a plain lookup. No filter, no single-hit rule, and no
129
- // ambiguity: one key names one entry.
130
- //
131
- // It replaced a system-blind short key for the unqualified form, which
132
- // could be silently overwritten by a second note of the same
133
- // `(type, shortcode)` under another system.
134
- const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
135
- const found = index.get(key);
136
- if (found || read.package) return found;
137
-
138
- // A local target may also be keyed by its short form. On the KB that is not
139
- // a second answer: an item note renders as **one page** which is its own
140
- // documentation, so `skill/climb`, `docskill/climb` and both canonical
141
- // addresses are all the same value — the fallback cannot pick
142
- // differently, only earlier. It stays because an index built before the
143
- // canonical documentation key was added still carries the short one, and
144
- // because a page collision here is caught by the site index's own
145
- // `ambiguous` set rather than by silent overwrite as in the pack index.
146
- return (
147
- index.get(`${read.itemDoc ? "doc" : ""}${read.type}/${read.shortcode}`.toLowerCase()) ??
148
- undefined
149
- );
150
- }
151
-
152
- /**
153
- * How an **unresolved** link renders.
154
- *
155
- * The author's text remains readable. The website uses an HTML span matching
156
- * the pack compiler's cue; the book uses text that Typst can print.
157
- *
158
- * The knowledgebase renders with `unsafe = true` (`kb/hugo.toml`), so raw HTML
159
- * in generated markdown reaches the page. That makes escaping obligatory: this
160
- * is the one path where *authored* text becomes markup rather than content.
161
- *
162
- * @param {string} text - The text to show, from the link's label or target.
163
- * @param {string} target - The address that resolved nowhere, for the tooltip.
164
- * @param {"html"|"book"} output - The destination format.
165
- * @returns {string} A visible cue in the destination's Markdown.
166
- */
167
- function unresolvedLink(text, target, output) {
168
- if (output === "book") return `${text} (unresolved link)`;
169
- const esc = (v) =>
170
- String(v)
171
- .replace(/&/g, "&amp;")
172
- .replace(/</g, "&lt;")
173
- .replace(/>/g, "&gt;")
174
- .replace(/"/g, "&quot;");
175
- return (
176
- `<span class="sohl-unresolved-link" title="Unresolved link: ` +
177
- `${esc(target)}">${esc(text)}</span>`
178
- );
179
- }
180
-
181
- /**
182
- * How the website marks a link to a **draft** note.
183
- *
184
- * A note tagged `draft` exists so a link into it is not dead, and nothing more.
185
- * Unmarked, a reader follows a promising link into an empty page and an author
186
- * cannot see which of their links still owe content.
187
- *
188
- * The link itself remains intact inside the HTML span.
189
- *
190
- * The appearance lives in the Hugo theme for the website and in
191
- * `scss/components/_draft-link.scss` for Foundry, not here.
192
- *
193
- * The website span matches the pack compiler's copy in `wikilinks.mjs`.
194
- *
195
- * The argument is already-built markup and is deliberately not escaped; the
196
- * *authored* text inside it was escaped, or made into a link, by the caller.
197
- *
198
- * @param {string} inner - The resolved link, as this build emits it.
199
- * @returns {string} An inline HTML span wrapping it.
200
- */
201
- function draftLink(inner) {
202
- return `<span class="sohl-draft-link" title="Draft — not yet written">${inner}</span>`;
203
- }
204
-
205
- /**
206
- * A wikilink, as it is written, anywhere in a value that is not markdown.
207
- *
208
- * Deliberately its own pattern rather than the body resolver's: nothing here is
209
- * markdown, so there is no fence or code span to step around, and a frontmatter
210
- * value is a single line by construction (`[^\]\n]` keeps a runaway match from
211
- * swallowing the rest of a folded scalar).
212
- */
213
- // The same syntax the body scan reads — see `./wikilink-syntax.mjs`.
214
- const FRONTMATTER_WIKILINK = new RegExp(WIKILINK.source, "g");
215
-
216
- /**
217
- * Every wikilink authored inside a frontmatter value.
218
- *
219
- * Wikilinks are resolved in a note's **body** — by {@link resolveWebWikilinks}
220
- * here, and by the pack compilers' `convertWikilinks` for Foundry. Frontmatter
221
- * is not markdown and is never walked by either, so a link written in one is
222
- * copied through verbatim and reaches the reader as literal `[[…]]` text, in
223
- * whatever the theme renders that field as (an infobox row, a description, a
224
- * card subtitle). Nothing downstream notices: the value is a valid string, the
225
- * page builds, and the defect is visible only to someone who looks at it.
226
- *
227
- * So the form is refused rather than resolved. Resolving it would mean choosing
228
- * an output syntax for a field whose renderer is unknown to this build — a
229
- * markdown link is inert in a Hugo template that prints the value as text, and
230
- * an `<a>` is unusable in one that escapes it — and would quietly bless an
231
- * authoring habit that the pack build has no way to honour at all. Frontmatter
232
- * carries data; a link belongs in prose.
233
- *
234
- * Values are read from the *parsed* frontmatter, so a `[[` inside a YAML comment
235
- * is not a hit, and every hit can be named by the path a reader would look at.
236
- *
237
- * @param {unknown} fm - Parsed frontmatter, as `gray-matter` returns it.
238
- * @returns {Array<{path: string, link: string}>} In reading order; `path` is the
239
- * dotted key path of the offending value (`government.summary`, `aliases.1`).
240
- */
241
- export function frontmatterWikilinks(fm) {
242
- const hits = [];
243
- const visit = (value, trail) => {
244
- if (typeof value === "string") {
245
- for (const m of value.matchAll(FRONTMATTER_WIKILINK)) {
246
- hits.push({ path: trail, link: m[0] });
247
- }
248
- } else if (Array.isArray(value)) {
249
- value.forEach((v, i) => visit(v, `${trail}.${i}`));
250
- } else if (isPlainMap(value)) {
251
- for (const [k, v] of Object.entries(value)) {
252
- visit(v, trail ? `${trail}.${k}` : k);
253
- }
254
- }
255
- };
256
- if (!isPlainMap(fm)) return hits;
257
- visit(fm, "");
258
- return hits;
259
- }
260
-
261
- /**
262
- * Whether a value is a YAML mapping rather than a scalar the parser built into
263
- * an object of its own (a `Date`, which is what an unquoted date becomes).
264
- *
265
- * @param {unknown} value
266
- * @returns {boolean}
267
- */
268
- function isPlainMap(value) {
269
- return (
270
- typeof value === "object" &&
271
- value !== null &&
272
- !Array.isArray(value) &&
273
- !(value instanceof Date)
274
- );
275
- }
276
-
277
- /**
278
- * Rewrites the wikilinks in a markdown body as KB-local markdown links.
279
- *
280
- * **Every target is an address**, parsed by {@link readQualifier} and looked up
281
- * case-insensitively in the KB-wide `ctx.index` (the canonical
282
- * `package-system-type-shortcode`, `type/shortcode`, and the site's own
283
- * `section/slug`), then in the vendored `ctx.foreign` manifests. A target is a
284
- * *partial* address: an omitted package means this package, an omitted system
285
- * is a wildcard, and exactly one match resolves — two are `ambiguous`. A link written
286
- * without a label addresses nothing at all and is reported as such —
287
- * there is no second namespace left for it to name.
288
- *
289
- * Only a slash-qualified target reaches the raw key, which is what keeps
290
- * `section/slug` addressable without a page's own slug answering for it.
291
- *
292
- * **Every target that resolves nowhere fails the build**, and is
293
- * classified into the vocabulary all three resolvers share — `unlabelled`,
294
- * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
295
- * `no-content-index`. Failures are collected in `ctx.errors`, each carrying
296
- * the authored `link` and its `occurrence` so a caller can report the line and
297
- * column it sits on.
298
- *
299
- * There is deliberately no exception letting a hyphen-form address through while
300
- * any linkable package had no vendored manifest, since a real cross-package
301
- * reference and a typo look identical from here. The pack compilers and the
302
- * link checker never made that allowance, so its only surviving effect was to
303
- * give one authored link two verdicts. The advice moved into the message
304
- * instead.
305
- *
306
- * A target that resolves nowhere still renders through {@link unresolvedLink}
307
- * rather than as bare prose: the author's text is kept, marked so a
308
- * reader can see a link was intended. The marking and the failure are separate
309
- * jobs and always were — the mark is for whoever reads the page a *previous*
310
- * build emitted, the failure is for the author of this one.
311
- *
312
- * A target that **resolved** to an entry with no page is not this case and is
313
- * not marked: a pack-only package publishes Foundry addresses and no
314
- * web pages, so the author wrote a real address and there is simply nothing to
315
- * link to.
316
- *
317
- * ## Embeds first, and they are not links
318
- *
319
- * `![[address|label]]` names a file rather than a note, so it is resolved into
320
- * an ordinary markdown image before anything looks for a link — which is what
321
- * stops the link pass reading an embed's interior as a link to a note nobody
322
- * wrote. The image the rewrite leaves behind is what
323
- * {@link module:engine/content-images.renderImageFigures} turns into a figure
324
- * for the website, and what the book reads its staging list out of.
325
- *
326
- * ## Every hit is recorded, so the page's edges come out of one pass
327
- *
328
- * A caller that hands in `ctx.resolved` is given every index entry a link on
329
- * the page resolved to, in body order, repeats included. That is the link
330
- * graph, read at the one point where it exists: the build never has to walk a
331
- * page a second time to learn what it links to. Nothing is recorded for a
332
- * same-page anchor or for a target that resolved nowhere.
333
- *
334
- * @param {string} body - The markdown body.
335
- * @param {object} ctx - `{ index, assets, collide, contentTypes,
336
- * packages, noIndexPackages, foreign, type, errors, src, file, resolved,
337
- * output }`. `output` is `"book"` for PDF Markdown and defaults to HTML.
338
- * `packages` is every package an address may name, without which the leading
339
- * package segment of a canonical address reads as an unknown type;
340
- * `noIndexPackages` is every package declared `contentIndex: false`, so a
341
- * qualified address naming one fails with `no-content-index` rather than
342
- * `not-an-address`; `foreign`
343
- * is the cross-package manifest index; `assets` is the address space an embed
344
- * resolves against. `src` is the page's display
345
- * path and `file` the source file a diagnostic should name — absent, `src`
346
- * stands in. `resolved`, when supplied, is the array every resolved
347
- * target's index entry is appended to. `anchors` is the `{#slug}` set
348
- * *this* page declares, which a `[[#slug]]` self-link is checked against;
349
- * absent, a self-link is not checked, exactly as before this existed.
350
- * @returns {string} The body with embeds and wikilinks rewritten.
351
- */
352
- export function resolveWebWikilinks(body, ctx) {
353
- // How many times each authored link has been seen, so two identical links
354
- // on one page are located at their own positions in the source file — the
355
- // same counting the checker does, and what turns a finding into a
356
- // `file:line:column:` diagnostic rather than a note-wide one.
357
- const seen = new Map();
358
- let ownAnchors = ctx.noteAnchors;
359
- /**
360
- * Records a finding against one authored literal.
361
- *
362
- * @param {string} all - The literal, exactly as authored.
363
- * @param {object} finding - The finding's own fields.
364
- * @returns {void}
365
- */
366
- const record = (all, finding) => {
367
- const occurrence = (seen.get(all) ?? 0) + 1;
368
- seen.set(all, occurrence);
369
- ctx.errors.push({
370
- // Absolute where the caller supplied one, so a diagnostic locates
371
- // the file an editor opens; the display path otherwise.
372
- file: ctx.file ?? ctx.src,
373
- src: ctx.src,
374
- link: all,
375
- occurrence,
376
- ...finding,
377
- });
378
- };
379
-
380
- /**
381
- * Records a finding, and returns the marked-up link it renders as.
382
- *
383
- * @param {string} all - The authored link, brackets and all.
384
- * @param {object} finding - `{ target, reason }`, plus any extras the class
385
- * carries.
386
- * @param {string} text - What the link renders as.
387
- * @returns {string} The marked span.
388
- */
389
- const report = (all, finding, text) => {
390
- record(all, finding);
391
- return unresolvedLink(text, finding.target, ctx.output ?? "html");
392
- };
393
-
394
- // An embed names a file, so it resolves before anything looks for a link.
395
- // Its findings are recorded rather than rendered: the rewrite already
396
- // leaves an embed it cannot resolve exactly as authored, which is the
397
- // visible degradation an unresolved link gets from `unresolvedLink`.
398
- const embedded = resolveEmbeds(body, { index: ctx.assets });
399
- for (const u of embedded.unresolved) {
400
- record(u.link, { target: u.target, reason: u.reason, ...(u.type ? { type: u.type } : {}) });
401
- }
402
- for (const problem of embedded.problems) {
403
- record(problem.link, { target: problem.link, message: problem.message });
404
- }
405
-
406
- // Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
407
- // inline span is source text, not a link.
408
- return replaceOutsideCode(embedded.markdown, WIKILINK, (all, rawInner) => {
409
- const parsed = readWikilink(
410
- rawInner,
411
- {
412
- package: ctx.contentPackage,
413
- system: "note",
414
- types: ctx.contentTypes ?? new Set(),
415
- packages: ctx.packages,
416
- noIndexPackages: ctx.noIndexPackages,
417
- },
418
- all,
419
- );
420
- const { rawTarget: target, anchor, display } = parsed;
421
- // An empty label is not a label: `[[x|]]` addresses the target and
422
- // shows its name, so `""` falls through to the same place `null` does.
423
- // One reading, from {@link authoredLabel}.
424
- const label = authoredLabel({ display });
425
- if (parsed.attributes?.problems.length)
426
- return report(
427
- all,
428
- { target, reason: "invalid-attributes", problems: parsed.attributes.problems },
429
- label ?? target,
430
- );
431
-
432
- // **Every link carries a label**. Without one there is nothing
433
- // to resolve against, and nothing to show either, a shortcode being an
434
- // address rather than prose. Reported before the same-page form, because the
435
- // rule is about how the link is *written*: `[[#slug]]` needs the pipe
436
- // exactly as `[[skill-clmb]]` does.
437
- if (!parsed.labelled) {
438
- return report(all, { target: parsed.inner, reason: "unlabelled" }, parsed.inner);
439
- }
440
-
441
- // `[[#section-slug|Text]]` — a section of this same page.
442
- if (isSamePage({ target, anchor })) {
443
- // Checked against this page's own anchors, exactly as a link into
444
- // another page is: a self-link naming a section the page does not
445
- // declare is as dead as one naming a section nowhere does.
446
- if (isEventAnchor(ctx, anchor)) {
447
- return report(
448
- all,
449
- { target, reason: "event-anchor", anchor, addressed: true },
450
- label ?? anchor,
451
- );
452
- }
453
- if (!hasAnchor(ctx.anchors, anchor)) {
454
- return report(
455
- all,
456
- { target, reason: "unknown-anchor", anchor, addressed: true },
457
- label ?? anchor,
458
- );
459
- }
460
- return presentWikilink(
461
- `[${label ?? ctx.captionLabels?.get(anchor) ?? anchor}](#${slugify(anchor)})`,
462
- parsed,
463
- { noteAnchors: (ownAnchors ??= collectAnchors(body)) },
464
- );
465
- }
466
-
467
- // The canonical separator has to be resolved, not merely
468
- // recognised. `null` here means the target is not an address at all,
469
- // which is a defect: there is no other namespace to try.
470
- const read = parsed.problem ?? parsed.target;
471
- const rawKey = target.toLowerCase();
472
- const hit =
473
- lookupRead(ctx.index, read, ctx.contentPackage) ??
474
- // A manifest entry carries the same `{ url, name }` shape as a
475
- // local one, so a cross-package hit needs no special case
476
- // below. Local wins: a live build is authoritative and a vendored
477
- // manifest can only be staler.
478
- // A short form names *this* package, so it never reaches a
479
- // vendored manifest; only a fully qualified address does.
480
- (ctx.foreign && read?.package ?
481
- lookupRead(ctx.foreign, read, ctx.contentPackage)
482
- : undefined);
483
- if (hit) {
484
- // A `#section` the target declares no heading for. Checked only
485
- // where there is a page to check against — a pack-only hit has no
486
- // `url` and therefore nothing for the anchor to address, which is
487
- // the asset-like case this must not touch. `anchorPageId` on the
488
- // pack build and the KB manifest's own `{slug: uuid}` map both
489
- // hash any slug into something that resolves, so an undeclared
490
- // one otherwise joins onto the URL unchecked and dead-ends for
491
- // the reader.
492
- if (anchor && isEventAnchor(hit, anchor)) {
493
- return report(
494
- all,
495
- { target, reason: "event-anchor", anchor, addressed: true },
496
- label ?? hit.name ?? target,
497
- );
498
- }
499
- if (anchor && !hasAnchor(hit.noteAnchors ?? hit.anchors, anchor)) {
500
- return report(
501
- all,
502
- { target, reason: "unknown-anchor", anchor, addressed: true },
503
- label ?? hit.name ?? target,
504
- );
505
- }
506
- // The edge, for a caller reading the link graph off this pass.
507
- ctx.resolved?.push(hit);
508
- // An address with an *empty* label has no prose to show (a
509
- // shortcode is not display text), so the document's **current**
510
- // name stands in and a rename shows at every citation.
511
- const text = label ?? hit.name;
512
- // A pack-only package publishes Foundry addresses and no pages,
513
- // so its entries carry no `path` and resolve to no URL.
514
- // The address is real — this is not a typo and must not fail the
515
- // build — but there is nothing on the web to point at, so the
516
- // reader gets the text and no href. Emitting the href anyway is
517
- // what the manifest exists to prevent: `[Name](undefined)` renders
518
- // as a link and goes nowhere.
519
- const link =
520
- hit.url ? `[${text}](${anchor ? `${hit.url}#${slugify(anchor)}` : hit.url})` : text;
521
- // A link into a note that exists but is not written renders marked.
522
- // Presentation only — the href above is unchanged, and a
523
- // `[[#anchor]]` self-link is not marked because the reader is
524
- // already on the page it would be telling them about.
525
- return presentWikilink(
526
- !hit.draft ? link
527
- : ctx.output === "book" ? `${link} (draft)`
528
- : draftLink(link),
529
- parsed,
530
- hit,
531
- );
532
- }
533
-
534
- // **An address resolving nowhere is a failure, unconditionally**.
535
- //
536
- // It was gated on a manifest-completeness check — while any linkable package was
537
- // invisible here, `Rules/Bestiary.md` addressing `being-grkrahk` in the
538
- // `thalorna` package was indistinguishable from a typo, so the form
539
- // stayed unguarded rather than fail correct content. The gate has
540
- // outlived that: every linkable package publishes a manifest now, and
541
- // more to the point the *other two* resolvers never had it. The pack
542
- // compilers fail the note and the link checker reports an error, so the
543
- // gate's only remaining effect was to give one authored link two
544
- // verdicts depending on which build read it. The advice a missing
545
- // manifest calls for is in the message instead — {@link
546
- // unresolvedAddressMessage} names both corrections — which informs the
547
- // author without excusing the link.
548
- // The bucket an ambiguity is recorded under is the *short* address —
549
- // the only form two packages can collide on, since a qualified one
550
- // names its package. A qualified target that matched more than one
551
- // document is reported by the matcher instead, not by this set.
552
- const collideKey =
553
- read && !read.reason && read.package === ctx.contentPackage ?
554
- `${read.type}/${read.shortcode}`.toLowerCase()
555
- : rawKey;
556
- // A capital in the shortcode is refused, never lowercased. The
557
- // lowercase spelling is looked up only to name it in the finding.
558
- if (read?.reason === "shortcode-not-lowercase") {
559
- const lower = read.lowercase;
560
- const declared =
561
- lookupRead(ctx.index, lower, ctx.contentPackage) ??
562
- (ctx.foreign && lower.package ?
563
- lookupRead(ctx.foreign, lower, ctx.contentPackage)
564
- : undefined);
565
- return report(
566
- all,
567
- {
568
- target,
569
- reason: "shortcode-not-lowercase",
570
- shortcode: read.shortcode,
571
- ...(declared ? { declared: lower.shortcode } : {}),
572
- },
573
- label ?? target,
574
- );
575
- }
576
- const reason =
577
- ctx.collide?.has(collideKey) ? "ambiguous"
578
- // "It parsed as an address" is a property of the parse, not of
579
- // a key: a partial address has no single key to be non-null.
580
- : read && !read.reason ? "unresolved"
581
- : read?.reason === "unknown-type" ? "unknown-type"
582
- : read?.reason === "no-content-index" ? "no-content-index"
583
- : read?.reason === "not-lowercase" ? "not-lowercase"
584
- // Every link is an address, and this is not one. Distinct from
585
- // a dead address, because the fix is different: a name has to
586
- // become an address, not be corrected.
587
- : "not-an-address";
588
-
589
- // Whether or not it failed the build, the link renders marked: the
590
- // author's text is kept so the sentence still reads, and a reader can
591
- // see that something was meant to be a link.
592
- return report(all, { target, reason }, label ?? target);
593
- });
594
- }
@@ -1,85 +0,0 @@
1
- {{- /*
2
- "Page not found" page.
3
-
4
- Hugo renders this template to `public/404.html`, and a static host such as
5
- Cloudflare Pages serves that file — with a genuine HTTP 404 — for any path
6
- the deployment does not publish. Without it, Pages falls back to the site
7
- root, so an unknown path answers 200 with the landing page: a soft-404 that
8
- makes every "does this URL resolve?" check report success, and lets search
9
- engines index withdrawn content as live.
10
-
11
- Every consumer needs exactly this, so it lives here rather than being copied
12
- into each one (issue #1454). The theme carries layout, not addresses, so the
13
- wording and the routes back are supplied by the consuming site:
14
-
15
- [params.notfound]
16
- heroimage = "images/banner.webp" # CDN-relative or absolute
17
- tagline = "This site has no page at"
18
- sitenoun = "site" # used in the body prose
19
- [[params.notfound.links]]
20
- title = "Home"
21
- url = "/" # relURL'd unless already absolute
22
- text = "Everything, from the top."
23
-
24
- Each is guarded: a consumer supplying none still builds, and simply gets the
25
- generic wording with no list of routes back.
26
- */ -}}
27
- {{ define "main" }}
28
- {{- $nf := .Site.Params.notfound | default dict -}}
29
- {{- $img := $nf.heroimage | default "images/banners/default.webp" -}}
30
- {{- $heroURL := partial "cdn-url.html" (dict "site" .Site "path" $img) -}}
31
- {{- $tagline := $nf.tagline | default "This site has no page at" -}}
32
- {{- $noun := $nf.sitenoun | default "site" -}}
33
- <section class="hero hero-with-image" style="background-image: url('{{ $heroURL }}');">
34
- <div class="hero-inner" data-pagefind-ignore>
35
- <h1>Page not found</h1>
36
- <p class="hero-tagline">{{ $tagline }} <code id="missing-path">the address you followed</code>.</p>
37
- </div>
38
- </section>
39
-
40
- <article class="single">
41
- <div class="single-content">
42
- <div class="single-body">
43
- <p>
44
- The page may have been moved to another part of the
45
- {{ $noun }}, or withdrawn along with the concept it described. Links
46
- from older discussions, issues, and search results can outlive the
47
- pages they point at.
48
- </p>
49
- {{- with $nf.links }}
50
- <p>Start again from one of these:</p>
51
- <ul class="page-list">
52
- {{- range . }}
53
- {{- $url := .url -}}
54
- {{- if not (or (hasPrefix $url "http://") (hasPrefix $url "https://") (hasPrefix $url "//")) -}}
55
- {{- /* relURL only mounts a path under the site's base when it is
56
- given relative: a leading slash means "from the origin", and
57
- the base path is left off. Trim it, so a consumer writing the
58
- obvious "/section/" gets a link inside its own site even when
59
- that site is served under a prefix. */ -}}
60
- {{- $url = strings.TrimPrefix "/" $url | relURL -}}
61
- {{- end }}
62
- <li class="page-list-item">
63
- <a class="page-list-title" href="{{ $url }}">{{ .title }}</a>
64
- {{- with .text }}
65
- <span class="page-list-desc">{{ . }}</span>
66
- {{- end }}
67
- </li>
68
- {{- end }}
69
- </ul>
70
- {{- end }}
71
- </div>
72
- </div>
73
- </article>
74
-
75
- {{/* Name the address that failed, so a reader can see what they followed and
76
- quote it in a bug report. Static hosting cannot template it — Pages serves
77
- this one file for every unmatched path — so it is filled in client-side,
78
- and the markup above reads sensibly without scripting. */}}
79
- <script>
80
- (function () {
81
- var el = document.getElementById("missing-path");
82
- if (el) el.textContent = window.location.pathname;
83
- })();
84
- </script>
85
- {{ end }}