@heroiclands/package-build 25.3.0 → 25.4.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 +15 -0
- package/README.md +1 -1
- package/assets/styles/content-base.css +32 -0
- package/docs/api.md +30 -6
- package/docs/authoring/links-and-markup.md +136 -53
- package/docs/commands.md +2 -2
- package/docs/content-format.md +14 -6
- package/docs/diagnostics.md +12 -11
- package/docs/guides/writing-in-invented-scripts.md +62 -35
- package/docs/reference/format-details.md +6 -6
- package/docs/reference/note-types.md +15 -15
- package/docs/reference/theme-pages.md +26 -18
- package/engine/anchors.mjs +14 -0
- package/engine/code-fences.mjs +15 -6
- package/engine/content-blocks.mjs +4 -2
- package/engine/content-figures.mjs +19 -5
- package/engine/content-footnotes.mjs +60 -1
- package/engine/content-index.mjs +1 -1
- package/engine/content-render.mjs +59 -43
- package/engine/content-scripts.mjs +204 -42
- package/engine/content-spans.mjs +73 -5
- package/engine/content-verse.mjs +320 -192
- package/engine/first-use.mjs +43 -12
- package/engine/heading-attributes.mjs +2 -1
- package/engine/journals.mjs +8 -1
- package/engine/markdown-blocks.mjs +3 -2
- package/engine/markup-validation.mjs +17 -0
- package/engine/name-markup.mjs +1 -0
- package/engine/note-vocabulary.mjs +2 -2
- package/engine/pdf-render.mjs +34 -23
- package/engine/prose-lint.mjs +14 -2
- package/engine/run-rows.mjs +1 -1
- package/engine/sql-tables.mjs +1 -1
- package/engine/verse-format.mjs +77 -0
- package/engine/verse-metre.mjs +18 -9
- package/engine/verse-regions.mjs +181 -0
- package/engine/wikilink-pattern.mjs +31 -0
- package/engine/wikilink-syntax.mjs +2 -16
- package/package.json +1 -1
- package/site-theme/static/css/style.css +24 -0
- package/types/engine/code-fences.d.mts +1 -3
- package/types/engine/content-footnotes.d.mts +23 -0
- package/types/engine/content-scripts.d.mts +4 -3
- package/types/engine/content-spans.d.mts +3 -1
- package/types/engine/content-verse.d.mts +69 -74
- package/types/engine/name-markup.d.mts +5 -0
- package/types/engine/verse-format.d.mts +9 -0
- package/types/engine/verse-metre.d.mts +4 -4
- package/types/engine/verse-regions.d.mts +78 -0
- package/types/engine/wikilink-pattern.d.mts +17 -0
- package/types/engine/wikilink-syntax.d.mts +1 -41
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
import MarkdownIt from "markdown-it";
|
|
15
|
+
import { WIKILINK } from "./wikilink-pattern.mjs";
|
|
16
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A raw verse div, opaque to ordinary Markdown block syntax.
|
|
20
|
+
* @typedef {object} VerseRegion
|
|
21
|
+
* @property {number} start - Opening line, zero based.
|
|
22
|
+
* @property {number} end - Closing line, zero based and inclusive.
|
|
23
|
+
* @property {number} from - First offset of the complete region.
|
|
24
|
+
* @property {number} to - Offset past the complete region.
|
|
25
|
+
* @property {number} bodyStart - First body offset.
|
|
26
|
+
* @property {number} bodyEnd - Offset past body text, excluding its final newline.
|
|
27
|
+
* @property {string} body - Authored body bytes.
|
|
28
|
+
* @property {Record<string,string>} attributes - Authored values.
|
|
29
|
+
* @property {string} id - Anchor or empty string.
|
|
30
|
+
* @property {string[]} classes - Authored classes including verse.
|
|
31
|
+
* @property {string} indent - Opening indentation.
|
|
32
|
+
* @property {"verse"} name - Region kind.
|
|
33
|
+
* @property {boolean} closed - Whether a closing delimiter is present.
|
|
34
|
+
* @property {string[]} problems - Attribute syntax findings.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Recognize an opener without interpreting its body as Markdown blocks. */
|
|
38
|
+
function opener(line) {
|
|
39
|
+
const named = /^ *:::verse(?:[ \t]+(.*))?$/.exec(line);
|
|
40
|
+
const generic = /^ *:::[ \t]+(\{.*)$/.exec(line);
|
|
41
|
+
if (!named && !generic) return undefined;
|
|
42
|
+
const raw = (named?.[1] ?? generic?.[1] ?? "").trim();
|
|
43
|
+
const braced = /^\{[^\n]*\}$/.test(raw);
|
|
44
|
+
const parsed = parseExtensionAttributes(braced ? raw.slice(1, -1) : raw.replace(/^\{/, ""));
|
|
45
|
+
if (!named && !parsed.classes.includes("verse")) return undefined;
|
|
46
|
+
if (raw && !braced) parsed.problems.push("verse div attributes need {…} braces");
|
|
47
|
+
if (!parsed.classes.includes("verse")) parsed.classes.unshift("verse");
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Install the opaque verse-div block rule. Tokens carry raw source regions.
|
|
53
|
+
* @param {import("markdown-it")} md - Markdown parser.
|
|
54
|
+
*/
|
|
55
|
+
export function verseDivPlugin(md) {
|
|
56
|
+
md.block.ruler.before(
|
|
57
|
+
"code",
|
|
58
|
+
"verse_div",
|
|
59
|
+
(state, first, past, silent) => {
|
|
60
|
+
if (state.sCount[first] - state.blkIndent >= 4) return false;
|
|
61
|
+
const line = state.src.slice(
|
|
62
|
+
state.bMarks[first] + state.tShift[first],
|
|
63
|
+
state.eMarks[first],
|
|
64
|
+
);
|
|
65
|
+
const parsed = opener(line);
|
|
66
|
+
if (!parsed) return false;
|
|
67
|
+
if (silent) return true;
|
|
68
|
+
let close = first + 1;
|
|
69
|
+
while (close < past) {
|
|
70
|
+
const text = state.src.slice(
|
|
71
|
+
state.bMarks[close] + state.tShift[close],
|
|
72
|
+
state.eMarks[close],
|
|
73
|
+
);
|
|
74
|
+
if (/^ *:::[ \t]*$/.test(text)) break;
|
|
75
|
+
close++;
|
|
76
|
+
}
|
|
77
|
+
const closed = close < past;
|
|
78
|
+
const original = state.env.verseSource ?? state.src;
|
|
79
|
+
const rawLines = original.split(/\r\n|\r|\n/);
|
|
80
|
+
const starts = [0];
|
|
81
|
+
for (const match of original.matchAll(/\r\n|\r|\n/g))
|
|
82
|
+
starts.push(match.index + match[0].length);
|
|
83
|
+
const from = starts[first] ?? original.length;
|
|
84
|
+
const bodyStart = starts[first + 1] ?? original.length;
|
|
85
|
+
let bodyEnd = starts[close] ?? original.length;
|
|
86
|
+
if (bodyEnd > bodyStart) {
|
|
87
|
+
if (original[bodyEnd - 1] === "\n") bodyEnd--;
|
|
88
|
+
if (original[bodyEnd - 1] === "\r") bodyEnd--;
|
|
89
|
+
}
|
|
90
|
+
const region = {
|
|
91
|
+
start: first,
|
|
92
|
+
end: closed ? close : past - 1,
|
|
93
|
+
from,
|
|
94
|
+
to: starts[closed ? close + 1 : past] ?? original.length,
|
|
95
|
+
bodyStart,
|
|
96
|
+
bodyEnd,
|
|
97
|
+
body: original.slice(bodyStart, bodyEnd),
|
|
98
|
+
attributes: parsed.values,
|
|
99
|
+
id: parsed.id,
|
|
100
|
+
classes: parsed.classes,
|
|
101
|
+
indent: /^ */.exec(rawLines[first] ?? "")[0],
|
|
102
|
+
name: "verse",
|
|
103
|
+
closed,
|
|
104
|
+
problems: parsed.problems,
|
|
105
|
+
};
|
|
106
|
+
const token = state.push("verse_div", "div", 0);
|
|
107
|
+
token.block = true;
|
|
108
|
+
token.map = [first, closed ? close + 1 : past];
|
|
109
|
+
token.content = region.body;
|
|
110
|
+
token.meta = { verseRegion: region };
|
|
111
|
+
state.line = closed ? close + 1 : past;
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
{ alt: ["paragraph", "reference", "blockquote", "list"] },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parser = new MarkdownIt({ html: true }).use(verseDivPlugin);
|
|
119
|
+
parser.core.ruler.disable(["inline", "linkify", "replacements", "smartquotes", "text_join"]);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Locate complete raw verse divs while leaving literal examples untouched.
|
|
123
|
+
* @param {string} source - Authored Markdown.
|
|
124
|
+
* @returns {VerseRegion[]} Regions in source order, including unclosed divs.
|
|
125
|
+
*/
|
|
126
|
+
export function verseRegions(source) {
|
|
127
|
+
return parser
|
|
128
|
+
.parse(source, { verseSource: source })
|
|
129
|
+
.filter((token) => token.type === "verse_div")
|
|
130
|
+
.map((token) => token.meta.verseRegion);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Source ranges belonging to inline syntax rather than visible verse text.
|
|
135
|
+
* @param {string} source - Inline Markdown.
|
|
136
|
+
* @returns {Array<[number,number]>} Protected UTF-16 ranges.
|
|
137
|
+
*/
|
|
138
|
+
export function inlineSyntaxRanges(source) {
|
|
139
|
+
const ranges = [];
|
|
140
|
+
for (const match of source.matchAll(new RegExp(WIKILINK.source, "g"))) {
|
|
141
|
+
const separator = match[0].indexOf("|");
|
|
142
|
+
const close = match[0].indexOf("]]");
|
|
143
|
+
ranges.push([match.index, match.index + (separator < 0 ? close + 2 : separator + 1)]);
|
|
144
|
+
ranges.push([match.index + close, match.index + match[0].length]);
|
|
145
|
+
}
|
|
146
|
+
for (let at = 0; at < source.length; at++) {
|
|
147
|
+
if (source[at] === "\\") {
|
|
148
|
+
at++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const char = source[at];
|
|
152
|
+
let past = -1;
|
|
153
|
+
if (char === "⟦") past = source.indexOf("⟧", at + 1) + 1;
|
|
154
|
+
else if (char === "`") {
|
|
155
|
+
const mark = /^`+/.exec(source.slice(at))[0];
|
|
156
|
+
const close = source.indexOf(mark, at + mark.length);
|
|
157
|
+
if (close >= 0) past = close + mark.length;
|
|
158
|
+
} else if (source.startsWith("<!--", at)) past = source.indexOf("-->", at + 4) + 3;
|
|
159
|
+
else if (char === "<") past = source.indexOf(">", at + 1) + 1;
|
|
160
|
+
else if (char === "{") past = source.indexOf("}", at + 1) + 1;
|
|
161
|
+
else if (char === "(" && source[at - 1] === "]") {
|
|
162
|
+
let depth = 1;
|
|
163
|
+
for (let next = at + 1; next < source.length; next++) {
|
|
164
|
+
if (source[next] === "\\") {
|
|
165
|
+
next++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (source[next] === "(") depth++;
|
|
169
|
+
if (source[next] === ")" && !--depth) {
|
|
170
|
+
past = next + 1;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (past > at) {
|
|
176
|
+
ranges.push([at, past]);
|
|
177
|
+
at = past - 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
181
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
/** The dependency-free lexical wikilink pattern. @module */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A wikilink, as authored.
|
|
18
|
+
*
|
|
19
|
+
* **Newlines are excluded deliberately.** A link is written on one line; an
|
|
20
|
+
* unclosed `[[` is a typo, and the alternative is letting it swallow arbitrary
|
|
21
|
+
* prose in search of a closer. Erring towards "not a link" leaves the author's
|
|
22
|
+
* text as written, which is the safe direction for a rewriter.
|
|
23
|
+
*
|
|
24
|
+
* **A leading `!` makes it an embed, not a link**, and the lookbehind is what
|
|
25
|
+
* keeps every reader from seeing one as the other. Stated here rather than in
|
|
26
|
+
* each reader for the reason the rest of this module exists: the checker, the
|
|
27
|
+
* resolver and the embed reader all match on this pattern, and a copy of the
|
|
28
|
+
* exclusion in each is a place for it to be forgotten. An embed's
|
|
29
|
+
* own grammar is {@link module:engine/content-embeds}.
|
|
30
|
+
*/
|
|
31
|
+
export const WIKILINK = /(?<!!)\[\[([^\]\n]+)\]\](?:\{(?:[^{}"\n]|"(?:\\.|[^"\\\n])*")*\})?/g;
|
|
@@ -42,22 +42,8 @@ import { parseAddress, isAddressTuple, splitAnchor } from "./address.mjs";
|
|
|
42
42
|
* @module
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*
|
|
48
|
-
* **Newlines are excluded deliberately.** A link is written on one line; an
|
|
49
|
-
* unclosed `[[` is a typo, and the alternative is letting it swallow arbitrary
|
|
50
|
-
* prose in search of a closer. Erring towards "not a link" leaves the author's
|
|
51
|
-
* text as written, which is the safe direction for a rewriter.
|
|
52
|
-
*
|
|
53
|
-
* **A leading `!` makes it an embed, not a link**, and the lookbehind is what
|
|
54
|
-
* keeps every reader from seeing one as the other. Stated here rather than in
|
|
55
|
-
* each reader for the reason the rest of this module exists: the checker, the
|
|
56
|
-
* resolver and the embed reader all match on this pattern, and a copy of the
|
|
57
|
-
* exclusion in each is a place for it to be forgotten. An embed's
|
|
58
|
-
* own grammar is {@link module:engine/content-embeds}.
|
|
59
|
-
*/
|
|
60
|
-
export const WIKILINK = /(?<!!)\[\[([^\]\n]+)\]\](?:\{(?:[^{}"\n]|"(?:\\.|[^"\\\n])*")*\})?/g;
|
|
45
|
+
import { WIKILINK } from "./wikilink-pattern.mjs";
|
|
46
|
+
export { WIKILINK } from "./wikilink-pattern.mjs";
|
|
61
47
|
|
|
62
48
|
/**
|
|
63
49
|
* The parts of a wikilink's interior.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.4.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -1384,6 +1384,30 @@ a.tag:hover {
|
|
|
1384
1384
|
margin: 0;
|
|
1385
1385
|
}
|
|
1386
1386
|
|
|
1387
|
+
/* A quotation stays within its prose paragraph; canto titles retain heading type. */
|
|
1388
|
+
.single-content .verse.verse-inline {
|
|
1389
|
+
display: inline-block;
|
|
1390
|
+
vertical-align: baseline;
|
|
1391
|
+
max-inline-size: 100%;
|
|
1392
|
+
margin: 0;
|
|
1393
|
+
container-type: normal;
|
|
1394
|
+
}
|
|
1395
|
+
.single-content .verse.verse-inline .verse-stanza {
|
|
1396
|
+
display: block;
|
|
1397
|
+
margin: 0;
|
|
1398
|
+
}
|
|
1399
|
+
.single-content .verse.verse-inline .verse-stanza + .verse-stanza {
|
|
1400
|
+
margin-top: 0.5em;
|
|
1401
|
+
}
|
|
1402
|
+
.single-content .verse .verse-canto-heading {
|
|
1403
|
+
display: block;
|
|
1404
|
+
margin: 0.5em 0;
|
|
1405
|
+
font-family: var(--font-display);
|
|
1406
|
+
font-size: 1.1em;
|
|
1407
|
+
font-weight: bold;
|
|
1408
|
+
line-height: 1.3;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1387
1411
|
/* Logical stanzas share labels across native and Roman presentations. */
|
|
1388
1412
|
.single-content .verse {
|
|
1389
1413
|
margin: 1.5rem 0;
|
|
@@ -17,13 +17,11 @@ export function parseHeaderArgs(info: string): {
|
|
|
17
17
|
* @param {object} [options]
|
|
18
18
|
* @param {boolean} [options.spans=true] - Include inline code spans. Set false
|
|
19
19
|
* to consider only block-level code.
|
|
20
|
-
* @param {boolean} [options.verseAsProse=true] - Treat verse bodies as inline prose.
|
|
21
20
|
* @returns {Array<{start: number, end: number}>} Non-overlapping regions, in
|
|
22
21
|
* source order. `end` is exclusive.
|
|
23
22
|
*/
|
|
24
|
-
export function codeRegions(markdown: string | null | undefined, { spans
|
|
23
|
+
export function codeRegions(markdown: string | null | undefined, { spans }?: {
|
|
25
24
|
spans?: boolean | undefined;
|
|
26
|
-
verseAsProse?: boolean | undefined;
|
|
27
25
|
}): Array<{
|
|
28
26
|
start: number;
|
|
29
27
|
end: number;
|
|
@@ -81,6 +81,29 @@ export function footnoteFindings(source: string): Array<{
|
|
|
81
81
|
column: number;
|
|
82
82
|
message: string;
|
|
83
83
|
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Labels defined at the top level of this note.
|
|
86
|
+
* @param {string} source - Complete authored note Markdown.
|
|
87
|
+
* @returns {Set<string>} Same-note footnote labels.
|
|
88
|
+
*/
|
|
89
|
+
export function footnoteLabels(source: string): Set<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Actual defined references, excluding literal code, escapes and metadata.
|
|
92
|
+
* @param {string} source - Full note or raw inline fragment.
|
|
93
|
+
* @param {object} [options] - Definition context and source grammar.
|
|
94
|
+
* @param {ReadonlySet<string>} [options.definitions] - Same-note labels; derived from source when omitted.
|
|
95
|
+
* @param {boolean} [options.inline] - Treat all source as inline verse/prose.
|
|
96
|
+
* @returns {Array<{from:number,to:number,key:string,text:string}>} Original UTF-16 source ranges.
|
|
97
|
+
*/
|
|
98
|
+
export function footnoteReferences(source: string, { definitions, inline }?: {
|
|
99
|
+
definitions?: ReadonlySet<string> | undefined;
|
|
100
|
+
inline?: boolean | undefined;
|
|
101
|
+
}): Array<{
|
|
102
|
+
from: number;
|
|
103
|
+
to: number;
|
|
104
|
+
key: string;
|
|
105
|
+
text: string;
|
|
106
|
+
}>;
|
|
84
107
|
/**
|
|
85
108
|
* A footnote reference's shape, wherever it is written — excluding a
|
|
86
109
|
* definition's own `[^id]:` opener, which this never matches.
|
|
@@ -18,7 +18,7 @@ export function runContext(config: {
|
|
|
18
18
|
};
|
|
19
19
|
} | undefined, records?: Iterable<Record<string, any>>): RunContext;
|
|
20
20
|
/**
|
|
21
|
-
* Find the spans, divs and verse
|
|
21
|
+
* Find the spans, divs and verse divs marked with `lang=`, `font=` or
|
|
22
22
|
* `enc=`, and every fault in their markup and their text.
|
|
23
23
|
*
|
|
24
24
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
@@ -80,7 +80,7 @@ export function scanRuns(source: string, ctx?: RunContext, syntax?: import("./co
|
|
|
80
80
|
* scanner projections.
|
|
81
81
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
82
82
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
83
|
-
* @returns {{markdown: string, errors: Array<{file?: string, line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>, verses: Map<string, {model: ReturnType<typeof import("./content-verse.mjs").parseVerse>, classes: string[], attributes: Record<string, string
|
|
83
|
+
* @returns {{markdown: string, errors: Array<{file?: string, line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>, verses: Map<string, {model: ReturnType<typeof import("./content-verse.mjs").parseVerse>, classes: string[], attributes: Record<string, string>, inline: boolean}>}}
|
|
84
84
|
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
85
85
|
* rewritten scroll fence's layout, by its rewritten body.
|
|
86
86
|
*/
|
|
@@ -99,6 +99,7 @@ export function renderRuns(source: string, ctx?: RunContext, syntax?: import("./
|
|
|
99
99
|
model: ReturnType<typeof import("./content-verse.mjs").parseVerse>;
|
|
100
100
|
classes: string[];
|
|
101
101
|
attributes: Record<string, string>;
|
|
102
|
+
inline: boolean;
|
|
102
103
|
}>;
|
|
103
104
|
};
|
|
104
105
|
/** The keys this module reads, which no construct passes through as written. */
|
|
@@ -350,7 +351,7 @@ export type RunContext = {
|
|
|
350
351
|
directions?: ReadonlyMap<string, "vertical-rl"> | undefined;
|
|
351
352
|
};
|
|
352
353
|
/**
|
|
353
|
-
* One span, div, verse
|
|
354
|
+
* One span, div, verse div or scroll fence carrying run attributes.
|
|
354
355
|
* Values combine explicit choices, class-only inheritance, encoding-document
|
|
355
356
|
* language/font defaults and the Roman default encoding.
|
|
356
357
|
*/
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @param {object} [options] - Surface presentation.
|
|
4
4
|
* @param {boolean} [options.source] - Emit opaque authored source tokens for buffer preview.
|
|
5
5
|
* @param {boolean} [options.spoilers] - Add focusable CSS spoiler wrappers (Foundry).
|
|
6
|
+
* @param {(token: any, state: any) => void} [options.verseInline] - Populate an opaque quoted-verse token during inline parsing.
|
|
6
7
|
*/
|
|
7
|
-
export function spanMarkdownPlugin(md: typeof import("markdown-it"), { spoilers, source }?: {
|
|
8
|
+
export function spanMarkdownPlugin(md: typeof import("markdown-it"), { spoilers, source, verseInline }?: {
|
|
8
9
|
source?: boolean | undefined;
|
|
9
10
|
spoilers?: boolean | undefined;
|
|
11
|
+
verseInline?: ((token: any, state: any) => void) | undefined;
|
|
10
12
|
}): void;
|
|
11
13
|
/** Read span anchors and located attribute diagnostics outside literal code.
|
|
12
14
|
* @param {string} source - Authored Markdown.
|
|
@@ -20,11 +20,11 @@ export function inlineVerse(text: any): string;
|
|
|
20
20
|
* @returns {boolean} Whether it starts right to left.
|
|
21
21
|
*/
|
|
22
22
|
export function rightToLeft(text: string): boolean;
|
|
23
|
-
/** Read verse
|
|
23
|
+
/** Read verse divs; start/end are inclusive zero-based source lines.
|
|
24
24
|
* @param {string} source - Authored Markdown.
|
|
25
25
|
* @param {object} [options] - Whole-page validation choices.
|
|
26
26
|
* @param {boolean} [options.checkWorks] - Whether to validate page-local versions.
|
|
27
|
-
* @returns {{blocks: VerseBlock[], errors: VerseDiagnostic[]}} Located
|
|
27
|
+
* @returns {{blocks: VerseBlock[], errors: VerseDiagnostic[]}} Located divs and findings.
|
|
28
28
|
*/
|
|
29
29
|
export function scanVerse(source: string, { checkWorks }?: {
|
|
30
30
|
checkWorks?: boolean | undefined;
|
|
@@ -32,68 +32,29 @@ export function scanVerse(source: string, { checkWorks }?: {
|
|
|
32
32
|
blocks: VerseBlock[];
|
|
33
33
|
errors: VerseDiagnostic[];
|
|
34
34
|
};
|
|
35
|
-
/**
|
|
36
|
-
* A logical line with its authored UTF-16 coordinates.
|
|
37
|
-
* @typedef {object} VerseLine
|
|
38
|
-
* @property {string} text - Inline Markdown in this presentation.
|
|
39
|
-
* @property {number} level - Leading-period indentation depth.
|
|
40
|
-
* @property {number} from - First authored offset, including indentation.
|
|
41
|
-
* @property {number} to - Offset at the line ending.
|
|
42
|
-
* @property {number[]} offsets - Source offset for each UTF-16 text unit.
|
|
43
|
-
* @property {string} [romanText] - Roman text paired with an encoded line.
|
|
44
|
-
*/
|
|
45
|
-
/**
|
|
46
|
-
* A stanza's logical lines and authored bounds.
|
|
47
|
-
* @typedef {object} VerseStanza
|
|
48
|
-
* @property {VerseLine[]} lines - Lines in reading order.
|
|
49
|
-
* @property {number} from - First authored offset.
|
|
50
|
-
* @property {number} to - Offset at the stanza ending.
|
|
51
|
-
*/
|
|
52
|
-
/**
|
|
53
|
-
* @typedef {VerseStanza & {canto: number, stanza: number, label: string, cantoBreak?: false}} VerseTextGroup
|
|
54
|
-
* @typedef {{cantoBreak: true, from: number, to: number, text?: string}} VerseCantoBreak
|
|
55
|
-
* @typedef {{kind: "line"|"stanza"|"canto", sourceOffset: number}} VerseBoundary
|
|
56
|
-
* @typedef {{offset: number, message: string}} VerseFinding
|
|
57
|
-
*/
|
|
58
|
-
/**
|
|
59
|
-
* Shared authored verse structure and source locations.
|
|
60
|
-
* @typedef {object} VerseModel
|
|
61
|
-
* @property {Array<{stanzas: VerseStanza[]}>} cantos - Cantos in reading order.
|
|
62
|
-
* @property {Array<VerseTextGroup|VerseCantoBreak>} groups - Stanzas and canto breaks.
|
|
63
|
-
* @property {VerseBoundary[]} boundaries - Explicit authored boundaries.
|
|
64
|
-
* @property {Array<[number, number]>} layout - Source ranges omitted from encoded letters.
|
|
65
|
-
* @property {VerseFinding[]} errors - Findings relative to the fence body.
|
|
66
|
-
*/
|
|
67
|
-
/**
|
|
68
|
-
* An authored verse fence with its parsed logical structure.
|
|
69
|
-
* @typedef {object} VerseBlock
|
|
70
|
-
* @property {number} start - Opening fence's zero-based source line.
|
|
71
|
-
* @property {number} end - Closing fence's zero-based source line.
|
|
72
|
-
* @property {string} fence - Authored fence delimiter.
|
|
73
|
-
* @property {"verse"} name - Fence language.
|
|
74
|
-
* @property {Record<string, string>} attributes - Authored attribute values.
|
|
75
|
-
* @property {string} id - Authored anchor, or the empty string.
|
|
76
|
-
* @property {string[]} classes - Authored presentation classes.
|
|
77
|
-
* @property {string} indent - Opening fence's leading spaces.
|
|
78
|
-
* @property {string} body - Authored fence body.
|
|
79
|
-
* @property {VerseModel} model - Logical structure relative to the body.
|
|
80
|
-
*/
|
|
81
|
-
/** @typedef {{line: number, column: number, message: string}} VerseDiagnostic */
|
|
82
|
-
/**
|
|
83
|
-
* An encoded verse's shared display model and native-cell presentation.
|
|
84
|
-
* @typedef {object} VersePresentation
|
|
85
|
-
* @property {VerseModel} model - Logical structure with paired line text.
|
|
86
|
-
* @property {string[]} classes - Native script and font classes.
|
|
87
|
-
* @property {Record<string, string>} attributes - Native language, direction and search attributes.
|
|
88
|
-
*/
|
|
89
35
|
/**
|
|
90
36
|
* Parse logical verse lines, stanzas and cantos without losing authored offsets.
|
|
91
37
|
* Offsets are UTF-16 positions relative to body. Delimiters and indentation are
|
|
92
38
|
* layout ranges; source newlines inside a logical line contribute one space.
|
|
93
|
-
* @param {string} body - Authored
|
|
39
|
+
* @param {string} body - Authored verse body.
|
|
40
|
+
* @param {{inline?: boolean, footnotes?: ReadonlySet<string>}} [options] - Inline quotations have no cantos; footnotes are same-note definition labels.
|
|
94
41
|
* @returns {VerseModel} The shared structure with offsets relative to the body.
|
|
95
42
|
*/
|
|
96
|
-
export function parseVerse(body: string
|
|
43
|
+
export function parseVerse(body: string, { inline, footnotes }?: {
|
|
44
|
+
inline?: boolean;
|
|
45
|
+
footnotes?: ReadonlySet<string>;
|
|
46
|
+
}): VerseModel;
|
|
47
|
+
/**
|
|
48
|
+
* Validate a quoted verse span using its original source coordinates.
|
|
49
|
+
* @param {{start: number, text: string, attrs: Array<[string,string]>}} span - Authored span.
|
|
50
|
+
* @param {string} source - Complete Markdown source.
|
|
51
|
+
* @returns {VerseDiagnostic[]} Located inline-only findings.
|
|
52
|
+
*/
|
|
53
|
+
export function inlineVerseFindings(span: {
|
|
54
|
+
start: number;
|
|
55
|
+
text: string;
|
|
56
|
+
attrs: Array<[string, string]>;
|
|
57
|
+
}, source: string): VerseDiagnostic[];
|
|
97
58
|
/** Logical line text for consumers that do not need source locations. */
|
|
98
59
|
export function verseLines(body: any): string[];
|
|
99
60
|
/** Paragraph groups with separate canto gaps. */
|
|
@@ -103,27 +64,29 @@ export function verseStanzas(body: any): {
|
|
|
103
64
|
}[][];
|
|
104
65
|
/**
|
|
105
66
|
* Compare the stanza counts of each version of a page-local work.
|
|
106
|
-
* @param {VerseBlock[]} blocks -
|
|
107
|
-
* @returns {VerseDiagnostic[]} Findings at affected opening
|
|
67
|
+
* @param {VerseBlock[]} blocks - Divs belonging to one page.
|
|
68
|
+
* @returns {VerseDiagnostic[]} Findings at affected opening divs.
|
|
108
69
|
*/
|
|
109
70
|
export function workShapeFindings(blocks: VerseBlock[]): VerseDiagnostic[];
|
|
110
71
|
/**
|
|
111
72
|
* Render a shared verse structure while preserving surrounding inline Markdown.
|
|
112
|
-
* @param {string} body - Authored
|
|
73
|
+
* @param {string} body - Authored verse body.
|
|
113
74
|
* @param {object} [options] - Shared structure and display choices.
|
|
114
75
|
* @param {boolean} [options.labelled] - Whether each stanza carries its label.
|
|
115
76
|
* @param {VerseModel} [options.model] - Parsed structure; defaults to parsing the body.
|
|
116
77
|
* @param {VersePresentation} [options.presentation] - Native-cell font and language.
|
|
78
|
+
* @param {boolean} [options.inline] - Whether wrappers must remain inline-safe.
|
|
117
79
|
* @returns {string} Markdown with verse layout wrappers.
|
|
118
80
|
*/
|
|
119
|
-
export function verseMarkdown(body: string, { labelled, model, presentation }?: {
|
|
81
|
+
export function verseMarkdown(body: string, { labelled, model, presentation, inline }?: {
|
|
120
82
|
labelled?: boolean | undefined;
|
|
121
83
|
model?: VerseModel | undefined;
|
|
122
84
|
presentation?: VersePresentation | undefined;
|
|
85
|
+
inline?: boolean | undefined;
|
|
123
86
|
}): string;
|
|
124
87
|
/**
|
|
125
|
-
* Replace verse
|
|
126
|
-
* @param {string} source - Markdown containing verse
|
|
88
|
+
* Replace verse divs with wrappers whose bodies remain inline Markdown.
|
|
89
|
+
* @param {string} source - Markdown containing verse divs.
|
|
127
90
|
* @param {object} [options] - Encoded display models.
|
|
128
91
|
* @param {Map<string, VersePresentation>} [options.verses] - Presentations keyed by rewritten body.
|
|
129
92
|
*/
|
|
@@ -133,7 +96,13 @@ export function renderVerse(source: string, { verses }?: {
|
|
|
133
96
|
markdown: string;
|
|
134
97
|
errors: VerseDiagnostic[];
|
|
135
98
|
};
|
|
136
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Render quoted verse with inline-safe wrappers and shared logical structure.
|
|
101
|
+
* @param {string} source - Markdown after block verse rendering.
|
|
102
|
+
* @returns {string} Markdown retaining surrounding prose.
|
|
103
|
+
*/
|
|
104
|
+
export function renderVerseQuotes(source: string): string;
|
|
105
|
+
/** The authored attributes of a verse div. */
|
|
137
106
|
export const VERSE_KEYS: readonly string[];
|
|
138
107
|
/** A canto separator, including the escaped form produced by a native run. */
|
|
139
108
|
export const STANZA_BREAK: RegExp;
|
|
@@ -195,6 +164,8 @@ export type VerseCantoBreak = {
|
|
|
195
164
|
from: number;
|
|
196
165
|
to: number;
|
|
197
166
|
text?: string;
|
|
167
|
+
id?: string;
|
|
168
|
+
title?: string;
|
|
198
169
|
};
|
|
199
170
|
export type VerseBoundary = {
|
|
200
171
|
kind: "line" | "stanza" | "canto";
|
|
@@ -227,28 +198,52 @@ export type VerseModel = {
|
|
|
227
198
|
*/
|
|
228
199
|
layout: Array<[number, number]>;
|
|
229
200
|
/**
|
|
230
|
-
* - Findings relative to the
|
|
201
|
+
* - Findings relative to the verse body.
|
|
231
202
|
*/
|
|
232
203
|
errors: VerseFinding[];
|
|
204
|
+
/**
|
|
205
|
+
* - Defined references without substantive verse text.
|
|
206
|
+
*/
|
|
207
|
+
notes: VerseLine[];
|
|
233
208
|
};
|
|
234
209
|
/**
|
|
235
|
-
* An authored verse
|
|
210
|
+
* An authored verse div with its parsed logical structure.
|
|
236
211
|
*/
|
|
237
212
|
export type VerseBlock = {
|
|
238
213
|
/**
|
|
239
|
-
* - Opening
|
|
214
|
+
* - Opening div's zero-based source line.
|
|
240
215
|
*/
|
|
241
216
|
start: number;
|
|
242
217
|
/**
|
|
243
|
-
* - Closing
|
|
218
|
+
* - Closing div's zero-based source line.
|
|
244
219
|
*/
|
|
245
220
|
end: number;
|
|
246
221
|
/**
|
|
247
|
-
* -
|
|
222
|
+
* - Complete div first offset.
|
|
223
|
+
*/
|
|
224
|
+
from: number;
|
|
225
|
+
/**
|
|
226
|
+
* - Offset past complete div.
|
|
227
|
+
*/
|
|
228
|
+
to: number;
|
|
229
|
+
/**
|
|
230
|
+
* - First body offset.
|
|
231
|
+
*/
|
|
232
|
+
bodyStart: number;
|
|
233
|
+
/**
|
|
234
|
+
* - Offset past body text.
|
|
235
|
+
*/
|
|
236
|
+
bodyEnd: number;
|
|
237
|
+
/**
|
|
238
|
+
* - Whether the closing delimiter exists.
|
|
239
|
+
*/
|
|
240
|
+
closed: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* - Attribute syntax findings.
|
|
248
243
|
*/
|
|
249
|
-
|
|
244
|
+
problems: string[];
|
|
250
245
|
/**
|
|
251
|
-
* -
|
|
246
|
+
* - Region name.
|
|
252
247
|
*/
|
|
253
248
|
name: "verse";
|
|
254
249
|
/**
|
|
@@ -264,11 +259,11 @@ export type VerseBlock = {
|
|
|
264
259
|
*/
|
|
265
260
|
classes: string[];
|
|
266
261
|
/**
|
|
267
|
-
* - Opening
|
|
262
|
+
* - Opening div's leading spaces.
|
|
268
263
|
*/
|
|
269
264
|
indent: string;
|
|
270
265
|
/**
|
|
271
|
-
* - Authored
|
|
266
|
+
* - Authored verse body.
|
|
272
267
|
*/
|
|
273
268
|
body: string;
|
|
274
269
|
/**
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* @property {string} visible - Visible text from the actual parsed children.
|
|
27
27
|
* @property {ReadonlyArray<readonly [string, string]>} attrs - Retained permitted HTML attributes.
|
|
28
28
|
* @property {Record<string, SpanAttributePosition>} attributePositions - Authored key/value sites by retained attribute name.
|
|
29
|
+
* @property {ReturnType<typeof import("./content-verse.mjs").parseVerse>} [verseModel] - Logical model of an inline verse quotation.
|
|
29
30
|
*/
|
|
30
31
|
/**
|
|
31
32
|
* One located span-grammar or refused-attribute finding.
|
|
@@ -139,6 +140,10 @@ export type ContentSpan = {
|
|
|
139
140
|
* - Authored key/value sites by retained attribute name.
|
|
140
141
|
*/
|
|
141
142
|
attributePositions: Record<string, SpanAttributePosition>;
|
|
143
|
+
/**
|
|
144
|
+
* - Logical model of an inline verse quotation.
|
|
145
|
+
*/
|
|
146
|
+
verseModel?: import("./content-verse.mjs").VerseModel | undefined;
|
|
142
147
|
};
|
|
143
148
|
/**
|
|
144
149
|
* One located span-grammar or refused-attribute finding.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace verse markup with opaque placeholders and restore its original bytes.
|
|
3
|
+
* @param {string} source - Complete Markdown source.
|
|
4
|
+
* @returns {{markdown: string, restore: (formatted: string) => string}} Protected source and exact restoration.
|
|
5
|
+
*/
|
|
6
|
+
export function protectVerseSource(source: string): {
|
|
7
|
+
markdown: string;
|
|
8
|
+
restore: (formatted: string) => string;
|
|
9
|
+
};
|
|
@@ -60,7 +60,7 @@ export function lineBeats(text: string, book: MetreBook): {
|
|
|
60
60
|
* The warning for a line that cannot have its metre's beats.
|
|
61
61
|
*
|
|
62
62
|
* @param {{min: number, max: number}} beats - The line's beats.
|
|
63
|
-
* @param {import("./metre-declaration.mjs").Metre} metre - The
|
|
63
|
+
* @param {import("./metre-declaration.mjs").Metre} metre - The div's metre.
|
|
64
64
|
* @returns {string} The message.
|
|
65
65
|
*/
|
|
66
66
|
export function metreMessage({ min, max }: {
|
|
@@ -68,13 +68,13 @@ export function metreMessage({ min, max }: {
|
|
|
68
68
|
max: number;
|
|
69
69
|
}, metre: import("./metre-declaration.mjs").Metre): string;
|
|
70
70
|
/**
|
|
71
|
-
* Check the verse
|
|
71
|
+
* Check the verse divs that state `scan=`.
|
|
72
72
|
*
|
|
73
73
|
* @param {ReadonlyArray<{start: number, end: number, indent: string, attributes: Record<string, string>, classes: string[]}>} blocks -
|
|
74
|
-
* The
|
|
74
|
+
* The divs, from {@link module:engine/content-verse.scanVerse}.
|
|
75
75
|
* @param {string[]} lines - The source's lines.
|
|
76
76
|
* @param {{languages?: Readonly<Record<string, {tag: string}>>, metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>, attributeColumns: (line: string, key: string) => {key: number, value: number}}} ctx -
|
|
77
|
-
* The metres and books, and how the
|
|
77
|
+
* The metres and books, and how the div says its columns and where its
|
|
78
78
|
* keys sit.
|
|
79
79
|
* @returns {{errors: object[], warnings: object[]}} The findings at 1-based
|
|
80
80
|
* positions.
|