@heroiclands/package-build 23.13.0 → 24.0.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.
- package/CHANGELOG.md +36 -0
- package/README.md +2 -1
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +150 -1
- package/bin/content-commands.mjs +356 -210
- package/bin/package-build.mjs +3 -2
- package/ci/check-banners.mjs +7 -7
- package/config.mjs +21 -6
- package/content-config.mjs +199 -80
- package/docs/api.md +93 -72
- package/docs/authoring/links-and-markup.md +316 -78
- package/docs/commands.md +244 -78
- package/docs/configuration.md +210 -102
- package/docs/content-format.md +2 -2
- package/docs/diagnostics.md +2 -2
- package/docs/getting-started.md +1 -1
- package/docs/guides/icons.md +1 -1
- package/docs/guides/module-project.md +5 -8
- package/docs/guides/release-and-deploy.md +2 -2
- package/docs/guides/site.md +112 -23
- package/docs/guides/system-project.md +1 -1
- package/docs/guides/writing-in-invented-scripts.md +1247 -0
- package/docs/index.md +1 -0
- package/docs/project-setup.md +28 -30
- package/docs/reference/format-details.md +589 -308
- package/docs/reference/note-types.md +40 -10
- package/docs/reference/theme-pages.md +127 -108
- package/docs/source-preview.md +15 -1
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +26 -11
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +13 -6
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- package/engine/content-links.mjs +3 -3
- package/engine/content-lint.mjs +1 -1
- package/engine/content-render.mjs +453 -0
- package/engine/content-scripts.mjs +326 -27
- package/engine/content-spans.mjs +4 -1
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +6 -18
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/heading-attributes.mjs +89 -0
- package/engine/helpers.mjs +61 -101
- package/engine/homepage.mjs +17 -29
- package/engine/html-minify.mjs +70 -0
- package/engine/index.mjs +8 -5
- package/engine/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +115 -14
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +221 -25
- package/engine/map-places.mjs +5 -4
- package/engine/markdown-expressions.mjs +0 -2
- package/engine/markdownlint-run.mjs +9 -3
- package/engine/markup-validation.mjs +21 -0
- package/engine/metre-declaration.mjs +134 -0
- package/engine/note-matter.mjs +2 -2
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +68 -133
- package/engine/pdf-render.mjs +102 -44
- package/engine/post-fields.mjs +157 -0
- package/engine/preview-index.mjs +130 -0
- package/engine/preview-source.mjs +450 -0
- package/engine/project-init.mjs +5 -12
- package/engine/prose-config.mjs +3 -3
- package/engine/run-words.mjs +3 -2
- package/engine/script-emphasis-terms.mjs +38 -0
- package/engine/script-emphasis.mjs +295 -0
- package/engine/script-font-delivery.mjs +4 -4
- package/engine/site-build.mjs +259 -314
- package/engine/site-config.mjs +26 -238
- package/engine/site-files.mjs +175 -0
- package/engine/site-index.mjs +9 -4
- package/engine/site-preview.mjs +50 -523
- package/engine/site-render.mjs +593 -0
- package/engine/site-root.mjs +22 -13
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +273 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +926 -0
- package/engine/text-maps.mjs +57 -10
- package/engine/typographer.mjs +248 -0
- package/engine/verse-metre.mjs +357 -0
- package/engine/wikilink-syntax.mjs +29 -47
- package/engine/wikilinks.mjs +758 -279
- package/engine/word-forms.mjs +39 -24
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +29 -4
- package/manifest.mjs +3 -4
- package/package.json +10 -7
- package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
- package/site-theme/layouts/base.hbs +31 -0
- package/site-theme/layouts/blog-list.hbs +30 -0
- package/site-theme/layouts/blog-post.hbs +40 -0
- package/site-theme/layouts/home.hbs +35 -0
- package/site-theme/layouts/list.hbs +20 -0
- package/site-theme/layouts/not-found.hbs +31 -0
- package/site-theme/layouts/page.hbs +7 -0
- package/site-theme/partials/copy-buttons.hbs +37 -0
- package/site-theme/partials/draft-notice.hbs +5 -0
- package/site-theme/partials/exturl.hbs +2 -0
- package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
- package/site-theme/partials/header.hbs +44 -0
- package/site-theme/partials/hero-banner.hbs +11 -0
- package/site-theme/partials/infobox-entry.hbs +2 -0
- package/site-theme/partials/infobox-section.hbs +31 -0
- package/site-theme/partials/infobox-value.hbs +2 -0
- package/site-theme/partials/infobox.hbs +18 -0
- package/site-theme/partials/list-members.hbs +9 -0
- package/site-theme/partials/nav-links.hbs +5 -0
- package/site-theme/partials/page-body.hbs +48 -0
- package/site-theme/partials/page-list.hbs +14 -0
- package/site-theme/partials/related.hbs +24 -0
- package/site-theme/partials/search-meta.hbs +6 -0
- package/site-theme/partials/search-names.hbs +2 -0
- package/site-theme/partials/search.hbs +52 -0
- package/site-theme/partials/tags.hbs +4 -0
- package/site-theme/partials/toc.hbs +7 -0
- package/{hugo-theme → site-theme}/static/css/style.css +14 -0
- package/stage.mjs +1 -2
- package/types/config.d.mts +8 -4
- package/types/content-config.d.mts +15 -15
- package/types/engine/asset-shipping.d.mts +5 -2
- package/types/engine/base-compiler.d.mts +20 -2
- package/types/engine/content-address.d.mts +1 -1
- package/types/engine/content-alerts.d.mts +8 -2
- package/types/engine/content-blocks.d.mts +11 -4
- package/types/engine/content-expand.d.mts +288 -0
- package/types/engine/content-figures.d.mts +11 -3
- package/types/engine/content-glyphs.d.mts +85 -0
- package/types/engine/content-images.d.mts +6 -7
- package/types/engine/content-links.d.mts +1 -1
- package/types/engine/content-render.d.mts +199 -0
- package/types/engine/content-scripts.d.mts +62 -0
- package/types/engine/content-spans.d.mts +1 -1
- package/types/engine/content-styles.d.mts +33 -0
- package/types/engine/content-symbols.d.mts +35 -0
- package/types/engine/core-report.d.mts +45 -0
- package/types/engine/core-vocabulary.d.mts +53 -0
- package/types/engine/dictionary-html.d.mts +8 -0
- package/types/engine/field-reference.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +20 -0
- package/types/engine/helpers.d.mts +3 -10
- package/types/engine/homepage.d.mts +11 -24
- package/types/engine/html-minify.d.mts +9 -0
- package/types/engine/index.d.mts +2 -1
- package/types/engine/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +48 -8
- package/types/engine/map-places.d.mts +8 -0
- package/types/engine/markdown-expressions.d.mts +0 -2
- package/types/engine/metre-declaration.d.mts +39 -0
- package/types/engine/pdf-render.d.mts +6 -2
- package/types/engine/post-fields.d.mts +44 -0
- package/types/engine/preview-index.d.mts +72 -0
- package/types/engine/preview-source.d.mts +51 -0
- package/types/engine/script-emphasis-terms.d.mts +21 -0
- package/types/engine/script-emphasis.d.mts +84 -0
- package/types/engine/script-font-delivery.d.mts +4 -4
- package/types/engine/site-build.d.mts +71 -113
- package/types/engine/site-config.d.mts +14 -134
- package/types/engine/site-files.d.mts +73 -0
- package/types/engine/site-index.d.mts +11 -3
- package/types/engine/site-preview.d.mts +15 -19
- package/types/engine/site-render.d.mts +171 -0
- package/types/engine/site-root.d.mts +12 -4
- package/types/engine/site-search.d.mts +54 -0
- package/types/engine/site-serve.d.mts +44 -0
- package/types/engine/site-theme-banner.d.mts +51 -0
- package/types/engine/site-theme-urls.d.mts +60 -0
- package/types/engine/site-theme.d.mts +127 -0
- package/types/engine/text-maps.d.mts +17 -3
- package/types/engine/typographer.d.mts +11 -0
- package/types/engine/verse-metre.d.mts +131 -0
- package/types/engine/wikilink-syntax.d.mts +30 -48
- package/types/engine/wikilinks.d.mts +330 -14
- package/types/engine/word-forms.d.mts +3 -2
- package/types/manifest.d.mts +3 -4
- package/types/stage.d.mts +1 -2
- package/types/vendor/markdownlint.d.mts +8 -0
- package/vendor/markdownlint.mjs +20388 -0
- package/engine/web-wikilinks.mjs +0 -594
- package/hugo-theme/layouts/404.html +0 -85
- package/hugo-theme/layouts/_default/baseof.html +0 -67
- package/hugo-theme/layouts/_default/list.html +0 -139
- package/hugo-theme/layouts/_default/single.html +0 -4
- package/hugo-theme/layouts/blog/list.html +0 -54
- package/hugo-theme/layouts/blog-post/single.html +0 -36
- package/hugo-theme/layouts/index.html +0 -75
- package/hugo-theme/layouts/partials/banner-url.html +0 -141
- package/hugo-theme/layouts/partials/cdn-url.html +0 -36
- package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
- package/hugo-theme/layouts/partials/draft-notice.html +0 -41
- package/hugo-theme/layouts/partials/draft-state.html +0 -26
- package/hugo-theme/layouts/partials/header.html +0 -35
- package/hugo-theme/layouts/partials/hero-banner.html +0 -85
- package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
- package/hugo-theme/layouts/partials/infobox/items.html +0 -26
- package/hugo-theme/layouts/partials/infobox/section.html +0 -56
- package/hugo-theme/layouts/partials/infobox/value.html +0 -43
- package/hugo-theme/layouts/partials/infobox.html +0 -57
- package/hugo-theme/layouts/partials/page-body.html +0 -152
- package/hugo-theme/layouts/partials/related.html +0 -138
- package/hugo-theme/layouts/partials/search.html +0 -84
- package/hugo-theme/layouts/partials/site-url.html +0 -32
- package/hugo-theme/layouts/partials/tags.html +0 -30
- package/hugo-theme/layouts/partials/toc.html +0 -28
- package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
- package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
- package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
- package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
- package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
- package/hugo-theme/theme.toml +0 -11
- package/types/engine/web-wikilinks.d.mts +0 -107
- /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
- /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
- /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
- /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
- /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
- /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
package/engine/web-wikilinks.mjs
DELETED
|
@@ -1,594 +0,0 @@
|
|
|
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
|
-
* Wikilink resolution for the knowledgebase build.
|
|
16
|
-
*
|
|
17
|
-
* The same authored links the pack compilers turn into Foundry `@UUID` enrichers
|
|
18
|
-
* (see `./wikilinks.mjs`) become site-local hrefs here:
|
|
19
|
-
*
|
|
20
|
-
* `[[type-shortcode|Text]]` → `[Text](/section/slug/)`
|
|
21
|
-
* `[[type-shortcode|]]` → the same, showing the target's own name
|
|
22
|
-
* `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
|
|
23
|
-
* `[[#slug|Text]]` → `[Text](#slug)`
|
|
24
|
-
*
|
|
25
|
-
* **Every link is an address and carries a label**. One written without
|
|
26
|
-
* a label addresses nothing and is reported — see
|
|
27
|
-
* {@link unlabelledLinkMessage}, which states the rule for this build and the
|
|
28
|
-
* pack build together.
|
|
29
|
-
*
|
|
30
|
-
* The KB *section* is not always the type: prose pages (`type: doc`) route by
|
|
31
|
-
* their `category`, so `doc/quickstart` lands on `/userguide/sohl-quickstart/`.
|
|
32
|
-
* The caller supplies that mapping already resolved, in the index it builds.
|
|
33
|
-
*
|
|
34
|
-
* Lives here rather than in a consumer so every package resolves a link the
|
|
35
|
-
* same way. `sohl-thalorna` carried a forked copy of this as
|
|
36
|
-
* `utils/site-wikilinks.mjs`, already 3 KB adrift from the original, which is
|
|
37
|
-
* exactly the drift one rule with two implementations produces.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
// How an address *parses* is the pack build's own rule, so the two builds
|
|
41
|
-
// cannot drift apart on it: they disagreed once over the unlabelled hyphen
|
|
42
|
-
// form, which the packs showed as a raw shortcode and the knowledgebase as a
|
|
43
|
-
// name.
|
|
44
|
-
import { collectAnchors } from "./anchors.mjs";
|
|
45
|
-
import { presentWikilink, readWikilink } from "./wikilink-syntax.mjs";
|
|
46
|
-
import { readQualifier } from "./wikilinks.mjs";
|
|
47
|
-
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
48
|
-
// The canonical `package-system-type-shortcode` key, so a package-qualified
|
|
49
|
-
// address is matched the way a vendored manifest publishes it — by the
|
|
50
|
-
// segments the target supplies, with the system wildcarded unless stated.
|
|
51
|
-
import { canonicalKey, expandAddress, readCanonicalKey } from "./content-address.mjs";
|
|
52
|
-
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
53
|
-
// The one rule about a link's shape both builds share: it carries a label, and
|
|
54
|
-
// {@link unlabelledLinkMessage} is the one place that says so.
|
|
55
|
-
import { unlabelledLinkMessage } from "./wikilink-syntax.mjs";
|
|
56
|
-
// One slug rule for the whole build — see `./content-slug.mjs`. This module
|
|
57
|
-
// carried a copy that dropped non-ASCII letters rather than transliterating
|
|
58
|
-
// them, so a link to a heading named `Kûrbúl Helm` pointed at `#k-rb-l-helm`.
|
|
59
|
-
import { slugify } from "./content-slug.mjs";
|
|
60
|
-
|
|
61
|
-
// Re-exported so a site build keeps one import path for the whole of link
|
|
62
|
-
// resolution: the same rule that names a page also names an anchor within it.
|
|
63
|
-
/** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
|
|
64
|
-
export { slugify };
|
|
65
|
-
import {
|
|
66
|
-
authoredLabel,
|
|
67
|
-
WIKILINK,
|
|
68
|
-
isEventAnchor,
|
|
69
|
-
isSamePage,
|
|
70
|
-
parseWikilink,
|
|
71
|
-
} from "./wikilink-syntax.mjs";
|
|
72
|
-
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Whether a page's declared anchors include the slug a link names.
|
|
76
|
-
*
|
|
77
|
-
* A local page carries a `Set` built by {@link module:engine/site-index}.
|
|
78
|
-
* A foreign page declares prose anchors in its note metadata, independently
|
|
79
|
-
* of whether it has a Foundry target map. An empty collection declares none.
|
|
80
|
-
* An absent collection leaves the target unchecked.
|
|
81
|
-
*
|
|
82
|
-
* @param {Set<string>|Record<string, string>|Array<{slug: string, kind: string}>|undefined} anchors - The page's
|
|
83
|
-
* declared anchors, or `undefined` where none were recorded.
|
|
84
|
-
* @param {string} slug - The anchor a link names.
|
|
85
|
-
* @returns {boolean} Whether the anchor is declared, or `true` when `anchors`
|
|
86
|
-
* is `undefined` — which reads as "nothing to check against" rather than
|
|
87
|
-
* "declares none".
|
|
88
|
-
*/
|
|
89
|
-
function hasAnchor(anchors, slug) {
|
|
90
|
-
if (!anchors) return true;
|
|
91
|
-
if (Array.isArray(anchors))
|
|
92
|
-
return anchors.some((anchor) => anchor.kind === "prose" && anchor.slug === slug);
|
|
93
|
-
return anchors instanceof Set ? anchors.has(slug) : Object.hasOwn(anchors, slug);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The index key a **piped** target resolves to, or `null` when it does not
|
|
98
|
-
* parse as an address at all.
|
|
99
|
-
*
|
|
100
|
-
* The KB index is keyed by the canonical `type/shortcode`, so a target written
|
|
101
|
-
* in the hyphen separator — which is what the content tree authors —
|
|
102
|
-
* has to be rewritten to it before lookup. The target is read by the pack
|
|
103
|
-
* build's own {@link readQualifier}, so recognising an address and resolving
|
|
104
|
-
* one can never disagree: the two separators and the optional leading package
|
|
105
|
-
* segment are stated once, there.
|
|
106
|
-
*
|
|
107
|
-
* It takes the **parsed** qualifier rather than the raw target because the
|
|
108
|
-
* caller needs the parse for a second purpose: `unknown-type` and
|
|
109
|
-
* `not-an-address` are different findings with different fixes, and only the
|
|
110
|
-
* `reason` tells them apart. Reading the target twice would let the two
|
|
111
|
-
* readings drift.
|
|
112
|
-
*
|
|
113
|
-
* The build indexes an item note under both `skill/climb` and `docskill/climb`,
|
|
114
|
-
* and `contentTypes` carries both qualifiers, so either form finds the page.
|
|
115
|
-
*
|
|
116
|
-
* @param {Map<string, object>} index - The page index to look in.
|
|
117
|
-
* @param {object|null} read - From {@link readQualifier}.
|
|
118
|
-
* @param {string} contentPackage - The citing package, which an omitted package
|
|
119
|
-
* segment defaults from.
|
|
120
|
-
* @returns {object|undefined} The indexed page, or `undefined` when the target
|
|
121
|
-
* is not an address or nothing publishes it.
|
|
122
|
-
*/
|
|
123
|
-
function lookupRead(index, read, contentPackage) {
|
|
124
|
-
if (!read || read.reason) return undefined;
|
|
125
|
-
// Every omitted segment defaults from where the link is written —
|
|
126
|
-
// package from the citing package, system from the block, which on a page
|
|
127
|
-
// body is `none` — so the target expands to exactly one canonical address
|
|
128
|
-
// and this is a plain lookup. No filter, no single-hit rule, and no
|
|
129
|
-
// ambiguity: one key names one entry.
|
|
130
|
-
//
|
|
131
|
-
// It replaced a system-blind short key for the unqualified form, which
|
|
132
|
-
// could be silently overwritten by a second note of the same
|
|
133
|
-
// `(type, shortcode)` under another system.
|
|
134
|
-
const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
|
|
135
|
-
const found = index.get(key);
|
|
136
|
-
if (found || read.package) return found;
|
|
137
|
-
|
|
138
|
-
// A local target may also be keyed by its short form. On the KB that is not
|
|
139
|
-
// a second answer: an item note renders as **one page** which is its own
|
|
140
|
-
// documentation, so `skill/climb`, `docskill/climb` and both canonical
|
|
141
|
-
// addresses are all the same value — the fallback cannot pick
|
|
142
|
-
// differently, only earlier. It stays because an index built before the
|
|
143
|
-
// canonical documentation key was added still carries the short one, and
|
|
144
|
-
// because a page collision here is caught by the site index's own
|
|
145
|
-
// `ambiguous` set rather than by silent overwrite as in the pack index.
|
|
146
|
-
return (
|
|
147
|
-
index.get(`${read.itemDoc ? "doc" : ""}${read.type}/${read.shortcode}`.toLowerCase()) ??
|
|
148
|
-
undefined
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* How an **unresolved** link renders.
|
|
154
|
-
*
|
|
155
|
-
* The author's text remains readable. The website uses an HTML span matching
|
|
156
|
-
* the pack compiler's cue; the book uses text that Typst can print.
|
|
157
|
-
*
|
|
158
|
-
* The knowledgebase renders with `unsafe = true` (`kb/hugo.toml`), so raw HTML
|
|
159
|
-
* in generated markdown reaches the page. That makes escaping obligatory: this
|
|
160
|
-
* is the one path where *authored* text becomes markup rather than content.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} text - The text to show, from the link's label or target.
|
|
163
|
-
* @param {string} target - The address that resolved nowhere, for the tooltip.
|
|
164
|
-
* @param {"html"|"book"} output - The destination format.
|
|
165
|
-
* @returns {string} A visible cue in the destination's Markdown.
|
|
166
|
-
*/
|
|
167
|
-
function unresolvedLink(text, target, output) {
|
|
168
|
-
if (output === "book") return `${text} (unresolved link)`;
|
|
169
|
-
const esc = (v) =>
|
|
170
|
-
String(v)
|
|
171
|
-
.replace(/&/g, "&")
|
|
172
|
-
.replace(/</g, "<")
|
|
173
|
-
.replace(/>/g, ">")
|
|
174
|
-
.replace(/"/g, """);
|
|
175
|
-
return (
|
|
176
|
-
`<span class="sohl-unresolved-link" title="Unresolved link: ` +
|
|
177
|
-
`${esc(target)}">${esc(text)}</span>`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* How the website marks a link to a **draft** note.
|
|
183
|
-
*
|
|
184
|
-
* A note tagged `draft` exists so a link into it is not dead, and nothing more.
|
|
185
|
-
* Unmarked, a reader follows a promising link into an empty page and an author
|
|
186
|
-
* cannot see which of their links still owe content.
|
|
187
|
-
*
|
|
188
|
-
* The link itself remains intact inside the HTML span.
|
|
189
|
-
*
|
|
190
|
-
* The appearance lives in the Hugo theme for the website and in
|
|
191
|
-
* `scss/components/_draft-link.scss` for Foundry, not here.
|
|
192
|
-
*
|
|
193
|
-
* The website span matches the pack compiler's copy in `wikilinks.mjs`.
|
|
194
|
-
*
|
|
195
|
-
* The argument is already-built markup and is deliberately not escaped; the
|
|
196
|
-
* *authored* text inside it was escaped, or made into a link, by the caller.
|
|
197
|
-
*
|
|
198
|
-
* @param {string} inner - The resolved link, as this build emits it.
|
|
199
|
-
* @returns {string} An inline HTML span wrapping it.
|
|
200
|
-
*/
|
|
201
|
-
function draftLink(inner) {
|
|
202
|
-
return `<span class="sohl-draft-link" title="Draft — not yet written">${inner}</span>`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* A wikilink, as it is written, anywhere in a value that is not markdown.
|
|
207
|
-
*
|
|
208
|
-
* Deliberately its own pattern rather than the body resolver's: nothing here is
|
|
209
|
-
* markdown, so there is no fence or code span to step around, and a frontmatter
|
|
210
|
-
* value is a single line by construction (`[^\]\n]` keeps a runaway match from
|
|
211
|
-
* swallowing the rest of a folded scalar).
|
|
212
|
-
*/
|
|
213
|
-
// The same syntax the body scan reads — see `./wikilink-syntax.mjs`.
|
|
214
|
-
const FRONTMATTER_WIKILINK = new RegExp(WIKILINK.source, "g");
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Every wikilink authored inside a frontmatter value.
|
|
218
|
-
*
|
|
219
|
-
* Wikilinks are resolved in a note's **body** — by {@link resolveWebWikilinks}
|
|
220
|
-
* here, and by the pack compilers' `convertWikilinks` for Foundry. Frontmatter
|
|
221
|
-
* is not markdown and is never walked by either, so a link written in one is
|
|
222
|
-
* copied through verbatim and reaches the reader as literal `[[…]]` text, in
|
|
223
|
-
* whatever the theme renders that field as (an infobox row, a description, a
|
|
224
|
-
* card subtitle). Nothing downstream notices: the value is a valid string, the
|
|
225
|
-
* page builds, and the defect is visible only to someone who looks at it.
|
|
226
|
-
*
|
|
227
|
-
* So the form is refused rather than resolved. Resolving it would mean choosing
|
|
228
|
-
* an output syntax for a field whose renderer is unknown to this build — a
|
|
229
|
-
* markdown link is inert in a Hugo template that prints the value as text, and
|
|
230
|
-
* an `<a>` is unusable in one that escapes it — and would quietly bless an
|
|
231
|
-
* authoring habit that the pack build has no way to honour at all. Frontmatter
|
|
232
|
-
* carries data; a link belongs in prose.
|
|
233
|
-
*
|
|
234
|
-
* Values are read from the *parsed* frontmatter, so a `[[` inside a YAML comment
|
|
235
|
-
* is not a hit, and every hit can be named by the path a reader would look at.
|
|
236
|
-
*
|
|
237
|
-
* @param {unknown} fm - Parsed frontmatter, as `readMatter` returns it.
|
|
238
|
-
* @returns {Array<{path: string, link: string}>} In reading order; `path` is the
|
|
239
|
-
* dotted key path of the offending value (`government.summary`, `aliases.1`).
|
|
240
|
-
*/
|
|
241
|
-
export function frontmatterWikilinks(fm) {
|
|
242
|
-
const hits = [];
|
|
243
|
-
const visit = (value, trail) => {
|
|
244
|
-
if (typeof value === "string") {
|
|
245
|
-
for (const m of value.matchAll(FRONTMATTER_WIKILINK)) {
|
|
246
|
-
hits.push({ path: trail, link: m[0] });
|
|
247
|
-
}
|
|
248
|
-
} else if (Array.isArray(value)) {
|
|
249
|
-
value.forEach((v, i) => visit(v, `${trail}.${i}`));
|
|
250
|
-
} else if (isPlainMap(value)) {
|
|
251
|
-
for (const [k, v] of Object.entries(value)) {
|
|
252
|
-
visit(v, trail ? `${trail}.${k}` : k);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
if (!isPlainMap(fm)) return hits;
|
|
257
|
-
visit(fm, "");
|
|
258
|
-
return hits;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Whether a value is a YAML mapping rather than a scalar the parser built into
|
|
263
|
-
* an object of its own (a `Date`, which is what an unquoted date becomes).
|
|
264
|
-
*
|
|
265
|
-
* @param {unknown} value
|
|
266
|
-
* @returns {boolean}
|
|
267
|
-
*/
|
|
268
|
-
function isPlainMap(value) {
|
|
269
|
-
return (
|
|
270
|
-
typeof value === "object" &&
|
|
271
|
-
value !== null &&
|
|
272
|
-
!Array.isArray(value) &&
|
|
273
|
-
!(value instanceof Date)
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Rewrites the wikilinks in a markdown body as KB-local markdown links.
|
|
279
|
-
*
|
|
280
|
-
* **Every target is an address**, parsed by {@link readQualifier} and looked up
|
|
281
|
-
* case-insensitively in the KB-wide `ctx.index` (the canonical
|
|
282
|
-
* `package-system-type-shortcode`, `type/shortcode`, and the site's own
|
|
283
|
-
* `section/slug`), then in the vendored `ctx.foreign` manifests. A target is a
|
|
284
|
-
* *partial* address: an omitted package means this package, an omitted system
|
|
285
|
-
* is a wildcard, and exactly one match resolves — two are `ambiguous`. A link written
|
|
286
|
-
* without a label addresses nothing at all and is reported as such —
|
|
287
|
-
* there is no second namespace left for it to name.
|
|
288
|
-
*
|
|
289
|
-
* Only a slash-qualified target reaches the raw key, which is what keeps
|
|
290
|
-
* `section/slug` addressable without a page's own slug answering for it.
|
|
291
|
-
*
|
|
292
|
-
* **Every target that resolves nowhere fails the build**, and is
|
|
293
|
-
* classified into the vocabulary all three resolvers share — `unlabelled`,
|
|
294
|
-
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
|
|
295
|
-
* `no-content-index`. Failures are collected in `ctx.errors`, each carrying
|
|
296
|
-
* the authored `link` and its `occurrence` so a caller can report the line and
|
|
297
|
-
* column it sits on.
|
|
298
|
-
*
|
|
299
|
-
* There is deliberately no exception letting a hyphen-form address through while
|
|
300
|
-
* any linkable package had no vendored manifest, since a real cross-package
|
|
301
|
-
* reference and a typo look identical from here. The pack compilers and the
|
|
302
|
-
* link checker never made that allowance, so its only surviving effect was to
|
|
303
|
-
* give one authored link two verdicts. The advice moved into the message
|
|
304
|
-
* instead.
|
|
305
|
-
*
|
|
306
|
-
* A target that resolves nowhere still renders through {@link unresolvedLink}
|
|
307
|
-
* rather than as bare prose: the author's text is kept, marked so a
|
|
308
|
-
* reader can see a link was intended. The marking and the failure are separate
|
|
309
|
-
* jobs and always were — the mark is for whoever reads the page a *previous*
|
|
310
|
-
* build emitted, the failure is for the author of this one.
|
|
311
|
-
*
|
|
312
|
-
* A target that **resolved** to an entry with no page is not this case and is
|
|
313
|
-
* not marked: a pack-only package publishes Foundry addresses and no
|
|
314
|
-
* web pages, so the author wrote a real address and there is simply nothing to
|
|
315
|
-
* link to.
|
|
316
|
-
*
|
|
317
|
-
* ## Embeds first, and they are not links
|
|
318
|
-
*
|
|
319
|
-
* `![[address|label]]` names a file rather than a note, so it is resolved into
|
|
320
|
-
* an ordinary markdown image before anything looks for a link — which is what
|
|
321
|
-
* stops the link pass reading an embed's interior as a link to a note nobody
|
|
322
|
-
* wrote. The image the rewrite leaves behind is what
|
|
323
|
-
* {@link module:engine/content-images.renderImageFigures} turns into a figure
|
|
324
|
-
* for the website, and what the book reads its staging list out of.
|
|
325
|
-
*
|
|
326
|
-
* ## Every hit is recorded, so the page's edges come out of one pass
|
|
327
|
-
*
|
|
328
|
-
* A caller that hands in `ctx.resolved` is given every index entry a link on
|
|
329
|
-
* the page resolved to, in body order, repeats included. That is the link
|
|
330
|
-
* graph, read at the one point where it exists: the build never has to walk a
|
|
331
|
-
* page a second time to learn what it links to. Nothing is recorded for a
|
|
332
|
-
* same-page anchor or for a target that resolved nowhere.
|
|
333
|
-
*
|
|
334
|
-
* @param {string} body - The markdown body.
|
|
335
|
-
* @param {object} ctx - `{ index, assets, collide, contentTypes,
|
|
336
|
-
* packages, noIndexPackages, foreign, type, errors, src, file, resolved,
|
|
337
|
-
* output }`. `output` is `"book"` for PDF Markdown and defaults to HTML.
|
|
338
|
-
* `packages` is every package an address may name, without which the leading
|
|
339
|
-
* package segment of a canonical address reads as an unknown type;
|
|
340
|
-
* `noIndexPackages` is every package declared `contentIndex: false`, so a
|
|
341
|
-
* qualified address naming one fails with `no-content-index` rather than
|
|
342
|
-
* `not-an-address`; `foreign`
|
|
343
|
-
* is the cross-package manifest index; `assets` is the address space an embed
|
|
344
|
-
* resolves against. `src` is the page's display
|
|
345
|
-
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
346
|
-
* stands in. `resolved`, when supplied, is the array every resolved
|
|
347
|
-
* target's index entry is appended to. `anchors` is the `{#slug}` set
|
|
348
|
-
* *this* page declares, which a `[[#slug]]` self-link is checked against;
|
|
349
|
-
* absent, a self-link is not checked, exactly as before this existed.
|
|
350
|
-
* @returns {string} The body with embeds and wikilinks rewritten.
|
|
351
|
-
*/
|
|
352
|
-
export function resolveWebWikilinks(body, ctx) {
|
|
353
|
-
// How many times each authored link has been seen, so two identical links
|
|
354
|
-
// on one page are located at their own positions in the source file — the
|
|
355
|
-
// same counting the checker does, and what turns a finding into a
|
|
356
|
-
// `file:line:column:` diagnostic rather than a note-wide one.
|
|
357
|
-
const seen = new Map();
|
|
358
|
-
let ownAnchors = ctx.noteAnchors;
|
|
359
|
-
/**
|
|
360
|
-
* Records a finding against one authored literal.
|
|
361
|
-
*
|
|
362
|
-
* @param {string} all - The literal, exactly as authored.
|
|
363
|
-
* @param {object} finding - The finding's own fields.
|
|
364
|
-
* @returns {void}
|
|
365
|
-
*/
|
|
366
|
-
const record = (all, finding) => {
|
|
367
|
-
const occurrence = (seen.get(all) ?? 0) + 1;
|
|
368
|
-
seen.set(all, occurrence);
|
|
369
|
-
ctx.errors.push({
|
|
370
|
-
// Absolute where the caller supplied one, so a diagnostic locates
|
|
371
|
-
// the file an editor opens; the display path otherwise.
|
|
372
|
-
file: ctx.file ?? ctx.src,
|
|
373
|
-
src: ctx.src,
|
|
374
|
-
link: all,
|
|
375
|
-
occurrence,
|
|
376
|
-
...finding,
|
|
377
|
-
});
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Records a finding, and returns the marked-up link it renders as.
|
|
382
|
-
*
|
|
383
|
-
* @param {string} all - The authored link, brackets and all.
|
|
384
|
-
* @param {object} finding - `{ target, reason }`, plus any extras the class
|
|
385
|
-
* carries.
|
|
386
|
-
* @param {string} text - What the link renders as.
|
|
387
|
-
* @returns {string} The marked span.
|
|
388
|
-
*/
|
|
389
|
-
const report = (all, finding, text) => {
|
|
390
|
-
record(all, finding);
|
|
391
|
-
return unresolvedLink(text, finding.target, ctx.output ?? "html");
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
// An embed names a file, so it resolves before anything looks for a link.
|
|
395
|
-
// Its findings are recorded rather than rendered: the rewrite already
|
|
396
|
-
// leaves an embed it cannot resolve exactly as authored, which is the
|
|
397
|
-
// visible degradation an unresolved link gets from `unresolvedLink`.
|
|
398
|
-
const embedded = resolveEmbeds(body, { index: ctx.assets });
|
|
399
|
-
for (const u of embedded.unresolved) {
|
|
400
|
-
record(u.link, { target: u.target, reason: u.reason, ...(u.type ? { type: u.type } : {}) });
|
|
401
|
-
}
|
|
402
|
-
for (const problem of embedded.problems) {
|
|
403
|
-
record(problem.link, { target: problem.link, message: problem.message });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
|
|
407
|
-
// inline span is source text, not a link.
|
|
408
|
-
return replaceOutsideCode(embedded.markdown, WIKILINK, (all, rawInner) => {
|
|
409
|
-
const parsed = readWikilink(
|
|
410
|
-
rawInner,
|
|
411
|
-
{
|
|
412
|
-
package: ctx.contentPackage,
|
|
413
|
-
system: "note",
|
|
414
|
-
types: ctx.contentTypes ?? new Set(),
|
|
415
|
-
packages: ctx.packages,
|
|
416
|
-
noIndexPackages: ctx.noIndexPackages,
|
|
417
|
-
},
|
|
418
|
-
all,
|
|
419
|
-
);
|
|
420
|
-
const { rawTarget: target, anchor, display } = parsed;
|
|
421
|
-
// An empty label is not a label: `[[x|]]` addresses the target and
|
|
422
|
-
// shows its name, so `""` falls through to the same place `null` does.
|
|
423
|
-
// One reading, from {@link authoredLabel}.
|
|
424
|
-
const label = authoredLabel({ display });
|
|
425
|
-
if (parsed.attributes?.problems.length)
|
|
426
|
-
return report(
|
|
427
|
-
all,
|
|
428
|
-
{ target, reason: "invalid-attributes", problems: parsed.attributes.problems },
|
|
429
|
-
label ?? target,
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// **Every link carries a label**. Without one there is nothing
|
|
433
|
-
// to resolve against, and nothing to show either, a shortcode being an
|
|
434
|
-
// address rather than prose. Reported before the same-page form, because the
|
|
435
|
-
// rule is about how the link is *written*: `[[#slug]]` needs the pipe
|
|
436
|
-
// exactly as `[[skill-clmb]]` does.
|
|
437
|
-
if (!parsed.labelled) {
|
|
438
|
-
return report(all, { target: parsed.inner, reason: "unlabelled" }, parsed.inner);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// `[[#section-slug|Text]]` — a section of this same page.
|
|
442
|
-
if (isSamePage({ target, anchor })) {
|
|
443
|
-
// Checked against this page's own anchors, exactly as a link into
|
|
444
|
-
// another page is: a self-link naming a section the page does not
|
|
445
|
-
// declare is as dead as one naming a section nowhere does.
|
|
446
|
-
if (isEventAnchor(ctx, anchor)) {
|
|
447
|
-
return report(
|
|
448
|
-
all,
|
|
449
|
-
{ target, reason: "event-anchor", anchor, addressed: true },
|
|
450
|
-
label ?? anchor,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
if (!hasAnchor(ctx.anchors, anchor)) {
|
|
454
|
-
return report(
|
|
455
|
-
all,
|
|
456
|
-
{ target, reason: "unknown-anchor", anchor, addressed: true },
|
|
457
|
-
label ?? anchor,
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
return presentWikilink(
|
|
461
|
-
`[${label ?? ctx.captionLabels?.get(anchor) ?? anchor}](#${slugify(anchor)})`,
|
|
462
|
-
parsed,
|
|
463
|
-
{ noteAnchors: (ownAnchors ??= collectAnchors(body)) },
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// The canonical separator has to be resolved, not merely
|
|
468
|
-
// recognised. `null` here means the target is not an address at all,
|
|
469
|
-
// which is a defect: there is no other namespace to try.
|
|
470
|
-
const read = parsed.problem ?? parsed.target;
|
|
471
|
-
const rawKey = target.toLowerCase();
|
|
472
|
-
const hit =
|
|
473
|
-
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
474
|
-
// A manifest entry carries the same `{ url, name }` shape as a
|
|
475
|
-
// local one, so a cross-package hit needs no special case
|
|
476
|
-
// below. Local wins: a live build is authoritative and a vendored
|
|
477
|
-
// manifest can only be staler.
|
|
478
|
-
// A short form names *this* package, so it never reaches a
|
|
479
|
-
// vendored manifest; only a fully qualified address does.
|
|
480
|
-
(ctx.foreign && read?.package ?
|
|
481
|
-
lookupRead(ctx.foreign, read, ctx.contentPackage)
|
|
482
|
-
: undefined);
|
|
483
|
-
if (hit) {
|
|
484
|
-
// A `#section` the target declares no heading for. Checked only
|
|
485
|
-
// where there is a page to check against — a pack-only hit has no
|
|
486
|
-
// `url` and therefore nothing for the anchor to address, which is
|
|
487
|
-
// the asset-like case this must not touch. `anchorPageId` on the
|
|
488
|
-
// pack build and the KB manifest's own `{slug: uuid}` map both
|
|
489
|
-
// hash any slug into something that resolves, so an undeclared
|
|
490
|
-
// one otherwise joins onto the URL unchecked and dead-ends for
|
|
491
|
-
// the reader.
|
|
492
|
-
if (anchor && isEventAnchor(hit, anchor)) {
|
|
493
|
-
return report(
|
|
494
|
-
all,
|
|
495
|
-
{ target, reason: "event-anchor", anchor, addressed: true },
|
|
496
|
-
label ?? hit.name ?? target,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
if (anchor && !hasAnchor(hit.noteAnchors ?? hit.anchors, anchor)) {
|
|
500
|
-
return report(
|
|
501
|
-
all,
|
|
502
|
-
{ target, reason: "unknown-anchor", anchor, addressed: true },
|
|
503
|
-
label ?? hit.name ?? target,
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
// The edge, for a caller reading the link graph off this pass.
|
|
507
|
-
ctx.resolved?.push(hit);
|
|
508
|
-
// An address with an *empty* label has no prose to show (a
|
|
509
|
-
// shortcode is not display text), so the document's **current**
|
|
510
|
-
// name stands in and a rename shows at every citation.
|
|
511
|
-
const text = label ?? hit.name;
|
|
512
|
-
// A pack-only package publishes Foundry addresses and no pages,
|
|
513
|
-
// so its entries carry no `path` and resolve to no URL.
|
|
514
|
-
// The address is real — this is not a typo and must not fail the
|
|
515
|
-
// build — but there is nothing on the web to point at, so the
|
|
516
|
-
// reader gets the text and no href. Emitting the href anyway is
|
|
517
|
-
// what the manifest exists to prevent: `[Name](undefined)` renders
|
|
518
|
-
// as a link and goes nowhere.
|
|
519
|
-
const link =
|
|
520
|
-
hit.url ? `[${text}](${anchor ? `${hit.url}#${slugify(anchor)}` : hit.url})` : text;
|
|
521
|
-
// A link into a note that exists but is not written renders marked.
|
|
522
|
-
// Presentation only — the href above is unchanged, and a
|
|
523
|
-
// `[[#anchor]]` self-link is not marked because the reader is
|
|
524
|
-
// already on the page it would be telling them about.
|
|
525
|
-
return presentWikilink(
|
|
526
|
-
!hit.draft ? link
|
|
527
|
-
: ctx.output === "book" ? `${link} (draft)`
|
|
528
|
-
: draftLink(link),
|
|
529
|
-
parsed,
|
|
530
|
-
hit,
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// **An address resolving nowhere is a failure, unconditionally**.
|
|
535
|
-
//
|
|
536
|
-
// It was gated on a manifest-completeness check — while any linkable package was
|
|
537
|
-
// invisible here, `Rules/Bestiary.md` addressing `being-grkrahk` in the
|
|
538
|
-
// `thalorna` package was indistinguishable from a typo, so the form
|
|
539
|
-
// stayed unguarded rather than fail correct content. The gate has
|
|
540
|
-
// outlived that: every linkable package publishes a manifest now, and
|
|
541
|
-
// more to the point the *other two* resolvers never had it. The pack
|
|
542
|
-
// compilers fail the note and the link checker reports an error, so the
|
|
543
|
-
// gate's only remaining effect was to give one authored link two
|
|
544
|
-
// verdicts depending on which build read it. The advice a missing
|
|
545
|
-
// manifest calls for is in the message instead — {@link
|
|
546
|
-
// unresolvedAddressMessage} names both corrections — which informs the
|
|
547
|
-
// author without excusing the link.
|
|
548
|
-
// The bucket an ambiguity is recorded under is the *short* address —
|
|
549
|
-
// the only form two packages can collide on, since a qualified one
|
|
550
|
-
// names its package. A qualified target that matched more than one
|
|
551
|
-
// document is reported by the matcher instead, not by this set.
|
|
552
|
-
const collideKey =
|
|
553
|
-
read && !read.reason && read.package === ctx.contentPackage ?
|
|
554
|
-
`${read.type}/${read.shortcode}`.toLowerCase()
|
|
555
|
-
: rawKey;
|
|
556
|
-
// A capital in the shortcode is refused, never lowercased. The
|
|
557
|
-
// lowercase spelling is looked up only to name it in the finding.
|
|
558
|
-
if (read?.reason === "shortcode-not-lowercase") {
|
|
559
|
-
const lower = read.lowercase;
|
|
560
|
-
const declared =
|
|
561
|
-
lookupRead(ctx.index, lower, ctx.contentPackage) ??
|
|
562
|
-
(ctx.foreign && lower.package ?
|
|
563
|
-
lookupRead(ctx.foreign, lower, ctx.contentPackage)
|
|
564
|
-
: undefined);
|
|
565
|
-
return report(
|
|
566
|
-
all,
|
|
567
|
-
{
|
|
568
|
-
target,
|
|
569
|
-
reason: "shortcode-not-lowercase",
|
|
570
|
-
shortcode: read.shortcode,
|
|
571
|
-
...(declared ? { declared: lower.shortcode } : {}),
|
|
572
|
-
},
|
|
573
|
-
label ?? target,
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
const reason =
|
|
577
|
-
ctx.collide?.has(collideKey) ? "ambiguous"
|
|
578
|
-
// "It parsed as an address" is a property of the parse, not of
|
|
579
|
-
// a key: a partial address has no single key to be non-null.
|
|
580
|
-
: read && !read.reason ? "unresolved"
|
|
581
|
-
: read?.reason === "unknown-type" ? "unknown-type"
|
|
582
|
-
: read?.reason === "no-content-index" ? "no-content-index"
|
|
583
|
-
: read?.reason === "not-lowercase" ? "not-lowercase"
|
|
584
|
-
// Every link is an address, and this is not one. Distinct from
|
|
585
|
-
// a dead address, because the fix is different: a name has to
|
|
586
|
-
// become an address, not be corrected.
|
|
587
|
-
: "not-an-address";
|
|
588
|
-
|
|
589
|
-
// Whether or not it failed the build, the link renders marked: the
|
|
590
|
-
// author's text is kept so the sentence still reads, and a reader can
|
|
591
|
-
// see that something was meant to be a link.
|
|
592
|
-
return report(all, { target, reason }, label ?? target);
|
|
593
|
-
});
|
|
594
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
{{- /*
|
|
2
|
-
"Page not found" page.
|
|
3
|
-
|
|
4
|
-
Hugo renders this template to `public/404.html`, and a static host such as
|
|
5
|
-
Cloudflare Pages serves that file — with a genuine HTTP 404 — for any path
|
|
6
|
-
the deployment does not publish. Without it, Pages falls back to the site
|
|
7
|
-
root, so an unknown path answers 200 with the landing page: a soft-404 that
|
|
8
|
-
makes every "does this URL resolve?" check report success, and lets search
|
|
9
|
-
engines index withdrawn content as live.
|
|
10
|
-
|
|
11
|
-
Every consumer needs exactly this, so it lives here rather than being copied
|
|
12
|
-
into each one (issue #1454). The theme carries layout, not addresses, so the
|
|
13
|
-
wording and the routes back are supplied by the consuming site:
|
|
14
|
-
|
|
15
|
-
[params.notfound]
|
|
16
|
-
heroimage = "images/banner.webp" # CDN-relative or absolute
|
|
17
|
-
tagline = "This site has no page at"
|
|
18
|
-
sitenoun = "site" # used in the body prose
|
|
19
|
-
[[params.notfound.links]]
|
|
20
|
-
title = "Home"
|
|
21
|
-
url = "/" # relURL'd unless already absolute
|
|
22
|
-
text = "Everything, from the top."
|
|
23
|
-
|
|
24
|
-
Each is guarded: a consumer supplying none still builds, and simply gets the
|
|
25
|
-
generic wording with no list of routes back.
|
|
26
|
-
*/ -}}
|
|
27
|
-
{{ define "main" }}
|
|
28
|
-
{{- $nf := .Site.Params.notfound | default dict -}}
|
|
29
|
-
{{- $img := $nf.heroimage | default "images/banners/default.webp" -}}
|
|
30
|
-
{{- $heroURL := partial "cdn-url.html" (dict "site" .Site "path" $img) -}}
|
|
31
|
-
{{- $tagline := $nf.tagline | default "This site has no page at" -}}
|
|
32
|
-
{{- $noun := $nf.sitenoun | default "site" -}}
|
|
33
|
-
<section class="hero hero-with-image" style="background-image: url('{{ $heroURL }}');">
|
|
34
|
-
<div class="hero-inner" data-pagefind-ignore>
|
|
35
|
-
<h1>Page not found</h1>
|
|
36
|
-
<p class="hero-tagline">{{ $tagline }} <code id="missing-path">the address you followed</code>.</p>
|
|
37
|
-
</div>
|
|
38
|
-
</section>
|
|
39
|
-
|
|
40
|
-
<article class="single">
|
|
41
|
-
<div class="single-content">
|
|
42
|
-
<div class="single-body">
|
|
43
|
-
<p>
|
|
44
|
-
The page may have been moved to another part of the
|
|
45
|
-
{{ $noun }}, or withdrawn along with the concept it described. Links
|
|
46
|
-
from older discussions, issues, and search results can outlive the
|
|
47
|
-
pages they point at.
|
|
48
|
-
</p>
|
|
49
|
-
{{- with $nf.links }}
|
|
50
|
-
<p>Start again from one of these:</p>
|
|
51
|
-
<ul class="page-list">
|
|
52
|
-
{{- range . }}
|
|
53
|
-
{{- $url := .url -}}
|
|
54
|
-
{{- if not (or (hasPrefix $url "http://") (hasPrefix $url "https://") (hasPrefix $url "//")) -}}
|
|
55
|
-
{{- /* relURL only mounts a path under the site's base when it is
|
|
56
|
-
given relative: a leading slash means "from the origin", and
|
|
57
|
-
the base path is left off. Trim it, so a consumer writing the
|
|
58
|
-
obvious "/section/" gets a link inside its own site even when
|
|
59
|
-
that site is served under a prefix. */ -}}
|
|
60
|
-
{{- $url = strings.TrimPrefix "/" $url | relURL -}}
|
|
61
|
-
{{- end }}
|
|
62
|
-
<li class="page-list-item">
|
|
63
|
-
<a class="page-list-title" href="{{ $url }}">{{ .title }}</a>
|
|
64
|
-
{{- with .text }}
|
|
65
|
-
<span class="page-list-desc">{{ . }}</span>
|
|
66
|
-
{{- end }}
|
|
67
|
-
</li>
|
|
68
|
-
{{- end }}
|
|
69
|
-
</ul>
|
|
70
|
-
{{- end }}
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</article>
|
|
74
|
-
|
|
75
|
-
{{/* Name the address that failed, so a reader can see what they followed and
|
|
76
|
-
quote it in a bug report. Static hosting cannot template it — Pages serves
|
|
77
|
-
this one file for every unmatched path — so it is filled in client-side,
|
|
78
|
-
and the markup above reads sensibly without scripting. */}}
|
|
79
|
-
<script>
|
|
80
|
-
(function () {
|
|
81
|
-
var el = document.getElementById("missing-path");
|
|
82
|
-
if (el) el.textContent = window.location.pathname;
|
|
83
|
-
})();
|
|
84
|
-
</script>
|
|
85
|
-
{{ end }}
|