@heroiclands/package-build 22.15.0 → 22.16.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 (223) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/itinerary-scenes.mjs +4 -2
  90. package/engine/journals.mjs +51 -10
  91. package/engine/macros.mjs +1 -1
  92. package/engine/map-build.mjs +1 -1
  93. package/engine/map-dot.mjs +1 -1
  94. package/engine/map-raster.mjs +4 -2
  95. package/engine/markdown-expressions.mjs +117 -0
  96. package/engine/metadata-index.mjs +7 -3
  97. package/engine/note-addresses.mjs +1 -0
  98. package/engine/note-dates.mjs +283 -61
  99. package/engine/note-package.mjs +5 -25
  100. package/engine/note-state.mjs +6 -2
  101. package/engine/note-vocabulary.mjs +74 -19
  102. package/engine/number-words.mjs +75 -0
  103. package/engine/office-holders.mjs +188 -0
  104. package/engine/pack-config.mjs +2 -15
  105. package/engine/pack-router.mjs +24 -5
  106. package/engine/packages.mjs +3 -2
  107. package/engine/pathnames.mjs +21 -24
  108. package/engine/pdf-build.mjs +217 -55
  109. package/engine/pdf-fonts.mjs +2 -2
  110. package/engine/pdf-images.mjs +82 -0
  111. package/engine/pdf-render.mjs +174 -22
  112. package/engine/populations.mjs +3 -2
  113. package/engine/project-init.mjs +499 -0
  114. package/engine/prose-config.mjs +1 -1
  115. package/engine/prose-lint.mjs +7 -6
  116. package/engine/readability-lint.mjs +86 -0
  117. package/engine/reckoning-markers.mjs +205 -93
  118. package/engine/scenes.mjs +5 -2
  119. package/engine/schema-check.mjs +3 -3
  120. package/engine/site-build.mjs +87 -37
  121. package/engine/site-config.mjs +8 -8
  122. package/engine/site-index.mjs +4 -0
  123. package/engine/site-maps.mjs +1 -1
  124. package/engine/site-preview.mjs +19 -8
  125. package/engine/social-ties.mjs +5 -1
  126. package/engine/sql-tables.mjs +92 -22
  127. package/engine/web-wikilinks.mjs +15 -31
  128. package/engine/wikilink-syntax.mjs +4 -1
  129. package/engine/wikilinks.mjs +12 -14
  130. package/hm3/item-fields.mjs +1 -1
  131. package/hm3/items.mjs +12 -45
  132. package/index.mjs +2 -14
  133. package/manifest.mjs +31 -4
  134. package/markdownlint-config.mjs +1 -1
  135. package/package.json +32 -11
  136. package/prettier-config.mjs +1 -1
  137. package/sohl/being-info.mjs +3 -145
  138. package/sohl/index.mjs +1 -1
  139. package/sohl/infobox.mjs +2 -1
  140. package/sohl/item-builders.mjs +3 -27
  141. package/sohl/item-fields.mjs +61 -22
  142. package/sohl/kb-passes.mjs +2 -2
  143. package/sohl/note-schemas.mjs +0 -8
  144. package/stage.mjs +19 -0
  145. package/types/config.d.mts +12 -3
  146. package/types/content-config.d.mts +15 -81
  147. package/types/engine/address-render.d.mts +13 -0
  148. package/types/engine/address.d.mts +2 -11
  149. package/types/engine/anchors.d.mts +3 -4
  150. package/types/engine/asset-bindings.d.mts +9 -2
  151. package/types/engine/base-compiler.d.mts +1 -2
  152. package/types/engine/being-measurements.d.mts +16 -0
  153. package/types/engine/calendar-choice.d.mts +14 -0
  154. package/types/engine/calendar-human.d.mts +4 -0
  155. package/types/engine/calendar-notes.d.mts +2 -0
  156. package/types/engine/calendars.d.mts +5 -3
  157. package/types/engine/code-fences.d.mts +6 -72
  158. package/types/engine/compile-corpus.d.mts +1 -3
  159. package/types/engine/content-admonitions.d.mts +24 -0
  160. package/types/engine/content-captions.d.mts +45 -0
  161. package/types/engine/content-footnotes.d.mts +10 -0
  162. package/types/engine/content-format-check.d.mts +17 -4
  163. package/types/engine/content-format.d.mts +24 -11
  164. package/types/engine/content-icons.d.mts +14 -23
  165. package/types/engine/content-images.d.mts +2 -2
  166. package/types/engine/content-index.d.mts +9 -2
  167. package/types/engine/content-links.d.mts +3 -5
  168. package/types/engine/content-tables.d.mts +11 -140
  169. package/types/engine/culture-choice.d.mts +14 -0
  170. package/types/engine/date-conversion.d.mts +4 -0
  171. package/types/engine/declaration-check.d.mts +24 -0
  172. package/types/engine/docs-checks.d.mts +28 -0
  173. package/types/engine/documentation-links.d.mts +7 -0
  174. package/types/engine/exported-scene.d.mts +6 -2
  175. package/types/engine/extension-attributes.d.mts +14 -0
  176. package/types/engine/field-spec.d.mts +9 -0
  177. package/types/engine/format-generated.d.mts +7 -0
  178. package/types/engine/frontmatter.d.mts +14 -4
  179. package/types/engine/generate.d.mts +1 -1
  180. package/types/engine/helpers.d.mts +37 -92
  181. package/types/engine/homepage.d.mts +2 -3
  182. package/types/engine/index.d.mts +5 -0
  183. package/types/engine/infobox-registry.d.mts +3 -1
  184. package/types/engine/infobox.d.mts +17 -6
  185. package/types/engine/item-compiler.d.mts +4 -4
  186. package/types/engine/item-docs.d.mts +4 -3
  187. package/types/engine/journals.d.mts +10 -7
  188. package/types/engine/map-graphviz.d.mts +1 -1
  189. package/types/engine/markdown-expressions.d.mts +17 -0
  190. package/types/engine/note-dates.d.mts +47 -1
  191. package/types/engine/note-package.d.mts +3 -19
  192. package/types/engine/note-state.d.mts +2 -1
  193. package/types/engine/note-vocabulary.d.mts +9 -0
  194. package/types/engine/number-words.d.mts +4 -0
  195. package/types/engine/office-holders.d.mts +14 -0
  196. package/types/engine/pack-config.d.mts +1 -1
  197. package/types/engine/packages.d.mts +3 -2
  198. package/types/engine/pdf-images.d.mts +38 -0
  199. package/types/engine/populations.d.mts +2 -2
  200. package/types/engine/project-init.d.mts +17 -0
  201. package/types/engine/prose-config.d.mts +1 -1
  202. package/types/engine/prose-lint.d.mts +3 -2
  203. package/types/engine/readability-lint.d.mts +22 -0
  204. package/types/engine/reckoning-markers.d.mts +4 -2
  205. package/types/engine/schema-check.d.mts +3 -3
  206. package/types/engine/site-build.d.mts +38 -28
  207. package/types/engine/site-config.d.mts +2 -2
  208. package/types/engine/site-index.d.mts +4 -0
  209. package/types/engine/site-preview.d.mts +1 -1
  210. package/types/engine/social-ties.d.mts +8 -2
  211. package/types/engine/sql-tables.d.mts +13 -2
  212. package/types/engine/web-wikilinks.d.mts +2 -1
  213. package/types/engine/wikilinks.d.mts +7 -1
  214. package/types/hm3/items.d.mts +5 -6
  215. package/types/labels.d.mts +40 -0
  216. package/types/manifest.d.mts +3 -3
  217. package/types/sohl/actors.d.mts +2 -2
  218. package/types/sohl/being-info.d.mts +3 -43
  219. package/types/sohl/index.d.mts +1 -1
  220. package/types/sohl/infobox.d.mts +5 -2
  221. package/types/sohl/item-fields.d.mts +4 -13
  222. package/types/stage.d.mts +9 -0
  223. package/CONTENT.md +0 -3239
