@heroiclands/package-build 25.5.4 → 25.6.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 (108) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +7 -0
  3. package/assets/styles/content-base.css +1 -1
  4. package/bin/content-commands.mjs +32 -8
  5. package/docs/api.md +81 -32
  6. package/docs/authoring/frontmatter.md +1 -1
  7. package/docs/authoring/links-and-markup.md +59 -10
  8. package/docs/commands.md +60 -17
  9. package/docs/configuration.md +3 -2
  10. package/docs/content-format.md +50 -1
  11. package/docs/diagnostics.md +17 -0
  12. package/docs/guides/module-project.md +11 -3
  13. package/docs/guides/reference-pages.md +4 -1
  14. package/docs/guides/site.md +13 -3
  15. package/docs/guides/writing-in-invented-scripts.md +5 -2
  16. package/docs/reference/format-details.md +233 -138
  17. package/docs/reference/note-types.md +165 -152
  18. package/docs/reference/theme-pages.md +1 -0
  19. package/e2e.mjs +1 -1
  20. package/engine/base-compiler.mjs +2 -0
  21. package/engine/compile-corpus.mjs +6 -2
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format.yaml +9 -7
  24. package/engine/content-html.mjs +3 -2
  25. package/engine/content-index.mjs +23 -0
  26. package/engine/content-links.mjs +8 -0
  27. package/engine/content-render.mjs +14 -2
  28. package/engine/content-scripts.mjs +44 -6
  29. package/engine/content-session.mjs +11 -7
  30. package/engine/content-spans.mjs +3 -4
  31. package/engine/content-tables.mjs +12 -5
  32. package/engine/content-verse.mjs +52 -10
  33. package/engine/core-report.mjs +27 -18
  34. package/engine/culture-reach.mjs +3 -0
  35. package/engine/declared-sections.mjs +100 -0
  36. package/engine/derived-sections.mjs +36 -2
  37. package/engine/dictionary-rows.mjs +2 -1
  38. package/engine/generated-sections.mjs +2 -0
  39. package/engine/holdings.mjs +107 -3
  40. package/engine/index-records.mjs +3 -1
  41. package/engine/index.mjs +1 -1
  42. package/engine/language-data.mjs +0 -1
  43. package/engine/language-names.mjs +53 -23
  44. package/engine/language-rows.mjs +1 -3
  45. package/engine/language-terms.mjs +7 -10
  46. package/engine/map-dot.mjs +22 -0
  47. package/engine/markdown-blocks.mjs +23 -8
  48. package/engine/markdownlint-run.mjs +1 -1
  49. package/engine/markup-validation.mjs +2 -0
  50. package/engine/metadata-index.mjs +1 -0
  51. package/engine/note-addresses.mjs +16 -0
  52. package/engine/note-vocabulary.mjs +29 -13
  53. package/engine/npc-generate.mjs +52 -7
  54. package/engine/office-holders.mjs +76 -0
  55. package/engine/page-breaks.mjs +143 -0
  56. package/engine/pdf-build.mjs +12 -4
  57. package/engine/pdf-render.mjs +12 -2
  58. package/engine/place-kinds.mjs +106 -0
  59. package/engine/preview-index.mjs +4 -2
  60. package/engine/settlement-tables.mjs +410 -675
  61. package/engine/settlement-terms.mjs +118 -0
  62. package/engine/site-build.mjs +14 -7
  63. package/engine/site-preview.mjs +3 -0
  64. package/engine/site-render.mjs +18 -4
  65. package/engine/site-root.mjs +10 -1
  66. package/engine/site-serve-http.mjs +151 -0
  67. package/engine/site-serve.mjs +196 -126
  68. package/engine/sql-tables.mjs +101 -7
  69. package/engine/standing-terms.mjs +9 -5
  70. package/engine/standings.mjs +83 -31
  71. package/engine/unique-items.mjs +85 -0
  72. package/engine/verse-metre.mjs +50 -5
  73. package/engine/verse-regions.mjs +16 -0
  74. package/package.json +9 -4
  75. package/site-theme/data/banner-kinds.yaml +2 -0
  76. package/sohl/actors.mjs +4 -3
  77. package/types/e2e.d.mts +1 -1
  78. package/types/engine/content-html.d.mts +2 -1
  79. package/types/engine/content-scripts.d.mts +9 -1
  80. package/types/engine/content-verse.d.mts +11 -2
  81. package/types/engine/core-report.d.mts +6 -2
  82. package/types/engine/culture-reach.d.mts +3 -0
  83. package/types/engine/declared-sections.d.mts +30 -0
  84. package/types/engine/derived-sections.d.mts +5 -1
  85. package/types/engine/holdings.d.mts +34 -2
  86. package/types/engine/index-records.d.mts +2 -1
  87. package/types/engine/language-names.d.mts +18 -6
  88. package/types/engine/language-terms.d.mts +5 -1
  89. package/types/engine/markdown-blocks.d.mts +5 -1
  90. package/types/engine/npc-generate.d.mts +3 -0
  91. package/types/engine/office-holders.d.mts +33 -0
  92. package/types/engine/page-breaks.d.mts +38 -0
  93. package/types/engine/pdf-render.d.mts +2 -0
  94. package/types/engine/place-kinds.d.mts +18 -0
  95. package/types/engine/settlement-tables.d.mts +25 -158
  96. package/types/engine/settlement-terms.d.mts +53 -0
  97. package/types/engine/site-build.d.mts +3 -1
  98. package/types/engine/site-render.d.mts +6 -2
  99. package/types/engine/site-root.d.mts +3 -1
  100. package/types/engine/site-serve-http.d.mts +1 -0
  101. package/types/engine/site-serve.d.mts +12 -6
  102. package/types/engine/sql-tables.d.mts +35 -3
  103. package/types/engine/standing-terms.d.mts +3 -2
  104. package/types/engine/standings.d.mts +14 -2
  105. package/types/engine/unique-items.d.mts +12 -0
  106. package/types/engine/verse-metre.d.mts +2 -1
  107. package/types/engine/verse-regions.d.mts +8 -0
  108. package/vendor/markdownlint.mjs +855 -569
