@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,134 @@
|
|
|
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 metre is declared once, as an entry of a `doc` note's `data.metres`**:
|
|
16
|
+
* its `name`, the `beats` of a line and the `kind` of count. This module holds
|
|
17
|
+
* the declaration and its field check, and imports nothing that reads the note
|
|
18
|
+
* vocabulary, which declares the field with them; reading the metres of an
|
|
19
|
+
* index and counting a verse against them is {@link module:engine/verse-metre}.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
25
|
+
|
|
26
|
+
/** Whether a value is a plain map. */
|
|
27
|
+
const isMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
/** A list value, or an empty one. */
|
|
30
|
+
const listOf = (value) => (Array.isArray(value) ? value : []);
|
|
31
|
+
|
|
32
|
+
/** What a metre's `kind` may be. */
|
|
33
|
+
export const METRE_KINDS = Object.freeze(["accentual"]);
|
|
34
|
+
|
|
35
|
+
/** What a metre's `name` may be. */
|
|
36
|
+
export const METRE_NAME = /^[a-z0-9-]+$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One declared metre.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} Metre
|
|
42
|
+
* @property {string} name - What `scan=` names it by.
|
|
43
|
+
* @property {number} beats - The beats of a line.
|
|
44
|
+
* @property {string} kind - How they are counted: `accentual`.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** The notes declaring each metre name, read once per lint index. */
|
|
48
|
+
const declaredNames = new WeakMap();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Each metre name the index's notes declare, and the notes declaring it.
|
|
52
|
+
*
|
|
53
|
+
* @param {{notes?: Iterable<{file: string, fm?: Record<string, any>}>}} index - The lint index.
|
|
54
|
+
* @returns {Map<string, string[]>} Name → the files that declare it.
|
|
55
|
+
*/
|
|
56
|
+
function namesIn(index) {
|
|
57
|
+
const known = declaredNames.get(index);
|
|
58
|
+
if (known) return known;
|
|
59
|
+
const names = new Map();
|
|
60
|
+
for (const note of index.notes ?? [])
|
|
61
|
+
for (const entry of listOf(note?.fm?.data?.metres)) {
|
|
62
|
+
if (!isMap(entry) || typeof entry.name !== "string") continue;
|
|
63
|
+
if (!names.has(entry.name)) names.set(entry.name, new Set());
|
|
64
|
+
names.get(entry.name).add(note.file);
|
|
65
|
+
}
|
|
66
|
+
const out = new Map([...names].map(([name, files]) => [name, [...files]]));
|
|
67
|
+
declaredNames.set(index, out);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check a note's `data.metres`: each name's form, that no other entry declares
|
|
73
|
+
* it, and that a beat count is a whole number above zero. The data-key check
|
|
74
|
+
* owns every key, its kind and the closed `kind` list.
|
|
75
|
+
*
|
|
76
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
77
|
+
* @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index.
|
|
78
|
+
* @returns {object[]} The findings, each at its key.
|
|
79
|
+
*/
|
|
80
|
+
export function checkMetresField(note, { index } = {}) {
|
|
81
|
+
const list = note?.fm?.data?.metres;
|
|
82
|
+
if (!Array.isArray(list)) return [];
|
|
83
|
+
const raw = note.raw ?? "";
|
|
84
|
+
const finding = (keyPath, message) => ({
|
|
85
|
+
file: note.file,
|
|
86
|
+
...positionOfFrontmatterPath(raw, ["data", "metres", ...keyPath]),
|
|
87
|
+
severity: "error",
|
|
88
|
+
message,
|
|
89
|
+
});
|
|
90
|
+
const findings = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
list.forEach((entry, at) => {
|
|
93
|
+
if (!isMap(entry)) return;
|
|
94
|
+
const { name, beats } = entry;
|
|
95
|
+
if (typeof name === "string") {
|
|
96
|
+
const label = `data.metres[${at}].name`;
|
|
97
|
+
if (!METRE_NAME.test(name))
|
|
98
|
+
findings.push(
|
|
99
|
+
finding(
|
|
100
|
+
[at, "name"],
|
|
101
|
+
`\`${label}\` is \`${name}\`; a metre's name is lowercase letters, digits and \`-\``,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
else if (seen.has(name))
|
|
105
|
+
findings.push(
|
|
106
|
+
finding(
|
|
107
|
+
[at, "name"],
|
|
108
|
+
`\`${label}\` is \`${name}\`, which this note declares twice; a metre's name is unique`,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
else {
|
|
112
|
+
const others = (index ? (namesIn(index).get(name) ?? []) : []).filter(
|
|
113
|
+
(file) => file !== note.file,
|
|
114
|
+
);
|
|
115
|
+
if (others.length)
|
|
116
|
+
findings.push(
|
|
117
|
+
finding(
|
|
118
|
+
[at, "name"],
|
|
119
|
+
`\`${label}\` is \`${name}\`, which ${others.join(", ")} also declares; a metre's name is unique`,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
seen.add(name);
|
|
124
|
+
}
|
|
125
|
+
if (beats !== undefined && Number.isFinite(Number(beats)) && !(Number(beats) >= 1))
|
|
126
|
+
findings.push(
|
|
127
|
+
finding(
|
|
128
|
+
[at, "beats"],
|
|
129
|
+
`\`data.metres[${at}].beats\` is \`${String(beats)}\`; a metre has one or more beats`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
return findings;
|
|
134
|
+
}
|
package/engine/names.mjs
CHANGED
|
@@ -33,6 +33,8 @@ const compare = (a, b) =>
|
|
|
33
33
|
* @property {string} full - Declared full name.
|
|
34
34
|
* @property {true} indexed - The identity belongs to the indexed catalog.
|
|
35
35
|
* @property {boolean} [fullUse] - The label is a full-name form rather than an alias.
|
|
36
|
+
* @property {string} [type] - The note's type, absent on an ambiguous label identity.
|
|
37
|
+
* @property {string|null} [subType] - The note's subtype, or null.
|
|
36
38
|
* @property {NameMatch[]} [candidates] - Indexed identities sharing the label.
|
|
37
39
|
* @property {string} [destination] - Canonical Address used by a resolved link.
|
|
38
40
|
*/
|
|
@@ -40,10 +42,18 @@ const compare = (a, b) =>
|
|
|
40
42
|
/**
|
|
41
43
|
* The prepared name and declared-alias lookup.
|
|
42
44
|
* @typedef {object} NameCatalog
|
|
43
|
-
* @property {(label: string, preferred?: {key: string}) => NameMatch|undefined} recognize - Match a whole visible label.
|
|
45
|
+
* @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.
|
|
44
46
|
* @property {(target: string) => NameMatch|undefined} resolve - Resolve an Address target without its anchor to its indexed identity.
|
|
45
47
|
*/
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* What a typed `.name` span says the named thing is. A null type matches any note;
|
|
51
|
+
* a type matches only notes of that type, and a subtype only notes of that subtype.
|
|
52
|
+
* @typedef {object} NameKind
|
|
53
|
+
* @property {string|null} type - Supplied type, or null.
|
|
54
|
+
* @property {string|null} subType - Supplied subtype, or null.
|
|
55
|
+
*/
|
|
56
|
+
|
|
47
57
|
/**
|
|
48
58
|
* One captured marked-name occurrence.
|
|
49
59
|
* @typedef {object} MarkedName
|
|
@@ -150,10 +160,16 @@ export function createNameCatalog({ records, index }) {
|
|
|
150
160
|
const names = new Map(),
|
|
151
161
|
byFile = new Map(),
|
|
152
162
|
entries = new Map();
|
|
153
|
-
const addName = (key, full, aliases
|
|
154
|
-
const entry = entries.get(key) ?? {
|
|
163
|
+
const addName = (key, full, aliases, type, subType) => {
|
|
164
|
+
const entry = entries.get(key) ?? {
|
|
165
|
+
key,
|
|
166
|
+
full,
|
|
167
|
+
indexed: true,
|
|
168
|
+
type,
|
|
169
|
+
subType: typeof subType === "string" && subType ? subType : null,
|
|
170
|
+
};
|
|
155
171
|
entries.set(key, entry);
|
|
156
|
-
for (const text of [full, ...aliases]) {
|
|
172
|
+
for (const text of [full, ...(aliases ?? [])]) {
|
|
157
173
|
if (typeof text !== "string" || !text.trim()) continue;
|
|
158
174
|
for (const spelling of nameForms(text)) {
|
|
159
175
|
if (!names.has(spelling)) names.set(spelling, new Map());
|
|
@@ -173,7 +189,13 @@ export function createNameCatalog({ records, index }) {
|
|
|
173
189
|
const full = record.name?.full;
|
|
174
190
|
if (!parts || parts.type === FOLDER_TYPE || typeof full !== "string") continue;
|
|
175
191
|
const page = canonicalKey(parts.package, "note", parts.type, parts.shortcode);
|
|
176
|
-
const entry = addName(
|
|
192
|
+
const entry = addName(
|
|
193
|
+
index.resolve(page) ? page : canonical,
|
|
194
|
+
full,
|
|
195
|
+
record.name.aliases,
|
|
196
|
+
parts.type,
|
|
197
|
+
record.subType,
|
|
198
|
+
);
|
|
177
199
|
entries.set(canonical, entry);
|
|
178
200
|
if (index.resolve(page)) entries.set(page, entry);
|
|
179
201
|
if (record.file?.path) byFile.set(record.file.path, entry);
|
|
@@ -189,11 +211,16 @@ export function createNameCatalog({ records, index }) {
|
|
|
189
211
|
continue;
|
|
190
212
|
const page = canonicalKey(parts.package, "note", parts.type, parts.shortcode);
|
|
191
213
|
const key = index.foreign.index.has(page) ? page : canonical;
|
|
192
|
-
entries.set(canonical, addName(key, hit.name, hit.aliases));
|
|
214
|
+
entries.set(canonical, addName(key, hit.name, hit.aliases, parts.type, hit.subType));
|
|
193
215
|
}
|
|
194
216
|
return {
|
|
195
|
-
recognize(label, preferred) {
|
|
196
|
-
const candidates = [...(names.get(normalizeName(label))?.values() ?? [])]
|
|
217
|
+
recognize(label, preferred, kind) {
|
|
218
|
+
const candidates = [...(names.get(normalizeName(label))?.values() ?? [])].filter(
|
|
219
|
+
(entry) =>
|
|
220
|
+
!kind?.type ||
|
|
221
|
+
(entry.type === kind.type &&
|
|
222
|
+
(kind.subType == null || entry.subType === kind.subType)),
|
|
223
|
+
);
|
|
197
224
|
const target =
|
|
198
225
|
candidates.find((entry) => entry.key === preferred?.key) ??
|
|
199
226
|
(candidates.length === 1 ? candidates[0] : undefined);
|
|
@@ -268,7 +295,7 @@ export function collectUnwrittenNames({ records, catalog }) {
|
|
|
268
295
|
const kinds = new Map();
|
|
269
296
|
for (const record of records.filter(isNoteRecord)) {
|
|
270
297
|
for (const occurrence of record.names ?? []) {
|
|
271
|
-
if (catalog.recognize(occurrence.name)) continue;
|
|
298
|
+
if (catalog.recognize(occurrence.name, undefined, occurrence)) continue;
|
|
272
299
|
const type = occurrence.type,
|
|
273
300
|
subType = occurrence.subType,
|
|
274
301
|
kind = JSON.stringify([type, subType]);
|
|
@@ -0,0 +1,66 @@
|
|
|
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 page's frontmatter, read and written.** Reading splits and parses a note
|
|
16
|
+
* exactly as the content index does ({@link splitFrontmatter} and `yaml`), so a
|
|
17
|
+
* value reads the same on the site and in the preview as in a compendium pack.
|
|
18
|
+
* Writing serializes with `js-yaml`, which quotes every string a YAML 1.1
|
|
19
|
+
* reader would take for a boolean, a number or a timestamp, so each
|
|
20
|
+
* value reads back as the string or number it was.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import YAML from "yaml";
|
|
26
|
+
import { dump } from "js-yaml";
|
|
27
|
+
import { splitFrontmatter } from "./diagnostics.mjs";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A page split at its frontmatter.
|
|
31
|
+
*
|
|
32
|
+
* @typedef {object} NoteMatter
|
|
33
|
+
* @property {unknown} data - The parsed frontmatter; an empty map where the
|
|
34
|
+
* page has none or the block is empty.
|
|
35
|
+
* @property {string} content - The body after the closing fence and its line
|
|
36
|
+
* break, or the whole text where there is no frontmatter.
|
|
37
|
+
* @property {string} matter - The YAML source between the fences; empty where
|
|
38
|
+
* there is no frontmatter.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Split a page's frontmatter from its body and parse it.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} source - The complete Markdown source.
|
|
45
|
+
* @returns {NoteMatter} The parsed frontmatter, the body and the YAML source.
|
|
46
|
+
* @throws {Error} The `yaml` parse error, where the frontmatter is not YAML.
|
|
47
|
+
*/
|
|
48
|
+
export function readMatter(source) {
|
|
49
|
+
const fence = splitFrontmatter(source);
|
|
50
|
+
if (!fence) return { data: {}, content: source, matter: "" };
|
|
51
|
+
return { data: YAML.parse(fence.yaml) ?? {}, content: fence.body, matter: fence.yaml };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A page with `data` as its frontmatter above `content`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} content - The body.
|
|
58
|
+
* @param {Record<string, unknown>} data - The frontmatter.
|
|
59
|
+
* @returns {string} The fenced YAML, then the body closed on a line break; the
|
|
60
|
+
* body alone where `data` serializes to an empty map.
|
|
61
|
+
*/
|
|
62
|
+
export function writeMatter(content, data) {
|
|
63
|
+
const yaml = dump(data).trim();
|
|
64
|
+
const body = content.endsWith("\n") ? content : `${content}\n`;
|
|
65
|
+
return yaml === "{}" ? body : `---\n${yaml}\n---\n${body}`;
|
|
66
|
+
}
|
|
@@ -112,6 +112,12 @@ import { checkNoteEvents } from "./note-events.mjs";
|
|
|
112
112
|
import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
|
|
113
113
|
import { EVENT_ENTRY } from "./note-event-terms.mjs";
|
|
114
114
|
import { CULTURE_FIELD, checkCultureSubType } from "./culture-choice.mjs";
|
|
115
|
+
import {
|
|
116
|
+
POST_AUTHOR_FIELD,
|
|
117
|
+
POST_DATE_FIELD,
|
|
118
|
+
POST_SUBTYPE,
|
|
119
|
+
checkPostFields,
|
|
120
|
+
} from "./post-fields.mjs";
|
|
115
121
|
import { checkCultureParents } from "./culture-parents.mjs";
|
|
116
122
|
import {
|
|
117
123
|
checkAppearanceProfile,
|
|
@@ -154,6 +160,10 @@ import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
|
154
160
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
155
161
|
import { checkSettlementTables } from "./settlement-tables.mjs";
|
|
156
162
|
import { checkMapField } from "./text-maps.mjs";
|
|
163
|
+
import { checkConceptList, checkCoreFlag } from "./core-vocabulary.mjs";
|
|
164
|
+
import { METRE_KINDS, checkMetresField } from "./metre-declaration.mjs";
|
|
165
|
+
import { checkLanguageEmphasis, checkScriptField } from "./script-emphasis.mjs";
|
|
166
|
+
import { EMPHASIS_CONVENTIONS, LANGUAGE_EMPHASES } from "./script-emphasis-terms.mjs";
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
169
|
* One `data:` key a note type may carry.
|
|
@@ -655,6 +665,67 @@ const ABBREVIATION_FIELD = Object.freeze({
|
|
|
655
665
|
"note only. See [a language's words](format-details.md#a-languages-words).",
|
|
656
666
|
});
|
|
657
667
|
|
|
668
|
+
/**
|
|
669
|
+
* A language's `data.core`: whether it must cover the core vocabulary.
|
|
670
|
+
* {@link checkCoreFlag} checks where it sits.
|
|
671
|
+
*
|
|
672
|
+
* @type {DataFieldSpec}
|
|
673
|
+
*/
|
|
674
|
+
const CORE_FIELD = Object.freeze({
|
|
675
|
+
name: "core",
|
|
676
|
+
kind: "boolean",
|
|
677
|
+
shape: "`true` or `false`",
|
|
678
|
+
check: checkCoreFlag,
|
|
679
|
+
describe:
|
|
680
|
+
"Whether the language must cover the core vocabulary: `true` lists it in the " +
|
|
681
|
+
"`package-build core` report. A language skill note only. See " +
|
|
682
|
+
"[the core vocabulary](format-details.md#the-core-vocabulary).",
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* A `doc` note's `data.concepts`: the core vocabulary's concept list.
|
|
687
|
+
* {@link checkConceptList} checks what an entry means.
|
|
688
|
+
*
|
|
689
|
+
* @type {DataFieldSpec}
|
|
690
|
+
*/
|
|
691
|
+
const CONCEPTS_FIELD = Object.freeze({
|
|
692
|
+
name: "concepts",
|
|
693
|
+
kind: "list",
|
|
694
|
+
shape: "list of `{ id, gloss, group }`",
|
|
695
|
+
check: checkConceptList,
|
|
696
|
+
entries: Object.freeze({
|
|
697
|
+
kind: "map",
|
|
698
|
+
shape: "a map — `{ id, gloss, group }`",
|
|
699
|
+
fields: Object.freeze([
|
|
700
|
+
Object.freeze({
|
|
701
|
+
name: "id",
|
|
702
|
+
kind: "string",
|
|
703
|
+
required: true,
|
|
704
|
+
shape: "lowercase letters, digits and `-`",
|
|
705
|
+
describe:
|
|
706
|
+
"The concept's id, which a word's `concept` names. Unique in the package.",
|
|
707
|
+
}),
|
|
708
|
+
Object.freeze({
|
|
709
|
+
name: "gloss",
|
|
710
|
+
kind: "string",
|
|
711
|
+
required: true,
|
|
712
|
+
shape: "a short gloss, such as `a smell; a scent`",
|
|
713
|
+
describe: "What the concept means, in brief.",
|
|
714
|
+
}),
|
|
715
|
+
Object.freeze({
|
|
716
|
+
name: "group",
|
|
717
|
+
kind: "string",
|
|
718
|
+
required: true,
|
|
719
|
+
shape: "a group name, such as `senses`",
|
|
720
|
+
describe: "The group the report lists the concept under.",
|
|
721
|
+
}),
|
|
722
|
+
]),
|
|
723
|
+
}),
|
|
724
|
+
describe:
|
|
725
|
+
"The concepts of a core vocabulary, in the order the report lists them. See " +
|
|
726
|
+
"[the core vocabulary](format-details.md#the-core-vocabulary).",
|
|
727
|
+
});
|
|
728
|
+
|
|
658
729
|
/**
|
|
659
730
|
* A culture's or an affiliation's `data.glossary`: the terms that belong to
|
|
660
731
|
* its people or members rather than to a language's grammar.
|
|
@@ -2096,8 +2167,64 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2096
2167
|
NAMES_FIELD,
|
|
2097
2168
|
FAMILY_FIELD,
|
|
2098
2169
|
ABBREVIATION_FIELD,
|
|
2170
|
+
CORE_FIELD,
|
|
2099
2171
|
...WORD_DATA_FIELDS,
|
|
2100
2172
|
...LANGUAGE_DATA_FIELDS,
|
|
2173
|
+
{
|
|
2174
|
+
name: "emphasis",
|
|
2175
|
+
kind: "string",
|
|
2176
|
+
shape: "`words`, `rubric` or `overline`",
|
|
2177
|
+
allowed: LANGUAGE_EMPHASES,
|
|
2178
|
+
check: checkLanguageEmphasis,
|
|
2179
|
+
describe:
|
|
2180
|
+
"How the language marks a stressed word, over its script's `emphasis`: " +
|
|
2181
|
+
"`rubric` or `overline` sets `*…*` and `**…**` so in a run of the language, " +
|
|
2182
|
+
"and `words` marks it with a word, so either is a finding. Only a skill of " +
|
|
2183
|
+
"`subType: language` states it.",
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
name: "script",
|
|
2187
|
+
kind: "map",
|
|
2188
|
+
shape: "a map — `{ fonts, emphasis, strongEmphasis? }`",
|
|
2189
|
+
check: checkScriptField,
|
|
2190
|
+
fields: Object.freeze([
|
|
2191
|
+
{
|
|
2192
|
+
name: "fonts",
|
|
2193
|
+
kind: "list",
|
|
2194
|
+
shape: "list of installed faces' slugs",
|
|
2195
|
+
required: true,
|
|
2196
|
+
entries: Object.freeze({
|
|
2197
|
+
kind: "string",
|
|
2198
|
+
shape: "an installed face's slug",
|
|
2199
|
+
}),
|
|
2200
|
+
describe:
|
|
2201
|
+
"The faces the script is written in, by the slug `font=` names them by. " +
|
|
2202
|
+
"A face belongs to one script.",
|
|
2203
|
+
},
|
|
2204
|
+
{
|
|
2205
|
+
name: "emphasis",
|
|
2206
|
+
kind: "string",
|
|
2207
|
+
shape: "`rubric` or `overline`",
|
|
2208
|
+
allowed: EMPHASIS_CONVENTIONS,
|
|
2209
|
+
required: true,
|
|
2210
|
+
describe:
|
|
2211
|
+
"How the script marks a stressed word: `rubric` sets it in red, " +
|
|
2212
|
+
"`overline` draws a line above it. It sets `*…*` and `**…**` inside a " +
|
|
2213
|
+
"run in one of its faces, unless the run's language states its own " +
|
|
2214
|
+
"`emphasis`.",
|
|
2215
|
+
},
|
|
2216
|
+
{
|
|
2217
|
+
name: "strongEmphasis",
|
|
2218
|
+
kind: "string",
|
|
2219
|
+
shape: "`rubric` or `overline`",
|
|
2220
|
+
allowed: EMPHASIS_CONVENTIONS,
|
|
2221
|
+
describe: "The convention `**…**` takes instead of `emphasis`'s.",
|
|
2222
|
+
},
|
|
2223
|
+
]),
|
|
2224
|
+
describe:
|
|
2225
|
+
"A script note's faces and how it marks emphasis. Only a skill of " +
|
|
2226
|
+
"`subType: script` states it.",
|
|
2227
|
+
},
|
|
2101
2228
|
]),
|
|
2102
2229
|
}),
|
|
2103
2230
|
|
|
@@ -2176,14 +2303,20 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2176
2303
|
"howto",
|
|
2177
2304
|
"concept",
|
|
2178
2305
|
"settingguide",
|
|
2306
|
+
POST_SUBTYPE,
|
|
2179
2307
|
]),
|
|
2180
|
-
// A setting guide introduces one culture and names it
|
|
2181
|
-
// states
|
|
2308
|
+
// A setting guide introduces one culture and names it, and a post
|
|
2309
|
+
// states the day it was published and who wrote it; no other genre
|
|
2310
|
+
// states either.
|
|
2182
2311
|
check: (note, context) => [
|
|
2183
2312
|
...checkCitedPopulations(note, context),
|
|
2184
2313
|
...checkCultureSubType(note, context),
|
|
2314
|
+
...checkPostFields(note),
|
|
2185
2315
|
],
|
|
2186
2316
|
data: Object.freeze([
|
|
2317
|
+
CONCEPTS_FIELD,
|
|
2318
|
+
POST_DATE_FIELD,
|
|
2319
|
+
POST_AUTHOR_FIELD,
|
|
2187
2320
|
{
|
|
2188
2321
|
...CULTURE_FIELD,
|
|
2189
2322
|
describe:
|
|
@@ -2193,7 +2326,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2193
2326
|
{
|
|
2194
2327
|
name: "map",
|
|
2195
2328
|
kind: "map",
|
|
2196
|
-
shape: "a map — `{ name, vowels?, fonts, rows }`",
|
|
2329
|
+
shape: "a map — `{ name, logographic?, vowels?, fonts, rows }`",
|
|
2197
2330
|
check: checkMapField,
|
|
2198
2331
|
fields: Object.freeze([
|
|
2199
2332
|
{
|
|
@@ -2204,6 +2337,15 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2204
2337
|
"What `map=` names the map by: lowercase letters, digits and `-`, " +
|
|
2205
2338
|
"unique in the package.",
|
|
2206
2339
|
},
|
|
2340
|
+
{
|
|
2341
|
+
name: "logographic",
|
|
2342
|
+
kind: "boolean",
|
|
2343
|
+
shape: "`true`",
|
|
2344
|
+
describe:
|
|
2345
|
+
"The script writes meanings: a mapped run's words are written by " +
|
|
2346
|
+
"the language's lexicon alone, and the rows write only the text " +
|
|
2347
|
+
"between them.",
|
|
2348
|
+
},
|
|
2207
2349
|
{
|
|
2208
2350
|
name: "vowels",
|
|
2209
2351
|
...TEXT,
|
|
@@ -2261,6 +2403,48 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2261
2403
|
"before it is set. See " +
|
|
2262
2404
|
"[mapping notes](format-details.md#mapping-notes).",
|
|
2263
2405
|
},
|
|
2406
|
+
{
|
|
2407
|
+
name: "metres",
|
|
2408
|
+
kind: "list",
|
|
2409
|
+
shape: "list of `{ name, beats, kind }`",
|
|
2410
|
+
check: checkMetresField,
|
|
2411
|
+
entries: Object.freeze({
|
|
2412
|
+
kind: "map",
|
|
2413
|
+
shape: "a map — `{ name, beats, kind }`",
|
|
2414
|
+
fields: Object.freeze([
|
|
2415
|
+
{
|
|
2416
|
+
name: "name",
|
|
2417
|
+
...TEXT,
|
|
2418
|
+
required: true,
|
|
2419
|
+
describe:
|
|
2420
|
+
"What a verse fence's `scan=` names the metre by: lowercase " +
|
|
2421
|
+
"letters, digits and `-`, unique in the package.",
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
name: "beats",
|
|
2425
|
+
kind: "integer",
|
|
2426
|
+
shape: "a whole number above zero",
|
|
2427
|
+
required: true,
|
|
2428
|
+
describe: "The beats of one line.",
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
name: "kind",
|
|
2432
|
+
kind: "string",
|
|
2433
|
+
allowed: METRE_KINDS,
|
|
2434
|
+
shape: `one of ${METRE_KINDS.join(", ")}`,
|
|
2435
|
+
required: true,
|
|
2436
|
+
describe:
|
|
2437
|
+
"How the beats are counted: `accentual`, one beat for each " +
|
|
2438
|
+
"stressed syllable the language's stress rule and the beat " +
|
|
2439
|
+
"convention give.",
|
|
2440
|
+
},
|
|
2441
|
+
]),
|
|
2442
|
+
}),
|
|
2443
|
+
describe:
|
|
2444
|
+
"The metres a verse fence's `scan=` names. A fence that states one is " +
|
|
2445
|
+
"warned at each line that cannot have its beats. See " +
|
|
2446
|
+
"[metres](../authoring/links-and-markup.md#metre).",
|
|
2447
|
+
},
|
|
2264
2448
|
]),
|
|
2265
2449
|
}),
|
|
2266
2450
|
|