@heroiclands/package-build 22.20.0 → 22.22.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 (118) hide show
  1. package/CHANGELOG-content-build.md +0 -8
  2. package/CHANGELOG.md +204 -0
  3. package/assets/styles/content-base.css +162 -0
  4. package/bin/content-commands.mjs +26 -3
  5. package/bin/package-build.mjs +15 -2
  6. package/ci/content-format-reference.mjs +68 -1
  7. package/config.mjs +26 -0
  8. package/content-config.mjs +73 -4
  9. package/docs/api.md +46 -28
  10. package/docs/authoring/dates-and-calendars.md +4 -2
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +237 -28
  13. package/docs/configuration.md +27 -1
  14. package/docs/content-format.md +3 -1
  15. package/docs/guides/reference-pages.md +15 -0
  16. package/docs/reference/format-details.md +265 -42
  17. package/docs/reference/note-types.md +45 -28
  18. package/engine/actor-compiler.mjs +23 -0
  19. package/engine/anchored-sections.mjs +35 -4
  20. package/engine/anchors.mjs +10 -20
  21. package/engine/base-compiler.mjs +86 -21
  22. package/engine/base-styles.mjs +149 -0
  23. package/engine/bundles.mjs +0 -1
  24. package/engine/compendiums.mjs +8 -1
  25. package/engine/content-blocks.mjs +410 -0
  26. package/engine/content-captions.mjs +57 -8
  27. package/engine/content-footnotes.mjs +189 -0
  28. package/engine/content-format-check.mjs +15 -12
  29. package/engine/content-index.mjs +2 -2
  30. package/engine/content-links.mjs +14 -6
  31. package/engine/content-tables.mjs +45 -4
  32. package/engine/content-tasklists.mjs +189 -0
  33. package/engine/docs-checks.mjs +4 -4
  34. package/engine/extension-attributes.mjs +48 -2
  35. package/engine/frontmatter-lint.mjs +62 -66
  36. package/engine/frontmatter.mjs +3 -3
  37. package/engine/heading-attributes.mjs +308 -0
  38. package/engine/helpers.mjs +19 -13
  39. package/engine/homepage.mjs +8 -8
  40. package/engine/index.mjs +3 -0
  41. package/engine/infobox-render.mjs +21 -4
  42. package/engine/infobox.mjs +167 -7
  43. package/engine/journals.mjs +36 -17
  44. package/engine/macros.mjs +1 -1
  45. package/engine/markdown-expressions.mjs +178 -53
  46. package/engine/note-addresses.mjs +17 -8
  47. package/engine/note-dates.mjs +49 -0
  48. package/engine/note-frontmatter.mjs +7 -16
  49. package/engine/note-vocabulary.mjs +216 -42
  50. package/engine/page-lists.mjs +358 -0
  51. package/engine/pdf-build.mjs +76 -42
  52. package/engine/pdf-render.mjs +422 -49
  53. package/engine/rank-ladder.mjs +198 -0
  54. package/engine/reckoning-markers.mjs +1 -1
  55. package/engine/reference-targets.mjs +13 -1
  56. package/engine/retired-fields.mjs +8 -13
  57. package/engine/scenes.mjs +10 -3
  58. package/engine/site-build.mjs +41 -12
  59. package/engine/site-config.mjs +10 -1
  60. package/engine/site-index.mjs +39 -4
  61. package/engine/site-preview.mjs +18 -7
  62. package/engine/social-ties.mjs +52 -14
  63. package/engine/sql-tables.mjs +66 -9
  64. package/engine/standing-terms.mjs +19 -0
  65. package/engine/standings.mjs +538 -0
  66. package/engine/svg-theme.mjs +15 -4
  67. package/engine/system-block.mjs +12 -14
  68. package/engine/web-wikilinks.mjs +48 -1
  69. package/engine/wikilinks.mjs +3 -1
  70. package/hm3/actors.mjs +6 -3
  71. package/manifest.mjs +13 -0
  72. package/package.json +1 -1
  73. package/sohl/actors.mjs +82 -103
  74. package/sohl/item-fields.mjs +4 -5
  75. package/sohl/items.mjs +1 -1
  76. package/sohl/note-schemas.mjs +11 -21
  77. package/types/config.d.mts +9 -0
  78. package/types/engine/actor-compiler.d.mts +11 -0
  79. package/types/engine/anchored-sections.d.mts +24 -0
  80. package/types/engine/base-compiler.d.mts +19 -0
  81. package/types/engine/base-styles.d.mts +93 -0
  82. package/types/engine/content-blocks.d.mts +87 -0
  83. package/types/engine/content-captions.d.mts +14 -1
  84. package/types/engine/content-footnotes.d.mts +73 -0
  85. package/types/engine/content-format-check.d.mts +8 -6
  86. package/types/engine/content-tables.d.mts +12 -4
  87. package/types/engine/content-tasklists.d.mts +72 -0
  88. package/types/engine/extension-attributes.d.mts +22 -1
  89. package/types/engine/frontmatter.d.mts +1 -1
  90. package/types/engine/heading-attributes.d.mts +174 -0
  91. package/types/engine/helpers.d.mts +7 -4
  92. package/types/engine/homepage.d.mts +7 -7
  93. package/types/engine/index.d.mts +1 -0
  94. package/types/engine/infobox-render.d.mts +6 -1
  95. package/types/engine/journals.d.mts +14 -7
  96. package/types/engine/markdown-expressions.d.mts +23 -0
  97. package/types/engine/note-frontmatter.d.mts +1 -4
  98. package/types/engine/note-vocabulary.d.mts +98 -8
  99. package/types/engine/page-lists.d.mts +126 -0
  100. package/types/engine/pdf-render.d.mts +72 -2
  101. package/types/engine/rank-ladder.d.mts +32 -0
  102. package/types/engine/retired-fields.d.mts +4 -6
  103. package/types/engine/site-build.d.mts +13 -1
  104. package/types/engine/site-config.d.mts +9 -0
  105. package/types/engine/site-index.d.mts +13 -1
  106. package/types/engine/site-preview.d.mts +1 -1
  107. package/types/engine/sql-tables.d.mts +31 -7
  108. package/types/engine/standing-terms.d.mts +16 -0
  109. package/types/engine/standings.d.mts +147 -0
  110. package/types/engine/system-block.d.mts +11 -12
  111. package/types/engine/web-wikilinks.d.mts +3 -1
  112. package/types/engine/wikilinks.d.mts +2 -1
  113. package/types/hm3/actors.d.mts +3 -3
  114. package/types/sohl/actors.d.mts +25 -7
  115. package/engine/content-admonitions.mjs +0 -114
  116. package/engine/content-secrets.mjs +0 -112
  117. package/types/engine/content-admonitions.d.mts +0 -24
  118. package/types/engine/content-secrets.d.mts +0 -17
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { acceptsType, parseAddress, renderAddress } from "./address.mjs";
12
+ import { AddressEntries } from "./address-values.mjs";
12
13
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
13
14
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
14
15
 