@@ -42,7 +42,7 @@
42
42
  * documentation entry reuses its document's folder verbatim.
43
43
  *
44
44
  * Not a standalone script — exports the `Journals` compiler class, imported
45
- * and driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
45
+ * and driven by `packages/package-build/engine/generate.mjs` (via `npm run build:compiledb`).
46
46
  *
47
47
  * The walk itself — filtering by type, expanding tables, converting
48
48
  * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
@@ -57,16 +57,20 @@ import {
57
57
  resolveName,
58
58
  defaultStats,
59
59
  renderFoundryMarkdown,
60
+ md,
60
61
  folderField,
61
62
  } from "./helpers.mjs";
62
63
  import { BasePackCompiler } from "./base-compiler.mjs";
63
64
  import { anchorPageId, resolveReference } from "./wikilinks.mjs";
64
65
  import { infoboxesToHtml, linkToUuid } from "./infobox-render.mjs";
65
66
  import { noteInfoboxes } from "./infobox-registry.mjs";
67
+ import { reckoningContext } from "./reckoning-markers.mjs";
66
68
  import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
67
69
  import { JOURNAL_TYPES } from "./ids.mjs";
68
70
  import { journalHasContent } from "./note-state.mjs";
69
71
  import { draftNoticeFor } from "./draft-notice.mjs";
