@heroiclands/package-build 23.12.8 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +3 -2
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +293 -24
- package/bin/content-commands.mjs +370 -213
- package/bin/package-build-preview.mjs +2 -2
- package/bin/package-build.mjs +3 -2
- package/ci/check-banners.mjs +7 -7
- package/config.mjs +21 -6
- package/content-config.mjs +199 -80
- package/docs/api.md +110 -89
- package/docs/authoring/links-and-markup.md +469 -97
- package/docs/commands.md +259 -78
- package/docs/configuration.md +211 -103
- package/docs/content-format.md +4 -4
- package/docs/diagnostics.md +44 -2
- package/docs/getting-started.md +1 -1
- package/docs/guides/icons.md +1 -1
- package/docs/guides/module-project.md +5 -8
- package/docs/guides/release-and-deploy.md +2 -2
- package/docs/guides/site.md +112 -23
- package/docs/guides/system-project.md +1 -1
- package/docs/guides/writing-in-invented-scripts.md +1247 -0
- package/docs/index.md +1 -0
- package/docs/project-setup.md +28 -30
- package/docs/reference/format-details.md +677 -319
- package/docs/reference/note-types.md +47 -10
- package/docs/reference/theme-pages.md +178 -108
- package/docs/source-preview.md +16 -2
- package/engine/anchors.mjs +2 -2
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/code-fences.mjs +3 -3
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +29 -14
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +18 -11
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- package/engine/content-index.mjs +1 -1
- package/engine/content-links.mjs +3 -3
- package/engine/content-lint.mjs +1 -1
- package/engine/content-render.mjs +453 -0
- package/engine/content-scripts.mjs +376 -43
- package/engine/content-spans.mjs +6 -3
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +738 -0
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/dictionary-rows.mjs +15 -4
- package/engine/field-reference.mjs +5 -5
- package/engine/first-use.mjs +17 -27
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/heading-attributes.mjs +89 -0
- package/engine/helpers.mjs +61 -101
- package/engine/homepage.mjs +17 -29
- package/engine/html-minify.mjs +70 -0
- package/engine/index.mjs +8 -5
- package/engine/key-case-collisions.mjs +136 -0
- package/engine/language-data.mjs +203 -6
- package/engine/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +186 -16
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +262 -47
- package/engine/map-places.mjs +5 -4
- package/engine/markdown-expressions.mjs +0 -2
- package/engine/markdownlint-run.mjs +9 -3
- package/engine/markup-validation.mjs +21 -0
- package/engine/metre-declaration.mjs +134 -0
- package/engine/names.mjs +36 -9
- package/engine/note-matter.mjs +66 -0
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +69 -133
- package/engine/pdf-render.mjs +205 -58
- package/engine/post-fields.mjs +157 -0
- package/engine/preview-index.mjs +130 -0
- package/engine/preview-source.mjs +450 -0
- package/engine/project-init.mjs +5 -12
- package/engine/prose-config.mjs +3 -3
- package/engine/read-stdin.mjs +29 -0
- package/engine/run-rows.mjs +2 -2
- package/engine/run-words.mjs +6 -112
- package/engine/script-emphasis-terms.mjs +38 -0
- package/engine/script-emphasis.mjs +295 -0
- package/engine/script-font-delivery.mjs +4 -4
- package/engine/site-build.mjs +260 -315
- package/engine/site-config.mjs +26 -238
- package/engine/site-files.mjs +175 -0
- package/engine/site-index.mjs +9 -4
- package/engine/site-preview.mjs +52 -525
- package/engine/site-render.mjs +593 -0
- package/engine/site-root.mjs +22 -13
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +273 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +926 -0
- package/engine/sql-tables.mjs +20 -3
- package/engine/text-maps.mjs +57 -10
- package/engine/typographer.mjs +248 -0
- package/engine/verse-metre.mjs +357 -0
- package/engine/wikilink-syntax.mjs +29 -47
- package/engine/wikilinks.mjs +758 -279
- package/engine/word-forms.mjs +364 -0
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +38 -4
- package/manifest.mjs +3 -4
- package/package.json +11 -8
- package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
- package/site-theme/layouts/base.hbs +31 -0
- package/site-theme/layouts/blog-list.hbs +30 -0
- package/site-theme/layouts/blog-post.hbs +40 -0
- package/site-theme/layouts/home.hbs +35 -0
- package/site-theme/layouts/list.hbs +20 -0
- package/site-theme/layouts/not-found.hbs +31 -0
- package/site-theme/layouts/page.hbs +7 -0
- package/site-theme/partials/copy-buttons.hbs +37 -0
- package/site-theme/partials/draft-notice.hbs +5 -0
- package/site-theme/partials/exturl.hbs +2 -0
- package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
- package/site-theme/partials/header.hbs +44 -0
- package/site-theme/partials/hero-banner.hbs +11 -0
- package/site-theme/partials/infobox-entry.hbs +2 -0
- package/site-theme/partials/infobox-section.hbs +31 -0
- package/site-theme/partials/infobox-value.hbs +2 -0
- package/site-theme/partials/infobox.hbs +18 -0
- package/site-theme/partials/list-members.hbs +9 -0
- package/site-theme/partials/nav-links.hbs +5 -0
- package/site-theme/partials/page-body.hbs +48 -0
- package/site-theme/partials/page-list.hbs +14 -0
- package/site-theme/partials/related.hbs +24 -0
- package/site-theme/partials/search-meta.hbs +6 -0
- package/site-theme/partials/search-names.hbs +2 -0
- package/site-theme/partials/search.hbs +52 -0
- package/site-theme/partials/tags.hbs +4 -0
- package/site-theme/partials/toc.hbs +7 -0
- package/{hugo-theme → site-theme}/static/css/style.css +106 -14
- package/stage.mjs +1 -2
- package/types/config.d.mts +8 -4
- package/types/content-config.d.mts +15 -15
- package/types/engine/asset-shipping.d.mts +5 -2
- package/types/engine/base-compiler.d.mts +20 -2
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/content-address.d.mts +1 -1
- package/types/engine/content-alerts.d.mts +8 -2
- package/types/engine/content-blocks.d.mts +11 -4
- package/types/engine/content-expand.d.mts +288 -0
- package/types/engine/content-figures.d.mts +11 -3
- package/types/engine/content-glyphs.d.mts +85 -0
- package/types/engine/content-images.d.mts +6 -7
- package/types/engine/content-links.d.mts +1 -1
- package/types/engine/content-render.d.mts +199 -0
- package/types/engine/content-scripts.d.mts +70 -4
- package/types/engine/content-spans.d.mts +1 -1
- package/types/engine/content-styles.d.mts +33 -0
- package/types/engine/content-symbols.d.mts +35 -0
- package/types/engine/content-verse.d.mts +240 -0
- package/types/engine/core-report.d.mts +45 -0
- package/types/engine/core-vocabulary.d.mts +53 -0
- package/types/engine/dictionary-html.d.mts +8 -0
- package/types/engine/dictionary-rows.d.mts +10 -0
- package/types/engine/field-reference.d.mts +2 -2
- package/types/engine/heading-attributes.d.mts +20 -0
- package/types/engine/helpers.d.mts +3 -10
- package/types/engine/homepage.d.mts +11 -24
- package/types/engine/html-minify.d.mts +9 -0
- package/types/engine/index.d.mts +2 -1
- package/types/engine/key-case-collisions.d.mts +28 -0
- package/types/engine/language-data.d.mts +0 -2
- package/types/engine/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +66 -13
- package/types/engine/map-places.d.mts +8 -0
- package/types/engine/markdown-expressions.d.mts +0 -2
- package/types/engine/metre-declaration.d.mts +39 -0
- package/types/engine/names.d.mts +34 -3
- package/types/engine/note-matter.d.mts +48 -0
- package/types/engine/pdf-render.d.mts +6 -2
- package/types/engine/post-fields.d.mts +44 -0
- package/types/engine/preview-index.d.mts +72 -0
- package/types/engine/preview-source.d.mts +51 -0
- package/types/engine/read-stdin.d.mts +11 -0
- package/types/engine/run-rows.d.mts +2 -2
- package/types/engine/run-words.d.mts +0 -15
- package/types/engine/script-emphasis-terms.d.mts +21 -0
- package/types/engine/script-emphasis.d.mts +84 -0
- package/types/engine/script-font-delivery.d.mts +4 -4
- package/types/engine/site-build.d.mts +71 -113
- package/types/engine/site-config.d.mts +14 -134
- package/types/engine/site-files.d.mts +73 -0
- package/types/engine/site-index.d.mts +11 -3
- package/types/engine/site-preview.d.mts +15 -19
- package/types/engine/site-render.d.mts +171 -0
- package/types/engine/site-root.d.mts +12 -4
- package/types/engine/site-search.d.mts +54 -0
- package/types/engine/site-serve.d.mts +44 -0
- package/types/engine/site-theme-banner.d.mts +51 -0
- package/types/engine/site-theme-urls.d.mts +60 -0
- package/types/engine/site-theme.d.mts +127 -0
- package/types/engine/sql-tables.d.mts +4 -1
- package/types/engine/text-maps.d.mts +17 -3
- package/types/engine/typographer.d.mts +11 -0
- package/types/engine/verse-metre.d.mts +131 -0
- package/types/engine/wikilink-syntax.d.mts +30 -48
- package/types/engine/wikilinks.d.mts +330 -14
- package/types/engine/word-forms.d.mts +83 -0
- package/types/manifest.d.mts +3 -4
- package/types/stage.d.mts +1 -2
- package/types/vendor/markdownlint.d.mts +8 -0
- package/vendor/markdownlint.mjs +20388 -0
- package/engine/content-poetry.mjs +0 -240
- package/engine/web-wikilinks.mjs +0 -594
- package/hugo-theme/layouts/404.html +0 -85
- package/hugo-theme/layouts/_default/baseof.html +0 -67
- package/hugo-theme/layouts/_default/list.html +0 -139
- package/hugo-theme/layouts/_default/single.html +0 -4
- package/hugo-theme/layouts/blog/list.html +0 -54
- package/hugo-theme/layouts/blog-post/single.html +0 -36
- package/hugo-theme/layouts/index.html +0 -75
- package/hugo-theme/layouts/partials/banner-url.html +0 -141
- package/hugo-theme/layouts/partials/cdn-url.html +0 -36
- package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
- package/hugo-theme/layouts/partials/draft-notice.html +0 -41
- package/hugo-theme/layouts/partials/draft-state.html +0 -26
- package/hugo-theme/layouts/partials/header.html +0 -35
- package/hugo-theme/layouts/partials/hero-banner.html +0 -85
- package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
- package/hugo-theme/layouts/partials/infobox/items.html +0 -26
- package/hugo-theme/layouts/partials/infobox/section.html +0 -56
- package/hugo-theme/layouts/partials/infobox/value.html +0 -43
- package/hugo-theme/layouts/partials/infobox.html +0 -57
- package/hugo-theme/layouts/partials/page-body.html +0 -152
- package/hugo-theme/layouts/partials/related.html +0 -138
- package/hugo-theme/layouts/partials/search.html +0 -84
- package/hugo-theme/layouts/partials/site-url.html +0 -32
- package/hugo-theme/layouts/partials/tags.html +0 -30
- package/hugo-theme/layouts/partials/toc.html +0 -28
- package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
- package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
- package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
- package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
- package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
- package/hugo-theme/theme.toml +0 -11
- package/types/engine/content-poetry.d.mts +0 -58
- package/types/engine/web-wikilinks.d.mts +0 -107
- /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
- /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
- /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
- /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
- /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
- /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
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 core report: for each language that must cover the core vocabulary, the
|
|
16
|
+
* concepts of the list it has no word for.
|
|
17
|
+
*
|
|
18
|
+
* The list is the `data.concepts` of the package's `doc` notes. A language
|
|
19
|
+
* must cover it where it states `data.core: true`. A word covers every
|
|
20
|
+
* concept its `concept` names, and a language reads the words of its
|
|
21
|
+
* `data.family` chain as its own. A missing concept is a fact to work through,
|
|
22
|
+
* never a finding.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
28
|
+
import { isLanguageRecord, languageLineages, noteAddressOf } from "./word-rows.mjs";
|
|
29
|
+
|
|
30
|
+
/** Whether a value is a plain map. */
|
|
31
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
/** The slug a record is written by in a reference, such as `skill-vylari`. */
|
|
34
|
+
const slugOf = (record) => record.address?.slug ?? record.shortcode;
|
|
35
|
+
|
|
36
|
+
/** The name a language is reported by. */
|
|
37
|
+
const nameOf = (record) =>
|
|
38
|
+
typeof record.name?.full === "string" && record.name.full ? record.name.full : record.shortcode;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The concept list the records state, in record then authored order. An id
|
|
42
|
+
* another list repeats keeps its first place.
|
|
43
|
+
*
|
|
44
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
45
|
+
* @returns {Array<{id: string, gloss: string, group: string}>} The concepts.
|
|
46
|
+
*/
|
|
47
|
+
function conceptsOf(records) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const record of records) {
|
|
51
|
+
const list = record?.data?.concepts;
|
|
52
|
+
if (!Array.isArray(list)) continue;
|
|
53
|
+
for (const entry of list) {
|
|
54
|
+
if (
|
|
55
|
+
!isMap(entry) ||
|
|
56
|
+
typeof entry.id !== "string" ||
|
|
57
|
+
typeof entry.gloss !== "string" ||
|
|
58
|
+
typeof entry.group !== "string" ||
|
|
59
|
+
seen.has(entry.id)
|
|
60
|
+
)
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(entry.id);
|
|
63
|
+
out.push({ id: entry.id, gloss: entry.gloss, group: entry.group });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The concept ids a word serves. */
|
|
70
|
+
function idsOf(word) {
|
|
71
|
+
const value = isMap(word) ? word.concept : undefined;
|
|
72
|
+
return (Array.isArray(value) ? value : [value]).filter((id) => typeof id === "string");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The core report of a set of index records.
|
|
77
|
+
*
|
|
78
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
79
|
+
* @param {{language?: string}} [options] - A language to report alone, by its
|
|
80
|
+
* name, shortcode or reference slug whether or not it states `data.core`.
|
|
81
|
+
* @returns {{concepts: number, languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
82
|
+
* The report, or the problem when there is no concept list or the language
|
|
83
|
+
* asked for is none.
|
|
84
|
+
*/
|
|
85
|
+
export function coreReport(records, { language } = {}) {
|
|
86
|
+
const all = [...records].map((record) => encodeAddresses(record));
|
|
87
|
+
const concepts = conceptsOf(all);
|
|
88
|
+
if (!concepts.length)
|
|
89
|
+
return {
|
|
90
|
+
about: "concepts",
|
|
91
|
+
problem: "no note states `data.concepts`, so there is no core vocabulary to cover",
|
|
92
|
+
};
|
|
93
|
+
const languages = all.filter(isLanguageRecord);
|
|
94
|
+
let chosen;
|
|
95
|
+
if (language === undefined) chosen = languages.filter((record) => record.data?.core === true);
|
|
96
|
+
else {
|
|
97
|
+
const want = language.toLowerCase();
|
|
98
|
+
chosen = languages.filter((record) =>
|
|
99
|
+
[nameOf(record), record.shortcode, slugOf(record)].some(
|
|
100
|
+
(key) => String(key).toLowerCase() === want,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
if (!chosen.length)
|
|
104
|
+
return {
|
|
105
|
+
about: "language",
|
|
106
|
+
problem: `\`${language}\` is no language note: name one by its name, its shortcode or its reference`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
chosen.sort((a, b) => nameOf(a).localeCompare(nameOf(b)));
|
|
110
|
+
|
|
111
|
+
const { languages: byKey, chain } = languageLineages(all);
|
|
112
|
+
const groups = [...new Set(concepts.map((concept) => concept.group))];
|
|
113
|
+
return {
|
|
114
|
+
concepts: concepts.length,
|
|
115
|
+
languages: chosen.map((record) => {
|
|
116
|
+
const have = new Set();
|
|
117
|
+
for (const key of chain(noteAddressOf(record) ?? ""))
|
|
118
|
+
for (const word of byKey.get(key)?.data?.words ?? [])
|
|
119
|
+
for (const id of idsOf(word)) have.add(id);
|
|
120
|
+
const missing = [];
|
|
121
|
+
for (const group of groups) {
|
|
122
|
+
const members = concepts.filter((concept) => concept.group === group);
|
|
123
|
+
const lacking = members.filter((concept) => !have.has(concept.id));
|
|
124
|
+
if (lacking.length)
|
|
125
|
+
missing.push({
|
|
126
|
+
group,
|
|
127
|
+
count: lacking.length,
|
|
128
|
+
total: members.length,
|
|
129
|
+
concepts: lacking.map(({ id, gloss }) => ({ id, gloss })),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const lacking = missing.reduce((sum, group) => sum + group.count, 0);
|
|
133
|
+
return {
|
|
134
|
+
name: nameOf(record),
|
|
135
|
+
language: slugOf(record),
|
|
136
|
+
core: record.data?.core === true,
|
|
137
|
+
total: concepts.length,
|
|
138
|
+
covered: concepts.length - lacking,
|
|
139
|
+
missing,
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A core report as text: one block per language, its missing concepts under
|
|
147
|
+
* their groups with counts, each concept's id and gloss tab-separated.
|
|
148
|
+
*
|
|
149
|
+
* @param {{concepts: number, languages: Array<object>}} report - A report from {@link coreReport}.
|
|
150
|
+
* @returns {string} The text, ending in a newline.
|
|
151
|
+
*/
|
|
152
|
+
export function coreReportText(report) {
|
|
153
|
+
if (!report.languages.length) return "No language states `data.core: true`.\n";
|
|
154
|
+
return report.languages
|
|
155
|
+
.map((one) => {
|
|
156
|
+
if (!one.missing.length)
|
|
157
|
+
return `${one.name} (${one.language}): all ${one.total} core concepts have a word\n`;
|
|
158
|
+
const lines = [
|
|
159
|
+
`${one.name} (${one.language}): ${one.total - one.covered} of ${one.total} core concepts have no word`,
|
|
160
|
+
];
|
|
161
|
+
for (const group of one.missing) {
|
|
162
|
+
lines.push(` ${group.group}: ${group.count} of ${group.total}`);
|
|
163
|
+
for (const concept of group.concepts)
|
|
164
|
+
lines.push(` ${concept.id}\t${concept.gloss}`);
|
|
165
|
+
}
|
|
166
|
+
return `${lines.join("\n")}\n`;
|
|
167
|
+
})
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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 setting's core vocabulary: the concept list a `doc` note states in
|
|
16
|
+
* `data.concepts`, the `data.core` flag that says a language must cover it, and
|
|
17
|
+
* the `concept` a word of a language serves.
|
|
18
|
+
*
|
|
19
|
+
* This check owns what the values mean, each finding located at the key at
|
|
20
|
+
* fault:
|
|
21
|
+
*
|
|
22
|
+
* - a concept's `id` is lowercase letters, digits and `-`;
|
|
23
|
+
* - an `id` is stated once in a list, and by one note in the package;
|
|
24
|
+
* - a concept's `gloss` and `group` are not blank;
|
|
25
|
+
* - `data.core` is stated on a language only;
|
|
26
|
+
* - a word's `concept` names entries of the concept list, each once.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { pathLabel as label } from "./data-keys.mjs";
|
|
32
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
33
|
+
import { nearest } from "./near-miss.mjs";
|
|
34
|
+
import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
35
|
+
|
|
36
|
+
/** What a concept's `id` is made of. */
|
|
37
|
+
export const CONCEPT_ID = /^[a-z0-9-]+$/;
|
|
38
|
+
|
|
39
|
+
/** Whether a value is a plain map. */
|
|
40
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
/** A list value, or an empty one. */
|
|
43
|
+
const listOf = (value) => (Array.isArray(value) ? value : []);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The notes of an index that state a concept list, read once per index.
|
|
47
|
+
*
|
|
48
|
+
* @type {WeakMap<object, Array<{file: string, ids: Map<string, number>}>>}
|
|
49
|
+
*/
|
|
50
|
+
const stated = new WeakMap();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Each note of the index that states `data.concepts`, with the ids it states
|
|
54
|
+
* and where each first stands.
|
|
55
|
+
*
|
|
56
|
+
* @param {{notes?: Iterable<{file: string, fm?: Record<string, any>}>}} index - The lint index.
|
|
57
|
+
* @returns {Array<{file: string, ids: Map<string, number>}>} The notes.
|
|
58
|
+
*/
|
|
59
|
+
function listsIn(index) {
|
|
60
|
+
const known = stated.get(index);
|
|
61
|
+
if (known) return known;
|
|
62
|
+
const lists = [];
|
|
63
|
+
for (const note of index.notes ?? []) {
|
|
64
|
+
const concepts = note?.fm?.data?.concepts;
|
|
65
|
+
if (!Array.isArray(concepts)) continue;
|
|
66
|
+
const ids = new Map();
|
|
67
|
+
concepts.forEach((entry, at) => {
|
|
68
|
+
if (isMap(entry) && typeof entry.id === "string" && !ids.has(entry.id))
|
|
69
|
+
ids.set(entry.id, at);
|
|
70
|
+
});
|
|
71
|
+
lists.push({ file: note.file, ids });
|
|
72
|
+
}
|
|
73
|
+
stated.set(index, lists);
|
|
74
|
+
return lists;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every concept id the index's notes state.
|
|
79
|
+
*
|
|
80
|
+
* @param {{notes?: Iterable<object>}|undefined} index - The lint index.
|
|
81
|
+
* @returns {Set<string>|undefined} The ids, or `undefined` without an index.
|
|
82
|
+
*/
|
|
83
|
+
export function conceptIdsIn(index) {
|
|
84
|
+
if (!index?.notes) return undefined;
|
|
85
|
+
return new Set(listsIn(index).flatMap((list) => [...list.ids.keys()]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check a `doc` note's `data.concepts`: each id's spelling, an id stated twice,
|
|
90
|
+
* an id another note states, and a blank `gloss` or `group`.
|
|
91
|
+
*
|
|
92
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
93
|
+
* @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index.
|
|
94
|
+
* @returns {object[]} The findings, each at its key.
|
|
95
|
+
*/
|
|
96
|
+
export function checkConceptList(note, { index } = {}) {
|
|
97
|
+
const concepts = note?.fm?.data?.concepts;
|
|
98
|
+
if (!Array.isArray(concepts)) return [];
|
|
99
|
+
const raw = note.raw ?? "";
|
|
100
|
+
const finding = (path, message) => ({
|
|
101
|
+
file: note.file,
|
|
102
|
+
...positionOfFrontmatterPath(raw, path),
|
|
103
|
+
severity: "error",
|
|
104
|
+
message,
|
|
105
|
+
});
|
|
106
|
+
const others = index ? listsIn(index).filter((list) => list.file !== note.file) : [];
|
|
107
|
+
const findings = [];
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
concepts.forEach((entry, at) => {
|
|
110
|
+
if (!isMap(entry)) return;
|
|
111
|
+
const where = (key) => ["data", "concepts", at, key];
|
|
112
|
+
if (typeof entry.id === "string") {
|
|
113
|
+
if (!CONCEPT_ID.test(entry.id))
|
|
114
|
+
findings.push(
|
|
115
|
+
finding(
|
|
116
|
+
where("id"),
|
|
117
|
+
`\`${label(where("id"))}\` is \`${entry.id}\`; a concept's id is lowercase letters, digits and \`-\``,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
else if (seen.has(entry.id))
|
|
121
|
+
findings.push(
|
|
122
|
+
finding(
|
|
123
|
+
where("id"),
|
|
124
|
+
`\`${entry.id}\` is stated twice in this list; a concept's id is unique`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
else {
|
|
128
|
+
const also = others.find((list) => list.ids.has(entry.id));
|
|
129
|
+
if (also)
|
|
130
|
+
findings.push(
|
|
131
|
+
finding(
|
|
132
|
+
where("id"),
|
|
133
|
+
`\`${entry.id}\` is also stated by ${also.file}; a concept's id is unique`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
seen.add(entry.id);
|
|
138
|
+
}
|
|
139
|
+
for (const key of ["gloss", "group"])
|
|
140
|
+
if (typeof entry[key] === "string" && entry[key].trim() === "")
|
|
141
|
+
findings.push(
|
|
142
|
+
finding(where(key), `\`${label(where(key))}\` is blank; state the ${key}`),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
return findings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check a `skill` note's `data.core`: stated on a language only.
|
|
150
|
+
*
|
|
151
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - A `skill` note.
|
|
152
|
+
* @returns {object[]} The findings.
|
|
153
|
+
*/
|
|
154
|
+
export function checkCoreFlag(note) {
|
|
155
|
+
const fm = note?.fm ?? {};
|
|
156
|
+
if (!isMap(fm.data) || fm.data.core === undefined || fm.data.core === null) return [];
|
|
157
|
+
if (String(fm.subType ?? "").toLowerCase() === LANGUAGE_SUBTYPE) return [];
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
file: note.file,
|
|
161
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "core"], { key: true }),
|
|
162
|
+
severity: "error",
|
|
163
|
+
message:
|
|
164
|
+
"`data.core` says a language must cover the core vocabulary, and this note's " +
|
|
165
|
+
`\`subType\` is not \`${LANGUAGE_SUBTYPE}\``,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* What is wrong with a word's `concept`: an id the list does not declare, and
|
|
172
|
+
* an id stated twice.
|
|
173
|
+
*
|
|
174
|
+
* @param {unknown} value - The word's `concept`: an id, or a list of them.
|
|
175
|
+
* @param {Set<string>|undefined} known - The ids the index declares, or
|
|
176
|
+
* `undefined` where there is no index to check against.
|
|
177
|
+
* @returns {Array<{entry: number|null, message: string}>} Each problem and the
|
|
178
|
+
* list entry it sits at, `null` for a single id.
|
|
179
|
+
*/
|
|
180
|
+
export function conceptProblems(value, known) {
|
|
181
|
+
if (!known) return [];
|
|
182
|
+
const single = typeof value === "string";
|
|
183
|
+
const entries = single ? [value] : listOf(value);
|
|
184
|
+
const problems = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
entries.forEach((id, at) => {
|
|
187
|
+
if (typeof id !== "string") return;
|
|
188
|
+
const entry = single ? null : at;
|
|
189
|
+
if (seen.has(id)) {
|
|
190
|
+
problems.push({
|
|
191
|
+
entry,
|
|
192
|
+
message: `\`${id}\` is stated twice in this word's \`concept\``,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
seen.add(id);
|
|
197
|
+
if (known.has(id)) return;
|
|
198
|
+
const near = nearest(id, known);
|
|
199
|
+
problems.push({
|
|
200
|
+
entry,
|
|
201
|
+
message:
|
|
202
|
+
known.size ?
|
|
203
|
+
`\`${id}\` is not a core concept${near ? `; did you mean \`${near}\`?` : ""}`
|
|
204
|
+
: `\`${id}\` is not a core concept: no note states \`data.concepts\``,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
return problems;
|
|
208
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* **Dictionary rows in finished HTML.** {@link groupDictionaryLists} gives a
|
|
16
|
+
* definition list written as HTML the rows and the `dictionary` class that
|
|
17
|
+
* {@link module:engine/definition-list.dictionaryListPlugin} gives one written
|
|
18
|
+
* in Markdown, so both read the same on a page.
|
|
19
|
+
*
|
|
20
|
+
* The page is read as tokens, and a stack of open elements gives each nested
|
|
21
|
+
* list its own rows. Comments, `script`, `style`, `pre` and `textarea` are
|
|
22
|
+
* opaque: a literal example inside one is left as written. A list that already
|
|
23
|
+
* has rows keeps them.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** One token: a comment, an opaque element, a declaration, a tag, text, or a bare `<`. */
|
|
29
|
+
const TOKEN =
|
|
30
|
+
/<!--[^]*?-->|<script\b(?:[^"'<>]|"[^"]*"|'[^']*')*>[^]*?<\/script\s*>|<style\b(?:[^"'<>]|"[^"]*"|'[^']*')*>[^]*?<\/style\s*>|<pre\b(?:[^"'<>]|"[^"]*"|'[^']*')*>[^]*?<\/pre\s*>|<textarea\b(?:[^"'<>]|"[^"]*"|'[^']*')*>[^]*?<\/textarea\s*>|<![^>]*>|<\/?[a-z][a-z0-9:-]*(?:[^"'<>]|"[^"]*"|'[^']*')*>|[^<]+|</gi;
|
|
31
|
+
|
|
32
|
+
/** Elements with no closing tag. */
|
|
33
|
+
const VOID = new Set([
|
|
34
|
+
"area",
|
|
35
|
+
"base",
|
|
36
|
+
"br",
|
|
37
|
+
"col",
|
|
38
|
+
"embed",
|
|
39
|
+
"hr",
|
|
40
|
+
"img",
|
|
41
|
+
"input",
|
|
42
|
+
"link",
|
|
43
|
+
"meta",
|
|
44
|
+
"param",
|
|
45
|
+
"source",
|
|
46
|
+
"track",
|
|
47
|
+
"wbr",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** Add the `dictionary` class to a `<dl …>` opening tag. */
|
|
51
|
+
function withDictionaryClass(token) {
|
|
52
|
+
if (
|
|
53
|
+
/\sclass\s*=\s*["'](?:[^"']*\s)?dictionary(?:\s|["'])/i.test(token) ||
|
|
54
|
+
/\sclass\s*=\s*dictionary(?:\s|>)/i.test(token)
|
|
55
|
+
)
|
|
56
|
+
return token;
|
|
57
|
+
if (/\sclass\s*=\s*["']/i.test(token))
|
|
58
|
+
return token.replace(/(\sclass\s*=\s*["'])/i, "$1dictionary ");
|
|
59
|
+
if (/\sclass\s*=/i.test(token))
|
|
60
|
+
return token.replace(/(\sclass\s*=\s*)([^\s>]+)/i, '$1"dictionary $2"');
|
|
61
|
+
return token.replace(/^<dl/i, '<dl class="dictionary"');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Group each term of every definition list with its definitions.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} html - A page's HTML.
|
|
68
|
+
* @returns {string} The HTML with a `dictionary` class on each `dl` and a `div`
|
|
69
|
+
* around each term and the definitions that follow it.
|
|
70
|
+
*/
|
|
71
|
+
export function groupDictionaryLists(html) {
|
|
72
|
+
const source = String(html ?? "");
|
|
73
|
+
if (!/<dl(?:\s|>)/i.test(source)) return source;
|
|
74
|
+
const tokens = source.match(TOKEN) ?? [];
|
|
75
|
+
const parts = [];
|
|
76
|
+
const lists = [];
|
|
77
|
+
const elements = [];
|
|
78
|
+
tokens.forEach((original, i) => {
|
|
79
|
+
let token = original;
|
|
80
|
+
const previous = tokens[i - 1];
|
|
81
|
+
const next = tokens[i + 1];
|
|
82
|
+
// Whitespace between a term and its definition, and between a definition's
|
|
83
|
+
// opening and its first paragraph, would add a word space to a fixed gap.
|
|
84
|
+
if (
|
|
85
|
+
lists.length &&
|
|
86
|
+
i > 0 &&
|
|
87
|
+
next !== undefined &&
|
|
88
|
+
/^\s+$/.test(token) &&
|
|
89
|
+
((/^<\/dt\s*>$/i.test(previous) && /^<dd(?:\s|>)/i.test(next)) ||
|
|
90
|
+
(/^<dd(?:\s[^>]*)?>$/i.test(previous) && /^<p(?:\s|>)/i.test(next)))
|
|
91
|
+
)
|
|
92
|
+
token = "";
|
|
93
|
+
const opaque = /^<(?:!--|script\b|style\b|pre\b|textarea\b)/i.test(token);
|
|
94
|
+
if (!opaque && /^<\/?[a-z]/i.test(token)) {
|
|
95
|
+
const tag = token.replace(/^<\/?([a-z][a-z0-9:-]*)[^]*$/i, "$1").toLowerCase();
|
|
96
|
+
const closing = token.startsWith("</");
|
|
97
|
+
if (tag === "dl" && !closing) {
|
|
98
|
+
lists.push({ row: false });
|
|
99
|
+
token = withDictionaryClass(token);
|
|
100
|
+
} else if (tag === "dt" && !closing && lists.length && elements.at(-1) === "dl") {
|
|
101
|
+
const list = lists.at(-1);
|
|
102
|
+
if (list.row) parts.push("</div>");
|
|
103
|
+
parts.push("<div>");
|
|
104
|
+
list.row = true;
|
|
105
|
+
} else if (tag === "dl" && closing && lists.length) {
|
|
106
|
+
const list = lists.at(-1);
|
|
107
|
+
if (list.row) parts.push("</div>");
|
|
108
|
+
lists.pop();
|
|
109
|
+
}
|
|
110
|
+
if (closing) {
|
|
111
|
+
if (elements.length) elements.pop();
|
|
112
|
+
} else if (!(/\/\s*>$/.test(token) || VOID.has(tag))) {
|
|
113
|
+
elements.push(tag);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
parts.push(token);
|
|
117
|
+
});
|
|
118
|
+
return parts.join("");
|
|
119
|
+
}
|
|
@@ -143,12 +143,23 @@ const defines = (entry) => typeof entry.definition === "string" && entry.definit
|
|
|
143
143
|
export function pluralOf(word, grammar) {
|
|
144
144
|
if (parseWordClass(word.class).base !== "n") return null;
|
|
145
145
|
if (typeof word.plural === "string" && word.plural) return word.plural;
|
|
146
|
+
return pluralByRule(word.form, grammar);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The plural a language's `data.grammar.plural` rule gives a form: the first
|
|
151
|
+
* literal ending replacement that fits, whatever the form's class.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} form - The form.
|
|
154
|
+
* @param {Record<string, any>|undefined} grammar - Its language's
|
|
155
|
+
* `data.grammar`.
|
|
156
|
+
* @returns {string|null} The plural, or `null` where no rule fits.
|
|
157
|
+
*/
|
|
158
|
+
export function pluralByRule(form, grammar) {
|
|
146
159
|
for (const rule of Array.isArray(grammar?.plural) ? grammar.plural : []) {
|
|
147
160
|
if (typeof rule?.ending !== "string" || typeof rule.becomes !== "string") continue;
|
|
148
|
-
if (
|
|
149
|
-
return (
|
|
150
|
-
(rule.ending ? word.form.slice(0, -rule.ending.length) : word.form) + rule.becomes
|
|
151
|
-
);
|
|
161
|
+
if (form.endsWith(rule.ending))
|
|
162
|
+
return (rule.ending ? form.slice(0, -rule.ending.length) : form) + rule.becomes;
|
|
152
163
|
}
|
|
153
164
|
return null;
|
|
154
165
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import path from "node:path";
|
|
38
|
-
import
|
|
38
|
+
import { writeMatter } from "./note-matter.mjs";
|
|
39
39
|
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
40
40
|
|
|
41
41
|
import { authoredFields, runtimeOnlyFields } from "./field-spec.mjs";
|
|
@@ -368,7 +368,7 @@ function deepMerge(base, overlay) {
|
|
|
368
368
|
* The one question that decides whether `docs item-fields` writes a note
|
|
369
369
|
* envelope: `assets/content/` walks every file under it for its `type:`, so a
|
|
370
370
|
* generated page filed there needs one to publish at all, while a page filed
|
|
371
|
-
* anywhere else — a repository's own `docs/` — is read by nobody but
|
|
371
|
+
* anywhere else — a repository's own `docs/` — is read by nobody but the
|
|
372
372
|
* `--check` guard and the reader following a link, neither of which wants
|
|
373
373
|
* frontmatter.
|
|
374
374
|
*
|
|
@@ -422,7 +422,7 @@ export function shortcodeFromBasename(destination) {
|
|
|
422
422
|
* {@link shortcodeFromBasename}.
|
|
423
423
|
* @param {Record<string, unknown>} [options.frontmatter] - The consumer's
|
|
424
424
|
* declared `docs.itemFields.frontmatter`.
|
|
425
|
-
* @returns {Record<string, unknown>} The envelope, ready for `
|
|
425
|
+
* @returns {Record<string, unknown>} The envelope, ready for `writeMatter`.
|
|
426
426
|
*/
|
|
427
427
|
export function itemFieldsEnvelope({ title, shortcode, frontmatter }) {
|
|
428
428
|
return deepMerge(
|
|
@@ -464,8 +464,8 @@ export function renderItemFieldsPage(body, { title, destination, contentRoot, fr
|
|
|
464
464
|
shortcode: shortcodeFromBasename(destination),
|
|
465
465
|
frontmatter,
|
|
466
466
|
});
|
|
467
|
-
const page =
|
|
468
|
-
// `
|
|
467
|
+
const page = writeMatter(body, envelope);
|
|
468
|
+
// `writeMatter` closes the frontmatter fence directly onto the
|
|
469
469
|
// body's first line; Prettier's markdown printer requires a blank line
|
|
470
470
|
// between them, so a page written without one fails a consumer's
|
|
471
471
|
// `lint:format` the moment it is committed. Insert it here rather than
|
package/engine/first-use.mjs
CHANGED
|
@@ -83,6 +83,14 @@ function readLabel(text, env = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
const labelText = (text, env) => readLabel(text, env).text;
|
|
85
85
|
|
|
86
|
+
/** The kind a span's `data-type` supplies; an invalid value is refused by the span check. */
|
|
87
|
+
function spanKind(span) {
|
|
88
|
+
const value = span.attrs?.find(([key]) => key === "data-type")?.[1];
|
|
89
|
+
if (typeof value !== "string") return undefined;
|
|
90
|
+
const [type, subType] = value.split("/");
|
|
91
|
+
return { type, subType: subType ?? null };
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
/** Join overlapping source ranges so lookups stay logarithmic. */
|
|
87
95
|
function ranges(values) {
|
|
88
96
|
const out = [];
|
|
@@ -119,7 +127,6 @@ function prose(source, capturedNames) {
|
|
|
119
127
|
const links = [],
|
|
120
128
|
regions = [],
|
|
121
129
|
excluded = [],
|
|
122
|
-
bold = [],
|
|
123
130
|
html = [];
|
|
124
131
|
const masked = source
|
|
125
132
|
.replace(/!\[\[[^\]\n]+\]\]|\{\{[^}\n]+\}\}/g, (text, start) => {
|
|
@@ -142,7 +149,6 @@ function prose(source, capturedNames) {
|
|
|
142
149
|
excluded.push({ start, end });
|
|
143
150
|
else if (node.type === "html") html.push(node);
|
|
144
151
|
else if (["strong", "emphasis", "delete", "link", "linkReference"].includes(node.type)) {
|
|
145
|
-
if (node.type === "strong") bold.push({ start, end });
|
|
146
152
|
if (["link", "linkReference"].includes(node.type) && source[start] === "[")
|
|
147
153
|
links.push({ start, end, labelled: true });
|
|
148
154
|
for (const child of node.children ?? []) inline(child);
|
|
@@ -224,18 +230,8 @@ function prose(source, capturedNames) {
|
|
|
224
230
|
const linked = ranges(authoredLinks);
|
|
225
231
|
const marked = (capturedNames ?? scanSpans(source).names)
|
|
226
232
|
.filter((span) => eligible(span) && !contains(linked, span))
|
|
227
|
-
.map((span) => ({ ...span, label: span.visible }));
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
marked,
|
|
231
|
-
links: authoredLinks,
|
|
232
|
-
bold: bold
|
|
233
|
-
.filter((span) => eligible(span) && !overlaps(explicit, span))
|
|
234
|
-
.map((span) => ({
|
|
235
|
-
...span,
|
|
236
|
-
label: labelText(source.slice(span.start, span.end), env),
|
|
237
|
-
})),
|
|
238
|
-
};
|
|
233
|
+
.map((span) => ({ ...span, label: span.visible, nameKind: spanKind(span) }));
|
|
234
|
+
return { marked, links: authoredLinks };
|
|
239
235
|
}
|
|
240
236
|
|
|
241
237
|
/**
|
|
@@ -264,7 +260,7 @@ export function createFirstUseChecker({
|
|
|
264
260
|
{ raw, bodyLine: authored.bodyLine, bodyColumn: authored.bodyColumn },
|
|
265
261
|
)
|
|
266
262
|
: undefined;
|
|
267
|
-
const { marked, links
|
|
263
|
+
const { marked, links } = prose(raw, capturedNames);
|
|
268
264
|
const events = [
|
|
269
265
|
...marked.map((span) => ({ ...span, kind: "marked" })),
|
|
270
266
|
...links.map((link) => {
|
|
@@ -286,7 +282,10 @@ export function createFirstUseChecker({
|
|
|
286
282
|
for (const event of [...events].sort(
|
|
287
283
|
(a, b) => a.label.length - b.label.length || a.label.localeCompare(b.label),
|
|
288
284
|
)) {
|
|
289
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
recognized(event.label, event.preferred, event.nameKind) ||
|
|
287
|
+
unknown.has(normalize(event.label))
|
|
288
|
+
)
|
|
290
289
|
continue;
|
|
291
290
|
const entry = {
|
|
292
291
|
key: "name:" + normalize(event.label),
|
|
@@ -309,7 +308,8 @@ export function createFirstUseChecker({
|
|
|
309
308
|
fullUsed = new Set();
|
|
310
309
|
for (const event of events.sort((a, b) => a.start - b.start)) {
|
|
311
310
|
const entry =
|
|
312
|
-
recognized(event.label, event.preferred
|
|
311
|
+
recognized(event.label, event.preferred, event.nameKind) ??
|
|
312
|
+
unknown.get(normalize(event.label));
|
|
313
313
|
if (event.kind === "marked" && entry.indexed) {
|
|
314
314
|
const destinations = (entry.candidates ?? [entry])
|
|
315
315
|
.map((target) => target.key)
|
|
@@ -338,16 +338,6 @@ export function createFirstUseChecker({
|
|
|
338
338
|
used.add(entry.key);
|
|
339
339
|
if (introduces) fullUsed.add(entry.key);
|
|
340
340
|
}
|
|
341
|
-
for (const span of bold) {
|
|
342
|
-
if (typeof span.label !== "string") continue;
|
|
343
|
-
const entry = recognized(span.label);
|
|
344
|
-
if (entry)
|
|
345
|
-
add(
|
|
346
|
-
span.start,
|
|
347
|
-
"bold-name",
|
|
348
|
-
`“${span.label}” matches an indexed name; mark or link this name`,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
341
|
const lineStarts = [0];
|
|
352
342
|
for (let i = 0; i < raw.length; i++) if (raw[i] === "\n") lineStarts.push(i + 1);
|
|
353
343
|
return [...findings]
|
|
@@ -813,7 +813,7 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
|
|
|
813
813
|
* lands at — would mean checking the value against the sections that could
|
|
814
814
|
* exist (every content type, plus whatever a repository configured) rather
|
|
815
815
|
* than against the genres its type declares. Two vocabularies in one field is
|
|
816
|
-
* the cause, and it is removed rather than the symptom: a section is a
|
|
816
|
+
* the cause, and it is removed rather than the symptom: a section is a
|
|
817
817
|
* directory the note format does not
|
|
818
818
|
* carry, and a page introducing a type is an ordinary note addressed
|
|
819
819
|
* `doc-<type>`. So the closed list answers for every note, whatever it is
|