@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
package/engine/run-words.mjs
CHANGED
|
@@ -17,20 +17,14 @@
|
|
|
17
17
|
* each word (see {@link module:engine/word-pattern.WORD}) is accepted when,
|
|
18
18
|
* case folded, it is:
|
|
19
19
|
*
|
|
20
|
-
* - a `data.words[].form` of the language, or a word of one
|
|
20
|
+
* - a `data.words[].form` of the language, or a word of one, or a declared
|
|
21
|
+
* `variants` entry of a word;
|
|
21
22
|
* - a word of a name in the language note's own `data.register` whose `tongue`
|
|
22
23
|
* is not `translation`, so a foreign name the run uses is listed there first,
|
|
23
24
|
* as `other:<tongue>` or under another tag the note declares;
|
|
24
|
-
* - a form one rule the language states derives from a lexicon form:
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* which takes the place of a closing `data.grammar.verbs.replaces` where
|
|
28
|
-
* the pattern states one (see {@link module:engine/verb-pattern.verbForm});
|
|
29
|
-
* - a suffix of `data.affixes` that states `replaces`, or its `front` form:
|
|
30
|
-
* `vowel` takes the place of a closing vowel (a letter whose base is `a`,
|
|
31
|
-
* `e`, `i`, `o` or `u`), letters take the place of those letters, and a
|
|
32
|
-
* stem closing otherwise takes the suffix whole. An affix that states no
|
|
33
|
-
* `replaces` derives nothing.
|
|
25
|
+
* - a form one rule the language states derives from a lexicon form: a noun
|
|
26
|
+
* plural, a verb's present form, a replacing suffix, an agreeing form or a
|
|
27
|
+
* participle, of a lexicon form or of one of its variants (see {@link module:engine/word-forms.derivedForms}).
|
|
34
28
|
*
|
|
35
29
|
* A word not accepted whole is split at its hyphens and en dashes, and each
|
|
36
30
|
* part is checked. An apostrophe joins a word, so `k'aralzon` is one word, and
|
|
@@ -42,11 +36,9 @@
|
|
|
42
36
|
* @module
|
|
43
37
|
*/
|
|
44
38
|
|
|
45
|
-
import { pluralOf } from "./dictionary-rows.mjs";
|
|
46
39
|
import { languageRecordsByName } from "./map-lexicon.mjs";
|
|
47
40
|
import { RESERVED_TONGUE } from "./language-terms.mjs";
|
|
48
|
-
import {
|
|
49
|
-
import { parseWordClass } from "./word-classes.mjs";
|
|
41
|
+
import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
|
|
50
42
|
import { WORD } from "./word-pattern.mjs";
|
|
51
43
|
|
|
52
44
|
/** Whether a value is a plain map. */
|
|
@@ -61,104 +53,6 @@ const JOINS = /[–-]/u;
|
|
|
61
53
|
/** A letter, which a checked word holds at least one of. */
|
|
62
54
|
const LETTER = /\p{L}/u;
|
|
63
55
|
|
|
64
|
-
/**
|
|
65
|
-
* A word as it is compared: NFC, lowercase, `’` read as `'`.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} word - The word.
|
|
68
|
-
* @returns {string} Its folded form.
|
|
69
|
-
*/
|
|
70
|
-
export function foldWord(word) {
|
|
71
|
-
return word.normalize("NFC").toLowerCase().replace(/’/gu, "'");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Whether a character is a vowel: its base letter is a, e, i, o or u. */
|
|
75
|
-
const isVowel = (char) => /^[aeiou]/iu.test(char.normalize("NFD"));
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Add a text's words to a set, each whole and each part of a compound.
|
|
79
|
-
*
|
|
80
|
-
* @param {Set<string>} set - The set.
|
|
81
|
-
* @param {string} text - A form or a name.
|
|
82
|
-
*/
|
|
83
|
-
function addWords(set, text) {
|
|
84
|
-
for (const [word] of text.matchAll(WORD)) {
|
|
85
|
-
const folded = foldWord(word);
|
|
86
|
-
set.add(folded);
|
|
87
|
-
for (const part of folded.split(JOINS)) if (part) set.add(part);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The form a suffix stating `replaces` makes of a stem.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} stem - The stem, folded.
|
|
95
|
-
* @param {string} suffix - The suffix, folded, with no hyphen.
|
|
96
|
-
* @param {string} replaces - The affix's `replaces`.
|
|
97
|
-
* @returns {string} The derived form.
|
|
98
|
-
*/
|
|
99
|
-
function attach(stem, suffix, replaces) {
|
|
100
|
-
if (replaces === "vowel") {
|
|
101
|
-
const last = Array.from(stem).at(-1);
|
|
102
|
-
return last !== undefined && isVowel(last) ?
|
|
103
|
-
stem.slice(0, -last.length) + suffix
|
|
104
|
-
: stem + suffix;
|
|
105
|
-
}
|
|
106
|
-
return replaceClosing(stem, suffix, foldWord(replaces));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The suffixes a language's affixes state `replaces` for: each `form`, and
|
|
111
|
-
* its `front` form, written `-x`.
|
|
112
|
-
*
|
|
113
|
-
* @param {Record<string, any>} data - The language note's `data`.
|
|
114
|
-
* @returns {Array<{suffix: string, replaces: string}>} The suffixes, folded.
|
|
115
|
-
*/
|
|
116
|
-
function replacingSuffixes(data) {
|
|
117
|
-
const out = [];
|
|
118
|
-
for (const affix of listOf(data.affixes)) {
|
|
119
|
-
if (!isMap(affix) || typeof affix.replaces !== "string") continue;
|
|
120
|
-
for (const written of [affix.form, affix.front]) {
|
|
121
|
-
if (typeof written !== "string") continue;
|
|
122
|
-
const form = foldWord(written.trim());
|
|
123
|
-
if (!/^[-–]/u.test(form) || /[-–]$/u.test(form)) continue;
|
|
124
|
-
const suffix = form.replace(/^[-–]+/u, "");
|
|
125
|
-
if (suffix) out.push({ suffix, replaces: affix.replaces });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* The words a language note accepts of its own: its forms, and what its
|
|
133
|
-
* plural rows, verb pattern and replacing suffixes derive from them.
|
|
134
|
-
*
|
|
135
|
-
* @param {Record<string, any>} data - The language note's `data`.
|
|
136
|
-
* @returns {Set<string>} The words, folded.
|
|
137
|
-
*/
|
|
138
|
-
export function lexiconWords(data) {
|
|
139
|
-
const accepted = new Set();
|
|
140
|
-
const grammar = isMap(data?.grammar) ? data.grammar : undefined;
|
|
141
|
-
const verbs = verbPattern(grammar?.verbs);
|
|
142
|
-
const replaces = verbs?.replaces === undefined ? undefined : foldWord(verbs.replaces);
|
|
143
|
-
const suffixes = replacingSuffixes(data ?? {});
|
|
144
|
-
for (const word of listOf(data?.words)) {
|
|
145
|
-
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
146
|
-
addWords(accepted, word.form);
|
|
147
|
-
const plural = pluralOf(word, grammar);
|
|
148
|
-
if (plural) addWords(accepted, plural);
|
|
149
|
-
if (verbs && parseWordClass(word.class).base === "v")
|
|
150
|
-
for (const { ending } of verbs.endings)
|
|
151
|
-
addWords(accepted, verbForm(foldWord(word.form), foldWord(ending), replaces));
|
|
152
|
-
if (suffixes.length)
|
|
153
|
-
for (const [stem] of word.form.matchAll(WORD)) {
|
|
154
|
-
const folded = foldWord(stem);
|
|
155
|
-
for (const { suffix, replaces } of suffixes)
|
|
156
|
-
accepted.add(attach(folded, suffix, replaces));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return accepted;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
56
|
/**
|
|
163
57
|
* The words of every name a language note registers under a tongue other than
|
|
164
58
|
* `translation`.
|
|
@@ -0,0 +1,38 @@
|
|
|
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 words a script note and a language note state emphasis by.** Read by
|
|
16
|
+
* the vocabulary as it is declared, so this module imports nothing; the
|
|
17
|
+
* checks and the conventions' use are in {@link module:engine/script-emphasis}.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The conventions a script states its emphasis by. */
|
|
23
|
+
export const EMPHASIS_CONVENTIONS = Object.freeze(["rubric", "overline"]);
|
|
24
|
+
|
|
25
|
+
/** The class each convention gives an `<em>` or a `<strong>`. */
|
|
26
|
+
export const EMPHASIS_CLASSES = Object.freeze({
|
|
27
|
+
rubric: "emph-rubric",
|
|
28
|
+
overline: "emph-overline",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The conventions a language note states, which override its script's.
|
|
33
|
+
* `words` marks emphasis with a word, so a mark in its text is a finding.
|
|
34
|
+
*/
|
|
35
|
+
export const LANGUAGE_EMPHASES = Object.freeze(["words", ...EMPHASIS_CONVENTIONS]);
|
|
36
|
+
|
|
37
|
+
/** The `subType` of a script note. */
|
|
38
|
+
export const SCRIPT_SUBTYPE = "script";
|
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
* **How a script marks emphasis.** A script note — a `skill` of
|
|
16
|
+
* `subType: script` — names the faces it is written in and its scribal
|
|
17
|
+
* convention for a stressed word:
|
|
18
|
+
*
|
|
19
|
+
* ```yaml
|
|
20
|
+
* data:
|
|
21
|
+
* script:
|
|
22
|
+
* fonts: [semaron-kuriale]
|
|
23
|
+
* emphasis: rubric
|
|
24
|
+
* strongEmphasis: overline
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* - **`fonts`** lists installed faces by the slug `font=` names them by. A
|
|
28
|
+
* face belongs to one script.
|
|
29
|
+
* - **`emphasis`** is `rubric` (red text) or `overline` (a line above the
|
|
30
|
+
* word), and sets both `*…*` and `**…**`.
|
|
31
|
+
* - **`strongEmphasis`**, where stated, sets `**…**` alone.
|
|
32
|
+
*
|
|
33
|
+
* Inside a run in one of those faces, `*…*` and `**…**` stay an `<em>` and a
|
|
34
|
+
* `<strong>` on the site and in Foundry, carrying {@link module:engine/script-emphasis-terms.EMPHASIS_CLASSES}'s
|
|
35
|
+
* class, and the book sets them in its rubric ink or under an overline. A face
|
|
36
|
+
* no script note lists sets its emphasis plain: the element with no class, in
|
|
37
|
+
* the face's Regular. See {@link module:engine/content-scripts.renderRuns}.
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
43
|
+
import { isNoteRecord } from "./index-records.mjs";
|
|
44
|
+
import { nearest } from "./near-miss.mjs";
|
|
45
|
+
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
46
|
+
import {
|
|
47
|
+
EMPHASIS_CONVENTIONS,
|
|
48
|
+
LANGUAGE_EMPHASES,
|
|
49
|
+
SCRIPT_SUBTYPE,
|
|
50
|
+
} from "./script-emphasis-terms.mjs";
|
|
51
|
+
import { GENERIC_FAMILIES } from "./text-maps.mjs";
|
|
52
|
+
import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
EMPHASIS_CLASSES,
|
|
56
|
+
EMPHASIS_CONVENTIONS,
|
|
57
|
+
LANGUAGE_EMPHASES,
|
|
58
|
+
} from "./script-emphasis-terms.mjs";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One face's conventions, one for each level of emphasis.
|
|
62
|
+
*
|
|
63
|
+
* @typedef {object} Emphasis
|
|
64
|
+
* @property {"rubric"|"overline"} em - What `*…*` is set by.
|
|
65
|
+
* @property {"rubric"|"overline"} strong - What `**…**` is set by.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A convention a language note states.
|
|
70
|
+
*
|
|
71
|
+
* @typedef {"words"|"rubric"|"overline"} LanguageEmphasis
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** Whether a value is a plain map. */
|
|
75
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
76
|
+
|
|
77
|
+
/** Whether a value is one of the conventions. */
|
|
78
|
+
const isConvention = (value) => EMPHASIS_CONVENTIONS.includes(value);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Each face's conventions, from the script notes among the index records. The
|
|
82
|
+
* first note to list a face keeps it; a second is the lint's to report, and a
|
|
83
|
+
* note whose convention is unknown gives none.
|
|
84
|
+
*
|
|
85
|
+
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
86
|
+
* @returns {Map<string, Emphasis>} The conventions, by face slug.
|
|
87
|
+
*/
|
|
88
|
+
export function emphasesFromRecords(records) {
|
|
89
|
+
const out = new Map();
|
|
90
|
+
for (const record of records ?? []) {
|
|
91
|
+
if (!record || !isNoteRecord(record)) continue;
|
|
92
|
+
if (
|
|
93
|
+
record.type !== "skill" ||
|
|
94
|
+
String(record.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE
|
|
95
|
+
)
|
|
96
|
+
continue;
|
|
97
|
+
const script = record.data?.script;
|
|
98
|
+
if (!isMap(script) || !Array.isArray(script.fonts) || !isConvention(script.emphasis))
|
|
99
|
+
continue;
|
|
100
|
+
const strong =
|
|
101
|
+
script.strongEmphasis === undefined || script.strongEmphasis === null ?
|
|
102
|
+
script.emphasis
|
|
103
|
+
: script.strongEmphasis;
|
|
104
|
+
if (!isConvention(strong)) continue;
|
|
105
|
+
const emphasis = Object.freeze({ em: script.emphasis, strong });
|
|
106
|
+
for (const slug of script.fonts)
|
|
107
|
+
if (typeof slug === "string" && !out.has(slug)) out.set(slug, emphasis);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The convention each language's note states.
|
|
114
|
+
*
|
|
115
|
+
* @param {ReadonlyMap<string, Record<string, any>>} byName - Each configured
|
|
116
|
+
* language's note record, by language name, from
|
|
117
|
+
* {@link module:engine/map-lexicon.languageRecordsByName}.
|
|
118
|
+
* @returns {Map<string, LanguageEmphasis>} The conventions, by language name;
|
|
119
|
+
* a language whose note states none, or one the build does not know, is
|
|
120
|
+
* absent.
|
|
121
|
+
*/
|
|
122
|
+
export function languageEmphasesOf(byName) {
|
|
123
|
+
const out = new Map();
|
|
124
|
+
for (const [name, record] of byName ?? [])
|
|
125
|
+
if (LANGUAGE_EMPHASES.includes(record?.data?.emphasis)) out.set(name, record.data.emphasis);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Refuse `data.emphasis` on a skill that is not a language, at its key.
|
|
131
|
+
*
|
|
132
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
133
|
+
* @returns {object[]} The findings.
|
|
134
|
+
*/
|
|
135
|
+
export function checkLanguageEmphasis(note) {
|
|
136
|
+
const value = note?.fm?.data?.emphasis;
|
|
137
|
+
if (value === undefined || value === null) return [];
|
|
138
|
+
if (String(note.fm?.subType ?? "").toLowerCase() === LANGUAGE_SUBTYPE) return [];
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
file: note.file,
|
|
142
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "emphasis"], { key: true }),
|
|
143
|
+
severity: "error",
|
|
144
|
+
message:
|
|
145
|
+
"`data.emphasis` states how a language marks emphasis, and this note's " +
|
|
146
|
+
`\`subType\` is not \`${LANGUAGE_SUBTYPE}\``,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The notes listing each face, read once per lint index. */
|
|
152
|
+
const listedFaces = new WeakMap();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Each face the index's script notes list, and the notes listing it.
|
|
156
|
+
*
|
|
157
|
+
* @param {{notes?: Iterable<{file: string, fm?: Record<string, any>}>}} index - The lint index.
|
|
158
|
+
* @returns {Map<string, string[]>} Slug → the files that list it.
|
|
159
|
+
*/
|
|
160
|
+
function facesIn(index) {
|
|
161
|
+
const known = listedFaces.get(index);
|
|
162
|
+
if (known) return known;
|
|
163
|
+
const faces = new Map();
|
|
164
|
+
for (const note of index.notes ?? []) {
|
|
165
|
+
const fm = note?.fm;
|
|
166
|
+
if (fm?.type !== "skill" || String(fm.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE)
|
|
167
|
+
continue;
|
|
168
|
+
const fonts = fm.data?.script?.fonts;
|
|
169
|
+
if (!Array.isArray(fonts)) continue;
|
|
170
|
+
for (const slug of new Set(fonts.filter((value) => typeof value === "string"))) {
|
|
171
|
+
if (!faces.has(slug)) faces.set(slug, []);
|
|
172
|
+
faces.get(slug).push(note.file);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
listedFaces.set(index, faces);
|
|
176
|
+
return faces;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check a note's `data.script`: that the note is a script, and that its
|
|
181
|
+
* `fonts` is not empty, names no generic family, lists no face twice, and
|
|
182
|
+
* lists no face another script note lists. The data-key check owns every key,
|
|
183
|
+
* its kind and the conventions; whether a face is installed is
|
|
184
|
+
* {@link checkScriptNoteFonts}'s.
|
|
185
|
+
*
|
|
186
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
187
|
+
* @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index.
|
|
188
|
+
* @returns {object[]} The findings, each at its key or entry.
|
|
189
|
+
*/
|
|
190
|
+
export function checkScriptField(note, { index } = {}) {
|
|
191
|
+
const value = note?.fm?.data?.script;
|
|
192
|
+
if (value === undefined || value === null) return [];
|
|
193
|
+
const raw = note.raw ?? "";
|
|
194
|
+
/**
|
|
195
|
+
* @param {Array<string|number>} keyPath - Where the fault is.
|
|
196
|
+
* @param {string} message - What is wrong.
|
|
197
|
+
* @param {boolean} [key] - Locate the key rather than its value.
|
|
198
|
+
*/
|
|
199
|
+
const finding = (keyPath, message, key = false) => ({
|
|
200
|
+
file: note.file,
|
|
201
|
+
...positionOfFrontmatterPath(raw, ["data", "script", ...keyPath], { key }),
|
|
202
|
+
severity: "error",
|
|
203
|
+
message,
|
|
204
|
+
});
|
|
205
|
+
if (String(note.fm?.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE)
|
|
206
|
+
return [
|
|
207
|
+
finding(
|
|
208
|
+
[],
|
|
209
|
+
"`data.script` states a script's faces and emphasis, and this note's " +
|
|
210
|
+
`\`subType\` is not \`${SCRIPT_SUBTYPE}\``,
|
|
211
|
+
true,
|
|
212
|
+
),
|
|
213
|
+
];
|
|
214
|
+
if (!isMap(value) || !Array.isArray(value.fonts)) return [];
|
|
215
|
+
const { fonts } = value;
|
|
216
|
+
if (!fonts.length)
|
|
217
|
+
return [
|
|
218
|
+
finding(
|
|
219
|
+
["fonts"],
|
|
220
|
+
"`data.script.fonts` is empty; a script lists each face it is written in",
|
|
221
|
+
),
|
|
222
|
+
];
|
|
223
|
+
const findings = [];
|
|
224
|
+
const first = new Map();
|
|
225
|
+
fonts.forEach((slug, at) => {
|
|
226
|
+
if (typeof slug !== "string") return;
|
|
227
|
+
if (GENERIC_FAMILIES.includes(slug)) {
|
|
228
|
+
findings.push(
|
|
229
|
+
finding(
|
|
230
|
+
["fonts", at],
|
|
231
|
+
`\`data.script.fonts[${at}]\` is \`${slug}\`, a generic family; a script lists the faces it is written in`,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (first.has(slug)) {
|
|
237
|
+
findings.push(
|
|
238
|
+
finding(
|
|
239
|
+
["fonts", at],
|
|
240
|
+
`\`data.script.fonts[${at}]\` lists the font \`${slug}\` a second time; the first is ` +
|
|
241
|
+
`\`data.script.fonts[${first.get(slug)}]\``,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
first.set(slug, at);
|
|
247
|
+
if (!index) return;
|
|
248
|
+
const others = (facesIn(index).get(slug) ?? []).filter((file) => file !== note.file);
|
|
249
|
+
if (others.length)
|
|
250
|
+
findings.push(
|
|
251
|
+
finding(
|
|
252
|
+
["fonts", at],
|
|
253
|
+
`\`data.script.fonts[${at}]\` is \`${slug}\`, which ${others.join(", ")} also lists; a face belongs to one script`,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
return findings;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Each face a script note lists that is not installed, at its entry.
|
|
262
|
+
*
|
|
263
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
264
|
+
* @param {{faces?: ReadonlyMap<string, unknown>}} [ctx] - The installed faces,
|
|
265
|
+
* from {@link module:engine/content-scripts.runContext}.
|
|
266
|
+
* @returns {object[]} The findings.
|
|
267
|
+
*/
|
|
268
|
+
export function checkScriptNoteFonts(note, ctx) {
|
|
269
|
+
const fm = note?.fm;
|
|
270
|
+
if (String(fm?.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE) return [];
|
|
271
|
+
const fonts = fm?.data?.script?.fonts;
|
|
272
|
+
if (!Array.isArray(fonts)) return [];
|
|
273
|
+
const faces = ctx?.faces ?? new Map();
|
|
274
|
+
const raw = note.raw ?? "";
|
|
275
|
+
const findings = [];
|
|
276
|
+
const seen = new Set();
|
|
277
|
+
fonts.forEach((slug, at) => {
|
|
278
|
+
if (typeof slug !== "string" || seen.has(slug)) return;
|
|
279
|
+
seen.add(slug);
|
|
280
|
+
if (GENERIC_FAMILIES.includes(slug) || faces.has(slug)) return;
|
|
281
|
+
const near = nearest(slug, [...faces.keys()]);
|
|
282
|
+
findings.push({
|
|
283
|
+
file: note.file,
|
|
284
|
+
...positionOfFrontmatterPath(raw, ["data", "script", "fonts", at]),
|
|
285
|
+
severity: "error",
|
|
286
|
+
message:
|
|
287
|
+
`\`${slug}\` is not a font: \`data.script.fonts[${at}]\` names ` +
|
|
288
|
+
(faces.size ? "an installed face" : (
|
|
289
|
+
`a face of ${FONTS_PACKAGE}, which this package has not installed`
|
|
290
|
+
)) +
|
|
291
|
+
(near ? `; did you mean \`${near}\`?` : ""),
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
return findings;
|
|
295
|
+
}
|
|
@@ -39,8 +39,8 @@ import { FONTS_MANIFEST, resolveScriptFonts } from "./script-fonts.mjs";
|
|
|
39
39
|
export const STAGED_FONTS_DIR = "fonts/heroiclands";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Where the files land in the
|
|
43
|
-
*
|
|
42
|
+
* Where the files land in the site's static tree, beneath a directory per
|
|
43
|
+
* version.
|
|
44
44
|
*
|
|
45
45
|
* @type {string}
|
|
46
46
|
*/
|
|
@@ -279,10 +279,10 @@ export function siteFontsParam(config) {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* The directory under the
|
|
282
|
+
* The directory under the site's static tree that holds this version's fonts.
|
|
283
283
|
*
|
|
284
284
|
* @param {string} version - The fonts package's version.
|
|
285
|
-
* @returns {string} The POSIX path, relative to
|
|
285
|
+
* @returns {string} The POSIX path, relative to the static tree.
|
|
286
286
|
*/
|
|
287
287
|
export function siteFontsDestination(version) {
|
|
288
288
|
return `${SITE_FONTS_DIR}/${version}`;
|