@heroiclands/package-build 23.13.0 → 24.0.1

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 (236) hide show
  1. package/CHANGELOG.md +50 -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 +248 -80
  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 +155 -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 +265 -313
  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-reach.mjs +213 -0
  88. package/engine/site-render.mjs +668 -0
  89. package/engine/site-root.mjs +90 -39
  90. package/engine/site-search.mjs +161 -0
  91. package/engine/site-serve.mjs +274 -0
  92. package/engine/site-theme-banner.mjs +154 -0
  93. package/engine/site-theme-urls.mjs +125 -0
  94. package/engine/site-theme.mjs +939 -0
  95. package/engine/sql-tables.mjs +9 -6
  96. package/engine/text-maps.mjs +57 -10
  97. package/engine/typographer.mjs +248 -0
  98. package/engine/verse-metre.mjs +357 -0
  99. package/engine/wikilink-syntax.mjs +29 -47
  100. package/engine/wikilinks.mjs +758 -279
  101. package/engine/word-forms.mjs +39 -24
  102. package/engine/word-rows.mjs +12 -2
  103. package/engine/word-terms.mjs +29 -4
  104. package/manifest.mjs +3 -4
  105. package/package.json +10 -7
  106. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  107. package/site-theme/layouts/base.hbs +31 -0
  108. package/site-theme/layouts/blog-list.hbs +30 -0
  109. package/site-theme/layouts/blog-post.hbs +40 -0
  110. package/site-theme/layouts/home.hbs +35 -0
  111. package/site-theme/layouts/list.hbs +20 -0
  112. package/site-theme/layouts/not-found.hbs +31 -0
  113. package/site-theme/layouts/page.hbs +7 -0
  114. package/site-theme/partials/copy-buttons.hbs +37 -0
  115. package/site-theme/partials/draft-notice.hbs +5 -0
  116. package/site-theme/partials/exturl.hbs +2 -0
  117. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  118. package/site-theme/partials/header.hbs +44 -0
  119. package/site-theme/partials/hero-banner.hbs +11 -0
  120. package/site-theme/partials/infobox-entry.hbs +2 -0
  121. package/site-theme/partials/infobox-section.hbs +31 -0
  122. package/site-theme/partials/infobox-value.hbs +2 -0
  123. package/site-theme/partials/infobox.hbs +18 -0
  124. package/site-theme/partials/list-members.hbs +9 -0
  125. package/site-theme/partials/nav-links.hbs +5 -0
  126. package/site-theme/partials/page-body.hbs +48 -0
  127. package/site-theme/partials/page-list.hbs +14 -0
  128. package/site-theme/partials/related.hbs +24 -0
  129. package/site-theme/partials/search-meta.hbs +6 -0
  130. package/site-theme/partials/search-names.hbs +2 -0
  131. package/site-theme/partials/search.hbs +52 -0
  132. package/site-theme/partials/tags.hbs +4 -0
  133. package/site-theme/partials/toc.hbs +7 -0
  134. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  135. package/stage.mjs +1 -2
  136. package/types/config.d.mts +8 -4
  137. package/types/content-config.d.mts +15 -15
  138. package/types/engine/asset-shipping.d.mts +5 -2
  139. package/types/engine/base-compiler.d.mts +20 -2
  140. package/types/engine/content-address.d.mts +1 -1
  141. package/types/engine/content-alerts.d.mts +8 -2
  142. package/types/engine/content-blocks.d.mts +11 -4
  143. package/types/engine/content-expand.d.mts +288 -0
  144. package/types/engine/content-figures.d.mts +11 -3
  145. package/types/engine/content-glyphs.d.mts +85 -0
  146. package/types/engine/content-images.d.mts +6 -7
  147. package/types/engine/content-links.d.mts +1 -1
  148. package/types/engine/content-render.d.mts +199 -0
  149. package/types/engine/content-scripts.d.mts +62 -0
  150. package/types/engine/content-spans.d.mts +1 -1
  151. package/types/engine/content-styles.d.mts +33 -0
  152. package/types/engine/content-symbols.d.mts +35 -0
  153. package/types/engine/core-report.d.mts +45 -0
  154. package/types/engine/core-vocabulary.d.mts +53 -0
  155. package/types/engine/dictionary-html.d.mts +8 -0
  156. package/types/engine/field-reference.d.mts +1 -1
  157. package/types/engine/heading-attributes.d.mts +20 -0
  158. package/types/engine/helpers.d.mts +3 -10
  159. package/types/engine/homepage.d.mts +11 -24
  160. package/types/engine/html-minify.d.mts +9 -0
  161. package/types/engine/index.d.mts +2 -1
  162. package/types/engine/language-terms.d.mts +24 -2
  163. package/types/engine/logographic.d.mts +45 -0
  164. package/types/engine/map-lexicon.d.mts +48 -8
  165. package/types/engine/map-places.d.mts +8 -0
  166. package/types/engine/markdown-expressions.d.mts +0 -2
  167. package/types/engine/metre-declaration.d.mts +39 -0
  168. package/types/engine/pdf-render.d.mts +6 -2
  169. package/types/engine/post-fields.d.mts +44 -0
  170. package/types/engine/preview-index.d.mts +72 -0
  171. package/types/engine/preview-source.d.mts +51 -0
  172. package/types/engine/script-emphasis-terms.d.mts +21 -0
  173. package/types/engine/script-emphasis.d.mts +84 -0
  174. package/types/engine/script-font-delivery.d.mts +4 -4
  175. package/types/engine/site-build.d.mts +71 -113
  176. package/types/engine/site-config.d.mts +14 -134
  177. package/types/engine/site-files.d.mts +73 -0
  178. package/types/engine/site-index.d.mts +11 -3
  179. package/types/engine/site-preview.d.mts +15 -19
  180. package/types/engine/site-reach.d.mts +76 -0
  181. package/types/engine/site-render.d.mts +173 -0
  182. package/types/engine/site-root.d.mts +14 -4
  183. package/types/engine/site-search.d.mts +54 -0
  184. package/types/engine/site-serve.d.mts +44 -0
  185. package/types/engine/site-theme-banner.d.mts +51 -0
  186. package/types/engine/site-theme-urls.d.mts +60 -0
  187. package/types/engine/site-theme.d.mts +127 -0
  188. package/types/engine/text-maps.d.mts +17 -3
  189. package/types/engine/typographer.d.mts +11 -0
  190. package/types/engine/verse-metre.d.mts +131 -0
  191. package/types/engine/wikilink-syntax.d.mts +30 -48
  192. package/types/engine/wikilinks.d.mts +330 -14
  193. package/types/engine/word-forms.d.mts +3 -2
  194. package/types/manifest.d.mts +3 -4
  195. package/types/stage.d.mts +1 -2
  196. package/types/vendor/markdownlint.d.mts +8 -0
  197. package/vendor/markdownlint.mjs +20388 -0
  198. package/engine/web-wikilinks.mjs +0 -594
  199. package/hugo-theme/layouts/404.html +0 -85
  200. package/hugo-theme/layouts/_default/baseof.html +0 -67
  201. package/hugo-theme/layouts/_default/list.html +0 -139
  202. package/hugo-theme/layouts/_default/single.html +0 -4
  203. package/hugo-theme/layouts/blog/list.html +0 -54
  204. package/hugo-theme/layouts/blog-post/single.html +0 -36
  205. package/hugo-theme/layouts/index.html +0 -75
  206. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  207. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  208. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  209. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  210. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  211. package/hugo-theme/layouts/partials/header.html +0 -35
  212. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  213. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  214. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  215. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  216. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  217. package/hugo-theme/layouts/partials/infobox.html +0 -57
  218. package/hugo-theme/layouts/partials/page-body.html +0 -152
  219. package/hugo-theme/layouts/partials/related.html +0 -138
  220. package/hugo-theme/layouts/partials/search.html +0 -84
  221. package/hugo-theme/layouts/partials/site-url.html +0 -32
  222. package/hugo-theme/layouts/partials/tags.html +0 -30
  223. package/hugo-theme/layouts/partials/toc.html +0 -28
  224. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  225. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  226. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  227. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  228. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  229. package/hugo-theme/theme.toml +0 -11
  230. package/types/engine/web-wikilinks.d.mts +0 -107
  231. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  232. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  233. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  234. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  235. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  236. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -112,6 +112,12 @@ import { checkNoteEvents } from "./note-events.mjs";
