@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,131 @@
1
+ /**
2
+ * The syllables of a word, each as the vowel group that makes it.
3
+ *
4
+ * @param {string} word - The word.
5
+ * @param {Iterable<string>} pairs - The vowel pairs that are one syllable.
6
+ * @returns {string[]} The nuclei, in order; none for a word with no vowel.
7
+ */
8
+ export function nucleiOf(word: string, pairs: Iterable<string>): string[];
9
+ /**
10
+ * The index of the stressed syllable of a word.
11
+ *
12
+ * @param {string} word - The word.
13
+ * @param {{two: string, longer: string, mark?: string}} rule - The language's
14
+ * `data.sounds.stress`.
15
+ * @param {Iterable<string>} pairs - The vowel pairs that are one syllable.
16
+ * @returns {number} The 0-based syllable; 0 for a word of one syllable or none.
17
+ */
18
+ export function stressIndex(word: string, rule: {
19
+ two: string;
20
+ longer: string;
21
+ mark?: string;
22
+ }, pairs: Iterable<string>): number;
23
+ /**
24
+ * Each configured language's metre book: one per language whose `note` names a
25
+ * language note stating a complete `data.sounds.stress`.
26
+ *
27
+ * @param {Iterable<Record<string, any>>|undefined} records - Index records.
28
+ * @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
29
+ * The resolved configuration.
30
+ * @returns {Map<string, MetreBook>} The books by language name.
31
+ */
32
+ export function metreBooksFromRecords(records: Iterable<Record<string, any>> | undefined, config?: {
33
+ languages?: Readonly<Record<string, {
34
+ tag: string;
35
+ note?: string;
36
+ }>>;
37
+ contentPackage?: string;
38
+ }): Map<string, MetreBook>;
39
+ /**
40
+ * Every metre the index's `doc` notes declare, by name. The first note to
41
+ * declare a name keeps it.
42
+ *
43
+ * @param {Iterable<Record<string, any>>|undefined} records - Index records.
44
+ * @returns {Map<string, import("./metre-declaration.mjs").Metre>} The metres.
45
+ */
46
+ export function metresFromRecords(records: Iterable<Record<string, any>> | undefined): Map<string, import("./metre-declaration.mjs").Metre>;
47
+ /**
48
+ * The beats a line of verse can have.
49
+ *
50
+ * @param {string} text - The line.
51
+ * @param {MetreBook} book - The line's language's book.
52
+ * @returns {{min: number, max: number}} The beats without the second beats a
53
+ * long word may take, and with all of them.
54
+ */
55
+ export function lineBeats(text: string, book: MetreBook): {
56
+ min: number;
57
+ max: number;
58
+ };
59
+ /**
60
+ * The warning for a line that cannot have its metre's beats.
61
+ *
62
+ * @param {{min: number, max: number}} beats - The line's beats.
63
+ * @param {import("./metre-declaration.mjs").Metre} metre - The fence's metre.
64
+ * @returns {string} The message.
65
+ */
66
+ export function metreMessage({ min, max }: {
67
+ min: number;
68
+ max: number;
69
+ }, metre: import("./metre-declaration.mjs").Metre): string;
70
+ /**
71
+ * Check the verse fences that state `scan=`.
72
+ *
73
+ * @param {ReadonlyArray<{start: number, end: number, indent: string, attributes: Record<string, string>, classes: string[]}>} blocks -
74
+ * The fences, from {@link module:engine/content-verse.scanVerse}.
75
+ * @param {string[]} lines - The source's lines.
76
+ * @param {{metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>, columnsClass: string, columnKeys: readonly string[], attributeColumns: (line: string, key: string) => {key: number, value: number}}} ctx -
77
+ * The metres and books, and how the fence says its columns and where its
78
+ * keys sit.
79
+ * @returns {{errors: object[], warnings: object[]}} The findings at 1-based
80
+ * positions.
81
+ */
82
+ export function metreFindings(blocks: ReadonlyArray<{
83
+ start: number;
84
+ end: number;
85
+ indent: string;
86
+ attributes: Record<string, string>;
87
+ classes: string[];
88
+ }>, lines: string[], ctx: {
89
+ metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>;
90
+ metreBooks?: ReadonlyMap<string, MetreBook>;
91
+ lexicons?: ReadonlyMap<string, unknown>;
92
+ columnsClass: string;
93
+ columnKeys: readonly string[];
94
+ attributeColumns: (line: string, key: string) => {
95
+ key: number;
96
+ value: number;
97
+ };
98
+ }): {
99
+ errors: object[];
100
+ warnings: object[];
101
+ };
102
+ /** The bases whose one-syllable words carry a beat. */
103
+ export const BEAT_BASES: readonly string[];
104
+ /**
105
+ * What a language's lines are counted by.
106
+ */
107
+ export type MetreBook = {
108
+ /**
109
+ * - The language's configured name.
110
+ */
111
+ language: string;
112
+ /**
113
+ * - Its stress rule.
114
+ */
115
+ rule: {
116
+ two: string;
117
+ longer: string;
118
+ mark?: string;
119
+ };
120
+ /**
121
+ * - Its vowel pairs.
122
+ */
123
+ pairs: ReadonlySet<string>;
124
+ /**
125
+ * - Each lexicon
126
+ * form, and each form its rules derive, folded, with the class bases of its
127
+ * entries.
128
+ */
129
+ classes: ReadonlyMap<string, ReadonlySet<string>>;
130
+ };
131
+ export { METRE_KINDS, METRE_NAME, checkMetresField } from "./metre-declaration.mjs";
@@ -36,12 +36,8 @@ export function parseWikilink(rawInner: string, literal?: string): ParsedWikilin
36
36
  * `[[x|]]` is labelled and `[[x]]` is not, and only the first addresses
