@heroiclands/package-build 22.21.0 → 22.23.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 (152) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +77 -17
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +153 -6
  8. package/docs/api.md +120 -89
  9. package/docs/authoring/assets.md +14 -0
  10. package/docs/authoring/dates-and-calendars.md +41 -3
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +325 -35
  13. package/docs/commands.md +12 -10
  14. package/docs/configuration.md +117 -14
  15. package/docs/content-format.md +4 -2
  16. package/docs/guides/book.md +5 -4
  17. package/docs/guides/reference-pages.md +15 -0
  18. package/docs/reference/format-details.md +545 -104
  19. package/docs/reference/note-types.md +30 -13
  20. package/engine/actor-compiler.mjs +23 -0
  21. package/engine/anchored-sections.mjs +35 -4
  22. package/engine/anchors.mjs +26 -30
  23. package/engine/art-fields.mjs +118 -0
  24. package/engine/art-slots.mjs +7 -1
  25. package/engine/asset-index.mjs +160 -10
  26. package/engine/asset-replacement.mjs +93 -0
  27. package/engine/asset-types.mjs +240 -0
  28. package/engine/base-compiler.mjs +139 -22
  29. package/engine/base-styles.mjs +149 -0
  30. package/engine/bundles.mjs +13 -2
  31. package/engine/compendiums.mjs +8 -1
  32. package/engine/content-blocks.mjs +410 -0
  33. package/engine/content-embeds.mjs +19 -0
  34. package/engine/content-figures.mjs +468 -0
  35. package/engine/content-footnotes.mjs +197 -0
  36. package/engine/content-format-check.mjs +15 -12
  37. package/engine/content-format.yaml +1 -1
  38. package/engine/content-images.mjs +126 -25
  39. package/engine/content-index.mjs +38 -12
  40. package/engine/content-links.mjs +14 -6
  41. package/engine/content-tables.mjs +45 -4
  42. package/engine/content-tasklists.mjs +189 -0
  43. package/engine/docs-checks.mjs +4 -4
  44. package/engine/extension-attributes.mjs +48 -2
  45. package/engine/field-reference.mjs +31 -10
  46. package/engine/field-spec.mjs +10 -8
  47. package/engine/foreign-catalog.mjs +143 -18
  48. package/engine/frontmatter-lint.mjs +120 -65
  49. package/engine/frontmatter.mjs +3 -3
  50. package/engine/heading-attributes.mjs +321 -0
  51. package/engine/helpers.mjs +109 -41
  52. package/engine/homepage.mjs +8 -8
  53. package/engine/index.mjs +3 -0
  54. package/engine/infobox-registry.mjs +4 -1
  55. package/engine/infobox-render.mjs +15 -3
  56. package/engine/infobox.mjs +129 -15
  57. package/engine/itinerary-scenes.mjs +4 -10
  58. package/engine/journals.mjs +294 -50
  59. package/engine/lore-events.mjs +197 -0
  60. package/engine/macros.mjs +1 -1
  61. package/engine/markdown-expressions.mjs +434 -59
  62. package/engine/note-dates.mjs +354 -12
  63. package/engine/note-frontmatter.mjs +7 -16
  64. package/engine/note-vocabulary.mjs +183 -42
  65. package/engine/page-lists.mjs +358 -0
  66. package/engine/pathnames.mjs +36 -0
  67. package/engine/pdf-build.mjs +431 -74
  68. package/engine/pdf-images.mjs +64 -0
  69. package/engine/pdf-render.mjs +563 -66
  70. package/engine/rank-ladder.mjs +198 -0
  71. package/engine/reckoning-markers.mjs +1 -1
  72. package/engine/retired-fields.mjs +8 -13
  73. package/engine/scenes.mjs +26 -7
  74. package/engine/site-build.mjs +166 -23
  75. package/engine/site-config.mjs +10 -1
  76. package/engine/site-index.mjs +90 -3
  77. package/engine/site-preview.mjs +20 -7
  78. package/engine/social-ties.mjs +52 -14
  79. package/engine/sql-tables.mjs +66 -9
  80. package/engine/standings.mjs +89 -6
  81. package/engine/system-block.mjs +56 -30
  82. package/engine/web-wikilinks.mjs +48 -1
  83. package/hm3/actors.mjs +8 -5
  84. package/manifest.mjs +43 -24
  85. package/package.json +1 -1
  86. package/packagebuild-metadata.jsonl +13 -13
  87. package/sohl/actors.mjs +22 -99
  88. package/sohl/item-fields.mjs +5 -5
  89. package/sohl/items.mjs +1 -1
  90. package/sohl/note-schemas.mjs +11 -21
  91. package/types/config.d.mts +9 -0
  92. package/types/content-config.d.mts +48 -1
  93. package/types/engine/actor-compiler.d.mts +11 -0
  94. package/types/engine/anchored-sections.d.mts +24 -0
  95. package/types/engine/anchors.d.mts +9 -4
  96. package/types/engine/art-fields.d.mts +64 -0
  97. package/types/engine/asset-index.d.mts +43 -0
  98. package/types/engine/asset-replacement.d.mts +22 -0
  99. package/types/engine/asset-types.d.mts +18 -0
  100. package/types/engine/base-compiler.d.mts +19 -17
  101. package/types/engine/base-styles.d.mts +93 -0
  102. package/types/engine/content-blocks.d.mts +87 -0
  103. package/types/engine/content-embeds.d.mts +16 -0
  104. package/types/engine/content-figures.d.mts +101 -0
  105. package/types/engine/content-footnotes.d.mts +80 -0
  106. package/types/engine/content-format-check.d.mts +8 -6
  107. package/types/engine/content-images.d.mts +78 -12
  108. package/types/engine/content-index.d.mts +8 -2
  109. package/types/engine/content-tables.d.mts +12 -4
  110. package/types/engine/content-tasklists.d.mts +72 -0
  111. package/types/engine/extension-attributes.d.mts +22 -1
  112. package/types/engine/field-reference.d.mts +21 -0
  113. package/types/engine/field-spec.d.mts +20 -16
  114. package/types/engine/foreign-catalog.d.mts +39 -0
  115. package/types/engine/frontmatter.d.mts +1 -1
  116. package/types/engine/heading-attributes.d.mts +179 -0
  117. package/types/engine/helpers.d.mts +29 -10
  118. package/types/engine/homepage.d.mts +7 -7
  119. package/types/engine/index.d.mts +1 -0
  120. package/types/engine/infobox-registry.d.mts +4 -1
  121. package/types/engine/infobox-render.d.mts +6 -1
  122. package/types/engine/infobox.d.mts +6 -0
  123. package/types/engine/journals.d.mts +41 -12
  124. package/types/engine/lore-events.d.mts +14 -0
  125. package/types/engine/markdown-expressions.d.mts +46 -2
  126. package/types/engine/note-dates.d.mts +64 -1
  127. package/types/engine/note-frontmatter.d.mts +1 -4
  128. package/types/engine/note-vocabulary.d.mts +82 -6
  129. package/types/engine/page-lists.d.mts +126 -0
  130. package/types/engine/pathnames.d.mts +26 -0
  131. package/types/engine/pdf-build.d.mts +28 -6
  132. package/types/engine/pdf-images.d.mts +52 -0
  133. package/types/engine/pdf-render.d.mts +84 -2
  134. package/types/engine/rank-ladder.d.mts +32 -0
  135. package/types/engine/retired-fields.d.mts +4 -6
  136. package/types/engine/site-build.d.mts +27 -4
  137. package/types/engine/site-config.d.mts +9 -0
  138. package/types/engine/site-index.d.mts +67 -1
  139. package/types/engine/site-preview.d.mts +1 -1
  140. package/types/engine/sql-tables.d.mts +31 -7
  141. package/types/engine/standings.d.mts +21 -3
  142. package/types/engine/system-block.d.mts +37 -14
  143. package/types/engine/web-wikilinks.d.mts +3 -1
  144. package/types/hm3/actors.d.mts +21 -3
  145. package/types/manifest.d.mts +19 -23
  146. package/types/sohl/actors.d.mts +6 -7
  147. package/engine/content-admonitions.mjs +0 -114
  148. package/engine/content-captions.mjs +0 -181
  149. package/engine/content-secrets.mjs +0 -112
  150. package/types/engine/content-admonitions.d.mts +0 -24
  151. package/types/engine/content-captions.d.mts +0 -45
  152. package/types/engine/content-secrets.d.mts +0 -17
