@heroiclands/package-build 25.1.2 → 25.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 +36 -0
- package/assets/styles/content-base.css +0 -9
- package/bin/content-commands.mjs +22 -16
- package/bin/package-build.mjs +24 -25
- package/content-config.mjs +7 -34
- package/docs/api.md +38 -21
- package/docs/authoring/first-note.md +3 -3
- package/docs/authoring/frontmatter.md +30 -2
- package/docs/authoring/links-and-markup.md +92 -31
- package/docs/commands.md +103 -59
- package/docs/configuration.md +10 -45
- package/docs/content-format.md +7 -5
- package/docs/diagnostics.md +75 -0
- package/docs/encoding-core.md +11 -0
- package/docs/guides/book.md +3 -3
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/reference-pages.md +7 -3
- package/docs/project-setup.md +3 -0
- package/docs/reference/format-details.md +329 -187
- package/docs/reference/note-types.md +274 -274
- package/engine/changelog-group.mjs +84 -173
- package/engine/changelog-labels.mjs +73 -0
- package/engine/changelog-lint.mjs +168 -44
- package/engine/content-expand.mjs +58 -38
- package/engine/content-format.yaml +0 -2
- package/engine/content-index.mjs +49 -23
- package/engine/content-links.mjs +4 -0
- package/engine/content-lint.mjs +2 -3
- package/engine/content-render.mjs +6 -2
- package/engine/culture-parents.mjs +1 -1
- package/engine/culture-reach.mjs +10 -1
- package/engine/diagnostics.mjs +3 -2
- package/engine/dictionary-rows.mjs +3 -1
- package/engine/encoding-lexicon.mjs +12 -2
- package/engine/first-use.mjs +301 -87
- package/engine/frontmatter-lint.mjs +40 -14
- package/engine/glossary-entries.mjs +5 -5
- package/engine/glossary-language.mjs +21 -44
- package/engine/homepage.mjs +0 -4
- package/engine/index-records.mjs +3 -1
- package/engine/infobox.mjs +6 -3
- package/engine/language-names.mjs +70 -10
- package/engine/language-rows.mjs +6 -7
- package/engine/lexical-sections.mjs +3 -1
- package/engine/literature-notes.mjs +4 -76
- package/engine/metadata-index.mjs +15 -8
- package/engine/name-forms.mjs +99 -0
- package/engine/name-refs.mjs +143 -0
- package/engine/name-rows.mjs +11 -1
- package/engine/names.mjs +27 -22
- package/engine/note-addresses.mjs +0 -3
- package/engine/note-frontmatter.mjs +3 -5
- package/engine/note-language.mjs +348 -0
- package/engine/note-vocabulary.mjs +35 -13
- package/engine/pdf-index.mjs +3 -2
- package/engine/pdf-render.mjs +2 -4
- package/engine/settlement-tables.mjs +4 -3
- package/engine/site-build.mjs +2 -7
- package/engine/site-search.mjs +3 -5
- package/engine/site-theme.mjs +1 -1
- package/engine/sql-tables.mjs +29 -14
- package/engine/wikilink-syntax.mjs +2 -0
- package/engine/word-citations.mjs +3 -3
- package/engine/word-rows.mjs +3 -1
- package/engine/word-terms.mjs +2 -2
- package/package.json +1 -1
- package/site-theme/static/css/style.css +0 -4
- package/types/engine/changelog-group.d.mts +6 -10
- package/types/engine/changelog-labels.d.mts +10 -0
- package/types/engine/changelog-lint.d.mts +11 -6
- package/types/engine/content-index.d.mts +16 -16
- package/types/engine/diagnostics.d.mts +1 -1
- package/types/engine/first-use.d.mts +97 -1
- package/types/engine/frontmatter-lint.d.mts +9 -2
- package/types/engine/glossary-language.d.mts +13 -6
- package/types/engine/homepage.d.mts +0 -3
- package/types/engine/language-names.d.mts +3 -0
- package/types/engine/literature-notes.d.mts +2 -27
- package/types/engine/name-forms.d.mts +61 -0
- package/types/engine/name-refs.d.mts +51 -0
- package/types/engine/name-rows.d.mts +4 -0
- package/types/engine/names.d.mts +12 -92
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-language.d.mts +47 -0
- package/types/engine/settlement-tables.d.mts +1 -1
- package/types/engine/site-build.d.mts +2 -6
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
} from "./content-address.mjs";
|
|
63
63
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
64
64
|
import { isGmNote } from "./note-vocabulary.mjs";
|
|
65
|
+
import { nameFormLists } from "./name-forms.mjs";
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* Written once a fetch completes, so a half-finished cache is never used.
|
|
@@ -448,7 +449,13 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
448
449
|
...(typeof record.name?.sort === "string" && record.name.sort ?
|
|
449
450
|
{ nameSort: record.name.sort }
|
|
450
451
|
: {}),
|
|
451
|
-
|
|
452
|
+
// Every form the note's name lists, by language, and the
|
|
453
|
+
// language the publishing package derived for the note.
|
|
454
|
+
forms:
|
|
455
|
+
nameFormLists(record.name).length ?
|
|
456
|
+
Object.fromEntries(nameFormLists(record.name))
|
|
457
|
+
: undefined,
|
|
458
|
+
lang: typeof record.lang === "string" ? record.lang : undefined,
|
|
452
459
|
// Absent where the package publishes no page for it. A consumer
|
|
453
460
|
// must tolerate that rather than invent an href, exactly as it
|
|
454
461
|
// already tolerates an entry with no `uuid`.
|
|
@@ -489,15 +496,15 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
489
496
|
// walking that parent's own chain.
|
|
490
497
|
culture: record.data?.culture ?? undefined,
|
|
491
498
|
...(parts.type === "lore" && record.subType === "culture" ?
|
|
492
|
-
{
|
|
493
|
-
data: {
|
|
494
|
-
language: record.data?.language,
|
|
495
|
-
appearanceProfile: record.data?.appearanceProfile,
|
|
496
|
-
},
|
|
497
|
-
}
|
|
499
|
+
{ data: { appearanceProfile: record.data?.appearanceProfile } }
|
|
498
500
|
: {}),
|
|
499
501
|
...(parts.type === "skill" && record.subType === "language" ?
|
|
500
|
-
{
|
|
502
|
+
{
|
|
503
|
+
data: { names: record.data?.names },
|
|
504
|
+
...(typeof record.languageCode === "string" ?
|
|
505
|
+
{ languageCode: record.languageCode }
|
|
506
|
+
: {}),
|
|
507
|
+
}
|
|
501
508
|
: {}),
|
|
502
509
|
population: record.data?.population ?? undefined,
|
|
503
510
|
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A note's names, by language.
|
|
16
|
+
*
|
|
17
|
+
* `name` is `{full, <code>: [forms]}`. `full` is the English name a page,
|
|
18
|
+
* a compendium entry and an index print. Every other name the subject goes by
|
|
19
|
+
* is listed under the code of its language: a code `languages:` declares, or
|
|
20
|
+
* `en`, which needs no declaration. The first form of a list is that
|
|
21
|
+
* language's standard form, and `full` counts as an English form.
|
|
22
|
+
*
|
|
23
|
+
* `sort`, and a character's `given` and `clan`, are single strings and no
|
|
24
|
+
* language's forms, so a configured language may not take one of those names.
|
|
25
|
+
*
|
|
26
|
+
* A leaf module, importing nothing.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A whole visible label as names are matched: NFC, straight apostrophes,
|
|
33
|
+
* single spaces, trimmed, and case folded. Matching ignores case; a name is
|
|
34
|
+
* displayed as it was written, never in this form.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text - A visible name, `full` or a form.
|
|
37
|
+
* @returns {string} Its matching key.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeName(text) {
|
|
40
|
+
return displayName(text).toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A whole visible label as it is displayed: NFC, straight apostrophes, single
|
|
45
|
+
* spaces and trimmed, its case as written.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} text - A visible name, `full` or a form.
|
|
48
|
+
* @returns {string} The label for display.
|
|
49
|
+
*/
|
|
50
|
+
export function displayName(text) {
|
|
51
|
+
return text.normalize("NFC").replaceAll("’", "'").replace(/\s+/g, " ").trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The language every note falls back to, and a code no configuration need declare. */
|
|
55
|
+
export const FALLBACK_LANGUAGE = "en";
|
|
56
|
+
|
|
57
|
+
/** The single-string name keys every note accepts, in display order. */
|
|
58
|
+
export const NOTE_NAME_KEYS = Object.freeze(["full", "sort"]);
|
|
59
|
+
|
|
60
|
+
/** Additional name components accepted by an individual character. */
|
|
61
|
+
export const CHARACTER_NAME_KEYS = Object.freeze(["given", "clan"]);
|
|
62
|
+
|
|
63
|
+
/** Every `name` key that is not a language's list of forms. */
|
|
64
|
+
export const NAME_STRING_KEYS = Object.freeze([...NOTE_NAME_KEYS, ...CHARACTER_NAME_KEYS]);
|
|
65
|
+
|
|
66
|
+
const STRING_KEYS = new Set(NAME_STRING_KEYS);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A note's form lists, in authored order: each key of `name` that is not one
|
|
70
|
+
* of {@link NAME_STRING_KEYS} and holds a list, with its string forms.
|
|
71
|
+
*
|
|
72
|
+
* Read without the configuration, so a reader of an index gets every list a
|
|
73
|
+
* note states; which codes a note may state is the frontmatter lint's
|
|
74
|
+
* question, not a reader's.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} name - A note's `name`.
|
|
77
|
+
* @returns {Array<[string, string[]]>} `[code, forms]` pairs; a form that is
|
|
78
|
+
* not a nonempty string is left out, and so is a list left empty by that.
|
|
79
|
+
*/
|
|
80
|
+
export function nameFormLists(name) {
|
|
81
|
+
if (!name || typeof name !== "object" || Array.isArray(name)) return [];
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const [code, value] of Object.entries(name)) {
|
|
84
|
+
if (STRING_KEYS.has(code) || !Array.isArray(value)) continue;
|
|
85
|
+
const forms = value.filter((form) => typeof form === "string" && form.trim() !== "");
|
|
86
|
+
if (forms.length) out.push([code, forms]);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every form a note's name lists, in authored order, without `full`.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} name - A note's `name`.
|
|
95
|
+
* @returns {string[]} The forms; empty where it lists none.
|
|
96
|
+
*/
|
|
97
|
+
export function nameFormsOf(name) {
|
|
98
|
+
return nameFormLists(name).flatMap(([, forms]) => forms);
|
|
99
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The `ref=` attribute of a `.name` span: the note a spelling belongs to, or a
|
|
16
|
+
* page-local key for a subject with no note. Shape findings need no catalog;
|
|
17
|
+
* resolution findings take the prepared name catalog.
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { rebaseNameSpans } from "./name-markup.mjs";
|
|
22
|
+
import { scanSpans } from "./content-spans.mjs";
|
|
23
|
+
|
|
24
|
+
/** A page-local key: `#` then a word of letters, digits, `_` and `-`. */
|
|
25
|
+
const LOCAL_KEY = /^#[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
|
|
26
|
+
|
|
27
|
+
const isName = (span) =>
|
|
28
|
+
span.attrs.some(([key, value]) => key === "class" && value.split(/\s+/).includes("name"));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether a `ref` value is a page-local key rather than a note address.
|
|
32
|
+
* @param {string} ref - The attribute value as written.
|
|
33
|
+
* @returns {boolean} True for a value beginning with `#`.
|
|
34
|
+
*/
|
|
35
|
+
export const isLocalRef = (ref) => ref.startsWith("#");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `ref` a span states.
|
|
39
|
+
* @param {{attrs: ReadonlyArray<readonly [string, string]>}} span - A parsed span.
|
|
40
|
+
* @returns {string|null} The value as written, or null when the span has none.
|
|
41
|
+
*/
|
|
42
|
+
export function readNameRef(span) {
|
|
43
|
+
return span.attrs.find(([key]) => key === "ref")?.[1] ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A finding at a named attribute part, with the position left out when unknown. */
|
|
47
|
+
function at(span, key, part, file, message) {
|
|
48
|
+
const site = span.attributePositions?.[key]?.[part];
|
|
49
|
+
return {
|
|
50
|
+
...(file === undefined ? {} : { file }),
|
|
51
|
+
...(Number.isFinite(site?.line) ? { line: site.line } : {}),
|
|
52
|
+
...(Number.isFinite(site?.column) ? { column: site.column } : {}),
|
|
53
|
+
severity: "error",
|
|
54
|
+
message,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The `ref=` findings that depend on the span alone.
|
|
60
|
+
* @param {ReadonlyArray<import("./names.mjs").ParsedNameSpan>} spans - Every parsed span, at file coordinates.
|
|
61
|
+
* @param {{file?: string}} [options] - Authored source file.
|
|
62
|
+
* @returns {Array<{file?: string, line?: number, column?: number, severity: "error", message: string}>} One error per defect.
|
|
63
|
+
*/
|
|
64
|
+
export function nameRefFindings(spans, { file } = {}) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
for (const span of spans) {
|
|
67
|
+
const ref = readNameRef(span);
|
|
68
|
+
if (ref === null) continue;
|
|
69
|
+
if (!isName(span)) {
|
|
70
|
+
findings.push(
|
|
71
|
+
at(
|
|
72
|
+
span,
|
|
73
|
+
"ref",
|
|
74
|
+
"key",
|
|
75
|
+
file,
|
|
76
|
+
"ref= belongs on a .name span; a wikilink never takes ref",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (span.attrs.some(([key]) => key === "data-type"))
|
|
82
|
+
findings.push(
|
|
83
|
+
at(
|
|
84
|
+
span,
|
|
85
|
+
"data-type",
|
|
86
|
+
"key",
|
|
87
|
+
file,
|
|
88
|
+
"data-type is for a name with no note; the ref states the type",
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
if (isLocalRef(ref) && !LOCAL_KEY.test(ref))
|
|
92
|
+
findings.push(
|
|
93
|
+
at(
|
|
94
|
+
span,
|
|
95
|
+
"ref",
|
|
96
|
+
"value",
|
|
97
|
+
file,
|
|
98
|
+
`ref ${JSON.stringify(ref)} must be a note address or a page-local key such as #jane`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create the check that every note `ref=` of a note names a note.
|
|
107
|
+
* @param {object} options - The lint's prepared inputs.
|
|
108
|
+
* @param {import("./names.mjs").NameCatalog} options.catalog - Resolves an address to its note.
|
|
109
|
+
* @param {Map<string, {raw: string, body: string, bodyLine?: number, bodyColumn?: number}>} [options.parsed] - Captured authored bodies.
|
|
110
|
+
* @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Exact-text scanner projections.
|
|
111
|
+
* @returns {(note: {file: string, raw: string}) => Array<{file: string, line?: number, column?: number, severity: "error", message: string}>} One error per unresolved address.
|
|
112
|
+
*/
|
|
113
|
+
export function createNameRefChecker({ catalog, parsed, syntax }) {
|
|
114
|
+
return ({ file, raw }) => {
|
|
115
|
+
const authored = parsed?.get(file);
|
|
116
|
+
const spans =
|
|
117
|
+
authored?.raw === raw && Number.isInteger(authored.bodyLine) ?
|
|
118
|
+
rebaseNameSpans(
|
|
119
|
+
(syntax ? syntax(authored.body).spans : scanSpans(authored.body)).names,
|
|
120
|
+
{
|
|
121
|
+
raw,
|
|
122
|
+
bodyLine: authored.bodyLine,
|
|
123
|
+
bodyColumn: authored.bodyColumn,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
: scanSpans(raw).names;
|
|
127
|
+
const findings = [];
|
|
128
|
+
for (const span of spans) {
|
|
129
|
+
const ref = readNameRef(span);
|
|
130
|
+
if (ref === null || isLocalRef(ref) || catalog.resolve(ref)) continue;
|
|
131
|
+
findings.push(
|
|
132
|
+
at(
|
|
133
|
+
span,
|
|
134
|
+
"ref",
|
|
135
|
+
"value",
|
|
136
|
+
file,
|
|
137
|
+
`ref ${JSON.stringify(ref)} names no note in this package or its declared dependencies`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
};
|
|
143
|
+
}
|
package/engine/name-rows.mjs
CHANGED
|
@@ -22,7 +22,15 @@ import { encodeAddresses } from "./address-values.mjs";
|
|
|
22
22
|
import { isNoteRecord } from "./index-records.mjs";
|
|
23
23
|
|
|
24
24
|
/** Every column of the `names` relation, in order. */
|
|
25
|
-
export const NAME_COLUMNS = Object.freeze([
|
|
25
|
+
export const NAME_COLUMNS = Object.freeze([
|
|
26
|
+
"owner",
|
|
27
|
+
"name",
|
|
28
|
+
"type",
|
|
29
|
+
"subType",
|
|
30
|
+
"ref",
|
|
31
|
+
"line",
|
|
32
|
+
"column",
|
|
33
|
+
]);
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* @typedef {object} NameRow
|
|
@@ -30,6 +38,7 @@ export const NAME_COLUMNS = Object.freeze(["owner", "name", "type", "subType", "
|
|
|
30
38
|
* @property {string} name - Rendered visible text of the occurrence.
|
|
31
39
|
* @property {string|null} type - Supplied note type, or null.
|
|
32
40
|
* @property {string|null} subType - Supplied subtype, or null.
|
|
41
|
+
* @property {string|null} ref - The `ref=` as written, a note address or a page-local `#key`, or null.
|
|
33
42
|
* @property {number} line - Original 1-based source line at the opening bracket.
|
|
34
43
|
* @property {number} column - Original 1-based UTF-16 source column at that bracket.
|
|
35
44
|
*/
|
|
@@ -61,6 +70,7 @@ export function namesRows(records) {
|
|
|
61
70
|
name: entry.name,
|
|
62
71
|
type: entry.type ?? null,
|
|
63
72
|
subType: entry.subType ?? null,
|
|
73
|
+
ref: entry.ref ?? null,
|
|
64
74
|
line: entry.line,
|
|
65
75
|
column: entry.column,
|
|
66
76
|
});
|
package/engine/names.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import { formatLocator } from "./diagnostics.mjs";
|
|
|
19
19
|
import { FOLDER_TYPE } from "./folder-notes.mjs";
|
|
20
20
|
import { isNoteRecord } from "./index-records.mjs";
|
|
21
21
|
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
22
|
+
import { readNameRef } from "./name-refs.mjs";
|
|
22
23
|
import { WORD } from "./word-pattern.mjs";
|
|
24
|
+
import { displayName, nameFormsOf, normalizeName } from "./name-forms.mjs";
|
|
23
25
|
|
|
24
26
|
const compare = (a, b) =>
|
|
25
27
|
a < b ? -1
|
|
@@ -32,7 +34,7 @@ const compare = (a, b) =>
|
|
|
32
34
|
* @property {string} key - Canonical page identity, or the ambiguous label identity.
|
|
33
35
|
* @property {string} full - Declared full name.
|
|
34
36
|
* @property {true} indexed - The identity belongs to the indexed catalog.
|
|
35
|
-
* @property {boolean} [fullUse] - The label is a full
|
|
37
|
+
* @property {boolean} [fullUse] - The label is a form of the full name rather than a listed form.
|
|
36
38
|
* @property {string} [type] - The note's type, absent on an ambiguous label identity.
|
|
37
39
|
* @property {string|null} [subType] - The note's subtype, or null.
|
|
38
40
|
* @property {NameMatch[]} [candidates] - Indexed identities sharing the label.
|
|
@@ -40,7 +42,7 @@ const compare = (a, b) =>
|
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* The prepared
|
|
45
|
+
* The prepared lookup of full names and listed forms.
|
|
44
46
|
* @typedef {object} NameCatalog
|
|
45
47
|
* @property {(label: string, preferred?: {key: string}, kind?: NameKind) => NameMatch|undefined} recognize - Match a whole visible label, among notes of `kind` when it supplies a type.
|
|
46
48
|
* @property {(target: string) => NameMatch|undefined} resolve - Resolve an Address target without its anchor to its indexed identity.
|
|
@@ -60,6 +62,7 @@ const compare = (a, b) =>
|
|
|
60
62
|
* @property {string} name - Rendered visible text.
|
|
61
63
|
* @property {string|null} type - Supplied valid type, or null.
|
|
62
64
|
* @property {string|null} subType - Supplied valid subtype, or null.
|
|
65
|
+
* @property {string|null} ref - The `ref=` as written, a note address or a `#key`, or null.
|
|
63
66
|
* @property {number} line - Original 1-based file line.
|
|
64
67
|
* @property {number} column - Original 1-based UTF-16 file column.
|
|
65
68
|
*/
|
|
@@ -116,19 +119,14 @@ const compare = (a, b) =>
|
|
|
116
119
|
* @property {NameConflict[]} conflicts - Labels with incompatible supplied kinds.
|
|
117
120
|
*/
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
* @param {string} text - Visible name or declared alias.
|
|
122
|
-
* @returns {string} NFC text with straight apostrophes and single spaces.
|
|
123
|
-
*/
|
|
124
|
-
export function normalizeName(text) {
|
|
125
|
-
return text.normalize("NFC").replaceAll("’", "'").replace(/\s+/g, " ").trim();
|
|
126
|
-
}
|
|
122
|
+
// A label's matching key folds case; its display form keeps it.
|
|
123
|
+
export { displayName, normalizeName } from "./name-forms.mjs";
|
|
127
124
|
|
|
128
125
|
/**
|
|
129
|
-
* Whole-label possessives and regular plurals
|
|
130
|
-
*
|
|
131
|
-
* @
|
|
126
|
+
* Whole-label possessives and regular plurals, as matching keys; any other
|
|
127
|
+
* form is one a note's name lists.
|
|
128
|
+
* @param {string} text - Full name or a listed form.
|
|
129
|
+
* @returns {string[]} Supported whole-label forms, case folded.
|
|
132
130
|
*/
|
|
133
131
|
export function nameForms(text) {
|
|
134
132
|
const value = normalizeName(text);
|
|
@@ -150,7 +148,7 @@ function identity(record) {
|
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
/**
|
|
153
|
-
* Prepare full names,
|
|
151
|
+
* Prepare full names, every listed form and link identities from captured records.
|
|
154
152
|
* @param {object} options - The invocation's existing corpus and link index.
|
|
155
153
|
* @param {readonly object[]} options.records - Captured local records.
|
|
156
154
|
* @param {ReturnType<typeof import("./content-links.mjs").buildLinkIndex>} options.index - Captured address space.
|
|
@@ -160,7 +158,7 @@ export function createNameCatalog({ records, index }) {
|
|
|
160
158
|
const names = new Map(),
|
|
161
159
|
byFile = new Map(),
|
|
162
160
|
entries = new Map();
|
|
163
|
-
const addName = (key, full,
|
|
161
|
+
const addName = (key, full, forms, type, subType) => {
|
|
164
162
|
const entry = entries.get(key) ?? {
|
|
165
163
|
key,
|
|
166
164
|
full,
|
|
@@ -169,7 +167,7 @@ export function createNameCatalog({ records, index }) {
|
|
|
169
167
|
subType: typeof subType === "string" && subType ? subType : null,
|
|
170
168
|
};
|
|
171
169
|
entries.set(key, entry);
|
|
172
|
-
for (const text of [full, ...(
|
|
170
|
+
for (const text of [full, ...(forms ?? [])]) {
|
|
173
171
|
if (typeof text !== "string" || !text.trim()) continue;
|
|
174
172
|
for (const spelling of nameForms(text)) {
|
|
175
173
|
if (!names.has(spelling)) names.set(spelling, new Map());
|
|
@@ -192,7 +190,7 @@ export function createNameCatalog({ records, index }) {
|
|
|
192
190
|
const entry = addName(
|
|
193
191
|
index.resolve(page) ? page : canonical,
|
|
194
192
|
full,
|
|
195
|
-
record.name
|
|
193
|
+
nameFormsOf(record.name),
|
|
196
194
|
parts.type,
|
|
197
195
|
record.subType,
|
|
198
196
|
);
|
|
@@ -211,7 +209,10 @@ export function createNameCatalog({ records, index }) {
|
|
|
211
209
|
continue;
|
|
212
210
|
const page = canonicalKey(parts.package, "note", parts.type, parts.shortcode);
|
|
213
211
|
const key = index.foreign.index.has(page) ? page : canonical;
|
|
214
|
-
entries.set(
|
|
212
|
+
entries.set(
|
|
213
|
+
canonical,
|
|
214
|
+
addName(key, hit.name, nameFormsOf(hit.forms), parts.type, hit.subType),
|
|
215
|
+
);
|
|
215
216
|
}
|
|
216
217
|
return {
|
|
217
218
|
recognize(label, preferred, kind) {
|
|
@@ -302,8 +303,9 @@ export function collectUnwrittenNames({ records, catalog }) {
|
|
|
302
303
|
if (!kinds.has(kind)) kinds.set(kind, { type, subType, names: new Map() });
|
|
303
304
|
const names = kinds.get(kind).names,
|
|
304
305
|
name = normalizeName(occurrence.name);
|
|
305
|
-
if (!names.has(name))
|
|
306
|
-
|
|
306
|
+
if (!names.has(name))
|
|
307
|
+
names.set(name, { display: displayName(occurrence.name), uses: [] });
|
|
308
|
+
names.get(name).uses.push(nameUse(record, occurrence));
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
const groups = [...kinds.values()]
|
|
@@ -316,7 +318,7 @@ export function collectUnwrittenNames({ records, catalog }) {
|
|
|
316
318
|
subType,
|
|
317
319
|
names: [...names]
|
|
318
320
|
.sort(([a], [b]) => compare(a, b))
|
|
319
|
-
.map(([
|
|
321
|
+
.map(([, { display, uses }]) => ({ name: display, uses: uses.sort(compareUses) })),
|
|
320
322
|
}));
|
|
321
323
|
const firstUse = firstNameUse(
|
|
322
324
|
groups.flatMap((group) => group.names.flatMap((name) => name.uses)),
|
|
@@ -383,6 +385,7 @@ export function analyzeNameSpans(spans, { file, vocabulary = NOTE_VOCABULARY, ty
|
|
|
383
385
|
name: span.visible,
|
|
384
386
|
type: kind.type,
|
|
385
387
|
subType: kind.subType,
|
|
388
|
+
ref: readNameRef(span),
|
|
386
389
|
line: span.line,
|
|
387
390
|
column: span.column,
|
|
388
391
|
});
|
|
@@ -401,11 +404,13 @@ export function analyzeNameSpans(spans, { file, vocabulary = NOTE_VOCABULARY, ty
|
|
|
401
404
|
*/
|
|
402
405
|
export function createNamesReport({ package: contentPackage, records, catalog }) {
|
|
403
406
|
const names = new Map();
|
|
407
|
+
const displays = new Map();
|
|
404
408
|
for (const record of records.filter(isNoteRecord))
|
|
405
409
|
for (const occurrence of record.names ?? []) {
|
|
406
410
|
if (occurrence.type === null) continue;
|
|
407
411
|
const name = normalizeName(occurrence.name);
|
|
408
412
|
if (!names.has(name)) names.set(name, new Map());
|
|
413
|
+
if (!displays.has(name)) displays.set(name, displayName(occurrence.name));
|
|
409
414
|
const kinds = names.get(name),
|
|
410
415
|
key = JSON.stringify([occurrence.type, occurrence.subType]);
|
|
411
416
|
if (!kinds.has(key))
|
|
@@ -421,7 +426,7 @@ export function createNamesReport({ package: contentPackage, records, catalog })
|
|
|
421
426
|
}
|
|
422
427
|
if (parents.size < 2 && ![...parents.values()].some((values) => values.size > 1)) continue;
|
|
423
428
|
conflicts.push({
|
|
424
|
-
name,
|
|
429
|
+
name: displays.get(name),
|
|
425
430
|
kinds: [...kinds.values()]
|
|
426
431
|
.sort(
|
|
427
432
|
(a, b) => compare(a.type, b.type) || compare(a.subType ?? "", b.subType ?? ""),
|
|
@@ -159,9 +159,6 @@ export function addressPositions(fm, context = {}) {
|
|
|
159
159
|
...(ASSET_TYPE_NAMES.has(field.ref) || field.ref === "folder" ?
|
|
160
160
|
{ system: "none" }
|
|
161
161
|
: {}),
|
|
162
|
-
...(field.name === "language" && field.ref === "skill" ?
|
|
163
|
-
{ system: NOTE_SYSTEM, noteOnly: true }
|
|
164
|
-
: {}),
|
|
165
162
|
});
|
|
166
163
|
}
|
|
167
164
|
out.push(...STRUCTURED_ADDRESSES.filter((p) => !p.noteType || p.noteType === fm.type));
|
|
@@ -21,11 +21,9 @@ export {
|
|
|
21
21
|
NOTE_TOP_LEVEL_KEY_SET,
|
|
22
22
|
} from "./note-vocabulary.mjs";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Additional name components accepted by an individual character. */
|
|
28
|
-
export const CHARACTER_NAME_KEYS = Object.freeze(["given", "clan"]);
|
|
24
|
+
// The single-string name keys, beside the language form lists they are told
|
|
25
|
+
// apart from.
|
|
26
|
+
export { CHARACTER_NAME_KEYS, NOTE_NAME_KEYS } from "./name-forms.mjs";
|
|
29
27
|
|
|
30
28
|
/** Return authored keys from an opening YAML frontmatter map. */
|
|
31
29
|
export function authoredNoteKeys(markdown) {
|