@heroiclands/package-build 23.13.0 → 24.0.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 (236) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -1
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +150 -1
  5. package/bin/content-commands.mjs +356 -210
  6. package/bin/package-build.mjs +3 -2
  7. package/ci/check-banners.mjs +7 -7
  8. package/config.mjs +21 -6
  9. package/content-config.mjs +199 -80
  10. package/docs/api.md +93 -72
  11. package/docs/authoring/links-and-markup.md +316 -78
  12. package/docs/commands.md +248 -80
  13. package/docs/configuration.md +210 -102
  14. package/docs/content-format.md +2 -2
  15. package/docs/diagnostics.md +2 -2
  16. package/docs/getting-started.md +1 -1
  17. package/docs/guides/icons.md +1 -1
  18. package/docs/guides/module-project.md +5 -8
  19. package/docs/guides/release-and-deploy.md +2 -2
  20. package/docs/guides/site.md +155 -23
  21. package/docs/guides/system-project.md +1 -1
  22. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  23. package/docs/index.md +1 -0
  24. package/docs/project-setup.md +28 -30
  25. package/docs/reference/format-details.md +589 -308
  26. package/docs/reference/note-types.md +40 -10
  27. package/docs/reference/theme-pages.md +127 -108
  28. package/docs/source-preview.md +15 -1
  29. package/engine/asset-shipping.mjs +22 -10
  30. package/engine/base-compiler.mjs +62 -104
  31. package/engine/changelog-lint.mjs +1 -1
  32. package/engine/content-address.mjs +1 -1
  33. package/engine/content-alerts.mjs +8 -4
  34. package/engine/content-blocks.mjs +26 -11
  35. package/engine/content-embeds.mjs +1 -1
  36. package/engine/content-expand.mjs +376 -0
  37. package/engine/content-figures.mjs +13 -6
  38. package/engine/content-format.yaml +8 -0
  39. package/engine/content-glyphs.mjs +213 -0
  40. package/engine/content-images.mjs +6 -7
  41. package/engine/content-links.mjs +3 -3
  42. package/engine/content-lint.mjs +1 -1
  43. package/engine/content-render.mjs +453 -0
  44. package/engine/content-scripts.mjs +326 -27
  45. package/engine/content-spans.mjs +4 -1
  46. package/engine/content-styles.mjs +91 -0
  47. package/engine/content-symbols.mjs +186 -0
  48. package/engine/content-verse.mjs +6 -18
  49. package/engine/core-report.mjs +169 -0
  50. package/engine/core-vocabulary.mjs +208 -0
  51. package/engine/dictionary-html.mjs +119 -0
  52. package/engine/field-reference.mjs +1 -1
  53. package/engine/frontmatter-lint.mjs +1 -1
  54. package/engine/heading-attributes.mjs +89 -0
  55. package/engine/helpers.mjs +61 -101
  56. package/engine/homepage.mjs +17 -29
  57. package/engine/html-minify.mjs +70 -0
  58. package/engine/index.mjs +8 -5
  59. package/engine/language-rows.mjs +8 -2
  60. package/engine/language-terms.mjs +115 -14
  61. package/engine/language-words.mjs +12 -0
  62. package/engine/logographic.mjs +255 -0
  63. package/engine/map-lexicon.mjs +221 -25
  64. package/engine/map-places.mjs +5 -4
  65. package/engine/markdown-expressions.mjs +0 -2
  66. package/engine/markdownlint-run.mjs +9 -3
  67. package/engine/markup-validation.mjs +21 -0
  68. package/engine/metre-declaration.mjs +134 -0
  69. package/engine/note-matter.mjs +2 -2
  70. package/engine/note-vocabulary.mjs +187 -3
  71. package/engine/pdf-build.mjs +68 -133
  72. package/engine/pdf-render.mjs +102 -44
  73. package/engine/post-fields.mjs +157 -0
  74. package/engine/preview-index.mjs +130 -0
  75. package/engine/preview-source.mjs +450 -0
  76. package/engine/project-init.mjs +5 -12
  77. package/engine/prose-config.mjs +3 -3
  78. package/engine/run-words.mjs +3 -2
  79. package/engine/script-emphasis-terms.mjs +38 -0
  80. package/engine/script-emphasis.mjs +295 -0
  81. package/engine/script-font-delivery.mjs +4 -4
  82. package/engine/site-build.mjs +265 -313
  83. package/engine/site-config.mjs +26 -238
  84. package/engine/site-files.mjs +175 -0
  85. package/engine/site-index.mjs +9 -4
  86. package/engine/site-preview.mjs +50 -523
  87. package/engine/site-reach.mjs +213 -0
  88. package/engine/site-render.mjs +668 -0
  89. package/engine/site-root.mjs +90 -39
  90. package/engine/site-search.mjs +161 -0
  91. package/engine/site-serve.mjs +274 -0
  92. package/engine/site-theme-banner.mjs +154 -0
  93. package/engine/site-theme-urls.mjs +125 -0
  94. package/engine/site-theme.mjs +939 -0
  95. package/engine/sql-tables.mjs +9 -6
  96. package/engine/text-maps.mjs +57 -10
  97. package/engine/typographer.mjs +248 -0
  98. package/engine/verse-metre.mjs +357 -0
  99. package/engine/wikilink-syntax.mjs +29 -47
  100. package/engine/wikilinks.mjs +758 -279
  101. package/engine/word-forms.mjs +39 -24
  102. package/engine/word-rows.mjs +12 -2
  103. package/engine/word-terms.mjs +29 -4
  104. package/manifest.mjs +3 -4
  105. package/package.json +10 -7
  106. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  107. package/site-theme/layouts/base.hbs +31 -0
  108. package/site-theme/layouts/blog-list.hbs +30 -0
  109. package/site-theme/layouts/blog-post.hbs +40 -0
  110. package/site-theme/layouts/home.hbs +35 -0
  111. package/site-theme/layouts/list.hbs +20 -0
  112. package/site-theme/layouts/not-found.hbs +31 -0
  113. package/site-theme/layouts/page.hbs +7 -0
  114. package/site-theme/partials/copy-buttons.hbs +37 -0
  115. package/site-theme/partials/draft-notice.hbs +5 -0
  116. package/site-theme/partials/exturl.hbs +2 -0
  117. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  118. package/site-theme/partials/header.hbs +44 -0
  119. package/site-theme/partials/hero-banner.hbs +11 -0
  120. package/site-theme/partials/infobox-entry.hbs +2 -0
  121. package/site-theme/partials/infobox-section.hbs +31 -0
  122. package/site-theme/partials/infobox-value.hbs +2 -0
  123. package/site-theme/partials/infobox.hbs +18 -0
  124. package/site-theme/partials/list-members.hbs +9 -0
  125. package/site-theme/partials/nav-links.hbs +5 -0
  126. package/site-theme/partials/page-body.hbs +48 -0
  127. package/site-theme/partials/page-list.hbs +14 -0
  128. package/site-theme/partials/related.hbs +24 -0
  129. package/site-theme/partials/search-meta.hbs +6 -0
  130. package/site-theme/partials/search-names.hbs +2 -0
  131. package/site-theme/partials/search.hbs +52 -0
  132. package/site-theme/partials/tags.hbs +4 -0
  133. package/site-theme/partials/toc.hbs +7 -0
  134. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  135. package/stage.mjs +1 -2
  136. package/types/config.d.mts +8 -4
  137. package/types/content-config.d.mts +15 -15
  138. package/types/engine/asset-shipping.d.mts +5 -2
  139. package/types/engine/base-compiler.d.mts +20 -2
  140. package/types/engine/content-address.d.mts +1 -1
  141. package/types/engine/content-alerts.d.mts +8 -2
  142. package/types/engine/content-blocks.d.mts +11 -4
  143. package/types/engine/content-expand.d.mts +288 -0
  144. package/types/engine/content-figures.d.mts +11 -3
  145. package/types/engine/content-glyphs.d.mts +85 -0
  146. package/types/engine/content-images.d.mts +6 -7
  147. package/types/engine/content-links.d.mts +1 -1
  148. package/types/engine/content-render.d.mts +199 -0
  149. package/types/engine/content-scripts.d.mts +62 -0
  150. package/types/engine/content-spans.d.mts +1 -1
  151. package/types/engine/content-styles.d.mts +33 -0
  152. package/types/engine/content-symbols.d.mts +35 -0
  153. package/types/engine/core-report.d.mts +45 -0
  154. package/types/engine/core-vocabulary.d.mts +53 -0
  155. package/types/engine/dictionary-html.d.mts +8 -0
  156. package/types/engine/field-reference.d.mts +1 -1
  157. package/types/engine/heading-attributes.d.mts +20 -0
  158. package/types/engine/helpers.d.mts +3 -10
  159. package/types/engine/homepage.d.mts +11 -24
  160. package/types/engine/html-minify.d.mts +9 -0
  161. package/types/engine/index.d.mts +2 -1
  162. package/types/engine/language-terms.d.mts +24 -2
  163. package/types/engine/logographic.d.mts +45 -0
  164. package/types/engine/map-lexicon.d.mts +48 -8
  165. package/types/engine/map-places.d.mts +8 -0
  166. package/types/engine/markdown-expressions.d.mts +0 -2
  167. package/types/engine/metre-declaration.d.mts +39 -0
  168. package/types/engine/pdf-render.d.mts +6 -2
  169. package/types/engine/post-fields.d.mts +44 -0
  170. package/types/engine/preview-index.d.mts +72 -0
  171. package/types/engine/preview-source.d.mts +51 -0
  172. package/types/engine/script-emphasis-terms.d.mts +21 -0
  173. package/types/engine/script-emphasis.d.mts +84 -0
  174. package/types/engine/script-font-delivery.d.mts +4 -4
  175. package/types/engine/site-build.d.mts +71 -113
  176. package/types/engine/site-config.d.mts +14 -134
  177. package/types/engine/site-files.d.mts +73 -0
  178. package/types/engine/site-index.d.mts +11 -3
  179. package/types/engine/site-preview.d.mts +15 -19
  180. package/types/engine/site-reach.d.mts +76 -0
  181. package/types/engine/site-render.d.mts +173 -0
  182. package/types/engine/site-root.d.mts +14 -4
  183. package/types/engine/site-search.d.mts +54 -0
  184. package/types/engine/site-serve.d.mts +44 -0
  185. package/types/engine/site-theme-banner.d.mts +51 -0
  186. package/types/engine/site-theme-urls.d.mts +60 -0
  187. package/types/engine/site-theme.d.mts +127 -0
  188. package/types/engine/text-maps.d.mts +17 -3
  189. package/types/engine/typographer.d.mts +11 -0
  190. package/types/engine/verse-metre.d.mts +131 -0
  191. package/types/engine/wikilink-syntax.d.mts +30 -48
  192. package/types/engine/wikilinks.d.mts +330 -14
  193. package/types/engine/word-forms.d.mts +3 -2
  194. package/types/manifest.d.mts +3 -4
  195. package/types/stage.d.mts +1 -2
  196. package/types/vendor/markdownlint.d.mts +8 -0
  197. package/vendor/markdownlint.mjs +20388 -0
  198. package/engine/web-wikilinks.mjs +0 -594
  199. package/hugo-theme/layouts/404.html +0 -85
  200. package/hugo-theme/layouts/_default/baseof.html +0 -67
  201. package/hugo-theme/layouts/_default/list.html +0 -139
  202. package/hugo-theme/layouts/_default/single.html +0 -4
  203. package/hugo-theme/layouts/blog/list.html +0 -54
  204. package/hugo-theme/layouts/blog-post/single.html +0 -36
  205. package/hugo-theme/layouts/index.html +0 -75
  206. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  207. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  208. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  209. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  210. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  211. package/hugo-theme/layouts/partials/header.html +0 -35
  212. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  213. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  214. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  215. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  216. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  217. package/hugo-theme/layouts/partials/infobox.html +0 -57
  218. package/hugo-theme/layouts/partials/page-body.html +0 -152
  219. package/hugo-theme/layouts/partials/related.html +0 -138
  220. package/hugo-theme/layouts/partials/search.html +0 -84
  221. package/hugo-theme/layouts/partials/site-url.html +0 -32
  222. package/hugo-theme/layouts/partials/tags.html +0 -30
  223. package/hugo-theme/layouts/partials/toc.html +0 -28
  224. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  225. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  226. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  227. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  228. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  229. package/hugo-theme/theme.toml +0 -11
  230. package/types/engine/web-wikilinks.d.mts +0 -107
  231. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  232. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  233. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  234. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  235. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  236. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -0,0 +1,154 @@
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
+ * **A page's banner image.** {@link bannerUrl} turns a note's banner fields into
16
+ * the address of the hero image, or into `""` when the page has none.
17
+ *
18
+ * Resolution, in order:
19
+ *
20
+ * 1. `data.banner` — a note's own hero image, which the content build resolved
21
+ * from an asset address. It is used as given; a blank one declines an image.
22
+ * 2. `banner` — a hand-authored page's value. `none` or `false` declines an
23
+ * image, a full URL is used as written, and anything else is a fragment
24
+ * under `images/`.
25
+ * 3. `<dir>/<name>.webp` — the default for the page's kind: the banner
26
+ * `banner-kinds.yaml` maps its `type` and `subType` to, or its `category`
27
+ * for the types the mapping lists as taking theirs from there.
28
+ * 4. `<dir>/<fallback>` — whenever the name step 2 or 3 picked is not in the
29
+ * declared inventory of `banners.yaml`. An asset host cannot be asked
30
+ * whether a name exists, so the inventory stands in for the test, and each
31
+ * name that misses is reported once.
32
+ *
33
+ * The inventory is checked only for a relative path directly inside the banner
34
+ * directory; an absolute URL, a step 1 address and a fragment elsewhere under
35
+ * `images/` pass through.
36
+ *
37
+ * @module
38
+ */
39
+
40
+ import fs from "node:fs";
41
+ import path from "node:path";
42
+ import { fileURLToPath } from "node:url";
43
+ import YAML from "yaml";
44
+
45
+ import { cdnUrl } from "./site-theme-urls.mjs";
46
+
47
+ /** The directory holding the banner inventory and the kind mapping. */
48
+ export const BANNER_DATA_DIR = path.resolve(
49
+ path.dirname(fileURLToPath(import.meta.url)),
50
+ "..",
51
+ "site-theme",
52
+ "data",
53
+ );
54
+
55
+ /** @type {{banners: object, kinds: object}|undefined} */
56
+ let shipped;
57
+
58
+ /**
59
+ * The banner data the theme ships.
60
+ *
61
+ * @returns {{banners: object, kinds: object}} The inventory and the kind mapping.
62
+ */
63
+ export function shippedBannerData() {
64
+ shipped ??= {
65
+ banners: YAML.parse(fs.readFileSync(path.join(BANNER_DATA_DIR, "banners.yaml"), "utf8")),
66
+ kinds: YAML.parse(fs.readFileSync(path.join(BANNER_DATA_DIR, "banner-kinds.yaml"), "utf8")),
67
+ };
68
+ return shipped;
69
+ }
70
+
71
+ /** The text of a value as a template prints it, with an unset value as `<nil>`. */
72
+ const printed = (value) => (value === undefined || value === null ? "<nil>" : String(value));
73
+
74
+ /**
75
+ * The address of a page's banner.
76
+ *
77
+ * @param {object} site - The site: `params.cdnBaseURL` and `basePath`.
78
+ * @param {object} fields - What the page states.
79
+ * @param {object} [fields.data] - The page's `data`, read for the resolved `banner`.
80
+ * @param {unknown} [fields.banner] - The hand-authored `banner` value.
81
+ * @param {string} [fields.type] - The note `type`.
82
+ * @param {string} [fields.subType] - The note `subType`.
83
+ * @param {string} [fields.category] - The page's `category`.
84
+ * @param {string} [fields.subtype] - The name to default to when no `type` is given.
85
+ * @param {string} [fields.context] - The page address, named in a report.
86
+ * @param {object} [options] - Options.
87
+ * @param {{banners: object, kinds: object}} [options.data] - The inventory and the
88
+ * kind mapping. The shipped ones by default.
89
+ * @param {(finding: {severity: string, message: string, page?: string}) => void} [options.report] -
90
+ * Receives each name that misses the inventory, once.
91
+ * @param {Set<string>} [options.reported] - The names already reported.
92
+ * @returns {string} The address, or `""` when the page has no banner.
93
+ */
94
+ export function bannerUrl(site, fields, { data = shippedBannerData(), report, reported } = {}) {
95
+ const banners = data.banners ?? {};
96
+ const kinds = data.kinds ?? {};
97
+ const dir = banners.dir || "images/banners";
98
+ const fallback = banners.fallback || "default.webp";
99
+ const available = banners.available ?? [];
100
+
101
+ let subtype = fields.subtype ?? "";
102
+ if (fields.type) {
103
+ const entry = (kinds.kinds ?? {})[fields.type];
104
+ if ((kinds.categoryTypes ?? []).includes(fields.type)) {
105
+ subtype = fields.category || "default";
106
+ } else if (entry && typeof entry === "object") {
107
+ subtype = entry[String(fields.subType ?? "").toLowerCase()] || fields.type;
108
+ } else {
109
+ subtype = entry || fields.type;
110
+ }
111
+ }
112
+
113
+ const raw = printed(fields.banner);
114
+ const declined = raw.toLowerCase() === "none" || raw === "false";
115
+ const resolved = fields.data ? fields.data.banner : undefined;
116
+ const fromBuild = resolved !== undefined && resolved !== null;
117
+
118
+ let banner = "";
119
+ let absolute = false;
120
+ if (fromBuild) {
121
+ banner = String(resolved);
122
+ absolute = true;
123
+ } else if (!declined) {
124
+ if (fields.banner) {
125
+ const value = String(fields.banner);
126
+ banner = `images/${value.replace(/^\//, "")}`;
127
+ if (/^(?:https?:)?\/\//.test(value)) {
128
+ banner = value;
129
+ absolute = true;
130
+ }
131
+ } else if (subtype) {
132
+ banner = `${dir}/${subtype}.webp`;
133
+ }
134
+ if (banner && !absolute && path.posix.dirname(banner) === dir) {
135
+ const name = path.posix.basename(banner, path.posix.extname(banner));
136
+ if (!available.includes(name)) {
137
+ if (!reported?.has(name)) {
138
+ reported?.add(name);
139
+ report?.({
140
+ severity: "warning",
141
+ page: fields.context || undefined,
142
+ message:
143
+ `hero-banner: no banner named "${name}" in the shared artwork set; ` +
144
+ `falling back to ${dir}/${fallback}. Draw it and add "${name}" to ` +
145
+ "data/banners.yaml, or name a hero image with `data.banner`. " +
146
+ "Write `data.banner: none` to decline a hero image.",
147
+ });
148
+ }
149
+ banner = `${dir}/${fallback}`;
150
+ }
151
+ }
152
+ }
153
+ return banner ? cdnUrl(site, banner) : "";
154
+ }
@@ -0,0 +1,125 @@
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
+ * **Addresses in the site layouts.** The layouts carry no address of their own:
16
+ * every URL they write comes from the site's configuration or from a page's
17
+ * data, and a site served under a path prefix such as `/thalorna/` needs its own
18
+ * links to carry the prefix without writing it down anywhere.
19
+ *
20
+ * | Function | Resolves |
21
+ * | -------- | -------- |
22
+ * | {@link relUrl} | a path inside the site, under its base path |
23
+ * | {@link cdnUrl} | an asset path, against `params.cdnBaseURL` when the site has one |
24
+ * | {@link siteUrl} | a consumer-supplied link: absolute, a fragment, or a path in the site |
25
+ * | {@link safeUrl} | a link from data, refused when its scheme could run script |
26
+ *
27
+ * @module
28
+ */
29
+
30
+ import { slugify } from "./content-slug.mjs";
31
+
32
+ /** Whether an address names a host: `http://`, `https://` or `//`. */
33
+ export function isAbsoluteUrl(url) {
34
+ return /^(?:https?:)?\/\//.test(String(url ?? ""));
35
+ }
36
+
37
+ /**
38
+ * The path a site is served under, with a leading and a trailing slash.
39
+ *
40
+ * @param {object} site - The site; `baseURL` is its address.
41
+ * @returns {string} `/` for a site at its origin's root, `/thalorna/` for one under a prefix.
42
+ */
43
+ export function basePath(site) {
44
+ if (!site.baseURL) return "/";
45
+ const { pathname } = new URL(site.baseURL, "https://site.invalid");
46
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
47
+ }
48
+
49
+ /**
50
+ * A path inside the site, under its base path.
51
+ *
52
+ * A leading slash is dropped first: it would otherwise read as "from the
53
+ * origin" and leave the base path off.
54
+ *
55
+ * @param {object} site - The site.
56
+ * @param {string} [target] - The path; empty for the site's root.
57
+ * @returns {string} The address.
58
+ */
59
+ export function relUrl(site, target = "") {
60
+ return `${basePath(site)}${String(target).replace(/^\/+/, "")}`;
61
+ }
62
+
63
+ /**
64
+ * An asset path, resolved against the site's asset host.
65
+ *
66
+ * An absolute address is returned as written. Otherwise the path is joined onto
67
+ * `params.cdnBaseURL`, and with none set it is a path inside the site.
68
+ *
69
+ * @param {object} site - The site; `params.cdnBaseURL` is the asset root.
70
+ * @param {string} target - The asset path.
71
+ * @returns {string} The address.
72
+ */
73
+ export function cdnUrl(site, target) {
74
+ if (isAbsoluteUrl(target)) return target;
75
+ const rel = String(target).replace(/^\/+/, "");
76
+ const root = site.params?.cdnBaseURL;
77
+ return root ? `${String(root).replace(/\/+$/, "")}/${rel}` : relUrl(site, rel);
78
+ }
79
+
80
+ /**
81
+ * A link a consuming site supplied, resolved against the site it is rendered on.
82
+ *
83
+ * An absolute address, a fragment and a query are returned as written; anything
84
+ * else is a path inside the site.
85
+ *
86
+ * @param {object} site - The site.
87
+ * @param {string} [url] - The link; empty for the site's root.
88
+ * @returns {string} The address.
89
+ */
90
+ export function siteUrl(site, url = "") {
91
+ const value = String(url ?? "");
92
+ return isAbsoluteUrl(value) || value.startsWith("#") || value.startsWith("?") ?
93
+ value
94
+ : relUrl(site, value);
95
+ }
96
+
97
+ /** A scheme that can run script or load a document of the page's own origin. */
98
+ const UNSAFE_SCHEME = /^\s*(?:javascript|vbscript|data):/i;
99
+
100
+ /**
101
+ * A link from a page's data, with a scheme that could run script refused.
102
+ *
103
+ * @param {unknown} url - The link as the data states it.
104
+ * @returns {string} The link, or `#` when its scheme is not one a reader should follow.
105
+ */
106
+ export function safeUrl(url) {
107
+ const value = String(url ?? "");
108
+ // Control characters and spaces are dropped by a browser inside a scheme.
109
+ const bare = value.replace(/[\u0000- ]/g, "");
110
+ return UNSAFE_SCHEME.test(bare) ? "#" : value;
111
+ }
112
+
113
+ /**
114
+ * The address of a tag's listing page.
115
+ *
116
+ * One function writes it for the link in a page's tag row and for the page the
117
+ * site build writes, so the two cannot disagree.
118
+ *
119
+ * @param {object} site - The site.
120
+ * @param {string} tag - The tag.
121
+ * @returns {string} The address.
122
+ */
123
+ export function tagUrl(site, tag) {
124
+ return relUrl(site, `tags/${slugify(tag)}/`);
125
+ }