@@ -1385,7 +1385,9 @@ async function buildBook(book, shared) {
1385
1385
  url: site,
1386
1386
  elsewhere,
1387
1387
  findings,
1388
- runs: runContext(resolved, records),
1388
+ runs: runContext(resolved, records, {
1389
+ referenceName: (target) => resolveInfoboxRef(gates.index, target)?.name,
1390
+ }),
1389
1391
  file: page.file,
1390
1392
  bodyLine: page.bodyLine,
1391
1393
  prepared: true,
@@ -1476,7 +1478,9 @@ async function buildBook(book, shared) {
1476
1478
  elsewhere,
1477
1479
  indexMentions: true,
1478
1480
  findings,
1479
- runs: runContext(resolved, records),
1481
+ runs: runContext(resolved, records, {
1482
+ referenceName: (target) => resolveInfoboxRef(gates.index, target)?.name,
1483
+ }),
1480
1484
  file,
1481
1485
  }),
1482
1486
  );
@@ -1508,7 +1512,9 @@ async function buildBook(book, shared) {
1508
1512
  elsewhere,
1509
1513
  indexMentions: true,
1510
1514
  findings,
1511
- runs: runContext(resolved, records),
1515
+ runs: runContext(resolved, records, {
1516
+ referenceName: (target) => resolveInfoboxRef(gates.index, target)?.name,
1517
+ }),
1512
1518
  file,
1513
1519
  });
1514
1520
  } catch {
@@ -1682,7 +1688,9 @@ async function buildBook(book, shared) {
1682
1688
  copyright: shared.copyright,
1683
1689
  license: shared.license,
1684
1690
  index,
1685
- runs: runContext(resolved, records),
1691
+ runs: runContext(resolved, records, {
1692
+ referenceName: (target) => resolveInfoboxRef(gates.index, target)?.name,
1693
+ }),
1686
1694
  });
1687
1695
  // Last, over the whole document: a reference can only be checked once every
1688
1696
  // declaration is in one string, and Typst treats a dangling one as fatal.
@@ -62,6 +62,7 @@
62
62
  * @module
63
63
  */
64
64
 
65
+ import { escapeInvalidPageBreaks, scanPageBreaks } from "./page-breaks.mjs";
65
66
  import { createRenderer } from "./content-render.mjs";
66
67
  import { bookDraftNoticePreamble } from "./draft-notice.mjs";
67
68
  import { ICON_PATTERN, ICON_SIZES } from "./content-icons.mjs";
@@ -359,6 +360,7 @@ function reportUnrenderable(source, definitions, opts, original) {
359
360
  }
360
361
  }
