@heroiclands/package-build 25.6.2 → 25.7.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 (55) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +19 -1
  3. package/bin/content-commands.mjs +64 -79
  4. package/bin/package-build.mjs +25 -10
  5. package/ci/wip-workflows.test.mjs +252 -0
  6. package/content-config.mjs +118 -8
  7. package/docs/api.md +237 -47
  8. package/docs/commands.md +99 -35
  9. package/docs/configuration.md +93 -44
  10. package/docs/guides/release-and-deploy.md +26 -14
  11. package/docs/guides/site.md +53 -0
  12. package/engine/cdn-publish.mjs +164 -6
  13. package/engine/content-index.mjs +71 -19
  14. package/engine/content-spans.mjs +18 -1
  15. package/engine/content-styles.mjs +85 -0
  16. package/engine/derived-sections.mjs +4 -3
  17. package/engine/event-views.mjs +6 -4
  18. package/engine/helpers.mjs +33 -9
  19. package/engine/index.mjs +1 -0
  20. package/engine/note-cache.mjs +1107 -0
  21. package/engine/note-page.mjs +424 -0
  22. package/engine/pack-config.mjs +47 -4
  23. package/engine/pathnames.mjs +22 -7
  24. package/engine/prepared-only.mjs +32 -0
  25. package/engine/preview-index.mjs +55 -13
  26. package/engine/site-build.mjs +34 -448
  27. package/engine/site-config.mjs +33 -4
  28. package/engine/site-maps.mjs +16 -24
  29. package/engine/site-origins.mjs +23 -0
  30. package/engine/site-page.mjs +648 -0
  31. package/engine/site-render.mjs +142 -10
  32. package/engine/site-serve.mjs +1 -1
  33. package/engine/sql-tables.mjs +12 -3
  34. package/package.json +6 -1
  35. package/site-theme/static/css/style.css +14 -0
  36. package/types/content-config.d.mts +34 -0
  37. package/types/engine/cdn-publish.d.mts +22 -0
  38. package/types/engine/content-index.d.mts +36 -2
  39. package/types/engine/content-styles.d.mts +20 -0
  40. package/types/engine/derived-sections.d.mts +3 -3
  41. package/types/engine/event-views.d.mts +4 -3
  42. package/types/engine/helpers.d.mts +18 -1
  43. package/types/engine/index.d.mts +1 -0
  44. package/types/engine/note-cache.d.mts +149 -0
  45. package/types/engine/note-page.d.mts +92 -0
  46. package/types/engine/pack-config.d.mts +30 -0
  47. package/types/engine/prepared-only.d.mts +14 -0
  48. package/types/engine/preview-index.d.mts +35 -0
  49. package/types/engine/site-build.d.mts +5 -100
  50. package/types/engine/site-config.d.mts +3 -2
  51. package/types/engine/site-maps.d.mts +7 -11
  52. package/types/engine/site-origins.d.mts +9 -0
  53. package/types/engine/site-page.d.mts +261 -0
  54. package/types/engine/site-render.d.mts +20 -1
  55. package/types/engine/sql-tables.d.mts +8 -4
