@heroiclands/package-build 25.2.1 → 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 +26 -0
- package/README.md +1 -1
- package/assets/styles/content-base.css +97 -116
- package/docs/api.md +30 -6
- package/docs/authoring/links-and-markup.md +214 -126
- package/docs/commands.md +2 -2
- package/docs/content-format.md +31 -6
- package/docs/diagnostics.md +27 -27
- package/docs/guides/writing-in-invented-scripts.md +89 -83
- package/docs/reference/format-details.md +6 -7
- package/docs/reference/note-types.md +15 -15
- package/docs/reference/theme-pages.md +38 -57
- package/engine/anchors.mjs +14 -0
- package/engine/code-fences.mjs +15 -6
- package/engine/content-blocks.mjs +7 -4
- 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 +66 -48
- package/engine/content-scripts.mjs +357 -101
- package/engine/content-spans.mjs +76 -10
- package/engine/content-verse.mjs +687 -358
- 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 +89 -130
- package/engine/prose-lint.mjs +14 -2
- package/engine/roman-verse.mjs +68 -57
- 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 +49 -47
- 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 +68 -96
- package/types/engine/code-fences.d.mts +1 -3
- package/types/engine/content-blocks.d.mts +3 -1
- package/types/engine/content-footnotes.d.mts +23 -0
- package/types/engine/content-scripts.d.mts +12 -7
- package/types/engine/content-spans.d.mts +3 -1
- package/types/engine/content-verse.d.mts +263 -82
- package/types/engine/name-markup.d.mts +5 -0
- package/types/engine/roman-verse.d.mts +7 -6
- 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
|
@@ -18,6 +18,7 @@ import MarkdownIt from "markdown-it";
|
|
|
18
18
|
import footnotePlugin from "markdown-it-footnote";
|
|
19
19
|
import deflistPlugin from "markdown-it-deflist";
|
|
20
20
|
|
|
21
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
21
22
|
import { slugify } from "./content-slug.mjs";
|
|
22
23
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
23
24
|
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
@@ -184,9 +185,20 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
184
185
|
errors = [],
|
|
185
186
|
ids = new Set();
|
|
186
187
|
const counts = {};
|
|
188
|
+
const verse = verseRegions(String(source ?? ""));
|
|
189
|
+
const verseStarts = new Map(verse.map((region) => [region.start, region]));
|
|
190
|
+
const verseLines = new Set(
|
|
191
|
+
verse.flatMap((region) =>
|
|
192
|
+
Array.from(
|
|
193
|
+
{ length: region.end - region.start + 1 },
|
|
194
|
+
(_, index) => region.start + index,
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
187
198
|
let codeFence = null;
|
|
188
199
|
const fault = (line, message) => errors.push({ line: line + 1, column: 1, message });
|
|
189
200
|
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
if (verseLines.has(i)) continue;
|
|
190
202
|
const fence = FENCE.exec(lines[i]);
|
|
191
203
|
if (codeFence) {
|
|
192
204
|
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
@@ -233,7 +245,11 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
233
245
|
let bodyEnd;
|
|
234
246
|
const firstLine = lines[bodyStart];
|
|
235
247
|
let kind;
|
|
236
|
-
if (
|
|
248
|
+
if (verseStarts.has(bodyStart)) {
|
|
249
|
+
const region = verseStarts.get(bodyStart);
|
|
250
|
+
bodyEnd = region.end + 1;
|
|
251
|
+
kind = "poem";
|
|
252
|
+
} else if (/^ {0,3}:::(?:[ \t]+\{.*\})?[ \t]*$/.test(firstLine)) {
|
|
237
253
|
let depth = 1,
|
|
238
254
|
literal = null;
|
|
239
255
|
bodyEnd = bodyStart + 1;
|
|
@@ -305,10 +321,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
305
321
|
fault(bodyStart, "this block cannot be captioned");
|
|
306
322
|
continue;
|
|
307
323
|
}
|
|
308
|
-
kind =
|
|
309
|
-
first.type === "fence" && /^verse(?:\s|$)/.test(first.info) ?
|
|
310
|
-
"poem"
|
|
311
|
-
: figureKind(first, contents.trim(), resolveRole);
|
|
324
|
+
kind = figureKind(first, contents.trim(), resolveRole);
|
|
312
325
|
}
|
|
313
326
|
if (explicit) kind = explicit === "verse" ? "poem" : explicit;
|
|
314
327
|
const id = attributes.id;
|
|
@@ -338,6 +351,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
|
|
|
338
351
|
}
|
|
339
352
|
let literal = null;
|
|
340
353
|
for (let at = 0; at < lines.length; at++) {
|
|
354
|
+
if (verseLines.has(at)) continue;
|
|
341
355
|
const fence = FENCE.exec(lines[at]);
|
|
342
356
|
if (literal) {
|
|
343
357
|
if (fence && fence[1][0] === literal[0] && fence[1].length >= literal.length)
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { inlineSyntaxRanges, verseRegions } from "./verse-regions.mjs";
|
|
33
|
+
import { codeRegions, matchAllOutsideCode } from "./code-fences.mjs";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Where a character offset in `source` falls, as a 1-based line and column.
|
|
@@ -52,9 +53,15 @@ function lineColumn(source, offset) {
|
|
|
52
53
|
export function separateFootnotes(source) {
|
|
53
54
|
const lines = String(source ?? "").split("\n");
|
|
54
55
|
const body = [...lines];
|
|
56
|
+
const verseLines = new Set(
|
|
57
|
+
verseRegions(String(source ?? "")).flatMap((region) =>
|
|
58
|
+
Array.from({ length: region.end - region.start + 1 }, (_, i) => region.start + i),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
55
61
|
const definitions = [];
|
|
56
62
|
let fence = null;
|
|
57
63
|
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
if (verseLines.has(i)) continue;
|
|
58
65
|
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(lines[i]);
|
|
59
66
|
if (fence) {
|
|
60
67
|
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length)
|
|
@@ -240,3 +247,55 @@ export function footnoteFindings(source) {
|
|
|
240
247
|
(a, b) => a.line - b.line || a.column - b.column,
|
|
241
248
|
);
|
|
242
249
|
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Labels defined at the top level of this note.
|
|
253
|
+
* @param {string} source - Complete authored note Markdown.
|
|
254
|
+
* @returns {Set<string>} Same-note footnote labels.
|
|
255
|
+
*/
|
|
256
|
+
export function footnoteLabels(source) {
|
|
257
|
+
if (!source.includes("[^")) return new Set();
|
|
258
|
+
return new Set(
|
|
259
|
+
[...separateFootnotes(source).definitions.matchAll(/^\[\^([^\]\s]+)\]:/gm)].map(
|
|
260
|
+
(match) => match[1],
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Actual defined references, excluding literal code, escapes and metadata.
|
|
267
|
+
* @param {string} source - Full note or raw inline fragment.
|
|
268
|
+
* @param {object} [options] - Definition context and source grammar.
|
|
269
|
+
* @param {ReadonlySet<string>} [options.definitions] - Same-note labels; derived from source when omitted.
|
|
270
|
+
* @param {boolean} [options.inline] - Treat all source as inline verse/prose.
|
|
271
|
+
* @returns {Array<{from:number,to:number,key:string,text:string}>} Original UTF-16 source ranges.
|
|
272
|
+
*/
|
|
273
|
+
export function footnoteReferences(
|
|
274
|
+
source,
|
|
275
|
+
{ definitions = footnoteLabels(source), inline = false } = {},
|
|
276
|
+
) {
|
|
277
|
+
if (!definitions.size || !source.includes("[^")) return [];
|
|
278
|
+
const protectedRanges = inlineSyntaxRanges(source).filter(
|
|
279
|
+
([from]) => source[from] !== "{" || source[from - 1] === "]",
|
|
280
|
+
);
|
|
281
|
+
const code = inline ? [] : codeRegions(source);
|
|
282
|
+
return [...source.matchAll(/\[\^([^\]\s]+)\]/g)].flatMap((match) => {
|
|
283
|
+
const from = match.index,
|
|
284
|
+
to = from + match[0].length;
|
|
285
|
+
const escapes = /\\+$/.exec(source.slice(0, from))?.[0].length ?? 0;
|
|
286
|
+
if (
|
|
287
|
+
!definitions.has(match[1]) ||
|
|
288
|
+
escapes % 2 ||
|
|
289
|
+
protectedRanges.some(([first, past]) => from >= first && from < past) ||
|
|
290
|
+
code.some((range) => from >= range.start && from < range.end)
|
|
291
|
+
)
|
|
292
|
+
return [];
|
|
293
|
+
if (
|
|
294
|
+
!inline &&
|
|
295
|
+
source[to] === ":" &&
|
|
296
|
+
/^[ \t]*$/.test(source.slice(source.lastIndexOf("\n", from - 1) + 1, from))
|
|
297
|
+
)
|
|
298
|
+
return [];
|
|
299
|
+
return [{ from, to, key: match[1], text: match[0] }];
|
|
300
|
+
});
|
|
301
|
+
}
|
package/engine/content-index.mjs
CHANGED
|
@@ -1568,7 +1568,7 @@ function dynamicPublicationBody(body) {
|
|
|
1568
1568
|
const source = String(body ?? "");
|
|
1569
1569
|
return (
|
|
1570
1570
|
[...matchAllOutsideCode(source, EXPRESSION)].length > 0 ||
|
|
1571
|
-
codeRegions(source, { spans: false
|
|
1571
|
+
codeRegions(source, { spans: false }).some(({ start, end }) => {
|
|
1572
1572
|
const opener = FENCE_LINE.exec(source.slice(start, end).split("\n", 1)[0]);
|
|
1573
1573
|
const language = opener && parseHeaderArgs(opener[3]).language;
|
|
1574
1574
|
return language === "sql" || language === PAGE_LIST_LANGUAGE;
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* 2. **Captioned items** — each figure's wrapper and label. Its number, kind
|
|
48
48
|
* and role come from the figure scan expansion made, so a figure is
|
|
49
49
|
* numbered once; see {@link module:engine/content-figures.renderFigureBlocks}.
|
|
50
|
-
* 3. **Named blocks, verse and scrolls** — `:::` blocks, verse
|
|
50
|
+
* 3. **Named blocks, verse and scrolls** — `:::` blocks, verse divs and
|
|
51
51
|
* `scroll` fences.
|
|
52
52
|
* 4. **Alerts.**
|
|
53
53
|
* 5. **Spans and withheld sections** — written here on the `web` target; the
|
|
@@ -79,7 +79,8 @@ import { imagePlugin } from "./content-images.mjs";
|
|
|
79
79
|
import { renderRuns } from "./content-scripts.mjs";
|
|
80
80
|
import { renderSpans, spanMarkdownPlugin } from "./content-spans.mjs";
|
|
81
81
|
import { cartoucheFigures, renderSymbolSpans } from "./content-symbols.mjs";
|
|
82
|
-
import {
|
|
82
|
+
import { inlineVerse, parseVerse, renderVerseQuotes } from "./content-verse.mjs";
|
|
83
|
+
import { verseDivPlugin } from "./verse-regions.mjs";
|
|
83
84
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
84
85
|
import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
|
|
85
86
|
import { headingAttributesPlugin, headingIdsPlugin } from "./heading-attributes.mjs";
|
|
@@ -115,50 +116,59 @@ export function highlightCode(code, language) {
|
|
|
115
116
|
return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
/** Parse logical verse lines in the note's shared inline environment. */
|
|
120
|
+
function parseVerseContent(token, md, env, region) {
|
|
121
|
+
const body = token.type === "verse_inline" ? token.meta.text : token.content;
|
|
122
|
+
const inline = token.type === "verse_inline";
|
|
123
|
+
const paired = inline ? undefined : env.verses?.get(body);
|
|
124
|
+
const footnotes = new Set(Object.keys(env.footnotes?.refs ?? {}).map((key) => key.slice(1)));
|
|
125
|
+
const model = paired?.model ?? region.model ?? parseVerse(body, { inline, footnotes });
|
|
126
|
+
token.meta ??= {};
|
|
127
|
+
token.meta.verse = region;
|
|
128
|
+
token.meta.versePaired = paired;
|
|
129
|
+
const parse = (text) => {
|
|
130
|
+
const children = [];
|
|
131
|
+
md.inline.parse(inlineVerse(text), md, env, children);
|
|
132
|
+
return children;
|
|
133
|
+
};
|
|
134
|
+
token.meta.verseNotes = (model.notes ?? []).map((line) => ({
|
|
135
|
+
children: parse(line.romanText ?? line.text),
|
|
136
|
+
}));
|
|
137
|
+
token.meta.verseGroups = model.groups.map((group) =>
|
|
138
|
+
group.cantoBreak ?
|
|
139
|
+
{
|
|
140
|
+
...group,
|
|
141
|
+
children: group.text ? parse(group.text) : [],
|
|
142
|
+
titleChildren: group.title ? parse(group.title) : [],
|
|
143
|
+
}
|
|
144
|
+
: {
|
|
145
|
+
...group,
|
|
146
|
+
lines: group.lines.map((line) => ({
|
|
147
|
+
...line,
|
|
148
|
+
children: parse(line.text),
|
|
149
|
+
...(paired ? { romanChildren: parse(line.romanText ?? line.text) } : {}),
|
|
150
|
+
})),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** An opaque quoted verse is parsed once, before footnote collection. */
|
|
156
|
+
function quotedVerse(token, state) {
|
|
157
|
+
const classes = (token.attrGet("class") ?? "")
|
|
158
|
+
.split(/\s+/)
|
|
159
|
+
.filter((name) => name && name !== "verse");
|
|
160
|
+
const attributes = Object.fromEntries(
|
|
161
|
+
(token.attrs ?? []).filter(([key]) => !["id", "class"].includes(key)),
|
|
162
|
+
);
|
|
163
|
+
parseVerseContent(token, state.md, state.env, { id: token.attrGet("id"), classes, attributes });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A verse div's raw body receives inline parsing without block interpretation. */
|
|
128
167
|
function verseInlinePlugin(md) {
|
|
129
168
|
md.core.ruler.after("inline", "verse_inline", (state) => {
|
|
130
169
|
for (const token of state.tokens) {
|
|
131
|
-
if (token.type !== "
|
|
132
|
-
|
|
133
|
-
token.meta ??= {};
|
|
134
|
-
const source = `${token.markup}${token.info}\n${token.content}${token.markup}`;
|
|
135
|
-
const poem = scanVerse(source).blocks[0];
|
|
136
|
-
if (hasColumns(poem)) {
|
|
137
|
-
token.meta.verseGroups = verseGroups(poem, source.split("\n")).map((group) => {
|
|
138
|
-
if (group.stanzaBreak) return group;
|
|
139
|
-
return {
|
|
140
|
-
...group,
|
|
141
|
-
parts: group.parts.map((part) => {
|
|
142
|
-
const children = [];
|
|
143
|
-
state.md.inline.parse(
|
|
144
|
-
inlineVerse(part.text),
|
|
145
|
-
state.md,
|
|
146
|
-
state.env,
|
|
147
|
-
children,
|
|
148
|
-
);
|
|
149
|
-
return { ...part, children };
|
|
150
|
-
}),
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
token.meta.verseStanzas = verseStanzas(token.content).map((stanza) =>
|
|
156
|
-
stanza.map(({ text, level }) => {
|
|
157
|
-
const children = [];
|
|
158
|
-
state.md.inline.parse(inlineVerse(text), state.md, state.env, children);
|
|
159
|
-
return { children, level, stanzaBreak: /^\*{3}$/.test(text) };
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
170
|
+
if (token.type !== "verse_div") continue;
|
|
171
|
+
parseVerseContent(token, state.md, state.env, token.meta.verseRegion);
|
|
162
172
|
}
|
|
163
173
|
});
|
|
164
174
|
}
|
|
@@ -386,7 +396,8 @@ export function createRenderer(target, options = {}) {
|
|
|
386
396
|
use("footnotes", footnotePlugin);
|
|
387
397
|
if (book) {
|
|
388
398
|
use("alerts", alertMarkdownPlugin);
|
|
389
|
-
use("spans", spanMarkdownPlugin);
|
|
399
|
+
use("spans", spanMarkdownPlugin, { verseInline: quotedVerse });
|
|
400
|
+
use("verse-div", verseDivPlugin);
|
|
390
401
|
use("verse-inline", verseInlinePlugin);
|
|
391
402
|
use("script-emphasis", scriptEmphasisPlugin);
|
|
392
403
|
use("definition-lists", deflistPlugin);
|
|
@@ -433,8 +444,12 @@ export function createRenderer(target, options = {}) {
|
|
|
433
444
|
const prepare = (markdown, env = {}) => {
|
|
434
445
|
let text = String(markdown ?? "");
|
|
435
446
|
if (preview) text = sourceMalformedAlerts(text);
|
|
436
|
-
|
|
437
|
-
|
|
447
|
+
let verses = env.verses;
|
|
448
|
+
if (env.runs) {
|
|
449
|
+
const marked = renderRuns(text, env.runs, undefined, { target: RUN_TARGET[target] });
|
|
450
|
+
text = marked.markdown;
|
|
451
|
+
verses = marked.verses;
|
|
452
|
+
}
|
|
438
453
|
// A glyph and a cartouche are pictures on every target that draws
|
|
439
454
|
// HTML, so they are written before any stage renders a fragment. The
|
|
440
455
|
// buffer-only preview shows a glyph as the source it was written in
|
|
@@ -448,12 +463,13 @@ export function createRenderer(target, options = {}) {
|
|
|
448
463
|
{ resolveRole: env.resolveRole, renderInline },
|
|
449
464
|
);
|
|
450
465
|
text = renderAlerts(
|
|
451
|
-
renderBlocks(figured.markdown, target, { renderInline }).markdown,
|
|
466
|
+
renderBlocks(figured.markdown, target, { renderInline, verses }).markdown,
|
|
452
467
|
target,
|
|
453
468
|
{
|
|
454
469
|
renderInline,
|
|
455
470
|
},
|
|
456
471
|
).markdown;
|
|
472
|
+
text = renderVerseQuotes(text);
|
|
457
473
|
if (target === "web") text = renderSpans(text).markdown;
|
|
458
474
|
return target === "web" ? renderWithheldSections(text) : text;
|
|
459
475
|
};
|
|
@@ -474,6 +490,8 @@ export function createRenderer(target, options = {}) {
|
|
|
474
490
|
)
|
|
475
491
|
),
|
|
476
492
|
renderInline:
|
|
477
|
-
book ? unsupported : (
|
|
493
|
+
book ? unsupported : (
|
|
494
|
+
(markdown) => restore(md.renderInline(renderVerseQuotes(String(markdown ?? ""))))
|
|
495
|
+
),
|
|
478
496
|
};
|
|
479
497
|
}
|