72
+ import { scanCaptions } from "./content-captions.mjs";
73
+ import { separateFootnotes } from "./content-footnotes.mjs";
70
74
 
71
75
  /**
72
76
  * Splits a markdown body into pages by top-level H1 headings. Fenced
@@ -74,7 +78,7 @@ import { draftNoticeFor } from "./draft-notice.mjs";
74
78
  * a split. Content before the first H1 (if non-empty) becomes a leading
75
79
  * page. Each H1 yields a page whose name is the heading
76
80
  * text (with any `{#anchor-id}` suffix stripped out and surfaced as
77
- * `anchorId`).
81
+ * `anchorSlug`).
78
82
  *
79
83
  * `leadName` names that leading page. A journal note's is "Introduction",
80
84
  * because it introduces the pages that follow. An item doc's is the item — a
@@ -82,10 +86,21 @@ import { draftNoticeFor } from "./draft-notice.mjs";
82
86
  * calling that page "Introduction" would label the description as a preamble to
83
87
  * nothing.
84
88
  *
85
- * Returns an array of `{ name, anchorId, markdown }` in document order.
89
+ * @param {string} body - Markdown body to split.
90
+ * @param {string} [leadName] - Name of the page before the first heading.
91
+ * @returns {Array<{name: string, anchorSlug: string|null, level: number,
92
+ * markdown: string}>} Pages in document order.
86
93
  */
