@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,450 @@
|
|
|
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
|
+
* **What the preview target adds to the shared renderer.** The editor preview
|
|
16
|
+
* renders a buffer that may have no content index. Where it has none, a
|
|
17
|
+
* wikilink, a span, an expression and a query cannot be resolved, so the
|
|
18
|
+
* preview shows each as highlighted authored source. Where it has one, links
|
|
19
|
+
* and spans render as every target renders them, and only what a buffer cannot
|
|
20
|
+
* answer — a query, a date that needs a calendar — stays highlighted source.
|
|
21
|
+
*
|
|
22
|
+
* {@link previewSourcePlugin} installs:
|
|
23
|
+
*
|
|
24
|
+
* - **Source highlighting** of wikilinks, embeds, bracket spans and
|
|
25
|
+
* expressions, when the buffer has no index; of expressions alone otherwise.
|
|
26
|
+
* - **Table cells that keep their pipes**, so an extension's `|` stays inside
|
|
27
|
+
* its cell, with alignment written as a class a sanitizing host keeps.
|
|
28
|
+
* - **Query fences as source**, since a buffer runs no query.
|
|
29
|
+
* - **No generated sections**: a `See also` or `Related pages` heading and
|
|
30
|
+
* what it holds are absent, since a buffer has no related pages.
|
|
31
|
+
* - **Secret headings as disclosures**: a `.secret` heading opens a closed
|
|
32
|
+
* disclosure that runs to the next heading at its level or above, wherever
|
|
33
|
+
* the heading sits. An editor shows the passage a reader will not see, and
|
|
34
|
+
* the build refuses a withheld heading that opens no page.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import MarkdownIt from "markdown-it";
|
|
40
|
+
import Handlebars from "handlebars";
|
|
41
|
+
import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
42
|
+
import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
|
|
43
|
+
import { splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
44
|
+
|
|
45
|
+
/** Explicit dependency policy: a new registered helper requires a preview decision. */
|
|
46
|
+
export const PREVIEW_HELPERS = Object.freeze({
|
|
47
|
+
eq: "local",
|
|
48
|
+
gt: "local",
|
|
49
|
+
gte: "local",
|
|
50
|
+
lt: "local",
|
|
51
|
+
lte: "local",
|
|
52
|
+
not: "local",
|
|
53
|
+
and: "local",
|
|
54
|
+
or: "local",
|
|
55
|
+
words: "local",
|
|
56
|
+
digits: "local",
|
|
57
|
+
sql: "dependent",
|
|
58
|
+
dateformat: "dependent",
|
|
59
|
+
ref: "reference",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What stands for an authored `<` in the preview between
|
|
64
|
+
* {@link module:engine/content-render.inertHtml} and the rendered HTML, where
|
|
65
|
+
* it is written `<`. A private-use character, so it can neither be read as
|
|
66
|
+
* markup nor collide with prose.
|
|
67
|
+
*/
|
|
68
|
+
export const INERT_BRACKET = "\uE000";
|
|
69
|
+
|
|
70
|
+
const escapeHtml = new MarkdownIt().utils.escapeHtml;
|
|
71
|
+
// An authored `<` stands as a private-use character until the HTML is written;
|
|
72
|
+
// source is shown as it was authored.
|
|
73
|
+
const escape = (value) => escapeHtml(String(value ?? "").replaceAll(INERT_BRACKET, "<"));
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A span of authored source, coloured by what it is.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} role - `bracket`, `address`, `label`, `attribute`, `dependent` or `fence`.
|
|
79
|
+
* @param {string} source - The authored text.
|
|
80
|
+
* @returns {string} The span, with the text escaped.
|
|
81
|
+
*/
|
|
82
|
+
export const colored = (role, source) =>
|
|
83
|
+
`<span class="hl-preview-${role}">${escape(source)}</span>`;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Highlight a wikilink or an embed without interpreting any Markdown or HTML
|
|
87
|
+
* inside the source.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} raw - The link as written, from `[[` or `![[` to its end.
|
|
90
|
+
* @returns {string} The highlighted source.
|
|
91
|
+
*/
|
|
92
|
+
export function wikiSource(raw) {
|
|
93
|
+
const open = raw.startsWith("!") ? "![[" : "[[";
|
|
94
|
+
const close = raw.indexOf("]]", open.length);
|
|
95
|
+
const end = close < 0 ? raw.length : close;
|
|
96
|
+
const inner = raw.slice(open.length, end);
|
|
97
|
+
const pipe = inner.indexOf("|");
|
|
98
|
+
const secret = close >= 0 && parseWikilink(inner, raw).attributes?.classes.includes("secret");
|
|
99
|
+
const label =
|
|
100
|
+
colored("address", pipe < 0 ? inner : inner.slice(0, pipe)) +
|
|
101
|
+
(pipe < 0 ? "" : colored("bracket", "|") + colored("label", inner.slice(pipe + 1)));
|
|
102
|
+
const html =
|
|
103
|
+
colored("bracket", open) +
|
|
104
|
+
(secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
|
|
105
|
+
(close < 0 ? "" : colored("bracket", "]]") + colored("attribute", raw.slice(close + 2)));
|
|
106
|
+
return secret ? `<span class="hl-preview-secret" tabindex="0">${html}</span>` : html;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Read a dependent call anywhere in the Handlebars AST, including hash values. */
|
|
110
|
+
function dependentExpression(source, fm) {
|
|
111
|
+
const authoredValue = (node) => {
|
|
112
|
+
if (node?.type === "StringLiteral") return node.value;
|
|
113
|
+
if (node?.type !== "PathExpression" || node.depth || node.data || !node.parts?.length)
|
|
114
|
+
return undefined;
|
|
115
|
+
let value = fm;
|
|
116
|
+
for (const part of node.parts) {
|
|
117
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, part))
|
|
118
|
+
return undefined;
|
|
119
|
+
value = value[part];
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
const visit = (node) => {
|
|
124
|
+
if (!node || typeof node !== "object") return false;
|
|
125
|
+
if (["MustacheStatement", "SubExpression"].includes(node.type)) {
|
|
126
|
+
const name = node.path?.original;
|
|
127
|
+
const policy = PREVIEW_HELPERS[name];
|
|
128
|
+
if (policy === "dependent") return true;
|
|
129
|
+
if (policy === "reference") {
|
|
130
|
+
const target = authoredValue(node.params?.[0]);
|
|
131
|
+
if (typeof target !== "string" || !target.startsWith("#")) return true;
|
|
132
|
+
// Date-valued event fields need calendar context even on this note.
|
|
133
|
+
if (
|
|
134
|
+
node.hash?.pairs?.some(
|
|
135
|
+
(pair) =>
|
|
136
|
+
pair.key === "field" &&
|
|
137
|
+
!["kind", "summary", "name"].includes(pair.value?.value),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if ((node.params?.length || node.hash?.pairs?.length) && !policy) return true;
|
|
143
|
+
}
|
|
144
|
+
return Object.values(node).some((value) =>
|
|
145
|
+
Array.isArray(value) ? value.some(visit) : visit(value),
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
return visit(Handlebars.parse(source));
|
|
150
|
+
} catch {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The end of a wikilink's partially entered trailing attribute block, so an
|
|
157
|
+
* unfinished `{…` stays with the link it follows.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} rest - The source from the link's start.
|
|
160
|
+
* @param {string} raw - The link itself.
|
|
161
|
+
* @returns {string} The link with its attribute block, as far as it goes on the line.
|
|
162
|
+
*/
|
|
163
|
+
function withTrailingAttributes(rest, raw) {
|
|
164
|
+
if (rest[raw.length] !== "{") return raw;
|
|
165
|
+
let quoted = false,
|
|
166
|
+
escaped = false,
|
|
167
|
+
end = raw.length + 1;
|
|
168
|
+
for (; end < rest.length && rest[end] !== "\n"; end++) {
|
|
169
|
+
const char = rest[end];
|
|
170
|
+
if (escaped) {
|
|
171
|
+
escaped = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (char === "\\") {
|
|
175
|
+
escaped = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (char === '"') quoted = !quoted;
|
|
179
|
+
if (char === "}" && !quoted) {
|
|
180
|
+
end++;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return rest.slice(0, end);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The cells of one table row, keeping a pipe inside a wikilink, a code span or
|
|
189
|
+
* an extension's braces in its cell.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} line - One table line.
|
|
192
|
+
* @returns {string[]} The trimmed cells, without the empty edges.
|
|
193
|
+
*/
|
|
194
|
+
function tableCells(line) {
|
|
195
|
+
const result = [];
|
|
196
|
+
let cell = "",
|
|
197
|
+
wiki = false,
|
|
198
|
+
code = "",
|
|
199
|
+
quote = false,
|
|
200
|
+
braces = 0;
|
|
201
|
+
for (let at = 0; at < line.length; at++) {
|
|
202
|
+
const char = line[at];
|
|
203
|
+
if (char === "\\" && at + 1 < line.length) {
|
|
204
|
+
cell += char + line[++at];
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (char === "`" && !wiki) {
|
|
208
|
+
const marks = /^`+/.exec(line.slice(at))[0];
|
|
209
|
+
if (!code) code = marks;
|
|
210
|
+
else if (code === marks) code = "";
|
|
211
|
+
cell += marks;
|
|
212
|
+
at += marks.length - 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!code) {
|
|
216
|
+
if (line.slice(at, at + 2) === "[[") wiki = true;
|
|
217
|
+
if (line.slice(at - 1, at + 1) === "]]") wiki = false;
|
|
218
|
+
if (char === '"' && braces) quote = !quote;
|
|
219
|
+
if (!quote && char === "{") braces++;
|
|
220
|
+
if (!quote && char === "}" && braces) braces--;
|
|
221
|
+
}
|
|
222
|
+
if (char === "|" && !wiki && !code && !braces) {
|
|
223
|
+
result.push(cell.trim());
|
|
224
|
+
cell = "";
|
|
225
|
+
} else cell += char;
|
|
226
|
+
}
|
|
227
|
+
result.push(cell.trim());
|
|
228
|
+
if (!result[0]) result.shift();
|
|
229
|
+
if (!result.at(-1)) result.pop();
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Install the preview target's rules.
|
|
235
|
+
*
|
|
236
|
+
* @param {import("markdown-it")} md - The shared parser.
|
|
237
|
+
* @param {object} [options]
|
|
238
|
+
* @param {boolean} [options.source] - The buffer has no index: show wikilinks,
|
|
239
|
+
* embeds, spans and local expressions as authored source.
|
|
240
|
+
* @param {object} [options.fm] - The buffer's frontmatter, which a local
|
|
241
|
+
* expression reads.
|
|
242
|
+
* @param {string} [options.file] - The buffer's name, for a finding.
|
|
243
|
+
* @param {Map<string, object>} [options.figures] - The buffer's captioned items
|
|
244
|
+
* by id, which a local `ref` reads.
|
|
245
|
+
*/
|
|
246
|
+
export function previewSourcePlugin(md, { source = false, fm = {}, file = "", figures } = {}) {
|
|
247
|
+
const inline = (text) => {
|
|
248
|
+
const tokens = [];
|
|
249
|
+
md.inline.parse(text, md, {}, tokens);
|
|
250
|
+
return md.renderer.renderInline(tokens, md.options, {});
|
|
251
|
+
};
|
|
252
|
+
md.renderer.rules.hl_preview_source = (tokens, idx) => tokens[idx].meta.html;
|
|
253
|
+
// Run before ordinary links and text so protected source becomes one opaque token.
|
|
254
|
+
md.inline.ruler.before("link", "hl_preview_source", (state, silent) => {
|
|
255
|
+
const rest = state.src.slice(state.pos);
|
|
256
|
+
let raw, html;
|
|
257
|
+
if (source && (rest.startsWith("[[") || rest.startsWith("![["))) {
|
|
258
|
+
const match = new RegExp(`^(?:!)?${WIKILINK.source.replace("(?<!!)", "")}`).exec(rest);
|
|
259
|
+
raw = withTrailingAttributes(rest, match?.[0] ?? rest.split("\n")[0]);
|
|
260
|
+
html = wikiSource(raw);
|
|
261
|
+
} else if (rest.startsWith("{{")) {
|
|
262
|
+
const match = new RegExp(`^${EXPRESSION.source.replace("(?<![\\\\{])", "")}`).exec(
|
|
263
|
+
rest,
|
|
264
|
+
);
|
|
265
|
+
raw = match?.[0] ?? rest.split("\n")[0];
|
|
266
|
+
if (source && match && !dependentExpression(raw, fm)) {
|
|
267
|
+
const entries =
|
|
268
|
+
Array.isArray(fm.data?.events) ?
|
|
269
|
+
fm.data.events.filter((entry) => entry && typeof entry === "object")
|
|
270
|
+
: [];
|
|
271
|
+
const own = {
|
|
272
|
+
events: entries.map((entry) => ({ id: entry.id, entry })),
|
|
273
|
+
anchors: entries
|
|
274
|
+
.filter((entry) => entry.id)
|
|
275
|
+
.map((entry) => ({ slug: entry.id, kind: "event" })),
|
|
276
|
+
};
|
|
277
|
+
const events = { note: () => undefined, own: () => own };
|
|
278
|
+
const result = renderMarkdownExpressions(raw, { fm, figures, events, file });
|
|
279
|
+
if (!result.findings.length)
|
|
280
|
+
html = /^{{\s*ref\b/.test(raw) ? inline(result.markdown) : result.markdown;
|
|
281
|
+
}
|
|
282
|
+
html ??= colored("dependent", raw);
|
|
283
|
+
} else return false;
|
|
284
|
+
if (!silent) {
|
|
285
|
+
const token = state.push("hl_preview_source", "", 0);
|
|
286
|
+
token.meta = { html };
|
|
287
|
+
}
|
|
288
|
+
state.pos += raw.length;
|
|
289
|
+
return true;
|
|
290
|
+
});
|
|
291
|
+
if (source)
|
|
292
|
+
md.renderer.rules.span_source = (tokens, idx) => {
|
|
293
|
+
const token = tokens[idx];
|
|
294
|
+
const end = token.meta.labelEnd;
|
|
295
|
+
const secret = Boolean(token.meta.secret);
|
|
296
|
+
const classes = `hl-preview-span${secret ? " hl-preview-secret" : ""}`;
|
|
297
|
+
const focus = secret ? ' tabindex="0"' : "";
|
|
298
|
+
// A secret's label sits behind a cover, so it is set as the reader
|
|
299
|
+
// will read it; any other label is the authored text.
|
|
300
|
+
const text = token.content.slice(1, end);
|
|
301
|
+
const label =
|
|
302
|
+
secret ?
|
|
303
|
+
`<span class="hl-preview-label">${inline(text)}</span>`
|
|
304
|
+
: colored("label", text);
|
|
305
|
+
return `<span class="${classes}"${focus}>${
|
|
306
|
+
colored("bracket", "[") +
|
|
307
|
+
(secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
|
|
308
|
+
colored("bracket", "]") +
|
|
309
|
+
colored("attribute", token.content.slice(end + 1))
|
|
310
|
+
}</span>`;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Keep extension pipes inside their cell. Markdown's table splitter only
|
|
314
|
+
// understands escaped pipes, before inline rules have a chance to see them.
|
|
315
|
+
md.block.ruler.before(
|
|
316
|
+
"table",
|
|
317
|
+
"hl_preview_table",
|
|
318
|
+
(state, start, end, silent) => {
|
|
319
|
+
const lineAt = (line) =>
|
|
320
|
+
state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
|
|
321
|
+
if (start + 1 >= end || state.sCount[start] - state.blkIndent >= 4) return false;
|
|
322
|
+
const header = tableCells(lineAt(start)),
|
|
323
|
+
separators = tableCells(lineAt(start + 1));
|
|
324
|
+
if (
|
|
325
|
+
!lineAt(start).includes("|") ||
|
|
326
|
+
!header.length ||
|
|
327
|
+
separators.length !== header.length ||
|
|
328
|
+
!separators.every((cell) => /^:?-+:?$/.test(cell))
|
|
329
|
+
)
|
|
330
|
+
return false;
|
|
331
|
+
if (silent) return true;
|
|
332
|
+
const row = (values, tag) => {
|
|
333
|
+
state.push("tr_open", "tr", 1);
|
|
334
|
+
for (let col = 0; col < header.length; col++) {
|
|
335
|
+
const cell = state.push(`${tag}_open`, tag, 1);
|
|
336
|
+
const alignment =
|
|
337
|
+
separators[col].startsWith(":") ?
|
|
338
|
+
separators[col].endsWith(":") ?
|
|
339
|
+
"center"
|
|
340
|
+
: "left"
|
|
341
|
+
: separators[col].endsWith(":") ? "right"
|
|
342
|
+
: "";
|
|
343
|
+
if (alignment) cell.attrSet("class", `hl-preview-align-${alignment}`);
|
|
344
|
+
const token = state.push("inline", "", 0);
|
|
345
|
+
token.content = values[col] ?? "";
|
|
346
|
+
token.children = [];
|
|
347
|
+
state.push(`${tag}_close`, tag, -1);
|
|
348
|
+
}
|
|
349
|
+
state.push("tr_close", "tr", -1);
|
|
350
|
+
};
|
|
351
|
+
const open = state.push("table_open", "table", 1);
|
|
352
|
+
open.map = [start, start + 2];
|
|
353
|
+
state.push("thead_open", "thead", 1);
|
|
354
|
+
row(header, "th");
|
|
355
|
+
state.push("thead_close", "thead", -1);
|
|
356
|
+
state.push("tbody_open", "tbody", 1);
|
|
357
|
+
let line = start + 2;
|
|
358
|
+
for (; line < end && lineAt(line).trim() && lineAt(line).includes("|"); line++)
|
|
359
|
+
row(tableCells(lineAt(line)), "td");
|
|
360
|
+
state.push("tbody_close", "tbody", -1);
|
|
361
|
+
state.push("table_close", "table", -1);
|
|
362
|
+
open.map[1] = line;
|
|
363
|
+
state.line = line;
|
|
364
|
+
return true;
|
|
365
|
+
},
|
|
366
|
+
{ alt: ["paragraph", "reference"] },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// A buffer runs no query: the fence is the authored source.
|
|
370
|
+
const fence = md.renderer.rules.fence;
|
|
371
|
+
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|
372
|
+
const token = tokens[idx];
|
|
373
|
+
const name = token.info.trim().split(/\s/)[0];
|
|
374
|
+
if (["sql", "pagelist", "map"].includes(name)) {
|
|
375
|
+
const raw =
|
|
376
|
+
token.meta?.source ??
|
|
377
|
+
`${token.markup}${token.info}\n${token.content}${token.markup}`;
|
|
378
|
+
return `<pre class="hl-preview-${name === "map" ? "dependent" : "fence"}"><code>${escape(raw)}</code></pre>\n`;
|
|
379
|
+
}
|
|
380
|
+
return fence(tokens, idx, options, env, self);
|
|
381
|
+
};
|
|
382
|
+
// Capture fences from their original source lines, retaining all delimiters.
|
|
383
|
+
md.core.ruler.before("inline", "hl_preview_structure", (state) => {
|
|
384
|
+
const lines = state.src.split("\n");
|
|
385
|
+
for (const token of state.tokens)
|
|
386
|
+
if (token.type === "fence")
|
|
387
|
+
token.meta = {
|
|
388
|
+
...token.meta,
|
|
389
|
+
source: lines.slice(token.map[0], token.map[1]).join("\n"),
|
|
390
|
+
};
|
|
391
|
+
// Use parsed heading tokens so a fenced example is never a section boundary.
|
|
392
|
+
for (let i = state.tokens.length - 1; i >= 0; i--) {
|
|
393
|
+
const token = state.tokens[i];
|
|
394
|
+
if (token.type !== "heading_open") continue;
|
|
395
|
+
const content = state.tokens[i + 1];
|
|
396
|
+
const parsed = splitHeadingAttributes(content.content);
|
|
397
|
+
if (/^(?:see also|related pages)$/i.test(parsed.text)) {
|
|
398
|
+
const level = Number(token.tag.slice(1));
|
|
399
|
+
let end = i + 3;
|
|
400
|
+
while (
|
|
401
|
+
end < state.tokens.length &&
|
|
402
|
+
!(
|
|
403
|
+
state.tokens[end].type === "heading_open" &&
|
|
404
|
+
Number(state.tokens[end].tag.slice(1)) <= level
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
end++;
|
|
408
|
+
state.tokens.splice(i, end - i);
|
|
409
|
+
} else if (!parsed.problems.length && parsed.classes.includes("secret")) {
|
|
410
|
+
// A withheld heading names only its anchor; the disclosure
|
|
411
|
+
// below carries the withholding.
|
|
412
|
+
content.content = parsed.text;
|
|
413
|
+
if (parsed.id) token.attrSet("id", parsed.id);
|
|
414
|
+
token.meta = { ...token.meta, secret: true };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// A `.secret` heading hides its section through the next heading at the
|
|
418
|
+
// same or a higher level, wherever the heading sits.
|
|
419
|
+
const output = [],
|
|
420
|
+
sections = [];
|
|
421
|
+
const closeSection = () => {
|
|
422
|
+
output.push(new state.Token("details_close", "details", -1));
|
|
423
|
+
sections.pop();
|
|
424
|
+
};
|
|
425
|
+
for (const token of state.tokens) {
|
|
426
|
+
const level = token.type === "heading_open" ? Number(token.tag.slice(1)) : null;
|
|
427
|
+
while (
|
|
428
|
+
sections.length &&
|
|
429
|
+
((token.nesting === -1 && token.level < sections.at(-1).scope) ||
|
|
430
|
+
(level !== null &&
|
|
431
|
+
token.level <= sections.at(-1).scope &&
|
|
432
|
+
level <= sections.at(-1).level))
|
|
433
|
+
)
|
|
434
|
+
closeSection();
|
|
435
|
+
if (token.type === "heading_open" && token.meta?.secret) {
|
|
436
|
+
const open = new state.Token("details_open", "details", 1);
|
|
437
|
+
open.attrSet("class", "secret");
|
|
438
|
+
output.push(open, new state.Token("summary_open", "summary", 1));
|
|
439
|
+
const title = new state.Token("inline", "", 0);
|
|
440
|
+
title.content = "Secret";
|
|
441
|
+
title.children = [];
|
|
442
|
+
output.push(title, new state.Token("summary_close", "summary", -1));
|
|
443
|
+
sections.push({ level, scope: token.level });
|
|
444
|
+
}
|
|
445
|
+
output.push(token);
|
|
446
|
+
}
|
|
447
|
+
while (sections.length) closeSection();
|
|
448
|
+
state.tokens = output;
|
|
449
|
+
});
|
|
450
|
+
}
|
package/engine/project-init.mjs
CHANGED
|
@@ -85,13 +85,10 @@ function projectScripts(answers) {
|
|
|
85
85
|
"build:local": "npm install && npm run build:noci",
|
|
86
86
|
"build:content-index": "package-build content-index",
|
|
87
87
|
"build:deps": "package-build deps fetch",
|
|
88
|
-
"build:site-
|
|
89
|
-
"build:site
|
|
90
|
-
"
|
|
91
|
-
"build:site"
|
|
92
|
-
"run-s build:deps build:content-index build:site-content build:site-html build:site-root",
|
|
93
|
-
"serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
|
|
94
|
-
"serve:site-html": "hugo server --source build/hugo",
|
|
88
|
+
"build:site-render": "package-build site build",
|
|
89
|
+
"build:site": "run-s build:deps build:content-index build:site-render",
|
|
90
|
+
"serve:site-render": "package-build site serve",
|
|
91
|
+
"serve:site": "run-s build:deps build:content-index serve:site-render",
|
|
95
92
|
"build:book": "package-build pdf",
|
|
96
93
|
lint: "run-s lint:format lint:markdown lint:addresses lint:content-links",
|
|
97
94
|
"lint:format": "package-build format",
|
|
@@ -199,7 +196,6 @@ function projectManifest(answers) {
|
|
|
199
196
|
scripts: projectScripts(answers),
|
|
200
197
|
devDependencies: {
|
|
201
198
|
"@changesets/cli": "^3.0.0",
|
|
202
|
-
"@heroiclands/hugo-theme": "^0.9.0",
|
|
203
199
|
"@heroiclands/package-build": `^${answers.version}`,
|
|
204
200
|
"npm-run-all": "^4.1.5",
|
|
205
201
|
},
|
|
@@ -268,7 +264,7 @@ function projectText(answers) {
|
|
|
268
264
|
"prettier.config.js":
|
|
269
265
|
'/* SPDX-License-Identifier: GPL-3.0-or-later */\nexport { default } from "@heroiclands/package-build/prettier";\n',
|
|
270
266
|
".prettierignore": "CHANGELOG.md\n",
|
|
271
|
-
"README.md": `# ${answers.title}\n\n${answers.description}\n\nRun \`npm install\`, then \`npm run lint\`. Build the website with \`npm run build:site\`, serve it with \`npm run serve:site\`, and build the book with \`npm run build:book\`. The
|
|
267
|
+
"README.md": `# ${answers.title}\n\n${answers.description}\n\nRun \`npm install\`, then \`npm run lint\`. Build the website with \`npm run build:site\`, serve it with \`npm run serve:site\`, and build the book with \`npm run build:book\`. The book requires Typst on your PATH. Site deployment needs its own hosting configuration.\n`,
|
|
272
268
|
"LICENSE.md": license,
|
|
273
269
|
"lang/en.json": JSON.stringify({
|
|
274
270
|
[answers.contentPackage.toUpperCase()]: { Title: answers.title },
|
|
@@ -444,9 +440,6 @@ export function checkProject(rootDir) {
|
|
|
444
440
|
if (!manifest.devDependencies?.["@heroiclands/package-build"]) {
|
|
445
441
|
add("package.json", "the package-build development dependency is required");
|
|
446
442
|
}
|
|
447
|
-
if (!manifest.devDependencies?.["@heroiclands/hugo-theme"]) {
|
|
448
|
-
add("package.json", "the Hugo theme development dependency is required");
|
|
449
|
-
}
|
|
450
443
|
if (!nonempty(manifest.homepage))
|
|
451
444
|
add("package.json", "homepage is required for the website");
|
|
452
445
|
} catch (err) {
|
package/engine/prose-config.mjs
CHANGED
|
@@ -228,9 +228,9 @@ export const MARKDOWNLINT_CONFIG = Object.freeze({
|
|
|
228
228
|
MD011: true,
|
|
229
229
|
|
|
230
230
|
// MD034 — no bare URLs. Whether a bare URL becomes a link depends on the
|
|
231
|
-
// renderer, and this content is rendered by three of them (
|
|
232
|
-
// for a knowledgebase, Foundry's own markdown for compendium
|
|
233
|
-
// GitHub for the repository pages). Writing the link explicitly is the only
|
|
231
|
+
// renderer, and this content is rendered by three of them (the site
|
|
232
|
+
// renderer for a knowledgebase, Foundry's own markdown for compendium
|
|
233
|
+
// journals, and GitHub for the repository pages). Writing the link explicitly is the only
|
|
234
234
|
// form that renders the same everywhere.
|
|
235
235
|
MD034: true,
|
|
236
236
|
|
package/engine/run-words.mjs
CHANGED
|
@@ -17,13 +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
25
|
* - a form one rule the language states derives from a lexicon form: a noun
|
|
25
26
|
* plural, a verb's present form, a replacing suffix, an agreeing form or a
|
|
26
|
-
* participle (see {@link module:engine/word-forms.derivedForms}).
|
|
27
|
+
* participle, of a lexicon form or of one of its variants (see {@link module:engine/word-forms.derivedForms}).
|
|
27
28
|
*
|
|
28
29
|
* A word not accepted whole is split at its hyphens and en dashes, and each
|
|
29
30
|
* part is checked. An apostrophe joins a word, so `k'aralzon` is one word, and
|
|
@@ -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";
|