@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
|
@@ -34,9 +34,8 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Span, div, verse and scroll scanners retain original UTF-16 source offsets.
|
|
36
36
|
* Public diagnostics count Unicode code-point columns. A scroll's face must
|
|
37
|
-
* belong to a script written vertically
|
|
38
|
-
*
|
|
39
|
-
* its line boundary before a stanza or canto.
|
|
37
|
+
* belong to a script written vertically. An encoded verse derives native and
|
|
38
|
+
* Roman presentations from one authored text and retains explicit boundaries.
|
|
40
39
|
* @module
|
|
41
40
|
*/
|
|
42
41
|
|
|
@@ -44,8 +43,9 @@ import MarkdownIt from "markdown-it";
|
|
|
44
43
|
import { blockTokens } from "./markdown-blocks.mjs";
|
|
45
44
|
|
|
46
45
|
import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
|
|
47
|
-
import { scanVerse
|
|
46
|
+
import { scanVerse } from "./content-verse.mjs";
|
|
48
47
|
import { scanSpans } from "./content-spans.mjs";
|
|
48
|
+
import { footnoteLabels, footnoteReferences } from "./content-footnotes.mjs";
|
|
49
49
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
50
50
|
import { directionsFromRecords } from "./script-direction.mjs";
|
|
51
51
|
import { isLanguageTag } from "./language-tag.mjs";
|
|
@@ -127,7 +127,7 @@ const CONFIG = "package-build.config.yaml";
|
|
|
127
127
|
*/
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* One span, div, verse
|
|
130
|
+
* One span, div, verse div or scroll fence carrying run attributes.
|
|
131
131
|
* Values combine explicit choices, class-only inheritance, encoding-document
|
|
132
132
|
* language/font defaults and the Roman default encoding.
|
|
133
133
|
*
|
|
@@ -233,7 +233,7 @@ function withinOneEdit(a, b) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/** The word each construct is called in a finding. */
|
|
236
|
-
const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse
|
|
236
|
+
const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse div" });
|
|
237
237
|
|
|
238
238
|
/** Line starts of a source, for offsets to positions. */
|
|
239
239
|
function lineStarts(text) {
|
|
@@ -835,9 +835,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
835
835
|
);
|
|
836
836
|
const errors = [];
|
|
837
837
|
const warnings = [];
|
|
838
|
-
const footnotes =
|
|
839
|
-
[...text.matchAll(/^ {0,3}\[\^([^\]\s]+)\]:/gm)].map((match) => match[1]),
|
|
840
|
-
);
|
|
838
|
+
const footnotes = footnoteLabels(text);
|
|
841
839
|
/** @param {{line: number, column: number}} at @param {string} message */
|
|
842
840
|
const report = (at, message) => errors.push({ line: at.line, column: at.column, message });
|
|
843
841
|
/** @param {Record<string, string>} values */
|
|
@@ -891,6 +889,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
891
889
|
const collect = (offset, body) => {
|
|
892
890
|
const scanned = offset === 0 && syntax ? syntax(body).blocks : scanBlocks(body);
|
|
893
891
|
for (const block of scanned.blocks) {
|
|
892
|
+
if (block.name === "verse" || block.classes?.includes("verse")) continue;
|
|
894
893
|
const first = offset + block.start;
|
|
895
894
|
const last = offset + block.end;
|
|
896
895
|
const opening = lines[first];
|
|
@@ -912,7 +911,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
912
911
|
for (const key of Object.keys(keys))
|
|
913
912
|
report(
|
|
914
913
|
at(key, "key"),
|
|
915
|
-
`\`${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`,
|
|
916
915
|
);
|
|
917
916
|
} else {
|
|
918
917
|
divs.set(first, block);
|
|
@@ -949,7 +948,6 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
949
948
|
for (const poem of scanVerse(text).blocks) {
|
|
950
949
|
poems.set(poem.start, poem);
|
|
951
950
|
const keys = runKeys(poem.attributes);
|
|
952
|
-
if (!Object.keys(keys).length && !poem.classes.includes(NATIVEFONT_CLASS)) continue;
|
|
953
951
|
const opening = lines[poem.start];
|
|
954
952
|
constructs.push({
|
|
955
953
|
kind: "verse",
|
|
@@ -1055,11 +1053,6 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1055
1053
|
nativeAt(construct),
|
|
1056
1054
|
"`.nativefont` is not an accepted class; select an encoding with `enc=`",
|
|
1057
1055
|
);
|
|
1058
|
-
if (native(construct) && construct.kind === "verse" && hasColumns(construct.node))
|
|
1059
|
-
report(
|
|
1060
|
-
construct.at("enc", "value"),
|
|
1061
|
-
"a verse with cols>1 selects encoding on individual runs, not the fence",
|
|
1062
|
-
);
|
|
1063
1056
|
|
|
1064
1057
|
if (lang !== undefined && !Object.hasOwn(languages, lang)) {
|
|
1065
1058
|
if (isLanguageTag(lang)) {
|
|
@@ -1202,48 +1195,98 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1202
1195
|
}
|
|
1203
1196
|
|
|
1204
1197
|
const verseMarkers = new Map();
|
|
1205
|
-
const verseContexts =
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
start + part.markerStart,
|
|
1229
|
-
start + part.markerStart + part.markerLength,
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
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: span.text,
|
|
1206
|
+
model: span.verseModel,
|
|
1207
|
+
})),
|
|
1208
|
+
];
|
|
1209
|
+
for (const poem of verseContexts) {
|
|
1210
|
+
const bodyStart = poem.bodyStart;
|
|
1211
|
+
for (const [from, to] of poem.model.layout)
|
|
1212
|
+
verseMarkers.set(bodyStart + from, bodyStart + to);
|
|
1213
|
+
for (const match of poem.body.matchAll(WIKILINK)) {
|
|
1214
|
+
const bar = match[1].indexOf("|");
|
|
1215
|
+
if (bar < 0) {
|
|
1216
|
+
verseMarkers.set(
|
|
1217
|
+
bodyStart + match.index,
|
|
1218
|
+
bodyStart + match.index + match[0].length,
|
|
1219
|
+
);
|
|
1220
|
+
continue;
|
|
1232
1221
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1222
|
+
verseMarkers.set(bodyStart + match.index, bodyStart + match.index + 3 + bar);
|
|
1223
|
+
verseMarkers.set(
|
|
1224
|
+
bodyStart + match.index + match[0].indexOf("]]"),
|
|
1225
|
+
bodyStart + match.index + match[0].length,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
// Link destinations and footnote references remain Markdown; only a
|
|
1229
|
+
// link's visible label contributes text to the encoding projection.
|
|
1230
|
+
for (let at = 0; at < poem.body.length; at++) {
|
|
1231
|
+
if (poem.body[at] === "\\") {
|
|
1232
|
+
at++;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (poem.body[at] !== "[") continue;
|
|
1236
|
+
const end = closing(poem.body, at);
|
|
1237
|
+
if (end < 0) continue;
|
|
1238
|
+
if (poem.body[end + 1] === "(" && poem.body[at - 1] !== "!") {
|
|
1239
|
+
const past = closing(poem.body, end + 1);
|
|
1240
|
+
if (past < 0) continue;
|
|
1241
|
+
verseMarkers.set(bodyStart + at, bodyStart + at + 1);
|
|
1242
|
+
verseMarkers.set(bodyStart + end, bodyStart + past + 1);
|
|
1240
1243
|
}
|
|
1244
|
+
}
|
|
1241
1245
|
}
|
|
1242
1246
|
|
|
1247
|
+
const noteReferences = [
|
|
1248
|
+
...new Map(
|
|
1249
|
+
[
|
|
1250
|
+
...footnoteReferences(text, { definitions: footnotes }),
|
|
1251
|
+
...spans.flatMap((span) => {
|
|
1252
|
+
const label = referenceLabel(span.text);
|
|
1253
|
+
if (!label) return [];
|
|
1254
|
+
const base = span.start + 1 + label[0];
|
|
1255
|
+
return footnoteReferences(span.text.slice(label[0], label[1]), {
|
|
1256
|
+
definitions: footnotes,
|
|
1257
|
+
inline: true,
|
|
1258
|
+
}).map((reference) => ({
|
|
1259
|
+
...reference,
|
|
1260
|
+
from: base + reference.from,
|
|
1261
|
+
to: base + reference.to,
|
|
1262
|
+
}));
|
|
1263
|
+
}),
|
|
1264
|
+
...verseContexts.flatMap((region) =>
|
|
1265
|
+
footnoteReferences(region.body, { definitions: footnotes, inline: true }).map(
|
|
1266
|
+
(reference) => ({
|
|
1267
|
+
...reference,
|
|
1268
|
+
from: reference.from + region.bodyStart,
|
|
1269
|
+
to: reference.to + region.bodyStart,
|
|
1270
|
+
}),
|
|
1271
|
+
),
|
|
1272
|
+
),
|
|
1273
|
+
].map((reference) => [reference.from, reference]),
|
|
1274
|
+
).values(),
|
|
1275
|
+
];
|
|
1276
|
+
|
|
1243
1277
|
/** Nested spans and verse markers excluded from a run, by absolute offset. */
|
|
1244
1278
|
const holesIn = (from, to) =>
|
|
1245
1279
|
new Map([
|
|
1280
|
+
...noteReferences
|
|
1281
|
+
.filter((reference) => reference.from >= from && reference.from < to)
|
|
1282
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1246
1283
|
...[...verseMarkers].filter(([start]) => start >= from && start < to),
|
|
1284
|
+
...verseContexts
|
|
1285
|
+
.filter(
|
|
1286
|
+
(region) =>
|
|
1287
|
+
Number.isFinite(region.from) && region.from >= from && region.from < to,
|
|
1288
|
+
)
|
|
1289
|
+
.map((region) => [region.from, Math.min(region.to, to)]),
|
|
1247
1290
|
...active
|
|
1248
1291
|
.filter(
|
|
1249
1292
|
(construct) =>
|
|
@@ -1406,6 +1449,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1406
1449
|
selection: enc === undefined ? undefined : encodingFor(ctx, { enc, font, lang }),
|
|
1407
1450
|
holes: new Map(),
|
|
1408
1451
|
body: undefined,
|
|
1452
|
+
verseModel: construct.kind === "span" ? construct.node.verseModel : undefined,
|
|
1409
1453
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
1410
1454
|
reference: undefined,
|
|
1411
1455
|
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
@@ -1416,7 +1460,10 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1416
1460
|
detail.conventions = conventionsOf(detail, ctx);
|
|
1417
1461
|
details.push(detail);
|
|
1418
1462
|
const nonEnglishSpan =
|
|
1419
|
-
construct.kind === "span" &&
|
|
1463
|
+
construct.kind === "span" &&
|
|
1464
|
+
!construct.node.verseModel &&
|
|
1465
|
+
run.tag !== undefined &&
|
|
1466
|
+
!/^en(?:-|$)/i.test(run.tag);
|
|
1420
1467
|
const emphasis = construct.own.classes.filter(
|
|
1421
1468
|
(name) => name === "emph" || name === "strong",
|
|
1422
1469
|
);
|
|
@@ -1447,7 +1494,14 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1447
1494
|
[from, to] = detail.reference;
|
|
1448
1495
|
}
|
|
1449
1496
|
const inline = text.slice(from, to);
|
|
1450
|
-
detail.holes =
|
|
1497
|
+
detail.holes =
|
|
1498
|
+
label ?
|
|
1499
|
+
new Map(
|
|
1500
|
+
noteReferences
|
|
1501
|
+
.filter((reference) => reference.from >= from && reference.to <= to)
|
|
1502
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1503
|
+
)
|
|
1504
|
+
: holesIn(from, to);
|
|
1451
1505
|
if (nonEnglishSpan) {
|
|
1452
1506
|
const relative = new Map(
|
|
1453
1507
|
[...detail.holes].map(([from, to]) => [
|
|
@@ -1507,7 +1561,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1507
1561
|
said(
|
|
1508
1562
|
top,
|
|
1509
1563
|
token.info.trim().split(/\s/)[0] === "verse" ?
|
|
1510
|
-
"a verse
|
|
1564
|
+
"a verse div"
|
|
1511
1565
|
: "a code fence",
|
|
1512
1566
|
);
|
|
1513
1567
|
else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
|
|
@@ -1560,23 +1614,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1560
1614
|
}
|
|
1561
1615
|
return owners.get(detail);
|
|
1562
1616
|
};
|
|
1563
|
-
|
|
1564
|
-
if (detail.run.kind === "verse") return true;
|
|
1565
|
-
if (detail.run.kind !== "span") return false;
|
|
1566
|
-
const line = detail.run.line - 1;
|
|
1567
|
-
const poem = verseContexts.get(line);
|
|
1568
|
-
if (!poem) return false;
|
|
1569
|
-
const end = starts[line] + lines[line].length;
|
|
1570
|
-
if (detail.run.end > end) return false;
|
|
1571
|
-
const reading = verseReadings.get(line);
|
|
1572
|
-
const from =
|
|
1573
|
-
hasColumns(poem) ? starts[line] + (reading?.textColumn ?? 1) - 1 : starts[line];
|
|
1574
|
-
return (
|
|
1575
|
-
from <= detail.run.start &&
|
|
1576
|
-
/^[ \t.]*$/.test(text.slice(from, detail.run.start)) &&
|
|
1577
|
-
/^[ \t\r]*$/.test(text.slice(detail.run.end, end))
|
|
1578
|
-
);
|
|
1579
|
-
};
|
|
1617
|
+
|
|
1580
1618
|
const spanDetails = new Map(
|
|
1581
1619
|
details
|
|
1582
1620
|
.filter((detail) => detail.run.kind === "span")
|
|
@@ -1588,7 +1626,18 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1588
1626
|
const owned = details.filter((detail) => ownerOf(detail) === owner);
|
|
1589
1627
|
for (const detail of owned) detail.owner = owner;
|
|
1590
1628
|
const excluded = [
|
|
1629
|
+
...noteReferences
|
|
1630
|
+
.filter((reference) => reference.from >= bounds[0] && reference.from < bounds[1])
|
|
1631
|
+
.map((reference) => [reference.from, reference.to]),
|
|
1591
1632
|
...[...verseMarkers].filter(([start]) => start >= bounds[0] && start < bounds[1]),
|
|
1633
|
+
...verseContexts
|
|
1634
|
+
.filter(
|
|
1635
|
+
(region) =>
|
|
1636
|
+
Number.isFinite(region.from) &&
|
|
1637
|
+
region.from >= bounds[0] &&
|
|
1638
|
+
region.from < bounds[1],
|
|
1639
|
+
)
|
|
1640
|
+
.map((region) => [region.from, Math.min(region.to, bounds[1])]),
|
|
1592
1641
|
...owned.flatMap((detail) =>
|
|
1593
1642
|
detail.emphasis.map((delimiter) => [
|
|
1594
1643
|
delimiter.offset,
|
|
@@ -1604,13 +1653,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1604
1653
|
includeSpan: (span) =>
|
|
1605
1654
|
spanDetails.has(span.start) && ownerOf(spanDetails.get(span.start)) === owner,
|
|
1606
1655
|
};
|
|
1607
|
-
const
|
|
1656
|
+
const poem = verseContexts
|
|
1657
|
+
.filter((region) => region.bodyStart <= bounds[0] && region.bodyEnd >= bounds[1])
|
|
1658
|
+
.sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
1659
|
+
const verse = poem !== undefined;
|
|
1608
1660
|
const projection =
|
|
1609
1661
|
verse ?
|
|
1610
1662
|
plainRomanVerse(text, {
|
|
1611
1663
|
...projectionOptions,
|
|
1612
|
-
|
|
1613
|
-
|
|
1664
|
+
model: poem.model,
|
|
1665
|
+
modelOffset: poem.bodyStart,
|
|
1614
1666
|
})
|
|
1615
1667
|
: plainRomanRun(text, projectionOptions);
|
|
1616
1668
|
const before = errors.length;
|
|
@@ -1628,18 +1680,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1628
1680
|
);
|
|
1629
1681
|
for (const problem of projection.errors ?? [])
|
|
1630
1682
|
report(positionOf(starts, problem.offset), problem.message);
|
|
1631
|
-
|
|
1632
|
-
for (const match of projection.text.matchAll(/\//g)) {
|
|
1633
|
-
if (
|
|
1634
|
-
projection.text.lastIndexOf("⟦", match.index) >
|
|
1635
|
-
projection.text.lastIndexOf("⟧", match.index)
|
|
1636
|
-
)
|
|
1637
|
-
continue;
|
|
1638
|
-
report(
|
|
1639
|
-
positionOf(starts, projection.offsets[match.index]),
|
|
1640
|
-
"a verse fence uses authored line endings for verse boundaries; write no literal `/` inside it",
|
|
1641
|
-
);
|
|
1642
|
-
}
|
|
1683
|
+
|
|
1643
1684
|
const book = ctx.wordBooks?.get(owner.run.lang);
|
|
1644
1685
|
if (book) {
|
|
1645
1686
|
const mask = projection.text.split("");
|
|
@@ -1799,7 +1840,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1799
1840
|
details.sort((a, b) => order(a.run, b.run));
|
|
1800
1841
|
errors.sort(order);
|
|
1801
1842
|
warnings.sort(order);
|
|
1802
|
-
return { runs, details, errors, warnings, spans, divs, poems };
|
|
1843
|
+
return { runs, details, errors, warnings, spans, divs, poems, references: noteReferences };
|
|
1803
1844
|
}
|
|
1804
1845
|
|
|
1805
1846
|
/**
|
|
@@ -1825,7 +1866,7 @@ function diagnosticColumns(source, findings) {
|
|
|
1825
1866
|
}
|
|
1826
1867
|
|
|
1827
1868
|
/**
|
|
1828
|
-
* Find the spans, divs and verse
|
|
1869
|
+
* Find the spans, divs and verse divs marked with `lang=`, `font=` or
|
|
1829
1870
|
* `enc=`, and every fault in their markup and their text.
|
|
1830
1871
|
*
|
|
1831
1872
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
@@ -2026,11 +2067,10 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
2026
2067
|
|
|
2027
2068
|
/**
|
|
2028
2069
|
* Rewrite text marked with `lang=`, `font=` and `enc=` into the plain span,
|
|
2029
|
-
* div and verse grammar the rest of each chain renders
|
|
2070
|
+
* div and verse grammar the rest of each chain renders.
|
|
2030
2071
|
*
|
|
2031
|
-
* - **
|
|
2032
|
-
*
|
|
2033
|
-
* lines and source positions remain unchanged.
|
|
2072
|
+
* - **Verse layout markers** stay outside encoded text. A shared logical
|
|
2073
|
+
* structure retains authored source positions and both presentations.
|
|
2034
2074
|
* - **The enc, if any, is applied first.** Characters Markdown would read are
|
|
2035
2075
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
2036
2076
|
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
@@ -2057,7 +2097,7 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
2057
2097
|
* scanner projections.
|
|
2058
2098
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
2059
2099
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
2060
|
-
* @returns {{markdown: string, errors: Array<{file?: string, line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>}}
|
|
2100
|
+
* @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}>}}
|
|
2061
2101
|
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
2062
2102
|
* rewritten scroll fence's layout, by its rewritten body.
|
|
2063
2103
|
*/
|
|
@@ -2067,11 +2107,12 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2067
2107
|
const text = String(source ?? "");
|
|
2068
2108
|
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
2069
2109
|
const scrolls = new Map();
|
|
2110
|
+
const verses = new Map();
|
|
2070
2111
|
if (!/\b(?:lang|font|enc)=|\bnativefont\b|\.(?:emph|strong)\b|⟦/.test(text))
|
|
2071
|
-
return { markdown: text, errors: [], scrolls };
|
|
2072
|
-
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
2112
|
+
return { markdown: text, errors: [], scrolls, verses };
|
|
2113
|
+
const { details, errors, references } = analyseRuns(text, ctx, syntax);
|
|
2073
2114
|
if (!details.length)
|
|
2074
|
-
return { markdown: text, errors: diagnosticColumns(text, errors), scrolls };
|
|
2115
|
+
return { markdown: text, errors: diagnosticColumns(text, errors), scrolls, verses };
|
|
2075
2116
|
const lines = text.split("\n");
|
|
2076
2117
|
const starts = lineStarts(text);
|
|
2077
2118
|
|
|
@@ -2090,6 +2131,33 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2090
2131
|
|
|
2091
2132
|
/** @type {Array<{from: number, to: number, text: string}>} */
|
|
2092
2133
|
const edits = [];
|
|
2134
|
+
const poems = [
|
|
2135
|
+
...scanVerse(text, { checkWorks: false }).blocks.map((block) => ({
|
|
2136
|
+
...block,
|
|
2137
|
+
inline: false,
|
|
2138
|
+
})),
|
|
2139
|
+
...scanSpans(text)
|
|
2140
|
+
.spans.filter((span) => span.verseModel)
|
|
2141
|
+
.map((span) => ({
|
|
2142
|
+
...span,
|
|
2143
|
+
inline: true,
|
|
2144
|
+
model: span.verseModel,
|
|
2145
|
+
body: span.text,
|
|
2146
|
+
bodyStart: span.start + 1,
|
|
2147
|
+
bodyEnd: span.contentEnd,
|
|
2148
|
+
})),
|
|
2149
|
+
].sort((a, b) => a.bodyStart - b.bodyStart);
|
|
2150
|
+
const logicalRanges = poems.flatMap((poem) => {
|
|
2151
|
+
const base = poem.bodyStart;
|
|
2152
|
+
return poem.model.groups.flatMap((group) =>
|
|
2153
|
+
group.cantoBreak ? [] : group.lines.map((line) => [base + line.from, base + line.to]),
|
|
2154
|
+
);
|
|
2155
|
+
});
|
|
2156
|
+
const rangesWithin = (from, to) => {
|
|
2157
|
+
const cuts = logicalRanges.flat().filter((at) => at > from && at < to);
|
|
2158
|
+
const points = [from, ...new Set(cuts.sort((a, b) => a - b)), to];
|
|
2159
|
+
return points.slice(1).map((past, index) => [points[index], past]);
|
|
2160
|
+
};
|
|
2093
2161
|
|
|
2094
2162
|
/**
|
|
2095
2163
|
* A scroll fence's body set as one line per paragraph, and its layout.
|
|
@@ -2207,8 +2275,11 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2207
2275
|
for (const [from, to] of detail.reference ?
|
|
2208
2276
|
[detail.reference]
|
|
2209
2277
|
: outsideHoles(run.start + 1, contentEnd, detail.holes)) {
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2278
|
+
for (const [first, past] of rangesWithin(from, to)) {
|
|
2279
|
+
const set = setRange(text, first, past, detail, false, book);
|
|
2280
|
+
if (set !== text.slice(first, past))
|
|
2281
|
+
edits.push({ from: first, to: past, text: set });
|
|
2282
|
+
}
|
|
2212
2283
|
}
|
|
2213
2284
|
continue;
|
|
2214
2285
|
}
|
|
@@ -2239,9 +2310,12 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2239
2310
|
}
|
|
2240
2311
|
if (detail.plain && detail.body)
|
|
2241
2312
|
for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
|
|
2313
|
+
for (const [first, past] of rangesWithin(from, to)) {
|
|
2314
|
+
const lineStart = first === 0 || text[first - 1] === "\n";
|
|
2315
|
+
const set = setRange(text, first, past, detail, lineStart, book);
|
|
2316
|
+
if (set !== text.slice(first, past))
|
|
2317
|
+
edits.push({ from: first, to: past, text: set });
|
|
2318
|
+
}
|
|
2245
2319
|
}
|
|
2246
2320
|
}
|
|
2247
2321
|
|
|
@@ -2265,8 +2339,190 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2265
2339
|
}
|
|
2266
2340
|
}
|
|
2267
2341
|
|
|
2342
|
+
const restoredReferences = new Map();
|
|
2343
|
+
for (const reference of references) {
|
|
2344
|
+
const nativeDiv = details.find(
|
|
2345
|
+
(detail) =>
|
|
2346
|
+
detail.run.kind === "verse" &&
|
|
2347
|
+
detail.selection &&
|
|
2348
|
+
detail.run.enc !== "default" &&
|
|
2349
|
+
detail.body &&
|
|
2350
|
+
reference.from >= detail.body[0] &&
|
|
2351
|
+
reference.to <= detail.body[1] &&
|
|
2352
|
+
!faulted(detail.run),
|
|
2353
|
+
);
|
|
2354
|
+
const nativeSpans = details
|
|
2355
|
+
.filter(
|
|
2356
|
+
(detail) =>
|
|
2357
|
+
detail.run.kind === "span" &&
|
|
2358
|
+
detail.selection &&
|
|
2359
|
+
detail.run.enc !== "default" &&
|
|
2360
|
+
reference.from > detail.run.start &&
|
|
2361
|
+
reference.to <= detail.contentEnd &&
|
|
2362
|
+
!faulted(detail.run),
|
|
2363
|
+
)
|
|
2364
|
+
.sort((a, b) => a.run.start - b.run.start);
|
|
2365
|
+
const outer = nativeSpans[0];
|
|
2366
|
+
if (!nativeDiv && !outer) continue;
|
|
2367
|
+
if (!edits.some((edit) => edit.from <= reference.from && edit.to >= reference.to))
|
|
2368
|
+
edits.push({ from: reference.from, to: reference.to, text: "" });
|
|
2369
|
+
if (nativeDiv) continue;
|
|
2370
|
+
if (!restoredReferences.has(outer.run.end)) restoredReferences.set(outer.run.end, []);
|
|
2371
|
+
restoredReferences.get(outer.run.end).push(reference);
|
|
2372
|
+
}
|
|
2373
|
+
for (const [at, notes] of restoredReferences)
|
|
2374
|
+
edits.push({
|
|
2375
|
+
from: at,
|
|
2376
|
+
to: at,
|
|
2377
|
+
text: notes
|
|
2378
|
+
.sort((a, b) => a.from - b.from)
|
|
2379
|
+
.map((reference) => reference.text)
|
|
2380
|
+
.join(""),
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2268
2383
|
let markdown = text;
|
|
2269
2384
|
for (const edit of edits.sort((a, b) => b.from - a.from))
|
|
2270
2385
|
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2271
|
-
|
|
2386
|
+
// Models retain authored structure; a native script may draw the boundaries
|
|
2387
|
+
// as glyphs which must not be reparsed as Roman delimiters.
|
|
2388
|
+
const rewritten = [
|
|
2389
|
+
...scanVerse(markdown, { checkWorks: false }).blocks.map((block) => ({
|
|
2390
|
+
...block,
|
|
2391
|
+
inline: false,
|
|
2392
|
+
})),
|
|
2393
|
+
...scanSpans(markdown)
|
|
2394
|
+
.spans.filter((span) => span.verseModel)
|
|
2395
|
+
.map((span) => ({
|
|
2396
|
+
...span,
|
|
2397
|
+
inline: true,
|
|
2398
|
+
body: span.text,
|
|
2399
|
+
bodyStart: span.start + 1,
|
|
2400
|
+
bodyEnd: span.contentEnd,
|
|
2401
|
+
})),
|
|
2402
|
+
].sort((a, b) => a.bodyStart - b.bodyStart);
|
|
2403
|
+
const pairedEdits = [];
|
|
2404
|
+
for (const [index, poem] of poems.entries()) {
|
|
2405
|
+
const detail = details.find(
|
|
2406
|
+
(detail) =>
|
|
2407
|
+
detail.run.kind === (poem.inline ? "span" : "verse") &&
|
|
2408
|
+
detail.run.start === poem.start,
|
|
2409
|
+
);
|
|
2410
|
+
if (!detail || detail.run.enc === "default" || !detail.selection || faulted(detail.run))
|
|
2411
|
+
continue;
|
|
2412
|
+
const block = rewritten[index];
|
|
2413
|
+
if (!block) continue;
|
|
2414
|
+
const base = poem.bodyStart;
|
|
2415
|
+
const applyLine = (line) => {
|
|
2416
|
+
const patches = [];
|
|
2417
|
+
for (const edit of edits) {
|
|
2418
|
+
const positions = line.offsets.flatMap((offset, index) => {
|
|
2419
|
+
const sourceOffset = base + offset;
|
|
2420
|
+
return sourceOffset >= edit.from && sourceOffset < edit.to ? [index] : [];
|
|
2421
|
+
});
|
|
2422
|
+
if (positions.length)
|
|
2423
|
+
patches.push({ from: positions[0], to: positions.at(-1) + 1, text: edit.text });
|
|
2424
|
+
else if (edit.from === edit.to && edit.from === base + line.to)
|
|
2425
|
+
patches.push({ from: line.text.length, to: line.text.length, text: edit.text });
|
|
2426
|
+
}
|
|
2427
|
+
let result = line.text;
|
|
2428
|
+
for (const edit of patches.sort((a, b) => b.from - a.from))
|
|
2429
|
+
result = result.slice(0, edit.from) + edit.text + result.slice(edit.to);
|
|
2430
|
+
return result.replace(/\r?\n[ ]*/g, "").trim();
|
|
2431
|
+
};
|
|
2432
|
+
const rewriteSpanAttributes = (source, rewrite) => {
|
|
2433
|
+
const changes = scanSpans(source).spans.flatMap((span) => {
|
|
2434
|
+
const attrs = parseExtensionAttributes(
|
|
2435
|
+
source.slice(span.contentEnd + 2, span.end - 1),
|
|
2436
|
+
);
|
|
2437
|
+
if (!rewrite(attrs, span)) return [];
|
|
2438
|
+
return [
|
|
2439
|
+
{
|
|
2440
|
+
from: span.contentEnd + 2,
|
|
2441
|
+
to: span.end - 1,
|
|
2442
|
+
text: attributeText({
|
|
2443
|
+
lang: attrs.values.lang,
|
|
2444
|
+
id: attrs.id,
|
|
2445
|
+
classes: attrs.classes,
|
|
2446
|
+
values: Object.entries(attrs.values).filter(([key]) => key !== "lang"),
|
|
2447
|
+
}),
|
|
2448
|
+
},
|
|
2449
|
+
];
|
|
2450
|
+
});
|
|
2451
|
+
let result = source;
|
|
2452
|
+
for (const change of changes.sort((a, b) => b.from - a.from))
|
|
2453
|
+
result = result.slice(0, change.from) + change.text + result.slice(change.to);
|
|
2454
|
+
return result;
|
|
2455
|
+
};
|
|
2456
|
+
const romanLine = (source) =>
|
|
2457
|
+
rewriteSpanAttributes(source, (attrs) => {
|
|
2458
|
+
const lang = attrs.values.lang;
|
|
2459
|
+
if (lang !== undefined) attrs.values.lang = ctx.languages?.[lang]?.tag ?? lang;
|
|
2460
|
+
delete attrs.values.enc;
|
|
2461
|
+
delete attrs.values.font;
|
|
2462
|
+
attrs.id = "";
|
|
2463
|
+
return true;
|
|
2464
|
+
});
|
|
2465
|
+
const model = structuredClone(poem.model);
|
|
2466
|
+
for (const note of model.notes ?? []) {
|
|
2467
|
+
note.romanText = romanLine(note.text);
|
|
2468
|
+
note.text = "";
|
|
2469
|
+
}
|
|
2470
|
+
for (const group of model.groups) {
|
|
2471
|
+
if (group.cantoBreak) {
|
|
2472
|
+
group.text = detail.selection.encoding.controls.verseCanto ?? "";
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
for (const line of group.lines) {
|
|
2476
|
+
line.romanText = romanLine(line.text);
|
|
2477
|
+
const ids = new Set(
|
|
2478
|
+
scanSpans(line.text)
|
|
2479
|
+
.spans.map((span) => span.id)
|
|
2480
|
+
.filter(Boolean),
|
|
2481
|
+
);
|
|
2482
|
+
line.text = rewriteSpanAttributes(applyLine(line), (attrs, span) => {
|
|
2483
|
+
if (!span.id || ids.has(span.id)) return false;
|
|
2484
|
+
attrs.id = "";
|
|
2485
|
+
return true;
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (poem.inline) {
|
|
2490
|
+
const values = Object.fromEntries(
|
|
2491
|
+
block.attrs.filter(([key]) => !["id", "class"].includes(key)),
|
|
2492
|
+
);
|
|
2493
|
+
const classes = (block.attrs.find(([key]) => key === "class")?.[1] ?? "")
|
|
2494
|
+
.split(/\s+/)
|
|
2495
|
+
.filter((name) => name && name !== "verse");
|
|
2496
|
+
const attributes = attributeText({
|
|
2497
|
+
lang: values.lang,
|
|
2498
|
+
id: block.id,
|
|
2499
|
+
classes,
|
|
2500
|
+
values: Object.entries(values).filter(([key]) => key !== "lang"),
|
|
2501
|
+
});
|
|
2502
|
+
const native = model.groups
|
|
2503
|
+
.flatMap((group) =>
|
|
2504
|
+
group.cantoBreak ? [group.text ?? ""] : group.lines.map((line) => line.text),
|
|
2505
|
+
)
|
|
2506
|
+
.join("");
|
|
2507
|
+
pairedEdits.push({
|
|
2508
|
+
from: block.start,
|
|
2509
|
+
to: block.end,
|
|
2510
|
+
text: `[${native}]{${attributes}}`,
|
|
2511
|
+
});
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
const classes = [SCRIPT_CLASS, ...(detail.face ? [detail.face.class] : [])];
|
|
2515
|
+
const attributes = {
|
|
2516
|
+
...(detail.run.tag ? { lang: detail.run.tag } : {}),
|
|
2517
|
+
...(detail.face?.direction === "rtl" ? { dir: "rtl" } : {}),
|
|
2518
|
+
...(search ? { "data-pagefind-ignore": "true" } : {}),
|
|
2519
|
+
};
|
|
2520
|
+
const marker = `<!--verse-pair:${verses.size + 1}-->${poem.inline ? "" : "\n"}`;
|
|
2521
|
+
const body = marker + block.body;
|
|
2522
|
+
verses.set(body, { model, classes, attributes, inline: poem.inline });
|
|
2523
|
+
pairedEdits.push({ from: block.bodyStart, to: block.bodyStart, text: marker });
|
|
2524
|
+
}
|
|
2525
|
+
for (const edit of pairedEdits.sort((a, b) => b.from - a.from))
|
|
2526
|
+
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2527
|
+
return { markdown, errors: diagnosticColumns(text, errors), scrolls, verses };
|
|
2272
2528
|
}
|