@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,376 @@
|
|
|
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 expansion step.** {@link expandContent} turns a note's authored body
|
|
16
|
+
* into markdown with every construct that reads the content index resolved,
|
|
17
|
+
* before a target renders it. In order:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Tables.** Each prepared `sql` fence and page list is replaced by the
|
|
20
|
+
* markdown its query returned, and the note's generated sections are
|
|
21
|
+
* appended — see {@link module:engine/content-tables.expandContentTables}.
|
|
22
|
+
* This runs outside code protection, because a table is authored as a
|
|
23
|
+
* fence.
|
|
24
|
+
* 2. **Figure numbers.** The expanded body's captions are numbered by
|
|
25
|
+
* {@link module:engine/content-figures.scanFigures}; a `ref` and a
|
|
26
|
+
* `[[#id]]` link read the label.
|
|
27
|
+
* 3. **Expressions.** `{{…}}` is evaluated — `sql`, `ref`, dates and the
|
|
28
|
+
* helpers — see {@link module:engine/markdown-expressions.renderMarkdownExpressions}.
|
|
29
|
+
* 4. **Wikilinks.** Each `[[…]]` outside code is resolved against the index by
|
|
30
|
+
* {@link module:engine/wikilinks.resolveWikilinks}, and written in the
|
|
31
|
+
* target's form.
|
|
32
|
+
*
|
|
33
|
+
* Steps 1 and 2 are {@link expandTables}, which a caller numbering figures
|
|
34
|
+
* across more than one note — the book — runs on its own first.
|
|
35
|
+
*
|
|
36
|
+
* **What the target decides.** Every step reads the same on each target except
|
|
37
|
+
* where the target's own medium differs:
|
|
38
|
+
*
|
|
39
|
+
* | | `foundry` | `web` | `book` |
|
|
40
|
+
* | --- | --- | --- | --- |
|
|
41
|
+
* | a resolved link | an `@UUID[…]{label}` content link | a markdown link to the page's URL | a markdown link to the page's URL, which the book sets as a cross-reference to the entry it prints |
|
|
42
|
+
* | an unresolved link | the error that fails the note, as `linkError` | a `sohl-unresolved-link` span, and a finding | the label, then `(unresolved link)`, and a finding |
|
|
43
|
+
* | a `ref` | a `[[#anchor\|label]]` link, which the link pass addresses by its journal page | a markdown link to the anchor, or to another note's page | as on the web |
|
|
44
|
+
* | the generated sections | part of the one body | resolved apart, and returned as `generated` | resolved with the body, and returned as `generated` |
|
|
45
|
+
* | the date context | the compile's link index | the site index's `dateContext`, which carries the present | as on the web |
|
|
46
|
+
* | a figure's number | counted on the note | counted on the note | counted across the book, by the caller |
|
|
47
|
+
*
|
|
48
|
+
* Expansion reports and never throws on what it finds: each step's findings
|
|
49
|
+
* are returned for the caller to report in its own form.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { collectAnchors, eventAnchors } from "./anchors.mjs";
|
|
55
|
+
import { protectCode } from "./code-fences.mjs";
|
|
56
|
+
import { scanFigures } from "./content-figures.mjs";
|
|
57
|
+
import { expandContentTables } from "./content-tables.mjs";
|
|
58
|
+
import { markGenerated, splitGenerated } from "./generated-sections.mjs";
|
|
59
|
+
import { convertNoteWikilinks } from "./helpers.mjs";
|
|
60
|
+
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
61
|
+
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
62
|
+
import { resolveCrossNoteFigures, wikiContext } from "./site-index.mjs";
|
|
63
|
+
import { resolveWebWikilinks } from "./wikilinks.mjs";
|
|
64
|
+
|
|
65
|
+
/** The targets a body is expanded for. */
|
|
66
|
+
export const EXPANSION_TARGETS = Object.freeze(["foundry", "web", "book"]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A `ref` call's Foundry rendering: a same-page wikilink rather than a
|
|
70
|
+
* Markdown link, so the wikilink pass that runs after expressions render
|
|
71
|
+
* addresses it exactly as an authored `[[#anchor|Text]]` is — by the
|
|
72
|
+
* JournalEntryPage the figure's own fence became, whether that page holds
|
|
73
|
+
* only the figure or carries it alongside other prose. A Markdown link's
|
|
74
|
+
* `#anchor` fragment resolves nowhere in Foundry, which addresses a page by
|
|
75
|
+
* UUID and an anchor within a page by that page's own anchor.
|
|
76
|
+
*
|
|
77
|
+
* @param {{anchor: string}} target - The figure's own anchor.
|
|
78
|
+
* @param {string} label - The link's text, which may itself carry Markdown.
|
|
79
|
+
* @returns {string} `[[#anchor|label]]`, with a literal `|` in the label
|
|
80
|
+
* escaped so it cannot be read as the wikilink's own separator.
|
|
81
|
+
*/
|
|
82
|
+
function foundryRefLink({ anchor }, label) {
|
|
83
|
+
return `[[#${anchor}|${label.replace(/\|/g, "\\|")}]]`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The note a body belongs to.
|
|
88
|
+
*
|
|
89
|
+
* @typedef {object} ExpandedNote
|
|
90
|
+
* @property {Record<string, any>} fm - Its frontmatter.
|
|
91
|
+
* @property {string} [file] - Its file, which findings name.
|
|
92
|
+
* @property {string} source - What a table's or a link's finding names the
|
|
93
|
+
* note by: its path on the web, its name in Foundry.
|
|
94
|
+
* @property {number} [bodyLine] - The file line its body starts on.
|
|
95
|
+
* @property {number} [bodyColumn] - The column its body starts at on that line.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* How a target resolves links, beyond the index.
|
|
100
|
+
*
|
|
101
|
+
* @typedef {object} ExpandLinks
|
|
102
|
+
* @property {string|null} [pack] - `foundry`: the pack the note's own document
|
|
103
|
+
* lands in, which a `[[#slug]]` self-link addresses.
|
|
104
|
+
* @property {string|null} [docPack] - `foundry`: the pack its journal lands in.
|
|
105
|
+
* @property {Map<string, object>} [foreign] - `web` and `book`: the merged
|
|
106
|
+
* foreign index.
|
|
107
|
+
* @property {object} [assets] - `web` and `book`: the address space an embed
|
|
108
|
+
* resolves against, from {@link module:engine/art-fields.assetAddressIndex}.
|
|
109
|
+
* @property {(text: string) => string} [beforeLinks] - `web` and `book`: a pass
|
|
110
|
+
* run on the text outside code before its links resolve.
|
|
111
|
+
* @property {(text: string) => string} [afterLinks] - `web` and `book`: a pass
|
|
112
|
+
* run on the text outside code once its links resolve.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The first two steps of an expansion: the tables, and the figures of the
|
|
117
|
+
* table-expanded body.
|
|
118
|
+
*
|
|
119
|
+
* @typedef {object} ExpandedTables
|
|
120
|
+
* @property {{markdown: string, errors: object[], lineMap: object[],
|
|
121
|
+
* generatedFrom: number|undefined}} tables - From
|
|
122
|
+
* {@link module:engine/content-tables.expandContentTables}.
|
|
123
|
+
* @property {{figures: object[], errors: object[]}} figures - The figure scan
|
|
124
|
+
* of `tables.markdown`. A caller numbering figures across notes replaces
|
|
125
|
+
* `figures.figures` with its numbering before expanding the rest.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Expand a note's tables and scan the result for figures — steps 1 and 2 of
|
|
130
|
+
* {@link expandContent}.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} markdown - The body as authored.
|
|
133
|
+
* @param {object} options
|
|
134
|
+
* @param {string} options.source - What a table's finding names the note by.
|
|
135
|
+
* @param {{sqlTables?: object[], pageLists?: object[]}} [options.prepared] -
|
|
136
|
+
* This note's prepared SQL tables and page lists.
|
|
137
|
+
* @param {(address: string) => string|undefined} [options.resolveRole] - The
|
|
138
|
+
* role an embedded picture takes, which numbers a map apart from a figure.
|
|
139
|
+
* @returns {ExpandedTables} The two steps' results.
|
|
140
|
+
*/
|
|
141
|
+
export function expandTables(markdown, { source, prepared = {}, resolveRole }) {
|
|
142
|
+
const tables = expandContentTables(markdown ?? "", {
|
|
143
|
+
source,
|
|
144
|
+
sqlTables: prepared.sqlTables,
|
|
145
|
+
pageLists: prepared.pageLists,
|
|
146
|
+
});
|
|
147
|
+
return { tables, figures: scanFigures(tables.markdown, { resolveRole }) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The expanded body, and what each step found.
|
|
152
|
+
*
|
|
153
|
+
* @typedef {object} Expansion
|
|
154
|
+
* @property {string} markdown - The whole body, expanded.
|
|
155
|
+
* @property {string} authored - The author's text, expanded. In Foundry it is
|
|
156
|
+
* the whole body.
|
|
157
|
+
* @property {string} generated - The generated sections, expanded apart from
|
|
158
|
+
* the author's text on the web; `""` in Foundry, and wherever there are none.
|
|
159
|
+
* @property {Array<{line: number, generated: boolean}>} lineMap - The authored
|
|
160
|
+
* line, 0-based, each line of the table-expanded body came from.
|
|
161
|
+
* @property {number|undefined} generatedFrom - The 0-based line of the
|
|
162
|
+
* table-expanded body the generated sections start on.
|
|
163
|
+
* @property {{figures: object[], errors: object[]}} figures - The figure scan
|
|
164
|
+
* of the table-expanded body.
|
|
165
|
+
* @property {{tables: object[], captions: object[], expressions: object[], links: object[]}} findings -
|
|
166
|
+
* Each step's findings, as the step reports them: a table's and a caption's
|
|
167
|
+
* by expanded body line, an expression's in file coordinates, a link's by
|
|
168
|
+
* occurrence of its literal.
|
|
169
|
+
* @property {Error|undefined} linkError - `foundry`: the error
|
|
170
|
+
* {@link module:engine/helpers.convertNoteWikilinks} fails a note with,
|
|
171
|
+
* carrying `file` and `position`.
|
|
172
|
+
* @property {object[]} unresolved - `foundry`: the links that did not resolve.
|
|
173
|
+
* @property {object[]} resolved - `web` and `book`: every link that resolved,
|
|
174
|
+
* in order.
|
|
175
|
+
* @property {number} authoredLinks - `web`: how many of `resolved` the
|
|
176
|
+
* author's text made, before the generated sections'.
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Expand a note's body for one target.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} markdown - The body as authored.
|
|
183
|
+
* @param {object} options
|
|
184
|
+
* @param {"foundry"|"web"|"book"} options.target - What the body is expanded
|
|
185
|
+
* for.
|
|
186
|
+
* @param {object} options.index - What links resolve against: the compile's
|
|
187
|
+
* link index for `foundry`, the {@link module:engine/site-index.buildSiteIndex}
|
|
188
|
+
* index for `web` and `book`.
|
|
189
|
+
* @param {ExpandedNote} options.note - The note.
|
|
190
|
+
* @param {{sqlTables?: object[], pageLists?: object[], inline?: object[]}} [options.prepared] -
|
|
191
|
+
* This note's prepared SQL tables, page lists and inline query results.
|
|
192
|
+
* @param {object} [options.events] - Every note's events, from
|
|
193
|
+
* {@link module:engine/event-fields.eventNoteIndex}.
|
|
194
|
+
* @param {(address: string) => string|undefined} [options.resolveRole] - The
|
|
195
|
+
* role an embedded picture takes, which numbers a map apart from a figure.
|
|
196
|
+
* @param {Map<string, Map<string, object>>} [options.figuresByAddress] - `web`
|
|
197
|
+
* and `book`: every page's figures, by the address a cross-note `ref` writes.
|
|
198
|
+
* @param {ExpandedTables} [options.tables] - The note's {@link expandTables},
|
|
199
|
+
* already run — with the figures numbered, for the book. Run here when
|
|
200
|
+
* omitted.
|
|
201
|
+
* @param {ExpandLinks} [options.links] - How the target resolves links.
|
|
202
|
+
* @returns {Expansion} The expanded body and the findings.
|
|
203
|
+
* @throws {Error} When `target` is not one of {@link EXPANSION_TARGETS}.
|
|
204
|
+
*/
|
|
205
|
+
export function expandContent(
|
|
206
|
+
markdown,
|
|
207
|
+
{
|
|
208
|
+
target,
|
|
209
|
+
index,
|
|
210
|
+
note,
|
|
211
|
+
prepared = {},
|
|
212
|
+
events,
|
|
213
|
+
resolveRole,
|
|
214
|
+
figuresByAddress = new Map(),
|
|
215
|
+
tables: staged,
|
|
216
|
+
links = {},
|
|
217
|
+
},
|
|
218
|
+
) {
|
|
219
|
+
if (!EXPANSION_TARGETS.includes(target))
|
|
220
|
+
throw new Error(
|
|
221
|
+
`\`${target}\` is not an expansion target; expected ` +
|
|
222
|
+
EXPANSION_TARGETS.slice(0, -1)
|
|
223
|
+
.map((name) => `\`${name}\``)
|
|
224
|
+
.join(", ") +
|
|
225
|
+
` or \`${EXPANSION_TARGETS.at(-1)}\``,
|
|
226
|
+
);
|
|
227
|
+
const { fm, file, source, bodyLine, bodyColumn } = note;
|
|
228
|
+
// The website and the book read the site index, and resolve the same way.
|
|
229
|
+
const web = target !== "foundry";
|
|
230
|
+
|
|
231
|
+
const { tables, figures } = staged ?? expandTables(markdown, { source, prepared, resolveRole });
|
|
232
|
+
const { lineMap, generatedFrom } = tables;
|
|
233
|
+
const figuresById = new Map(
|
|
234
|
+
figures.figures
|
|
235
|
+
.filter((figure) => figure.id)
|
|
236
|
+
.map((figure) => [
|
|
237
|
+
figure.id,
|
|
238
|
+
{ label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
|
|
239
|
+
]),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const expressions = renderMarkdownExpressions(
|
|
243
|
+
web ? markGenerated(tables.markdown, generatedFrom) : tables.markdown,
|
|
244
|
+
{
|
|
245
|
+
fm,
|
|
246
|
+
dates: web ? index.dateContext : reckoningContext(index),
|
|
247
|
+
file,
|
|
248
|
+
bodyLine,
|
|
249
|
+
sqlResults: prepared.inline,
|
|
250
|
+
events,
|
|
251
|
+
figures:
|
|
252
|
+
web ?
|
|
253
|
+
{
|
|
254
|
+
get: (id) => figuresById.get(id),
|
|
255
|
+
// A cross-note `ref` resolves the written address
|
|
256
|
+
// exactly as a wikilink does, against every page's
|
|
257
|
+
// figures scanned before any page's expressions render.
|
|
258
|
+
note: (address) =>
|
|
259
|
+
resolveCrossNoteFigures(address, {
|
|
260
|
+
contentTypes: index.contentTypes,
|
|
261
|
+
packages: index.packages,
|
|
262
|
+
noIndexPackages: index.noIndexPackages,
|
|
263
|
+
contentPackage: index.contentPackage,
|
|
264
|
+
siteIndexMap: index.index,
|
|
265
|
+
figuresByAddress,
|
|
266
|
+
}),
|
|
267
|
+
}
|
|
268
|
+
: { get: (id) => figuresById.get(id), link: foundryRefLink },
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const findings = {
|
|
273
|
+
tables: tables.errors,
|
|
274
|
+
captions: figures.errors,
|
|
275
|
+
expressions: expressions.findings,
|
|
276
|
+
links: [],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (!web) {
|
|
280
|
+
let converted;
|
|
281
|
+
let linkError;
|
|
282
|
+
try {
|
|
283
|
+
converted = convertNoteWikilinks(expressions.markdown, {
|
|
284
|
+
type: fm.type,
|
|
285
|
+
id: fm.id,
|
|
286
|
+
file,
|
|
287
|
+
bodyLine,
|
|
288
|
+
bodyColumn,
|
|
289
|
+
lineMap,
|
|
290
|
+
pack: links.pack,
|
|
291
|
+
docPack: links.docPack,
|
|
292
|
+
index,
|
|
293
|
+
name: source,
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
linkError = error;
|
|
297
|
+
}
|
|
298
|
+
const body = converted?.markdown ?? expressions.markdown;
|
|
299
|
+
return {
|
|
300
|
+
markdown: body,
|
|
301
|
+
authored: body,
|
|
302
|
+
generated: "",
|
|
303
|
+
lineMap,
|
|
304
|
+
generatedFrom,
|
|
305
|
+
figures,
|
|
306
|
+
findings,
|
|
307
|
+
linkError,
|
|
308
|
+
unresolved: converted?.unresolved ?? [],
|
|
309
|
+
resolved: [],
|
|
310
|
+
authoredLinks: 0,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const resolved = [];
|
|
315
|
+
const book = target === "book";
|
|
316
|
+
const ctx = wikiContext(index, {
|
|
317
|
+
src: source,
|
|
318
|
+
file,
|
|
319
|
+
type: fm.type ?? null,
|
|
320
|
+
errors: findings.links,
|
|
321
|
+
foreignIndex: links.foreign,
|
|
322
|
+
assets: links.assets,
|
|
323
|
+
resolved,
|
|
324
|
+
});
|
|
325
|
+
ctx.captionLabels = new Map(
|
|
326
|
+
figures.figures.filter((figure) => figure.id).map((figure) => [figure.id, figure.label]),
|
|
327
|
+
);
|
|
328
|
+
// This page's own anchors, so a `[[#slug]]` self-link is checked against
|
|
329
|
+
// what the page actually declares rather than trusted unconditionally.
|
|
330
|
+
ctx.anchors = new Set(collectAnchors(tables.markdown).map((anchor) => anchor.slug));
|
|
331
|
+
ctx.eventAnchors = new Set(eventAnchors(fm).map((anchor) => anchor.slug));
|
|
332
|
+
if (book) ctx.output = "book";
|
|
333
|
+
const resolve = (text) => {
|
|
334
|
+
const before = links.beforeLinks ? links.beforeLinks(text) : text;
|
|
335
|
+
const linked = resolveWebWikilinks(before, ctx);
|
|
336
|
+
return links.afterLinks ? links.afterLinks(linked) : linked;
|
|
337
|
+
};
|
|
338
|
+
// The book resolves the body whole, so a link's occurrence counts across
|
|
339
|
+
// the author's text and the generated sections it prints after it.
|
|
340
|
+
if (book) {
|
|
341
|
+
const split = splitGenerated(protectCode(expressions.markdown, resolve));
|
|
342
|
+
return {
|
|
343
|
+
markdown: split.generated ? `${split.authored}\n${split.generated}` : split.authored,
|
|
344
|
+
authored: split.authored,
|
|
345
|
+
generated: split.generated ?? "",
|
|
346
|
+
lineMap,
|
|
347
|
+
generatedFrom,
|
|
348
|
+
figures,
|
|
349
|
+
findings,
|
|
350
|
+
linkError: undefined,
|
|
351
|
+
unresolved: [],
|
|
352
|
+
resolved,
|
|
353
|
+
authoredLinks: 0,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// The author's text and the generated sections are resolved apart, so a
|
|
357
|
+
// caller can tell the links the author wrote from the ones a section
|
|
358
|
+
// derives.
|
|
359
|
+
const split = splitGenerated(expressions.markdown);
|
|
360
|
+
const authored = protectCode(split.authored, resolve);
|
|
361
|
+
const authoredLinks = resolved.length;
|
|
362
|
+
const generated = split.generated ? protectCode(split.generated, resolve) : "";
|
|
363
|
+
return {
|
|
364
|
+
markdown: split.generated ? `${authored}\n${generated}` : authored,
|
|
365
|
+
authored,
|
|
366
|
+
generated,
|
|
367
|
+
lineMap,
|
|
368
|
+
generatedFrom,
|
|
369
|
+
figures,
|
|
370
|
+
findings,
|
|
371
|
+
linkError: undefined,
|
|
372
|
+
unresolved: [],
|
|
373
|
+
resolved,
|
|
374
|
+
authoredLinks,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -374,18 +374,24 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
374
374
|
* @param {string} source - A note body with expanded tables.
|
|
375
375
|
* @param {(markdown: string) => string} [renderMarkdown] - How to render a
|
|
376
376
|
* figure's own contents.
|
|
377
|
-
* @param {Array<{id: string, label: string}>} [numbers] -
|
|
378
|
-
*
|
|
377
|
+
* @param {Array<{id: string, kind: string, label: string}>} [numbers] - The
|
|
378
|
+
* figures as {@link scanFigures} numbered them, with roles, where the
|
|
379
|
+
* surface counted them: across a note's pages, or in expansion, where a
|
|
380
|
+
* `ref` reads the same label. They are matched in order when they are as many
|
|
381
|
+
* as the text holds, by id otherwise, and the caption takes its number, kind
|
|
382
|
+
* and label from them. Omitted, the text is numbered as it stands.
|
|
379
383
|
* @param {object} [options] - Options.
|
|
380
384
|
* @param {(address: string) => string|undefined} [options.resolveRole] - See
|
|
381
385
|
* {@link scanFigures}.
|
|
386
|
+
* @param {(markdown: string) => string} [options.renderInline] - How a caption
|
|
387
|
+
* is rendered; omitted, a caption is plain inline Markdown.
|
|
382
388
|
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
|
|
383
389
|
*/
|
|
384
390
|
export function renderFigureBlocks(
|
|
385
391
|
source,
|
|
386
392
|
renderMarkdown = parser.render.bind(parser),
|
|
387
393
|
numbers,
|
|
388
|
-
{ resolveRole } = {},
|
|
394
|
+
{ resolveRole, renderInline = parser.renderInline.bind(parser) } = {},
|
|
389
395
|
) {
|
|
390
396
|
const { figures, errors } = scanFigures(source, { resolveRole });
|
|
391
397
|
if (errors.length) return { markdown: source, errors };
|
|
@@ -393,12 +399,13 @@ export function renderFigureBlocks(
|
|
|
393
399
|
numbers ?
|
|
394
400
|
new Map(numbers.filter((entry) => entry.id).map((entry) => [entry.id, entry]))
|
|
395
401
|
: new Map();
|
|
402
|
+
const inOrder = numbers?.length === figures.length ? numbers : undefined;
|
|
396
403
|
const lines = String(source ?? "").split("\n");
|
|
397
404
|
const output = [];
|
|
398
405
|
let cursor = 0;
|
|
399
|
-
for (const figure of figures) {
|
|
406
|
+
for (const [at, figure] of figures.entries()) {
|
|
400
407
|
output.push(...lines.slice(cursor, figure.line - 1));
|
|
401
|
-
const numbered = (figure.id ? byId.get(figure.id) : null) ?? figure;
|
|
408
|
+
const numbered = inOrder?.[at] ?? (figure.id ? byId.get(figure.id) : null) ?? figure;
|
|
402
409
|
const contents = lines.slice(figure.bodyStart, figure.bodyEnd).join("\n");
|
|
403
410
|
const classes = ["content-figure", `content-figure-${numbered.kind}`, ...figure.classes];
|
|
404
411
|
const id = figure.slug ? ` id="${escapeHtml(figure.slug)}"` : "";
|
|
@@ -416,7 +423,7 @@ export function renderFigureBlocks(
|
|
|
416
423
|
output.push(
|
|
417
424
|
`<p class="content-figure-label">${
|
|
418
425
|
figure.hasCaption ?
|
|
419
|
-
`${label ? `${label}: ` : ""}${
|
|
426
|
+
`${label ? `${label}: ` : ""}${renderInline(figure.caption)}`
|
|
420
427
|
: label
|
|
421
428
|
}</p>`,
|
|
422
429
|
"</div>",
|
|
@@ -207,6 +207,7 @@ types:
|
|
|
207
207
|
- names
|
|
208
208
|
- family
|
|
209
209
|
- abbreviation
|
|
210
|
+
- core
|
|
210
211
|
- classModifiers
|
|
211
212
|
- fields
|
|
212
213
|
- words
|
|
@@ -224,6 +225,8 @@ types:
|
|
|
224
225
|
- older
|
|
225
226
|
- negation
|
|
226
227
|
- naming
|
|
228
|
+
- emphasis
|
|
229
|
+
- script
|
|
227
230
|
subTypes:
|
|
228
231
|
- social
|
|
229
232
|
- nature
|
|
@@ -363,7 +366,11 @@ types:
|
|
|
363
366
|
doc:
|
|
364
367
|
data:
|
|
365
368
|
- culture
|
|
369
|
+
- concepts
|
|
370
|
+
- date
|
|
371
|
+
- author
|
|
366
372
|
- map
|
|
373
|
+
- metres
|
|
367
374
|
subTypes:
|
|
368
375
|
- rules
|
|
369
376
|
- userguide
|
|
@@ -371,6 +378,7 @@ types:
|
|
|
371
378
|
- howto
|
|
372
379
|
- concept
|
|
373
380
|
- settingguide
|
|
381
|
+
- post
|
|
374
382
|
macro:
|
|
375
383
|
data: []
|
|
376
384
|
subTypes: []
|
|
@@ -0,0 +1,213 @@
|
|
|
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 vocabulary of the two picture constructs a note writes for a sign.
|
|
16
|
+
*
|
|
17
|
+
* Both are spans, set in a line of text:
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* The name [Ra]{.glyph slug=ra} is written with one sign, and
|
|
21
|
+
* [Au'Khelathu]{.cartouche slug=aukhelathu height=10rem align=right caption="Au'Khelathu"}
|
|
22
|
+
* is sealed in a frame.
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* A span's label is its alt text; with an empty label the humanized slug
|
|
26
|
+
* stands in. A cartouche's caption is an attribute.
|
|
27
|
+
*
|
|
28
|
+
* The picture is a file the package ships, addressed by the one pathname rule
|
|
29
|
+
* every picture follows, so each target serves the address it can: `icons/<slug>.svg`
|
|
30
|
+
* for a glyph and `images/cartouches/<slug>.svg` for a cartouche. Both
|
|
31
|
+
* closed vocabularies below are refused when departed from, because an
|
|
32
|
+
* attribute that cannot mean anything must not read as one that worked.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Text safe inside a double-quoted attribute or an element.
|
|
39
|
+
* @param {string} value - Authored text.
|
|
40
|
+
* @returns {string} The text with markup characters escaped.
|
|
41
|
+
*/
|
|
42
|
+
function escapeHtml(value) {
|
|
43
|
+
return String(value)
|
|
44
|
+
.replace(/&/g, "&")
|
|
45
|
+
.replace(/</g, "<")
|
|
46
|
+
.replace(/>/g, ">")
|
|
47
|
+
.replace(/"/g, """);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The frames a glyph may sit in. */
|
|
51
|
+
export const GLYPH_FRAMES = Object.freeze(["tablet"]);
|
|
52
|
+
|
|
53
|
+
/** The sizes a glyph may take. */
|
|
54
|
+
export const GLYPH_SIZES = Object.freeze(["inline", "block"]);
|
|
55
|
+
|
|
56
|
+
/** The sides a cartouche may float to; absent, it is centred. */
|
|
57
|
+
export const CARTOUCHE_ALIGNS = Object.freeze(["left", "right"]);
|
|
58
|
+
|
|
59
|
+
/** The attribute keys a glyph takes. */
|
|
60
|
+
export const GLYPH_KEYS = Object.freeze(["slug", "frame", "size"]);
|
|
61
|
+
/** The attribute keys a cartouche takes. */
|
|
62
|
+
export const CARTOUCHE_KEYS = Object.freeze(["slug", "align", "height", "caption"]);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The units a cartouche height may be written in: every one a printed page
|
|
66
|
+
* can honour as well as a screen.
|
|
67
|
+
*/
|
|
68
|
+
export const HEIGHT_UNITS = Object.freeze(["rem", "em", "px", "pt", "cm", "mm", "in"]);
|
|
69
|
+
|
|
70
|
+
const SLUG = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
71
|
+
const HEIGHT = new RegExp(`^(\\d+(?:\\.\\d+)?)(${HEIGHT_UNITS.join("|")})$`);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The pathname of a glyph's picture.
|
|
75
|
+
* @param {string} slug - The glyph's slug.
|
|
76
|
+
* @returns {string} The authored pathname.
|
|
77
|
+
*/
|
|
78
|
+
export function glyphPathname(slug) {
|
|
79
|
+
return `icons/${slug}.svg`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pathname of a cartouche's picture.
|
|
84
|
+
* @param {string} slug - The cartouche's slug.
|
|
85
|
+
* @returns {string} The authored pathname.
|
|
86
|
+
*/
|
|
87
|
+
export function cartouchePathname(slug) {
|
|
88
|
+
return `images/cartouches/${slug}.svg`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A slug as words, for the alt text of a sign whose author wrote none.
|
|
93
|
+
* @param {string} slug - The slug.
|
|
94
|
+
* @returns {string} Title-cased words.
|
|
95
|
+
*/
|
|
96
|
+
export function humanizeSlug(slug) {
|
|
97
|
+
return String(slug)
|
|
98
|
+
.split(/[-_]+/)
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
101
|
+
.join(" ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What is wrong with an attribute list common to both constructs.
|
|
106
|
+
* @param {{id: string, classes: string[], values: Record<string, string>}} parsed - The parsed attributes.
|
|
107
|
+
* @param {string} noun - `glyph` or `cartouche`.
|
|
108
|
+
* @param {readonly string[]} keys - The keys the construct takes.
|
|
109
|
+
* @returns {string[]} One message per problem.
|
|
110
|
+
*/
|
|
111
|
+
function sharedProblems({ id, classes, values }, noun, keys) {
|
|
112
|
+
const problems = [];
|
|
113
|
+
if (id) problems.push(`a ${noun} takes no #id`);
|
|
114
|
+
for (const name of classes.filter((name) => name !== noun))
|
|
115
|
+
problems.push(`a ${noun} takes no class besides .${noun}; .${name} is not one`);
|
|
116
|
+
for (const key of Object.keys(values))
|
|
117
|
+
if (!keys.includes(key) && !/^on/i.test(key) && !["id", "class"].includes(key))
|
|
118
|
+
problems.push(`a ${noun} takes ${keys.join(", ")}; \`${key}\` is not one of them`);
|
|
119
|
+
if (!Object.hasOwn(values, "slug"))
|
|
120
|
+
problems.push(`a ${noun} needs slug="…" naming its picture`);
|
|
121
|
+
else if (!SLUG.test(values.slug))
|
|
122
|
+
problems.push(`slug \`${values.slug}\` must be letters, digits, \`-\` and \`_\``);
|
|
123
|
+
return problems;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What is wrong with a glyph's attributes.
|
|
128
|
+
* @param {{id: string, classes: string[], values: Record<string, string>}} parsed - The parsed attributes.
|
|
129
|
+
* @returns {string[]} One message per problem.
|
|
130
|
+
*/
|
|
131
|
+
export function glyphProblems(parsed) {
|
|
132
|
+
const problems = sharedProblems(parsed, "glyph", GLYPH_KEYS);
|
|
133
|
+
const { values } = parsed;
|
|
134
|
+
if (Object.hasOwn(values, "alt"))
|
|
135
|
+
problems.splice(
|
|
136
|
+
problems.findIndex((message) => message.includes("`alt`")),
|
|
137
|
+
1,
|
|
138
|
+
"write a glyph's alt text as its label: [alt text]{.glyph slug=…}",
|
|
139
|
+
);
|
|
140
|
+
if (Object.hasOwn(values, "frame") && !GLYPH_FRAMES.includes(values.frame))
|
|
141
|
+
problems.push(`frame \`${values.frame}\` is not one of ${GLYPH_FRAMES.join(", ")}`);
|
|
142
|
+
if (Object.hasOwn(values, "size") && !GLYPH_SIZES.includes(values.size))
|
|
143
|
+
problems.push(`size \`${values.size}\` is not one of ${GLYPH_SIZES.join(", ")}`);
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What is wrong with a cartouche's attributes.
|
|
149
|
+
* @param {{id: string, classes: string[], values: Record<string, string>}} parsed - The parsed attributes.
|
|
150
|
+
* @returns {string[]} One message per problem.
|
|
151
|
+
*/
|
|
152
|
+
export function cartoucheProblems(parsed) {
|
|
153
|
+
const { values } = parsed;
|
|
154
|
+
const problems = sharedProblems(parsed, "cartouche", CARTOUCHE_KEYS);
|
|
155
|
+
if (Object.hasOwn(values, "alt"))
|
|
156
|
+
problems.splice(
|
|
157
|
+
problems.findIndex((message) => message.includes("`alt`")),
|
|
158
|
+
1,
|
|
159
|
+
"write a cartouche's alt text as its label: [alt text]{.cartouche slug=…}",
|
|
160
|
+
);
|
|
161
|
+
if (Object.hasOwn(values, "align") && !CARTOUCHE_ALIGNS.includes(values.align))
|
|
162
|
+
problems.push(`align \`${values.align}\` is not one of ${CARTOUCHE_ALIGNS.join(", ")}`);
|
|
163
|
+
if (Object.hasOwn(values, "height") && !HEIGHT.test(values.height))
|
|
164
|
+
problems.push(
|
|
165
|
+
`height \`${values.height}\` must be a number in ${HEIGHT_UNITS.join(", ")}, such as 10rem`,
|
|
166
|
+
);
|
|
167
|
+
return problems;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A cartouche's height as Typst writes a length.
|
|
172
|
+
* @param {string} height - A validated CSS length.
|
|
173
|
+
* @returns {string} The same length in a unit Typst reads.
|
|
174
|
+
*/
|
|
175
|
+
export function typstLength(height) {
|
|
176
|
+
const [, number, unit] = HEIGHT.exec(height);
|
|
177
|
+
if (unit === "rem") return `${number}em`;
|
|
178
|
+
if (unit === "px") return `${Number(number) * 0.75}pt`;
|
|
179
|
+
return `${number}${unit}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A glyph as HTML.
|
|
184
|
+
* @param {{slug: string, alt: string, frame?: string, size?: string}} glyph - Its attributes.
|
|
185
|
+
* @param {string} src - The address the target serves its picture at.
|
|
186
|
+
* @returns {string} A span holding an image.
|
|
187
|
+
*/
|
|
188
|
+
export function glyphHtml({ slug, alt, frame, size }, src) {
|
|
189
|
+
const classes = [
|
|
190
|
+
"glyph",
|
|
191
|
+
...(frame ? [`glyph-${frame}`] : []),
|
|
192
|
+
...(size ? [`glyph-${size}`] : []),
|
|
193
|
+
];
|
|
194
|
+
return `<span class="${classes.join(" ")}"><img src="${escapeHtml(src)}" alt="${escapeHtml(alt || humanizeSlug(slug))}" loading="lazy" /></span>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A cartouche as HTML.
|
|
199
|
+
* @param {{slug: string, alt?: string, align?: string, height?: string, caption?: string}} cartouche - Its attributes.
|
|
200
|
+
* @param {string} src - The address the target serves its picture at.
|
|
201
|
+
* @returns {string} A figure holding an image and an optional caption, on one
|
|
202
|
+
* line so that it sits inside a paragraph without ending it.
|
|
203
|
+
*/
|
|
204
|
+
export function cartoucheHtml({ slug, alt, align, height, caption }, src) {
|
|
205
|
+
const style =
|
|
206
|
+
height ? ` style="height: ${escapeHtml(height)}; width: auto; max-width: none;"` : "";
|
|
207
|
+
return [
|
|
208
|
+
`<figure class="cartouche${align ? ` cartouche-${align}` : ""}">`,
|
|
209
|
+
`<img src="${escapeHtml(src)}" alt="${escapeHtml(alt || humanizeSlug(slug))}" loading="lazy"${style} />`,
|
|
210
|
+
...(caption ? [`<figcaption>${escapeHtml(caption)}</figcaption>`] : []),
|
|
211
|
+
"</figure>",
|
|
212
|
+
].join("");
|
|
213
|
+
}
|