@heroiclands/package-build 23.0.0 → 23.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +124 -141
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/frontmatter.md +2 -2
  9. package/docs/authoring/links-and-markup.md +145 -29
  10. package/docs/commands.md +8 -10
  11. package/docs/configuration.md +13 -11
  12. package/docs/content-format.md +2 -2
  13. package/docs/guides/book.md +59 -4
  14. package/docs/guides/icons.md +4 -5
  15. package/docs/guides/packs.md +6 -0
  16. package/docs/guides/reference-pages.md +21 -1
  17. package/docs/guides/release-and-deploy.md +39 -7
  18. package/docs/guides/site.md +17 -18
  19. package/docs/reference/format-details.md +877 -276
  20. package/docs/reference/note-types.md +278 -91
  21. package/docs/reference/theme-pages.md +58 -97
  22. package/engine/address-values.mjs +124 -4
  23. package/engine/address.mjs +44 -3
  24. package/engine/anchors.mjs +103 -3
  25. package/engine/art-slots.mjs +2 -9
  26. package/engine/base-compiler.mjs +25 -6
  27. package/engine/calendar-artifacts.mjs +4 -1
  28. package/engine/calendar-notes.mjs +166 -102
  29. package/engine/content-format-check.mjs +2 -3
  30. package/engine/content-format.mjs +44 -1
  31. package/engine/content-format.yaml +79 -16
  32. package/engine/content-images.mjs +10 -1
  33. package/engine/content-index.mjs +37 -9
  34. package/engine/content-links.mjs +24 -5
  35. package/engine/content-tables.mjs +24 -3
  36. package/engine/data-keys.mjs +347 -0
  37. package/engine/derived-sections.mjs +322 -0
  38. package/engine/diagnostics.mjs +28 -0
  39. package/engine/event-fields.mjs +187 -0
  40. package/engine/event-rows.mjs +126 -0
  41. package/engine/event-views.mjs +305 -0
  42. package/engine/exported-scene.mjs +29 -53
  43. package/engine/frontmatter-lint.mjs +103 -77
  44. package/engine/frontmatter.mjs +7 -2
  45. package/engine/generate.mjs +53 -0
  46. package/engine/generated-sections.mjs +223 -0
  47. package/engine/heading-attributes.mjs +4 -2
  48. package/engine/helpers.mjs +12 -1
  49. package/engine/holdings.mjs +12 -33
  50. package/engine/ids.mjs +17 -11
  51. package/engine/index.mjs +4 -4
  52. package/engine/infobox-render.mjs +21 -6
  53. package/engine/infobox.mjs +217 -99
  54. package/engine/journals.mjs +104 -47
  55. package/engine/literature-notes.mjs +4 -1
  56. package/engine/literature-works.mjs +21 -13
  57. package/engine/map-graphviz.mjs +4 -18
  58. package/engine/markdown-expressions.mjs +116 -11
  59. package/engine/metadata-index.mjs +26 -0
  60. package/engine/near-miss.mjs +90 -0
  61. package/engine/note-addresses.mjs +75 -19
  62. package/engine/note-dates.mjs +5 -3
  63. package/engine/note-event-terms.mjs +364 -0
  64. package/engine/note-events.mjs +823 -0
  65. package/engine/note-vocabulary.mjs +184 -88
  66. package/engine/office-holders.mjs +20 -54
  67. package/engine/packagebuild-index.mjs +16 -1
  68. package/engine/pdf-build.mjs +97 -60
  69. package/engine/pdf-index.mjs +263 -0
  70. package/engine/pdf-render.mjs +47 -5
  71. package/engine/place-relation-terms.mjs +174 -0
  72. package/engine/place-relations.mjs +72 -160
  73. package/engine/rank-ladder.mjs +14 -41
  74. package/engine/reckoning-markers.mjs +2 -0
  75. package/engine/region-events.mjs +6 -12
  76. package/engine/scenes.mjs +73 -468
  77. package/engine/site-build.mjs +75 -101
  78. package/engine/site-index.mjs +3 -1
  79. package/engine/site-maps.mjs +43 -29
  80. package/engine/site-preview.mjs +40 -31
  81. package/engine/social-ties.mjs +2 -1
  82. package/engine/sql-tables.mjs +212 -9
  83. package/engine/standing-terms.mjs +145 -6
  84. package/engine/standings.mjs +37 -49
  85. package/engine/web-wikilinks.mjs +21 -1
  86. package/engine/wikilink-syntax.mjs +34 -4
  87. package/engine/wikilinks.mjs +27 -2
  88. package/engine/yaml-lint.mjs +7 -4
  89. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  90. package/hugo-theme/layouts/partials/page-body.html +0 -3
  91. package/hugo-theme/static/css/style.css +25 -55
  92. package/package.json +1 -2
  93. package/packagebuild-metadata.jsonl +13 -13
  94. package/release.mjs +62 -4
  95. package/sohl/note-schemas.mjs +8 -73
  96. package/types/engine/address-values.d.mts +50 -3
  97. package/types/engine/address.d.mts +21 -0
  98. package/types/engine/anchors.d.mts +49 -2
  99. package/types/engine/art-slots.d.mts +1 -1
  100. package/types/engine/base-compiler.d.mts +13 -4
  101. package/types/engine/calendar-notes.d.mts +7 -0
  102. package/types/engine/content-format.d.mts +52 -0
  103. package/types/engine/content-images.d.mts +5 -1
  104. package/types/engine/content-tables.d.mts +9 -1
  105. package/types/engine/data-keys.d.mts +145 -0
  106. package/types/engine/derived-sections.d.mts +73 -0
  107. package/types/engine/diagnostics.d.mts +15 -0
  108. package/types/engine/event-fields.d.mts +99 -0
  109. package/types/engine/event-rows.d.mts +12 -0
  110. package/types/engine/event-views.d.mts +67 -0
  111. package/types/engine/exported-scene.d.mts +5 -2
  112. package/types/engine/frontmatter-lint.d.mts +3 -3
  113. package/types/engine/generated-sections.d.mts +143 -0
  114. package/types/engine/holdings.d.mts +6 -20
  115. package/types/engine/ids.d.mts +15 -12
  116. package/types/engine/index.d.mts +1 -1
  117. package/types/engine/infobox.d.mts +13 -3
  118. package/types/engine/journals.d.mts +24 -11
  119. package/types/engine/literature-works.d.mts +1 -2
  120. package/types/engine/map-graphviz.d.mts +2 -10
  121. package/types/engine/markdown-expressions.d.mts +12 -3
  122. package/types/engine/near-miss.d.mts +42 -0
  123. package/types/engine/note-addresses.d.mts +7 -1
  124. package/types/engine/note-event-terms.d.mts +68 -0
  125. package/types/engine/note-events.d.mts +71 -0
  126. package/types/engine/note-vocabulary.d.mts +37 -8
  127. package/types/engine/office-holders.d.mts +3 -0
  128. package/types/engine/pdf-index.d.mts +35 -0
  129. package/types/engine/pdf-render.d.mts +8 -2
  130. package/types/engine/place-relation-terms.d.mts +83 -0
  131. package/types/engine/place-relations.d.mts +11 -66
  132. package/types/engine/rank-ladder.d.mts +2 -0
  133. package/types/engine/region-events.d.mts +6 -12
  134. package/types/engine/scenes.d.mts +6 -32
  135. package/types/engine/site-build.d.mts +8 -22
  136. package/types/engine/site-maps.d.mts +15 -7
  137. package/types/engine/sql-tables.d.mts +77 -2
  138. package/types/engine/standing-terms.d.mts +37 -4
  139. package/types/engine/standings.d.mts +14 -9
  140. package/types/engine/wikilink-syntax.d.mts +20 -0
  141. package/types/engine/wikilinks.d.mts +3 -2
  142. package/types/engine/yaml-lint.d.mts +5 -4
  143. package/types/release.d.mts +4 -0
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  153. package/hugo-theme/layouts/partials/place-map.html +0 -9
  154. package/types/engine/itinerary-scenes.d.mts +0 -29
  155. package/types/engine/lore-events.d.mts +0 -14
  156. package/types/engine/map-notes.d.mts +0 -297
  157. package/types/engine/map-raster.d.mts +0 -8