361
362
  for (const error of [
363
+ ...scanPageBreaks(original).errors,
362
364
  ...scanSpans(source).errors,
363
365
  ...scanRuns(source, opts.runs).errors,
364
366
  ...markupAnchorFindings(source),
@@ -469,6 +471,7 @@ function reportUnrenderable(source, definitions, opts, original) {
469
471
  * @param {Array<{line: number, generated: boolean}>} [opts.lineMap] - From
470
472
  * table expansion, mapping each line handed in back to the line an author
471
473
  * wrote.
474
+ * @param {boolean} [opts.pageBreaks] - Internal: whether this slice remains at the note top level.
472
475
  * @param {boolean} [opts.prepared] - Whether this body has been through the
473
476
  * passes that resolve a wikilink, an embed and an inline expression. A note's
474
477
  * has; a front-matter file's and a prose file's have not, and markup of theirs
@@ -519,7 +522,8 @@ export function markdownToTypst(markdown, opts = {}) {
519
522
  renderRuns(markingSource, opts.runs, undefined, { target: "book" })
520
523
  );
521
524
  const markedFootnotes = marked && definitions ? separateFootnotes(marked.markdown) : undefined;
522
- const source = markedFootnotes?.markdown ?? marked?.markdown ?? authored;
525
+ const markedSource = markedFootnotes?.markdown ?? marked?.markdown ?? authored;
526
+ const source = opts.nested ? markedSource : escapeInvalidPageBreaks(markedSource);
523
527
  if (markedFootnotes) definitions = markedFootnotes.definitions;
524
528
  if (marked) {
525
529
  sharedOptions.footnoteDefinitions = definitions;
@@ -547,6 +551,7 @@ export function markdownToTypst(markdown, opts = {}) {
547
551
  footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
548
552
  scrolls: marked?.scrolls ?? opts.scrolls ?? new Map(),
549
553
  verses: marked?.verses ?? opts.verses ?? new Map(),
554
+ pageBreaks: opts.pageBreaks !== false,
550
555
  };
551
556
  // A withheld section is set in the box a `:::secret` is set in, and its
552
557
  // heading is set inside it: a printed page honours no reader permission, so
@@ -600,6 +605,7 @@ export function markdownToTypst(markdown, opts = {}) {
600
605
  if (block.name === "div") {
601
606
  const content = markdownToTypst(block.body, {
602
607
  ...sharedOptions,
608
+ pageBreaks: false,
603
609
  insideAdmonition: false,
604
610
  });
605
611
  const anchor = block.id ? ` <${sectionLabel(anchorPrefix, block.id)}>` : "";
@@ -620,6 +626,7 @@ export function markdownToTypst(markdown, opts = {}) {
620
626
  // of its own; a box holds none, and the scan has already said so.
621
627
  const content = markdownToTypst(block.body, {
622
628
  ...sharedOptions,
629
+ pageBreaks: false,
623
630
  insideAdmonition: !BLOCK_CONTAINERS.includes(block.name),
624
631
  });
625
632
  output.push(
@@ -695,7 +702,7 @@ export function markdownToTypst(markdown, opts = {}) {
695
702
  /** Render one fragment with the note's footnote definitions available. */
696
703
  function renderMarkdownSegment(source, md, ctx, definitions) {
697
704
  const input = definitions ? `${source}\n\n${definitions}` : source;
698
- const tokens = md.parse(input, { verses: ctx.verses });
705
+ const tokens = md.parse(input, { verses: ctx.verses, pageBreaks: ctx.pageBreaks });
699
706
  const footnoteStart = tokens.findIndex((token) => token.type === "footnote_block_open");
700
707
  const visible = footnoteStart < 0 ? tokens : tokens.slice(0, footnoteStart);
701
708
  const footnotes = new Map();
@@ -791,6 +798,9 @@ function renderBlock(tokens, i, out, ctx) {
791
798
  out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
792
799
  return 1;
793
800
  }
801
+ case "page_break":
802
+ out.push("\n#pagebreak(weak: true)\n\n");
803
+ return 1;
794
804
  case "hr":
795
805
  out.push("\n#line(length: 100%, stroke: 0.4pt)\n\n");
796
806
  return 1;
@@ -0,0 +1,106 @@
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
+ * **What a district and a route require of their notes.**
16
+ *
17
+ * A `place/district` is a recognised division of a larger place, usually
18
+ * administrative: a city's quarter, a town's ward, a kingdom's shire. It always
19
+ * stands within a `place/settlement` or a `place/region`, named in
20
+ * `data.parents`. It may name its own governing affiliation in
21
+ * `data.government`; without one it falls under its parent's government.
22
+ *
23
+ * A `place/route` is a road, a pass or a sea lane, and the journeys it carries
24
+ * are its `data.routes`.
25
+ *
26
+ * @module
27
+ */
28
+
29
+ import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
30
+ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
31
+
32
+ /** The place kinds a district may stand within. */
33
+ export const DISTRICT_PARENT_KINDS = Object.freeze(["settlement", "region"]);
34
+
35
+ /**
36
+ * The place one `parents` entry names, as the lint's index holds it.
37
+ *
38
+ * @param {unknown} value - The authored entry.
39
+ * @param {object} [index] - The lint's address index.
40
+ * @returns {{resolved: boolean, fm?: object}} Whether it resolves, and its
41
+ * frontmatter where it does.
42
+ */
43
+ function parentOf(value, index) {
44
+ const tuple =
45
+ isAddressTuple(value) ? value : (
46
+ parseAddress(value, {
47
+ package: index?.contentPackage,
48
+ system: "note",
49
+ type: "place",
50
+ types: index?.types,
51
+ packages: index?.packages,
52
+ })
53
+ );
54
+ if (!isAddressTuple(tuple) || !index?.addressHit) return { resolved: false };
55
+ const hit = index.addressHit(renderAddress({ ...tuple, system: "note" }));
56
+ return hit ? { resolved: true, fm: hit.fm ?? hit } : { resolved: false };
57
+ }
58
+
59
+ /**
60
+ * Check a district's parent and a route's journeys.
61
+ *
62
+ * A district naming no parent is reported at its `subType`; one whose parents
63
+ * all resolve and none is a settlement or a region is reported at `parents`.
64
+ * A parent that does not resolve is the Address check's finding, so a district
65
+ * whose only parents are unresolved earns nothing more here. A route stating no
66
+ * `data.routes` entry is reported at its `subType`.
67
+ *
68
+ * @param {object} note - The note: `file`, `raw` and parsed `fm`.
69
+ * @param {{index?: object}} [options] - The lint's options.
70
+ * @returns {object[]} Located findings.
71
+ */
72
+ export function checkPlaceKind(note, { index } = {}) {
73
+ const subType = note.fm?.subType;
74
+ const at = (path, key) => ({
75
+ file: note.file,
76
+ ...positionOfFrontmatterPath(note.raw ?? "", path, key ? { key: true } : undefined),
77
+ severity: "error",
78
+ });
79
+ if (subType === "district") {
80
+ const parents = note.fm?.data?.parents;
81
+ const list =
82
+ Array.isArray(parents) ? parents
83
+ : parents == null ? []
84
+ : [parents];
85
+ const message =
86
+ "a district stands within a settlement or a region; name one in `data.parents`";
87
+ if (!list.length) return [{ ...at(["subType"]), message }];
88
+ const found = list.map((value) => parentOf(value, index));
89
+ if (found.some((one) => !one.resolved)) return [];
90
+ if (found.some((one) => DISTRICT_PARENT_KINDS.includes(one.fm?.subType))) return [];
91
+ return [{ ...at(["data", "parents"], true), message }];
92
+ }
93
+ if (subType === "route") {
94
+ const routes = note.fm?.data?.routes;
95
+ if (Array.isArray(routes) && routes.length) return [];
96
+ return [
97
+ {
98
+ ...at(["subType"]),
99
+ message:
100
+ "a route carries its journeys in `data.routes`; state at least one, " +
101
+ "each `{ to, bearing, mode, days }`",
102
+ },
103
+ ];
104
+ }
105
+ return [];
106
+ }
@@ -37,7 +37,7 @@ import { noContentIndexPackages } from "./metadata-index.mjs";
37
37
  import { isGmNote } from "./note-vocabulary.mjs";
38
38
  import { assetAddressIndex } from "./art-fields.mjs";
39
39
  import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
40
- import { buildSiteIndex } from "./site-index.mjs";
40
+ import { buildSiteIndex, resolveInfoboxRef } from "./site-index.mjs";
41
41
  import { siteBase } from "../content-config.mjs";
42
42
 
43
43
  /**
@@ -107,7 +107,9 @@ export function previewIndex({ config, records, foreign }) {
107
107
  return key ? assets?.byPath?.get(key) : undefined;
108
108
  };
109
109
  return {
110
- runs: runContext(config, records),
110
+ runs: runContext(config, records, {
111
+ referenceName: (target) => resolveInfoboxRef(site, target)?.name,
112
+ }),
111
113
  icons: config.icons,
112
114
  lookupAsset,
113
115
  resolveSrc: webSrc,