@heroiclands/package-build 23.1.0 → 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 +71 -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 +54 -6
- 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/engine/yaml-lint.mjs +7 -4
- 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 +83 -18
- 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/engine/yaml-lint.d.mts +5 -4
- 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 +12 -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
|
@@ -39,12 +39,10 @@
|
|
|
39
39
|
* vocabulary (an adventure module ships skills, beings and magic swords), so
|
|
40
40
|
* every consumer loads all of it.
|
|
41
41
|
*
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* knowledge — `retired-fields.mjs` and `homepage.mjs` — and this module only
|
|
47
|
-
* locates it in the file.
|
|
42
|
+
* The rule that is not schema-driven sits on the *note format* side of that
|
|
43
|
+
* line, which is why it is here and not in `sohl/`: the address-bearing fields
|
|
44
|
+
* a `type: homepage` note refuses. The message comes from `homepage.mjs`, which
|
|
45
|
+
* owns the knowledge, and this module only locates it in the file.
|
|
48
46
|
*
|
|
49
47
|
* **It takes a built link index rather than walking itself.** The dead-
|
|
50
48
|
* reference check has to resolve exactly as a wikilink does, cross-package
|
|
@@ -67,12 +65,10 @@ import {
|
|
|
67
65
|
import { AddressEntries, AddressLink } from "./address-values.mjs";
|
|
68
66
|
import { resolveEventReference } from "./note-events.mjs";
|
|
69
67
|
import { addressPositions } from "./note-addresses.mjs";
|
|
70
|
-
import { authoredFields
|
|
68
|
+
import { authoredFields } from "./field-spec.mjs";
|
|
71
69
|
import {
|
|
72
|
-
|
|
73
|
-
resolveDataProperty,
|
|
70
|
+
inBlockKey,
|
|
74
71
|
resolveFieldValue,
|
|
75
|
-
retiredTopLevelKey,
|
|
76
72
|
systemBlock,
|
|
77
73
|
SYSTEM_BLOCK_KEYS,
|
|
78
74
|
unknownBlockKeys,
|
|
@@ -84,7 +80,6 @@ import {
|
|
|
84
80
|
} from "./diagnostics.mjs";
|
|
85
81
|
import { pathnameProblem } from "./pathnames.mjs";
|
|
86
82
|
import { checkHomepageAddressFields } from "./homepage.mjs";
|
|
87
|
-
import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from "./ids.mjs";
|
|
88
83
|
import { parseAddress, acceptsType, renderAddress, hasAnchor } from "./address.mjs";
|
|
89
84
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
90
85
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
@@ -101,19 +96,6 @@ import {
|
|
|
101
96
|
} from "./note-vocabulary.mjs";
|
|
102
97
|
// The art slots, declared once. The linter states no art key of its own.
|
|
103
98
|
import { ART_SLOTS } from "./art-fields.mjs";
|
|
104
|
-
import {
|
|
105
|
-
RETIRED_FIELD_ALIASES,
|
|
106
|
-
declaresRetiredAlias,
|
|
107
|
-
aliasesRetiredMessage,
|
|
108
|
-
declaresRetiredAliasesField,
|
|
109
|
-
draftRetiredMessage,
|
|
110
|
-
legacyKeyMessage,
|
|
111
|
-
readAliasedField,
|
|
112
|
-
retiredAliasMessage,
|
|
113
|
-
retiredTopLevelMessage,
|
|
114
|
-
sectionRetiredMessage,
|
|
115
|
-
traitsRetiredMessage,
|
|
116
|
-
} from "./retired-fields.mjs";
|
|
117
99
|
|
|
118
100
|
/**
|
|
119
101
|
* `sohl:` keys every type accepts, whatever its schema says.
|
|
@@ -123,10 +105,10 @@ import {
|
|
|
123
105
|
* declaration and both would otherwise be reported as unknown on every note
|
|
124
106
|
* that uses them.
|
|
125
107
|
*
|
|
126
|
-
* - `
|
|
108
|
+
* - `packFolder` — the folder note the document is filed in.
|
|
127
109
|
* - `pack` — which compendium of the note's document type receives it, for a
|
|
128
110
|
* repository shipping several.
|
|
129
|
-
* - `
|
|
111
|
+
* - `templatePriority` — which of several templates the Create dialog offers.
|
|
130
112
|
* - `kbcat` — the knowledgebase category a note is grouped under.
|
|
131
113
|
*
|
|
132
114
|
* `kbcat` is the one that matters for what this check *is*. **The pack build is
|
|
@@ -144,22 +126,9 @@ import {
|
|
|
144
126
|
* @type {ReadonlySet<string>}
|
|
145
127
|
*/
|
|
146
128
|
export const UNIVERSAL_KEYS = Object.freeze(
|
|
147
|
-
new Set(["packFolder", "pack", "
|
|
129
|
+
new Set(["packFolder", "pack", "templatePriority", "kbcat"]),
|
|
148
130
|
);
|
|
149
131
|
|
|
150
|
-
/**
|
|
151
|
-
* The shared `data:` keys declared for every note type that still have a
|
|
152
|
-
* retiring top-level spelling — `pack` and `packFolder`, the two
|
|
153
|
-
* {@link module:engine/note-vocabulary.SHARED_DATA_FIELDS} entries
|
|
154
|
-
* {@link module:engine/system-block.resolveDataProperty} reads off a tree's
|
|
155
|
-
* bare top level. Unlike a type's own declarations, which the retiring set
|
|
156
|
-
* below derives from `schemas`, these apply to every type at once, so they
|
|
157
|
-
* are named once here rather than attached to one type's vocabulary.
|
|
158
|
-
*
|
|
159
|
-
* @type {readonly string[]}
|
|
160
|
-
*/
|
|
161
|
-
const UNIVERSAL_RETIRING_DATA_KEYS = Object.freeze(["pack", "packFolder"]);
|
|
162
|
-
|
|
163
132
|
/**
|
|
164
133
|
* The system blocks a build checks, and what each accepts beyond the shared
|
|
165
134
|
* vocabulary.
|
|
@@ -331,17 +300,10 @@ export function matchesKind(value, kind, context = {}) {
|
|
|
331
300
|
// field admits. A list has neither.
|
|
332
301
|
return matchesKind(value, "string") || matchesKind(value, "map");
|
|
333
302
|
case "string-or-list":
|
|
334
|
-
// One value or several of the same kind.
|
|
303
|
+
// One value or several of the same kind. The
|
|
335
304
|
// two halves say the same thing at different lengths, so either is
|
|
336
305
|
// accepted here and the entries are read by the field's own check.
|
|
337
306
|
return matchesKind(value, "string") || Array.isArray(value);
|
|
338
|
-
case "list-or-map":
|
|
339
|
-
// A list, or a map. The two carry different facts rather than
|
|
340
|
-
// being two spellings of one — a map keyed by Address says what is
|
|
341
|
-
// true *of* each key, which a list has nowhere to put — so the
|
|
342
|
-
// field's own check is what reads the entries, and all this
|
|
343
|
-
// answers is whether the value has one of the two shapes.
|
|
344
|
-
return Array.isArray(value) || matchesKind(value, "map");
|
|
345
307
|
default:
|
|
346
308
|
return true;
|
|
347
309
|
}
|
|
@@ -432,34 +394,8 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
432
394
|
/** First segment of each declared name — `charges.value` is authored as `charges`. */
|
|
433
395
|
const declared = new Set(fields.map((f) => f.name.split(".")[0]));
|
|
434
396
|
|
|
435
|
-
// The retired spelling of a key this type declares → what to write now.
|
|
436
|
-
// Built from the type's own declaration, so a renamed `data:` key is
|
|
437
|
-
// retired exactly where its replacement exists and stays an unrecognised
|
|
438
|
-
// key on every other type.
|
|
439
|
-
const renamed = new Map();
|
|
440
|
-
for (const name of declared) {
|
|
441
|
-
const retired = RETIRED_FIELD_ALIASES[name];
|
|
442
|
-
if (retired) renamed.set(retired, name);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
397
|
for (const key of Object.keys(entries)) {
|
|
446
398
|
if (declared.has(key)) continue;
|
|
447
|
-
const current = renamed.get(key);
|
|
448
|
-
if (current) {
|
|
449
|
-
findings.push({
|
|
450
|
-
file: note.file,
|
|
451
|
-
...positionOfFrontmatterPath(raw, ["data", key], { key: true }),
|
|
452
|
-
// A warning, as every retired spelling is: the note compiles to
|
|
453
|
-
// the correct document, so failing a build over it would red a
|
|
454
|
-
// tree that has done nothing wrong yet. The refusal comes once
|
|
455
|
-
// no tree writes the key, and needs no code — with the alias
|
|
456
|
-
// gone the spelling is an unrecognised key again, which the
|
|
457
|
-
// loop below already refuses.
|
|
458
|
-
severity: "warning",
|
|
459
|
-
message: retiredAliasMessage(key, current),
|
|
460
|
-
});
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
399
|
const guess = nearest(key, declared);
|
|
464
400
|
findings.push({
|
|
465
401
|
file: note.file,
|
|
@@ -468,9 +404,6 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
468
404
|
message:
|
|
469
405
|
type === "affiliation" && key === "commonSkills" ?
|
|
470
406
|
"`data.commonSkills` is not an affiliation field; write skill Addresses at `sohl.system.commonSkills`"
|
|
471
|
-
: type === "lore" && key === "event" ?
|
|
472
|
-
"`data.event` is retired — write `data.events`, a list of occurrences, " +
|
|
473
|
-
"instead, each `{ when, until?, recurs? }`; a note may state several"
|
|
474
407
|
: `"${key}" is not a \`data:\` property declared by ${type}; ` +
|
|
475
408
|
`the container is closed, so the key reaches no document ` +
|
|
476
409
|
`and no page` +
|
|
@@ -604,13 +537,7 @@ function eventReferenceProblem(value, field, defaults, index) {
|
|
|
604
537
|
function checkDataReferences(note, field, value, segments, context, index) {
|
|
605
538
|
const checks = [];
|
|
606
539
|
if (field.kind === "address") checks.push({ value, path: [], kind: "address" });
|
|
607
|
-
|
|
608
|
-
// `entryKind` the list's entries, `keyKind` the map's keys. Reading both of
|
|
609
|
-
// one value would check a map's standings as if they were Addresses and a
|
|
610
|
-
// list's indices as if they were its keys.
|
|
611
|
-
const dual = field.kind === "list-or-map";
|
|
612
|
-
const written = Array.isArray(value) ? "list" : "map";
|
|
613
|
-
if (field.entryKind && !(dual && written === "map")) {
|
|
540
|
+
if (field.entryKind) {
|
|
614
541
|
if (Array.isArray(value)) {
|
|
615
542
|
value.forEach((entry, i) =>
|
|
616
543
|
checks.push({ value: entry, path: [i], kind: field.entryKind }),
|
|
@@ -622,7 +549,7 @@ function checkDataReferences(note, field, value, segments, context, index) {
|
|
|
622
549
|
});
|
|
623
550
|
} else checks.push({ value, path: [], kind: field.entryKind });
|
|
624
551
|
}
|
|
625
|
-
if (field.keyKind && !(
|
|
552
|
+
if (field.keyKind && !Array.isArray(value)) {
|
|
626
553
|
if (value instanceof AddressEntries) {
|
|
627
554
|
for (const entry of value.entries)
|
|
628
555
|
checks.push({
|
|
@@ -639,7 +566,9 @@ function checkDataReferences(note, field, value, segments, context, index) {
|
|
|
639
566
|
const findings = [];
|
|
640
567
|
const defaults = { ...context, type: field.ref };
|
|
641
568
|
const accepted = field.accepts;
|
|
569
|
+
const slot = ART_SLOTS.find((candidate) => candidate.key === field.name);
|
|
642
570
|
for (const check of checks) {
|
|
571
|
+
if (field.declines !== undefined && check.value === field.declines) continue;
|
|
643
572
|
if (
|
|
644
573
|
check.key &&
|
|
645
574
|
field.keySelector === "subType" &&
|
|
@@ -661,10 +590,14 @@ function checkDataReferences(note, field, value, segments, context, index) {
|
|
|
661
590
|
reason = `accepts ${accepted.join(" or ")}, not ${tuple.type}`;
|
|
662
591
|
else if (
|
|
663
592
|
index?.addressHit &&
|
|
664
|
-
!
|
|
593
|
+
(!slot || slot.declines !== undefined) &&
|
|
665
594
|
!index.addressHit(renderAddress(tuple))
|
|
666
595
|
)
|
|
667
|
-
reason =
|
|
596
|
+
reason =
|
|
597
|
+
`names ${renderAddress(tuple)}, which does not resolve in this package or its declared dependencies` +
|
|
598
|
+
(slot?.declines === undefined ?
|
|
599
|
+
""
|
|
600
|
+
: `; write \`${slot.declines}\` to decline the image on purpose`);
|
|
668
601
|
}
|
|
669
602
|
if (!reason) continue;
|
|
670
603
|
const path = ["data", ...segments, ...check.path];
|
|
@@ -767,10 +700,7 @@ export function systemAddressFindings(note, { index, schemas, systems } = {}) {
|
|
|
767
700
|
const address =
|
|
768
701
|
isAddressTuple(item.value) ? item.value
|
|
769
702
|
: typeof item.value === "string" ?
|
|
770
|
-
parseAddress(item.value, context, {
|
|
771
|
-
declared: true,
|
|
772
|
-
legacyShortcodeCase: position.legacyShortcodeCase,
|
|
773
|
-
})
|
|
703
|
+
parseAddress(item.value, context, { declared: true })
|
|
774
704
|
: null;
|
|
775
705
|
if (!address || address.reason) continue;
|
|
776
706
|
if (position.accepts && !acceptsType(address, position.accepts)) continue;
|
|
@@ -883,11 +813,8 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
|
|
|
883
813
|
* whatever the type declares, and only once it is a well-formed term is the
|
|
884
814
|
* type's own list the reason to refuse it.
|
|
885
815
|
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
* has swept, so it would guard nothing: the old spelling falls through to the
|
|
889
|
-
* charset check, which refuses it for the reason that always applied — it
|
|
890
|
-
* contains a hyphen.
|
|
816
|
+
* A hyphenated term such as `user-guide` falls through to the charset check,
|
|
817
|
+
* which refuses it because it contains a hyphen.
|
|
891
818
|
*
|
|
892
819
|
* @param {object} note - The note.
|
|
893
820
|
* @param {object} opts
|
|
@@ -1027,10 +954,8 @@ export const ART_FIELDS = Object.freeze(
|
|
|
1027
954
|
* The keys a field declaration is authored at **inside a system block**.
|
|
1028
955
|
*
|
|
1029
956
|
* The first segment of each field's in-block key: `impact.die` is authored as
|
|
1030
|
-
* `impact
|
|
1031
|
-
*
|
|
1032
|
-
* the name instead would report `sohl.species` as a property no `being` has,
|
|
1033
|
-
* against exactly the notes the sweep has not reached yet.
|
|
957
|
+
* `impact`. A field whose `name` is a path into `data:` has none: it is
|
|
958
|
+
* authored in the container.
|
|
1034
959
|
*
|
|
1035
960
|
* Written once and read twice: the note type's own declaration answers for the
|
|
1036
961
|
* system whose vocabulary the caller's `schemas` are, and a second system's
|
|
@@ -1041,7 +966,11 @@ export const ART_FIELDS = Object.freeze(
|
|
|
1041
966
|
* @returns {Set<string>} The in-block keys.
|
|
1042
967
|
*/
|
|
1043
968
|
function inBlockKeys(schema) {
|
|
1044
|
-
return new Set(
|
|
969
|
+
return new Set(
|
|
970
|
+
authoredFields(schema ?? [])
|
|
971
|
+
.map((f) => inBlockKey(f)?.split(".")[0])
|
|
972
|
+
.filter((key) => key !== undefined),
|
|
973
|
+
);
|
|
1045
974
|
}
|
|
1046
975
|
|
|
1047
976
|
/**
|
|
@@ -1060,8 +989,8 @@ function inBlockKeys(schema) {
|
|
|
1060
989
|
* nothing outside that item supplies them and a note-level check must not go
|
|
1061
990
|
* looking in the block for one.
|
|
1062
991
|
*
|
|
1063
|
-
* Keyed on the **in-block** key —
|
|
1064
|
-
*
|
|
992
|
+
* Keyed on the **in-block** key — its first segment where that is dotted —
|
|
993
|
+
* because that is the position a note
|
|
1065
994
|
* authors, and so the position a note-level check would otherwise read.
|
|
1066
995
|
*
|
|
1067
996
|
* @param {readonly object[]|null|undefined} schema - The type's declarations.
|
|
@@ -1073,7 +1002,8 @@ function collidingBlockKeys(schema) {
|
|
|
1073
1002
|
if (!Array.isArray(schema)) return keys;
|
|
1074
1003
|
for (const field of authoredFields(schema)) {
|
|
1075
1004
|
if (field.topLevelMeans === undefined) continue;
|
|
1076
|
-
|
|
1005
|
+
const key = inBlockKey(field);
|
|
1006
|
+
if (key !== undefined) keys.add(key.split(".")[0]);
|
|
1077
1007
|
}
|
|
1078
1008
|
return keys;
|
|
1079
1009
|
}
|
|
@@ -1256,21 +1186,6 @@ export function lintNote(
|
|
|
1256
1186
|
const raw = () => note.raw ?? "";
|
|
1257
1187
|
const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
|
|
1258
1188
|
|
|
1259
|
-
// The keys the closed top-level region refuses in general, but a
|
|
1260
|
-
// declaration still reads at the retiring position — this type's own
|
|
1261
|
-
// fields, which `resolveFieldValue`'s step 3b resolves, plus `pack` and
|
|
1262
|
-
// `packFolder`, declared for every type and resolved the same way by
|
|
1263
|
-
// {@link resolveDataProperty}. Derived ahead of the schema lookup below,
|
|
1264
|
-
// which runs after the closed-region check, so that check can skip
|
|
1265
|
-
// exactly these and nothing else: a key no declaration names stays
|
|
1266
|
-
// refused.
|
|
1267
|
-
const retiringTopLevelKeys = new Set([
|
|
1268
|
-
...UNIVERSAL_RETIRING_DATA_KEYS,
|
|
1269
|
-
...authoredFields(schemas?.[currentType(type)] ?? [])
|
|
1270
|
-
.filter((field) => field.topLevelMeans === undefined)
|
|
1271
|
-
.map((field) => retiredTopLevelKey(field))
|
|
1272
|
-
.filter((key) => key !== undefined),
|
|
1273
|
-
]);
|
|
1274
1189
|
if (type === "being") {
|
|
1275
1190
|
const archetypes = fm.data?.archetypes;
|
|
1276
1191
|
if (
|
|
@@ -1374,76 +1289,26 @@ export function lintNote(
|
|
|
1374
1289
|
* each check would otherwise ask the same question of the same
|
|
1375
1290
|
* declarations.
|
|
1376
1291
|
*/
|
|
1377
|
-
const blockCollisions = collidingBlockKeys(schemas?.[
|
|
1292
|
+
const blockCollisions = collidingBlockKeys(schemas?.[type]);
|
|
1378
1293
|
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
// every finding in the tree at once, rather than one note at a time.
|
|
1294
|
+
// `package:` is never a note property: the package is the repository's
|
|
1295
|
+
// configured `contentPackage`. Reported here as well as refused at compile
|
|
1296
|
+
// because this is where an author meets every finding in the tree at once.
|
|
1383
1297
|
if (Object.hasOwn(fm, "package")) {
|
|
1384
1298
|
findings.push({
|
|
1385
1299
|
file: note.file,
|
|
1386
1300
|
...at("package"),
|
|
1387
1301
|
severity: "error",
|
|
1388
1302
|
message:
|
|
1389
|
-
"`package:` is a
|
|
1390
|
-
"
|
|
1391
|
-
"
|
|
1392
|
-
"note in the tree belongs to it",
|
|
1303
|
+
"`package:` is not a frontmatter field — delete it. A note's package " +
|
|
1304
|
+
"is this repository's configured `contentPackage`, in " +
|
|
1305
|
+
"package-build.config.yaml, and every note in the tree belongs to it",
|
|
1393
1306
|
});
|
|
1394
1307
|
}
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
// Checked here as well as refused at compile because this is where an
|
|
1400
|
-
// author meets every one of them in the tree at once — which is what a
|
|
1401
|
-
// tree still to sweep needs, the whole corpus rather than the first note
|
|
1402
|
-
// the compile happens to reach.
|
|
1403
|
-
//
|
|
1404
|
-
// **Both positions**, because notes wrote it both ways: top-level, and
|
|
1405
|
-
// inside the `sohl:` block. The block spelling is no longer a universal
|
|
1406
|
-
// key, so it would otherwise be reported as merely unrecognized, which
|
|
1407
|
-
// says nothing about what to write instead.
|
|
1408
|
-
const sohlBlock = fm.sohl;
|
|
1409
|
-
const folderInBlock =
|
|
1410
|
-
!!sohlBlock && typeof sohlBlock === "object" && Object.hasOwn(sohlBlock, "folder");
|
|
1411
|
-
if (Object.hasOwn(fm, "folder") || folderInBlock) {
|
|
1412
|
-
findings.push({
|
|
1413
|
-
file: note.file,
|
|
1414
|
-
...at("folder"),
|
|
1415
|
-
severity: "error",
|
|
1416
|
-
message:
|
|
1417
|
-
"`folder:` is a retired frontmatter field — write `packFolder` " +
|
|
1418
|
-
"instead. A folder is a note (`type: folder`) now, and " +
|
|
1419
|
-
"`packFolder` names it by its address, not by the Foundry id a " +
|
|
1420
|
-
"retired `*-folders.yaml` used to declare",
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
// `img: ""` was how a note said "I name no art" while `resolveImg`
|
|
1424
|
-
// conflated the two empties and every caller defaulted with `||`. It now
|
|
1425
|
-
// says the opposite — "ship no art, and do not default me" — so a
|
|
1426
|
-
// note carrying the old spelling has quietly changed meaning. Forty-five
|
|
1427
|
-
// `sohl-thalorna` notes were written under the old reading and would have
|
|
1428
|
-
// lost their default art with no error and no warning; this is the guard
|
|
1429
|
-
// that would have caught them.
|
|
1430
|
-
//
|
|
1431
|
-
// **Both art fields, because both go through `resolveImg`.** `portrait` is
|
|
1432
|
-
// not a variant spelling of `img` — a being carries the two independently —
|
|
1433
|
-
// and checking only the more common one is how the sweep that prompted this
|
|
1434
|
-
// guard missed eleven `sohl-kethira-basic` beings that write
|
|
1435
|
-
// `portrait: ""`. Whatever the rule is, it belongs to the function, not to
|
|
1436
|
-
// one of the keys that reaches it.
|
|
1437
|
-
//
|
|
1438
|
-
// A **warning**, on the pattern the `package:` and retired-alias sweeps
|
|
1439
|
-
// set: the note still compiles, to a document that is merely iconless, so
|
|
1440
|
-
// reddening a tree over it would refuse before the sweep rather than after
|
|
1441
|
-
// it. It is transitional in the same sense — `""` is a legal thing to mean,
|
|
1442
|
-
// and the message says so, but nothing in any tree means it yet.
|
|
1443
|
-
//
|
|
1444
|
-
// **The art slots only.** The rule reads as a general one about optional
|
|
1445
|
-
// strings, and it is not — it belongs to `resolveImg`, and nothing else
|
|
1446
|
-
// goes through it.
|
|
1308
|
+
// An empty art value is an error. A slot names an image or is left out; a
|
|
1309
|
+
// blank is how an unfinished note looks, and declining art has to be said
|
|
1310
|
+
// by omitting the key. Every art slot is checked, wherever the note writes
|
|
1311
|
+
// it, because the rule belongs to the slots and not to one key.
|
|
1447
1312
|
//
|
|
1448
1313
|
// **A block key a type claims for something else is read past.**
|
|
1449
1314
|
// `topLevelMeans` settles which position the *emitted field* reads; it says
|
|
@@ -1452,29 +1317,6 @@ export function lintNote(
|
|
|
1452
1317
|
// of address would answer for a note's art, in twenty-eight
|
|
1453
1318
|
// `sohl-kethira-basic` affiliations. Hence `blockCollisions`.
|
|
1454
1319
|
|
|
1455
|
-
// The template priority is a *shared source* — the specification states it
|
|
1456
|
-
// once for every type, as it does `pack` — so its retirement is reported
|
|
1457
|
-
// here rather than by the per-type loop below, which only reaches a field
|
|
1458
|
-
// some type's vocabulary declares.
|
|
1459
|
-
if (declaresRetiredAlias(fm, "templatePriority")) {
|
|
1460
|
-
findings.push({
|
|
1461
|
-
file: note.file,
|
|
1462
|
-
...at(RETIRED_FIELD_ALIASES.templatePriority),
|
|
1463
|
-
// An error, unlike the other retired alias. `archetype` is not
|
|
1464
|
-
// a field of its own — it is `templatePriority` under its prior
|
|
1465
|
-
// name, and both sit one letter from `archetypes`, which means
|
|
1466
|
-
// something else entirely. A tree still on it is one where a
|
|
1467
|
-
// priority and a taxonomy are told apart by a plural `s`, which
|
|
1468
|
-
// is worth stopping rather than mentioning. This reds every
|
|
1469
|
-
// tree until each is swept; that is the point.
|
|
1470
|
-
severity: "error",
|
|
1471
|
-
message: retiredAliasMessage(
|
|
1472
|
-
RETIRED_FIELD_ALIASES.templatePriority,
|
|
1473
|
-
"templatePriority",
|
|
1474
|
-
),
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
1320
|
// An art field a note's own type never emits. `img` is a *shared
|
|
1479
1321
|
// top-level* field — `BLOCK_DOCUMENT_PROPERTIES` maps it onto
|
|
1480
1322
|
// `document.img`, so it is legal on every note whatever the type — and a
|
|
@@ -1500,12 +1342,11 @@ export function lintNote(
|
|
|
1500
1342
|
// state, and telling each of them to delete a key that already means
|
|
1501
1343
|
// nothing would bury the fifty-seven that name a path they believe ships.
|
|
1502
1344
|
//
|
|
1503
|
-
// A **warning
|
|
1504
|
-
// compiles correctly and the value is merely inert. Nor is it certainly
|
|
1345
|
+
// A **warning**: the note compiles correctly and the value is merely inert. Nor is it certainly
|
|
1505
1346
|
// unwanted — a note's top level is the generated page's front matter as
|
|
1506
1347
|
// well, so a template may read there what no document carries, which is a
|
|
1507
1348
|
// judgement only the tree's author can make.
|
|
1508
|
-
const emitted = emittedArt ? emittedArt(
|
|
1349
|
+
const emitted = emittedArt ? emittedArt(type) : null;
|
|
1509
1350
|
/** The art fields this note's type reaches nothing through. */
|
|
1510
1351
|
const inertArt = new Set(
|
|
1511
1352
|
emitted ?
|
|
@@ -1521,6 +1362,8 @@ export function lintNote(
|
|
|
1521
1362
|
blockCollides: blockCollisions.has(key),
|
|
1522
1363
|
});
|
|
1523
1364
|
if (typeof authored !== "string" && !isAddressTuple(authored)) continue;
|
|
1365
|
+
// An empty value has its own finding below; one finding per key.
|
|
1366
|
+
if (authored === "") continue;
|
|
1524
1367
|
findings.push({
|
|
1525
1368
|
file: note.file,
|
|
1526
1369
|
...at(key),
|
|
@@ -1549,6 +1392,8 @@ export function lintNote(
|
|
|
1549
1392
|
blockCollides: blockCollisions.has(key),
|
|
1550
1393
|
});
|
|
1551
1394
|
if (typeof authored !== "string" && !isAddressTuple(authored)) continue;
|
|
1395
|
+
// An empty value has its own finding below.
|
|
1396
|
+
if (authored === "") continue;
|
|
1552
1397
|
const problem = pathnameProblem(authored);
|
|
1553
1398
|
if (!problem) continue;
|
|
1554
1399
|
findings.push({
|
|
@@ -1560,70 +1405,20 @@ export function lintNote(
|
|
|
1560
1405
|
}
|
|
1561
1406
|
|
|
1562
1407
|
for (const { key, inData } of ART_FIELDS) {
|
|
1563
|
-
// Reported above, and the distinction this draws does not exist there:
|
|
1564
|
-
// where nothing is emitted, `""` and `null` are equally inert and the
|
|
1565
|
-
// note has no default art to lose.
|
|
1566
|
-
if (inertArt.has(key)) continue;
|
|
1567
1408
|
if (authoredValue(fm, key, { inData, blockCollides: blockCollisions.has(key) }) !== "")
|
|
1568
1409
|
continue;
|
|
1569
1410
|
findings.push({
|
|
1570
1411
|
file: note.file,
|
|
1571
1412
|
...at(key),
|
|
1572
|
-
severity: "warning",
|
|
1573
|
-
message:
|
|
1574
|
-
`\`${key}: ""\` means "ship no art at all" — it no longer falls ` +
|
|
1575
|
-
`back to this type's default. Write \`${key}: null\` for a note ` +
|
|
1576
|
-
'that simply names none; keep `""` only where the document is ' +
|
|
1577
|
-
"meant to have no image",
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
if (Object.hasOwn(fm, "draft")) {
|
|
1581
|
-
findings.push({
|
|
1582
|
-
file: note.file,
|
|
1583
|
-
...at("draft"),
|
|
1584
|
-
severity: "error",
|
|
1585
|
-
message: draftRetiredMessage(),
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
// Anchored at column 1 for the same reason `aliases` is: a nested
|
|
1589
|
-
// `section` under some other block is not this field.
|
|
1590
|
-
if (Object.hasOwn(fm, "section")) {
|
|
1591
|
-
findings.push({
|
|
1592
|
-
file: note.file,
|
|
1593
|
-
...positionInFrontmatter(raw(), "section", undefined, { topLevel: true }),
|
|
1594
|
-
severity: "error",
|
|
1595
|
-
message: sectionRetiredMessage(),
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
// Anchored at column 1 for the same reason `section` is, and with more at
|
|
1599
|
-
// stake: `sohl.traits` is a *different field that shares the name* —
|
|
1600
|
-
// `projectilegear` declares one and the theme's gear sidebar reads it — so
|
|
1601
|
-
// a finding about the retired top-level block must never open on it.
|
|
1602
|
-
if (Object.hasOwn(fm, "traits")) {
|
|
1603
|
-
findings.push({
|
|
1604
|
-
file: note.file,
|
|
1605
|
-
...positionInFrontmatter(raw(), "traits", undefined, { topLevel: true }),
|
|
1606
|
-
severity: "error",
|
|
1607
|
-
message: traitsRetiredMessage(),
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
// Only the top-level `aliases` is retired. `name.aliases` writes the same
|
|
1611
|
-
// key indented under `name:` and is **permitted** — reserved and unread —
|
|
1612
|
-
// so both the test and the locator are anchored at column 1.
|
|
1613
|
-
if (declaresRetiredAliasesField(fm)) {
|
|
1614
|
-
findings.push({
|
|
1615
|
-
file: note.file,
|
|
1616
|
-
...positionInFrontmatter(raw(), "aliases", undefined, { topLevel: true }),
|
|
1617
1413
|
severity: "error",
|
|
1618
|
-
message:
|
|
1414
|
+
message:
|
|
1415
|
+
`\`${key}: ""\` is not a valid art value — an empty value names no ` +
|
|
1416
|
+
`image. Remove the key to name none, or name an image`,
|
|
1619
1417
|
});
|
|
1620
1418
|
}
|
|
1621
|
-
|
|
1622
1419
|
// What the address rule says about a homepage's top-level fields: the
|
|
1623
|
-
// `shortcode` it owes, and the `id` it may not write.
|
|
1624
|
-
//
|
|
1625
|
-
// about the same note, and, like them, it must survive the two early
|
|
1626
|
-
// returns below: the finding stands whatever else the type is.
|
|
1420
|
+
// `shortcode` it owes, and the `id` it may not write. It must survive the
|
|
1421
|
+
// two early returns below: the finding stands whatever else the type is.
|
|
1627
1422
|
// Tags are checked here for the same reason: a classifying tag is not a
|
|
1628
1423
|
// type's property — `draft` belongs to any note and `village` to a place —
|
|
1629
1424
|
// so the finding must survive the early returns below.
|
|
@@ -1651,9 +1446,6 @@ export function lintNote(
|
|
|
1651
1446
|
|
|
1652
1447
|
for (const key of authoredNoteKeys(raw())) {
|
|
1653
1448
|
if (NOTE_TOP_LEVEL_KEY_SET.has(key)) continue;
|
|
1654
|
-
// Reported below, as the retiring position it is rather than a key
|
|
1655
|
-
// nobody recognises.
|
|
1656
|
-
if (retiringTopLevelKeys.has(key)) continue;
|
|
1657
1449
|
const position = positionInFrontmatter(raw(), key, undefined, { topLevel: true });
|
|
1658
1450
|
if (
|
|
1659
1451
|
findings.some(
|
|
@@ -1675,28 +1467,6 @@ export function lintNote(
|
|
|
1675
1467
|
});
|
|
1676
1468
|
}
|
|
1677
1469
|
|
|
1678
|
-
// `pack` and `packFolder` are declared for every type, so each is
|
|
1679
|
-
// checked once here rather than in the per-type `fields` loop below,
|
|
1680
|
-
// which only iterates a type's own declarations. A **warning**, for the
|
|
1681
|
-
// reason the per-type retiring position above is one: the note compiles
|
|
1682
|
-
// to the correct document either way, so failing a build over it would
|
|
1683
|
-
// red a tree that has done nothing wrong yet. Checked against every
|
|
1684
|
-
// configured system block, because `resolveDataProperty` reads a
|
|
1685
|
-
// system's own override before the top level, and a note may configure
|
|
1686
|
-
// more than one.
|
|
1687
|
-
for (const key of UNIVERSAL_RETIRING_DATA_KEYS) {
|
|
1688
|
-
const retiring = Object.keys(systems ?? {}).some(
|
|
1689
|
-
(blockName) => resolveDataProperty(fm, blockName, key).from === "topLevel",
|
|
1690
|
-
);
|
|
1691
|
-
if (!retiring) continue;
|
|
1692
|
-
findings.push({
|
|
1693
|
-
file: note.file,
|
|
1694
|
-
...at(key),
|
|
1695
|
-
severity: "warning",
|
|
1696
|
-
message: retiredTopLevelMessage({ name: `data.${key}` }),
|
|
1697
|
-
});
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
1470
|
// The type's charset, before anything that looks the type up. A
|
|
1701
1471
|
// hyphenated type is unaddressable, and every lookup below would report it
|
|
1702
1472
|
// as a type nobody declared — true, but not the reason, and it would send
|
|
@@ -1716,41 +1486,7 @@ export function lintNote(
|
|
|
1716
1486
|
return findings;
|
|
1717
1487
|
}
|
|
1718
1488
|
|
|
1719
|
-
const
|
|
1720
|
-
if (replacement) {
|
|
1721
|
-
findings.push({
|
|
1722
|
-
file: note.file,
|
|
1723
|
-
...at("type", type),
|
|
1724
|
-
severity: "error",
|
|
1725
|
-
message:
|
|
1726
|
-
`content type "${type}" was retired in favour of ` +
|
|
1727
|
-
`"${replacement}"; both compiled to the same document, so the ` +
|
|
1728
|
-
`fix is mechanical: write "${replacement}"`,
|
|
1729
|
-
});
|
|
1730
|
-
return findings;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// A **renamed** type is the opposite case, and the opposite answer: the
|
|
1734
|
-
// note compiles into exactly the document it always did, so refusing it
|
|
1735
|
-
// would fail a build over a note that is not wrong. It is reported, and
|
|
1736
|
-
// every lookup below reads the current spelling.
|
|
1737
|
-
const renamedTo = RENAMED_TYPES[type];
|
|
1738
|
-
if (renamedTo) {
|
|
1739
|
-
findings.push({
|
|
1740
|
-
file: note.file,
|
|
1741
|
-
...at("type", type),
|
|
1742
|
-
// A warning, for the reason the retired *field* alias below is one:
|
|
1743
|
-
// the sweep is the content trees' work and the refusal comes after
|
|
1744
|
-
// it, as `package:`'s did.
|
|
1745
|
-
severity: "warning",
|
|
1746
|
-
message: renamedTypeMessage(type, renamedTo),
|
|
1747
|
-
});
|
|
1748
|
-
}
|
|
1749
|
-
// What every type-keyed table is keyed by. The authored spelling is still
|
|
1750
|
-
// what a message quotes — it is what the reader has in front of them.
|
|
1751
|
-
const current = currentType(type);
|
|
1752
|
-
|
|
1753
|
-
const schema = schemas[current];
|
|
1489
|
+
const schema = schemas[type];
|
|
1754
1490
|
if (!schema) {
|
|
1755
1491
|
findings.push({
|
|
1756
1492
|
file: note.file,
|
|
@@ -1768,9 +1504,9 @@ export function lintNote(
|
|
|
1768
1504
|
// one this type may write. Skipped entirely when the caller declares no
|
|
1769
1505
|
// vocabulary — reporting every key as unknown because nothing was loaded
|
|
1770
1506
|
// to recognise it would be worse than not checking.
|
|
1771
|
-
const entry = vocabulary?.[
|
|
1507
|
+
const entry = vocabulary?.[type];
|
|
1772
1508
|
if (entry) {
|
|
1773
|
-
const fields = dataFields(
|
|
1509
|
+
const fields = dataFields(type, vocabulary) ?? [];
|
|
1774
1510
|
findings.push(
|
|
1775
1511
|
...checkDataContainer(note, {
|
|
1776
1512
|
type,
|
|
@@ -1801,33 +1537,6 @@ export function lintNote(
|
|
|
1801
1537
|
/** The keys this type's own declaration is authored at inside a block. */
|
|
1802
1538
|
const declared = inBlockKeys(schema);
|
|
1803
1539
|
|
|
1804
|
-
// The retired spelling of a field this type declares → what to write now.
|
|
1805
|
-
// Built from the type's own vocabulary, so a renamed field is retired
|
|
1806
|
-
// exactly where its replacement exists and the old name stays an unknown
|
|
1807
|
-
// key everywhere else.
|
|
1808
|
-
const renamed = new Map();
|
|
1809
|
-
for (const name of declared) {
|
|
1810
|
-
const retired = RETIRED_FIELD_ALIASES[name];
|
|
1811
|
-
if (retired) renamed.set(retired, name);
|
|
1812
|
-
}
|
|
1813
|
-
for (const [retired, current] of renamed) {
|
|
1814
|
-
// `declaresRetiredAlias` searches both regions, because both are read:
|
|
1815
|
-
// a note that moved the key to the top level without renaming it has
|
|
1816
|
-
// done half the migration. It is the same predicate the compile-time
|
|
1817
|
-
// report asks, so the two cannot disagree about what a note declares.
|
|
1818
|
-
if (!declaresRetiredAlias(fm, current)) continue;
|
|
1819
|
-
findings.push({
|
|
1820
|
-
file: note.file,
|
|
1821
|
-
...at(retired),
|
|
1822
|
-
// A warning, not an error: the note compiles to the correct
|
|
1823
|
-
// document, so failing a build over it would red a tree that has
|
|
1824
|
-
// done nothing wrong yet. The refusal comes after the sweep, as
|
|
1825
|
-
// `package:`'s did.
|
|
1826
|
-
severity: "warning",
|
|
1827
|
-
message: retiredAliasMessage(retired, current),
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
1540
|
// Every declared system's block, each against its own vocabulary. A
|
|
1832
1541
|
// block carries the shared keys any system's does — `system`, `type`,
|
|
1833
1542
|
// `img`, `effects`, `flags`, `pack` — plus whatever that system declares:
|
|
@@ -1859,12 +1568,29 @@ export function lintNote(
|
|
|
1859
1568
|
...(spec?.fieldVocabulary ? declared : []),
|
|
1860
1569
|
...(own ? inBlockKeys(own[type]) : []),
|
|
1861
1570
|
]);
|
|
1571
|
+
// A field whose home is `data:`, written in the block instead. The
|
|
1572
|
+
// block does not read it, and it may share a name with a field the
|
|
1573
|
+
// block does read, so it is named here rather than left to the
|
|
1574
|
+
// unknown-key finding below.
|
|
1575
|
+
const misplaced = new Set(
|
|
1576
|
+
[...(spec?.fieldVocabulary ? fields : []), ...authoredFields(own?.[type] ?? [])]
|
|
1577
|
+
.filter((field) => field.name.startsWith("data."))
|
|
1578
|
+
.map((field) => field.name.slice("data.".length).split(".")[0]),
|
|
1579
|
+
);
|
|
1580
|
+
const block = systemBlock(fm, blockName);
|
|
1581
|
+
for (const key of Object.keys(block ?? {})) {
|
|
1582
|
+
if (!misplaced.has(key)) continue;
|
|
1583
|
+
findings.push({
|
|
1584
|
+
file: note.file,
|
|
1585
|
+
...positionOfFrontmatterPath(raw(), [blockName, key], { key: true }),
|
|
1586
|
+
severity: "error",
|
|
1587
|
+
message:
|
|
1588
|
+
`\`${blockName}.${key}\` is a \`data:\` property of a ${type}; ` +
|
|
1589
|
+
`write it at \`data.${key}\` — the block does not read it`,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1862
1592
|
for (const key of unknownBlockKeys(fm, blockName, { known: accepted })) {
|
|
1863
|
-
|
|
1864
|
-
// is a rename to schedule, not a key nobody recognises. Only where
|
|
1865
|
-
// the block's vocabulary is this type's: the alias renames *this*
|
|
1866
|
-
// system's field, and another system's like-spelled key is not it.
|
|
1867
|
-
if (spec?.fieldVocabulary && renamed.has(key)) continue;
|
|
1593
|
+
if (misplaced.has(key)) continue;
|
|
1868
1594
|
const guess = nearest(key, [...accepted, ...SYSTEM_BLOCK_KEYS]);
|
|
1869
1595
|
findings.push({
|
|
1870
1596
|
file: note.file,
|
|
@@ -1890,42 +1616,7 @@ export function lintNote(
|
|
|
1890
1616
|
// the moment it moved to another of them. A **shared** field needs
|
|
1891
1617
|
// nothing extra here — the note's top level *is* the third step — so
|
|
1892
1618
|
// `shared` says only where the field's home is, for the message below.
|
|
1893
|
-
|
|
1894
|
-
// A **renamed** field may still be written under its retired spelling,
|
|
1895
|
-
// which that order knows nothing about. It resolves through the reader
|
|
1896
|
-
// the compiler uses, so the lint cannot disagree with the build about
|
|
1897
|
-
// which value a note carries.
|
|
1898
|
-
if ((from === "default" || value == null) && RETIRED_FIELD_ALIASES[field.name]) {
|
|
1899
|
-
const aliased = readAliasedField(fm, field.name);
|
|
1900
|
-
if (aliased !== undefined) {
|
|
1901
|
-
value = aliased;
|
|
1902
|
-
from = "block";
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
// The sweep's progress signal. A **warning**, for the reason a
|
|
1906
|
-
// retired spelling is one: the note compiles to the correct document,
|
|
1907
|
-
// so failing a build over it would red a tree that has done nothing
|
|
1908
|
-
// wrong yet. The refusal comes once no tree writes the position.
|
|
1909
|
-
if (readsLegacyKey(field, from)) {
|
|
1910
|
-
findings.push({
|
|
1911
|
-
file: note.file,
|
|
1912
|
-
...at(legacyKeyOf(field)),
|
|
1913
|
-
severity: "warning",
|
|
1914
|
-
message: legacyKeyMessage("sohl", field),
|
|
1915
|
-
});
|
|
1916
|
-
}
|
|
1917
|
-
// `readsLegacyKey`'s sibling for the other retiring position — the
|
|
1918
|
-
// note's own top level, which `data:` gathered the fact off. Also a
|
|
1919
|
-
// **warning**: the closed-region check above already let this exact
|
|
1920
|
-
// key through for this exact reason, and the two must agree.
|
|
1921
|
-
if (readsRetiredTopLevel(field, from)) {
|
|
1922
|
-
findings.push({
|
|
1923
|
-
file: note.file,
|
|
1924
|
-
...at(retiredTopLevelKey(field)),
|
|
1925
|
-
severity: "warning",
|
|
1926
|
-
message: retiredTopLevelMessage(field),
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1619
|
+
const { value, from } = resolveFieldValue(field, fm, { block: "sohl" });
|
|
1929
1620
|
const absent = from === "default" || value === undefined || value === null;
|
|
1930
1621
|
// Where the field belongs, as a message names it: a shared field is not
|
|
1931
1622
|
// under `sohl:`, so telling an author to write `sohl.img` would send
|
|
@@ -1933,7 +1624,7 @@ export function lintNote(
|
|
|
1933
1624
|
// path into `data:` — `sohl.data.species` is a region that does not
|
|
1934
1625
|
// exist, and the home of that field is the container it names.
|
|
1935
1626
|
const label =
|
|
1936
|
-
field.shared ||
|
|
1627
|
+
field.shared || field.name.startsWith("data.") ?
|
|
1937
1628
|
`\`${field.name}\``
|
|
1938
1629
|
: `\`sohl.${field.name}\``;
|
|
1939
1630
|
|