@@ -11,7 +11,7 @@
11
11
  * SPDX-License-Identifier: GPL-3.0-or-later
12
12
  */
13
13
 
14
- import { parseAddress, isAddressTuple } from "./address.mjs";
14
+ import { parseAddress, isAddressTuple, splitAnchor } from "./address.mjs";
15
15
 
16
16
  /**
17
17
  * What a `[[…]]` **is**, before anything decides where it points.
@@ -104,9 +104,9 @@ export function parseWikilink(rawInner) {
104
104
  const linkPart = (labelled ? inner.slice(0, bar) : inner).trim();
105
105
  const display = labelled ? inner.slice(bar + 1).trim() : null;
106
106
 
107
- const hash = linkPart.indexOf("#");
108
- const target = (hash === -1 ? linkPart : linkPart.slice(0, hash)).trim();
109
- const anchor = hash === -1 ? "" : linkPart.slice(hash + 1).trim();
107
+ // The anchor is read by the one reader a frontmatter Address uses, so a
108
+ // link and a field cannot disagree about where the anchor starts.
109
+ const { address: target, anchor = "" } = splitAnchor(linkPart);
110
110
 
111
111
  return { inner: inner.trim(), target, anchor, display, labelled };
112
112
  }
@@ -206,10 +206,32 @@ export const LINK_FINDING_REASONS = Object.freeze(
206
206
  "gm",
207
207
  "ambiguous",
208
208
  "unknown-anchor",
209
+ "event-anchor",
209
210
  "no-content-index",
210
211
  ]),
211
212
  );
212
213
 
214
+ /**
215
+ * Whether an anchor a link names is one of its target's events.
216
+ *
217
+ * A wikilink names a `prose` anchor; an `event` anchor is named only by the
218
+ * frontmatter keys that accept one. A local target carries its event ids as
219
+ * `eventAnchors`, a target another package publishes carries its kinded
220
+ * anchors as `noteAnchors`.
221
+ *
222
+ * @param {{eventAnchors?: Set<string>, noteAnchors?: Array<{slug: string,
223
+ * kind?: string}>}|undefined} hit - The link's resolved target.
224
+ * @param {string} anchor - The anchor the link names.
225
+ * @returns {boolean} Whether it is an event.
226
+ */
227
+ export function isEventAnchor(hit, anchor) {
228
+ if (!hit || !anchor) return false;
229
+ if (hit.eventAnchors?.has?.(anchor)) return true;
230
+ return Array.isArray(hit.noteAnchors) ?
231
+ hit.noteAnchors.some((one) => one?.slug === anchor && one.kind === "event")
232
+ : false;
233
+ }
234
+
213
235
  /**
214
236
  * What an author writing an address that resolves to nothing is told.
215
237
  *
@@ -341,6 +363,14 @@ export function linkFindingMessage({ reason, target, packages, anchor, type, stu
341
363
  `address [[${target}]] resolves, but no section "#${anchor ?? ""}" ` +
342
364
  `is published for it`
343
365
  );
366
+ case "event-anchor":
367
+ // A link on the event's own note has no note to link to instead,
368
+ // so it names nothing to write in its place.
369
+ return target ?
370
+ `address [[${target}#${anchor ?? ""}]] names an event, and prose links to ` +
371
+ `the note, not to an event — write [[${target}|…]]`
372
+ : `address [[#${anchor ?? ""}]] names an event of this note, and prose ` +
373
+ "links to sections, not to events — write the words without a link";
344
374
  case "unresolved":
345
375
  return unresolvedAddressMessage(target);
346
376
  case "stub":
@@ -56,6 +56,7 @@ import { replaceOutsideCode } from "./code-fences.mjs";
56
56
  import {
57
57
  authoredLabel,
58
58
  WIKILINK,
59
+ isEventAnchor,
59
60
  parseWikilink,
60
61
  unlabelledLinkMessage,
61
62
  } from "./wikilink-syntax.mjs";
@@ -100,8 +101,8 @@ export function anchorPageId(noteId, anchorSlug) {
100
101
  *
101
102
  * @param {Array<{type: string, id: string, shortcode?: string|null,
102
103
  * name?: string, pack?: string, docPack?: string, none?: boolean,
103
- * draft?: boolean, anchors?: Set<string>, anchorUuids?: Record<string,string>,
104
- * docAnchorUuids?: Record<string,string>}>} docs -
104
+ * draft?: boolean, anchors?: Set<string>, eventAnchors?: Set<string>,
105
+ * anchorUuids?: Record<string,string>, docAnchorUuids?: Record<string,string>}>} docs -
105
106
  * One entry per content note. `pack` / `docPack` name the packs the note's
106
107
  * document and its documentation entry landed in; omitted, the conventional
107
108
  * one-pack-per-type names stand in. `none` says the note declares
@@ -486,6 +487,17 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index, cap
486
487
  return unresolvedLink(text || target, target);
487
488
  }
488
489
  const hit = hits[0] ?? null;
490
+ if (hit && slug && isEventAnchor(hit, slug)) {
491
+ unresolved.push({
492
+ link: all,
493
+ target,
494
+ offset,
495
+ reason: "event-anchor",
496
+ anchor: slug,
497
+ addressed: true,
498
+ });
499
+ return unresolvedLink(text || target, target);
500
+ }
489
501
  if (hit) {
490
502
  const uuid = slug ? hit.anchors?.[slug] : hit.uuid;
491
503
  if (uuid) {
@@ -553,6 +565,19 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index, cap
553
565
  // the manifest carries the map — and a local one now is too, from the
554
566
  // anchor set the index carries.
555
567
  const anchorUuids = itemDoc ? addresses.docAnchors : addresses.anchors;
568
+ // A wikilink names a prose anchor; an event's id is refused here, before
569
+ // any surface renders a link to it.
570
+ if (slug && isEventAnchor(doc, slug)) {
571
+ unresolved.push({
572
+ link: all,
573
+ target,
574
+ offset,
575
+ reason: "event-anchor",
576
+ anchor: slug,
577
+ addressed: true,
578
+ });
579
+ return unresolvedLink(text || doc.name || target, target);
580
+ }
556
581
  if (
557
582
  slug &&
558
583
  isJournal &&
@@ -104,10 +104,11 @@ export const frontmatterProcessor = {
104
104
  * - `no-empty-key`, `no-empty-document` — a fence or a file that parses to
105
105
  * nothing at all.
106
106
  *
107
- * **GitHub workflows are exempt from `no-empty-mapping-value`**, because an
108
- * empty value is the language there: `on:` `push:` and `workflow_dispatch:`
109
- * carry their meaning by being present, and writing `push: null` to satisfy a
110
- * linter would be worse YAML, not better.
107
+ * **Workflows are exempt from `no-empty-mapping-value`**, under
108
+ * `.github/workflows/` and Gitea's `.gitea/workflows/` alike, because an empty
109
+ * value is the language there: `on:` `push:` and `workflow_dispatch:` carry
110
+ * their meaning by being present, and writing `push: null` to satisfy a linter
111
+ * would be worse YAML, not better.
111
112
  *
112
113
  * @param {object} plugin - The `eslint-plugin-yml` module.
113
114
  * @returns {object[]} A complete flat config, for `overrideConfig`.
@@ -128,6 +129,8 @@ export function yamlLintConfig(plugin) {
128
129
  files: [
129
130
  "**/.github/workflows/*.yml",
130
131
  "**/.github/workflows/*.yaml",
132
+ "**/.gitea/workflows/*.yml",
133
+ "**/.gitea/workflows/*.yaml",
131
134
  "**/action.yml",