37
37
  * anything.
38
38
  *
39
- * Stated here because the two resolvers had already drawn the line in two
40
- * places and drawn it differently: the packs tested falsiness and were right,
41
- * the web tested `??` — which falls through on `null` only — and emitted
42
- * `[](/url/)`, a link with no clickable text, through every build. That
43
- * is the same drift this module exists to prevent, in the case its own
44
- * {@link ParsedWikilink} docstring calls out. One reading, one place.
39
+ * Read with `??`, which falls through on `null` only, an empty label would
40
+ * emit `[](/url/)`: a link with no clickable text. One reading, one place.
45
41
  *
46
42
  * @param {{display: string|null}} parsed - A parsed wikilink, or anything
47
43
  * carrying its `display`.
@@ -53,7 +49,7 @@ export function authoredLabel({ display }: {
53
49
  /**
54
50
  * What an author writing an unlabelled link is told, in one place.
55
51
  *
56
- * Shared by the link checker, the pack compilers and the web resolver, because
52
+ * Shared by the link checker and the resolver, on every target, because
57
53
  * an author meets whichever runs first and they should read the same. It names
58
54
  * the form to write rather than a value to correct: there is no value that
59
55
  * makes an unlabelled link resolve.
@@ -99,7 +95,7 @@ export function isEventAnchor(hit: {
99
95
  *
100
96
  * An address naming no note is a typo or an omission: the intent of a
101
97
  * placeholder for a note nobody has written yet is spelled as a `draft`-tagged
102
- * note that exists, resolves and renders marked. Both want fixing, and all three builds say so.
98
+ * note that exists, resolves and renders marked. Both want fixing, and every build says so.
103
99
  *
104
100
  * @param {string} target - The address as authored, named in the message.
105
101
  * @returns {string} The message, unpunctuated at the end as a finding is.
@@ -143,9 +139,9 @@ export function ambiguousAddressMessage(target: string, packages?: Iterable<stri
143
139
  /**
144
140
  * The message for one link finding, whichever resolver found it.
145
141
  *
146
- * The single table the checker, the pack compilers and the web resolver all
147
- * report through, so one authored link cannot get three different explanations
148
- * of the same mistake depending on which build the author ran first. Callers
142
+ * The single table the checker and the resolver report through, on every
143
+ * target, so one authored link cannot get different explanations of the same
144
+ * mistake depending on which build the author ran first. Callers
149
145
  * add their own context around it — the note's name, the file it sits in — and
150
146
  * never their own wording for the defect.
151
147
  *
@@ -181,8 +177,9 @@ export function linkFindingMessage({ reason, target, packages, anchor, type, stu
181
177
  /**
182
178
  * Whether a parsed link addresses a section of the page it is written on.
183
179
  *
184
- * `[[#some-heading]]` — no target, only an anchor. Both resolvers special-case
185
- * it before consulting any index, because there is nothing to look up.
180
+ * `[[#some-heading]]` — no target, only an anchor. The resolver and the checker
181
+ * special-case it before consulting any index, because there is nothing to look
182
+ * up.
186
183
  *
187
184
  * @param {ParsedWikilink} parsed - A parsed wikilink.
188
185
  * @returns {boolean} True when the link is same-page.
@@ -212,36 +209,23 @@ export function presentWikilink(reference: string, parsed: object, destination:
212
209
  /**
213
210
  * What a `[[…]]` **is**, before anything decides where it points.
214
211
  *
215
- * One authored link compiles to two different addresses — a Foundry `@UUID`
216
- * enricher for the packs, a URL for the web — and those two destinations are the
217
- * *only* thing that legitimately differs. The syntax is the author's, and it is
218
- * the same syntax whichever build is reading it.
212
+ * One authored link is written three ways — a Foundry `@UUID` content link for
213
+ * the packs, a URL for the web, a cross-reference for the book — and those
214
+ * forms are the *only* thing that legitimately differs. The syntax is the
215
+ * author's, and it is the same syntax whichever build is reading it.
219
216
  *
220
- * It was written twice, and the two copies had already drifted:
217
+ * So the parse lives here, once, and two readers take it: the link checker
218
+ * (`content-links.mjs`) and the resolver (`wikilinks.mjs`). Each receives the
219
+ * parts and decides only what it is for: which document the address names.
221
220
  *
222
- * | Body | `foundry` side | `web` side |
223
- * | --- | --- | --- |
224
- * | `[[weapongear-bsw` ⏎ `]]` in a table cell | not a link | a link whose target ends in a newline |
225
- * | a stray `[[`, a real link two paragraphs later | finds the real link | swallows both paragraphs as one target |
226
- *
227
- * The web side's pattern omitted `\n` from the excluded set, so an unclosed
228
- * bracket consumed everything up to the next `]]` anywhere in the document —
229
- * the same shape of corruption a hand-rolled code-fence regex caused on the one
230
- * page whose subject is link syntax. It was also internally
231
- * inconsistent: its *frontmatter* scan excluded newlines while its body scan did
232
- * not.
233
- *
234
- * So the parse lives here, once. A resolver receives the parts and decides only
235
- * what it is actually for: which document the address names.
236
- *
237
- * The one rule about a link's *shape* that both resolvers share also lives here
238
- * — **a link carries a label** ({@link unlabelledLinkMessage}) — because the
239
- * two state it through one message and the author reads the same words
240
- * whichever ran first.
221
+ * The one rule about a link's *shape* also lives here — **a link carries a
222
+ * label** ({@link unlabelledLinkMessage}) — because every build states it
223
+ * through one message and the author reads the same words whichever ran
224
+ * first.
241
225
  *
242
226
  * For the same reason, so does the **vocabulary of link findings**
243
227
  * ({@link LINK_FINDING_REASONS}) and the message each one reports through
244
- * ({@link linkFindingMessage}). Three builds read one authored link; an author
228
+ * ({@link linkFindingMessage}). Every build reads one authored link; an author
245
229
  * meets whichever ran first, and a consumer switching on a `reason` should not
246
230
  * be switching on which build produced it.
247
231
  *
@@ -258,20 +242,18 @@ export function presentWikilink(reference: string, parsed: object, destination:
258
242
  * **A leading `!` makes it an embed, not a link**, and the lookbehind is what
259
243
  * keeps every reader from seeing one as the other. Stated here rather than in
260
244
  * each reader for the reason the rest of this module exists: the checker, the
261
- * pack compilers and the web resolver all match on this pattern, and three
262
- * copies of the exclusion is three places for it to be forgotten. An embed's
245
+ * resolver and the embed reader all match on this pattern, and a copy of the
246
+ * exclusion in each is a place for it to be forgotten. An embed's
263
247
  * own grammar is {@link module:engine/content-embeds}.
264
248
  */
265
249
  export const WIKILINK: RegExp;
266
250
  /**
267
- * Every way a link can fail, named once for all three resolvers.
268
- *
269
- * A link is read in three places — the checker (`content-links.mjs`), the pack
270
- * compilers (`wikilinks.mjs`) and the web resolver (`web-wikilinks.mjs`) — and
271
- * each would otherwise name the failures in its own words. `unknown` in one is
272
- * `unresolved` in another and `broken type/shortcode` in the third, so a
273
- * consumer switching on a `reason` was switching on which build had produced
274
- * it. The set is closed and lives here, beside the syntax the three share.
251
+ * Every way a link can fail, named once for every reader of a link.
252
+ *
253
+ * A link is read by the checker (`content-links.mjs`) and by the resolver
254
+ * (`wikilinks.mjs`) for every target, and each names a failure by one of these
255
+ * reasons, so a consumer switching on a `reason` never switches on which build
256
+ * produced it. The set is closed and lives here, beside the syntax they share.
275
257
  *
276
258
  * - `unlabelled` — no `|`, so the link addresses nothing.
277
259
  * - `not-an-address` — labelled, but the target does not parse as an address.
@@ -69,19 +69,86 @@ export function buildWikilinkIndex(docs: Array<{
69
69
  types: Set<string>;
70
70
  };
71
71
  /**
72
- * Rewrites every wikilink in a markdown body as a Foundry UUID enricher.
72
+ * Resolve every wikilink in a markdown body, and write each in one target's
73
+ * form.
73
74
  *
74
- * A link that cannot be resolved is left exactly as it was and reported in
75
- * `unresolved`, so a content gap degrades to visible literal text rather than
76
- * a broken link or a failed build.
75
+ * The one link resolver. The two indexes a link is read against differ, and
76
+ * only that differs, so the index is a {@link LinkView} and the target a
77
+ * writer: {@link convertWikilinks} and {@link resolveWebWikilinks} are this,
78
+ * with their own view and writer.
77
79
  *
78
80
  * **Code is verbatim.** A `[[…]]` inside a fenced or indented code block, or
79
81
  * inside an inline code span, is source text an author wrote to be read as
80
- * written, so it is left alone and not reported. Without that, a
81
- * script sample containing `grid[[0]]` became a link — and only for some
82
- * array shapes, `[[1,2],[3,4]]` having an inner `]` the pattern cannot cross,
83
- * so the corruption looked arbitrary. It reaches the reader through the
84
- * *documented* copy of a macro while the executable copy stays correct.
82
+ * written, so it is left alone and not reported. Without that, a script sample
83
+ * containing `grid[[0]]` would become a link — and only for some array shapes,
84
+ * `[[1,2],[3,4]]` having an inner `]` the pattern cannot cross.
85
+ *
86
+ * **Every failure is a finding**, classified into the vocabulary
87
+ * {@link LINK_FINDING_REASONS} names for every reader of a link. A finding
88
+ * keeps the link's `offset` in `markdown` and its `occurrence` — the how-many-th
89
+ * finding on that literal — so a caller can report the line and column it sits
90
+ * on by either.
91
+ *
92
+ * @param {string} markdown - The body.
93
+ * @param {object} opts
94
+ * @param {LinkView} opts.view - The index the links resolve against.
95
+ * @param {(link: ResolvedLink) => string} opts.write - The target's writer,
96
+ * from {@link LINK_WRITERS}.
97
+ * @param {Map<string, string>} [opts.captionLabels] - Numbered caption labels.
98
+ * @returns {{markdown: string, links: ResolvedLink[], findings: Array<{link:
99
+ * string, offset: number, occurrence: number, target: string, reason:
100
+ * string}>, resolved: object[]}} The written body; every link in body order;
101
+ * the findings; and every destination a link outside its own page resolved
102
+ * to, in body order, repeats included — the page's edges.
103
+ */
104
+ export function resolveWikilinks(markdown: string, { view, write, captionLabels }: {
105
+ view: LinkView;
106
+ write: (link: ResolvedLink) => string;
107
+ captionLabels?: Map<string, string> | undefined;
108
+ }): {
109
+ markdown: string;
110
+ links: ResolvedLink[];
111
+ findings: Array<{
112
+ link: string;
113
+ offset: number;
114
+ occurrence: number;
115
+ target: string;
116
+ reason: string;
117
+ }>;
118
+ resolved: object[];
119
+ };
120
+ /**
121
+ * The pack compile's view of a link: {@link buildWikilinkIndex}'s index,
122
+ * answered with compendium UUIDs.
123
+ *
124
+ * `[[type-shortcode|Text]]` defaults to the readable `note` Address. A stated
125
+ * game system names an Actor or Item; `none` names a systemless document.
126
+ * `doc<type>` names the readable note of an item. A journal link may carry a
127
+ * heading anchor, which addresses the journal's page for it; an Actor, Item or
128
+ * Macro link opens a sheet, which has no sections, so its anchor addresses
129
+ * nothing and is dropped.
130
+ *
131
+ * @param {object} index - From {@link buildWikilinkIndex}.
132
+ * @param {object} source - The note the links are written on.
133
+ * @param {string} source.type - Its type.
134
+ * @param {string} source.id - Its document id.
135
+ * @param {string} [source.pack] - The pack its own document lands in.
136
+ * @param {string} [source.docPack] - The pack its documentation entry lands in.
137
+ * @returns {LinkView} The view.
138
+ */
139
+ export function packLinkView(index: object, { type, id, pack, docPack }: {
140
+ type: string;
141
+ id: string;
142
+ pack?: string | undefined;
143
+ docPack?: string | undefined;
144
+ }): LinkView;
145
+ /**
146
+ * Rewrites every wikilink in a markdown body as a Foundry UUID enricher — see
147
+ * {@link resolveWikilinks}, with the pack compile's {@link packLinkView} and
148
+ * the `foundry` {@link LINK_WRITERS} entry.
149
+ *
150
+ * A link that cannot be resolved renders marked and is reported in
151
+ * `unresolved`.
85
152
  *
86
153
  * @param {string} markdown - The note body (frontmatter already stripped).
87
154
  * @param {object} ctx
@@ -96,10 +163,10 @@ export function buildWikilinkIndex(docs: Array<{
96
163
  * @param {{byShortcode: Map, types: Set}} ctx.index - From
97
164
  * {@link buildWikilinkIndex}.
98
165
  * @param {Map<string, string>} [ctx.captionLabels] - Numbered caption references.
99
- * @returns {{markdown: string, unresolved: Array<{link: string, target: string,
100
- * offset: number, reason: string, packages?: string[], anchor?: string}>}}
101
- * Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all
102
- * three resolvers share — `ambiguous` carries the claiming `packages`
166
+ * @returns {{markdown: string, links: ResolvedLink[], unresolved: Array<{link:
167
+ * string, target: string, offset: number, reason: string, packages?:
168
+ * string[], anchor?: string}>}} Each `reason` is one of
169
+ * {@link LINK_FINDING_REASONS} — `ambiguous` carries the claiming `packages`
103
170
  * and `unknown-anchor` the section it named. `offset` is the link's 0-based
104
171
  * position in `markdown`, which is what lets a caller report the line and
105
172
  * column it sits on.
@@ -116,6 +183,7 @@ export function convertWikilinks(markdown: string, { type, id, pack, docPack, in
116
183
  captionLabels?: Map<string, string> | undefined;
117
184
  }): {
118
185
  markdown: string;
186
+ links: ResolvedLink[];
119
187
  unresolved: Array<{
120
188
  link: string;
121
189
  target: string;
@@ -148,9 +216,257 @@ export function resolveReference(index: object, ref: unknown, hint?: object): {
148
216
  subType?: string;
149
217
  standings?: object;
150
218
  } | undefined;
219
+ /**
220
+ * The website's and the book's view of a link: the site index, answered with
221
+ * URLs.
222
+ *
223
+ * The site index is keyed case-insensitively by the canonical
224
+ * `package-system-type-shortcode` address, by `type/shortcode` and by the
225
+ * site's own `section/slug`; a fully qualified address another package
226
+ * publishes is looked up in its fetched index. Local wins: a live build is
227
+ * authoritative and a fetched index can only be staler. A short address two
228
+ * packages claim is in the index's `ambiguous` set, and is a finding of its
229
+ * own.
230
+ *
231
+ * A target that **resolved** to an entry with no page is not unresolved: a
232
+ * pack-only package publishes Foundry addresses and no web pages, so the
233
+ * author wrote a real address and there is simply nothing to link to.
234
+ *
235
+ * @param {object} ctx - What {@link module:engine/site-index.wikiContext}
236
+ * returns, with the page's own `anchors` and `eventAnchors`.
237
+ * @param {string} body - The page's text, which a link to its own section is
238
+ * concealed by when the section is secret.
239
+ * @returns {LinkView} The view.
240
+ */
241
+ export function siteLinkView(ctx: object, body: string): LinkView;
242
+ /**
243
+ * Every wikilink authored inside a frontmatter value.
244
+ *
245
+ * Wikilinks are resolved in a note's **body**. Frontmatter is not markdown and
246
+ * is never walked by the resolver, so a link written in one would be copied
247
+ * through verbatim and reach the reader as literal `[[…]]` text, in whatever the
248
+ * theme renders that field as (an infobox row, a description, a card
249
+ * subtitle). Nothing downstream notices: the value is a valid string, the page
250
+ * builds, and the defect is visible only to someone who looks at it.
251
+ *
252
+ * So the form is refused rather than resolved. Resolving it would mean choosing
253
+ * an output syntax for a field whose renderer is unknown to this build — a
254
+ * markdown link is inert in a template that prints the value as text, and an
255
+ * `<a>` is unusable in one that escapes it — and would quietly bless an
256
+ * authoring habit that the pack build has no way to honour at all. Frontmatter
257
+ * carries data; a link belongs in prose.
258
+ *
259
+ * Values are read from the *parsed* frontmatter, so a `[[` inside a YAML comment
260
+ * is not a hit, and every hit can be named by the path a reader would look at.
261
+ *
262
+ * @param {unknown} fm - Parsed frontmatter, as `readMatter` returns it.
263
+ * @returns {Array<{path: string, link: string}>} In reading order; `path` is the
264
+ * dotted key path of the offending value (`government.summary`, `aliases.1`).
265
+ */
266
+ export function frontmatterWikilinks(fm: unknown): Array<{
267
+ path: string;
268
+ link: string;
269
+ }>;
270
+ /**
271
+ * Rewrites the embeds and wikilinks in a markdown body for the website or the
272
+ * book — see {@link resolveWikilinks}, with {@link siteLinkView} and the `web`
273
+ * or `book` {@link LINK_WRITERS} entry.
274
+ *
275
+ * `[[type-shortcode|Text]]` → `[Text](/section/slug/)`
276
+ * `[[type-shortcode|]]` → the same, showing the target's own name
277
+ * `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
278
+ * `[[#slug|Text]]` → `[Text](#slug)`
279
+ *
280
+ * **Every target that resolves nowhere fails the build.** Failures are
281
+ * collected in `ctx.errors`, each carrying the authored `link` and its
282
+ * `occurrence` so a caller can report the line and column it sits on. The
283
+ * link still renders, marked: the mark is for whoever reads the page, the
284
+ * failure for its author.
285
+ *
286
+ * ## Embeds first, and they are not links
287
+ *
288
+ * `![[address|label]]` names a file rather than a note, so it is resolved into
289
+ * an ordinary markdown image before anything looks for a link — which is what
290
+ * stops the link pass reading an embed's interior as a link to a note nobody
291
+ * wrote. The image the rewrite leaves behind is what
292
+ * {@link module:engine/content-images.renderImageFigures} turns into a figure
293
+ * for the website, and what the book reads its staging list out of.
294
+ *
295
+ * ## Every hit is recorded, so the page's edges come out of one pass
296
+ *
297
+ * A caller that hands in `ctx.resolved` is given every index entry a link on
298
+ * the page resolved to, in body order, repeats included. Nothing is recorded
299
+ * for a same-page anchor or for a target that resolved nowhere.
300
+ *
301
+ * @param {string} body - The markdown body.
302
+ * @param {object} ctx - `{ index, assets, collide, contentTypes, packages,
303
+ * noIndexPackages, foreign, errors, src, file, resolved, output, anchors,
304
+ * eventAnchors, captionLabels }`, as
305
+ * {@link module:engine/site-index.wikiContext} builds it. `output` is
306
+ * `"book"` for the book and defaults to the website. `anchors` and
307
+ * `eventAnchors` are what *this* page declares, which a `[[#slug]]`
308
+ * self-link is checked against; absent, a self-link is not checked. `src`
309
+ * is the page's display path and `file` the source file a diagnostic names
310
+ * — absent, `src` stands in.
311
+ * @returns {string} The body with embeds and wikilinks rewritten.
312
+ */
313
+ export function resolveWebWikilinks(body: string, ctx: object): string;
314
+ /**
315
+ * How each target writes a {@link ResolvedLink}.
316
+ *
317
+ * | | `foundry` | `web` | `book` |
318
+ * | --- | --- | --- | --- |
319
+ * | a link | `@UUID[…]{text}`, a content link that survives import into a world | `[text](url)`, the page's relative URL | `[text](url)`, which the book sets as a cross-reference to the entry it prints, or as the page's address where it prints none |
320
+ * | a draft target | the link in a `sohl-draft-link` span | the same span | the link, then `(draft)` |
321
+ * | nothing to open | the text | the text, marked when the target is a draft | the text, then `(draft)` when the target is one |
322
+ * | unresolved | the text in a `sohl-unresolved-link` span | the same span | the text, then `(unresolved link)` |
323
+ *
324
+ * A link to a GM-only note, or to a secret section, is concealed by
325
+ * {@link presentWikilink} on every target alike.
326
+ *
327
+ * Foundry does not mark a link to a note another package publishes as a
328
+ * draft: its `@UUID` names the document the dependency installed, and the
329
+ * dependency's journal is what the reader opens.
330
+ *
331
+ * @type {Readonly<Record<"foundry"|"web"|"book", (link: ResolvedLink) => string>>}
332
+ */
333
+ export const LINK_WRITERS: Readonly<Record<"foundry" | "web" | "book", (link: ResolvedLink) => string>>;
334
+ /**
335
+ * A wikilink once it is resolved, before any target writes it.
336
+ *
337
+ * This is the link's **neutral form**: what it names and how it reads, with no
338
+ * target's syntax in it. Each target writes it through its own
339
+ * {@link LINK_WRITERS} entry, so one authored link reaches a Foundry journal as
340
+ * an `@UUID` content link, a web page as a relative URL and the book as a
341
+ * cross-reference, from the same resolution.
342
+ */
343
+ export type ResolvedLink = {
344
+ /**
345
+ * - `link`: the target has a
346
+ * destination the reader can open. `text`: the address is real and names a
347
+ * note with nothing to open — a note with no single Foundry document, or one
348
+ * a package publishing no pages holds — so the reader gets the words.
349
+ * `unresolved`: the link is a finding, and the words are kept, marked.
350
+ */
351
+ kind: "link" | "text" | "unresolved";
352
+ /**
353
+ * - What the reader sees.
354
+ */
355
+ text: string;
356
+ /**
357
+ * - The link exactly as authored.
358
+ */
359
+ literal: string;
360
+ /**
361
+ * - The target as authored, `""` for a link to a
362
+ * section of the same page.
363
+ */
364
+ target: string;
365
+ /**
366
+ * - The `#section` named, `""` when none.
367
+ */
368
+ anchor: string;
369
+ /**
370
+ * - Whether it names a section of its own page.
371
+ */
372
+ samePage: boolean;
373
+ /**
374
+ * - The canonical address it names, the
375
+ * same on every target; `undefined` for a same-page link and for a target
376
+ * that is not an address.
377
+ */
378
+ address: string | undefined;
379
+ /**
380
+ * - The Foundry document or page it opens.
381
+ */
382
+ uuid?: string | undefined;
383
+ /**
384
+ * - The URL it opens on the web and in the book,
385
+ * fragment included.
386
+ */
387
+ href?: string | undefined;
388
+ /**
389
+ * - Whether it names a note tagged `draft`.
390
+ */
391
+ draft: boolean;
392
+ /**
393
+ * - What it resolved to: an index entry,
394
+ * which decides whether the reference is concealed.
395
+ */
396
+ destination?: object | undefined;
397
+ /**
398
+ * - The link's parts, from {@link readWikilink}.
399
+ */
400
+ parsed: object;
401
+ /**
402
+ * - For `unresolved`, what is wrong: a `reason`
403
+ * from {@link LINK_FINDING_REASONS}, the `target` the message names, and the
404
+ * fields that reason carries.
405
+ */
406
+ finding?: object | undefined;
407
+ };
408
+ /**
409
+ * What one index answers a link with: the half of resolution that depends on
410
+ * which index is read. A pack compile reads {@link buildWikilinkIndex}'s
411
+ * index and answers with UUIDs ({@link packLinkView}); the website and the
412
+ * book read the site index and answer with URLs ({@link siteLinkView}).
413
+ * Everything else — the syntax, the label rule, which failure is which, and
414
+ * the order the checks run in — is {@link resolveWikilinks}'s, once.
415
+ */
416
+ export type LinkView = {
417
+ /**
418
+ * - The Address context a target is read in:
419
+ * `{package, system, types, packages, noIndexPackages}`.
420
+ */
421
+ context: object;
422
+ /**
423
+ * - The page the link is written
424
+ * on, as a destination.
425
+ */
426
+ self: (markdown: string) => object;
427
+ /**
428
+ * - What an address names: a destination, which
429
+ * may be an item's readable note or another package's note; a claim by more
430
+ * than one package; or nothing.
431
+ */
432
+ find: (read: object, target: string) => ({
433
+ destination: object;
434
+ itemDoc?: boolean;
435
+ foreign?: boolean;
436
+ } | {
437
+ ambiguous: true;
438
+ packages?: string[];
439
+ } | undefined);
440
+ /**
441
+ * - Whether a
442
+ * target that is not an address is a short address two packages claim.
443
+ */
444
+ collides: (read: object, target: string) => boolean;
445
+ /**
446
+ * - Whether a lowercase
447
+ * spelling of the shortcode names a note.
448
+ */
449
+ declared: (read: object) => boolean;
450
+ /**
451
+ * - Where a destination opens: a finding reason
452
+ * when the anchor does not belong to it, the UUID or URL it opens, or
453
+ * neither when there is nothing to open.
454
+ */
455
+ locate: (destination: object, anchor: string, how: {
456
+ samePage: boolean;
457
+ itemDoc?: boolean;
458
+ foreign?: boolean;
459
+ }) => {
460
+ reason?: string;
461
+ uuid?: string;
462
+ href?: string;
463
+ draft?: boolean;
464
+ };
465
+ };
466
+ import { slugify } from "./content-slug.mjs";
151
467
  import { ITEM_PACK } from "./ids.mjs";
152
468
  import { PACK_BY_TYPE } from "./ids.mjs";
153
469
  import { packForType } from "./ids.mjs";
154
470
  import { readQualifier } from "./address.mjs";
155
471
  import { resolveItemDocType } from "./address.mjs";
156
- export { ITEM_PACK, PACK_BY_TYPE, packForType, readQualifier, resolveItemDocType };
472
+ export { slugify, ITEM_PACK, PACK_BY_TYPE, packForType, readQualifier, resolveItemDocType };
@@ -20,8 +20,9 @@ export function addWords(set: Set<string>, text: string): void;
20
20
  * @property {Record<string, any>} word - The `data.words` entry it comes from.
21
21
  */
22
22
  /**
23
- * Every form the language's rules derive from its lexicon words, in
24
- * `data.words` order, each once, leaving out a form that is a lexicon word.
23
+ * Every form the language's rules derive from its lexicon words, and each
24
+ * declared variant of a word, in `data.words` order, each once, leaving out a
25
+ * form that is a lexicon word.
25
26
  *
26
27
  * @param {Record<string, any>|undefined} data - The language note's `data`.
27
28
  * @returns {DerivedForm[]} The forms.
@@ -27,10 +27,9 @@ export function normalizeRepoUrl(repository: string | {
27
27
  * to drift — and the one that drifts is this one, because nothing fetches `url`
28
28
  * the way Foundry fetches `manifest`, so a wrong value is never reported.
29
29
  *
30
- * The site's own `baseURL` says the same thing, and is deliberately not the
31
- * source: it sits at `site/hugo.toml` in most repositories and `kb/hugo.toml`
32
- * in the system, so reading it would mean the packaging half knowing where each
33
- * repository keeps its site configuration.
30
+ * `package.json`'s `homepage` says the same thing, and is deliberately not the
31
+ * source: reading it would mean the packaging half depending on the site's
32
+ * configuration.
34
33
  *
35
34
  * @param {string} contentPackage - The package's content-package name.
36
35
  * @returns {string} The homepage URL, with its trailing slash.