@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
@@ -25,8 +25,7 @@
25
25
  * journals.mjs, actors.mjs).
26
26
  */
27
27
 
28
- import { renderAlerts } from "./content-alerts.mjs";
29
- import { renderRuns } from "./content-scripts.mjs";
28
+ import { createRenderer } from "./content-render.mjs";
30
29
  import { buildReferenceTargets } from "./reference-targets.mjs";
31
30
  import { standingsDigest } from "./standings.mjs";
32
31
 
@@ -39,12 +38,7 @@ import crypto from "crypto";
39
38
  import path from "path";
40
39
  import yaml from "yaml";
41
40
  import unidecode from "unidecode";
42
- import markdownit from "markdown-it";
43
- import footnotePlugin from "markdown-it-footnote";
44
- import deflistPlugin from "markdown-it-deflist";
45
- import { dictionaryListPlugin } from "./definition-list.mjs";
46
- import { iconPlugin } from "./content-icons.mjs";
47
- import { imagePlugin, imagesIn } from "./content-images.mjs";
41
+ import { imagesIn } from "./content-images.mjs";
48
42
  import { embedRole, resolveEmbeds } from "./content-embeds.mjs";
49
43
  import { collectAssetRecords } from "./asset-index.mjs";
50
44
  import { assetImageInfoByPathname } from "./art-fields.mjs";
@@ -67,19 +61,15 @@ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs
67
61
  // a record needs none of that.
68
62
  import { authoredFrontmatter, isAssetRecord, isNoteRecord, noteFile } from "./index-records.mjs";
69
63
  import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
70
- // One vocabulary of link findings, and one message per class, so the three
71
- // resolvers cannot word the same defect differently.
64
+ // One vocabulary of link findings, and one message per class, so no two
65
+ // builds word the same defect differently.
72
66
  import { linkFindingMessage } from "./wikilink-syntax.mjs";
73
67
  // The declared tag vocabulary, which is where `draft` is stated. Read
74
68
  // from there rather than respelt, so the tag and its one reader cannot drift.
75
69
  import { isGmNote, isDraftNote } from "./note-vocabulary.mjs";
76
70
  import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
77
71
  import { expandContentTables } from "./content-tables.mjs";
78
- import { spanMarkdownPlugin } from "./content-spans.mjs";
79
- import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
80
- import { renderBlocks } from "./content-blocks.mjs";
81
- import { headingAttributesPlugin } from "./heading-attributes.mjs";
82
- import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
72
+ import { scanFigures } from "./content-figures.mjs";
83
73
  import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
84
74
  // The pure `sohl:` frontmatter readers live in a leaf module so the item-type
85
75
  // registry can import them without reaching back through this one.