132
135
  "**/action.yaml",
133
136
  ],
@@ -1,13 +1,16 @@
1
1
  {{- /*
2
2
  One entry of a `runin` group or a `list` section.
3
3
 
4
- An entry is `{text, url}`. It links when the build reached the page it
4
+ An entry is `{lead, text, url}`. It links when the build reached the page it
5
5
  names, and keeps its words when it did not — a panel naming something this
6
6
  site does not publish is better than a panel silently one entry short.
7
+ `lead`, where present, is plain text set before the link, so a line such as
8
+ a membership links the body's name and nothing else.
7
9
 
8
10
  Takes `entry` and the `class` its medium gives it.
9
11
  */ -}}
10
12
  {{- $entry := .entry -}}
13
+ {{- with $entry.lead }}{{ . }}{{ end -}}
11
14
  {{- with $entry.url -}}
12
15
  <a class="{{ $.class }}" href="{{ . }}">{{ $entry.text }}</a>
13
16
  {{- else -}}
@@ -70,9 +70,6 @@
70
70
  {{ .Content }}
71
71
  </div>
72
72
 
73
- {{ partial "holdings.html" . }}
74
- {{ partial "literature-works.html" . }}
75
- {{ partial "place-map.html" . }}
76
73
  {{ partial "related.html" . }}
