@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/content-spans.mjs
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
import MarkdownIt from "markdown-it";
|
|
15
15
|
import { alertMarkdownPlugin } from "./content-alerts.mjs";
|
|
16
16
|
import { slugify } from "./content-slug.mjs";
|
|
17
|
-
import {
|
|
17
|
+
import { scanVerse } from "./content-verse.mjs";
|
|
18
18
|
import footnotePlugin from "markdown-it-footnote";
|
|
19
19
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
20
20
|
import { inlineText } from "./name-markup.mjs";
|
|
21
|
+
import { cartoucheProblems, glyphProblems } from "./content-glyphs.mjs";
|
|
21
22
|
|
|
22
23
|
/** Add bracket spans after recognized Markdown links and footnotes.
|
|
23
24
|
* @param {import("markdown-it")} md - Markdown parser to extend.
|
|
@@ -91,6 +92,8 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
const problems = [...parsed.problems, ...refusedAttributes(parsed.values)];
|
|
95
|
+
if (parsed.classes.includes("glyph")) problems.push(...glyphProblems(parsed));
|
|
96
|
+
else if (parsed.classes.includes("cartouche")) problems.push(...cartoucheProblems(parsed));
|
|
94
97
|
if (silent) {
|
|
95
98
|
state.pos = finish;
|
|
96
99
|
return true;
|
|
@@ -263,7 +266,7 @@ export function scanSpans(source) {
|
|
|
263
266
|
}
|
|
264
267
|
// Verse fences preserve inline Markdown even though the block parser
|
|
265
268
|
// normally treats every code fence as literal text.
|
|
266
|
-
for (const poem of
|
|
269
|
+
for (const poem of scanVerse(text).blocks) {
|
|
267
270
|
for (let line = poem.start + 1; line < poem.end; line++) {
|
|
268
271
|
const children = [];
|
|
269
272
|
parser.inline.parse(lines[line], parser, env, children);
|
|
@@ -283,7 +286,7 @@ export function scanSpans(source) {
|
|
|
283
286
|
};
|
|
284
287
|
}
|
|
285
288
|
|
|
286
|
-
/** Emit span tags while leaving their inline content as Markdown
|
|
289
|
+
/** Emit span tags while leaving their inline content as Markdown.
|
|
287
290
|
* @param {string} source - Authored Markdown.
|
|
288
291
|
*/
|
|
289
292
|
export function renderSpans(source) {
|
|
@@ -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
|
+
}
|