@@ -16,14 +17,51 @@ export { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES };
16
17
 
17
18
  const TERMS = new Set(SOCIAL_TIES.map(({ term }) => term));
18
19
 
20
+ /**
21
+ * A being's defining ties, in one shape whichever shape they reach here in.
22
+ *
23
+ * Two spellings reach this check, and both read the same entries from here
24
+ * on:
25
+ *
26
+ * - an **{@link AddressEntries}** — the map after the note boundary decoded
27
+ * its keys into typed Addresses, the shape every compile sees;
28
+ * - a **plain map**, the shape a caller reading raw YAML sees.
29
+ *
30
+ * `sourceKey` is always the authored key text, so a finding locates on what
31
+ * the file actually says even when `key` already carries a parsed Address.
32
+ *
33
+ * @param {unknown} value - The authored `data.socialTies`.
34
+ * @returns {{form: "absent"|"map"|"malformed",
35
+ * entries: Array<{key: unknown, term: unknown, sourceKey: string}>}}
36
+ */
37
+ function readSocialTies(value) {
38
+ if (value === undefined || value === null) return { form: "absent", entries: [] };
39
+ if (value instanceof AddressEntries) {
40
+ return {
41
+ form: "map",
42
+ entries: value.entries.map((entry) => ({
43
+ key: entry.target,
44
+ term: entry.value,
45
+ sourceKey: entry.sourceKey,
46
+ })),
47
+ };
48
+ }
49
+ if (typeof value !== "object" || Array.isArray(value))
50
+ return { form: "malformed", entries: [] };
51
+ return {
52
+ form: "map",
53
+ entries: Object.entries(value).map(([key, term]) => ({ key, term, sourceKey: key })),
54
+ };
55
+ }
56
+
19
57
  /**
20
58
  * Validate a being's map of defining relationships.
21
59
  * @param {object} note
22
60
  * @param {{index?: object}} [options]
23
61
  */
