@heroiclands/package-build 23.1.1 → 23.2.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 +63 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +32 -0
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +21 -14
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +8 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
package/engine/standings.mjs
CHANGED
|
@@ -68,14 +68,12 @@ function mapping(value) {
|
|
|
68
68
|
/**
|
|
69
69
|
* The standings a being authored, in one shape whichever shape it wrote.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* none of them asks which it was handed:
|
|
71
|
+
* Two spellings reach here and every reader downstream takes the same entries,
|
|
72
|
+
* so none of them asks which it was handed:
|
|
74
73
|
*
|
|
75
74
|
* - an **{@link AddressEntries}** — the map after the note boundary decoded its
|
|
76
75
|
* keys into typed Addresses, which is the shape every compile sees;
|
|
77
|
-
* - a **plain map**, which is the shape a checker reading raw YAML sees
|
|
78
|
-
* - a **list**, which names bodies and says nothing about standing in them.
|
|
76
|
+
* - a **plain map**, which is the shape a checker reading raw YAML sees.
|
|
79
77
|
*
|
|
80
78
|
* An emptied map arrives from the property editor as `[]` and means what `{}`
|
|
81
79
|
* means — this being authors no membership — so both read as no entries.
|
|
@@ -85,7 +83,7 @@ function mapping(value) {
|
|
|
85
83
|
* Address is what a finding should name.
|
|
86
84
|
*
|
|
87
85
|
* @param {unknown} value - The authored `data.affiliations`.
|
|
88
|
-
* @returns {{form: "absent"|"
|
|
86
|
+
* @returns {{form: "absent"|"map"|"malformed",
|
|
89
87
|
* entries: Array<{body: unknown, sourceKey: string|number, standing: unknown}>}}
|
|
90
88
|
* The form it was written in, and its entries.
|
|
91
89
|
*/
|
|
@@ -101,18 +99,9 @@ export function readStandings(value) {
|
|
|
101
99
|
})),
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
form: "list",
|
|
109
|
-
entries: value.map((body, position) => ({
|
|
110
|
-
body,
|
|
111
|
-
sourceKey: position,
|
|
112
|
-
standing: undefined,
|
|
113
|
-
})),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
102
|
+
// An emptied map arrives from the property editor as `[]`; any other list
|
|
103
|
+
// is malformed.
|
|
104
|
+
if (Array.isArray(value) && value.length === 0) return { form: "map", entries: [] };
|
|
116
105
|
if (!mapping(value) || isAddressTuple(value)) return { form: "malformed", entries: [] };
|
|
117
106
|
return {
|
|
118
107
|
form: "map",
|
|
@@ -270,7 +259,7 @@ export function standingLead(standing, digest) {
|
|
|
270
259
|
* Five questions, and the last three are what keying the entry by its body
|
|
271
260
|
* buys:
|
|
272
261
|
*
|
|
273
|
-
* 1. the value is a map keyed by Address
|
|
262
|
+
* 1. the value is a map keyed by Address;
|
|
274
263
|
* 2. every key names an affiliation that resolves, once;
|
|
275
264
|
* 3. the entry states a standing at all — what a stated standing holds is the
|
|
276
265
|
* inner-key check's, read from {@link STANDING_FIELDS};
|
|
@@ -278,7 +267,7 @@ export function standingLead(standing, digest) {
|
|
|
278
267
|
* 5. an `office` is a key of that body's `governance.offices`.
|
|
279
268
|
*
|
|
280
269
|
* Both of the last two are read from the named body's own frontmatter as the
|
|
281
|
-
* lint runs, so a
|
|
270
|
+
* lint runs, so a rung or a post that does not exist fails on every being pointing
|
|
282
271
|
* at it rather than on a list somebody has to remember to edit.
|
|
283
272
|
*
|
|
284
273
|
* @param {object} note - The note.
|
|
@@ -301,18 +290,14 @@ export function checkStandings(note, { index } = {}) {
|
|
|
301
290
|
|
|
302
291
|
const findings = [];
|
|
303
292
|
const seen = new Map();
|
|
304
|
-
// The list form names bodies and has nowhere to put a standing. Its entries
|
|
305
|
-
// are still held to being affiliations that resolve, so a tree part-way
|
|
306
|
-
// through the sweep keeps the check it had.
|
|
307
|
-
const isKey = form === "map";
|
|
308
293
|
for (const { body, sourceKey, standing } of entries) {
|
|
309
294
|
const path = ["data", "affiliations", sourceKey];
|
|
310
295
|
const resolved = resolveBody(note, index, body);
|
|
311
|
-
findings.push(...bodyFindings(note, body, path,
|
|
296
|
+
findings.push(...bodyFindings(note, body, path, true, resolved));
|
|
312
297
|
if (resolved.address) {
|
|
313
298
|
if (seen.has(resolved.address)) {
|
|
314
299
|
findings.push({
|
|
315
|
-
...position(note, path,
|
|
300
|
+
...position(note, path, true),
|
|
316
301
|
message:
|
|
317
302
|
`\`data.affiliations\` names ${resolved.address} twice, as ` +
|
|
318
303
|
`${JSON.stringify(String(seen.get(resolved.address)))} and ` +
|
|
@@ -322,7 +307,6 @@ export function checkStandings(note, { index } = {}) {
|
|
|
322
307
|
}
|
|
323
308
|
seen.set(resolved.address, sourceKey);
|
|
324
309
|
}
|
|
325
|
-
if (!isKey) continue;
|
|
326
310
|
const named = resolved.address ?? String(body);
|
|
327
311
|
// An entry with nothing after its key, and an emptied map arriving from
|
|
328
312
|
// the property editor as `[]`, say exactly what an authored `{}` says:
|
package/engine/system-block.mjs
CHANGED
|
@@ -37,68 +37,25 @@
|
|
|
37
37
|
*
|
|
38
38
|
* This is the load-bearing part, and the reason the whole module exists.
|
|
39
39
|
* `sohl.system.portrait` and `hm3.system.bioImage` both default from one shared
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* property — and they are two *real* fields with different names. The two Actor
|
|
41
|
+
* schemas one `being` note has to feed share **no field name at all**, so a rule
|
|
42
|
+
* that matched on spelling would not be a rule with exceptions; it would be a
|
|
43
|
+
* rule that never fires.
|
|
44
44
|
*
|
|
45
45
|
* So each field declares where its shared value comes from, and the resolution
|
|
46
46
|
* order for a system `S` is:
|
|
47
47
|
*
|
|
48
48
|
* 1. `S.system.<to>` — authored directly, wins outright;
|
|
49
|
-
* 2. `S.<
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* off — the retiring *shared* position, derived rather than declared;
|
|
49
|
+
* 2. `S.<name>` — the field's own name, written in the block. A field whose
|
|
50
|
+
* `name` is a path into `data:` has no in-block position, because its home
|
|
51
|
+
* is the container;
|
|
52
|
+
* 3. the shared property the field **declares** as its source, which may be a
|
|
53
|
+
* dotted path (`data.portrait`) rather than a sibling key;
|
|
55
54
|
* 4. the field's own default.
|
|
56
55
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* shared source and the in-block key were the same word. `data:` ended
|
|
61
|
-
* that — a shared source is now a path *into* a container, so `data.species`
|
|
62
|
-
* and `species` are two spellings of two different places, and no single value
|
|
63
|
-
* of `name` reached both. `name: "species"` read `hm3.species` and could not
|
|
64
|
-
* see `data.species`; `name: "data.species"` read the shared source and could
|
|
65
|
-
* not see `hm3.species`. Each yielded the field's **default** wherever only the
|
|
66
|
-
* other position was authored — silently, since the field compiles and the
|
|
67
|
-
* document is emitted with the value simply gone.
|
|
68
|
-
*
|
|
69
|
-
* That made every move into `data:` a flag day. Each of this package's other
|
|
70
|
-
* retirements — `package:`, `image`, `archetype`, `relation` — works because
|
|
71
|
-
* *both spellings are read while the corpus moves*, and one property could not
|
|
72
|
-
* offer that here.
|
|
73
|
-
*
|
|
74
|
-
* So a field declares `legacyKey` beside `name`: the key it is authored at
|
|
75
|
-
* inside the block, which step 2 reads and which **falls back to `name`** when
|
|
76
|
-
* absent, so every declaration written before this resolves unchanged. A field
|
|
77
|
-
* that declares one is mid-sweep by construction, which is what
|
|
78
|
-
* {@link module:engine/field-spec.readsLegacyKey} reports on.
|
|
79
|
-
*
|
|
80
|
-
* ## Step 3 has a retiring position too, and it is derived
|
|
81
|
-
*
|
|
82
|
-
* `legacyKey` retires the *in-block* position, and for a while that looked like
|
|
83
|
-
* the whole of what `data:` left behind. It is not. The facts `data:` holds
|
|
84
|
-
* were not invented by it — the sweep **gathered** them out of the note's open top
|
|
85
|
-
* level, where `portrait:` sat beside `img:` and `shortcode:` — so a field that
|
|
86
|
-
* declares `data.portrait` has two shared spellings to read, not one, and
|
|
87
|
-
* reading only the current one is the same silent miss `legacyKey` exists to
|
|
88
|
-
* prevent. It is worse here, because the caller's `?? default` cannot tell a
|
|
89
|
-
* value that is absent from one that is merely unreachable: 646 `sohl-thalorna`
|
|
90
|
-
* beings shipped the generic person icon over an authored path, and no tree
|
|
91
|
-
* that still writes the top-level spelling — `sohl`'s own bestiary does —
|
|
92
|
-
* looked any different from one that names no art at all.
|
|
93
|
-
*
|
|
94
|
-
* So step 3b reads it, and unlike `legacyKey` it is **derived**: the retiring
|
|
95
|
-
* spelling of `data.<key>` is `<key>`, mechanically, because that is precisely
|
|
96
|
-
* what the move did. A second declaration would be a second place for the same
|
|
97
|
-
* fact to be stated, and the retirement it describes is one rule rather than a
|
|
98
|
-
* per-field decision. {@link retiredTopLevelKey} is that derivation, and it
|
|
99
|
-
* answers for `data.` sources alone — `protection.blunt` is a path into a
|
|
100
|
-
* container notes have always written at the top level, never a `blunt:` that
|
|
101
|
-
* moved.
|
|
56
|
+
* A key written at a position that is not on this list is not read. For a
|
|
57
|
+
* field whose home is `data:`, the frontmatter lint reports the key at the
|
|
58
|
+
* position it was written.
|
|
102
59
|
*
|
|
103
60
|
* ## A name that collides across the two vocabularies skips step 3
|
|
104
61
|
*
|
|
@@ -108,28 +65,23 @@
|
|
|
108
65
|
* all. They do not always. An `affiliation` item's `system.title` is the style
|
|
109
66
|
* of address an office carries — "Ajaw", "Warden"; a note's top-level `title`
|
|
110
67
|
* is refused outright by the closed top-level vocabulary, which has no `title`
|
|
111
|
-
* entry. Nothing relates them
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* applying `field.default` — only step 2 does — so an authored `title: null`
|
|
115
|
-
* reached the field's coercion unguarded and shipped as the literal string
|
|
116
|
-
* `"null"` in fifteen documents.
|
|
68
|
+
* entry. Nothing relates them. Step 3 answers **without** applying
|
|
69
|
+
* `field.default`, so an authored `title: null` would reach the field's
|
|
70
|
+
* coercion unguarded.
|
|
117
71
|
*
|
|
118
72
|
* So a field may declare `topLevelMeans`: why the note's top-level key of this
|
|
119
73
|
* name is not this field's source — declared wherever that key's spelling
|
|
120
74
|
* means something else at the note level, or the closed top-level vocabulary
|
|
121
|
-
* refuses it outright. Declaring it removes the
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* that describe the document rather than the note. It is deliberately
|
|
125
|
-
* a per-field opt-out rather than a change to the order — step 3 is right
|
|
75
|
+
* refuses it outright. Declaring it removes the shared level (step 3), leaving
|
|
76
|
+
* the positions that describe the document rather than the note. It is a
|
|
77
|
+
* per-field opt-out rather than a change to the order — step 3 is right
|
|
126
78
|
* wherever the two levels state the same quantity, which is nearly everywhere —
|
|
127
79
|
* and its value is the reason rather than a bare flag, so the collision is
|
|
128
80
|
* legible where the field is declared. See {@link module:engine/field-spec.FieldSpec}.
|
|
129
81
|
*
|
|
130
82
|
* `sohlField()` — read `fm.sohl[key]`, fall back to `fm[key]` — is the
|
|
131
|
-
* degenerate case where source and destination happen to share a name. It
|
|
132
|
-
*
|
|
83
|
+
* degenerate case where source and destination happen to share a name. It is
|
|
84
|
+
* not the general rule; {@link blockField} is what remains of it.
|
|
133
85
|
*
|
|
134
86
|
* ## Nothing here knows a game system
|
|
135
87
|
*
|
|
@@ -365,17 +317,14 @@ export function blockProperty(fm, block, key, defaultValue = undefined) {
|
|
|
365
317
|
}
|
|
366
318
|
|
|
367
319
|
/**
|
|
368
|
-
* A system override followed by a shared `data:` value
|
|
369
|
-
* retiring top-level spelling `data:` gathered it off, reporting which one
|
|
320
|
+
* A system override followed by a shared `data:` value, reporting which one
|
|
370
321
|
* answered.
|
|
371
322
|
*
|
|
372
323
|
* {@link module:engine/field-spec.resolveFieldValue}'s counterpart for a
|
|
373
324
|
* universal `data:` key — `pack` and `packFolder`, declared for every note
|
|
374
325
|
* type rather than per type, so neither is a `FieldSpec` that resolver takes.
|
|
375
326
|
* The order is the same shape: a system's own override, then the shared
|
|
376
|
-
* `data:` position
|
|
377
|
-
* `data:` existed. The top-level value stays readable while a tree still on
|
|
378
|
-
* it moves its authored fields.
|
|
327
|
+
* `data:` position.
|
|
379
328
|
*
|
|
380
329
|
* @param {object} fm - The note's frontmatter.
|
|
381
330
|
* @param {string} block - The system block key.
|
|
@@ -389,8 +338,6 @@ export function resolveDataProperty(fm, block, key, defaultValue = undefined) {
|
|
|
389
338
|
if (declared !== undefined && declared !== null) return { value: declared, from: "block" };
|
|
390
339
|
const shared = isMapping(fm?.data) ? fm.data[key] : undefined;
|
|
391
340
|
if (shared !== undefined && shared !== null) return { value: shared, from: "shared" };
|
|
392
|
-
const legacy = isMapping(fm) ? fm[key] : undefined;
|
|
393
|
-
if (legacy !== undefined && legacy !== null) return { value: legacy, from: "topLevel" };
|
|
394
341
|
return { value: defaultValue, from: "default" };
|
|
395
342
|
}
|
|
396
343
|
|
|
@@ -408,27 +355,6 @@ export function blockDataProperty(fm, block, key, defaultValue = undefined) {
|
|
|
408
355
|
return resolveDataProperty(fm, block, key, defaultValue).value;
|
|
409
356
|
}
|
|
410
357
|
|
|
411
|
-
/**
|
|
412
|
-
* The key a field is authored at **inside** a system block — step 2.
|
|
413
|
-
*
|
|
414
|
-
* `legacyKey` when the field declares one, and `name` otherwise. The fallback
|
|
415
|
-
* is what makes this change invisible to every declaration written before it:
|
|
416
|
-
* a field whose shared source and in-block key are the same word says so once,
|
|
417
|
-
* as it always did.
|
|
418
|
-
*
|
|
419
|
-
* Named and exported rather than spelled inline because three readers ask the
|
|
420
|
-
* question and must agree — the resolver here, the frontmatter lint building
|
|
421
|
-
* the set of keys a block may carry, and the author-facing surfaces naming
|
|
422
|
-
* where a value was written.
|
|
423
|
-
*
|
|
424
|
-
* @param {{name?: string, legacyKey?: string}} field - The declaration.
|
|
425
|
-
* @returns {string|undefined} The in-block key, or `undefined` for a field that
|
|
426
|
-
* is not authored at all.
|
|
427
|
-
*/
|
|
428
|
-
export function legacyKeyOf(field) {
|
|
429
|
-
return field?.legacyKey ?? field?.name;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
358
|
/**
|
|
433
359
|
* The `data:` container's prefix, as a shared source spells it.
|
|
434
360
|
*
|
|
@@ -437,31 +363,20 @@ export function legacyKeyOf(field) {
|
|
|
437
363
|
const DATA_PREFIX = "data.";
|
|
438
364
|
|
|
439
365
|
/**
|
|
440
|
-
* The
|
|
441
|
-
*
|
|
442
|
-
* The retiring spelling of `data.portrait` is not a second declaration anyone
|
|
443
|
-
* has to write — it is `portrait`, mechanically, and the same holds for every
|
|
444
|
-
* other key the container gathered. Deriving it is what keeps the two spellings
|
|
445
|
-
* of one field from disagreeing the way two declarations would.
|
|
446
|
-
*
|
|
447
|
-
* **Only a `data.` source has one.** `protection.blunt` and `impact.die` are
|
|
448
|
-
* paths into containers a note has always written at the top level; they were
|
|
449
|
-
* never `blunt:` or `die:`, and reading those would invent a position rather
|
|
450
|
-
* than remember one.
|
|
366
|
+
* The key a field is authored at **inside** a system block — step 2.
|
|
451
367
|
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
368
|
+
* The field's `name`, except that a name which is a path into `data:` has no
|
|
369
|
+
* in-block position. Named and exported because the resolver and the
|
|
370
|
+
* frontmatter lint, which builds the set of keys a block may carry, must agree.
|
|
455
371
|
*
|
|
456
372
|
* @param {{name?: string}} field - The declaration.
|
|
457
|
-
* @returns {string|undefined} The
|
|
458
|
-
*
|
|
373
|
+
* @returns {string|undefined} The in-block key, or `undefined` for a field that
|
|
374
|
+
* is not authored in the block.
|
|
459
375
|
*/
|
|
460
|
-
export function
|
|
376
|
+
export function inBlockKey(field) {
|
|
461
377
|
const name = field?.name;
|
|
462
|
-
if (typeof name !== "string" ||
|
|
463
|
-
|
|
464
|
-
return rest === "" ? undefined : rest;
|
|
378
|
+
if (typeof name !== "string" || name.startsWith(DATA_PREFIX)) return undefined;
|
|
379
|
+
return name;
|
|
465
380
|
}
|
|
466
381
|
|
|
467
382
|
/**
|
|
@@ -492,7 +407,7 @@ export function authoredKey(name) {
|
|
|
492
407
|
* can distinguish a value an author wrote from one a default supplied, which
|
|
493
408
|
* the value alone never says.
|
|
494
409
|
*
|
|
495
|
-
* @typedef {"system"|"block"|"shared"|"
|
|
410
|
+
* @typedef {"system"|"block"|"shared"|"default"|"value"} FieldSource
|
|
496
411
|
*/
|
|
497
412
|
|
|
498
413
|
/**
|
|
@@ -529,19 +444,16 @@ export function resolveFieldValue(field, fm, { block = "sohl" } = {}) {
|
|
|
529
444
|
if (own !== undefined) return { value: own, from: "system" };
|
|
530
445
|
}
|
|
531
446
|
|
|
532
|
-
// 2. The
|
|
533
|
-
//
|
|
534
|
-
// the two are declared separately. Every note in every tree
|
|
535
|
-
// writes here today, and will until the corpus moves them; dropping it would
|
|
536
|
-
// be a corpus migration disguised as a mechanism change.
|
|
447
|
+
// 2. The field's own name, written in the block. A name that is a path
|
|
448
|
+
// into `data:` has no in-block position: its home is the container.
|
|
537
449
|
const declared = systemBlock(fm, block);
|
|
538
|
-
const
|
|
539
|
-
if (declared &&
|
|
540
|
-
if (
|
|
541
|
-
const value = declared[
|
|
450
|
+
const key = inBlockKey(field);
|
|
451
|
+
if (declared && key !== undefined) {
|
|
452
|
+
if (key in declared) {
|
|
453
|
+
const value = declared[key];
|
|
542
454
|
return { value: value ?? field.default, from: "block" };
|
|
543
455
|
}
|
|
544
|
-
const nested = getFrontmatter(declared,
|
|
456
|
+
const nested = getFrontmatter(declared, key, undefined);
|
|
545
457
|
if (nested !== undefined) return { value: nested, from: "block" };
|
|
546
458
|
}
|
|
547
459
|
|
|
@@ -551,20 +463,6 @@ export function resolveFieldValue(field, fm, { block = "sohl" } = {}) {
|
|
|
551
463
|
if (field.topLevelMeans === undefined) {
|
|
552
464
|
const shared = getFrontmatter(fm, field.name, undefined);
|
|
553
465
|
if (shared !== undefined) return { value: shared, from: "shared" };
|
|
554
|
-
|
|
555
|
-
// 3b. The bare top-level key the `data:` source was gathered off — the
|
|
556
|
-
// retiring *shared* position, exactly as `legacyKey` is the
|
|
557
|
-
// retiring *in-block* one. Without it a field declaring
|
|
558
|
-
// `data.portrait` cannot see the `portrait:` every tree still
|
|
559
|
-
// writes, and the miss arrives at the caller's `?? default` as an
|
|
560
|
-
// ordinary absence: 646 `sohl-thalorna` beings compiled the generic
|
|
561
|
-
// person icon over an authored path, deterministically and with
|
|
562
|
-
// nothing said.
|
|
563
|
-
const retiring = retiredTopLevelKey(field);
|
|
564
|
-
if (retiring !== undefined) {
|
|
565
|
-
const legacy = getFrontmatter(fm, retiring, undefined);
|
|
566
|
-
if (legacy !== undefined) return { value: legacy, from: "topLevel" };
|
|
567
|
-
}
|
|
568
466
|
}
|
|
569
467
|
|
|
570
468
|
// 4. The field's own default.
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
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
|
+
*
|
|
5
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
12
|
*/
|
|
7
13
|
|
|
@@ -0,0 +1,69 @@
|
|
|
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 repository walk shared by the prose checks.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Directories never walked, whatever the ignore files say.
|
|
25
|
+
*
|
|
26
|
+
* `node_modules` is in every consumer's `.gitignore` and would be excluded
|
|
27
|
+
* anyway; skipping it in the walk rather than per file is the difference
|
|
28
|
+
* between a check that takes a second and one that stats a hundred thousand
|
|
29
|
+
* files to be told each is ignored. `.git` is in nobody's `.gitignore`, so it
|
|
30
|
+
* has to be named.
|
|
31
|
+
*/
|
|
32
|
+
const NEVER_WALK = Object.freeze(new Set([".git", "node_modules"]));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every file under a root, minus the directories nothing should walk.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} root - Absolute path to walk.
|
|
38
|
+
* @param {object} [hooks]
|
|
39
|
+
* @param {(dir: string) => void} [hooks.enter] - Called with each directory
|
|
40
|
+
* before its entries are read.
|
|
41
|
+
* @param {(abs: string, isDirectory: boolean) => boolean} [hooks.skip] -
|
|
42
|
+
* Whether to leave a path out; a skipped directory is not entered.
|
|
43
|
+
* @returns {string[]} Absolute file paths, in directory order.
|
|
44
|
+
*/
|
|
45
|
+
export function walkFiles(root, hooks = {}) {
|
|
46
|
+
const out = [];
|
|
47
|
+
/** @param {string} dir - Directory to descend into. */
|
|
48
|
+
const descend = (dir) => {
|
|
49
|
+
hooks.enter?.(dir);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
// A directory that vanished or cannot be read is not a finding
|
|
55
|
+
// about prose; leave it to whatever owns it.
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const abs = path.join(dir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!NEVER_WALK.has(entry.name) && !hooks.skip?.(abs, true)) descend(abs);
|
|
62
|
+
} else if (entry.isFile() && !hooks.skip?.(abs, false)) {
|
|
63
|
+
out.push(abs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
descend(root);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -416,9 +416,8 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
416
416
|
const label = authoredLabel({ display });
|
|
417
417
|
|
|
418
418
|
// **Every link carries a label**. Without one there is nothing
|
|
419
|
-
// to resolve against
|
|
420
|
-
//
|
|
421
|
-
// rather than prose. Reported before the same-page form, because the
|
|
419
|
+
// to resolve against, and nothing to show either, a shortcode being an
|
|
420
|
+
// address rather than prose. Reported before the same-page form, because the
|
|
422
421
|
// rule is about how the link is *written*: `[[#slug]]` needs the pipe
|
|
423
422
|
// exactly as `[[skill-clmb]]` does.
|
|
424
423
|
if (!parsed.labelled) {
|
|
@@ -531,6 +530,26 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
531
530
|
read && !read.reason && read.package === ctx.contentPackage ?
|
|
532
531
|
`${read.type}/${read.shortcode}`.toLowerCase()
|
|
533
532
|
: rawKey;
|
|
533
|
+
// A capital in the shortcode is refused, never lowercased. The
|
|
534
|
+
// lowercase spelling is looked up only to name it in the finding.
|
|
535
|
+
if (read?.reason === "shortcode-not-lowercase") {
|
|
536
|
+
const lower = read.lowercase;
|
|
537
|
+
const declared =
|
|
538
|
+
lookupRead(ctx.index, lower, ctx.contentPackage) ??
|
|
539
|
+
(ctx.foreign && lower.package ?
|
|
540
|
+
lookupRead(ctx.foreign, lower, ctx.contentPackage)
|
|
541
|
+
: undefined);
|
|
542
|
+
return report(
|
|
543
|
+
all,
|
|
544
|
+
{
|
|
545
|
+
target,
|
|
546
|
+
reason: "shortcode-not-lowercase",
|
|
547
|
+
shortcode: read.shortcode,
|
|
548
|
+
...(declared ? { declared: lower.shortcode } : {}),
|
|
549
|
+
},
|
|
550
|
+
label ?? target,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
534
553
|
const reason =
|
|
535
554
|
ctx.collide?.has(collideKey) ? "ambiguous"
|
|
536
555
|
// "It parsed as an address" is a property of the parse, not of
|
|
@@ -538,6 +557,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
538
557
|
: read && !read.reason ? "unresolved"
|
|
539
558
|
: read?.reason === "unknown-type" ? "unknown-type"
|
|
540
559
|
: read?.reason === "no-content-index" ? "no-content-index"
|
|
560
|
+
: read?.reason === "not-lowercase" ? "not-lowercase"
|
|
541
561
|
// Every link is an address, and this is not one. Distinct from
|
|
542
562
|
// a dead address, because the fix is different: a name has to
|
|
543
563
|
// become an address, not be corrected.
|
|
@@ -40,8 +40,8 @@ import { parseAddress, isAddressTuple, splitAnchor } from "./address.mjs";
|
|
|
40
40
|
*
|
|
41
41
|
* The one rule about a link's *shape* that both resolvers share also lives here
|
|
42
42
|
* — **a link carries a label** ({@link unlabelledLinkMessage}) — because the
|
|
43
|
-
* two
|
|
44
|
-
* first.
|
|
43
|
+
* two state it through one message and the author reads the same words
|
|
44
|
+
* whichever ran first.
|
|
45
45
|
*
|
|
46
46
|
* For the same reason, so does the **vocabulary of link findings**
|
|
47
47
|
* ({@link LINK_FINDING_REASONS}) and the message each one reports through
|
|
@@ -145,15 +145,10 @@ export function authoredLabel({ display }) {
|
|
|
145
145
|
* the form to write rather than a value to correct: there is no value that
|
|
146
146
|
* makes an unlabelled link resolve.
|
|
147
147
|
*
|
|
148
|
-
* **
|
|
149
|
-
*
|
|
150
|
-
* empty in practice: across 8,305 wikilinks in three content trees, not one
|
|
151
|
-
* bare `[[Alias]]` resolved to a note. What the index it looked up in *did* do
|
|
152
|
-
* was fold every note's `name.full` into itself, which forbade two notes of a
|
|
153
|
-
* type from sharing a display name. So the namespace is gone, every
|
|
154
|
-
* link is an address, and an address needs the pipe that says so.
|
|
148
|
+
* **A bare link has nothing to mean.** Every link is an address, and an
|
|
149
|
+
* address needs the pipe that says so.
|
|
155
150
|
*
|
|
156
|
-
* The **link part may
|
|
151
|
+
* The **link part may be an anchor**: `[[#slug|Text]]` addresses a
|
|
157
152
|
* section of the page it is written on. It is the label that is required, not
|
|
158
153
|
* a target.
|
|
159
154
|
*
|
|
@@ -164,7 +159,7 @@ export function unlabelledLinkMessage(target) {
|
|
|
164
159
|
return (
|
|
165
160
|
`wikilink [[${target}]] carries no label, so it addresses nothing — ` +
|
|
166
161
|
`write [[type-shortcode|Text]]. Every link is an address; the bare ` +
|
|
167
|
-
`[[Name]] form
|
|
162
|
+
`[[Name]] form addresses nothing`
|
|
168
163
|
);
|
|
169
164
|
}
|
|
170
165
|
|
|
@@ -181,6 +176,8 @@ export function unlabelledLinkMessage(target) {
|
|
|
181
176
|
* - `unlabelled` — no `|`, so the link addresses nothing.
|
|
182
177
|
* - `not-an-address` — labelled, but the target does not parse as an address.
|
|
183
178
|
* - `not-lowercase` — a package, system or type segment carries a capital.
|
|
179
|
+
* - `shortcode-not-lowercase` — the shortcode segment carries a capital. A
|
|
180
|
+
* shortcode is lowercase, and the link is refused rather than lowercased.
|
|
184
181
|
* - `unknown-type` — definitely qualified, but names no type this build knows.
|
|
185
182
|
* - `not-an-asset` — a real address, embedded where only a file can be drawn.
|
|
186
183
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
@@ -199,6 +196,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
|
|
|
199
196
|
"unlabelled",
|
|
200
197
|
"not-an-address",
|
|
201
198
|
"not-lowercase",
|
|
199
|
+
"shortcode-not-lowercase",
|
|
202
200
|
"unknown-type",
|
|
203
201
|
"not-an-asset",
|
|
204
202
|
"unresolved",
|
|
@@ -240,13 +238,9 @@ export function isEventAnchor(hit, anchor) {
|
|
|
240
238
|
* package publishing it is not a declared dependency — or is one whose index
|
|
241
239
|
* has not been fetched — and the link itself looks identical in every case.
|
|
242
240
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* written yet. That reasoning was a property of the bare form, which is retired;
|
|
247
|
-
* the intent behind it now has a real spelling, a `draft`-tagged note
|
|
248
|
-
* that exists and resolves and renders marked. So an address naming no
|
|
249
|
-
* note is a typo or an omission, both want fixing, and all three builds say so.
|
|
241
|
+
* An address naming no note is a typo or an omission: the intent of a
|
|
242
|
+
* placeholder for a note nobody has written yet is spelled as a `draft`-tagged
|
|
243
|
+
* note that exists, resolves and renders marked. Both want fixing, and all three builds say so.
|
|
250
244
|
*
|
|
251
245
|
* @param {string} target - The address as authored, named in the message.
|
|
252
246
|
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
@@ -327,11 +321,24 @@ export function ambiguousAddressMessage(target, packages = []) {
|
|
|
327
321
|
* @param {string} [finding.type] - For `not-an-asset`, the type the address
|
|
328
322
|
* named.
|
|
329
323
|
* @param {string} [finding.stub] - For `stub`, where the stub is.
|
|
324
|
+
* @param {string} [finding.shortcode] - For `shortcode-not-lowercase`, the
|
|
325
|
+
* shortcode as written.
|
|
326
|
+
* @param {string} [finding.declared] - For `shortcode-not-lowercase`, the
|
|
327
|
+
* lowercase spelling, when it is a declared shortcode.
|
|
330
328
|
* @returns {string} The message.
|
|
331
329
|
* @throws {Error} On a reason outside the closed set — a resolver inventing one
|
|
332
330
|
* would otherwise report a link with no explanation at all.
|
|
333
331
|
*/
|
|
334
|
-
export function linkFindingMessage({
|
|
332
|
+
export function linkFindingMessage({
|
|
333
|
+
reason,
|
|
334
|
+
target,
|
|
335
|
+
packages,
|
|
336
|
+
anchor,
|
|
337
|
+
type,
|
|
338
|
+
stub,
|
|
339
|
+
shortcode,
|
|
340
|
+
declared,
|
|
341
|
+
}) {
|
|
335
342
|
switch (reason) {
|
|
336
343
|
case "unlabelled":
|
|
337
344
|
return unlabelledLinkMessage(target);
|
|
@@ -345,8 +352,12 @@ export function linkFindingMessage({ reason, target, packages, anchor, type, stu
|
|
|
345
352
|
case "not-lowercase":
|
|
346
353
|
return (
|
|
347
354
|
`address [[${target}]] capitalises a package, system or type ` +
|
|
348
|
-
`segment —
|
|
349
|
-
|
|
355
|
+
`segment — every segment of an address is lowercase`
|
|
356
|
+
);
|
|
357
|
+
case "shortcode-not-lowercase":
|
|
358
|
+
return (
|
|
359
|
+
`address [[${target}]] shortcode \`${shortcode ?? target}\` must be ` +
|
|
360
|
+
`lowercase${declared ? `; the declared shortcode is \`${declared}\`` : ""}`
|
|
350
361
|
);
|
|
351
362
|
case "unknown-type":
|
|
352
363
|
return `address [[${target}]] names no known content type`;
|