87
94
  export function splitPages(body, leadName = "Introduction") {
88
- const lines = body.split("\n");
95
+ const { markdown, definitions } = separateFootnotes(body);
96
+ const lines = markdown.split("\n");
97
+ const captions = scanCaptions(markdown).captions;
98
+ const captionStarts = new Map(captions.map((caption) => [caption.line - 1, caption]));
99
+ const captionedHeadings = new Set(
100
+ captions
101
+ .filter((caption) => /^\s*#{1,6}\s/.test(lines[caption.blockStart] ?? ""))
102
+ .map((caption) => caption.blockStart),
103
+ );
89
104
  const pages = [];
90
105
  const beforeFirstH1 = [];
91
106
  let current = null;
@@ -103,7 +118,7 @@ export function splitPages(body, leadName = "Introduction") {
103
118
  current = null;
104
119
  };
105
120
 
106
- for (const line of lines) {
121
+ for (const [lineIndex, line] of lines.entries()) {
107
122
  if (line.trim().startsWith("```")) {
108
123
  inCodeBlock = !inCodeBlock;
109
124
  }
@@ -118,7 +133,18 @@ export function splitPages(body, leadName = "Introduction") {
118
133
  const rawHeading = headingMatch?.[2]?.trim();
119
134
  const anchorMatch = rawHeading?.match(/^(.*?)\s*\{#([^}]+)\}\s*$/);
120
135
  const startsPage = headingMatch && (headingMatch[1].length === 1 || anchorMatch);
121
- if (startsPage) {
136
+ const caption = !inCodeBlock && !inSecret ? captionStarts.get(lineIndex) : null;
137
+ if (caption) {
138
+ closeCurrent();
139
+ current = {
140
+ name: caption.label,
141
+ anchorSlug: caption.id,
142
+ level: 1,
143
+ lines: [line],
144
+ };
145
+ continue;
146
+ }
147
+ if (startsPage && !captionedHeadings.has(lineIndex)) {
122
148
  closeCurrent();
123
149
  current = {
124
150
  name: (anchorMatch ? anchorMatch[1] : rawHeading).trim(),
@@ -147,6 +173,9 @@ export function splitPages(body, leadName = "Introduction") {
147
173
  });
148
174
  }
149
175
 
176
+ if (definitions) {
177
+ for (const page of pages) page.markdown += `\n\n${definitions}`;
178
+ }
150
179
  return pages;
151
180
  }
152
181
 
@@ -214,7 +243,7 @@ export function assertUniqueAnchors(rawPages, noteName) {
214
243
  * An anchored page takes the id its inbound links compute from the note id and
215
244
  * the slug, so link and page agree without shared state. Every other page is
216
245
  * keyed by its **name**, which is what lets the items pass address an item
217
- * doc's first page without having compiled it (see
246
+ * doc's selected page without having compiled it (see
218
247
  * {@link sohl.utils.packs.itemDocPointer}).
219
248
  *
220
249
  * **It takes no index**. Keying a page by position *and* name,
@@ -250,13 +279,21 @@ export function journalPageId(entryId, page) {
250
279
  * documents, in order.
251
280
  * @throws {Error} When the note has no content at all, or repeats an anchor.
252
281
  */
253
- export function buildPages(rawPages, entryId, noteName) {
282
+ export function buildPages(rawPages, entryId, noteName, captions) {
254
283
  if (rawPages.length === 0) {
255
284
  throw new Error(
256
285
  `note "${noteName}" has no Introduction content and no H1 headings — nothing to compile`,
257
286
  );
258
287
  }
259
288
  assertUniquePages(rawPages, noteName);
289
+ const footnoteNumbers = new Map();
290
+ for (const page of rawPages) {
291
+ const env = {};
292
+ md.parse(page.markdown ?? "", env);
293
+ for (const { label } of env.footnotes?.list ?? []) {
294
+ if (!footnoteNumbers.has(label)) footnoteNumbers.set(label, footnoteNumbers.size + 1);
295
+ }
296
+ }
260
297
  return rawPages.map((page) => {
261
298
  const pageId = journalPageId(entryId, page);
262
299
  return {
@@ -266,7 +303,10 @@ export function buildPages(rawPages, entryId, noteName) {
266
303
  title: { show: true, level: page.level ?? 1 },
267
304
  text: {
268
305
  format: 1,
269
- content: page.markdown ? renderFoundryMarkdown(page.markdown) : "",
306
+ content:
307
+ page.markdown ?
308
+ renderFoundryMarkdown(page.markdown, captions, footnoteNumbers, pageId)
309
+ : "",
270
310
  },
271
311
  _key: `!journal.pages!${entryId}.${pageId}`,
272
312
  };
@@ -321,7 +361,7 @@ export function buildJournalEntry({
321
361
  notice = "",
322
362
  }) {
323
363
  const rawPages = splitPages(markdown, leadName);
324
- const pages = buildPages(rawPages, id, name);
364
+ const pages = buildPages(rawPages, id, name, scanCaptions(markdown).captions);
325
365
  if (infobox.trim() && pages.length) {
326
366
  pages[0].text.content = `${infobox}\n${pages[0].text.content}`;
327
367
  }
@@ -475,6 +515,7 @@ export class Journals extends BasePackCompiler {
475
515
  // This compile's own router, so the panel's `available` is decided
476
516
  // by the pack list this build is being driven by.
477
517
  router: this.router,
518
+ dates: reckoningContext(this.linkIndex),
478
519
  });
479
520
 
480
521
  return buildJournalEntry({
package/engine/macros.mjs CHANGED
@@ -42,7 +42,7 @@
42
42
  * Nothing here evaluates, compiles, or revives anything.
43
43
  *
44
44
  * Not a standalone script — exports the `Macros` compiler class, imported and
45
- * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
45
+ * driven by `packages/package-build/engine/generate.mjs` (via `npm run build:compiledb`).
46
46
  *
47
47
  * The walk itself — filtering by type, expanding tables, converting
48
48
  * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * `content-build map`: the drawings, written under `build/map/`.
15
+ * `package-build map`: the drawings, written under `build/map/`.
16
16
  *
17
17
  * Four modes, each a function of the world the content index describes:
18
18
  *
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The drawings of `content-build map`, as DOT.
15
+ * The drawings of `package-build map`, as DOT.
16
16
  *
17
17
  * Every drawing is emitted as DOT text and kept beside its rendering, so a
18
18
  * picture can be re-rendered, diffed or read without the content index.
@@ -5,10 +5,12 @@
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { Resvg } from "@resvg/resvg-js";
8
+ import { resolvePackageBuildConfig } from "../config.mjs";
8
9
 
9
10
  /** Rasterize an authored SVG map into the staged Foundry package. */
10
11
  export function rasterizeMapSvg({ foundryPath, width, height, sceneId, config }) {
11
12
  if (!config) throw new Error("SVG map rasterization requires the package build configuration");
13
+ const buildConfig = resolvePackageBuildConfig(config);
12
14
  if (![width, height].every((value) => Number.isInteger(value) && value > 0)) {
13
15
  throw new Error("an SVG map needs `sohl.dimensions: [width, height]` in whole pixels");
14
16
  }
@@ -25,7 +27,7 @@ export function rasterizeMapSvg({ foundryPath, width, height, sceneId, config })
25
27
  }
26
28
  const staged = path.resolve(
27
29
  config.rootDir,
28
- config.packageBuild.stageDir,
30
+ buildConfig.stageDir,
29
31
  "assets/maps/rasterized",
30
32
  `${sceneId}.png`,
31
33
  );
@@ -45,5 +47,5 @@ export function rasterizeMapSvg({ foundryPath, width, height, sceneId, config })
45
47
  cause: error,
46
48
  });
47
49
  }
48
- return `${config.packageKind}/${config.foundryPackage.id}/assets/maps/rasterized/${sceneId}.png`;
50
+ return `${buildConfig.packageKind}/${buildConfig.packageId}/assets/maps/rasterized/${sceneId}.png`;
49
51
  }
@@ -0,0 +1,117 @@
1
+ /* SPDX-License-Identifier: GPL-3.0-or-later */
2
+
3
+ import Handlebars from "handlebars";
4
+
5
+ import { isAddressTuple, renderAddress } from "./address.mjs";
6
+ import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
7
+ import { formatDateInCalendar, parseNoteDate } from "./note-dates.mjs";
8
+ import { numberWords, numberDigits } from "./number-words.mjs";
9
+
10
+ // Hugo shortcodes begin `{{<` or `{{%` and stay in the source for Hugo.
11
+ const EXPRESSION = /(?<![\\{])\{\{(?![{<%])([^{}\n]+)\}\}(?!\})/g;
12
+
13
+ /** Find SQL helper calls, including calls nested inside Boolean expressions. */
14
+ export function sqlQueriesInMarkdown(body, fm = {}) {
15
+ const queries = [];
16
+ const queryValue = (parameter) => {
17
+ if (parameter?.type === "StringLiteral") return parameter.value;
18
+ if (parameter?.type !== "PathExpression") return null;
19
+ return parameter.parts.reduce((value, part) => value?.[part], fm);
20
+ };
21
+ const visit = (node) => {
22
+ if (!node || typeof node !== "object") return;
23
+ if (node.path?.original === "sql") {
24
+ const query = queryValue(node.params?.[0]);
25
+ if (typeof query === "string") queries.push(query);
26
+ }
27
+ for (const parameter of node.params ?? []) visit(parameter);
28
+ };
29
+ for (const match of matchAllOutsideCode(body, EXPRESSION)) {
30
+ try {
31
+ for (const node of Handlebars.parse(match[0]).body) visit(node);
32
+ } catch {
33
+ // The render pass reports the malformed expression at its source position.
34
+ }
35
+ }
36
+ return queries;
37
+ }
38
+
39
+ /**
40
+ * Expand scalar frontmatter references and registered functions in Markdown prose.
41
+ * @param {string} body
42
+ * @param {{fm?: object, dates?: object, sqlResults?: Map<string, object>, file?: string, bodyLine?: number}} [options]
43
+ */
44
+ export function renderMarkdownExpressions(
45
+ body,
46
+ { fm, dates, sqlResults, file, bodyLine = 1 } = {},
47
+ ) {
48
+ const findings = [];
49
+ const engine = Handlebars.create();
50
+ const numeric = (name, left, right) => {
51
+ if (
52
+ !["number", "bigint"].includes(typeof left) ||
53
+ !["number", "bigint"].includes(typeof right) ||
54
+ (typeof left === "number" && !Number.isFinite(left)) ||
55
+ (typeof right === "number" && !Number.isFinite(right))
56
+ )
57
+ throw new TypeError(`${name} needs two finite numbers`);
58
+ };
59
+ for (const [name, compare] of Object.entries({
60
+ gt: (a, b) => a > b,
61
+ gte: (a, b) => a >= b,
62
+ lt: (a, b) => a < b,
63
+ lte: (a, b) => a <= b,
64
+ }))
65
+ engine.registerHelper(name, (left, right) => {
66
+ numeric(name, left, right);
67
+ return compare(left, right);
68
+ });
69
+ engine.registerHelper("eq", (left, right) => left === right);
70
+ engine.registerHelper("not", (value) => !value);
71
+ engine.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
72
+ engine.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
73
+ engine.registerHelper("words", (value) => numberWords(value));
74
+ engine.registerHelper("digits", (value) => numberDigits(value));
75
+ engine.registerHelper("sql", (query) => {
76
+ if (typeof query !== "string" || !query.trim())
77
+ throw new TypeError("sql needs a nonempty query string");
78
+ const result = sqlResults?.get(query);
79
+ if (!result) throw new RangeError(`SQL expression has no prepared result: ${query}`);
80
+ if (result.error) throw new RangeError(`SQL expression failed: ${result.error}`);
81
+ return result.value;
82
+ });
83
+ engine.registerHelper("dateformat", (calendar, authored) => {
84
+ if (
85
+ !(typeof calendar === "string" || isAddressTuple(calendar)) ||
86
+ !["string", "number"].includes(typeof authored)
87
+ )
88
+ throw new TypeError("dateformat needs a calendar Address and a date");
89
+ const parsed = parseNoteDate(authored, { ...dates, allowUnknown: false });
90
+ const error = parsed.findings.find((item) => item.severity === "error");
91
+ if (error || !parsed.date) throw new RangeError(error?.message ?? "date does not resolve");
92
+ const printable = formatDateInCalendar(
93
+ parsed.date,
94
+ isAddressTuple(calendar) ? renderAddress(calendar) : calendar,
95
+ dates,
96
+ );
97
+ if (!printable) throw new RangeError(`date cannot be printed in calendar ${calendar}`);
98
+ return printable.prose ?? printable.text;
99
+ });
100
+ const markdown = replaceOutsideCode(body, EXPRESSION, (source, _inside, offset) => {
101
+ try {
102
+ return engine.compile(source, { strict: true })(fm ?? {});
103
+ } catch (error) {
104
+ const before = String(body).slice(0, offset);
105
+ const lines = before.split("\n");
106
+ findings.push({
107
+ ...(file ? { file } : {}),
108
+ line: bodyLine + lines.length - 1,
109
+ column: lines.at(-1).length + 1,
110
+ severity: "error",
111
+ message: `Markdown expression ${source} failed: ${error.message}`,
112
+ });
113
+ return source;
114
+ }
115
+ });
116
+ return { markdown, findings };
117
+ }
@@ -61,6 +61,7 @@ import {
61
61
  resolvePackageUrl,
62
62
  } from "./content-address.mjs";
63
63
  import { HOMEPAGE_TYPE } from "./homepage.mjs";
64
+ import { isGmNote } from "./note-vocabulary.mjs";
64
65
 
65
66
  /**
66
67
  * Written once a fetch completes, so a half-finished cache is never used.
@@ -236,7 +237,7 @@ export function cachedMetadataIndexes(config) {
236
237
  if (!cached.length) {
237
238
  throw new Error(
238
239
  `${rel.id} is a declared dependency but its content index has ` +
239
- `not been fetched. Run \`content-build deps fetch\` first.`,
240
+ `not been fetched. Run \`package-build deps fetch\` first.`,
240
241
  );
241
242
  }
242
243
  files.push({ id: rel.id, file: newestIndex(cached) });
@@ -401,7 +402,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
401
402
  // must tolerate that rather than invent an href, exactly as it
402
403
  // already tolerates an entry with no `uuid`.
403
404
  url:
404
- !web || !record.address.slug ? undefined
405
+ !web || !record.address.slug || isGmNote(record) ? undefined
405
406
  // A package's homepage is its root: the note is the
406
407
  // mount's `_index.md`, so `[[thalorna-homepage-root]]`
407
408
  // lands on `/thalorna/`, not on a page below it.
@@ -411,6 +412,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
411
412
  doc: record.documentation ?? undefined,
412
413
  anchors: foundry?.anchors,
413
414
  type: parts.type,
415
+ gm: isGmNote(record),
414
416
  // What the note *is*, not only where it lives. A consumer
415
417
  // grouping a reference by the family its target declares —
416
418
  // an infobox sorting a being's skills — has no other way to
@@ -426,11 +428,13 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
426
428
  // checks a border across the package boundary from both ends,
427
429
  // a consumer's map draws the dependency's places beside its
428
430
  // own, and a consumer's pages list what a dependency's
429
- // affiliation holds and what lies within its regions.
431
+ // affiliation holds and what lies within its regions. A
432
+ // population lets the same checks include dependency figures.
430
433
  borders: record.data?.borders ?? undefined,
431
434
  routes: record.data?.routes ?? undefined,
432
435
  parents: record.data?.parents ?? undefined,
433
436
  domains: record.data?.domains ?? undefined,
437
+ population: record.data?.population ?? undefined,
434
438
  package: pkg,
435
439
  });
436
440
  const entry = index.get(key);
@@ -296,6 +296,7 @@ export function noteAddressContext(config) {
296
296
  return {
297
297
  package: config.contentPackage,
298
298
  system: "note",
299
+ types: new Set(Object.keys(NOTE_VOCABULARY)),
299
300
  systemBlocks,
300
301
  };
301
302
  }