@heroiclands/package-build 25.3.0 → 25.5.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 +39 -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 +143 -53
- package/docs/commands.md +8 -2
- package/docs/content-format.md +14 -6
- package/docs/diagnostics.md +12 -11
- package/docs/encoding-core.md +18 -4
- package/docs/guides/writing-in-invented-scripts.md +83 -37
- package/docs/reference/format-details.md +10 -11
- 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 +69 -1
- package/engine/content-index.mjs +1 -1
- package/engine/content-render.mjs +59 -43
- package/engine/content-scripts.mjs +301 -58
- package/engine/content-scroll.mjs +8 -3
- package/engine/content-spans.mjs +107 -6
- package/engine/content-verse.mjs +338 -198
- package/engine/encoding-standard.mjs +1 -0
- package/engine/encoding-template.mjs +5 -2
- package/engine/encoding.mjs +19 -14
- 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/markdownlint-run.mjs +190 -5
- 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/roman-characters.mjs +32 -10
- package/engine/roman-run.mjs +3 -0
- package/engine/roman-verse.mjs +1 -0
- package/engine/run-rows.mjs +1 -1
- package/engine/sql-tables.mjs +1 -1
- package/engine/verse-format.mjs +89 -0
- package/engine/verse-metre.mjs +18 -9
- package/engine/verse-regions.mjs +190 -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-scroll.d.mts +1 -1
- package/types/engine/content-spans.d.mts +3 -1
- package/types/engine/content-verse.d.mts +75 -75
- package/types/engine/name-markup.d.mts +5 -0
- package/types/engine/roman-run.d.mts +3 -1
- package/types/engine/verse-format.d.mts +10 -0
- package/types/engine/verse-metre.d.mts +4 -4
- package/types/engine/verse-regions.d.mts +82 -0
- package/types/engine/wikilink-pattern.d.mts +17 -0
- package/types/engine/wikilink-syntax.d.mts +1 -41
|
@@ -45,6 +45,7 @@ import { blockTokens } from "./markdown-blocks.mjs";
|
|
|
45
45
|
import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
|
|
46
46
|
import { scanVerse } from "./content-verse.mjs";
|
|
47
47
|
import { scanSpans } from "./content-spans.mjs";
|
|
48
|
+
import { footnoteLabels, footnoteReferences } from "./content-footnotes.mjs";
|
|
48
49
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
49
50
|
import { directionsFromRecords } from "./script-direction.mjs";
|
|
50
51
|
import { isLanguageTag } from "./language-tag.mjs";
|
|
@@ -126,7 +127,7 @@ const CONFIG = "package-build.config.yaml";
|
|
|
126
127
|
*/
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* One span, div, verse
|
|
130
|
+
* One span, div, verse div or scroll fence carrying run attributes.
|
|
130
131
|
* Values combine explicit choices, class-only inheritance, encoding-document
|
|
131
132
|
* language/font defaults and the Roman default encoding.
|
|
132
133
|
*
|
|
@@ -232,7 +233,7 @@ function withinOneEdit(a, b) {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
/** The word each construct is called in a finding. */
|
|
235
|
-
const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse
|
|
236
|
+
const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse div" });
|
|
236
237
|
|
|
237
238
|
/** Line starts of a source, for offsets to positions. */
|
|
238
239
|
function lineStarts(text) {
|
|
@@ -834,9 +835,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
834
835
|
);
|
|
835
836
|
const errors = [];
|
|
836
837
|
const warnings = [];
|
|
837
|
-
const footnotes =
|
|
838
|
-
[...text.matchAll(/^ {0,3}\[\^([^\]\s]+)\]:/gm)].map((match) => match[1]),
|
|
839
|
-
);
|
|
838
|
+
const footnotes = footnoteLabels(text);
|
|
840
839
|
/** @param {{line: number, column: number}} at @param {string} message */
|
|
841
840
|
const report = (at, message) => errors.push({ line: at.line, column: at.column, message });
|
|
842
841
|
/** @param {Record<string, string>} values */
|
|
@@ -890,6 +889,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
890
889
|
const collect = (offset, body) => {
|
|
891
890
|
const scanned = offset === 0 && syntax ? syntax(body).blocks : scanBlocks(body);
|
|
892
891
|
for (const block of scanned.blocks) {
|
|
892
|
+
if (block.name === "verse" || block.classes?.includes("verse")) continue;
|
|
893
893
|
const first = offset + block.start;
|
|
894
894
|
const last = offset + block.end;
|
|
895
895
|
const opening = lines[first];
|
|
@@ -911,7 +911,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
911
911
|
for (const key of Object.keys(keys))
|
|
912
912
|
report(
|
|
913
913
|
at(key, "key"),
|
|
914
|
-
`\`${key}=\` is read on a span, a div or a verse
|
|
914
|
+
`\`${key}=\` is read on a span, a div or a verse div, not on a ${block.name} block`,
|
|
915
915
|
);
|
|
916
916
|
} else {
|
|
917
917
|
divs.set(first, block);
|
|
@@ -948,7 +948,6 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
948
948
|
for (const poem of scanVerse(text).blocks) {
|
|
949
949
|
poems.set(poem.start, poem);
|
|
950
950
|
const keys = runKeys(poem.attributes);
|
|
951
|
-
if (!Object.keys(keys).length && !poem.classes.includes(NATIVEFONT_CLASS)) continue;
|
|
952
951
|
const opening = lines[poem.start];
|
|
953
952
|
constructs.push({
|
|
954
953
|
kind: "verse",
|
|
@@ -1196,10 +1195,35 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1196
1195
|
}
|
|
1197
1196
|
|
|
1198
1197
|
const verseMarkers = new Map();
|
|
1199
|
-
const verseContexts =
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1198
|
+
const verseContexts = [
|
|
1199
|
+
...scanVerse(text, { checkWorks: false }).blocks,
|
|
1200
|
+
...spans
|
|
1201
|
+
.filter((span) => span.verseModel)
|
|
1202
|
+
.map((span) => ({
|
|
1203
|
+
bodyStart: span.start + 1,
|
|
1204
|
+
bodyEnd: span.contentEnd,
|
|
1205
|
+
body: text.slice(span.start + 1, span.contentEnd),
|
|
1206
|
+
model: span.verseModel,
|
|
1207
|
+
})),
|
|
1208
|
+
];
|
|
1209
|
+
// A reference with its own run keys is an independent child, just as
|
|
1210
|
+
// the presentation span produced by the link resolver is. Keep its label
|
|
1211
|
+
// out of the enclosing language and encoder without rewriting its target.
|
|
1212
|
+
const independentReferences = rawReferences.filter(
|
|
1213
|
+
(reference) =>
|
|
1214
|
+
Object.keys(reference.construct.keys).length &&
|
|
1215
|
+
verseContexts.some(
|
|
1216
|
+
(poem) =>
|
|
1217
|
+
reference.where.start >= poem.bodyStart && reference.where.end <= poem.bodyEnd,
|
|
1218
|
+
),
|
|
1219
|
+
);
|
|
1220
|
+
const referenceSpans = independentReferences.map((reference) => ({
|
|
1221
|
+
start: reference.where.start,
|
|
1222
|
+
end: reference.where.end,
|
|
1223
|
+
contentEnd: reference.to,
|
|
1224
|
+
}));
|
|
1225
|
+
for (const poem of verseContexts) {
|
|
1226
|
+
const bodyStart = poem.bodyStart;
|
|
1203
1227
|
for (const [from, to] of poem.model.layout)
|
|
1204
1228
|
verseMarkers.set(bodyStart + from, bodyStart + to);
|
|
1205
1229
|
for (const match of poem.body.matchAll(WIKILINK)) {
|
|
@@ -1227,11 +1251,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1227
1251
|
if (poem.body[at] !== "[") continue;
|
|
1228
1252
|
const end = closing(poem.body, at);
|
|
1229
1253
|
if (end < 0) continue;
|
|
1230
|
-
|
|
1231
|
-
if (/^\^[^\s\]]+$/.test(label) && footnotes.has(label.slice(1))) {
|
|
1232
|
-
verseMarkers.set(bodyStart + at, bodyStart + end + 1);
|
|
1233
|
-
at = end;
|
|
1234
|
-
} else if (poem.body[end + 1] === "(" && poem.body[at - 1] !== "!") {
|
|
1254
|
+
if (poem.body[end + 1] === "(" && poem.body[at - 1] !== "!") {
|
|
1235
1255
|
const past = closing(poem.body, end + 1);
|
|
1236
1256
|
if (past < 0) continue;
|
|
1237
1257
|
verseMarkers.set(bodyStart + at, bodyStart + at + 1);
|
|
@@ -1240,10 +1260,52 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1240
1260
|
}
|
|
1241
1261
|
}
|
|
1242
1262
|
|
|
1263
|
+
const noteReferences = [
|
|
1264
|
+
...new Map(
|
|
1265
|
+
[
|
|
1266
|
+
...footnoteReferences(text, { definitions: footnotes }),
|
|
1267
|
+
...spans.flatMap((span) => {
|
|
1268
|
+
const label = referenceLabel(span.text);
|
|
1269
|
+
if (!label) return [];
|
|
1270
|
+
const base = span.start + 1 + label[0];
|
|
1271
|
+
return footnoteReferences(span.text.slice(label[0], label[1]), {
|
|
1272
|
+
definitions: footnotes,
|
|
1273
|
+
inline: true,
|
|
1274
|
+
}).map((reference) => ({
|
|
1275
|
+
...reference,
|
|
1276
|
+
from: base + reference.from,
|
|
1277
|
+
to: base + reference.to,
|
|
1278
|
+
}));
|
|
1279
|
+
}),
|
|
1280
|
+
...verseContexts.flatMap((region) =>
|
|
1281
|
+
footnoteReferences(region.body, { definitions: footnotes, inline: true }).map(
|
|
1282
|
+
(reference) => ({
|
|
1283
|
+
...reference,
|
|
1284
|
+
from: reference.from + region.bodyStart,
|
|
1285
|
+
to: reference.to + region.bodyStart,
|
|
1286
|
+
}),
|
|
1287
|
+
),
|
|
1288
|
+
),
|
|
1289
|
+
].map((reference) => [reference.from, reference]),
|
|
1290
|
+
).values(),
|
|
1291
|
+
];
|
|
1292
|
+
|
|
1243
1293
|
/** Nested spans and verse markers excluded from a run, by absolute offset. */
|
|
1244
1294
|
const holesIn = (from, to) =>
|
|
1245
1295
|
new Map([
|
|
1296
|
+
...noteReferences
|
|
1297
|
+
.filter((reference) => reference.from >= from && reference.from < to)
|
|
1298
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1246
1299
|
...[...verseMarkers].filter(([start]) => start >= from && start < to),
|
|
1300
|
+
...independentReferences
|
|
1301
|
+
.filter((reference) => reference.where.start >= from && reference.where.start < to)
|
|
1302
|
+
.map((reference) => [reference.where.start, reference.where.end]),
|
|
1303
|
+
...verseContexts
|
|
1304
|
+
.filter(
|
|
1305
|
+
(region) =>
|
|
1306
|
+
Number.isFinite(region.from) && region.from >= from && region.from < to,
|
|
1307
|
+
)
|
|
1308
|
+
.map((region) => [region.from, Math.min(region.to, to)]),
|
|
1247
1309
|
...active
|
|
1248
1310
|
.filter(
|
|
1249
1311
|
(construct) =>
|
|
@@ -1406,6 +1468,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1406
1468
|
selection: enc === undefined ? undefined : encodingFor(ctx, { enc, font, lang }),
|
|
1407
1469
|
holes: new Map(),
|
|
1408
1470
|
body: undefined,
|
|
1471
|
+
verseModel: construct.kind === "span" ? construct.node.verseModel : undefined,
|
|
1409
1472
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
1410
1473
|
reference: undefined,
|
|
1411
1474
|
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
@@ -1416,7 +1479,10 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1416
1479
|
detail.conventions = conventionsOf(detail, ctx);
|
|
1417
1480
|
details.push(detail);
|
|
1418
1481
|
const nonEnglishSpan =
|
|
1419
|
-
construct.kind === "span" &&
|
|
1482
|
+
construct.kind === "span" &&
|
|
1483
|
+
!construct.node.verseModel &&
|
|
1484
|
+
run.tag !== undefined &&
|
|
1485
|
+
!/^en(?:-|$)/i.test(run.tag);
|
|
1420
1486
|
const emphasis = construct.own.classes.filter(
|
|
1421
1487
|
(name) => name === "emph" || name === "strong",
|
|
1422
1488
|
);
|
|
@@ -1447,7 +1513,14 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1447
1513
|
[from, to] = detail.reference;
|
|
1448
1514
|
}
|
|
1449
1515
|
const inline = text.slice(from, to);
|
|
1450
|
-
detail.holes =
|
|
1516
|
+
detail.holes =
|
|
1517
|
+
label ?
|
|
1518
|
+
new Map(
|
|
1519
|
+
noteReferences
|
|
1520
|
+
.filter((reference) => reference.from >= from && reference.to <= to)
|
|
1521
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1522
|
+
)
|
|
1523
|
+
: holesIn(from, to);
|
|
1451
1524
|
if (nonEnglishSpan) {
|
|
1452
1525
|
const relative = new Map(
|
|
1453
1526
|
[...detail.holes].map(([from, to]) => [
|
|
@@ -1471,8 +1544,15 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1471
1544
|
|
|
1472
1545
|
const first = construct.line0 + 1;
|
|
1473
1546
|
const last = construct.last;
|
|
1474
|
-
|
|
1475
|
-
|
|
1547
|
+
// Verse regions retain authored UTF-16 bounds, including the full
|
|
1548
|
+
// width of a CRLF terminator. A one-byte closing-line adjustment
|
|
1549
|
+
// would leave CR outside the model and lose verse projection.
|
|
1550
|
+
const from =
|
|
1551
|
+
construct.kind === "verse" ? construct.node.bodyStart : (starts[first] ?? text.length);
|
|
1552
|
+
const to =
|
|
1553
|
+
construct.kind === "verse" ? construct.node.bodyEnd
|
|
1554
|
+
: last > first ? starts[last] - 1
|
|
1555
|
+
: from;
|
|
1476
1556
|
detail.body = [from, to];
|
|
1477
1557
|
detail.holes = holesIn(from, to);
|
|
1478
1558
|
|
|
@@ -1507,7 +1587,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1507
1587
|
said(
|
|
1508
1588
|
top,
|
|
1509
1589
|
token.info.trim().split(/\s/)[0] === "verse" ?
|
|
1510
|
-
"a verse
|
|
1590
|
+
"a code fence named `verse`; write a `::: {.verse}` div"
|
|
1511
1591
|
: "a code fence",
|
|
1512
1592
|
);
|
|
1513
1593
|
else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
|
|
@@ -1572,7 +1652,23 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1572
1652
|
const owned = details.filter((detail) => ownerOf(detail) === owner);
|
|
1573
1653
|
for (const detail of owned) detail.owner = owner;
|
|
1574
1654
|
const excluded = [
|
|
1575
|
-
...
|
|
1655
|
+
...noteReferences
|
|
1656
|
+
.filter((reference) => reference.from >= bounds[0] && reference.from < bounds[1])
|
|
1657
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1658
|
+
...[...verseMarkers].filter(
|
|
1659
|
+
([start]) =>
|
|
1660
|
+
start >= bounds[0] &&
|
|
1661
|
+
start < bounds[1] &&
|
|
1662
|
+
!referenceSpans.some((span) => start === span.start),
|
|
1663
|
+
),
|
|
1664
|
+
...verseContexts
|
|
1665
|
+
.filter(
|
|
1666
|
+
(region) =>
|
|
1667
|
+
Number.isFinite(region.from) &&
|
|
1668
|
+
region.from >= bounds[0] &&
|
|
1669
|
+
region.from < bounds[1],
|
|
1670
|
+
)
|
|
1671
|
+
.map((region) => [region.from, Math.min(region.to, bounds[1])]),
|
|
1576
1672
|
...owned.flatMap((detail) =>
|
|
1577
1673
|
detail.emphasis.map((delimiter) => [
|
|
1578
1674
|
delimiter.offset,
|
|
@@ -1583,22 +1679,21 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1583
1679
|
const projectionOptions = {
|
|
1584
1680
|
from: bounds[0],
|
|
1585
1681
|
to: bounds[1],
|
|
1586
|
-
spans,
|
|
1682
|
+
spans: [...spans, ...referenceSpans],
|
|
1587
1683
|
exclude: excluded,
|
|
1588
1684
|
includeSpan: (span) =>
|
|
1589
1685
|
spanDetails.has(span.start) && ownerOf(spanDetails.get(span.start)) === owner,
|
|
1590
1686
|
};
|
|
1591
|
-
const poem =
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
: verseContexts.get(owner.run.line - 1);
|
|
1687
|
+
const poem = verseContexts
|
|
1688
|
+
.filter((region) => region.bodyStart <= bounds[0] && region.bodyEnd >= bounds[1])
|
|
1689
|
+
.sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
1595
1690
|
const verse = poem !== undefined;
|
|
1596
1691
|
const projection =
|
|
1597
1692
|
verse ?
|
|
1598
1693
|
plainRomanVerse(text, {
|
|
1599
1694
|
...projectionOptions,
|
|
1600
1695
|
model: poem.model,
|
|
1601
|
-
modelOffset:
|
|
1696
|
+
modelOffset: poem.bodyStart,
|
|
1602
1697
|
})
|
|
1603
1698
|
: plainRomanRun(text, projectionOptions);
|
|
1604
1699
|
const before = errors.length;
|
|
@@ -1680,12 +1775,25 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1680
1775
|
}
|
|
1681
1776
|
if (verse) {
|
|
1682
1777
|
for (const boundary of projection.boundaries) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1778
|
+
let target;
|
|
1779
|
+
if (boundary.kind === "canto") {
|
|
1780
|
+
const encoded = encodeWithLanguage(owner.selection.encoding, "***", {
|
|
1781
|
+
font: owner.selection.font,
|
|
1782
|
+
lexicon: owner.selection.lexicon,
|
|
1783
|
+
verse: true,
|
|
1784
|
+
});
|
|
1785
|
+
for (const message of new Set(
|
|
1786
|
+
encoded.findings.map((finding) => finding.message),
|
|
1787
|
+
))
|
|
1788
|
+
report(positionOf(starts, boundary.sourceOffset), message);
|
|
1789
|
+
target = encoded.text;
|
|
1790
|
+
} else
|
|
1791
|
+
target =
|
|
1792
|
+
boundary.kind === "line" ?
|
|
1793
|
+
encodeText(owner.selection.encoding, "/", {
|
|
1794
|
+
font: owner.selection.font,
|
|
1795
|
+
})
|
|
1796
|
+
: owner.selection.encoding.controls.verseStanza;
|
|
1689
1797
|
if (target === undefined) continue;
|
|
1690
1798
|
pieces.push({
|
|
1691
1799
|
from: "",
|
|
@@ -1734,6 +1842,17 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1734
1842
|
});
|
|
1735
1843
|
}
|
|
1736
1844
|
|
|
1845
|
+
for (const reference of independentReferences) {
|
|
1846
|
+
const book = ctx.wordBooks?.get(reference.lang);
|
|
1847
|
+
if (!book) continue;
|
|
1848
|
+
const projection = plainRomanRun(text, { from: reference.from, to: reference.to });
|
|
1849
|
+
for (const problem of unknownWords(projection.text, book))
|
|
1850
|
+
report(
|
|
1851
|
+
positionOf(starts, projection.offsets[problem.offset] ?? reference.from),
|
|
1852
|
+
unknownWordMessage(problem.word, reference.lang),
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1737
1856
|
const firstEncodingRun =
|
|
1738
1857
|
active.find(native) ??
|
|
1739
1858
|
rawReferences.find((reference) => native(reference.construct))?.construct;
|
|
@@ -1776,7 +1895,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1776
1895
|
details.sort((a, b) => order(a.run, b.run));
|
|
1777
1896
|
errors.sort(order);
|
|
1778
1897
|
warnings.sort(order);
|
|
1779
|
-
return { runs, details, errors, warnings, spans, divs, poems };
|
|
1898
|
+
return { runs, details, errors, warnings, spans, divs, poems, references: noteReferences };
|
|
1780
1899
|
}
|
|
1781
1900
|
|
|
1782
1901
|
/**
|
|
@@ -1802,7 +1921,7 @@ function diagnosticColumns(source, findings) {
|
|
|
1802
1921
|
}
|
|
1803
1922
|
|
|
1804
1923
|
/**
|
|
1805
|
-
* Find the spans, divs and verse
|
|
1924
|
+
* Find the spans, divs and verse divs marked with `lang=`, `font=` or
|
|
1806
1925
|
* `enc=`, and every fault in their markup and their text.
|
|
1807
1926
|
*
|
|
1808
1927
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
@@ -2033,7 +2152,7 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
2033
2152
|
* scanner projections.
|
|
2034
2153
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
2035
2154
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
2036
|
-
* @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
|
|
2155
|
+
* @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}>}}
|
|
2037
2156
|
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
2038
2157
|
* rewritten scroll fence's layout, by its rewritten body.
|
|
2039
2158
|
*/
|
|
@@ -2046,7 +2165,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2046
2165
|
const verses = new Map();
|
|
2047
2166
|
if (!/\b(?:lang|font|enc)=|\bnativefont\b|\.(?:emph|strong)\b|⟦/.test(text))
|
|
2048
2167
|
return { markdown: text, errors: [], scrolls, verses };
|
|
2049
|
-
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
2168
|
+
const { details, errors, references } = analyseRuns(text, ctx, syntax);
|
|
2050
2169
|
if (!details.length)
|
|
2051
2170
|
return { markdown: text, errors: diagnosticColumns(text, errors), scrolls, verses };
|
|
2052
2171
|
const lines = text.split("\n");
|
|
@@ -2067,9 +2186,24 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2067
2186
|
|
|
2068
2187
|
/** @type {Array<{from: number, to: number, text: string}>} */
|
|
2069
2188
|
const edits = [];
|
|
2070
|
-
const poems =
|
|
2189
|
+
const poems = [
|
|
2190
|
+
...scanVerse(text, { checkWorks: false }).blocks.map((block) => ({
|
|
2191
|
+
...block,
|
|
2192
|
+
inline: false,
|
|
2193
|
+
})),
|
|
2194
|
+
...scanSpans(text)
|
|
2195
|
+
.spans.filter((span) => span.verseModel)
|
|
2196
|
+
.map((span) => ({
|
|
2197
|
+
...span,
|
|
2198
|
+
inline: true,
|
|
2199
|
+
model: span.verseModel,
|
|
2200
|
+
body: text.slice(span.start + 1, span.contentEnd),
|
|
2201
|
+
bodyStart: span.start + 1,
|
|
2202
|
+
bodyEnd: span.contentEnd,
|
|
2203
|
+
})),
|
|
2204
|
+
].sort((a, b) => a.bodyStart - b.bodyStart);
|
|
2071
2205
|
const logicalRanges = poems.flatMap((poem) => {
|
|
2072
|
-
const base =
|
|
2206
|
+
const base = poem.bodyStart;
|
|
2073
2207
|
return poem.model.groups.flatMap((group) =>
|
|
2074
2208
|
group.cantoBreak ? [] : group.lines.map((line) => [base + line.from, base + line.to]),
|
|
2075
2209
|
);
|
|
@@ -2223,11 +2357,19 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2223
2357
|
if (attributes !== undefined) {
|
|
2224
2358
|
const open = lines[run.start].indexOf("{");
|
|
2225
2359
|
const close = lines[run.start].lastIndexOf("}");
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2360
|
+
if (open < 0 && run.kind === "verse") {
|
|
2361
|
+
const opening = lines[run.start];
|
|
2362
|
+
edits.push({
|
|
2363
|
+
from: starts[run.start] + opening.indexOf(":::"),
|
|
2364
|
+
to: starts[run.start] + opening.replace(/\r$/, "").length,
|
|
2365
|
+
text: `::: {${attributes}}`,
|
|
2366
|
+
});
|
|
2367
|
+
} else
|
|
2368
|
+
edits.push({
|
|
2369
|
+
from: starts[run.start] + open + 1,
|
|
2370
|
+
to: starts[run.start] + close,
|
|
2371
|
+
text: attributes,
|
|
2372
|
+
});
|
|
2231
2373
|
}
|
|
2232
2374
|
if (detail.plain && detail.body)
|
|
2233
2375
|
for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
|
|
@@ -2260,22 +2402,79 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2260
2402
|
}
|
|
2261
2403
|
}
|
|
2262
2404
|
|
|
2405
|
+
const restoredReferences = new Map();
|
|
2406
|
+
for (const reference of references) {
|
|
2407
|
+
const nativeDiv = details.find(
|
|
2408
|
+
(detail) =>
|
|
2409
|
+
detail.run.kind === "verse" &&
|
|
2410
|
+
detail.selection &&
|
|
2411
|
+
detail.run.enc !== "default" &&
|
|
2412
|
+
detail.body &&
|
|
2413
|
+
reference.from >= detail.body[0] &&
|
|
2414
|
+
reference.to <= detail.body[1] &&
|
|
2415
|
+
!faulted(detail.run),
|
|
2416
|
+
);
|
|
2417
|
+
const nativeSpans = details
|
|
2418
|
+
.filter(
|
|
2419
|
+
(detail) =>
|
|
2420
|
+
detail.run.kind === "span" &&
|
|
2421
|
+
detail.selection &&
|
|
2422
|
+
detail.run.enc !== "default" &&
|
|
2423
|
+
reference.from > detail.run.start &&
|
|
2424
|
+
reference.to <= detail.contentEnd &&
|
|
2425
|
+
!faulted(detail.run),
|
|
2426
|
+
)
|
|
2427
|
+
.sort((a, b) => a.run.start - b.run.start);
|
|
2428
|
+
const outer = nativeSpans[0];
|
|
2429
|
+
if (!nativeDiv && !outer) continue;
|
|
2430
|
+
if (!edits.some((edit) => edit.from <= reference.from && edit.to >= reference.to))
|
|
2431
|
+
edits.push({ from: reference.from, to: reference.to, text: "" });
|
|
2432
|
+
if (nativeDiv) continue;
|
|
2433
|
+
if (!restoredReferences.has(outer.run.end)) restoredReferences.set(outer.run.end, []);
|
|
2434
|
+
restoredReferences.get(outer.run.end).push(reference);
|
|
2435
|
+
}
|
|
2436
|
+
for (const [at, notes] of restoredReferences)
|
|
2437
|
+
edits.push({
|
|
2438
|
+
from: at,
|
|
2439
|
+
to: at,
|
|
2440
|
+
text: notes
|
|
2441
|
+
.sort((a, b) => a.from - b.from)
|
|
2442
|
+
.map((reference) => reference.text)
|
|
2443
|
+
.join(""),
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2263
2446
|
let markdown = text;
|
|
2264
2447
|
for (const edit of edits.sort((a, b) => b.from - a.from))
|
|
2265
2448
|
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2266
2449
|
// Models retain authored structure; a native script may draw the boundaries
|
|
2267
2450
|
// as glyphs which must not be reparsed as Roman delimiters.
|
|
2268
|
-
const rewritten =
|
|
2451
|
+
const rewritten = [
|
|
2452
|
+
...scanVerse(markdown, { checkWorks: false }).blocks.map((block) => ({
|
|
2453
|
+
...block,
|
|
2454
|
+
inline: false,
|
|
2455
|
+
})),
|
|
2456
|
+
...scanSpans(markdown)
|
|
2457
|
+
.spans.filter((span) => span.verseModel)
|
|
2458
|
+
.map((span) => ({
|
|
2459
|
+
...span,
|
|
2460
|
+
inline: true,
|
|
2461
|
+
body: markdown.slice(span.start + 1, span.contentEnd),
|
|
2462
|
+
bodyStart: span.start + 1,
|
|
2463
|
+
bodyEnd: span.contentEnd,
|
|
2464
|
+
})),
|
|
2465
|
+
].sort((a, b) => a.bodyStart - b.bodyStart);
|
|
2269
2466
|
const pairedEdits = [];
|
|
2270
2467
|
for (const [index, poem] of poems.entries()) {
|
|
2271
2468
|
const detail = details.find(
|
|
2272
|
-
(detail) =>
|
|
2469
|
+
(detail) =>
|
|
2470
|
+
detail.run.kind === (poem.inline ? "span" : "verse") &&
|
|
2471
|
+
detail.run.start === poem.start,
|
|
2273
2472
|
);
|
|
2274
2473
|
if (!detail || detail.run.enc === "default" || !detail.selection || faulted(detail.run))
|
|
2275
2474
|
continue;
|
|
2276
2475
|
const block = rewritten[index];
|
|
2277
2476
|
if (!block) continue;
|
|
2278
|
-
const base =
|
|
2477
|
+
const base = poem.bodyStart;
|
|
2279
2478
|
const applyLine = (line) => {
|
|
2280
2479
|
const patches = [];
|
|
2281
2480
|
for (const edit of edits) {
|
|
@@ -2327,9 +2526,16 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2327
2526
|
return true;
|
|
2328
2527
|
});
|
|
2329
2528
|
const model = structuredClone(poem.model);
|
|
2529
|
+
for (const note of model.notes ?? []) {
|
|
2530
|
+
note.romanText = romanLine(note.text);
|
|
2531
|
+
note.text = "";
|
|
2532
|
+
}
|
|
2330
2533
|
for (const group of model.groups) {
|
|
2331
2534
|
if (group.cantoBreak) {
|
|
2332
|
-
group.text =
|
|
2535
|
+
group.text =
|
|
2536
|
+
detail.plan?.pieces.find(
|
|
2537
|
+
(piece) => piece.boundary === "canto" && piece.offset === base + group.from,
|
|
2538
|
+
)?.to ?? "";
|
|
2333
2539
|
continue;
|
|
2334
2540
|
}
|
|
2335
2541
|
for (const line of group.lines) {
|
|
@@ -2346,21 +2552,58 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2346
2552
|
});
|
|
2347
2553
|
}
|
|
2348
2554
|
}
|
|
2555
|
+
if (poem.inline) {
|
|
2556
|
+
const values = Object.fromEntries(
|
|
2557
|
+
block.attrs.filter(([key]) => !["id", "class"].includes(key)),
|
|
2558
|
+
);
|
|
2559
|
+
const classes = (block.attrs.find(([key]) => key === "class")?.[1] ?? "")
|
|
2560
|
+
.split(/\s+/)
|
|
2561
|
+
.filter((name) => name && name !== "verse");
|
|
2562
|
+
const attributes = attributeText({
|
|
2563
|
+
lang: values.lang,
|
|
2564
|
+
id: block.id,
|
|
2565
|
+
classes,
|
|
2566
|
+
values: Object.entries(values).filter(([key]) => key !== "lang"),
|
|
2567
|
+
});
|
|
2568
|
+
const fragments = model.groups.flatMap((group) =>
|
|
2569
|
+
group.cantoBreak ? [{ from: group.from, text: group.text ?? "" }] : group.lines,
|
|
2570
|
+
);
|
|
2571
|
+
// Consecutive boundaries can stand between logical lines. They
|
|
2572
|
+
// still contribute controls even when no line owns their offsets.
|
|
2573
|
+
for (const piece of detail.plan?.pieces ?? []) {
|
|
2574
|
+
if (!piece.boundary || piece.boundary === "canto") continue;
|
|
2575
|
+
const offset = piece.anchor - base;
|
|
2576
|
+
if (
|
|
2577
|
+
model.groups.some((group) =>
|
|
2578
|
+
group.lines.some((line) => offset >= line.from && offset <= line.to),
|
|
2579
|
+
)
|
|
2580
|
+
)
|
|
2581
|
+
continue;
|
|
2582
|
+
fragments.push({ from: offset, text: escapeRoman(piece.to, false) });
|
|
2583
|
+
}
|
|
2584
|
+
const native = fragments
|
|
2585
|
+
.sort((a, b) => a.from - b.from)
|
|
2586
|
+
.map((fragment) => fragment.text)
|
|
2587
|
+
.join("");
|
|
2588
|
+
pairedEdits.push({
|
|
2589
|
+
from: block.start,
|
|
2590
|
+
to: block.end,
|
|
2591
|
+
text: `[${native}]{${attributes}}`,
|
|
2592
|
+
});
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2349
2595
|
const classes = [SCRIPT_CLASS, ...(detail.face ? [detail.face.class] : [])];
|
|
2350
2596
|
const attributes = {
|
|
2351
2597
|
...(detail.run.tag ? { lang: detail.run.tag } : {}),
|
|
2352
2598
|
...(detail.face?.direction === "rtl" ? { dir: "rtl" } : {}),
|
|
2353
2599
|
...(search ? { "data-pagefind-ignore": "true" } : {}),
|
|
2354
2600
|
};
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
pairedEdits.push({
|
|
2359
|
-
at: rewrittenStarts[block.start + 1],
|
|
2360
|
-
text: `<!--verse-pair:${verses.size}-->\n`,
|
|
2361
|
-
});
|
|
2601
|
+
const marker = `<!--verse-pair:${verses.size + 1}-->${poem.inline ? "" : "\n"}`;
|
|
2602
|
+
const body = marker + block.body;
|
|
2603
|
+
verses.set(body, { model, classes, attributes, inline: poem.inline });
|
|
2604
|
+
pairedEdits.push({ from: block.bodyStart, to: block.bodyStart, text: marker });
|
|
2362
2605
|
}
|
|
2363
|
-
for (const edit of pairedEdits.sort((a, b) => b.
|
|
2364
|
-
markdown = markdown.slice(0, edit.
|
|
2606
|
+
for (const edit of pairedEdits.sort((a, b) => b.from - a.from))
|
|
2607
|
+
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2365
2608
|
return { markdown, errors: diagnosticColumns(text, errors), scrolls, verses };
|
|
2366
2609
|
}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { blockTokens } from "./markdown-blocks.mjs";
|
|
46
|
+
import { verseRegions } from "./verse-regions.mjs";
|
|
46
47
|
|
|
47
48
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
48
49
|
|
|
@@ -114,7 +115,7 @@ function keyColumns(line, key) {
|
|
|
114
115
|
* Read every `scroll` fence in a source, and every fault in its opening.
|
|
115
116
|
*
|
|
116
117
|
* A fence inside a block quote or an HTML block is not read, and neither is a
|
|
117
|
-
* fence quoted inside another fence.
|
|
118
|
+
* fence quoted inside another fence. Verse lines are inline content.
|
|
118
119
|
*
|
|
119
120
|
* @param {string} source - Markdown.
|
|
120
121
|
* @returns {{blocks: Scroll[], errors: Array<{line: number, column: number, message: string}>}}
|
|
@@ -125,8 +126,12 @@ export function scanScrolls(source) {
|
|
|
125
126
|
const blocks = [];
|
|
126
127
|
const errors = [];
|
|
127
128
|
if (!/^ *(?:`{3,}|~{3,}) *scroll(?:\s|$)/m.test(text)) return { blocks, errors };
|
|
128
|
-
const lines = text.split(
|
|
129
|
-
const literalLines = new Set(
|
|
129
|
+
const lines = text.split(/\r?\n/);
|
|
130
|
+
const literalLines = new Set(
|
|
131
|
+
verseRegions(text).flatMap((region) =>
|
|
132
|
+
Array.from({ length: region.end - region.start + 1 }, (_, i) => region.start + i),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
130
135
|
for (const token of blockTokens(text)) {
|
|
131
136
|
if (!token.map || !["blockquote_open", "html_block"].includes(token.type)) continue;
|
|
132
137
|
for (let line = token.map[0]; line < token.map[1]; line++) literalLines.add(line);
|