@@ -100,72 +90,68 @@ export {
100
90
  let localAssetImageInfoCache;
101
91
 
102
92
  /**
103
- * The markdown renderer every surface shares.
93
+ * The Foundry renderer: the shared configuration of
94
+ * {@link module:engine/content-render.createRenderer} for the `foundry`
95
+ * target.
104
96
  *
105
97
  * `html: true` is long-standing and load-bearing — notes carry raw blocks — and
106
98
  * it is also why {@link module:engine/content-icons} exists rather than an
107
99
  * instruction to write `<i class="fa-solid …">` by hand: that would render on
108
100
  * the two HTML surfaces and be silently dropped by the third.
101
+ *
102
+ * The icon registry and the image resolvers are read per render, not at import:
103
+ * this constant is built before any configuration is read, and a package's own
104
+ * icons live in the configuration. A tree with none — or a caller with no
105
+ * configuration to find — falls back to the shipped table and to the authored
106
+ * pathname.
109
107
  */
110
- export const md = markdownit({ html: true })
111
- .use(footnotePlugin)
112
- .use(deflistPlugin)
113
- .use(dictionaryListPlugin)
114
- .use(spanMarkdownPlugin, { spoilers: true })
115
- .use(dictionaryEntryPlugin)
116
- // A heading's attribute block, written onto the heading element rather than
117
- // typeset into it — the reading the website's renderer does for itself.
118
- .use(headingAttributesPlugin)
119
- .use(
120
- // Resolved per render, not at import: this constant is built before any
121
- // configuration is read, and a package's own icons live in the
122
- // configuration. A tree with none — or a caller with no configuration to
123
- // find — falls back to the shipped table.
124
- iconPlugin(() => {
125
- try {
126
- return loadPackConfig().icons;
127
- } catch {
128
- return undefined;
129
- }
130
- }),
131
- )
108
+ const renderer = createRenderer("foundry", {
109
+ icons: () => {
110
+ try {
111
+ return loadPackConfig().icons;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ },
132
116
  // The width and position an image states, honoured as a figure. The
133
117
  // vocabularies are closed and need no configuration; the address does, and
134
118
  // is resolved per render for the reason the icon registry is — Foundry
135
119
  // serves a file from inside the install, so a body image's address is
136
120
  // translated by the same rule `img:` follows.
137
- .use(
138
- imagePlugin(
139
- (src) => {
140
- // **Reported upstream, never here.** A renderer has no channel to
141
- // report through, and this one runs inside the very passes whose
142
- // job is to collect findings — a throw would take the whole lint
143
- // down and lose every other finding in the tree. So nothing reaches
144
- // this point unreported: {@link convertNoteWikilinks} refuses a body
145
- // image with no address inside the install before a compiler renders
146
- // one, and the address passes report the same pathname with a line
147
- // and a column. What is left here is a fallback for a caller with no
148
- // configuration to resolve against, where the authored pathname is
149
- // the most honest thing to emit.
150
- try {
151
- return resolveImg(src, loadPackConfig()) ?? src;
152
- } catch {
153
- return src;
154
- }
155
- },
156
- (src) => {
157
- // A body image is free to write the bare, own-package form,
158
- // so the address is normalized to the pathname the cache
159
- // below keys by before it is looked up.
160
- try {
161
- const key = assetPathnameKey(src, loadPackConfig());
162
- return key ? localAssetImageInfo().get(key) : undefined;
163
- } catch {
164
- return undefined;
165
- }
166
- },
167
- ),
168
- );
121
+ images: {
122
+ resolveSrc: (src) => {
123
+ // **Reported upstream, never here.** A renderer has no channel to
124
+ // report through, and this one runs inside the very passes whose
125
+ // job is to collect findings — a throw would take the whole lint
126
+ // down and lose every other finding in the tree. So nothing reaches
127
+ // this point unreported: {@link convertNoteWikilinks} refuses a body
128
+ // image with no address inside the install before a compiler renders
129
+ // one, and the address passes report the same pathname with a line
130
+ // and a column. What is left here is a fallback for a caller with no
131
+ // configuration to resolve against, where the authored pathname is
132
+ // the most honest thing to emit.
133
+ try {
134
+ return resolveImg(src, loadPackConfig()) ?? src;
135
+ } catch {
136
+ return src;
137
+ }
138
+ },
139
+ lookupAsset: (src) => {
140
+ // A body image is free to write the bare, own-package form,
141
+ // so the address is normalized to the pathname the cache
142
+ // below keys by before it is looked up.
143
+ try {
144
+ const key = assetPathnameKey(src, loadPackConfig());
145
+ return key ? localAssetImageInfo().get(key) : undefined;
146
+ } catch {
147
+ return undefined;
148
+ }
149
+ },
150
+ },
151
+ });
152
+
153
+ /** The Foundry renderer's parser, for callers that parse or render a fragment. */
154
+ export const md = renderer.md;
169
155
 
170
156
  /**
171
157
  * The pictures this package's own tree ships, by the pathname their address
@@ -194,25 +180,6 @@ function localAssetImageInfo() {
194
180
  return localAssetImageInfoCache;
195
181
  }
196
182
 
197
- md.renderer.rules.footnote_block_open = () =>
198
- '<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
199
- md.renderer.rules.footnote_caption = (tokens, idx, _options, env) => {
200
- const meta = tokens[idx].meta;
201
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
202
- return `[${number}${meta.subId > 0 ? `:${meta.subId}` : ""}]`;
203
- };
204
- md.renderer.rules.footnote_anchor_name = (tokens, idx, _options, env) => {
205
- const meta = tokens[idx].meta;
206
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
207
- return `${env.docId ? `-${env.docId}-` : ""}${number}`;
208
- };
209
- md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
210
- const meta = tokens[idx].meta;
211
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
212
- const id = renderer.rules.footnote_anchor_name(tokens, idx, options, env, renderer);
213
- return `<li id="fn${id}" class="footnote-item" value="${number}">`;
214
- };
215
-
216
183
  /**
217
184
  * Render a note body with Foundry's named-block markup.
218
185
  *
@@ -232,12 +199,7 @@ md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
232
199
  * @returns {string} The rendered HTML.
233
200
  */
234
201
  export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole, runs) {
235
- const marked = runs ? renderRuns(body, runs, undefined, { target: "foundry" }).markdown : body;
236
- const figured = renderFigureBlocks(marked, (block) => md.render(block), figures, {
237
- resolveRole,
238
- });
239
- const blocks = renderBlocks(figured.markdown, "foundry");
240
- return md.render(renderAlerts(blocks.markdown, "foundry").markdown, { footnoteNumbers, docId });
202
+ return renderer.render(body, { runs, figures, resolveRole, footnoteNumbers, docId });
241
203
  }
242
204
 
243
205
  /**
@@ -983,7 +945,7 @@ export function buildContentLinkIndex(
983
945
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line.
984
946
  * @param {number} [ctx.bodyColumn] - 1-based file column of the same character.
985
947
  * @param {Array<{line: number, generated: boolean}>} [ctx.lineMap] - Which
986
- * authored line each body line came from, from {@link expandNoteTables}.
948
+ * authored line each body line came from, from {@link module:engine/content-tables.expandContentTables}.
987
949
  * @returns {{markdown: string, unresolved: Array<object>}}
988
950
  * @throws {Error} On any link that does not resolve — an unlabelled one, a
989
951
  * target that is not an address, or an address nothing publishes — and on a
@@ -1086,11 +1048,9 @@ export function convertNoteWikilinks(
1086
1048
  }
1087
1049
 
1088
1050
  for (const u of result.unresolved) {
1089
- // Every class fails, and every class is worded by the shared table.
1090
- // The three resolvers read one authored link, so an author who
1091
- // ran the pack build first and the link checker second must not be told
1092
- // two different things about the same mistake — and a class the pack
1093
- // build alone knew how to describe is how they came apart before.
1051
+ // Every class fails, and every class is worded by the shared table,
1052
+ // so an author who ran the pack build first and the link checker second
1053
+ // is told one thing about one mistake.
1094
1054
  //
1095
1055
  // The note's name is appended rather than woven in: the message is the
1096
1056
  // defect, the name is the context this build can add.
@@ -43,8 +43,8 @@
43
43
  * module, which is most of the packages that need a homepage and nothing else.
44
44
  *
45
45
  * **Published at the package root, addressed like every other note.** A
46
- * homepage is written as the mount's `_index.md`, so Hugo's `home` kind renders
47
- * it at `/<package>/` — the address a reader expects a package's front page at,
46
+ * homepage is rendered at `/<package>/` — the address a reader expects a
47
+ * package's front page at,
48
48
  * and the one `package.json`'s `homepage` states. It still declares a
49
49
  * `shortcode` — conventionally {@link HOMEPAGE_SHORTCODE} — because that is what
50
50
  * a link is written with: `[[homepage-root|Text]]` is an ordinary wikilink, and
@@ -96,19 +96,6 @@ export const HOMEPAGE_FIELDS = Object.freeze([]);
96
96
  */
97
97
  export const HOMEPAGE_SHORTCODE = "root";
98
98
 
99
- /**
100
- * The file a homepage is written to, relative to the package's site root.
101
- *
102
- * The mount's own `_index.md`: Hugo renders it as the `home` kind, at
103
- * `baseURL` — which is `/<package>/`, the package's own address. One fixed
104
- * destination rather than one derived from the note's address, because the
105
- * homepage's address *is* the package root; the note's `shortcode` names the
106
- * page in links and decides no file.
107
- *
108
- * @type {string}
109
- */
110
- export const HOMEPAGE_DESTINATION = "_index.md";
111
-
112
99
  /**
113
100
  * Whether a note's frontmatter declares the homepage type.
114
101
  *
@@ -337,8 +324,10 @@ function positionOfType(file) {
337
324
  * authored `name.full` wins, because a front page may greet a reader
338
325
  * differently from a package browser.
339
326
  *
340
- * Falls back to `contentPackage` last, so a package that has no manifest of its
341
- * own still yields a titled page rather than a blank heading.
327
+ * A package with no manifest title — a `documentation` package has no manifest
328
+ * at all — takes `site.title`, the name its site is known by. It falls back to
329
+ * `contentPackage` last, so a package that states neither still yields a
330
+ * titled page rather than a blank heading.
342
331
  *
343
332
  * @param {object|null|undefined} fm - The note's frontmatter.
344
333
  * @param {object} config - The resolved configuration.
@@ -351,7 +340,9 @@ export function homepageTitle(fm, config) {
351
340
  config?.packageBuild?.manifest
352
341
  );
353
342
  const title = manifest?.title;
354
- return typeof title === "string" && title.trim() ? title : config.contentPackage;
343
+ if (typeof title === "string" && title.trim()) return title;
344
+ const site = config?.site?.title;
345
+ return typeof site === "string" && site.trim() ? site : config.contentPackage;
355
346
  }
356
347
 
357
348
  /**
@@ -359,20 +350,17 @@ export function homepageTitle(fm, config) {
359
350
  *
360
351
  * The note's own, plus the two derived values every emitted page carries: the
361
352
  * resolved `title`, and the package the build derived — no note declares one
362
- * and the theme's breadcrumb partial reads `.Params.package`.
353
+ * and the theme's breadcrumbs read.
363
354
  *
364
- * **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
365
- * whose path is already where the package is served, so the page has no
366
- * address to state; a content page states one because its file sits under the
367
- * mount and its address does not.
355
+ * **No `url` and no `slug`.** The homepage publishes at the package root, so
356
+ * the page has no address to state.
368
357
  *
369
- * An authored `aliases` is dropped for the same reason it is on every other
370
- * page: Hugo reads it as URL redirects, so passing it through would publish a
371
- * redirect stub at each one.
358
+ * An authored `aliases` is dropped, as it is on every other page: this build
359
+ * emits no redirects.
372
360
  *
373
361
  * `related` is derived like every other page's — what links to the homepage
374
362
  * is a fact about the rest of the tree — so an authored value is dropped and
375
- * the caller's block, where it has one, is written in its place.
363
+ * the caller's block, where it has one, is set in its place.
376
364
  *
377
365
  * @param {object} fm - The note's frontmatter.
378
366
  * @param {object} options - Options.
@@ -380,7 +368,7 @@ export function homepageTitle(fm, config) {
380
368
  * @param {string} options.title - The resolved title.
381
369
  * @param {object} [options.related] - The homepage's backlinks and mentions,
382
370
  * where the site build found either.
383
- * @returns {object} The frontmatter to write.
371
+ * @returns {object} The frontmatter.
384
372
  */
385
373
  export function homepageFrontmatter(fm, { contentPackage, title, related }) {
386
374
  const data = { ...fm, package: contentPackage, title };
@@ -395,7 +383,7 @@ export function homepageFrontmatter(fm, { contentPackage, title, related }) {
395
383
  *
396
384
  * Reference-style links are deliberately not matched: a homepage is published
397
385
  * verbatim, so a `[x][y]` whose definition sits in the body reaches the reader
398
- * as Hugo renders it, and one whose definition is missing is a defect the
386
+ * as authored, and one whose definition is missing is a defect the
399
387
  * rendered page shows for itself.
400
388
  *
401
389
  * @type {RegExp}
@@ -0,0 +1,70 @@
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
+ * **Minified site pages.** {@link minifyHtml} drops what a browser does not
16
+ * read: comments, and every run of white space but one character.
17
+ *
18
+ * What a reader sees is unchanged. A run of white space between two words or
19
+ * two tags still separates them, as one space; it is only made shorter. The
20
+ * places where white space is the content are kept exactly as written: a
21
+ * `<pre>`, a `<textarea>`, a `<script>` or `<style>`, and a verse's lines,
22
+ * which set their spacing as authored. A tag is kept as written too, its
23
+ * attribute values included.
24
+ *
25
+ * @module
26
+ */
27
+
28
+ /** The elements whose content is written out as it stands. */
29
+ const VERBATIM = /^<(pre|textarea|script|style)\b/i;
30
+
31
+ /** A verse's spans, whose white space is set as authored. */
32
+ const VERSE_SPAN = /\bclass\s*=\s*"[^"]*\bverse-(?:line|column|part)\b/i;
33
+
34
+ /**
35
+ * Minify a page.
36
+ *
37
+ * @param {string} html - A complete document.
38
+ * @returns {string} The same document without comments and with each run of
39
+ * white space outside the verbatim elements cut to one character: a newline
40
+ * where the run held one, a space otherwise.
41
+ */
42
+ export function minifyHtml(html) {
43
+ const source = String(html ?? "");
44
+ const pieces =
45
+ /<!--[\s\S]*?-->|<(pre|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1\s*>|<[^>]*>|[^<]+|</gi;
46
+ const verse = [];
47
+ let out = "";
48
+ for (const [piece] of source.matchAll(pieces)) {
49
+ if (piece.startsWith("<!--")) {
50
+ // A conditional comment is read by the browser it names.
51
+ if (piece.startsWith("<!--[if")) out += piece;
52
+ continue;
53
+ }
54
+ if (piece.startsWith("<")) {
55
+ if (VERBATIM.test(piece)) {
56
+ out += piece;
57
+ continue;
58
+ }
59
+ if (/^<span\b/i.test(piece)) verse.push(verse.at(-1) || VERSE_SPAN.test(piece));
60
+ else if (/^<\/span\s*>/i.test(piece)) verse.pop();
61
+ out += piece;
62
+ continue;
63
+ }
64
+ out +=
65
+ verse.at(-1) ? piece : (
66
+ piece.replace(/\s+/g, (run) => (run.includes("\n") ? "\n" : " "))
67
+ );
68
+ }
69
+ return out.replace(/^\s+/, "");
70
+ }
package/engine/index.mjs CHANGED
@@ -216,6 +216,12 @@ export * as siteBuild from "./site-build.mjs";
216
216
  /** In-memory page rendering for an editor preview. */
217
217
  export * as sitePreview from "./site-preview.mjs";
218
218
 
219
+ /** What an editor with a content index gives the preview, so it renders as the site does. */
220
+ export * as previewIndex from "./preview-index.mjs";
221
+
222
+ /** The one markdown-it configuration every target renders through. */
223
+ export * as contentRender from "./content-render.mjs";
224
+
219
225
  /** Address rules every content tree is linted against: shape, uniqueness, alias. */
220
226
  export * as contentLint from "./content-lint.mjs";
221
227
 
@@ -244,9 +250,6 @@ export * as pathnames from "./pathnames.mjs";
244
250
  /** Resolving every link in a tree, and the ones that land nowhere. */
245
251
  export * as contentLinks from "./content-links.mjs";
246
252
 
247
- /** Wikilinks resolved to a **web URL** — the site half of the pair below. */
248
- export * as webWikilinks from "./web-wikilinks.mjs";
249
-
250
253
  /** Prepared SQL content tables, expanded into Markdown at compile time. */
251
254
  export * as contentTables from "./content-tables.mjs";
252
255
 
@@ -262,10 +265,10 @@ export * as documentSubtypes from "./document-subtypes.mjs";
262
265
  /** Which types carry documentation of their own, and where it is addressed. */
263
266
  export * as itemDocs from "./item-docs.mjs";
264
267
 
265
- /** Wikilinks resolved to a **Foundry UUID**: qualifiers, the index, rewriting. */
268
+ /** The link resolver: the pack index, the views, and each target's link form. */
266
269
  export * as wikilinks from "./wikilinks.mjs";
267
270
 
268
- /** What a `[[…]]` is, before either resolver decides where it points. */
271
+ /** What a `[[…]]` is, before the resolver decides where it points. */
269
272
  export * as wikilinkSyntax from "./wikilink-syntax.mjs";
270
273
 
271
274
  /** The address index a site build resolves its wikilinks against. */
@@ -130,7 +130,7 @@ export const NEAR_NAME_COLUMNS = Object.freeze([...IDENTITY, "near", "given", "m
130
130
  /** Every column of the `negation` relation, in order: one row per language. */
131
131
  export const NEGATION_COLUMNS = Object.freeze(["language", "particle", "mutation"]);
132
132
  /** Every column of the `stress` relation, in order. */
133
- export const STRESS_COLUMNS = Object.freeze(["language", "stress"]);
133
+ export const STRESS_COLUMNS = Object.freeze(["language", "two", "longer", "mark"]);
134
134
  /** Every column of the `finals` relation, in order. */
135
135
  export const FINAL_COLUMNS = Object.freeze([...IDENTITY, ...FINAL_KEYS]);
136
136
  /** Every column of the `syllables` relation, in order: one row per language. */
@@ -789,7 +789,13 @@ export function stressRows(records) {
789
789
  const rows = [];
790
790
  for (const [language, record] of languagesOf(records)) {
791
791
  const stress = soundsOf(isMap(record.data) ? record.data : {}).stress;
792
- if (typeof stress === "string") rows.push({ language, stress });
792
+ if (isMap(stress))
793
+ rows.push({
794
+ language,
795
+ two: stress.two ?? null,
796
+ longer: stress.longer ?? null,
797
+ mark: stress.mark ?? null,
798
+ });
793
799
  }
794
800
  return rows;
795
801
  }
@@ -126,13 +126,21 @@ export const HELD_CLIPS = Object.freeze(["first"]);
126
126
  /** How often a group of clusters stands inside a word. */
127
127
  export const CLUSTER_USES = Object.freeze(["common", "sparing"]);
128
128
 
129
- /** Where a language's stress falls. */
130
- export const STRESS_RULES = Object.freeze([
131
- "first syllable",
132
- "penultimate or final",
133
- "by weight",
134
- "as marked",
135
- ]);
129
+ /** The syllable a word of two stresses. */
130
+ export const STRESS_TWO = Object.freeze(["first", "last"]);
131
+
132
+ /** The syllable a word of three or more stresses, counted from its end. */
133
+ export const STRESS_LONGER = Object.freeze(["antepenultimate", "penultimate", "final"]);
134
+
135
+ /**
136
+ * The diacritics that mark a stress the rule would not choose, with the
137
+ * combining character each is written with.
138
+ */
139
+ export const STRESS_MARKS = Object.freeze({
140
+ acute: "\u0301",
141
+ grave: "\u0300",
142
+ circumflex: "\u0302",
143
+ });
136
144
 
137
145
  /** Which sounds meet where two parts of a word join. */
138
146
  export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
@@ -690,6 +698,39 @@ export const HARMONY_FIELDS = Object.freeze([
690
698
  }),
691
699
  ]);
692
700
 
701
+ /**
702
+ * The keys of a language's `data.sounds.stress`.
703
+ *
704
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
705
+ */
706
+ export const STRESS_FIELDS = Object.freeze([
707
+ Object.freeze({
708
+ name: "two",
709
+ kind: "string",
710
+ required: true,
711
+ allowed: STRESS_TWO,
712
+ shape: `one of ${STRESS_TWO.join(", ")}`,
713
+ describe: "The stressed syllable of a word of two syllables.",
714
+ }),
715
+ Object.freeze({
716
+ name: "longer",
717
+ kind: "string",
718
+ required: true,
719
+ allowed: STRESS_LONGER,
720
+ shape: `one of ${STRESS_LONGER.join(", ")}`,
721
+ describe: "The stressed syllable of a word of three or more, counted from its end.",
722
+ }),
723
+ Object.freeze({
724
+ name: "mark",
725
+ kind: "string",
726
+ allowed: Object.freeze(Object.keys(STRESS_MARKS)),
727
+ shape: `one of ${Object.keys(STRESS_MARKS).join(", ")}`,
728
+ describe:
729
+ "The diacritic that marks a stress the rule would not choose; it moves the stress to " +
730
+ "the syllable it sits on.",
731
+ }),
732
+ ]);
733
+
693
734
  /**
694
735
  * The keys of a language's `data.sounds.syllables`: what a syllable may open
695
736
  * and close on, and what a word may.
@@ -901,10 +942,10 @@ export const SOUND_FIELDS = Object.freeze([
901
942
  }),
902
943
  Object.freeze({
903
944
  name: "stress",
904
- kind: "string",
905
- allowed: STRESS_RULES,
906
- shape: `one of ${STRESS_RULES.join(", ")}`,
907
- describe: "Where a word's stress falls.",
945
+ kind: "map",
946
+ shape: "a map — `{ two, longer, mark? }`",
947
+ fields: STRESS_FIELDS,
948
+ describe: "Where a word's stress falls, as a rule a program reads.",
908
949
  }),
909
950
  Object.freeze({
910
951
  name: "finals",
@@ -1623,6 +1664,52 @@ export const VERB_PERSON_FIELDS = Object.freeze(
1623
1664
  ),
1624
1665
  );
1625
1666
 
1667
+ /**
1668
+ * The endings `data.grammar.native` writes under a logographic map: the noun
1669
+ * plural, then the six persons of the verb pattern.
1670
+ */
1671
+ export const WRITTEN_ENDINGS = Object.freeze([
1672
+ "plural",
1673
+ ...VERB_PERSON_FIELDS.map((person) => person.name),
1674
+ ]);
1675
+
1676
+ /** The keys of one `data.grammar.native` entry. */
1677
+ const GRAMMAR_NATIVE_FIELDS = Object.freeze([
1678
+ Object.freeze({
1679
+ name: "ending",
1680
+ kind: "string",
1681
+ required: true,
1682
+ allowed: WRITTEN_ENDINGS,
1683
+ shape: `one of ${WRITTEN_ENDINGS.join(", ")}`,
1684
+ describe:
1685
+ "The ending written: `plural` for a noun's plural, or a person of `verbs.persons` " +
1686
+ "for a verb's present form.",
1687
+ }),
1688
+ Object.freeze({
1689
+ name: "map",
1690
+ kind: "string",
1691
+ required: true,
1692
+ shape: "a logographic mapping note's `data.map.name`",
1693
+ describe: "The logographic map the ending is written for.",
1694
+ }),
1695
+ Object.freeze({
1696
+ name: "font",
1697
+ kind: "string",
1698
+ shape: "a face the map lists",
1699
+ describe:
1700
+ "The one face the writing is for; unstated, it is for every face the map lists, " +
1701
+ "and a writing stated for a face wins over it there.",
1702
+ }),
1703
+ Object.freeze({
1704
+ name: "text",
1705
+ kind: "string",
1706
+ required: true,
1707
+ allowEmpty: true,
1708
+ shape: "the ending in the script, as the map's characters; empty writes the stem alone",
1709
+ describe: "The signs written after the stem's writing.",
1710
+ }),
1711
+ ]);
1712
+
1626
1713
  /** The two alternatives of a supplied optional verb pattern. */
1627
1714
  const VERB_PATTERN_FIELDS = Object.freeze([
1628
1715
  Object.freeze({
@@ -1749,6 +1836,19 @@ const GRAMMAR_FIELDS = Object.freeze([
1749
1836
  "named suffix attaches to what is left; a participle closing on an `agreement` " +
1750
1837
  "ending agrees. Read by the mapped-run word check.",
1751
1838
  }),
1839
+ Object.freeze({
1840
+ name: "native",
1841
+ kind: "list",
1842
+ shape: "a list of `{ ending, map, font?, text }`",
1843
+ entries: Object.freeze({
1844
+ kind: "map",
1845
+ shape: "a map — `{ ending, map, font?, text }`",
1846
+ fields: GRAMMAR_NATIVE_FIELDS,
1847
+ }),
1848
+ describe:
1849
+ "Under a logographic map, the signs a noun's plural or a verb's present form is " +
1850
+ "written with after its stem's writing, one entry per ending, map and face.",
1851
+ }),
1752
1852
  ]);
1753
1853
 
1754
1854
  /**
@@ -1762,11 +1862,12 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1762
1862
  Object.freeze({
1763
1863
  name: "grammar",
1764
1864
  kind: "map",
1765
- shape: "a map — `{ plural?, verbs?, agreement?, participles? }`",
1865
+ shape: "a map — `{ plural?, verbs?, agreement?, participles?, native? }`",
1766
1866
  fields: GRAMMAR_FIELDS,
1767
1867
  describe:
1768
- "Optional noun plurals, one verb pattern, agreement and participles, for dictionary " +
1769
- "presentation and the mapped-run word check. Only the shape of supplied keys is " +
1868
+ "Optional noun plurals, one verb pattern, agreement, participles and the endings' " +
1869
+ "logographic writings, for dictionary presentation, the mapped-run word check and " +
1870
+ "logographic maps. Only the shape of supplied keys is " +
1770
1871
  "checked; no word must follow or state a rule. A language skill note only.",
1771
1872
  }),
1772
1873
  listOfMaps(