@heroiclands/package-build 23.13.0 → 24.0.1
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 +50 -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 +248 -80
- 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 +155 -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 +265 -313
- 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-reach.mjs +213 -0
- package/engine/site-render.mjs +668 -0
- package/engine/site-root.mjs +90 -39
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +274 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +939 -0
- package/engine/sql-tables.mjs +9 -6
- 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-reach.d.mts +76 -0
- package/types/engine/site-render.d.mts +173 -0
- package/types/engine/site-root.d.mts +14 -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,91 @@
|
|
|
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 shared rules of the content stylesheet**, rescoped for a surface that
|
|
16
|
+
* is not Foundry.
|
|
17
|
+
*
|
|
18
|
+
* `assets/styles/content-base.css` styles what the renderer emits, scoped to
|
|
19
|
+
* the roots a Foundry journal and its editors use. Two other surfaces reuse its
|
|
20
|
+
* rules without a second copy: the editor preview takes the stretches marked
|
|
21
|
+
* `preview:shared`, and a site takes the highlighted-code rules. Both read the
|
|
22
|
+
* one file and replace the scope, so a colour changed there changes everywhere.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
|
|
29
|
+
import { baseStylesheetSource } from "./base-styles.mjs";
|
|
30
|
+
|
|
31
|
+
/** The roots the shipped content stylesheet scopes its rules to. */
|
|
32
|
+
export const CONTENT_ROOTS =
|
|
33
|
+
":is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)";
|
|
34
|
+
|
|
35
|
+
/** The stretches of the shipped content stylesheet other surfaces share. */
|
|
36
|
+
const SHARED_RULES = /\/\* preview:shared [^]*?\*\/([^]*?)\/\* \/preview:shared \*\//g;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The rules the Foundry content stylesheet shares with other surfaces, scoped
|
|
40
|
+
* to a selector.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} scope - The selector that replaces the Foundry roots.
|
|
43
|
+
* @returns {string} The rules.
|
|
44
|
+
* @throws {Error} When the stylesheet marks no shared rules.
|
|
45
|
+
*/
|
|
46
|
+
export function sharedContentRules(scope) {
|
|
47
|
+
const css = fs.readFileSync(baseStylesheetSource(), "utf8");
|
|
48
|
+
const shared = [...css.matchAll(SHARED_RULES)].map((match) => match[1]);
|
|
49
|
+
if (!shared.length) throw new Error("the content stylesheet marks no rules for the preview");
|
|
50
|
+
return shared.join("").replaceAll(CONTENT_ROOTS, scope);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The rules that colour highlighted code, scoped to a selector.
|
|
55
|
+
*
|
|
56
|
+
* A rule belongs to the set when one of its selectors names a `hljs-` token
|
|
57
|
+
* class. The rules are taken from the shared stretches, so the colours, their
|
|
58
|
+
* custom properties and their `light-dark()` pairs are the stylesheet's own.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} scope - The selector that replaces the Foundry roots.
|
|
61
|
+
* @returns {string} The token rules, one per block.
|
|
62
|
+
*/
|
|
63
|
+
export function highlightRules(scope) {
|
|
64
|
+
const shared = sharedContentRules(scope)
|
|
65
|
+
.replace(/\/\*[^]*?\*\//g, "")
|
|
66
|
+
.replace(/\s*\n\s+(?=\.hljs-)/g, " ");
|
|
67
|
+
return (
|
|
68
|
+
shared
|
|
69
|
+
.split("}")
|
|
70
|
+
.map((rule) => rule.trim())
|
|
71
|
+
.filter(
|
|
72
|
+
(rule) => rule.includes("{") && /\.hljs-/.test(rule.slice(0, rule.indexOf("{"))),
|
|
73
|
+
)
|
|
74
|
+
.map((rule) => `${rule}\n}`)
|
|
75
|
+
.join("\n\n") + "\n"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The stylesheet a site links for highlighted code.
|
|
81
|
+
*
|
|
82
|
+
* A site sets no colour scheme, and the token colours are `light-dark()` pairs
|
|
83
|
+
* that read the scheme in force, so code is given the dark one its pages are
|
|
84
|
+
* drawn in.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} [scope] - The element the page body sits in.
|
|
87
|
+
* @returns {string} The stylesheet.
|
|
88
|
+
*/
|
|
89
|
+
export function siteCodeStylesheet(scope = ".single-body") {
|
|
90
|
+
return `${scope} :is(pre, code) {\n color-scheme: dark;\n}\n\n${highlightRules(scope)}`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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 glyph and cartouche spans of a note, read in one place for the stages
|
|
16
|
+
* that need their pictures: the renderer that draws them and the book that
|
|
17
|
+
* stages their files.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
glyphHtml,
|
|
24
|
+
glyphPathname,
|
|
25
|
+
cartoucheHtml,
|
|
26
|
+
cartouchePathname,
|
|
27
|
+
cartoucheProblems,
|
|
28
|
+
glyphProblems,
|
|
29
|
+
} from "./content-glyphs.mjs";
|
|
30
|
+
import { scanSpans } from "./content-spans.mjs";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A span's attributes as the construct's checker reads them.
|
|
34
|
+
* @param {Array<[string, string]>} attrs - The span's attribute pairs.
|
|
35
|
+
* @returns {{id: string, classes: string[], values: Record<string, string>}} Parsed form.
|
|
36
|
+
*/
|
|
37
|
+
function parsedAttributes(attrs) {
|
|
38
|
+
const find = (name) => attrs.find(([key]) => key === name)?.[1] ?? "";
|
|
39
|
+
return {
|
|
40
|
+
id: find("id"),
|
|
41
|
+
classes: find("class").split(/\s+/).filter(Boolean),
|
|
42
|
+
values: Object.fromEntries(attrs.filter(([key]) => !["id", "class"].includes(key))),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The well-formed glyph and cartouche spans of a note, outermost only, outside
|
|
48
|
+
* code.
|
|
49
|
+
* @param {string} source - Authored Markdown.
|
|
50
|
+
* @returns {Array<{kind: "glyph"|"cartouche", start: number, end: number, slug: string, alt: string, frame?: string, size?: string, align?: string, height?: string, caption?: string}>}
|
|
51
|
+
* One entry per sign, in source order. A sign with a finding is left out: it
|
|
52
|
+
* is reported and draws as an ordinary span.
|
|
53
|
+
*/
|
|
54
|
+
function symbolSpans(source) {
|
|
55
|
+
if (!/\.(glyph|cartouche)\b/.test(source)) return [];
|
|
56
|
+
const { spans } = scanSpans(source);
|
|
57
|
+
const signs = spans
|
|
58
|
+
.map((span) => ({ span, parsed: parsedAttributes(span.attrs) }))
|
|
59
|
+
.flatMap(({ span, parsed }) => {
|
|
60
|
+
const kind = ["glyph", "cartouche"].find((name) => parsed.classes.includes(name));
|
|
61
|
+
const problems = { glyph: glyphProblems, cartouche: cartoucheProblems }[kind];
|
|
62
|
+
if (!problems || problems(parsed).length) return [];
|
|
63
|
+
const { slug, frame, size, align, height, caption } = parsed.values;
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
kind,
|
|
67
|
+
start: span.start,
|
|
68
|
+
end: span.end,
|
|
69
|
+
slug,
|
|
70
|
+
alt: span.visible.trim(),
|
|
71
|
+
...(frame ? { frame } : {}),
|
|
72
|
+
...(size ? { size } : {}),
|
|
73
|
+
...(align ? { align } : {}),
|
|
74
|
+
...(height ? { height } : {}),
|
|
75
|
+
...(caption ? { caption } : {}),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
|
+
return signs.filter(
|
|
80
|
+
(sign) => !signs.some((outer) => outer.start < sign.start && outer.end >= sign.end),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const OPEN = "\uE000",
|
|
85
|
+
CLOSE = "\uE001";
|
|
86
|
+
const PLACEHOLDER = new RegExp(`${OPEN}([0-9a-f]+)${CLOSE}`, "g");
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A cartouche as the inert text a Markdown parser passes through untouched.
|
|
90
|
+
* A figure written as HTML at the start of a line would open a CommonMark HTML
|
|
91
|
+
* block and leave the rest of its paragraph unparsed, so the figure is written
|
|
92
|
+
* as inline HTML by {@link cartoucheFigures} after the paragraph is parsed.
|
|
93
|
+
* @param {object} cartouche - The figure's attributes, with `src` the address of its picture.
|
|
94
|
+
* @returns {string} Private-use delimiters around the hex of the attributes' JSON.
|
|
95
|
+
*/
|
|
96
|
+
function cartouchePlaceholder(cartouche) {
|
|
97
|
+
const hex = [...new TextEncoder().encode(JSON.stringify(cartouche))]
|
|
98
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
99
|
+
.join("");
|
|
100
|
+
return `${OPEN}${hex}${CLOSE}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Markdown-it plugin: write each cartouche placeholder as the inline HTML of
|
|
105
|
+
* its figure, in whatever paragraph, cell or title it stands.
|
|
106
|
+
* @param {import("markdown-it")} md - Parser to extend.
|
|
107
|
+
*/
|
|
108
|
+
export function cartoucheFigures(md) {
|
|
109
|
+
md.core.ruler.after("inline", "cartouche_figures", (state) => {
|
|
110
|
+
for (const token of state.tokens) {
|
|
111
|
+
if (token.type !== "inline" || !token.children) continue;
|
|
112
|
+
token.children = token.children.flatMap((child) => {
|
|
113
|
+
if (child.type !== "text" || !child.content.includes(OPEN)) return [child];
|
|
114
|
+
const pieces = [];
|
|
115
|
+
let last = 0;
|
|
116
|
+
const piece = (type, content) => {
|
|
117
|
+
const made = new state.Token(type, "", 0);
|
|
118
|
+
made.content = content;
|
|
119
|
+
made.level = child.level;
|
|
120
|
+
pieces.push(made);
|
|
121
|
+
};
|
|
122
|
+
for (const match of child.content.matchAll(PLACEHOLDER)) {
|
|
123
|
+
if (match.index > last) piece("text", child.content.slice(last, match.index));
|
|
124
|
+
const { src, ...cartouche } = JSON.parse(
|
|
125
|
+
new TextDecoder().decode(
|
|
126
|
+
Uint8Array.from(match[1].match(/../g), (pair) => parseInt(pair, 16)),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
piece("html_inline", cartoucheHtml(cartouche, src));
|
|
130
|
+
last = match.index + match[0].length;
|
|
131
|
+
}
|
|
132
|
+
if (last < child.content.length) piece("text", child.content.slice(last));
|
|
133
|
+
return pieces;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Write each glyph and cartouche span as the HTML that draws its picture. A
|
|
141
|
+
* cartouche is written as a placeholder that {@link cartoucheFigures} turns
|
|
142
|
+
* into its figure.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} source - Markdown that may contain glyph and cartouche spans.
|
|
145
|
+
* @param {(src: string) => string} [resolveSrc] - From a sign's authored
|
|
146
|
+
* pathname to the address the target serves; omitted, the pathname.
|
|
147
|
+
* @param {object} [options]
|
|
148
|
+
* @param {boolean} [options.glyphs] - Write glyph spans; `false` leaves them
|
|
149
|
+
* as the source they were written in. Default `true`.
|
|
150
|
+
* @returns {string} The Markdown with each well-formed sign written as HTML or a placeholder.
|
|
151
|
+
*/
|
|
152
|
+
export function renderSymbolSpans(source, resolveSrc, { glyphs = true } = {}) {
|
|
153
|
+
let markdown = String(source ?? "");
|
|
154
|
+
for (const sign of symbolSpans(markdown).sort((a, b) => b.start - a.start)) {
|
|
155
|
+
if (sign.kind === "glyph" && !glyphs) continue;
|
|
156
|
+
const picture = (sign.kind === "glyph" ? glyphPathname : cartouchePathname)(sign.slug);
|
|
157
|
+
const src = resolveSrc?.(picture) ?? picture;
|
|
158
|
+
const html =
|
|
159
|
+
sign.kind === "glyph" ?
|
|
160
|
+
glyphHtml(sign, src)
|
|
161
|
+
: cartouchePlaceholder({ ...sign, alt: sign.alt || undefined, src });
|
|
162
|
+
markdown = markdown.slice(0, sign.start) + html + markdown.slice(sign.end);
|
|
163
|
+
}
|
|
164
|
+
return markdown;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Every picture the glyphs and cartouches of a body name, for a stage that has
|
|
169
|
+
* to place the files before a compiler reads them.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} body - The note's Markdown.
|
|
172
|
+
* @returns {Array<{src: string, locate: string}>} The authored pathname of each
|
|
173
|
+
* picture and the slug that locates it in the note's text, in document order
|
|
174
|
+
* within glyphs and then cartouches.
|
|
175
|
+
*/
|
|
176
|
+
export function symbolImagesIn(body) {
|
|
177
|
+
const signs = symbolSpans(String(body ?? ""));
|
|
178
|
+
return ["glyph", "cartouche"].flatMap((kind) =>
|
|
179
|
+
signs
|
|
180
|
+
.filter((sign) => sign.kind === kind)
|
|
181
|
+
.map(({ slug }) => ({
|
|
182
|
+
src: (kind === "glyph" ? glyphPathname : cartouchePathname)(slug),
|
|
183
|
+
locate: slug,
|
|
184
|
+
})),
|
|
185
|
+
);
|
|
186
|
+
}
|
package/engine/content-verse.mjs
CHANGED
|
@@ -164,9 +164,7 @@ export const COLUMNS_CLASS = "verse-columns";
|
|
|
164
164
|
*/
|
|
165
165
|
export const VERSE_KEYS = Object.freeze([
|
|
166
166
|
"form",
|
|
167
|
-
"
|
|
168
|
-
"rhyme",
|
|
169
|
-
"syllables",
|
|
167
|
+
"scan",
|
|
170
168
|
"lang",
|
|
171
169
|
"font",
|
|
172
170
|
"map",
|
|
@@ -222,15 +220,6 @@ export function scanVerse(source) {
|
|
|
222
220
|
columned ?
|
|
223
221
|
verseGroups(block, lines).filter((group) => !group.stanzaBreak).length
|
|
224
222
|
: verseLines(body).length;
|
|
225
|
-
if (
|
|
226
|
-
opening.attributes.syllables &&
|
|
227
|
-
opening.attributes.syllables.split(",").length !== count
|
|
228
|
-
)
|
|
229
|
-
errors.push({
|
|
230
|
-
line: opening.start + 1,
|
|
231
|
-
column: 1,
|
|
232
|
-
message: `verse syllables= gives ${opening.attributes.syllables.split(",").length} counts for ${count} verse lines`,
|
|
233
|
-
});
|
|
234
223
|
if (columned) errors.push(...columnFindings(block, lines));
|
|
235
224
|
blocks.push(block);
|
|
236
225
|
}
|
|
@@ -270,17 +259,16 @@ export function scanVerse(source) {
|
|
|
270
259
|
message: `\`${key}=\` is not a column: a verse has at most two, \`col1=\` and \`col2=\``,
|
|
271
260
|
});
|
|
272
261
|
else if (!VERSE_KEYS.includes(key))
|
|
273
|
-
errors.push({ ...at, message: `${key}= is not a verse attribute` });
|
|
274
|
-
else if (key === "numstep" && !/^\d+$/.test(value))
|
|
275
262
|
errors.push({
|
|
276
263
|
line: i + 1,
|
|
277
264
|
column: keyColumn(lines[i], key),
|
|
278
|
-
message:
|
|
265
|
+
message: `${key}= is not a verse attribute`,
|
|
279
266
|
});
|
|
280
|
-
else if (key === "
|
|
267
|
+
else if (key === "numstep" && !/^\d+$/.test(value))
|
|
281
268
|
errors.push({
|
|
282
|
-
|
|
283
|
-
|
|
269
|
+
line: i + 1,
|
|
270
|
+
column: keyColumn(lines[i], key),
|
|
271
|
+
message: "verse numstep= numbers every Nth line: a whole number, 0 for none",
|
|
284
272
|
});
|
|
285
273
|
}
|
|
286
274
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The core report: for each language that must cover the core vocabulary, the
|
|
16
|
+
* concepts of the list it has no word for.
|
|
17
|
+
*
|
|
18
|
+
* The list is the `data.concepts` of the package's `doc` notes. A language
|
|
19
|
+
* must cover it where it states `data.core: true`. A word covers every
|
|
20
|
+
* concept its `concept` names, and a language reads the words of its
|
|
21
|
+
* `data.family` chain as its own. A missing concept is a fact to work through,
|
|
22
|
+
* never a finding.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
28
|
+
import { isLanguageRecord, languageLineages, noteAddressOf } from "./word-rows.mjs";
|
|
29
|
+
|
|
30
|
+
/** Whether a value is a plain map. */
|
|
31
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
/** The slug a record is written by in a reference, such as `skill-vylari`. */
|
|
34
|
+
const slugOf = (record) => record.address?.slug ?? record.shortcode;
|
|
35
|
+
|
|
36
|
+
/** The name a language is reported by. */
|
|
37
|
+
const nameOf = (record) =>
|
|
38
|
+
typeof record.name?.full === "string" && record.name.full ? record.name.full : record.shortcode;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The concept list the records state, in record then authored order. An id
|
|
42
|
+
* another list repeats keeps its first place.
|
|
43
|
+
*
|
|
44
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
45
|
+
* @returns {Array<{id: string, gloss: string, group: string}>} The concepts.
|
|
46
|
+
*/
|
|
47
|
+
function conceptsOf(records) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const record of records) {
|
|
51
|
+
const list = record?.data?.concepts;
|
|
52
|
+
if (!Array.isArray(list)) continue;
|
|
53
|
+
for (const entry of list) {
|
|
54
|
+
if (
|
|
55
|
+
!isMap(entry) ||
|
|
56
|
+
typeof entry.id !== "string" ||
|
|
57
|
+
typeof entry.gloss !== "string" ||
|
|
58
|
+
typeof entry.group !== "string" ||
|
|
59
|
+
seen.has(entry.id)
|
|
60
|
+
)
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(entry.id);
|
|
63
|
+
out.push({ id: entry.id, gloss: entry.gloss, group: entry.group });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The concept ids a word serves. */
|
|
70
|
+
function idsOf(word) {
|
|
71
|
+
const value = isMap(word) ? word.concept : undefined;
|
|
72
|
+
return (Array.isArray(value) ? value : [value]).filter((id) => typeof id === "string");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The core report of a set of index records.
|
|
77
|
+
*
|
|
78
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
79
|
+
* @param {{language?: string}} [options] - A language to report alone, by its
|
|
80
|
+
* name, shortcode or reference slug whether or not it states `data.core`.
|
|
81
|
+
* @returns {{concepts: number, languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
82
|
+
* The report, or the problem when there is no concept list or the language
|
|
83
|
+
* asked for is none.
|
|
84
|
+
*/
|
|
85
|
+
export function coreReport(records, { language } = {}) {
|
|
86
|
+
const all = [...records].map((record) => encodeAddresses(record));
|
|
87
|
+
const concepts = conceptsOf(all);
|
|
88
|
+
if (!concepts.length)
|
|
89
|
+
return {
|
|
90
|
+
about: "concepts",
|
|
91
|
+
problem: "no note states `data.concepts`, so there is no core vocabulary to cover",
|
|
92
|
+
};
|
|
93
|
+
const languages = all.filter(isLanguageRecord);
|
|
94
|
+
let chosen;
|
|
95
|
+
if (language === undefined) chosen = languages.filter((record) => record.data?.core === true);
|
|
96
|
+
else {
|
|
97
|
+
const want = language.toLowerCase();
|
|
98
|
+
chosen = languages.filter((record) =>
|
|
99
|
+
[nameOf(record), record.shortcode, slugOf(record)].some(
|
|
100
|
+
(key) => String(key).toLowerCase() === want,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
if (!chosen.length)
|
|
104
|
+
return {
|
|
105
|
+
about: "language",
|
|
106
|
+
problem: `\`${language}\` is no language note: name one by its name, its shortcode or its reference`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
chosen.sort((a, b) => nameOf(a).localeCompare(nameOf(b)));
|
|
110
|
+
|
|
111
|
+
const { languages: byKey, chain } = languageLineages(all);
|
|
112
|
+
const groups = [...new Set(concepts.map((concept) => concept.group))];
|
|
113
|
+
return {
|
|
114
|
+
concepts: concepts.length,
|
|
115
|
+
languages: chosen.map((record) => {
|
|
116
|
+
const have = new Set();
|
|
117
|
+
for (const key of chain(noteAddressOf(record) ?? ""))
|
|
118
|
+
for (const word of byKey.get(key)?.data?.words ?? [])
|
|
119
|
+
for (const id of idsOf(word)) have.add(id);
|
|
120
|
+
const missing = [];
|
|
121
|
+
for (const group of groups) {
|
|
122
|
+
const members = concepts.filter((concept) => concept.group === group);
|
|
123
|
+
const lacking = members.filter((concept) => !have.has(concept.id));
|
|
124
|
+
if (lacking.length)
|
|
125
|
+
missing.push({
|
|
126
|
+
group,
|
|
127
|
+
count: lacking.length,
|
|
128
|
+
total: members.length,
|
|
129
|
+
concepts: lacking.map(({ id, gloss }) => ({ id, gloss })),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const lacking = missing.reduce((sum, group) => sum + group.count, 0);
|
|
133
|
+
return {
|
|
134
|
+
name: nameOf(record),
|
|
135
|
+
language: slugOf(record),
|
|
136
|
+
core: record.data?.core === true,
|
|
137
|
+
total: concepts.length,
|
|
138
|
+
covered: concepts.length - lacking,
|
|
139
|
+
missing,
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A core report as text: one block per language, its missing concepts under
|
|
147
|
+
* their groups with counts, each concept's id and gloss tab-separated.
|
|
148
|
+
*
|
|
149
|
+
* @param {{concepts: number, languages: Array<object>}} report - A report from {@link coreReport}.
|
|
150
|
+
* @returns {string} The text, ending in a newline.
|
|
151
|
+
*/
|
|
152
|
+
export function coreReportText(report) {
|
|
153
|
+
if (!report.languages.length) return "No language states `data.core: true`.\n";
|
|
154
|
+
return report.languages
|
|
155
|
+
.map((one) => {
|
|
156
|
+
if (!one.missing.length)
|
|
157
|
+
return `${one.name} (${one.language}): all ${one.total} core concepts have a word\n`;
|
|
158
|
+
const lines = [
|
|
159
|
+
`${one.name} (${one.language}): ${one.total - one.covered} of ${one.total} core concepts have no word`,
|
|
160
|
+
];
|
|
161
|
+
for (const group of one.missing) {
|
|
162
|
+
lines.push(` ${group.group}: ${group.count} of ${group.total}`);
|
|
163
|
+
for (const concept of group.concepts)
|
|
164
|
+
lines.push(` ${concept.id}\t${concept.gloss}`);
|
|
165
|
+
}
|
|
166
|
+
return `${lines.join("\n")}\n`;
|
|
167
|
+
})
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|