@@ -62,12 +62,20 @@ import { addressSlug } from "./content-address.mjs";
62
62
  import { protectCode } from "./code-fences.mjs";
63
63
  import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
64
64
  import { expandContentTables } from "./content-tables.mjs";
65
- import { renderSecretBlocks } from "./content-secrets.mjs";
66
- import { renderAdmonitions, scanAdmonitions } from "./content-admonitions.mjs";
67
- import { renderCaptionBlocks, scanCaptions } from "./content-captions.mjs";
68
- import { renderImageFigures } from "./content-images.mjs";
69
- import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
70
- import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
65
+ import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
66
+ import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
67
+ import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
68
+ import { footnoteFindings } from "./content-footnotes.mjs";
69
+ import { collectAnchors } from "./anchors.mjs";
70
+ import { checkImages, renderImageFigures } from "./content-images.mjs";
71
+ import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
72
+ import {
73
+ buildSiteIndex,
74
+ figureIndexKeys,
75
+ resolveCrossNoteFigures,
76
+ resolveInfoboxRef,
77
+ wikiContext,
78
+ } from "./site-index.mjs";
71
79
  import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
72
80
  import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
73
81
  import { noteInfoboxes } from "./infobox-registry.mjs";
@@ -86,7 +94,8 @@ import { isNoteRecord, noteFile } from "./index-records.mjs";
86
94
  // The one statement of what an empty body means, shared with the index.