24
62
  export function checkSocialTies(note, { index } = {}) {
25
- const ties = note.fm?.data?.socialTies;
26
- if (ties === undefined || ties === null) return [];
63
+ const { form, entries } = readSocialTies(note.fm?.data?.socialTies);
64
+ if (form === "absent") return [];
27
65
  const at = (key) => ({
28
66
  file: note.file,
29
67
  ...positionOfFrontmatterPath(
@@ -33,7 +71,7 @@ export function checkSocialTies(note, { index } = {}) {
33
71
  ),
34
72
  severity: "error",
35
73
  });
36
- if (typeof ties !== "object" || Array.isArray(ties))
74
+ if (form === "malformed")
37
75
  return [
38
76
  {
39
77
  ...at(),
@@ -42,11 +80,11 @@ export function checkSocialTies(note, { index } = {}) {
42
80
  ];
43
81
  const findings = [];
44
82
  const seen = new Map();
45
- for (const [key, term] of Object.entries(ties)) {
83
+ for (const { key, term, sourceKey } of entries) {
46
84
  const tuple = parseAddress(
47
85
  key,
48
86
  {
49
- package: index?.contentPackage ?? note.fm?.package,
87
+ package: index?.contentPackage,
50
88
  system: "note",
51
89
  types: index?.types,
52
90
  packages: index?.packages,
@@ -55,43 +93,43 @@ export function checkSocialTies(note, { index } = {}) {
55
93
  );
56
94
  if (tuple.reason) {
57
95
  findings.push({
58
- ...at(key),
59
- message: `data.socialTies key ${JSON.stringify(key)} is not a complete Address (${tuple.reason}); write being or affiliation as its type`,
96
+ ...at(sourceKey),
97
+ message: `data.socialTies key ${JSON.stringify(sourceKey)} is not a complete Address (${tuple.reason}); write being or affiliation as its type`,
60
98
  });
61
99
  } else if (!acceptsType(tuple, SOCIAL_TIE_TARGET_TYPES)) {
62
100
  findings.push({
63
- ...at(key),
101
+ ...at(sourceKey),
64
102
  message: `data.socialTies accepts being or affiliation, not ${tuple.type}`,
65
103
  });
66
104
  } else {
67
105
  const target = renderAddress(tuple);
68
106
  if (seen.has(target)) {
69
107
  findings.push({
70
- ...at(key),
108
+ ...at(sourceKey),
71
109
  message: `data.socialTies names the same target ${target} as ${JSON.stringify(seen.get(target))}; keep one tie per target`,
72
110
  });
73
111
  } else {
74
- seen.set(target, key);
112
+ seen.set(target, sourceKey);
75
113
  }
76
114
  if (
77
- tuple.package === (index?.contentPackage ?? note.fm?.package) &&
115
+ tuple.package === index?.contentPackage &&
78
116
  tuple.type === "being" &&
79
117
  tuple.shortcode === note.fm?.shortcode
80
118
  ) {
81
119
  findings.push({
82
- ...at(key),
120
+ ...at(sourceKey),
83
121
  message: `data.socialTies cannot address this being itself`,
84
122
  });
85
123
  } else if (index?.addressHit && !index.addressHit(target)) {
86
124
  findings.push({
87
- ...at(key),
125
+ ...at(sourceKey),
88
126
  message: `data.socialTies target ${target} does not resolve`,
89
127
  });
90
128
  }
91
129
  }
92
130
  if (!TERMS.has(term))
93
131
  findings.push({
94
- ...at(key),
132
+ ...at(sourceKey),
95
133
  message: `data.socialTies value ${JSON.stringify(term)} must be one of ${SOCIAL_TIES.map(({ term: value }) => value).join(", ")}`,
96
134
  });
97
135
  }
@@ -35,6 +35,7 @@ import path from "node:path";
35
35
 
36
36
  import { FENCE_LINE, parseHeaderArgs } from "./code-fences.mjs";
37
37
  import { booleanAttribute } from "./extension-attributes.mjs";
38
+ import { findPageListBlocks, preparePageLists } from "./page-lists.mjs";
38
39
  import { MARKET_CLASSES } from "./market-class.mjs";
39
40
  import { parseMarkdownFile } from "./helpers.mjs";
40
41
  import { sqlQueriesInMarkdown } from "./markdown-expressions.mjs";
@@ -53,6 +54,31 @@ const EMPTY_CELL = "—";
53
54
  */
54
55
  export const RENDER_ALIASES = Object.freeze({ ref: "_ref", section: "_section" });
55
56
 
57
+ /**
58
+ * The attributes an SQL fence takes, with what each one is for.
59
+ *
60
+ * **This is the one list.** The fence is validated against it and the authoring
61
+ * document is checked against it, so an attribute added here is one the document
62
+ * must describe, and anything else an author writes is reported by name.
63
+ *
64
+ * @type {Readonly<Record<string, Readonly<{value: string, default: string, summary: string}>>>}
65
+ */
66
+ export const SQL_FENCE_ATTRIBUTES = Object.freeze({
67
+ "allow-empty": Object.freeze({
68
+ value: "`true` or `false`",
69
+ default: "`false`",
70
+ summary:
71
+ "Whether a query selecting no rows is allowed. A zero-row result is a finding " +
72
+ "otherwise, because a table that silently prints nothing is the commonest way a " +
73
+ "query goes wrong unnoticed.",
74
+ }),
75
+ "section-level": Object.freeze({
76
+ value: "an integer from 1 through 6",
77
+ default: "`2`",
78
+ summary: "The heading level given to each group a `_section` column creates.",
79
+ }),
80
+ });
81
+
56
82
  /** A table cell may not carry a raw `|` or a line break. */
57
83
  const escapeCell = (text) =>
58
84
  String(text)
@@ -92,7 +118,7 @@ export function findSqlBlocks(markdown) {
92
118
  const level = Number(args["section-level"] ?? 2);
93
119
  const errors = [...problems];
94
120
  for (const key of Object.keys(args)) {
95
- if (!["allow-empty", "section-level"].includes(key))
121
+ if (!Object.hasOwn(SQL_FENCE_ATTRIBUTES, key))
96
122
  errors.push(`${key} is not an SQL fence attribute`);
97
123
  }
98
124
  let allowEmpty = false;
@@ -569,6 +595,10 @@ export function renderSqlTable(
569
595
  * @param {object} [opts]
570
596
  * @param {(ref: string) => boolean} [opts.linkable] - Passed to
571
597
  * {@link renderSqlTable}.
598
+ * @param {object[]} [opts.records] - Content-index records, narrowed to the
599
+ * audience this surface publishes to. Given them, the result also carries
600
+ * `pageLists`, so one prepared object answers every corpus-reading directive
601
+ * a body can hold and a caller looks results up in one place.
572
602
  * **Keyed by note, then by the directive's ordinal within it** — not by its
573
603
  * line. The passes do not agree on what a body is: `walkMarkdownTree` trims it,
574
604
  * while the link checker strips the frontmatter fence and leaves the newlines
@@ -578,8 +608,9 @@ export function renderSqlTable(
578
608
  * @returns {Promise<Map<string, object[]>>} Note to results, in document order,
579
609
  * each carrying either a rendered `markdown` and its `rows`, or a `reason`.
580
610
  */
581
- export async function prepareSqlTables(db, sources, { linkable } = {}) {
611
+ export async function prepareSqlTables(db, sources, { linkable, records } = {}) {
582
612
  const prepared = new Map();
613
+ if (records) prepared.pageLists = preparePageLists(records, sources);
583
614
  for (const { source, markdown } of sources) {
584
615
  const blocks = findSqlBlocks(markdown);
585
616
  if (!blocks.length) continue;
@@ -658,10 +689,14 @@ export async function prepareInlineSqlExpressions(db, sources) {
658
689
  * shared index
659
690
  * describes, where N passes each derive the corpus their own way.
660
691
  *
661
- * **Nothing is opened for a tree with no `sql` directive.** The corpus is still
662
- * written entirely in the retiring language, so until a table is converted this
663
- * costs one walk and no database at all — which is what lets every pass call it
664
- * unconditionally.
692
+ * **Every corpus-reading directive is answered here**, which is why one call
693
+ * precedes every pass: the `sql` tables, the inline scalar queries, and the
694
+ * page lists, whose results ride on the same object as `pageLists`.
695
+ *
696
+ * **Nothing is opened for a tree that asks no query.** A tree with no `sql`
697
+ * directive costs one walk and no database at all, and so does one whose only
698
+ * directive is a page list — a page list is a filter over the records. That is
699
+ * what lets every pass call this unconditionally.
665
700
  *
666
701
  * @param {string} contentBase - Root of the content tree.
667
702
  * @param {object} [opts]
@@ -671,8 +706,9 @@ export async function prepareInlineSqlExpressions(db, sources) {
671
706
  * A command that also builds a link index holds them already, and deriving
672
707
  * them twice is the duplicated-corpus failure this closes.
673
708
  * @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes.
674
- * @returns {Promise<Map<string, object[]>|undefined>} Results by note path, or
675
- * nothing when the tree has no such directive.
709
+ * @returns {Promise<Map<string, object[]>|undefined>} Results by note path,
710
+ * carrying `inline` and `pageLists` beside them, or nothing when the tree
711
+ * holds no such directive.
676
712
  */
677
713
  export async function prepareTreeSqlTables(
678
714
  contentBase,
@@ -701,10 +737,30 @@ export async function prepareTreeSqlTables(
701
737
  if (!isNoteRecord(record)) continue;
702
738
  const absPath = noteFile(contentBase, record);
703
739
  const { body, frontmatter } = parseMarkdownFile(absPath);
704
- if (body && (findSqlBlocks(body).length || sqlQueriesInMarkdown(body, frontmatter).length))
740
+ if (
741
+ body &&
742
+ (findSqlBlocks(body).length ||
743
+ findPageListBlocks(body).length ||
744
+ sqlQueriesInMarkdown(body, frontmatter).length)
745
+ )
705
746
  sources.push({ source: absPath, markdown: body, frontmatter });
706
747
  }
707
748
  if (!sources.length) return undefined;
749
+
750
+ // A page list is a filter over the records, so it needs no database. A tree
751
+ // whose only corpus-reading directive is one therefore opens none — which
752
+ // is the same bargain the early return above makes for a tree with no
753
+ // directive at all.
754
+ const needsDatabase = sources.some(
755
+ ({ markdown, frontmatter }) =>
756
+ findSqlBlocks(markdown).length || sqlQueriesInMarkdown(markdown, frontmatter).length,
757
+ );
758
+ if (!needsDatabase) {
759
+ const pageOnly = new Map();
760
+ pageOnly.inline = new Map();
761
+ pageOnly.pageLists = preparePageLists(indexRecords, sources);
762
+ return pageOnly;
763
+ }
708
764
  // A cell links only where the address it would emit resolves, so a table
709
765
  // never ships a link the wikilink pass will then report dead.
710
766
  //
@@ -736,6 +792,7 @@ export async function prepareTreeSqlTables(
736
792
  try {
737
793
  const prepared = await prepareSqlTables(db, sources, {
738
794
  linkable: (ref) => addresses.has(renderAddress(ref)),
795
+ records: indexRecords,
739
796
  });
740
797
  prepared.inline = await prepareInlineSqlExpressions(db, sources);
741
798
  return prepared;
@@ -0,0 +1,19 @@
1
+ /* SPDX-License-Identifier: GPL-3.0-or-later */
2
+
3
+ /**
4
+ * The note type a standing is held in.
5
+ *
6
+ * A standing is always held in a body: the entry is keyed by that body's
7
+ * Address, which is what lets the rung and the post be checked against what
8
+ * the body itself declares.
9
+ */
10
+ export const STANDING_BODY_TYPES = Object.freeze(["affiliation"]);
11
+
12
+ /**
13
+ * The two keys one standing carries.
14
+ *
15
+ * `rank` is the level on the body's own `governance.ranks` ladder, as a number.
16
+ * `office` is a key of its `governance.offices` map, as a string. Both are
17
+ * optional — a membership with neither is the ordinary shape.
18
+ */
19
+ export const STANDING_KEYS = Object.freeze(["rank", "office"]);