@@ -60,6 +60,9 @@ export const BRAND = Object.freeze({
60
60
  /** Where the navigation is published. */
61
61
  export const NAVIGATION_URL = "https://www.heroiclands.org/nav.json";
62
62
 
63
+ // Shared with the pathname leaf without importing configuration back into it.
64
+ export { CANONICAL_CDN_ORIGIN } from "./site-origins.mjs";
65
+
63
66
  /** The cached navigation's file name. */
64
67
  export const NAVIGATION_FILE = "nav.json";
65
68
 
@@ -101,7 +104,7 @@ function requireNonEmptyString(value, where) {
101
104
  /**
102
105
  * Check a navigation's shape, and return it.
103
106
  *
104
- * `[{name, url, children?: [{name, url}]}]`, every `url` absolute. Checked
107
+ * `[{name, url, children?: [{name, url}]}]`, every `url` a non-empty string. Checked
105
108
  * on fetch and again on read: a file that is not a navigation would otherwise
106
109
  * reach the menu as `undefined` labels and links.
107
110
  *
@@ -148,7 +151,22 @@ export function navigationCacheDir(config) {
148
151
  }
149
152
 
150
153
  /**
151
- * The cached navigation.
154
+ * Project a canonical owned chrome link onto the selected site origin.
155
+ * Attribution and ordinary content links do not pass through this function.
156
+ *
157
+ * @param {string} value - A chrome URL.
158
+ * @param {object} config - The effective configuration.
159
+ * @returns {string} The selected URL, or the unchanged external URL.
160
+ */
161
+ function chromeURL(value, config) {
162
+ if (!URL.canParse(value)) return value;
163
+ const url = new URL(value);
164
+ if (url.origin !== new URL(NAVIGATION_URL).origin) return value;
165
+ return new URL(`${url.pathname}${url.search}${url.hash}`, new URL(config.homepage).origin).href;
166
+ }
167
+
168
+ /**
169
+ * The cached navigation, with canonical owned links on the effective homepage origin.
152
170
  *
153
171
  * **Reads the cache only.** A cold cache is an error naming the command that
154
172
  * fills it, rather than a download nobody asked for: a site build that reaches
@@ -168,7 +186,18 @@ export function readCachedNavigation(config) {
168
186
  "the site navigation has not been fetched. Run `package-build deps fetch` first.",
169
187
  );
170
188
  }
171
- return checkNavigation(JSON.parse(fs.readFileSync(file, "utf8")), file);
189
+ return checkNavigation(JSON.parse(fs.readFileSync(file, "utf8")), file).map((entry) => ({
190
+ ...entry,
191
+ url: chromeURL(entry.url, config),
192
+ ...(entry.children ?
193
+ {
194
+ children: entry.children.map((child) => ({
195
+ ...child,
196
+ url: chromeURL(child.url, config),
197
+ })),
198
+ }
199
+ : {}),
200
+ }));
172
201
  }
173
202
 
174
203
  /**
@@ -252,7 +281,7 @@ export function siteSettings({ config }) {
252
281
  const params = { description: config.site.description };
253
282
  if (config.author?.name) params.author = config.author.name;
254
283
  if (config.site.assets) params.cdnBaseURL = config.site.assets;
255
- params.brand = { ...BRAND };
284
+ params.brand = { ...BRAND, licenseURL: chromeURL(BRAND.licenseURL, config) };
256
285
  if (config.site.notfound) params.notfound = structuredClone(config.site.notfound);
257
286
  // Named only when the build copies the fonts, so the theme links nothing
258
287
  // it does not ship.
@@ -95,20 +95,33 @@ export function inlineSvg(svg) {
95
95
  * @param {object} opts.config - The resolved configuration.
96
96
  * @param {string} opts.base - The site base every page is served under,
97
97
  * ending in a slash: what every name in a drawing links through.
98
+ * @param {Iterable<string>} [opts.only] - The places to draw, by shortcode,
99
+ * of those given a map; absent, every one.
100
+ * @param {string} [opts.outDir] - Where to draw, emptied first;
101
+ * {@link SITE_MAP_DIR} under the package root by default.
98
102
  * @returns {{maps: Map<string, SiteMap>, findings: Array<{file: string,
99
103
  * line?: number, column?: number, severity: "error"|"warning",
100
104
  * message: string}>}} The drawings, keyed by the file name a **From here**
101
105
  * section names, and warnings for relations that name no place.
102
106
  */
103
- export function drawSiteMaps({ records, foreignIndex, config, base }) {
107
+ export function drawSiteMaps({
108
+ records,
109
+ foreignIndex,
110
+ config,
111
+ base,
112
+ only,
113
+ outDir = path.join(config.rootDir, ...SITE_MAP_DIR.split("/")),
114
+ }) {
104
115
  /** @type {Map<string, SiteMap>} */
105
116
  const maps = new Map();
106
- const outDir = path.join(config.rootDir, ...SITE_MAP_DIR.split("/"));
117
+ const wanted = only ? new Set(only) : undefined;
107
118
  // Every run draws afresh: a place that lost its relations must not keep
108
119
  // the map an earlier run drew.
109
120
  fs.rmSync(outDir, { recursive: true, force: true });
110
121
 
111
- const centres = placesWithMaps(records, { foreignIndex, config });
122
+ const centres = placesWithMaps(records, { foreignIndex, config }).filter(
123
+ (centre) => !wanted || wanted.has(centre),
124
+ );
112
125
  if (centres.length === 0) return { maps, findings: [] };
113
126
  const world = mapWorld({
114
127
  records,
@@ -126,24 +139,3 @@ export function drawSiteMaps({ records, foreignIndex, config, base }) {
126
139
  }
127
140
  return { maps, findings };
128
141
  }
129
-
130
- /**
131
- * The figure the website sets a drawing in: the SVG inline, so its place names
132
- * stay links, with no blank line inside it — a blank line would end the HTML
133
- * block and hand the rest of the drawing to the Markdown renderer.
134
- *
135
- * @param {string} svg - The drawing, as {@link inlineSvg} wrote it.
136
- * @param {string} alt - What the drawing shows, for a screen reader.
137
- * @returns {string} One HTML block.
138
- */
139
- export function inlineDrawingFigure(svg, alt) {
140
- const label = String(alt ?? "").replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
141
- const body = String(svg)
142
- .split("\n")
143
- .filter((line) => line.trim() !== "")
144
- .join("\n");
145
- return (
146
- `<figure class="note-image note-image-full-width" role="img" ` +
147
- `aria-label="${label}">\n${body}\n</figure>`
148
- );
149
- }
@@ -0,0 +1,23 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The owned CDN origin, re-exported by site-config beside the chrome addresses.
16
+ * Kept in a leaf because site-config reaches pathnames through configuration and
17
+ * content rendering; importing site-config from pathnames would close that cycle.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ /** The canonical public origin whose asset URLs follow the selected build host. */
23
+ export const CANONICAL_CDN_ORIGIN = "https://cdn.heroiclands.org";