@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
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
-
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
-
*
|
|
5
|
-
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
-
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
-
*
|
|
8
|
-
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
-
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
-
*
|
|
11
|
-
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Frontmatter fields a note may no longer declare.
|
|
16
|
-
*
|
|
17
|
-
* A retired field has exactly two possible fates, and only one of them is
|
|
18
|
-
* honest. Left honoured, it keeps doing whatever it did — which is why it was
|
|
19
|
-
* retired. Left *ignored*, it reads to its author as though it still works: the
|
|
20
|
-
* note says one thing and the build does another, and nothing says so. So a
|
|
21
|
-
* retired field is **refused**, naming the file and the line, and the message
|
|
22
|
-
* says what to write instead rather than which value to correct.
|
|
23
|
-
*
|
|
24
|
-
* `package:` is retired the same way and is refused from `note-package.mjs`,
|
|
25
|
-
* where the concept it belonged to still lives. `draft:`, the top-level
|
|
26
|
-
* `aliases:`, `section:` and `traits:` have no such home — there is no
|
|
27
|
-
* surviving concept any of them was part of — so they are refused here.
|
|
28
|
-
*
|
|
29
|
-
* **What `draft:` did.** It excluded a note from the compiled packs, from
|
|
30
|
-
* the link manifest and from a consuming site build. Nothing reported the
|
|
31
|
-
* consequence: `content-links.mjs`, `site-index.mjs` and `content-lint.mjs`
|
|
32
|
-
* never read the field, so a link into a drafted note was indistinguishable
|
|
33
|
-
* from a link to a note that does not exist, and the checkers could not say
|
|
34
|
-
* which. The field's entire effect was to move a note from *published* to
|
|
35
|
-
* *unresolvable*, silently — and it also suppressed real build failures, since
|
|
36
|
-
* a note the compilers never reached could not fail on the defects it carried.
|
|
37
|
-
*
|
|
38
|
-
* **What `aliases:` did.** It fed the alias index, which is what a bare
|
|
39
|
-
* `[[Alias]]` was looked up in. That form resolved to nothing anywhere in the
|
|
40
|
-
* corpus, while the collision rule guarding it folded in every note's
|
|
41
|
-
* `name.full` and so decided what a note could be named. The form and
|
|
42
|
-
* the index are retired together, leaving the field with no reader at all.
|
|
43
|
-
*
|
|
44
|
-
* **What `section:` did.** It named the section a `collection` note
|
|
45
|
-
* headed, under the `collection` landing rule — its only reader anywhere. That
|
|
46
|
-
* rule is retired, a section being landed by the `README.md` in its directory,
|
|
47
|
-
* so the field has none. No schema or vocabulary ever declared it either, and
|
|
48
|
-
* nothing checks unrecognized top-level keys, so left in place it would be
|
|
49
|
-
* silently ignored rather than reported.
|
|
50
|
-
*
|
|
51
|
-
* **`name.aliases` lists alternate names.** The note infobox shows them in
|
|
52
|
-
* authored order. They do not create addresses or change link resolution.
|
|
53
|
-
*
|
|
54
|
-
* **What `traits:` did.** It held a being's own description — gender,
|
|
55
|
-
* species, age, birth date, height, weight, frame and `appearance.*` — at the
|
|
56
|
-
* note's top level. The content format gives those a home: `data:`, the closed
|
|
57
|
-
* container for a subject's type-specific facts, which `being` declares every
|
|
58
|
-
* one of them in. Every note that carried one has been moved, across four
|
|
59
|
-
* repositories, and this is the third step of that retirement.
|
|
60
|
-
*
|
|
61
|
-
* Refusing it by name rather than as an unknown key is what names the home the
|
|
62
|
-
* facts have: the finding says which `data:` key each one becomes, which an
|
|
63
|
-
* author cannot read off a list of the keys the region declares.
|
|
64
|
-
*
|
|
65
|
-
* `sohl.traits` is a different field that shares the name — `projectilegear`
|
|
66
|
-
* declares one, and the theme's gear sidebar reads it — so the refusal is
|
|
67
|
-
* anchored at column 1 and never reaches inside a system block.
|
|
68
|
-
*
|
|
69
|
-
* **A field retired in favour of another is a third case.** `draft:` and
|
|
70
|
-
* `package:` were retired outright: nothing replaced them, so no value made
|
|
71
|
-
* writing one right and refusal was the only honest answer. A *renamed* field
|
|
72
|
-
* has a replacement, and the two spellings mean the same thing — so the note
|
|
73
|
-
* still compiles, correctly, and refusing it would fail a build over a document
|
|
74
|
-
* that is not wrong. Those retire in the three steps `package:` took, and
|
|
75
|
-
* this module carries the **first**: both spellings are read, the current one
|
|
76
|
-
* wins, and the retired one is *reported* rather than refused. See
|
|
77
|
-
* {@link RETIRED_FIELD_ALIASES}.
|
|
78
|
-
*
|
|
79
|
-
* **The third step is deletion, and it needs no code.** `image`, a map's
|
|
80
|
-
* background art, illustrates all three states: reported,
|
|
81
|
-
* swept, then dropped from the
|
|
82
|
-
* table. Removing the entry is the whole of it — with no alias, the spelling is
|
|
83
|
-
* an ordinary unknown key in the `sohl:` block, which the frontmatter lint
|
|
84
|
-
* already refuses as an error alongside the required field it failed to supply.
|
|
85
|
-
* So nothing here *records* a finished retirement: the absence of an entry is
|
|
86
|
-
* the record, and a retirement that needed a standing refusal would mean the
|
|
87
|
-
* replacement never arrived. Do not keep a tombstone for one.
|
|
88
|
-
*
|
|
89
|
-
* **A retired *position* is the same case, and reads the same.** A field
|
|
90
|
-
* whose shared source moved under `data:` is not renamed — `data.species` and
|
|
91
|
-
* `hm3.species` are one field written in two places — but the retirement has
|
|
92
|
-
* the identical shape: both are read, the one the note carries wins, and the
|
|
93
|
-
* legacy in-block key is reported by {@link legacyKeyMessage} so the sweep has
|
|
94
|
-
* something to count down. Which position a value came from is
|
|
95
|
-
* {@link module:engine/system-block.resolveFieldValue}'s answer; this module
|
|
96
|
-
* only says what an author is told about it.
|
|
97
|
-
*
|
|
98
|
-
* **A field has two retiring positions, not one.** The in-block key is
|
|
99
|
-
* the obvious one; the other is the note's **top level**, because the sweep
|
|
100
|
-
* did not
|
|
101
|
-
* invent the facts `data:` holds — it gathered them from exactly there. So
|
|
102
|
-
* `portrait:` beside `img:` is the pre-`data:` spelling of `data.portrait`,
|
|
103
|
-
* read for the same reason and reported by {@link retiredTopLevelMessage}. Both
|
|
104
|
-
* are needed, and separately: a note may have moved one and not the other, and
|
|
105
|
-
* a single finding covering both would name the wrong line half the time.
|
|
106
|
-
*
|
|
107
|
-
* **A `data:` field's retired spelling is written in `data:`**, and nowhere
|
|
108
|
-
* else. The container is closed, so a key in it is either one the type declares
|
|
109
|
-
* or one refused by name — which makes the retired spelling of a declared key
|
|
110
|
-
* an error the moment the replacement lands, unless the container check knows
|
|
111
|
-
* to read it as a rename. {@link declaresRetiredAlias} and
|
|
112
|
-
* {@link readAliasedField} therefore take `inData`, and search that one region
|
|
113
|
-
* rather than the block and the top level a system field is written in.
|
|
114
|
-
*
|
|
115
|
-
* @module
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
import fs from "node:fs";
|
|
119
|
-
|
|
120
|
-
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
121
|
-
import { getFrontmatter, sohlField } from "./frontmatter.mjs";
|
|
122
|
-
import { retiredTopLevelKey } from "./system-block.mjs";
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* What a note declaring `draft:` is told, in one place.
|
|
126
|
-
*
|
|
127
|
-
* Written once and shared by the compile-time refusal and the frontmatter lint,
|
|
128
|
-
* because an author meets whichever of the two runs first and they should read
|
|
129
|
-
* the same. It says what the field did and what to write instead, rather than
|
|
130
|
-
* which value to correct: no value makes declaring it right.
|
|
131
|
-
*
|
|
132
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
133
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
134
|
-
* line — repeating it prints the path twice.
|
|
135
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
136
|
-
*/
|
|
137
|
-
export function draftRetiredMessage(file) {
|
|
138
|
-
return (
|
|
139
|
-
"`draft:` is a retired frontmatter field — delete it" +
|
|
140
|
-
(file ? ` — ${file}` : "") +
|
|
141
|
-
". It excluded the note from the compiled packs, the link manifest " +
|
|
142
|
-
"and the site, and no checker reported the exclusion, so every " +
|
|
143
|
-
"wikilink into it read as a link to a note that does not exist. To " +
|
|
144
|
-
"mark a note as unfinished, tag it `#draft` instead: the note still " +
|
|
145
|
-
"compiles and publishes, a link into it renders marked, and a " +
|
|
146
|
-
"`FROM #draft` query still finds it"
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Refuse a note that declares `draft:` at all.
|
|
152
|
-
*
|
|
153
|
-
* Presence is the whole test. `draft: false` is as retired as `draft: true` —
|
|
154
|
-
* it reads as "publish this note", which is what happens either way, and is
|
|
155
|
-
* exactly the belief the message exists to correct.
|
|
156
|
-
*
|
|
157
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
158
|
-
* could not be parsed.
|
|
159
|
-
* @param {object} [options] - Options.
|
|
160
|
-
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
161
|
-
* it where the caller emits through a diagnostic, which puts the locator at
|
|
162
|
-
* the start of the line already — repeating it prints the path twice.
|
|
163
|
-
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
164
|
-
* failing path to locate the offending line and column. The position rides on
|
|
165
|
-
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
166
|
-
* @returns {void}
|
|
167
|
-
* @throws {Error} When the note declares the field.
|
|
168
|
-
*/
|
|
169
|
-
export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
170
|
-
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "draft")) return;
|
|
171
|
-
|
|
172
|
-
const err = new Error(`${draftRetiredMessage(file)}.`);
|
|
173
|
-
// Where the field is, so the caller's diagnostic opens on the line that has
|
|
174
|
-
// to be deleted. Read here rather than carried through every walk: this is
|
|
175
|
-
// the failing path, and the build stops on it.
|
|
176
|
-
const position = locateFrontmatterKey(absPath, "draft");
|
|
177
|
-
if (position) err.position = position;
|
|
178
|
-
throw err;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* What a note declaring a top-level `aliases:` is told, in one place.
|
|
183
|
-
*
|
|
184
|
-
* Shared by the compile-time refusal and the frontmatter lint, because an
|
|
185
|
-
* author meets whichever of the two runs first and they should read the same.
|
|
186
|
-
* It says what the field fed and what to write instead, rather than which value
|
|
187
|
-
* to correct: no value makes declaring it right.
|
|
188
|
-
*
|
|
189
|
-
* **What it did.** It was the authored half of the alias index — the
|
|
190
|
-
* namespace a bare `[[Alias]]` was looked up in. Across the three content trees
|
|
191
|
-
* not one bare link resolved through it, while the collision rule that kept it
|
|
192
|
-
* unambiguous folded in every note's `name.full` and so dictated what a note
|
|
193
|
-
* could be named. The form is retired, so the list has no reader.
|
|
194
|
-
*
|
|
195
|
-
* **`name.aliases` is a different field.** It lists alternate names in the
|
|
196
|
-
* infobox and is deliberately unmentioned by this message. See
|
|
197
|
-
* {@link assertNoAliasesField}.
|
|
198
|
-
*
|
|
199
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
200
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
201
|
-
* line — repeating it prints the path twice.
|
|
202
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
203
|
-
*/
|
|
204
|
-
export function aliasesRetiredMessage(file) {
|
|
205
|
-
return (
|
|
206
|
-
"`aliases:` is a retired frontmatter field — delete it" +
|
|
207
|
-
(file ? ` — ${file}` : "") +
|
|
208
|
-
". It listed names the bare `[[Alias]]` form could cite, and that " +
|
|
209
|
-
"form is retired: every wikilink is now an address, written " +
|
|
210
|
-
"`[[type-shortcode|Text]]`. Nothing else ever read the list"
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Refuse a note that declares a top-level `aliases:`.
|
|
216
|
-
*
|
|
217
|
-
* Presence is the whole test. `aliases: []` is as retired as a populated one —
|
|
218
|
-
* it reads as "this note claims no other names", a statement about a namespace
|
|
219
|
-
* that no longer exists.
|
|
220
|
-
*
|
|
221
|
-
* **The nested `name.aliases` is deliberately not refused.** It is the note's
|
|
222
|
-
* ordered alternate names and appears in the note infobox. It does not create
|
|
223
|
-
* addresses or change how authored links resolve.
|
|
224
|
-
*
|
|
225
|
-
* That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
|
|
226
|
-
* rather than resolving a dotted key: the top-level field is the whole subject,
|
|
227
|
-
* and reaching into `name` at all is the thing being avoided.
|
|
228
|
-
*
|
|
229
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
230
|
-
* could not be parsed.
|
|
231
|
-
* @param {object} [options] - Options.
|
|
232
|
-
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
233
|
-
* it where the caller emits through a diagnostic, which puts the locator at
|
|
234
|
-
* the start of the line already — repeating it prints the path twice.
|
|
235
|
-
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
236
|
-
* failing path to locate the offending line and column. The position rides on
|
|
237
|
-
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
238
|
-
* @returns {void}
|
|
239
|
-
* @throws {Error} When the note declares a top-level `aliases`.
|
|
240
|
-
*/
|
|
241
|
-
export function assertNoAliasesField(fm, { file, absPath } = {}) {
|
|
242
|
-
if (!declaresRetiredAliasesField(fm)) return;
|
|
243
|
-
|
|
244
|
-
const err = new Error(`${aliasesRetiredMessage(file)}.`);
|
|
245
|
-
// Anchored at column 1: a permitted `name.aliases` writes the same key,
|
|
246
|
-
// indented, and a finding about the retired top-level field must never
|
|
247
|
-
// open on it.
|
|
248
|
-
const position = locateFrontmatterKey(absPath, "aliases", undefined, { topLevel: true });
|
|
249
|
-
if (position) err.position = position;
|
|
250
|
-
throw err;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Whether a note declares the retired top-level `aliases:`.
|
|
255
|
-
*
|
|
256
|
-
* A nested `name.aliases` is **not** this field and never answers true here —
|
|
257
|
-
* see {@link assertNoAliasesField} for why the two part company.
|
|
258
|
-
*
|
|
259
|
-
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
260
|
-
* @returns {boolean} Whether the retired field is declared.
|
|
261
|
-
*/
|
|
262
|
-
export function declaresRetiredAliasesField(fm) {
|
|
263
|
-
return Boolean(fm) && typeof fm === "object" && Object.hasOwn(fm, "aliases");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* What a note declaring `section:` is told, in one place.
|
|
268
|
-
*
|
|
269
|
-
* Shared by the compile-time refusal and the frontmatter lint, because an
|
|
270
|
-
* author meets whichever of the two runs first and they should read the same.
|
|
271
|
-
* It names what lands a section now rather than a value to correct: no value
|
|
272
|
-
* makes declaring the field right.
|
|
273
|
-
*
|
|
274
|
-
* **What it did.** It named the section a `collection` note headed,
|
|
275
|
-
* under the `collection` landing rule — the only reader it ever had, in the
|
|
276
|
-
* second branch of `landingOf` (`engine/content-address.mjs`). That rule went
|
|
277
|
-
* first, and the whole mechanism went with it: a section is a Hugo
|
|
278
|
-
* directory the note format does not carry, so no note lands one and a page
|
|
279
|
-
* that introduces a type is an ordinary note addressed `doc-<type>`. Nothing
|
|
280
|
-
* else read the field, and no schema or vocabulary declared it, so left in
|
|
281
|
-
* place it would be ignored in silence — the note saying one thing and the
|
|
282
|
-
* build doing another.
|
|
283
|
-
*
|
|
284
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
285
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
286
|
-
* line — repeating it prints the path twice.
|
|
287
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
288
|
-
*/
|
|
289
|
-
export function sectionRetiredMessage(file) {
|
|
290
|
-
return (
|
|
291
|
-
"`section:` is a retired frontmatter field — delete it" +
|
|
292
|
-
(file ? ` — ${file}` : "") +
|
|
293
|
-
". It named the section a `collection` note headed, and both the rule " +
|
|
294
|
-
"and the sections it routed to are retired: a page that introduces " +
|
|
295
|
-
"the notes of a type is an ordinary note — `type: doc`, " +
|
|
296
|
-
"`subType: reference`, `shortcode: <type>` — addressed `doc-<type>`. " +
|
|
297
|
-
"Nothing else ever read the field"
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Refuse a note that declares `section:` at all.
|
|
303
|
-
*
|
|
304
|
-
* Presence is the whole test, as it is for `draft:` and `aliases:`: an empty
|
|
305
|
-
* value reads as "this note heads a section and names none", a statement about
|
|
306
|
-
* a rule that no longer exists.
|
|
307
|
-
*
|
|
308
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
309
|
-
* could not be parsed.
|
|
310
|
-
* @param {object} [options] - Options.
|
|
311
|
-
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
312
|
-
* it where the caller emits through a diagnostic, which puts the locator at
|
|
313
|
-
* the start of the line already — repeating it prints the path twice.
|
|
314
|
-
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
315
|
-
* failing path to locate the offending line and column. The position rides on
|
|
316
|
-
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
317
|
-
* @returns {void}
|
|
318
|
-
* @throws {Error} When the note declares the field.
|
|
319
|
-
*/
|
|
320
|
-
export function assertNoSectionField(fm, { file, absPath } = {}) {
|
|
321
|
-
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "section")) return;
|
|
322
|
-
|
|
323
|
-
const err = new Error(`${sectionRetiredMessage(file)}.`);
|
|
324
|
-
// Anchored at column 1: a nested `section:` inside some other block is
|
|
325
|
-
// not this field — a finding about the top-level one must not open on it.
|
|
326
|
-
const position = locateFrontmatterKey(absPath, "section", undefined, { topLevel: true });
|
|
327
|
-
if (position) err.position = position;
|
|
328
|
-
throw err;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* What a note writing a top-level `traits:` block is told.
|
|
333
|
-
*
|
|
334
|
-
* The message states the **mapping**, not just the destination, because three
|
|
335
|
-
* of the keys reshaped as well as moved: the block nested its measurements
|
|
336
|
-
* where the format flattens them. A bare "write `data:` instead" would send an
|
|
337
|
-
* author to author `data.height: {m: 1.78}`, which is a declared key holding an
|
|
338
|
-
* undeclared shape.
|
|
339
|
-
*
|
|
340
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
341
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
342
|
-
* line — repeating it prints the path twice.
|
|
343
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
344
|
-
*/
|
|
345
|
-
export function traitsRetiredMessage(file) {
|
|
346
|
-
return (
|
|
347
|
-
"`traits:` is a retired frontmatter block — move it into `data:`" +
|
|
348
|
-
(file ? ` — ${file}` : "") +
|
|
349
|
-
". A being's own description belongs in the closed container the " +
|
|
350
|
-
"content format declares, where `being` declares every one of its " +
|
|
351
|
-
"keys. " +
|
|
352
|
-
"`gender`, `species`, `age` and `appearance.*` move unchanged; " +
|
|
353
|
-
"`traits.birthday` becomes `data.born`; three reshape — " +
|
|
354
|
-
"`traits.height.m` becomes `data.height` " +
|
|
355
|
-
"(metres), `traits.weight.kg` becomes `data.weight` (kilograms), and " +
|
|
356
|
-
"`traits.build.frame` becomes `data.frame`"
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Refuse a note that declares a top-level `traits:` block at all.
|
|
362
|
-
*
|
|
363
|
-
* Presence is the whole test, as it is for `draft:` and `aliases:`: an empty
|
|
364
|
-
* `traits:` is still a note claiming a block that no longer exists.
|
|
365
|
-
*
|
|
366
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
367
|
-
* could not be parsed.
|
|
368
|
-
* @param {object} [options] - Options.
|
|
369
|
-
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
370
|
-
* it where the caller emits through a diagnostic, which puts the locator at
|
|
371
|
-
* the start of the line already.
|
|
372
|
-
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
373
|
-
* failing path to locate the offending line and column. The position rides on
|
|
374
|
-
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
375
|
-
* @returns {void}
|
|
376
|
-
* @throws {Error} When the note declares the block.
|
|
377
|
-
*/
|
|
378
|
-
export function assertNoTraitsField(fm, { file, absPath } = {}) {
|
|
379
|
-
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "traits")) return;
|
|
380
|
-
|
|
381
|
-
const err = new Error(`${traitsRetiredMessage(file)}.`);
|
|
382
|
-
// Anchored at column 1. `sohl.traits` is a *different field that shares the
|
|
383
|
-
// name* — `projectilegear` declares one and the theme's gear sidebar reads
|
|
384
|
-
// it — so a finding about the top-level block must never open on it.
|
|
385
|
-
const position = locateFrontmatterKey(absPath, "traits", undefined, { topLevel: true });
|
|
386
|
-
if (position) err.position = position;
|
|
387
|
-
throw err;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* A frontmatter key's position in a note's file, or nothing.
|
|
392
|
-
*
|
|
393
|
-
* {@link positionInFrontmatter} answers the same question from the file's
|
|
394
|
-
* *text*; this reads the file to ask it. Kept apart from either caller because
|
|
395
|
-
* both refusals need it and a second copy is a second thing to keep correct.
|
|
396
|
-
*
|
|
397
|
-
* @param {string|undefined} absPath - The note's file.
|
|
398
|
-
* @param {string} key - The frontmatter key.
|
|
399
|
-
* @param {string} [value] - When given, prefer the occurrence whose line also
|
|
400
|
-
* carries this text — so a finding about one entry of a block opens on that
|
|
401
|
-
* entry rather than on the key that introduces it.
|
|
402
|
-
* @param {object} [options] - Options, forwarded to
|
|
403
|
-
* {@link positionInFrontmatter}.
|
|
404
|
-
* @param {boolean} [options.topLevel=false] - Require the key at column 1, so
|
|
405
|
-
* an identically named nested key cannot answer for it.
|
|
406
|
-
* @returns {{line?: number, column?: number}|undefined} Spreadable position
|
|
407
|
-
* fields, dropped rather than guessed when the file cannot be read or the key
|
|
408
|
-
* cannot be found — as `formatDiagnostic` requires.
|
|
409
|
-
*/
|
|
410
|
-
export function locateFrontmatterKey(absPath, key, value = undefined, { topLevel = false } = {}) {
|
|
411
|
-
if (!absPath) return undefined;
|
|
412
|
-
let raw;
|
|
413
|
-
try {
|
|
414
|
-
raw = fs.readFileSync(absPath, "utf8");
|
|
415
|
-
} catch {
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
const at = positionInFrontmatter(raw, key, value, { topLevel });
|
|
419
|
-
return at.line === undefined ? undefined : at;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/* -------------------------------------------------------------------- */
|
|
423
|
-
/* Retired *in favour of another field* */
|
|
424
|
-
/* -------------------------------------------------------------------- */
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* The current field name a retired spelling was renamed to.
|
|
428
|
-
*
|
|
429
|
-
* Keyed by the **current** name, because that is what a type's schema declares
|
|
430
|
-
* and what every reader asks for; the value is the spelling still honoured.
|
|
431
|
-
* The table is therefore scoped by the schema without saying so twice: an alias
|
|
432
|
-
* applies to a note only where that note's type declares the current field, so
|
|
433
|
-
* `relation` is retired on an affiliation — which declares `relations` — and
|
|
434
|
-
* remains an unknown key anywhere else.
|
|
435
|
-
*
|
|
436
|
-
* **`templatePriority`.** The number that decides which of several
|
|
437
|
-
* competing templates the Create dialog offers was called `archetype`, and
|
|
438
|
-
* `archetypes` — one letter apart — is a list of what *sort* a character is.
|
|
439
|
-
* A priority and a taxonomy cannot be told apart by a plural `s`, so the
|
|
440
|
-
* priority takes the name that says what it is. The specification already
|
|
441
|
-
* called it `templatePriority` on two of its three sides.
|
|
442
|
-
*
|
|
443
|
-
* **`relations`.** The field holds a *map* of standings, one per
|
|
444
|
-
* affiliation — its own description says so, and `resolveRelation` has always
|
|
445
|
-
* read it that way. `relation` named the many as one, which every author had to
|
|
446
|
-
* read past. Only `affiliation` declares the field, so the alias is reported
|
|
447
|
-
* there and the old spelling stays an ordinary unknown key everywhere else.
|
|
448
|
-
*
|
|
449
|
-
* **`born`.** A birthday is a day that recurs; what a note records is the date
|
|
450
|
-
* somebody was born, once. The two read alike while every value belonged to a
|
|
451
|
-
* living person, and `born` is what the field has to be called beside `died`,
|
|
452
|
-
* which a being now also states. It is the first alias whose field lives in
|
|
453
|
-
* `data:` rather than in a system block, so the two readers below take
|
|
454
|
-
* `inData`.
|
|
455
|
-
*
|
|
456
|
-
* @type {Readonly<Record<string, string>>}
|
|
457
|
-
*/
|
|
458
|
-
export const RETIRED_FIELD_ALIASES = Object.freeze({
|
|
459
|
-
templatePriority: "archetype",
|
|
460
|
-
relations: "relation",
|
|
461
|
-
born: "birthday",
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* What a note writing a renamed field is told, in one place.
|
|
466
|
-
*
|
|
467
|
-
* Shared by the compile-time report and the frontmatter lint, because an author
|
|
468
|
-
* meets whichever runs first and they should read the same. It names the key to
|
|
469
|
-
* write rather than a value to correct — no value makes the retired spelling
|
|
470
|
-
* right — and it says the note compiles either way, so a reader knows this is a
|
|
471
|
-
* rename to schedule rather than a build to unbreak.
|
|
472
|
-
*
|
|
473
|
-
* @param {string} retired - The spelling the note used.
|
|
474
|
-
* @param {string} current - What to write instead.
|
|
475
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
476
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
477
|
-
* line — repeating it prints the path twice.
|
|
478
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
479
|
-
*/
|
|
480
|
-
export function retiredAliasMessage(retired, current, file) {
|
|
481
|
-
return (
|
|
482
|
-
`\`${retired}:\` is a retired frontmatter field — write \`${current}:\` ` +
|
|
483
|
-
`instead` +
|
|
484
|
-
(file ? ` — ${file}` : "") +
|
|
485
|
-
`. Both are read and \`${current}\` wins, so the note compiles ` +
|
|
486
|
-
`identically either way; \`${retired}\` is removed in a later release`
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* What a note writing a field at its **legacy in-block position** is told.
|
|
492
|
-
*
|
|
493
|
-
* The same three-step retirement `retiredAliasMessage` announces, applied to a
|
|
494
|
-
* *position* rather than a spelling. A field whose shared source moved
|
|
495
|
-
* under `data:` keeps reading the key its block still carries, that read wins,
|
|
496
|
-
* and it is reported here so a sweep has something to count down — without it,
|
|
497
|
-
* moving a field into `data:` would be a flag day across every repository that
|
|
498
|
-
* authors it.
|
|
499
|
-
*
|
|
500
|
-
* It names the block, because the key is `hm3.species` and not `species`: an
|
|
501
|
-
* author told to move "`species`" has two regions to guess between.
|
|
502
|
-
*
|
|
503
|
-
* @param {string} block - The system block the key was written in.
|
|
504
|
-
* @param {{name?: string, legacyKey?: string}} field - The declaration, which
|
|
505
|
-
* names both positions.
|
|
506
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
507
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
508
|
-
* line — repeating it prints the path twice.
|
|
509
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
510
|
-
*/
|
|
511
|
-
export function legacyKeyMessage(block, field, file) {
|
|
512
|
-
const legacy = `${block}.${field.legacyKey}`;
|
|
513
|
-
return (
|
|
514
|
-
`\`${legacy}:\` is the legacy position of the shared \`${field.name}:\` ` +
|
|
515
|
-
`— write it under \`data:\` instead` +
|
|
516
|
-
(file ? ` — ${file}` : "") +
|
|
517
|
-
`. Both are read and \`${legacy}\` wins, so the note compiles ` +
|
|
518
|
-
`identically either way; the in-block key is removed in a later release`
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* What a note writing a field at the **top-level key `data:` gathered it off**
|
|
524
|
-
* is told.
|
|
525
|
-
*
|
|
526
|
-
* {@link legacyKeyMessage}'s counterpart for the other retiring position.
|
|
527
|
-
* `portrait:` beside `img:` is the bare spelling of `data.portrait`, and both
|
|
528
|
-
* are read for the same reason both in-block spellings are: a package moves its
|
|
529
|
-
* corpus when it is ready, not on a flag day.
|
|
530
|
-
*
|
|
531
|
-
* It says nothing about which value is emitted, because that is not what an
|
|
532
|
-
* author needs from it. The value is the same either way; what the finding
|
|
533
|
-
* counts is one more note still on the old position.
|
|
534
|
-
*
|
|
535
|
-
* @param {{name?: string}} field - The declaration, which names the current
|
|
536
|
-
* position; the retiring one is derived from it.
|
|
537
|
-
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
538
|
-
* the caller emits through a diagnostic, whose locator already starts the
|
|
539
|
-
* line — repeating it prints the path twice.
|
|
540
|
-
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
541
|
-
*/
|
|
542
|
-
export function retiredTopLevelMessage(field, file) {
|
|
543
|
-
const retiring = retiredTopLevelKey(field);
|
|
544
|
-
return (
|
|
545
|
-
`top-level \`${retiring}:\` is the pre-\`data:\` position of the shared ` +
|
|
546
|
-
`\`${field.name}:\` — move it under \`data:\` instead` +
|
|
547
|
-
(file ? ` — ${file}` : "") +
|
|
548
|
-
`. Both are read and \`${field.name}\` wins, so the note compiles ` +
|
|
549
|
-
`identically either way; the top-level key is removed in a later release`
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Whether a note writes the retired spelling of a field, wherever it put it.
|
|
555
|
-
*
|
|
556
|
-
* Both regions are searched, because {@link sohlField} reads both: a note that
|
|
557
|
-
* moved the key to the top level without renaming it has done half the
|
|
558
|
-
* migration, and should be told so rather than passing in silence.
|
|
559
|
-
*
|
|
560
|
-
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
561
|
-
* @param {string} current - The field's current name.
|
|
562
|
-
* @param {object} [options] - Options.
|
|
563
|
-
* @param {boolean} [options.inData=false] - Search the `data:` container
|
|
564
|
-
* instead. A `data:` field has exactly one home, so its retired spelling has
|
|
565
|
-
* exactly one too, and searching the block or the top level for it would
|
|
566
|
-
* answer about a key the container check has already refused.
|
|
567
|
-
* @returns {boolean} Whether the retired spelling is declared.
|
|
568
|
-
*/
|
|
569
|
-
export function declaresRetiredAlias(fm, current, { inData = false } = {}) {
|
|
570
|
-
const retired = RETIRED_FIELD_ALIASES[current];
|
|
571
|
-
if (!retired || !fm || typeof fm !== "object") return false;
|
|
572
|
-
if (inData) return Object.hasOwn(dataContainer(fm), retired);
|
|
573
|
-
const block = fm.sohl;
|
|
574
|
-
const inBlock =
|
|
575
|
-
block &&
|
|
576
|
-
typeof block === "object" &&
|
|
577
|
-
!Array.isArray(block) &&
|
|
578
|
-
Object.hasOwn(block, retired);
|
|
579
|
-
return Boolean(inBlock) || Object.hasOwn(fm, retired);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* A note's `data:` container, or an empty one where it authored none.
|
|
584
|
-
*
|
|
585
|
-
* The property editor writes an emptied map as `[]`, which means the same thing
|
|
586
|
-
* `{}` does — this note authors no entries — so both read as empty rather than
|
|
587
|
-
* as something to search.
|
|
588
|
-
*
|
|
589
|
-
* @param {object} fm - Parsed frontmatter.
|
|
590
|
-
* @returns {object} The container.
|
|
591
|
-
*/
|
|
592
|
-
function dataContainer(fm) {
|
|
593
|
-
const data = fm.data;
|
|
594
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Read a field that has a retired spelling, the current name winning.
|
|
599
|
-
*
|
|
600
|
-
* This is the whole of the retirement window's behaviour, in one function, so
|
|
601
|
-
* the compiler and the linter cannot disagree about which value a note carries.
|
|
602
|
-
* Resolution within each spelling is {@link sohlField}'s — the `sohl:` block
|
|
603
|
-
* first, then the note's top level — so a renamed field keeps working wherever
|
|
604
|
-
* it was already written while the canonical home is the top level.
|
|
605
|
-
*
|
|
606
|
-
* A blank value counts as absent: `relations:` cleared in an editor means the
|
|
607
|
-
* note records no standings there, and falling through to the retired spelling
|
|
608
|
-
* is what an author part-way through the rename means by it.
|
|
609
|
-
*
|
|
610
|
-
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
611
|
-
* @param {string} current - The field's current name.
|
|
612
|
-
* @param {object} [options] - Options.
|
|
613
|
-
* @param {boolean} [options.inData=false] - Read the `data:` container instead,
|
|
614
|
-
* which is where a `data:` field and its retired spelling are both written.
|
|
615
|
-
* Dotted names resolve inside it, so `appearance.eye_color` reads the way it
|
|
616
|
-
* is authored.
|
|
617
|
-
* @returns {any} The value, or `undefined` when neither spelling carries one.
|
|
618
|
-
*/
|
|
619
|
-
export function readAliasedField(fm, current, { inData = false } = {}) {
|
|
620
|
-
const read =
|
|
621
|
-
inData ?
|
|
622
|
-
/** @param {string} key */ (key) =>
|
|
623
|
-
fm && typeof fm === "object" ?
|
|
624
|
-
getFrontmatter(dataContainer(fm), key, undefined)
|
|
625
|
-
: undefined
|
|
626
|
-
: /** @param {string} key */ (key) => sohlField(fm, key, undefined);
|
|
627
|
-
const value = read(current);
|
|
628
|
-
if (value !== undefined && value !== null && value !== "") return value;
|
|
629
|
-
const retired = RETIRED_FIELD_ALIASES[current];
|
|
630
|
-
return retired ? read(retired) : undefined;
|
|
631
|
-
}
|