@heroiclands/package-build 25.2.0 → 25.3.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 +18 -0
- package/assets/styles/content-base.css +65 -116
- package/docs/authoring/links-and-markup.md +114 -109
- package/docs/content-format.md +20 -3
- package/docs/diagnostics.md +25 -26
- package/docs/guides/writing-in-invented-scripts.md +41 -62
- package/docs/reference/format-details.md +0 -1
- package/docs/reference/theme-pages.md +30 -57
- package/docs/source-preview.md +2 -1
- package/engine/content-blocks.mjs +3 -2
- package/engine/content-render.mjs +31 -29
- package/engine/content-scripts.mjs +180 -86
- package/engine/content-spans.mjs +4 -6
- package/engine/content-verse.mjs +482 -281
- package/engine/journals.mjs +14 -14
- package/engine/pdf-render.mjs +65 -117
- package/engine/preview-source.mjs +15 -8
- package/engine/roman-verse.mjs +68 -57
- package/engine/verse-metre.mjs +33 -40
- package/package.json +1 -1
- package/site-theme/static/css/style.css +49 -101
- package/types/engine/content-blocks.d.mts +3 -1
- package/types/engine/content-scripts.d.mts +9 -5
- package/types/engine/content-verse.d.mts +269 -83
- package/types/engine/journals.d.mts +3 -3
- package/types/engine/roman-verse.d.mts +7 -6
package/engine/journals.mjs
CHANGED
|
@@ -116,9 +116,9 @@ function namedBlockLines(markdown, syntax) {
|
|
|
116
116
|
* code blocks are respected so `# foo` inside ``` or ~~~ blocks doesn't
|
|
117
117
|
* trigger a split. Content before the first H1 (if non-empty) becomes a
|
|
118
118
|
* leading page. Each H1 yields a page whose name is the heading
|
|
119
|
-
* text (with any `{#anchor-id}` suffix stripped out and
|
|
120
|
-
* `anchorSlug`). The classes the heading declares come along as
|
|
121
|
-
* a surface that honours one has it without reading the suffix again.
|
|
119
|
+
* text without inline markup (with any `{#anchor-id}` suffix stripped out and
|
|
120
|
+
* surfaced as `anchorSlug`). The classes the heading declares come along as
|
|
121
|
+
* `classes`, so a surface that honours one has it without reading the suffix again.
|
|
122
122
|
*
|
|
123
123
|
* `leadName` names that leading page. A journal note's is "Introduction",
|
|
124
124
|
* because it introduces the pages that follow. An item doc's is the item — a
|
|
@@ -220,7 +220,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole, syntax)
|
|
|
220
220
|
anchorSlug: null,
|
|
221
221
|
level: section.level,
|
|
222
222
|
classes: section.classes,
|
|
223
|
-
continues: item.slug || item.label ||
|
|
223
|
+
continues: item.slug || item.label || pageNameText(item.caption),
|
|
224
224
|
lines: [],
|
|
225
225
|
};
|
|
226
226
|
}
|
|
@@ -231,7 +231,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole, syntax)
|
|
|
231
231
|
if (figure) {
|
|
232
232
|
closeCurrent();
|
|
233
233
|
current = {
|
|
234
|
-
name: figure.label ||
|
|
234
|
+
name: figure.label || pageNameText(figure.caption),
|
|
235
235
|
anchorSlug: figure.id || null,
|
|
236
236
|
level: 1,
|
|
237
237
|
classes: section.classes.includes(WITHHELD_CLASS) ? [WITHHELD_CLASS] : [],
|
|
@@ -243,7 +243,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole, syntax)
|
|
|
243
243
|
if (opening) {
|
|
244
244
|
closeCurrent();
|
|
245
245
|
current = {
|
|
246
|
-
name: opening.text,
|
|
246
|
+
name: pageNameText(opening.text),
|
|
247
247
|
anchorSlug: opening.id || null,
|
|
248
248
|
level: opening.level,
|
|
249
249
|
classes: opening.classes,
|
|
@@ -384,20 +384,20 @@ export function journalPageId(entryId, page) {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
* The name of
|
|
387
|
+
* The name of a page a heading or caption begins: its visible text.
|
|
388
388
|
*
|
|
389
|
-
* Foundry shows a page name as plain text, so
|
|
389
|
+
* Foundry shows a page name as plain text, so inline markup — emphasis, a
|
|
390
390
|
* code span, a Markdown link, or the `@UUID[...]{label}` a resolved wikilink
|
|
391
391
|
* has already become, inside its wrapping `<span>` — reduces to the text a
|
|
392
392
|
* reader sees, the same text a reference to the caption displays. The markup
|
|
393
393
|
* itself stays in the page's content, where it renders.
|
|
394
394
|
*
|
|
395
|
-
* @param {string}
|
|
395
|
+
* @param {string} markdown - A heading or caption, as the journal pass sees it.
|
|
396
396
|
* @returns {string} Its visible text, whitespace collapsed.
|
|
397
397
|
*/
|
|
398
|
-
function
|
|
399
|
-
if (!
|
|
400
|
-
const [inline] = md.parseInline(
|
|
398
|
+
function pageNameText(markdown) {
|
|
399
|
+
if (!markdown) return "";
|
|
400
|
+
const [inline] = md.parseInline(markdown, {});
|
|
401
401
|
const text = (inline?.children ?? [])
|
|
402
402
|
.map((token) =>
|
|
403
403
|
token.type === "text" || token.type === "code_inline" ? token.content
|
|
@@ -501,7 +501,7 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
501
501
|
// image's own render takes when no configuration resolves it.
|
|
502
502
|
}
|
|
503
503
|
return {
|
|
504
|
-
name: figure.label ||
|
|
504
|
+
name: figure.label || pageNameText(figure.caption),
|
|
505
505
|
type: "image",
|
|
506
506
|
src,
|
|
507
507
|
image: figure.hasCaption ? { caption: figure.caption } : {},
|
|
@@ -520,7 +520,7 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
520
520
|
runs,
|
|
521
521
|
);
|
|
522
522
|
return {
|
|
523
|
-
name: figure.label ||
|
|
523
|
+
name: figure.label || pageNameText(figure.caption),
|
|
524
524
|
type: "text",
|
|
525
525
|
text: {
|
|
526
526
|
format: 1,
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -86,13 +86,7 @@ import { scanSpans } from "./content-spans.mjs";
|
|
|
86
86
|
import { GENERIC_FONTS, renderRuns, scanRuns } from "./content-scripts.mjs";
|
|
87
87
|
import { scanFigures } from "./content-figures.mjs";
|
|
88
88
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
89
|
-
import {
|
|
90
|
-
hasColumns,
|
|
91
|
-
lineNumber,
|
|
92
|
-
rightToLeft,
|
|
93
|
-
scanVerse,
|
|
94
|
-
verseColumnCount,
|
|
95
|
-
} from "./content-verse.mjs";
|
|
89
|
+
import { rightToLeft, scanVerse } from "./content-verse.mjs";
|
|
96
90
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
97
91
|
import { panelRoman, scanScrolls, scrollPanels, scrollParagraphs } from "./content-scroll.mjs";
|
|
98
92
|
import { BOOK_INK, BOOK_RUBRIC, fontOutlines, scrollPanelSvg } from "./font-outlines.mjs";
|
|
@@ -522,7 +516,10 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
522
516
|
const marked =
|
|
523
517
|
opts.nested ? undefined : renderRuns(authored, opts.runs, undefined, { target: "book" });
|
|
524
518
|
const source = marked ? marked.markdown : authored;
|
|
525
|
-
if (marked)
|
|
519
|
+
if (marked) {
|
|
520
|
+
sharedOptions.scrolls = marked.scrolls;
|
|
521
|
+
sharedOptions.verses = marked.verses;
|
|
522
|
+
}
|
|
526
523
|
// One map for the whole body, not one per block: a heading inside a
|
|
527
524
|
// blockquote or a list item shares the entry's anchor namespace with every
|
|
528
525
|
// other heading in the same body, because `sectionLabel` scopes by entry
|
|
@@ -543,6 +540,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
543
540
|
footnoteState,
|
|
544
541
|
footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
|
|
545
542
|
scrolls: marked?.scrolls ?? opts.scrolls ?? new Map(),
|
|
543
|
+
verses: marked?.verses ?? opts.verses ?? new Map(),
|
|
546
544
|
};
|
|
547
545
|
// A withheld section is set in the box a `:::secret` is set in, and its
|
|
548
546
|
// heading is set inside it: a printed page honours no reader permission, so
|
|
@@ -691,7 +689,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
691
689
|
/** Render one fragment with the note's footnote definitions available. */
|
|
692
690
|
function renderMarkdownSegment(source, md, ctx, definitions) {
|
|
693
691
|
const input = definitions ? `${source}\n\n${definitions}` : source;
|
|
694
|
-
const tokens = md.parse(input, {});
|
|
692
|
+
const tokens = md.parse(input, { verses: ctx.verses });
|
|
695
693
|
const footnoteStart = tokens.findIndex((token) => token.type === "footnote_block_open");
|
|
696
694
|
const visible = footnoteStart < 0 ? tokens : tokens.slice(0, footnoteStart);
|
|
697
695
|
const footnotes = new Map();
|
|
@@ -779,34 +777,19 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
779
777
|
}
|
|
780
778
|
if (token.type === "fence" && /^verse(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
781
779
|
const opening = `${token.markup}${token.info}`;
|
|
782
|
-
const scanned = scanVerse(`${opening}\n${token.content}${token.markup}
|
|
780
|
+
const scanned = scanVerse(`${opening}\n${token.content}${token.markup}`, {
|
|
781
|
+
checkWorks: false,
|
|
782
|
+
});
|
|
783
783
|
// Findings are collected before rendering. Keep malformed verse
|
|
784
784
|
// literal rather than assigning invalid parts to display cells.
|
|
785
|
-
if (scanned.errors.length) {
|
|
785
|
+
if (scanned.errors.length && !token.meta?.versePaired) {
|
|
786
786
|
out.push(rawBlock(token.content, token.info));
|
|
787
787
|
return 1;
|
|
788
788
|
}
|
|
789
789
|
const poem = scanned.blocks[0];
|
|
790
|
-
const
|
|
791
|
-
const content =
|
|
792
|
-
hasColumns(poem) ? verseGrid(token, poem, step, ctx)
|
|
793
|
-
: step ? numberedVerse(token, step, ctx)
|
|
794
|
-
: token.meta.verseStanzas
|
|
795
|
-
.map((stanza) =>
|
|
796
|
-
stanza[0]?.stanzaBreak ?
|
|
797
|
-
"#v(0.5em)"
|
|
798
|
-
: stanza
|
|
799
|
-
.map(({ children, level }) => {
|
|
800
|
-
const verse = renderInline({ children }, ctx);
|
|
801
|
-
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
802
|
-
})
|
|
803
|
-
.join(" \\\n"),
|
|
804
|
-
)
|
|
805
|
-
.join("\n\n");
|
|
790
|
+
const content = workVerse(token, poem, ctx);
|
|
806
791
|
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
807
|
-
out.push(
|
|
808
|
-
`\n#block(width: 100%)[${scriptWrapped(content, poem?.classes, poem?.attributes)}]${anchor}\n`,
|
|
809
|
-
);
|
|
792
|
+
out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
810
793
|
} else out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
811
794
|
return 1;
|
|
812
795
|
}
|
|
@@ -1300,90 +1283,51 @@ function typstLanguage(tag) {
|
|
|
1300
1283
|
/** The classes that set a run in a font: a face's run carries `script`. */
|
|
1301
1284
|
const FONT_CLASSES = Object.freeze(["script", ...Object.values(GENERIC_FONTS)]);
|
|
1302
1285
|
|
|
1303
|
-
/**
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
* @param {object} poem - The verse, from `scanVerse`.
|
|
1344
|
-
* @param {number} step - Its `numstep`.
|
|
1345
|
-
* @param {object} ctx - Render context.
|
|
1346
|
-
* @returns {string} Typst markup.
|
|
1347
|
-
*/
|
|
1348
|
-
function verseGrid(token, poem, step, ctx) {
|
|
1349
|
-
const count = verseColumnCount(poem);
|
|
1350
|
-
const width = count + (step ? 1 : 0);
|
|
1351
|
-
const columns = [...(step ? ["2.5em"] : []), ...Array(count).fill("1fr")];
|
|
1352
|
-
const rows = [];
|
|
1353
|
-
let row = 0;
|
|
1354
|
-
for (const group of token.meta.verseGroups) {
|
|
1355
|
-
if (group.stanzaBreak) {
|
|
1356
|
-
rows.push(` grid.cell(colspan: ${width})[#v(0.5em)],`);
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
const cells = Array.from({ length: count }, () => []);
|
|
1360
|
-
for (const part of group.parts) {
|
|
1361
|
-
const rtl = rightToLeft(part.text);
|
|
1362
|
-
const secondary = cells[part.column - 1].length > 0;
|
|
1363
|
-
const text = renderInline(
|
|
1364
|
-
{ children: part.children },
|
|
1365
|
-
secondary ? { ...ctx, textFill: "luma(45%)" } : ctx,
|
|
1286
|
+
/** The logical lines of one stanza, with a shared label and paired scripts. */
|
|
1287
|
+
function workVerse(token, poem, ctx) {
|
|
1288
|
+
const paired = token.meta.versePaired;
|
|
1289
|
+
const labelled = Boolean(poem?.attributes.work);
|
|
1290
|
+
const nativeClasses = paired?.classes ?? poem?.classes;
|
|
1291
|
+
const nativeAttributes = paired?.attributes ?? poem?.attributes;
|
|
1292
|
+
return (token.meta.verseGroups ?? [])
|
|
1293
|
+
.map((group) => {
|
|
1294
|
+
if (group.cantoBreak) {
|
|
1295
|
+
const mark =
|
|
1296
|
+
group.children?.length ? renderInline({ children: group.children }, ctx) : "";
|
|
1297
|
+
return `${mark ? scriptWrapped(mark, nativeClasses, nativeAttributes) + "\n" : ""}#v(0.5em)`;
|
|
1298
|
+
}
|
|
1299
|
+
const label = labelled ? `#text(size: 0.75em, fill: luma(45%))[${group.label}]` : "";
|
|
1300
|
+
const lineText = (line, roman = false) => {
|
|
1301
|
+
const text = renderInline(
|
|
1302
|
+
{ children: roman ? line.romanChildren : line.children },
|
|
1303
|
+
ctx,
|
|
1304
|
+
);
|
|
1305
|
+
const padded = `${line.level ? `#h(${line.level * 1.25}em)` : ""}${text}`;
|
|
1306
|
+
if (roman || !paired) return padded;
|
|
1307
|
+
const wrapped = scriptWrapped(padded, nativeClasses, nativeAttributes);
|
|
1308
|
+
return rightToLeft(line.text) ? `#set text(dir: rtl)\n${wrapped}` : wrapped;
|
|
1309
|
+
};
|
|
1310
|
+
if (!paired) {
|
|
1311
|
+
const lines = scriptWrapped(
|
|
1312
|
+
group.lines.map((line) => lineText(line)).join(" \\\n"),
|
|
1313
|
+
nativeClasses,
|
|
1314
|
+
nativeAttributes,
|
|
1315
|
+
);
|
|
1316
|
+
return labelled ?
|
|
1317
|
+
`#grid(columns: (2.5em, 1fr), column-gutter: 0.5em, [${label}], [${lines}])`
|
|
1318
|
+
: lines;
|
|
1319
|
+
}
|
|
1320
|
+
const cells = group.lines.map((line, index) =>
|
|
1321
|
+
[
|
|
1322
|
+
...(labelled ? [index === 0 ? `[${label}]` : "[]"] : []),
|
|
1323
|
+
`[${lineText(line)}]`,
|
|
1324
|
+
`[${lineText(line, true)}]`,
|
|
1325
|
+
].join(", "),
|
|
1366
1326
|
);
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
cells[part.column - 1].push(rtl ? `#set text(dir: rtl)\n${padded}` : padded);
|
|
1372
|
-
}
|
|
1373
|
-
const rendered = cells.map((stacked) => {
|
|
1374
|
-
if (!stacked.length) return "[]";
|
|
1375
|
-
if (stacked.length === 1) return `[${stacked[0]}]`;
|
|
1376
|
-
return `[#stack(dir: ttb, spacing: 0.3em, ${stacked.map((part, k) => (k ? `[#text(fill: luma(45%))[${part}]]` : `[${part}]`)).join(", ")})]`;
|
|
1377
|
-
});
|
|
1378
|
-
const number = step ? [typstNumber(lineNumber(++row, step))] : [];
|
|
1379
|
-
rows.push(` ${[...number, ...rendered].join(", ")},`);
|
|
1380
|
-
}
|
|
1381
|
-
return [
|
|
1382
|
-
"#set text(dir: ltr)",
|
|
1383
|
-
`#grid(columns: (${columns.join(", ")}), column-gutter: 1.5em, row-gutter: 0.65em,`,
|
|
1384
|
-
...rows,
|
|
1385
|
-
")",
|
|
1386
|
-
].join("\n");
|
|
1327
|
+
const columns = labelled ? "2.5em, 1fr, 1fr" : "1fr, 1fr";
|
|
1328
|
+
return `#set text(dir: ltr)\n#grid(columns: (${columns}), column-gutter: 1.5em, row-gutter: 0.35em,\n ${cells.join(",\n ")}\n)`;
|
|
1329
|
+
})
|
|
1330
|
+
.join("\n\n");
|
|
1387
1331
|
}
|
|
1388
1332
|
|
|
1389
1333
|
/**
|
|
@@ -1719,8 +1663,7 @@ function pageOfLabel(label) {
|
|
|
1719
1663
|
* @returns {string} Typst markup.
|
|
1720
1664
|
*/
|
|
1721
1665
|
function renderLink(href, inner, ctx, { bracketed = false } = {}) {
|
|
1722
|
-
// A
|
|
1723
|
-
// link presentation, including any native spans or emphasis in that label.
|
|
1666
|
+
// A caller's explicit text fill applies inside the link's label.
|
|
1724
1667
|
if (ctx.textFill) inner = `#text(fill: ${ctx.textFill})[${inner}]`;
|
|
1725
1668
|
const url = String(href ?? "");
|
|
1726
1669
|
const fragment = /#([^?]*)/.exec(url)?.[1] ?? "";
|
|
@@ -2012,9 +1955,14 @@ export function bookTypstPreamble(runs = {}, families = {}) {
|
|
|
2012
1955
|
" }\n" +
|
|
2013
1956
|
" if lang != none { text(lang: lang, styled) } else { styled }\n" +
|
|
2014
1957
|
"}",
|
|
2015
|
-
//
|
|
1958
|
+
// Plain semantic hooks retain the span classes without choosing their appearance.
|
|
1959
|
+
"#let book-direction(body) = body",
|
|
1960
|
+
"#let book-speaker(body) = body",
|
|
2016
1961
|
"#let book-span(body, classes: (), lang: none) = {\n" +
|
|
2017
|
-
"
|
|
1962
|
+
" let styled = book-script-text(body, classes: classes, lang: lang)\n" +
|
|
1963
|
+
' if "direction" in classes { styled = book-direction(styled) }\n' +
|
|
1964
|
+
' if "speaker" in classes { styled = book-speaker(styled) }\n' +
|
|
1965
|
+
" box(styled)\n" +
|
|
2018
1966
|
"}",
|
|
2019
1967
|
// Use the file's natural measure for auto. Every requested width is
|
|
2020
1968
|
// bounded by the available measure and by the height of a page.
|
|
@@ -67,7 +67,8 @@ export const PREVIEW_HELPERS = Object.freeze({
|
|
|
67
67
|
*/
|
|
68
68
|
export const INERT_BRACKET = "\uE000";
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const syntax = new MarkdownIt();
|
|
71
|
+
const escapeHtml = syntax.utils.escapeHtml;
|
|
71
72
|
// An authored `<` stands as a private-use character until the HTML is written;
|
|
72
73
|
// source is shown as it was authored.
|
|
73
74
|
const escape = (value) => escapeHtml(String(value ?? "").replaceAll(INERT_BRACKET, "<"));
|
|
@@ -185,16 +186,16 @@ function withTrailingAttributes(rest, raw) {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
|
-
* The cells of one table row, keeping
|
|
189
|
-
*
|
|
189
|
+
* The cells of one table row, keeping pipes inside balanced brackets, code
|
|
190
|
+
* spans and extension braces in their cell.
|
|
190
191
|
*
|
|
191
192
|
* @param {string} line - One table line.
|
|
192
193
|
* @returns {string[]} The trimmed cells, without the empty edges.
|
|
193
194
|
*/
|
|
194
195
|
function tableCells(line) {
|
|
195
196
|
const result = [];
|
|
197
|
+
const state = new syntax.inline.State(line, syntax, {}, []);
|
|
196
198
|
let cell = "",
|
|
197
|
-
wiki = false,
|
|
198
199
|
code = "",
|
|
199
200
|
quote = false,
|
|
200
201
|
braces = 0;
|
|
@@ -204,7 +205,15 @@ function tableCells(line) {
|
|
|
204
205
|
cell += char + line[++at];
|
|
205
206
|
continue;
|
|
206
207
|
}
|
|
207
|
-
if (char === "
|
|
208
|
+
if (char === "[" && !code && !braces) {
|
|
209
|
+
const end = syntax.helpers.parseLinkLabel(state, at, false);
|
|
210
|
+
if (end >= 0) {
|
|
211
|
+
cell += line.slice(at, end + 1);
|
|
212
|
+
at = end;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (char === "`") {
|
|
208
217
|
const marks = /^`+/.exec(line.slice(at))[0];
|
|
209
218
|
if (!code) code = marks;
|
|
210
219
|
else if (code === marks) code = "";
|
|
@@ -213,13 +222,11 @@ function tableCells(line) {
|
|
|
213
222
|
continue;
|
|
214
223
|
}
|
|
215
224
|
if (!code) {
|
|
216
|
-
if (line.slice(at, at + 2) === "[[") wiki = true;
|
|
217
|
-
if (line.slice(at - 1, at + 1) === "]]") wiki = false;
|
|
218
225
|
if (char === '"' && braces) quote = !quote;
|
|
219
226
|
if (!quote && char === "{") braces++;
|
|
220
227
|
if (!quote && char === "}" && braces) braces--;
|
|
221
228
|
}
|
|
222
|
-
if (char === "|" && !
|
|
229
|
+
if (char === "|" && !code && !braces) {
|
|
223
230
|
result.push(cell.trim());
|
|
224
231
|
cell = "";
|
|
225
232
|
} else cell += char;
|
package/engine/roman-verse.mjs
CHANGED
|
@@ -12,16 +12,15 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** Logical verse text and separately located structural boundaries. @module */
|
|
15
|
-
import {
|
|
15
|
+
import { parseVerse } from "./content-verse.mjs";
|
|
16
16
|
import { plainRomanRun } from "./roman-run.mjs";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* The caller writes the boundaries through its selected encoding.
|
|
19
|
+
* Project verse text with explicit authored line, stanza and canto boundaries.
|
|
20
|
+
* Source newlines within a logical line are whitespace. Delimiters and visual
|
|
21
|
+
* indentation do not reach the encoder as letters.
|
|
23
22
|
* @param {string} source - Complete authored source.
|
|
24
|
-
* @param {Parameters<typeof plainRomanRun>[1] & {
|
|
23
|
+
* @param {Parameters<typeof plainRomanRun>[1] & {model?: ReturnType<typeof parseVerse>, modelOffset?: number}} [options] - Run bounds, spans and layout projection.
|
|
25
24
|
* @returns {ReturnType<typeof plainRomanRun> & {boundaries: Array<{kind: "line"|"stanza"|"canto", at: number, sourceOffset: number}>, errors: Array<{offset: number, message: string}>}} Visible text, positions and structural events.
|
|
26
25
|
*/
|
|
27
26
|
export function plainRomanVerse(source, options = {}) {
|
|
@@ -29,28 +28,60 @@ export function plainRomanVerse(source, options = {}) {
|
|
|
29
28
|
from = 0,
|
|
30
29
|
to = source.length,
|
|
31
30
|
exclude = [],
|
|
32
|
-
|
|
31
|
+
model: sharedModel,
|
|
32
|
+
modelOffset = from,
|
|
33
33
|
} = options;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
const model = sharedModel ?? parseVerse(source.slice(from, to));
|
|
35
|
+
const base = sharedModel ? modelOffset : from;
|
|
36
|
+
const boundaries = model.boundaries
|
|
37
|
+
.map((boundary) => ({ ...boundary, sourceOffset: base + boundary.sourceOffset }))
|
|
38
|
+
.filter((boundary) => boundary.sourceOffset >= from && boundary.sourceOffset <= to);
|
|
39
|
+
const projected = plainRomanRun(source, {
|
|
40
|
+
...options,
|
|
41
|
+
from,
|
|
42
|
+
to,
|
|
43
|
+
exclude: [
|
|
44
|
+
...exclude,
|
|
45
|
+
...model.layout
|
|
46
|
+
.map(([first, past]) => [Math.max(from, base + first), Math.min(to, base + past)])
|
|
47
|
+
.filter(([first, past]) => past > first),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
const text = [],
|
|
51
|
+
offsets = [],
|
|
52
|
+
positions = [],
|
|
53
|
+
ends = [];
|
|
54
|
+
const pending = [...boundaries];
|
|
55
|
+
let boundaryIndex = 0;
|
|
56
|
+
const appendBoundary = (boundary) => {
|
|
57
|
+
text.push("\n");
|
|
58
|
+
offsets.push(boundary.sourceOffset);
|
|
59
|
+
};
|
|
60
|
+
for (let index = 0; index < projected.text.length; index++) {
|
|
61
|
+
ends[index] = text.length;
|
|
62
|
+
while (
|
|
63
|
+
pending[boundaryIndex] &&
|
|
64
|
+
pending[boundaryIndex].sourceOffset <= projected.offsets[index]
|
|
65
|
+
)
|
|
66
|
+
appendBoundary(pending[boundaryIndex++]);
|
|
67
|
+
positions[index] = text.length;
|
|
68
|
+
const char = projected.text[index];
|
|
69
|
+
if (char === "\n" && projected.text[index - 1] === "\r") continue;
|
|
70
|
+
text.push(/[\r\n]/.test(char) ? " " : char);
|
|
71
|
+
offsets.push(projected.offsets[index]);
|
|
52
72
|
}
|
|
53
|
-
|
|
73
|
+
ends[projected.text.length] = text.length;
|
|
74
|
+
while (pending[boundaryIndex]) appendBoundary(pending[boundaryIndex++]);
|
|
75
|
+
positions[projected.text.length] = text.length;
|
|
76
|
+
projected.text = text.join("");
|
|
77
|
+
projected.offsets = offsets;
|
|
78
|
+
projected.spans = projected.spans.map((span) => ({
|
|
79
|
+
...span,
|
|
80
|
+
from: positions[span.from],
|
|
81
|
+
to: ends[span.to],
|
|
82
|
+
contentFrom: positions[span.contentFrom],
|
|
83
|
+
contentTo: ends[span.contentTo],
|
|
84
|
+
}));
|
|
54
85
|
const logicalAt = (offset) => {
|
|
55
86
|
let first = 0,
|
|
56
87
|
past = projected.offsets.length;
|
|
@@ -61,35 +92,15 @@ export function plainRomanVerse(source, options = {}) {
|
|
|
61
92
|
}
|
|
62
93
|
return first;
|
|
63
94
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
boundaries.push({
|
|
76
|
-
kind: "stanza",
|
|
77
|
-
at: logicalAt(line.from),
|
|
78
|
-
sourceOffset: line.from,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const errors = [];
|
|
83
|
-
for (let index = 0; index < projected.text.length; index++) {
|
|
84
|
-
if (projected.text[index] === "⟦") {
|
|
85
|
-
const close = projected.text.indexOf("⟧", index + 1);
|
|
86
|
-
index = close < 0 ? projected.text.length : close;
|
|
87
|
-
} else if (projected.text[index] === "/")
|
|
88
|
-
errors.push({
|
|
89
|
-
offset: projected.offsets[index],
|
|
90
|
-
message:
|
|
91
|
-
"a verse fence uses authored line endings for verse boundaries; write no literal `/` inside it",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return { ...projected, boundaries, errors };
|
|
95
|
+
return {
|
|
96
|
+
...projected,
|
|
97
|
+
boundaries: boundaries.map(({ kind, sourceOffset }) => ({
|
|
98
|
+
kind,
|
|
99
|
+
sourceOffset,
|
|
100
|
+
at: logicalAt(sourceOffset),
|
|
101
|
+
})),
|
|
102
|
+
errors: model.errors
|
|
103
|
+
.map(({ offset, message }) => ({ offset: base + offset, message }))
|
|
104
|
+
.filter((error) => error.offset >= from && error.offset < to),
|
|
105
|
+
};
|
|
95
106
|
}
|
package/engine/verse-metre.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import { WORD } from "./word-pattern.mjs";
|
|
|
60
60
|
|
|
61
61
|
import MarkdownIt from "markdown-it";
|
|
62
62
|
import { spanMarkdownPlugin } from "./content-spans.mjs";
|
|
63
|
-
import {
|
|
63
|
+
import { parseVerse } from "./content-verse.mjs";
|
|
64
64
|
import { inlineText } from "./name-markup.mjs";
|
|
65
65
|
|
|
66
66
|
const verseParser = new MarkdownIt().use(spanMarkdownPlugin);
|
|
@@ -311,13 +311,6 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
311
311
|
column: ctx.attributeColumns(opening, key)[part],
|
|
312
312
|
});
|
|
313
313
|
const error = (position, message) => errors.push({ ...position, message });
|
|
314
|
-
if (Number(block.attributes.cols ?? 1) > 1) {
|
|
315
|
-
error(
|
|
316
|
-
at("scan", "key"),
|
|
317
|
-
"`scan=` counts the beats of one language's lines; a verse with cols>1 groups independent physical lines",
|
|
318
|
-
);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
314
|
const metre = metres.get(name);
|
|
322
315
|
if (!metre) {
|
|
323
316
|
const near = nearest(name, [...metres.keys()]);
|
|
@@ -346,40 +339,40 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
346
339
|
);
|
|
347
340
|
continue;
|
|
348
341
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
(reading.markerStart <= reading.textColumn - 1 + foreign.meta.start ?
|
|
369
|
-
reading.markerLength
|
|
370
|
-
: 0),
|
|
371
|
-
},
|
|
372
|
-
"`scan=` counts one language's beats; this verse line contains another language",
|
|
342
|
+
const body = lines.slice(block.start + 1, block.end).join("\n");
|
|
343
|
+
const model = block.model ?? parseVerse(body);
|
|
344
|
+
const sourcePosition = (offset) => {
|
|
345
|
+
const preceding = body.slice(0, offset);
|
|
346
|
+
return {
|
|
347
|
+
line: block.start + 2 + preceding.split("\n").length - 1,
|
|
348
|
+
column: preceding.length - preceding.lastIndexOf("\n"),
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
for (const group of model.groups) {
|
|
352
|
+
if (group.cantoBreak) continue;
|
|
353
|
+
for (const reading of group.lines) {
|
|
354
|
+
const tokens = verseParser.parseInline(reading.text, {})[0]?.children ?? [];
|
|
355
|
+
const tag = (name) => (ctx.languages?.[name]?.tag ?? name).toLowerCase();
|
|
356
|
+
const foreign = tokens.find(
|
|
357
|
+
(token) =>
|
|
358
|
+
token.type === "span_open" &&
|
|
359
|
+
token.attrGet("lang") &&
|
|
360
|
+
tag(token.attrGet("lang")) !== tag(lang),
|
|
373
361
|
);
|
|
374
|
-
|
|
362
|
+
if (foreign) {
|
|
363
|
+
error(
|
|
364
|
+
sourcePosition(reading.offsets[foreign.meta.start] ?? reading.from),
|
|
365
|
+
"`scan=` counts one language's beats; this verse line contains another language",
|
|
366
|
+
);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const beats = lineBeats(inlineText(tokens), book);
|
|
370
|
+
if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
|
|
371
|
+
warnings.push({
|
|
372
|
+
...sourcePosition(reading.from),
|
|
373
|
+
message: metreMessage(beats, metre),
|
|
374
|
+
});
|
|
375
375
|
}
|
|
376
|
-
const beats = lineBeats(inlineText(tokens), book);
|
|
377
|
-
if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
|
|
378
|
-
warnings.push({
|
|
379
|
-
line: line + 1,
|
|
380
|
-
column: /^\s*/.exec(text)[0].length + 1,
|
|
381
|
-
message: metreMessage(beats, metre),
|
|
382
|
-
});
|
|
383
376
|
}
|
|
384
377
|
}
|
|
385
378
|
return { errors, warnings };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.3.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",
|