@heroiclands/package-build 23.13.0 → 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 +36 -0
- package/README.md +2 -1
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +150 -1
- package/bin/content-commands.mjs +356 -210
- 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 +93 -72
- package/docs/authoring/links-and-markup.md +316 -78
- package/docs/commands.md +244 -78
- package/docs/configuration.md +210 -102
- package/docs/content-format.md +2 -2
- package/docs/diagnostics.md +2 -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 +589 -308
- package/docs/reference/note-types.md +40 -10
- package/docs/reference/theme-pages.md +127 -108
- package/docs/source-preview.md +15 -1
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +26 -11
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +13 -6
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- 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 +326 -27
- package/engine/content-spans.mjs +4 -1
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +6 -18
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/field-reference.mjs +1 -1
- 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/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +115 -14
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +221 -25
- 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/note-matter.mjs +2 -2
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +68 -133
- package/engine/pdf-render.mjs +102 -44
- 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/run-words.mjs +3 -2
- 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 +259 -314
- 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 +50 -523
- 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/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 +39 -24
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +29 -4
- package/manifest.mjs +3 -4
- package/package.json +10 -7
- 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 +14 -0
- 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/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 +62 -0
- 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/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/field-reference.d.mts +1 -1
- 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/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +48 -8
- 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/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/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/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 +3 -2
- 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/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/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,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
|
+
}
|
|
@@ -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
|
*
|
|
@@ -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
|
|
@@ -216,6 +216,95 @@ export function headingAttributesPlugin(md) {
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The id a heading's text gives it on the web: GitHub's form, lower case, each
|
|
221
|
+
* space or hyphen a hyphen, every character that is not a letter, a digit or
|
|
222
|
+
* an underscore dropped.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} text - The heading's text, without its markup.
|
|
225
|
+
* @returns {string} The id, or `heading` when nothing is left of the text.
|
|
226
|
+
*/
|
|
227
|
+
export function headingId(text) {
|
|
228
|
+
let id = "";
|
|
229
|
+
for (const char of String(text ?? "").trim()) {
|
|
230
|
+
if (char === " " || char === "-") id += "-";
|
|
231
|
+
else if (char === "_" || /[\p{L}\p{Nd}]/u.test(char)) id += char.toLowerCase();
|
|
232
|
+
}
|
|
233
|
+
return id || "heading";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The tokens whose content is a heading's text. */
|
|
237
|
+
const TEXT_TOKENS = new Set(["text", "code_inline", "typographic"]);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The text a heading's id is made from.
|
|
241
|
+
*
|
|
242
|
+
* Its words, code and punctuation; a line break as a newline; no markup. An
|
|
243
|
+
* element inside the heading — emphasis, a link, an image — gives only its
|
|
244
|
+
* first piece of text, which ends at the first punctuation the page sets
|
|
245
|
+
* typographically, so `[Nkaru'thar](…)` gives `Nkaru`.
|
|
246
|
+
*
|
|
247
|
+
* @param {object[]} children - The heading's inline tokens.
|
|
248
|
+
* @returns {string} The text.
|
|
249
|
+
*/
|
|
250
|
+
function headingText(children) {
|
|
251
|
+
const list = children ?? [];
|
|
252
|
+
/** The first piece of text inside the element opening at `index`. */
|
|
253
|
+
const first = (index) => {
|
|
254
|
+
const token = list[index];
|
|
255
|
+
if (!token || token.nesting === -1) return "";
|
|
256
|
+
if (TEXT_TOKENS.has(token.type)) return token.content;
|
|
257
|
+
if (token.type === "image") return headingText(token.children?.slice(0, 1));
|
|
258
|
+
if (token.nesting === 1) return first(index + 1);
|
|
259
|
+
return "";
|
|
260
|
+
};
|
|
261
|
+
let text = "";
|
|
262
|
+
for (let i = 0; i < list.length; i++) {
|
|
263
|
+
const token = list[i];
|
|
264
|
+
if (TEXT_TOKENS.has(token.type)) text += token.content;
|
|
265
|
+
else if (token.type === "softbreak" || token.type === "hardbreak") text += "\n";
|
|
266
|
+
else if (token.type === "image") text += headingText(token.children?.slice(0, 1));
|
|
267
|
+
else if (token.nesting === 1) {
|
|
268
|
+
text += first(i + 1);
|
|
269
|
+
for (let depth = 1; depth > 0 && i + 1 < list.length;) {
|
|
270
|
+
i++;
|
|
271
|
+
depth += list[i].nesting;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Give every heading an id, so a page's contents list and a reader's link can
|
|
280
|
+
* name any section.
|
|
281
|
+
*
|
|
282
|
+
* An authored `{#id}` is kept, and every other heading takes {@link headingId}
|
|
283
|
+
* of its text. A repeated id takes `-1`, `-2` and so on, counted over every id
|
|
284
|
+
* on the page that came before it.
|
|
285
|
+
*
|
|
286
|
+
* @param {import("markdown-it")} md - Markdown parser to extend.
|
|
287
|
+
*/
|
|
288
|
+
export function headingIdsPlugin(md) {
|
|
289
|
+
md.core.ruler.push("heading_ids", (state) => {
|
|
290
|
+
const taken = new Set();
|
|
291
|
+
const tokens = state.tokens;
|
|
292
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
293
|
+
if (tokens[i].type !== "heading_open") continue;
|
|
294
|
+
const authored = tokens[i].attrGet("id");
|
|
295
|
+
if (authored) {
|
|
296
|
+
taken.add(authored);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const base = headingId(headingText(tokens[i + 1]?.children));
|
|
300
|
+
let id = base;
|
|
301
|
+
for (let n = 1; taken.has(id); n++) id = `${base}-${n}`;
|
|
302
|
+
taken.add(id);
|
|
303
|
+
tokens[i].attrSet("id", id);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
219
308
|
/**
|
|
220
309
|
* Parse one line as a heading, and say whether it starts a journal page.
|
|
221
310
|
*
|