87
95
  import { isStubNote } from "./note-state.mjs";
88
96
  import { isGmNote } from "./note-vocabulary.mjs";
89
- import { ART_SLOTS, artPathname, assetAddressIndex } from "./art-fields.mjs";
97
+ import { ART_SLOTS, artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
98
+ import { embedRole } from "./content-embeds.mjs";
90
99
  import {
91
100
  HOMEPAGE_DESTINATION,
92
101
  checkHomepageCount,
@@ -601,8 +610,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
601
610
  }
602
611
 
603
612
  /**
604
- * The title a content page publishes under: an authored `title`, else the
605
- * note's name.
613
+ * The title a content page publishes under — the note's name.
606
614
  *
607
615
  * One rule, read by the page's own front matter and by every `related` entry
608
616
  * that names the page, so a card lists a page by exactly the title its heading
@@ -612,7 +620,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
612
620
  * @returns {string} The title.
613
621
  */
614
622
  function pageTitle(page) {
615
- return page.fm.title ?? page.name;
623
+ return page.name;
616
624
  }
617
625
 
618
626
  /**
@@ -698,14 +706,18 @@ export function renderSitePage(
698
706
  sqlTables,
699
707
  config,
700
708
  artIndex,
709
+ figuresByAddress = new Map(),
701
710
  },
702
711
  ) {
703
712
  const tableErrors = [];
704
713
  const expressionErrors = [];
705
714
  const secretErrors = [];
706
715
  const captionErrors = [];
716
+ const headingErrors = [];
717
+ const footnoteErrors = [];
707
718
  const wikiErrors = [];
708
719
  const imageErrors = [];
720
+ const embedErrors = [];
709
721
  const resolved = [];
710
722
  const src = page.relPath ?? page.base;
711
723
  const ctx = wikiContext(index, {
@@ -742,10 +754,36 @@ export function renderSitePage(
742
754
  return src;
743
755
  };
744
756
  const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
757
+ // The role lookup a `:::figure` fence's `map` counter reaches through —
758
+ // see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for
759
+ // the page's own scan below, which runs before an embed is rewritten into
760
+ // an ordinary image; `roleByWebSrc` is the same lookup keyed by the web
761
+ // address each picture that reaches `renderImageFigures` resolves to,
762
+ // recorded as it is resolved rather than built across the whole corpus —
763
+ // `webSrc` itself reports an image's own problems as a side effect, which
764
+ // a speculative call over every asset would misfire for one this page
765
+ // never names.
766
+ const resolveRole = (address) => embedRole(artIndex, address);
767
+ const pathRoles = pathnameRoles(artIndex);
768
+ const roleByWebSrc = new Map();
769
+ const webSrcWithRole = (src) => {
770
+ const result = webSrc(src);
771
+ const role = pathRoles.get(src);
772
+ if (role) roleByWebSrc.set(result, role);
773
+ return result;
774
+ };
775
+ // The picture's role and pixel size, by the address it resolved to — read
776
+ // before `webSrc` translates it to the page's own host. A body image is
777
+ // free to write the bare, own-package form, so the address is normalized
778
+ // to the pathname the asset index keys by first.
779
+ const lookupAsset = (src) => {
780
+ const key = assetPathnameKey(src, config);
781
+ return key ? artIndex?.byPath?.get(key) : undefined;
782
+ };
745
783
  const resolve = (text) => {
746
784
  let transformed = pass.beforeLinks ? pass.beforeLinks(text, page) : text;
747
785
  transformed = resolveWebWikilinks(transformed, ctx);
748
- return renderImageFigures(transformed, webSrc);
786
+ return renderImageFigures(transformed, webSrcWithRole, lookupAsset);
749
787
  };
750
788
 
751
789
  const { markdown, errors, lineMap } = expandContentTables(page.body, {
@@ -753,55 +791,104 @@ export function renderSitePage(
753
791
  linkable,
754
792
  source: src,
755
793
  sqlTables: sqlTables?.get(page.file),
794
+ pageLists: sqlTables?.pageLists?.get(page.file),
756
795
  self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
757
796
  });
758
797
  tableErrors.push(...errors);
759
- const captionScan = scanCaptions(markdown);
760
- ctx.captionLabels = new Map(captionScan.captions.map((caption) => [caption.id, caption.label]));
798
+ const figureScan = scanFigures(markdown, { resolveRole });
799
+ ctx.captionLabels = new Map(
800
+ figureScan.figures.filter((figure) => figure.id).map((figure) => [figure.id, figure.label]),
801
+ );
802
+ // What the `ref` expression helper needs beyond the label: the caption
803
+ // text and whether one was authored, by the same id `ctx.captionLabels`
804
+ // keys on.
805
+ const figuresById = new Map(
806
+ figureScan.figures
807
+ .filter((figure) => figure.id)
808
+ .map((figure) => [
809
+ figure.id,
810
+ { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
811
+ ]),
812
+ );
813
+ // This page's own anchors, so a `[[#slug]]` self-link is checked against
814
+ // what the page actually declares rather than trusted unconditionally.
815
+ ctx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
761
816
  const expressions = renderMarkdownExpressions(markdown, {
762
817
  fm: page.fm,
763
818
  dates: index.dateContext,
764
819
  file: page.file,
765
820
  bodyLine: page.bodyLine,
766
821
  sqlResults: sqlTables?.inline?.get(page.file),
822
+ figures: {
823
+ get: (id) => figuresById.get(id),
824
+ // A cross-note `ref` resolves the written address exactly as a
825
+ // wikilink does, against every page's figures scanned before any
826
+ // page's expressions render — see `renderPages`.
827
+ note: (target) =>
828
+ resolveCrossNoteFigures(target, {
829
+ contentTypes: index.contentTypes,
830
+ packages: index.packages,
831
+ noIndexPackages: index.noIndexPackages,
832
+ contentPackage: index.contentPackage,
833
+ siteIndexMap: index.index,
834
+ figuresByAddress,
835
+ }),
836
+ },
767
837
  });
768
838
  expressionErrors.push(...expressions.findings);
769
839
  const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
770
- const secrets = renderSecretBlocks(protectCode(expressions.markdown, resolve), "web");
771
- const admonitions = renderAdmonitions(secrets.markdown);
772
- const captioned = renderCaptionBlocks(admonitions.markdown);
773
- for (const error of captionScan.errors)
840
+ const blocks = renderBlocks(protectCode(expressions.markdown, resolve), "web");
841
+ const figured = renderFigureBlocks(blocks.markdown, undefined, undefined, {
842
+ resolveRole: (address) => roleByWebSrc.get(address),
843
+ });
844
+ for (const error of figureScan.errors)
774
845
  captionErrors.push({
775
846
  file: page.file,
776
847
  line: (page.bodyLine ?? 1) + (lineMap[error.line - 1]?.line ?? error.line - 1),
777
848
  column: error.column,
778
849
  message: error.message,
779
850
  });
780
- for (const error of renderSecretBlocks(page.body, "book").errors)
851
+ for (const error of scanBlocks(page.body).errors)
781
852
  secretErrors.push({
782
853
  file: page.file,
783
854
  line: (page.bodyLine ?? 1) + error.line - 1,
784
855
  column: error.column,
785
856
  message: error.message,
786
857
  });
787
- for (const error of scanAdmonitions(page.body).errors)
788
- secretErrors.push({
858
+ for (const error of scanHeadingAttributes(page.body, page.bodyLine ?? 1).errors)
859
+ headingErrors.push({ file: page.file, ...error });
860
+ for (const error of withheldSections(page.body, page.bodyLine ?? 1).errors)
861
+ headingErrors.push({ file: page.file, ...error });
862
+ for (const error of footnoteFindings(page.body))
863
+ footnoteErrors.push({
789
864
  file: page.file,
790
865
  line: (page.bodyLine ?? 1) + error.line - 1,
791
866
  column: error.column,
792
867
  message: error.message,
793
868
  });
869
+ // An image sharing its paragraph with other text, or an address or title
870
+ // the lint already refuses — asked here too, so a site build run on its
871
+ // own fails on the same input `renderImageFigures` otherwise renders as
872
+ // though the directive were absent.
873
+ for (const error of checkImages(page.body, page.file, { bodyLine: page.bodyLine ?? 1 }))
874
+ embedErrors.push(error);
794
875
  return {
795
876
  page,
796
- body: captioned.markdown,
877
+ // The disclosure is written last, over the Markdown the page ships: the
878
+ // passes before this one carry line positions into their findings, and a
879
+ // line inserted ahead of them would move every one of them.
880
+ body: renderWithheldSections(figured.markdown),
797
881
  data,
798
882
  resolved,
799
883
  tableErrors,
800
884
  expressionErrors,
801
885
  secretErrors,
802
886
  captionErrors,
887
+ headingErrors,
888
+ footnoteErrors,
803
889
  wikiErrors,
804
890
  imageErrors,
891
+ embedErrors,
805
892
  };
806
893
  }
807
894
 
@@ -840,7 +927,7 @@ export function renderSitePage(
840
927
  * it by wikilink, and its own markdown links name content pages. `maps` is
841
928
  * each drawing by the URL of the page that carries it.
842
929
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
843
- * wikiErrors: object[], imageErrors: object[],
930
+ * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
844
931
  * related: Map<string, import("./related-pages.mjs").Related>,
845
932
  * maps: number}} `related`
846
933
  * is keyed by page URL, and holds the homepage's block beside every content
@@ -873,12 +960,47 @@ export function renderPages(pages, options) {
873
960
  types: index?.contentTypes ?? [],
874
961
  });
875
962
 
963
+ // Every page's own figures, by the address a `ref` crossing into it would
964
+ // write — scanned once, before any page's own expressions render, so a
965
+ // page citing another's figure finds it already numbered. Numbering stays
966
+ // per page: each `scanFigures` call here starts its own counters, exactly
967
+ // as the page's own render does, so the number a cross-note `ref` reports
968
+ // is the one the target's own page carries.
969
+ const figuresByAddress = new Map();
970
+ for (const page of pages) {
971
+ const shortcode = page.fm.shortcode;
972
+ if (typeof shortcode !== "string" || !shortcode) continue;
973
+ const src = page.relPath ?? page.base;
974
+ const { markdown } = expandContentTables(page.body, {
975
+ docs: universe.get(page.pkg) ?? [],
976
+ linkable,
977
+ source: src,
978
+ sqlTables: sqlTables?.get(page.file),
979
+ pageLists: sqlTables?.pageLists?.get(page.file),
980
+ self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
981
+ });
982
+ const byId = new Map(
983
+ scanFigures(markdown)
984
+ .figures.filter((figure) => figure.id)
985
+ .map((figure) => [
986
+ figure.id,
987
+ { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
988
+ ]),
989
+ );
990
+ const type = String(page.fm.type ?? "").toLowerCase();
991
+ for (const key of figureIndexKeys(page.pkg, type, shortcode))
992
+ figuresByAddress.set(key, byId);
993
+ }
994
+
876
995
  const tableErrors = [];
877
996
  const expressionErrors = [];
878
997
  const secretErrors = [];
879
998
  const captionErrors = [];
999
+ const headingErrors = [];
1000
+ const footnoteErrors = [];
880
1001
  const wikiErrors = [];
881
1002
  const imageErrors = [];
1003
+ const embedErrors = [];
882
1004
  const byKind = {};
883
1005
  // The link graph, as `(source URL, target URL)` — read off each page's
884
1006
  // resolution below, and off the homepage's markdown links.
@@ -915,13 +1037,17 @@ export function renderPages(pages, options) {
915
1037
  sqlTables,
916
1038
  config,
917
1039
  artIndex,
1040
+ figuresByAddress,
918
1041
  });
919
1042
  tableErrors.push(...result.tableErrors);
920
1043
  expressionErrors.push(...result.expressionErrors);
921
1044
  secretErrors.push(...result.secretErrors);
922
1045
  captionErrors.push(...result.captionErrors);
1046
+ headingErrors.push(...result.headingErrors);
1047
+ footnoteErrors.push(...result.footnoteErrors);
923
1048
  wikiErrors.push(...result.wikiErrors);
924
1049
  imageErrors.push(...result.imageErrors);
1050
+ embedErrors.push(...result.embedErrors);
925
1051
  rendered.push({ page, body: result.body, data: result.data });
926
1052
  for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
927
1053
  }
@@ -963,8 +1089,11 @@ export function renderPages(pages, options) {
963
1089
  expressionErrors,
964
1090
  secretErrors,
965
1091
  captionErrors,
1092
+ headingErrors,
1093
+ footnoteErrors,
966
1094
  wikiErrors,
967
1095
  imageErrors,
1096
+ embedErrors,
968
1097
  related,
969
1098
  maps: withMap,
970
1099
  ...(capture ? { ...(outputs ? { outputs } : {}), edges, entries } : {}),
@@ -1015,6 +1144,7 @@ export function sitePageDecorator(config, index) {
1015
1144
  resolve: (ref, hint) => resolveInfoboxRef(index, ref, hint),
1016
1145
  router,
1017
1146
  dates: index.dateContext,
1147
+ contentPackage: index.contentPackage,
1018
1148
  });
1019
1149
  };
1020
1150
  }
@@ -1036,7 +1166,8 @@ export function sitePageDecorator(config, index) {
1036
1166
  * `sql` directive with none prepared is a table error: nothing here runs a
1037
1167
  * query.
1038
1168
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
1039
- * wikiErrors: object[], imageErrors: object[], mapFindings: object[],
1169
+ * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
1170
+ * mapFindings: object[],
1040
1171
  * manifests: object|null}} `mapFindings` is what drawing the maps found —
1041
1172
  * warnings, never a reason to fail the build.
1042
1173
  */
@@ -1131,8 +1262,11 @@ export function buildSite({ config, sqlTables } = {}) {
1131
1262
  expressionErrors: [],
1132
1263
  secretErrors: [],
1133
1264
  captionErrors: [],
1265
+ headingErrors: [],
1266
+ footnoteErrors: [],
1134
1267
  wikiErrors: [],
1135
1268
  imageErrors: [],
1269
+ embedErrors: [],
1136
1270
  mapFindings: [],
1137
1271
  stats: null,
1138
1272
  };
@@ -1153,8 +1287,11 @@ export function buildSite({ config, sqlTables } = {}) {
1153
1287
  expressionErrors: [],
1154
1288
  secretErrors: [],
1155
1289
  captionErrors: [],
1290
+ headingErrors: [],
1291
+ footnoteErrors: [],
1156
1292
  wikiErrors: [],
1157
1293
  imageErrors: [],
1294
+ embedErrors: [],
1158
1295
  mapFindings: [],
1159
1296
  stats: {
1160
1297
  homepages: writeHomepages(homeRoot, homepages, resolved),
@@ -1201,8 +1338,11 @@ export function buildSite({ config, sqlTables } = {}) {
1201
1338
  expressionErrors: [],
1202
1339
  secretErrors: [],
1203
1340
  captionErrors: [],
1341
+ headingErrors: [],
1342
+ footnoteErrors: [],
1204
1343
  wikiErrors: [],
1205
1344
  imageErrors: [],
1345
+ embedErrors: [],
1206
1346
  mapFindings: [],
1207
1347
  };
1208
1348
  }
@@ -1259,8 +1399,11 @@ export function buildSite({ config, sqlTables } = {}) {
1259
1399
  expressionErrors: rendered.expressionErrors,
1260
1400
  secretErrors: rendered.secretErrors,
1261
1401
  captionErrors: rendered.captionErrors,
1402
+ headingErrors: rendered.headingErrors,
1403
+ footnoteErrors: rendered.footnoteErrors,
1262
1404
  wikiErrors: rendered.wikiErrors,
1263
1405
  imageErrors: rendered.imageErrors,
1406
+ embedErrors: rendered.embedErrors,
1264
1407
  mapFindings: drawn.findings,
1265
1408
  stats: {
1266
1409
  ...rendered.byKind,
@@ -102,9 +102,18 @@ export const DISABLE_KINDS = Object.freeze(["section", "taxonomy", "term", "RSS"
102
102
  * `<span>` marking an unresolved link — and Goldmark drops raw HTML unless
103
103
  * told otherwise. A theme cannot supply this: Hugo does not merge a theme's
104
104
  * `markup` block.
105
+ *
106
+ * `extensions.taskList` is off because the format has no checkbox: Goldmark
107
+ * enables it by default, which is the only reason a list item opened with
108
+ * `[ ]` renders a `<input type="checkbox">` here and the same two characters as
109
+ * plain text in the pack and the book. Off, a list item's text begins right
110
+ * after its marker on every surface.
105
111
  */
106
112
  export const MARKUP = Object.freeze({
107
- goldmark: Object.freeze({ renderer: Object.freeze({ unsafe: true }) }),
113
+ goldmark: Object.freeze({
114
+ renderer: Object.freeze({ unsafe: true }),
115
+ extensions: Object.freeze({ taskList: false }),
116
+ }),
108
117
  });
109
118
 
110
119
  /** Where the navigation is published. */
@@ -51,6 +51,9 @@
51
51
 
52
52
  import { buildReferenceTargets } from "./reference-targets.mjs";
53
53
  import { standingsDigest } from "./standings.mjs";
54
+ // The leaf reader, shared with the content index and the link checker, so a
55
+ // page's anchors are read once rather than re-derived a third way here.
56
+ import { collectAnchors } from "./anchors.mjs";
54
57
 
55
58
  import { resolveShortcodeReference } from "./shortcode-references.mjs";
56
59
 
@@ -63,10 +66,12 @@ import {
63
66
  renderAddress,
64
67
  isAddressTuple,
65
68
  ownDocumentSystem,
69
+ expandAddress,
66
70
  } from "./address.mjs";
67
71
  import { NOTE_SYSTEM } from "./systems.mjs";
68
72
  import { contentPackage } from "./content-package.mjs";
69
73
  import { reckoningContext } from "./reckoning-markers.mjs";
74
+ import { presentAmongRecords } from "./being-age.mjs";
70
75
  // The declared tag vocabulary, which is where `draft` is stated.
71
76
  import { isDraftNote } from "./note-vocabulary.mjs";
72
77
 
@@ -83,16 +88,24 @@ import { isDraftNote } from "./note-vocabulary.mjs";
83
88
  * @property {string} slug URL segment.
84
89
  * @property {string} base Source file's basename, e.g. `Climbing.md`.
85
90
  * @property {string} url The page's published address.
91
+ * @property {string} [body] The note's markdown body, for its anchors. A
92
+ * content entry always carries one; absent, the
93
+ * page is indexed as declaring none.
86
94
  */
87
95
 
88
96
  /**
89
97
  * The resolved index and everything a wikilink resolver reads beside it.
90
98
  *
91
99
  * @typedef {object} SiteIndex
92
- * @property {Map<string, {url: string, name?: string, draft?: boolean}>} index
100
+ * @property {Map<string, {url: string, name?: string, draft?: boolean, anchors?: Set<string>|Record<string, string>}>} index
93
101
  * Address → page. `draft` says the page
94
102
  * carries the `draft` tag, which marks a
95
- * link *into* it.
103
+ * link *into* it. `anchors` names the
104
+ * `{#slug}` sections it declares — a
105
+ * `Set` for a local page, or the
106
+ * `{slug: uuid}` map a foreign manifest
107
+ * publishes — and is absent where
108
+ * neither build recorded one.
96
109
  * @property {Set<string>} ambiguous Short addresses claimed by two
97
110
  * packages, and so deliberately absent
98
111
  * from `index`.
@@ -229,6 +242,11 @@ export function buildSiteIndex(
229
242
  draft: isDraftNote(e.fm),
230
243
  ...(e.fm.subType ? { subType: e.fm.subType } : {}),
231
244
  ...(standings ? { standings } : {}),
245
+ // The `{#anchor}` slugs this page declares, so a `#section`
246
+ // written against it can be checked rather than joined onto the
247
+ // URL unverified. A Set even when empty: a page that declares no
248
+ // anchor still has a definite answer for one an author names.
249
+ anchors: new Set(collectAnchors(e.body ?? "").map((anchor) => anchor.slug)),
232
250
  };
233
251
 
234
252
  const shortcode = e.fm.shortcode;
@@ -261,7 +279,12 @@ export function buildSiteIndex(
261
279
 
262
280
  return {
263
281
  contentPackage: ownPackage,
264
- dateContext: reckoningContext({ notes: records }),
282
+ // Carried on the same context `resolvedDateFields` reads: a recurring
283
+ // event's `next` is computed against this present.
284
+ dateContext: {
285
+ ...reckoningContext({ notes: records }),
286
+ present: presentAmongRecords(records),
287
+ },
265
288
  referenceTargets: buildReferenceTargets(records, new Map([...foreignReferences, ...index])),
266
289
  index,
267
290
  ambiguous,
@@ -272,6 +295,70 @@ export function buildSiteIndex(
272
295
  };
273
296
  }
274
297
 
298
+ /**
299
+ * The address keys one page's figures are reachable by — the same two forms
300
+ * {@link buildSiteIndex} assigns the page itself, so a `ref` crossing into it
301
+ * resolves by exactly the address a wikilink would use.
302
+ *
303
+ * @param {string} pkg - The page's own content package.
304
+ * @param {string} type - The note's `type`.
305
+ * @param {string} shortcode - The note's `shortcode`.
306
+ * @returns {Set<string>} The short `type/shortcode` key and the canonical
307
+ * `package-system-type-shortcode` one.
308
+ */
309
+ export function figureIndexKeys(pkg, type, shortcode) {
310
+ const system = ownDocumentSystem(type);
311
+ const keys = new Set([
312
+ `${type}/${shortcode}`.toLowerCase(),
313
+ canonicalKey(pkg, system, type, shortcode),
314
+ ]);
315
+ if (system !== NOTE_SYSTEM) keys.add(canonicalKey(pkg, NOTE_SYSTEM, type, shortcode));
316
+ return keys;
317
+ }
318
+
319
+ /**
320
+ * Resolve a `ref` call's cross-note address to the target's own figures and
321
+ * its own address — the `figures.note` a `ref` helper's render context reads,
322
+ * built once and threaded into every page.
323
+ *
324
+ * Reads the written address exactly as a wikilink does
325
+ * ({@link readQualifier}, {@link expandAddress}), so a `ref` crossing into
326
+ * another note and a `[[…]]` link to the same note agree on what resolves and
327
+ * what does not: a target naming no known type or no fetched package's index
328
+ * is unresolved for both, and one address names one note for both.
329
+ *
330
+ * @param {string} target - The address as written, anchor already removed —
331
+ * {@link module:engine/wikilink-syntax.ParsedWikilink.target}.
332
+ * @param {object} options
333
+ * @param {Set<string>} options.contentTypes - Every type this build reads as
334
+ * an address qualifier.
335
+ * @param {Set<string>} options.packages - Every package an address may name.
336
+ * @param {Set<string>} options.noIndexPackages - Packages declared
337
+ * `contentIndex: false`.
338
+ * @param {string} options.contentPackage - The package a target with no
339
+ * package segment defaults to.
340
+ * @param {Map<string, {url: string}>} options.siteIndexMap - {@link SiteIndex}'s
341
+ * own `index`, read for the target's existence and its URL.
342
+ * @param {Map<string, Map<string, object>>} options.figuresByAddress - Every
343
+ * note's figures, by id, keyed as {@link figureIndexKeys} keys the note.
344
+ * @returns {{url: string, figures: Map<string, object>}|undefined} The
345
+ * target's own address and figures, or `undefined` when the address names
346
+ * no note this build resolves.
347
+ */
348
+ export function resolveCrossNoteFigures(
349
+ target,
350
+ { contentTypes, packages, noIndexPackages, contentPackage, siteIndexMap, figuresByAddress },
351
+ ) {
352
+ const read = readQualifier(target, contentTypes, packages, noIndexPackages);
353
+ if (!read || read.reason) return undefined;
354
+ const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
355
+ const shortKey = `${read.itemDoc ? "doc" : ""}${read.type}/${read.shortcode}`.toLowerCase();
356
+ const entry = siteIndexMap.get(key) ?? siteIndexMap.get(shortKey);
357
+ if (!entry) return undefined;
358
+ const figures = figuresByAddress.get(key) ?? figuresByAddress.get(shortKey) ?? new Map();
359
+ return { url: entry.url, figures };
360
+ }
361
+
275
362
  /**
276
363
  * The per-page context a wikilink resolver takes.
277
364
  *
@@ -18,6 +18,7 @@ import { reckoningContext } from "./reckoning-markers.mjs";
18
18
  import { cachedMetadataIndexes, noContentIndexPackages } from "./metadata-index.mjs";
19
19
  import { buildSiteIndex } from "./site-index.mjs";
20
20
  import { openNotesDatabase, prepareSqlTables, findSqlBlocks } from "./sql-tables.mjs";
21
+ import { findPageListBlocks } from "./page-lists.mjs";
21
22
  import { isGmNote } from "./note-vocabulary.mjs";
22
23
  import { relatedPages } from "./related-pages.mjs";
23
24
  import { holdingsNode, holdingsPages, foreignHoldingsNodes } from "./holdings.mjs";
@@ -90,9 +91,13 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
90
91
  );
91
92
  try {
92
93
  const sources = pages
93
- .filter((page) => findSqlBlocks(page.body).length)
94
+ .filter(
95
+ (page) =>
96
+ findSqlBlocks(page.body).length || findPageListBlocks(page.body).length,
97
+ )
94
98
  .map((page) => ({ source: page.file, markdown: page.body }));
95
- const sqlTables = await prepareSqlTables(db, sources);
99
+ const publicRecords = records.filter((record) => !isGmNote(record));
100
+ const sqlTables = await prepareSqlTables(db, sources, { records: publicRecords });
96
101
  const rendered = renderPages(pages, {
97
102
  index: gates.index,
98
103
  foreign: gates.foreign,
@@ -214,9 +219,11 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
214
219
  },
215
220
  );
216
221
  }
217
- const sqlTables = await prepareSqlTables(db, [
218
- { source: absolute, markdown: body },
219
- ]);
222
+ const sqlTables = await prepareSqlTables(
223
+ db,
224
+ [{ source: absolute, markdown: body }],
225
+ { records: records.filter((record) => !isGmNote(record)) },
226
+ );
220
227
  const allPages = snapshot.pages.map((item) =>
221
228
  item.file === absolute ? page : item,
222
229
  );
@@ -246,6 +253,10 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
246
253
  findings.push({ ...error, severity: "error" });
247
254
  for (const error of result.captionErrors)
248
255
  findings.push({ ...error, severity: "error" });
256
+ for (const error of result.headingErrors)
257
+ findings.push({ ...error, severity: "error" });
258
+ for (const error of result.footnoteErrors)
259
+ findings.push({ ...error, severity: "error" });
249
260
  for (const error of result.wikiErrors) {
250
261
  const pos = positionOfLiteral(text, error.link, error.occurrence);
251
262
  findings.push({
@@ -259,13 +270,15 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
259
270
  const pos = positionOfLiteral(text, error.src, error.occurrence);
260
271
  findings.push({ ...error, ...pos, severity: "error" });
261
272
  }
273
+ for (const error of result.embedErrors)
274
+ findings.push({ ...error, severity: "error" });
262
275
  if (findings.length) return { ok: false, findings };
263
276
  const edges = snapshot.rendered.edges.filter(([source]) => source !== original.url);
264
277
  for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
265
278
  const entries = new Map(snapshot.rendered.entries);
266
279
  entries.delete(original.url);
267
280
  entries.set(page.url, {
268
- title: fm.title ?? page.name,
281
+ title: page.name,
269
282
  url: page.url,
270
283
  type: String(fm.type),
271
284
  });
@@ -273,7 +286,7 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
273
286
  if (related) result.data.related = related;
274
287
  const holdings = holdingsPages([
275
288
  ...allPages.map((item) =>
276
- holdingsNode(item.fm, { title: item.fm.title ?? item.name, url: item.url }),
289
+ holdingsNode(item.fm, { title: item.name, url: item.url }),
277
290
  ),
278
291
  ...foreignHoldingsNodes(snapshot.gates.foreign.index),
279
292
  ]);