77
74
 
78
75
  {{- /*
@@ -2862,20 +2862,14 @@ summary.info-sidebar-title::after {
2862
2862
  }
2863
2863
  }
2864
2864
 
2865
- /* ---- Related and Holdings cards (bottom of article) ----
2866
-
2867
- Both are bordered, tinted cards holding one or more labelled groups, each
2868
- group's entries laid out as a CSS grid that reflows its own column count
2869
- from the available width — two or three columns on a desktop, one on a
2870
- phone — with no breakpoint needed. Related groups by the connected page's
2871
- `type`; Holdings groups `contains` and `governed_places` by place `subType`
2872
- (`.holdings-kind-group`, one level deeper, for the region/settlement/site
2873
- hierarchy) and leaves `governed_by` a flat grid with the kind printed beside
2874
- each name. */
2875
-
2876
- .related,
2877
- .holdings,
2878
- .place-map {
2865
+ /* ---- Related card (bottom of article) ----
2866
+
2867
+ A bordered, tinted card holding one or more labelled groups, each group's
2868
+ entries laid out as a CSS grid that reflows its own column count from the
2869
+ available width — two or three columns on a desktop, one on a phone — with
2870
+ no breakpoint needed. Related groups by the connected page's `type`. */
2871
+
2872
+ .related {
2879
2873
  margin: 3rem 0 1rem;
2880
2874
  padding: 1.5rem 1.75rem;
2881
2875
  background: var(--bg-card);
@@ -2883,8 +2877,7 @@ summary.info-sidebar-title::after {
2883
2877
  border-radius: 0.5rem;
2884
2878
  }
2885
2879
 
2886
- .related-heading,
2887
- .place-map-heading {
2880
+ .related-heading {
2888
2881
  font-family: var(--font-display);
2889
2882
  font-size: 1.4rem;
2890
2883
  font-weight: 700;
@@ -2895,13 +2888,11 @@ summary.info-sidebar-title::after {
2895
2888
  border-bottom: 1px solid var(--border);
2896
2889
  }
2897
2890
 
2898
- .related-group + .related-group,
2899
- .holdings-table + .holdings-table {
2891
+ .related-group + .related-group {
2900
2892
  margin-top: 1.25rem;
2901
2893
  }
2902
2894
 
2903
- .related-group-label,
2904
- .holdings-table-label {
2895
+ .related-group-label {
2905
2896
  font-family: var(--font-display);
2906
2897
  font-size: 0.94rem;
2907
2898
  font-weight: 700;
@@ -2910,8 +2901,7 @@ summary.info-sidebar-title::after {
2910
2901
  margin: 0 0 0.5rem;
2911
2902
  }
2912
2903
 
2913
- .related-group-grid,
2914
- .holdings-table-grid {
2904
+ .related-group-grid {
2915
2905
  display: grid;
2916
2906
  grid-template-columns: repeat(auto-fill, minmax(14em, 1fr));
2917
2907
  gap: 0.4rem 1rem;
@@ -2920,49 +2910,27 @@ summary.info-sidebar-title::after {
2920
2910
  list-style: none;
2921
2911
  }
2922
2912
 
2923
- .related-group-grid li,
2924
- .holdings-table-grid li {
2913
+ .related-group-grid li {
2925
2914
  color: var(--text);
2926
2915
  font-size: 0.94rem;
2927
2916
  line-height: 1.5;
2928
2917
  overflow-wrap: break-word;
2929
2918
  }
2930
2919
 
2931
- .related-group-grid a,
2932
- .holdings-table-grid a {
2920
+ .related-group-grid a {
2933
2921
  color: var(--accent);
2934
2922
  border-bottom: 1px solid transparent;
2935
2923
  }
2936
2924
 
2937
- .related-group-grid a:hover,
2938
- .holdings-table-grid a:hover {
2925
+ .related-group-grid a:hover {
2939
2926
  color: var(--accent-hover);
2940
2927
  border-bottom-color: var(--accent-dim);
2941
2928
  }
2942
2929
 
2943
- .holdings-kind-group + .holdings-kind-group {
2944
- margin-top: 0.75rem;
2945
- }
2946
-
2947
- .holdings-kind-label {
2948
- font-weight: 700;
2949
- font-size: 0.76rem;
2950
- letter-spacing: 0.06em;
2951
- text-transform: uppercase;
2952
- color: var(--text-muted);
2953
- margin: 0 0 0.35rem;
2954
- }
2955
-
2956
- .holdings-entry-kind {
2957
- color: var(--text-muted);
2958
- }
2959
-
2960
- .place-map-drawing {
2961
- max-width: 100%;
2962
- overflow-x: auto;
2963
- }
2964
-
2965
- .place-map-drawing svg {
2930
+ /* A drawing the build sets inline — the map in a place's From here section —
2931
+ scales to its figure, and scrolls rather than shrinking past legibility on
2932
+ a phone. */
2933
+ .single-content figure.note-image > svg {
2966
2934
  display: block;
2967
2935
  width: 100%;
2968
2936
  max-width: 100%;
@@ -2970,13 +2938,15 @@ summary.info-sidebar-title::after {
2970
2938
  }
2971
2939
 
2972
2940
  @media (max-width: 640px) {
2973
- .place-map-drawing svg {
2941
+ .single-content figure.note-image:has(> svg) {
2942
+ overflow-x: auto;
2943
+ }
2944
+
2945
+ .single-content figure.note-image > svg {
2974
2946
  min-width: 32rem;
2975
2947
  }
2976
2948
 
2977
- .related,
2978
- .holdings,
2979
- .place-map {
2949
+ .related {
2980
2950
  padding: 1.25rem 1.25rem;
2981
2951
  }
2982
2952
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.0.0",
3
+ "version": "23.1.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -133,7 +133,6 @@
133
133
  "dependencies": {
134
134
  "@duckdb/node-api": "^1.5.5-r.4",
135
135
  "@foundryvtt/foundryvtt-cli": "^3.0.4",
136
- "@resvg/resvg-js": "^2.6.2",
137
136
  "@viz-js/viz": "^3.30.0",
138
137
  "acorn": "^8.18.0",
139
138
  "archiver": "^8.0.0",
@@ -1,13 +1,13 @@
1
- {"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
2
- {"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
3
- {"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
4
- {"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
5
- {"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
6
- {"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
7
- {"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
8
- {"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
9
- {"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
10
- {"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
11
- {"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
12
- {"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
13
- {"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
1
+ {"address":{"canonical":{"address":"packagebuild-none-image-afflictionbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
2
+ {"address":{"canonical":{"address":"packagebuild-none-image-armorclothingbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
3
+ {"address":{"canonical":{"address":"packagebuild-none-image-attributebnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
4
+ {"address":{"canonical":{"address":"packagebuild-none-image-containerbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
5
+ {"address":{"canonical":{"address":"packagebuild-none-image-devdocsbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
6
+ {"address":{"canonical":{"address":"packagebuild-none-image-miscgearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
7
+ {"address":{"canonical":{"address":"packagebuild-none-image-mysticalabilitybnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
8
+ {"address":{"canonical":{"address":"packagebuild-none-image-projectilebnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
9
+ {"address":{"canonical":{"address":"packagebuild-none-image-rulesbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
10
+ {"address":{"canonical":{"address":"packagebuild-none-image-skillbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
11
+ {"address":{"canonical":{"address":"packagebuild-none-image-traumabnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
12
+ {"address":{"canonical":{"address":"packagebuild-none-image-userguidebnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
13
+ {"address":{"canonical":{"address":"packagebuild-none-image-weapongearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
package/release.mjs CHANGED
@@ -45,6 +45,60 @@ import { ZipArchive } from "archiver";
45
45
 
46
46
  import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
47
47
 
48
+ /** The earliest instant a zip entry's date field can hold. */
49
+ const ZIP_EPOCH_MS = Date.UTC(1980, 0, 1);
50
+
51
+ /**
52
+ * The timestamp stamped on every archive entry: `SOURCE_DATE_EPOCH` (seconds)
53
+ * when set to a number, otherwise 1980-01-01T00:00:00Z. A value earlier than
54
+ * 1980 is raised to it, which is as early as a zip can record.
55
+ *
56
+ * @returns {Date}
57
+ */
58
+ function archiveDate() {
59
+ const seconds = Number(process.env.SOURCE_DATE_EPOCH);
60
+ const ms =
61
+ process.env.SOURCE_DATE_EPOCH && Number.isFinite(seconds) ? seconds * 1000 : ZIP_EPOCH_MS;
62
+ return new Date(Math.max(ms, ZIP_EPOCH_MS));
63
+ }
64
+
65
+ /**
66
+ * Append a directory tree to an archive in path order, with a fixed date and
67
+ * fixed permissions (0755 directories, 0644 files) so the bytes depend only on
68
+ * the tree's names and contents.
69
+ *
70
+ * Entries are appended one at a time from buffers: archiver's own directory
71
+ * walk stats files concurrently, so its entry order and recorded mtimes follow
72
+ * the filesystem.
73
+ *
74
+ * @param {import("archiver").ZipArchive} archive
75
+ * @param {string} root - The directory whose contents become the archive.
76
+ * @param {Date} date - The date recorded on every entry.
77
+ * @returns {Promise<void>}
78
+ */
79
+ async function appendTree(archive, root, date) {
80
+ /** @param {string} rel - Path relative to `root`, `/`-separated, or "". */
81
+ async function walk(rel) {
82
+ const entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true });
83
+ entries.sort((a, b) =>
84
+ a.name < b.name ? -1
85
+ : a.name > b.name ? 1
86
+ : 0,
87
+ );
88
+ for (const entry of entries) {
89
+ const name = rel ? `${rel}/${entry.name}` : entry.name;
90
+ if (entry.isDirectory()) {
91
+ archive.append(Buffer.alloc(0), { name: `${name}/`, date, mode: 0o755 });
92
+ await walk(name);
93
+ } else {
94
+ const body = await fsp.readFile(path.join(root, name));
95
+ archive.append(body, { name, date, mode: 0o644 });
96
+ }
97
+ }
98
+ }
99
+ await walk("");
100
+ }
101
+
48
102
  /**
49
103
  * Zip the staged tree and place the manifest beside the archive.
50
104
  *
@@ -55,6 +109,10 @@ import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
55
109
  * failure is timing-dependent, so it survives every run that happens to be
56
110
  * fast enough, which is what makes it worth being explicit about.
57
111
  *
112
+ * **The archive is reproducible.** Entries are written in path order with one
113
+ * date (`SOURCE_DATE_EPOCH` when set, otherwise 1980-01-01) and fixed
114
+ * permissions, so a rebuild of one staged tree is byte-identical.
115
+ *
58
116
  * @param {object} [opts]
59
117
  * @param {string} [opts.stageDir] - The staged package tree.
60
118
  * @param {string} [opts.outDir] - Where the release assets are written.
@@ -114,10 +172,10 @@ export async function packRelease({
114
172
  });
115
173
 
116
174
  archive.pipe(output);
117
- // `false` — no top-level directory inside the zip. Foundry unpacks the
118
- // archive *into* the package directory, so an extra level would nest the
119
- // manifest one deeper than it looks for it.
120
- archive.directory(stage, false);
175
+ // No top-level directory inside the zip: Foundry unpacks the archive
176
+ // *into* the package directory, so an extra level would nest the manifest
177
+ // one deeper than it looks for it.
178
+ await appendTree(archive, stage, archiveDate());
121
179
  await archive.finalize();
122
180
  await closed;
123
181
 
@@ -20,7 +20,7 @@
20
20
  * builder cannot disagree — they are one object. The linter simply reads it.
21
21
  *
22
22
  * **The other types are declared, because their compilers are hand-written.**
23
- * A being, a macro, a journal note and the three map types are built by code
23
+ * A being, a macro, a journal note and a map are built by code
24
24
  * that reads frontmatter directly rather than from a field list, so their
25
25
  * vocabulary has to be *stated* to be checkable. These entries carry no `to`:
26
26
  * nothing here builds anything, and claiming an emitted path they do not
@@ -37,7 +37,7 @@
37
37
  * @module
38
38
  */
39
39
 
40
- import { AS_AUTHORED, NUMBER, STRING } from "../engine/field-spec.mjs";
40
+ import { AS_AUTHORED, STRING } from "../engine/field-spec.mjs";
41
41
  import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
42
42
  import { ITEM_FIELDS } from "./item-fields.mjs";
43
43
 
@@ -128,81 +128,16 @@ const BEING_FIELDS = Object.freeze([
128
128
  ]);
129
129
 
130
130
  /**
131
- * A map note — one type whose `battlemap` / `localmap` / `regionalmap`
132
- * subType decides the derived canvas. Compiled into a
133
- * Foundry Scene.
131
+ * A `map` note — an exported Foundry Scene under `data.scene`.
134
132
  *
135
- * The three differ only in derived canvas defaults, which is the map compiler's
136
- * business; their authored vocabulary is the same, so they share one
137
- * declaration rather than three copies that could drift.
138
- *
139
- * `data.bgImage` is the one required piece of art — the compiler refuses a map
140
- * note without it, since a scene with no background is not a map. It is
141
- * declared in the note vocabulary rather than here, because it is an art slot
142
- * like every other and a Scene has no `img` for the shared key to reach.
133
+ * Empty on purpose: everything a map note says is a `data:` field, and the
134
+ * Scene's walls, lights, sounds, regions and pins are the exported Scene's own.
135
+ * Declaring the type with no fields is what makes every `sohl:` key on a map
136
+ * note a finding rather than a note of an unknown type.
143
137
  *
144
138
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
145
139
  */
146
- const MAP_FIELDS = Object.freeze([
147
- {
148
- name: "dimensions",
149
- ...LIST,
150
- required: true,
151
- describe: "`[width, height]` in whole pixels — the map art's own size.",
152
- },
153
- {
154
- name: "pxPerGrid",
155
- ...NUMBER,
156
- required: true,
157
- describe:
158
- "Whole pixels per grid square. Must match the art, so the compiler refuses a map note without it.",
159
- },
160
- {
161
- name: "navName",
162
- ...STRING,
163
- describe: "The scene's short name in Foundry's navigation bar.",
164
- },
165
- {
166
- name: "levelName",
167
- ...STRING,
168
- describe: "Name of the ground level.",
169
- },
170
- {
171
- name: "backgroundColor",
172
- ...STRING,
173
- describe: "Colour shown beyond the background image.",
174
- },
175
- {
176
- name: "overlay",
177
- ...STRING,
178
- describe: "Foreground image drawn over the scene.",
179
- },
180
- { name: "walls", ...MAP, describe: "Wall segments, by key." },
181
- { name: "doors", ...MAP, describe: "Doors, by key." },
182
- { name: "lights", ...MAP, describe: "Light sources, by key." },
183
- { name: "tiles", ...MAP, describe: "Tiles, by key." },
184
- { name: "sounds", ...MAP, describe: "Ambient sounds, by key." },
185
- {
186
- name: "locations",
187
- ...MAP,
188
- describe: "Map notes pinned to the scene, by key.",
189
- },
190
- {
191
- name: "regions",
192
- ...MAP,
193
- describe: "Regions and their behaviours, by key.",
194
- },
195
- {
196
- name: "place",
197
- ...AS_AUTHORED,
198
- describe: "The place this map depicts.",
199
- },
200
- {
201
- name: "placeName",
202
- ...STRING,
203
- describe: "Display name of the place this map depicts.",
204
- },
205
- ]);
140
+ const MAP_FIELDS = Object.freeze([]);
206
141
 
207
142
  /**
208
143
  * Authored, but not compiled — vocabulary the *presentation* surfaces read.
@@ -1,3 +1,42 @@
1
+ /**
2
+ * Whether a parsed JSON value is an Address in its published form.
3
+ *
4
+ * @param {unknown} value - A value read from a published index.
5
+ * @returns {boolean} Whether it is a map of exactly `address`, `anchor` and
6
+ * `anchorKind`, with `address` a string.
7
+ */
8
+ export function isPublishedAddress(value: unknown): boolean;
9
+ /**
10
+ * Write typed properties in the published index's form.
11
+ *
12
+ * Every Address becomes `{ address, anchor, anchorKind }`: `address` the full
13
+ * canonical Address, `anchor` the anchor's slug or `null`, `anchorKind` its
14
+ * kind or `null`. An Address-keyed map keeps string keys — JSON has no other —
15
+ * and a key takes no anchor, so nothing is lost.
16
+ *
17
+ * @param {any} value - A tuple-bearing tree.
18
+ * @param {WeakMap<object, object>} [seen] - Shared containers in this output.
19
+ * @returns {any} A serializable tree.
20
+ */
21
+ export function publishAddresses(value: any, seen?: WeakMap<object, object>): any;
22
+ /**
23
+ * Read a published index record back into typed properties: every published
24
+ * Address becomes a tuple, or an {@link AddressLink} where it carries an anchor.
25
+ *
26
+ * @param {any} value - A parsed record.
27
+ * @returns {any} The same tree, typed.
28
+ * @throws {Error} When a published Address is not a complete canonical one.
29
+ */
30
+ export function readPublishedAddresses(value: any): any;
31
+ /**
32
+ * Write every published Address in a parsed record as a plain string — the
33
+ * canonical Address, with `#<anchor>` where it carries one. What the SQL tables
34
+ * present, so a fence compares an Address as the text an author writes.
35
+ *
36
+ * @param {any} value - A parsed record.
37
+ * @returns {any} The same tree with each Address a string.
38
+ */
39
+ export function flattenPublishedAddresses(value: any): any;
1
40
  /**
2
41
  * Render typed properties at a generated-output boundary.
3
42
  * @param {any} value - A tuple-bearing tree.
@@ -25,10 +64,18 @@ export class AddressEntries {
25
64
  sourceKey: string;
26
65
  }[];
27
66
  }
28
- /** An Address and the section within its document. */
67
+ /** An Address and an anchor within its note, with the anchor's kind where it is known. */
29
68
  export class AddressLink {
30
- /** @param {object} target - Complete tuple. @param {string} anchor - Section slug. */
31
- constructor(target: object, anchor: string);
69
+ /**
70
+ * @param {object} target - Complete tuple.
71
+ * @param {string} anchor - The anchor's slug.
72
+ * @param {"prose"|"event"|null} [kind] - The anchor's kind, or `null` where the
73
+ * position that holds the link does not fix one.
74
+ */
75
+ constructor(target: object, anchor: string, kind?: "prose" | "event" | null);
32
76
  target: object;
33
77
  anchor: string;
78
+ kind: "event" | "prose" | null;
34
79
  }
80
+ /** The keys of an Address as the published index writes it, and no others. */
81
+ export const PUBLISHED_ADDRESS_KEYS: readonly string[];