112
112
  import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
113
113
  import { EVENT_ENTRY } from "./note-event-terms.mjs";
114
114
  import { CULTURE_FIELD, checkCultureSubType } from "./culture-choice.mjs";
115
+ import {
116
+ POST_AUTHOR_FIELD,
117
+ POST_DATE_FIELD,
118
+ POST_SUBTYPE,
119
+ checkPostFields,
120
+ } from "./post-fields.mjs";
115
121
  import { checkCultureParents } from "./culture-parents.mjs";
116
122
  import {
117
123
  checkAppearanceProfile,
@@ -154,6 +160,10 @@ import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
154
160
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
155
161
  import { checkSettlementTables } from "./settlement-tables.mjs";
156
162
  import { checkMapField } from "./text-maps.mjs";
163
+ import { checkConceptList, checkCoreFlag } from "./core-vocabulary.mjs";
164
+ import { METRE_KINDS, checkMetresField } from "./metre-declaration.mjs";
165
+ import { checkLanguageEmphasis, checkScriptField } from "./script-emphasis.mjs";
166
+ import { EMPHASIS_CONVENTIONS, LANGUAGE_EMPHASES } from "./script-emphasis-terms.mjs";
157
167
 
158
168
  /**
159
169
  * One `data:` key a note type may carry.
@@ -655,6 +665,67 @@ const ABBREVIATION_FIELD = Object.freeze({
655
665
  "note only. See [a language's words](format-details.md#a-languages-words).",
656
666
  });
657
667
 
668
+ /**
669
+ * A language's `data.core`: whether it must cover the core vocabulary.
670
+ * {@link checkCoreFlag} checks where it sits.
671
+ *
672
+ * @type {DataFieldSpec}
673
+ */
674
+ const CORE_FIELD = Object.freeze({
675
+ name: "core",
676
+ kind: "boolean",
677
+ shape: "`true` or `false`",
678
+ check: checkCoreFlag,
679
+ describe:
680
+ "Whether the language must cover the core vocabulary: `true` lists it in the " +
681
+ "`package-build core` report. A language skill note only. See " +
682
+ "[the core vocabulary](format-details.md#the-core-vocabulary).",
683
+ });
684
+
685
+ /**
686
+ * A `doc` note's `data.concepts`: the core vocabulary's concept list.
687
+ * {@link checkConceptList} checks what an entry means.
688
+ *
689
+ * @type {DataFieldSpec}
690
+ */
691
+ const CONCEPTS_FIELD = Object.freeze({
692
+ name: "concepts",
693
+ kind: "list",
694
+ shape: "list of `{ id, gloss, group }`",
695
+ check: checkConceptList,
696
+ entries: Object.freeze({
697
+ kind: "map",
698
+ shape: "a map — `{ id, gloss, group }`",
699
+ fields: Object.freeze([
700
+ Object.freeze({
701
+ name: "id",
702
+ kind: "string",
703
+ required: true,
704
+ shape: "lowercase letters, digits and `-`",
705
+ describe:
706
+ "The concept's id, which a word's `concept` names. Unique in the package.",
707
+ }),
708
+ Object.freeze({
709
+ name: "gloss",
710
+ kind: "string",
711
+ required: true,
712
+ shape: "a short gloss, such as `a smell; a scent`",
713
+ describe: "What the concept means, in brief.",
714
+ }),
715
+ Object.freeze({
716
+ name: "group",
717
+ kind: "string",
718
+ required: true,
719
+ shape: "a group name, such as `senses`",
720
+ describe: "The group the report lists the concept under.",
721
+ }),
722
+ ]),
723
+ }),
724
+ describe:
725
+ "The concepts of a core vocabulary, in the order the report lists them. See " +
726
+ "[the core vocabulary](format-details.md#the-core-vocabulary).",
727
+ });
728
+
658
729
  /**
659
730
  * A culture's or an affiliation's `data.glossary`: the terms that belong to
660
731
  * its people or members rather than to a language's grammar.
@@ -2096,8 +2167,64 @@ export const NOTE_VOCABULARY = Object.freeze({
2096
2167
  NAMES_FIELD,
2097
2168
  FAMILY_FIELD,
2098
2169
  ABBREVIATION_FIELD,
2170
+ CORE_FIELD,
2099
2171
  ...WORD_DATA_FIELDS,
2100
2172
  ...LANGUAGE_DATA_FIELDS,
2173
+ {
2174
+ name: "emphasis",
2175
+ kind: "string",
2176
+ shape: "`words`, `rubric` or `overline`",
2177
+ allowed: LANGUAGE_EMPHASES,
2178
+ check: checkLanguageEmphasis,
2179
+ describe:
2180
+ "How the language marks a stressed word, over its script's `emphasis`: " +
2181
+ "`rubric` or `overline` sets `*…*` and `**…**` so in a run of the language, " +
2182
+ "and `words` marks it with a word, so either is a finding. Only a skill of " +
2183
+ "`subType: language` states it.",
2184
+ },
2185
+ {
2186
+ name: "script",
2187
+ kind: "map",
2188
+ shape: "a map — `{ fonts, emphasis, strongEmphasis? }`",
2189
+ check: checkScriptField,
2190
+ fields: Object.freeze([
2191
+ {
2192
+ name: "fonts",
2193
+ kind: "list",
2194
+ shape: "list of installed faces' slugs",
2195
+ required: true,
2196
+ entries: Object.freeze({
2197
+ kind: "string",
2198
+ shape: "an installed face's slug",
2199
+ }),
2200
+ describe:
2201
+ "The faces the script is written in, by the slug `font=` names them by. " +
2202
+ "A face belongs to one script.",
2203
+ },
2204
+ {
2205
+ name: "emphasis",
2206
+ kind: "string",
2207
+ shape: "`rubric` or `overline`",
2208
+ allowed: EMPHASIS_CONVENTIONS,
2209
+ required: true,
2210
+ describe:
2211
+ "How the script marks a stressed word: `rubric` sets it in red, " +
2212
+ "`overline` draws a line above it. It sets `*…*` and `**…**` inside a " +
2213
+ "run in one of its faces, unless the run's language states its own " +
2214
+ "`emphasis`.",
2215
+ },
2216
+ {
2217
+ name: "strongEmphasis",
2218
+ kind: "string",
2219
+ shape: "`rubric` or `overline`",
2220
+ allowed: EMPHASIS_CONVENTIONS,
2221
+ describe: "The convention `**…**` takes instead of `emphasis`'s.",
2222
+ },
2223
+ ]),
2224
+ describe:
2225
+ "A script note's faces and how it marks emphasis. Only a skill of " +
2226
+ "`subType: script` states it.",
2227
+ },
2101
2228
  ]),
2102
2229
  }),
2103
2230
 
@@ -2176,14 +2303,20 @@ export const NOTE_VOCABULARY = Object.freeze({
2176
2303
  "howto",
2177
2304
  "concept",
2178
2305
  "settingguide",
2306
+ POST_SUBTYPE,
2179
2307
  ]),
2180
- // A setting guide introduces one culture and names it; no other genre
2181
- // states one.
2308
+ // A setting guide introduces one culture and names it, and a post
2309
+ // states the day it was published and who wrote it; no other genre
2310
+ // states either.
2182
2311
  check: (note, context) => [
2183
2312
  ...checkCitedPopulations(note, context),
2184
2313
  ...checkCultureSubType(note, context),
2314
+ ...checkPostFields(note),
2185
2315
  ],
2186
2316
  data: Object.freeze([
2317
+ CONCEPTS_FIELD,
2318
+ POST_DATE_FIELD,
2319
+ POST_AUTHOR_FIELD,
2187
2320
  {
2188
2321
  ...CULTURE_FIELD,
2189
2322
  describe:
@@ -2193,7 +2326,7 @@ export const NOTE_VOCABULARY = Object.freeze({
2193
2326
  {
2194
2327
  name: "map",
2195
2328
  kind: "map",
2196
- shape: "a map — `{ name, vowels?, fonts, rows }`",
2329
+ shape: "a map — `{ name, logographic?, vowels?, fonts, rows }`",
2197
2330
  check: checkMapField,
2198
2331
  fields: Object.freeze([
2199
2332
  {
@@ -2204,6 +2337,15 @@ export const NOTE_VOCABULARY = Object.freeze({
2204
2337
  "What `map=` names the map by: lowercase letters, digits and `-`, " +
2205
2338
  "unique in the package.",
2206
2339
  },
2340
+ {
2341
+ name: "logographic",
2342
+ kind: "boolean",
2343
+ shape: "`true`",
2344
+ describe:
2345
+ "The script writes meanings: a mapped run's words are written by " +
2346
+ "the language's lexicon alone, and the rows write only the text " +
2347
+ "between them.",
2348
+ },
2207
2349
  {
2208
2350
  name: "vowels",
2209
2351
  ...TEXT,
@@ -2261,6 +2403,48 @@ export const NOTE_VOCABULARY = Object.freeze({
2261
2403
  "before it is set. See " +
2262
2404
  "[mapping notes](format-details.md#mapping-notes).",
2263
2405
  },
2406
+ {
2407
+ name: "metres",
2408
+ kind: "list",
2409
+ shape: "list of `{ name, beats, kind }`",
2410
+ check: checkMetresField,
2411
+ entries: Object.freeze({
2412
+ kind: "map",
2413
+ shape: "a map — `{ name, beats, kind }`",
2414
+ fields: Object.freeze([
2415
+ {
2416
+ name: "name",
2417
+ ...TEXT,
2418
+ required: true,
2419
+ describe:
2420
+ "What a verse fence's `scan=` names the metre by: lowercase " +
2421
+ "letters, digits and `-`, unique in the package.",
2422
+ },
2423
+ {
2424
+ name: "beats",
2425
+ kind: "integer",
2426
+ shape: "a whole number above zero",
2427
+ required: true,
2428
+ describe: "The beats of one line.",
2429
+ },
2430
+ {
2431
+ name: "kind",
2432
+ kind: "string",
2433
+ allowed: METRE_KINDS,
2434
+ shape: `one of ${METRE_KINDS.join(", ")}`,
2435
+ required: true,
2436
+ describe:
2437
+ "How the beats are counted: `accentual`, one beat for each " +
2438
+ "stressed syllable the language's stress rule and the beat " +
2439
+ "convention give.",
2440
+ },
2441
+ ]),
2442
+ }),
2443
+ describe:
2444
+ "The metres a verse fence's `scan=` names. A fence that states one is " +
2445
+ "warned at each line that cannot have its beats. See " +
2446
+ "[metres](../authoring/links-and-markup.md#metre).",
2447
+ },
2264
2448
  ]),
2265
2449
  }),
2266
2450
 
@@ -26,11 +26,12 @@
26
26
  * has found a bug in one of them, so the book does not get its own walk, its
27
27
  * own frontmatter reader or its own link resolver. It collects pages with
28
28
  * {@link module:engine/site-build.collectContentPages}, indexes them with
29
- * `siteGates`, expands tables with `expandContentTables` and resolves links
30
- * with `resolveWebWikilinks` — the same four calls, in the same order, that
31
- * `renderPages` makes. What differs is only the last step and the *selection*:
32
- * the site publishes every page, and the book publishes what its document tree
33
- * asked for.
29
+ * `siteGates`, and expands each body with
30
+ * {@link module:engine/content-expand.expandContent} for the `book` target —
31
+ * the same calls the site makes. What differs is how a link is written, the
32
+ * figure numbering, which runs across the whole book, the last step and the
33
+ * *selection*: the site publishes every page, and the book publishes what its
34
+ * document tree asked for.
34
35
  *
35
36
  * The book builds when the authored tree contains content pages. The same
36
37
  * {@link module:content-config.publishesContentPages} decision governs the
@@ -97,20 +98,9 @@ import {
97
98
  } from "./pdf-pages.mjs";
98
99
  import { cachedDependencies, openNotesDatabase, prepareTreeSqlTables } from "./sql-tables.mjs";
99
100
  import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
100
- import {
101
- collectContentPages,
102
- siteGates,
103
- tableUniverse,
104
- gatesFailed,
105
- resolveSitePass,
106
- } from "./site-build.mjs";
107
- import {
108
- figureIndexKeys,
109
- resolveCrossNoteFigures,
110
- resolveInfoboxRef,
111
- wikiContext,
112
- } from "./site-index.mjs";
113
- import { resolveWebWikilinks } from "./web-wikilinks.mjs";
101
+ import { collectContentPages, siteGates, gatesFailed, resolveSitePass } from "./site-build.mjs";
102
+ import { figureIndexKeys, resolveInfoboxRef } from "./site-index.mjs";
103
+ import { expandContent, expandTables } from "./content-expand.mjs";
114
104
  import { linkFindingMessage } from "./wikilink-syntax.mjs";
115
105
  import {
116
106
  positionOfFrontmatterPath,
@@ -120,14 +110,11 @@ import {
120
110
  } from "./diagnostics.mjs";
121
111
  import { artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
122
112
  import { embedRole } from "./content-embeds.mjs";
123
- import { expandContentTables } from "./content-tables.mjs";
124
113
  import { seeAlsoEntries, seeAlsoMarkdown, seeAlsoWebLink } from "./see-also.mjs";
125
- import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
126
114
  import { eventNoteIndex } from "./event-fields.mjs";
127
115
  import { numberFigures, scanFigures } from "./content-figures.mjs";
128
- import { collectAnchors } from "./anchors.mjs";
129
- import { protectCode } from "./code-fences.mjs";
130
116
  import { imagesIn, parseImageDirective } from "./content-images.mjs";
117
+ import { symbolImagesIn } from "./content-symbols.mjs";
131
118
  import {
132
119
  bookImageWidthIn,
133
120
  PDF_IMAGE_INCHES,
@@ -159,7 +146,7 @@ import { infoboxTypstPreamble, infoboxesToTypst, linkToTypst } from "./infobox-r
159
146
  import { noteInfoboxes } from "./infobox-registry.mjs";
160
147
  import { resolveIconGlyphs } from "./pdf-fonts.mjs";
161
148
  import { drawMapsFrom } from "./derived-sections.mjs";
162
- import { generatedDrawing, markGenerated, splitGenerated } from "./generated-sections.mjs";
149
+ import { generatedDrawing } from "./generated-sections.mjs";
163
150
  import { resolveAssetReplacement } from "./asset-replacement.mjs";
164
151
  import { typstScriptFontPath } from "./script-font-delivery.mjs";
165
152
 
@@ -1041,7 +1028,6 @@ async function prepareWorld(resolved, { contentBase, findings, resolveReplacemen
1041
1028
  }
1042
1029
 
1043
1030
  const byFile = new Map(collected.pages.map((page) => [page.file, page]));
1044
- const universe = tableUniverse(collected.pages);
1045
1031
  // The address space an `![[…]]` embed resolves against — the same one the
1046
1032
  // site builds, so one authored picture reaches both surfaces or neither.
1047
1033
  const assets = assetAddressIndex(records, {
@@ -1070,7 +1056,6 @@ async function prepareWorld(resolved, { contentBase, findings, resolveReplacemen
1070
1056
  site,
1071
1057
  gates,
1072
1058
  byFile,
1073
- universe,
1074
1059
  sqlTables,
1075
1060
  assets,
1076
1061
  md,
@@ -1196,8 +1181,7 @@ async function buildBook(book, shared) {
1196
1181
  const staged = stagedImagePath(src, resolved, world.stagingOpts);
1197
1182
  return staged && { ...staged, to: inBook(staged.to) };
1198
1183
  };
1199
- const { records, site, gates, byFile, universe, sqlTables, assets, md, glyphs, stagingOpts } =
1200
- world;
1184
+ const { records, site, gates, byFile, sqlTables, assets, md, glyphs, stagingOpts } = world;
1201
1185
 
1202
1186
  fs.mkdirSync(outDir, { recursive: true });
1203
1187
 
@@ -1244,7 +1228,14 @@ async function buildBook(book, shared) {
1244
1228
  }
1245
1229
  return positionOfLiteral(raw, src, occurrence);
1246
1230
  };
1247
- for (const image of imagesIn(body)) {
1231
+ // A glyph or a cartouche is a picture the package ships, staged like
1232
+ // any other and located in the note by its slug.
1233
+ const symbols = symbolImagesIn(body).map(({ src, locate }) => ({
1234
+ src,
1235
+ locate,
1236
+ directive: "",
1237
+ }));
1238
+ for (const image of [...imagesIn(body), ...symbols]) {
1248
1239
  const src = image.src;
1249
1240
  const occurrence = (seenHere.get(src) ?? 0) + 1;
1250
1241
  seenHere.set(src, occurrence);
@@ -1287,7 +1278,7 @@ async function buildBook(book, shared) {
1287
1278
  if (problem) {
1288
1279
  findings.push({
1289
1280
  file,
1290
- ...where(src, occurrence),
1281
+ ...where(image.locate ?? src, occurrence),
1291
1282
  severity: "error",
1292
1283
  message: problem,
1293
1284
  });
@@ -1299,7 +1290,7 @@ async function buildBook(book, shared) {
1299
1290
  } catch (err) {
1300
1291
  findings.push({
1301
1292
  file,
1302
- ...where(src, occurrence),
1293
+ ...where(image.locate ?? src, occurrence),
1303
1294
  severity: "error",
1304
1295
  message: err.message,
1305
1296
  });
@@ -1308,7 +1299,7 @@ async function buildBook(book, shared) {
1308
1299
  if (!staged) {
1309
1300
  findings.push({
1310
1301
  file,
1311
- ...where(src, occurrence),
1302
+ ...where(image.locate ?? src, occurrence),
1312
1303
  severity: "warning",
1313
1304
  message:
1314
1305
  `\`${src}\` names a file this package does not ship, so the book ` +
@@ -1324,7 +1315,7 @@ async function buildBook(book, shared) {
1324
1315
  } catch (err) {
1325
1316
  findings.push({
1326
1317
  file,
1327
- ...where(src, occurrence),
1318
+ ...where(image.locate ?? src, occurrence),
1328
1319
  severity: "warning",
1329
1320
  message:
1330
1321
  `\`${src}\` cannot be read from ` +
@@ -1389,12 +1380,12 @@ async function buildBook(book, shared) {
1389
1380
  * order, before any entry's expressions render — so a cross-note `ref`
1390
1381
  * finds the number its target carries in the finished book rather than a
1391
1382
  * page-local recount. Walked in the same order {@link bodies} fills
1392
- * below, so `figureCounts` lands on the same final tallies either way;
1393
- * the note's table expansion is cached here too, so the render loop does
1394
- * not redo it.
1383
+ * below, so `figureCounts` lands on the same final tallies either way. A
1384
+ * note's {@link module:engine/content-expand.expandTables} is kept here
1385
+ * with its figures numbered, and the render loop expands the rest from it.
1395
1386
  *
1396
- * @type {Map<string, {markdown?: string, errors?: object[],
1397
- * lineMap?: object[], numberedFigures: object[]}>}
1387
+ * @type {Map<string, {staged?: import("./content-expand.mjs").ExpandedTables,
1388
+ * numberedFigures: object[]}>}
1398
1389
  */
1399
1390
  const prepared = new Map();
1400
1391
  // The map from each selected place given a From here section, drawn before
@@ -1424,25 +1415,18 @@ async function buildBook(book, shared) {
1424
1415
  const file = noteFile(contentBase, entry.record);
1425
1416
  const page = byFile.get(file);
1426
1417
  if (!page) continue; // reported as `missing` by the render loop below
1427
- const src = page.relPath ?? page.base;
1428
- const { markdown, errors, lineMap, generatedFrom } = expandContentTables(page.body, {
1429
- docs: universe.get(page.pkg) ?? [],
1430
- linkable: (d) => Boolean(d.fm.shortcode),
1431
- source: src,
1432
- sqlTables: sqlTables?.get(page.file),
1433
- pageLists: sqlTables?.pageLists?.get(page.file),
1434
- self: { fm: page.fm, path: page.relPath },
1418
+ const staged = expandTables(page.body, {
1419
+ source: page.relPath ?? page.base,
1420
+ prepared: {
1421
+ sqlTables: sqlTables?.get(page.file),
1422
+ pageLists: sqlTables?.pageLists?.get(page.file),
1423
+ },
1424
+ resolveRole,
1435
1425
  });
1436
- const numberedFigures = numberFigures(
1437
- scanFigures(markdown, { resolveRole }).figures,
1438
- figureCounts,
1439
- );
1426
+ const numberedFigures = numberFigures(staged.figures.figures, figureCounts);
1440
1427
  prepared.set(entry.anchor, {
1441
- markdown,
1442
- errors,
1443
- lineMap,
1428
+ staged: { ...staged, figures: { ...staged.figures, figures: numberedFigures } },
1444
1429
  numberedFigures,
1445
- generatedFrom,
1446
1430
  });
1447
1431
  const shortcode = page.fm.shortcode;
1448
1432
  if (typeof shortcode === "string" && shortcode) {
@@ -1482,15 +1466,36 @@ async function buildBook(book, shared) {
1482
1466
  }
1483
1467
 
1484
1468
  const renderPage = (page, headingOffset, anchorPrefix, columns) => {
1485
- const src = page.relPath ?? page.base;
1486
- const wikiErrors = [];
1487
- const { markdown, errors, lineMap, numberedFigures, generatedFrom } =
1488
- prepared.get(anchorPrefix);
1469
+ const { staged, numberedFigures } = prepared.get(anchorPrefix);
1470
+ // The book's own figure numbering, from `prepared`: the book numbers
1471
+ // each kind across the whole book in reading order, so a reference
1472
+ // reads the number the book gives the figure, not the number the note
1473
+ // would give it alone.
1474
+ const expanded = expandContent(page.body, {
1475
+ target: "book",
1476
+ index: gates.index,
1477
+ note: {
1478
+ fm: page.fm,
1479
+ file: page.file,
1480
+ source: page.relPath ?? page.base,
1481
+ bodyLine: page.bodyLine,
1482
+ },
1483
+ prepared: { inline: sqlTables?.inline?.get(page.file) },
1484
+ events,
1485
+ figuresByAddress,
1486
+ tables: staged,
1487
+ links: {
1488
+ foreign: gates.foreign.index,
1489
+ assets,
1490
+ beforeLinks: pass.beforeLinks ? (text) => pass.beforeLinks(text, page) : undefined,
1491
+ },
1492
+ });
1493
+ const { lineMap, generatedFrom } = expanded;
1489
1494
  // A content-table finding states its `reason`, its 0-based line within
1490
1495
  // the body and its column, exactly as the site build reports them.
1491
1496
  // Reading it as `message` yields `[object Object]` and discards the
1492
1497
  // position, which is the one thing a reader needs to find the fence.
1493
- for (const err of errors) {
1498
+ for (const err of expanded.findings.tables) {
1494
1499
  findings.push({
1495
1500
  file: page.file,
1496
1501
  ...(typeof err.line === "number" ?
@@ -1500,77 +1505,7 @@ async function buildBook(book, shared) {
1500
1505
  message: err.reason ?? String(err.message ?? err),
1501
1506
  });
1502
1507
  }
1503
- const linkCtx = wikiContext(gates.index, {
1504
- src,
1505
- file: page.file,
1506
- type: page.fm.type ?? null,
1507
- errors: wikiErrors,
1508
- foreignIndex: gates.foreign.index,
1509
- assets,
1510
- });
1511
- linkCtx.captionLabels = new Map(
1512
- numberedFigures
1513
- .filter((figure) => figure.id)
1514
- .map((figure) => [figure.id, figure.label]),
1515
- );
1516
- // This page's own anchors, so a `[[#slug]]` self-link is checked
1517
- // against what the page actually declares, exactly as the site build
1518
- // checks it.
1519
- linkCtx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
1520
- linkCtx.output = "book";
1521
- // What the `ref` expression helper needs beyond the label — the
1522
- // caption text and whether one was authored — keyed as
1523
- // `engine/site-build.mjs` keys its own, but built from this note's
1524
- // *book-wide* numbering rather than its page-local count: the book
1525
- // numbers each kind across the whole book in reading order, so a
1526
- // reference has to read the number the book gives the figure, not the
1527
- // number the note would give it alone.
1528
- const figuresById = new Map(
1529
- numberedFigures
1530
- .filter((figure) => figure.id)
1531
- .map((figure) => [
1532
- figure.id,
1533
- { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
1534
- ]),
1535
- );
1536
- // Code fences are protected for the same reason every other pass
1537
- // protects them: a wikilink shown as an example is prose about a
1538
- // wikilink, and resolving it would make the example impossible to write.
1539
- // Where the generated sections begin is marked before any pass can move
1540
- // a line, and the body is split there once its links resolve: the
1541
- // sections are set by the rules for generated links — see
1542
- // `renderLink` in `engine/pdf-render.mjs`.
1543
- const expressions = renderMarkdownExpressions(markGenerated(markdown, generatedFrom), {
1544
- fm: page.fm,
1545
- dates: gates.index.dateContext,
1546
- sqlResults: sqlTables?.inline?.get(page.file),
1547
- file: page.file,
1548
- bodyLine: page.bodyLine,
1549
- events,
1550
- figures: {
1551
- get: (id) => figuresById.get(id),
1552
- // A cross-note `ref` resolves the written address exactly as
1553
- // a wikilink does, against every note's figures numbered
1554
- // before any entry's expressions render — see `prepared`
1555
- // above. The default link renders the target's own web
1556
- // address, which `renderLink` in `engine/pdf-render.mjs`
1557
- // resolves to the entry the book gave it, exactly as an
1558
- // ordinary cross-note wikilink does.
1559
- note: (target) =>
1560
- resolveCrossNoteFigures(target, {
1561
- contentTypes: gates.index.contentTypes,
1562
- packages: gates.index.packages,
1563
- noIndexPackages: gates.index.noIndexPackages,
1564
- contentPackage: gates.index.contentPackage,
1565
- siteIndexMap: gates.index.index,
1566
- figuresByAddress,
1567
- }),
1568
- },
1569
- });
1570
- findings.push(...expressions.findings);
1571
- const resolvedBody = protectCode(expressions.markdown, (text) =>
1572
- resolveWebWikilinks(pass.beforeLinks ? pass.beforeLinks(text, page) : text, linkCtx),
1573
- );
1508
+ findings.push(...expanded.findings.expressions);
1574
1509
  // A link finding carries the authored link and which occurrence of it
1575
1510
  // this was, which is exactly what locates it: the position is implicit
1576
1511
  // in the literal the pass matched, so it is recovered by searching the
@@ -1594,7 +1529,7 @@ async function buildBook(book, shared) {
1594
1529
  // The source is written and the compiler still runs, so the finding is a
1595
1530
  // report on a book that was built, exactly as the site writes its pages
1596
1531
  // and refuses the run.
1597
- for (const err of wikiErrors) {
1532
+ for (const err of expanded.findings.links) {
1598
1533
  findings.push({
1599
1534
  file: page.file,
1600
1535
  ...positionOfLiteral(noteSource(), err.link, err.occurrence),
@@ -1605,8 +1540,8 @@ async function buildBook(book, shared) {
1605
1540
  message: err.message ?? (err.reason ? linkFindingMessage(err) : String(err)),
1606
1541
  });
1607
1542
  }
1608
- stageImages(resolvedBody, page.file, columns);
1609
- const { authored, generated } = splitGenerated(resolvedBody);
1543
+ stageImages(expanded.markdown, page.file, columns);
1544
+ const { authored, generated } = expanded;
1610
1545
  const typst = (text, options) =>
1611
1546
  markdownToTypst(text, {
1612
1547
  md,