@heroiclands/package-build 25.4.0 → 25.5.1
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 +30 -0
- package/docs/authoring/links-and-markup.md +10 -3
- package/docs/commands.md +6 -0
- package/docs/encoding-core.md +18 -4
- package/docs/guides/writing-in-invented-scripts.md +25 -6
- package/docs/reference/format-details.md +4 -5
- package/engine/content-footnotes.mjs +9 -0
- package/engine/content-scripts.mjs +104 -23
- package/engine/content-scroll.mjs +8 -3
- package/engine/content-spans.mjs +35 -2
- package/engine/content-verse.mjs +27 -15
- package/engine/encoding-standard.mjs +1 -0
- package/engine/encoding-template.mjs +5 -2
- package/engine/encoding.mjs +19 -14
- package/engine/markdownlint-run.mjs +202 -5
- package/engine/roman-characters.mjs +32 -10
- package/engine/roman-run.mjs +3 -0
- package/engine/roman-verse.mjs +1 -0
- package/engine/verse-format.mjs +25 -13
- package/engine/verse-regions.mjs +9 -0
- package/package.json +1 -1
- package/types/engine/content-scroll.d.mts +1 -1
- package/types/engine/content-verse.d.mts +7 -2
- package/types/engine/roman-run.d.mts +3 -1
- package/types/engine/verse-format.d.mts +3 -2
- package/types/engine/verse-regions.d.mts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 25.5.1
|
|
4
|
+
|
|
5
|
+
**Builds and commands**
|
|
6
|
+
|
|
7
|
+
- `package-build markdown` checks a repository of any size, rather than crashing on large ones.
|
|
8
|
+
|
|
9
|
+
## 25.5.0
|
|
10
|
+
|
|
11
|
+
**Languages and scripts**
|
|
12
|
+
|
|
13
|
+
- A `.`, `?`, `‼` or `!` placed at the start of a sentence in invented-language roman is reported at that mark.
|
|
14
|
+
|
|
15
|
+
**Verse**
|
|
16
|
+
|
|
17
|
+
- A `--- {.canto}` break keeps the stanza break before it and decodes back without adding an extra line ending.
|
|
18
|
+
- Inline `[…]{.verse}` keeps repeated and trailing `//` stanza marks alongside its `/` line marks and footnotes, including in files with CRLF line endings.
|
|
19
|
+
- A literal `\/` in verse stays inside its line and decodes with its backslash intact.
|
|
20
|
+
- A link inside verse is written in the link label's own language, not the verse's.
|
|
21
|
+
- A verse line that looks like a footnote definition or a code fence stays ordinary verse text instead of raising a footnote error or swallowing what follows.
|
|
22
|
+
- A scroll placed after verse renders and reports its own findings correctly, whichever line endings the file uses.
|
|
23
|
+
- A nested shorthand verse div takes the language of the verse around it and shows no stray attribute text on the page.
|
|
24
|
+
- Verse findings inside lists and block quotes point to the verse's opening mark, and leave out the column when the position is unknown.
|
|
25
|
+
- `package-build format` keeps block quotes that contain verse, aligns tables that quote verse, and gives each document one consistent line ending.
|
|
26
|
+
- `package-build markdown` leaves verse untouched while still checking and fixing the prose around it.
|
|
27
|
+
- The authoring guide describes verse scansion and the warnings for names as the build applies them.
|
|
28
|
+
|
|
29
|
+
**Upgrading**
|
|
30
|
+
|
|
31
|
+
- Move any `.`, `?`, `‼` or `!` that opens a sentence in invented-language roman to the end of that sentence.
|
|
32
|
+
|
|
3
33
|
## 25.4.0
|
|
4
34
|
|
|
5
35
|
**Verse**
|
|
@@ -598,7 +598,8 @@ indentation and source-line joining as a div. It takes ordinary span attributes,
|
|
|
598
598
|
including `lang=` and `enc=`. An unencoded quotation draws Roman text with its
|
|
599
599
|
line and stanza layout. A concrete encoding draws only its script, continuously
|
|
600
600
|
inside the surrounding prose, like any encoded span. Its `/` and `//` reach the
|
|
601
|
-
encoder as verse controls, so a script with verse signs draws them.
|
|
601
|
+
encoder as verse controls, so a script with verse signs draws them. Consecutive
|
|
602
|
+
stanza endings and a trailing stanza ending each retain their control. Only divs
|
|
602
603
|
have paired script and Roman columns.
|
|
603
604
|
Inline verse has no `work=`, `form=`, `scan=`, stanza labels or canto breaks.
|
|
604
605
|
Directions, speaker labels, links, names and shared note footnotes work inside it.
|
|
@@ -663,8 +664,14 @@ The captain raises the lamp.
|
|
|
663
664
|
The heading appears once across an encoded div's presentations. Its title is
|
|
664
665
|
heading text, separate from the letters passed to the encoder. A valid break
|
|
665
666
|
supplies the encoder's standard Roman `***` canto control. `package-build format`
|
|
666
|
-
|
|
667
|
-
|
|
667
|
+
preserves each verse div and quotation's text, spacing, delimiters and
|
|
668
|
+
attributes while formatting surrounding prose. Quote and list
|
|
669
|
+
containers remain intact, table columns use inline verse's authored width, and
|
|
670
|
+
verse follows the same configured line endings as surrounding prose.
|
|
671
|
+
|
|
672
|
+
The stanza mark before a canto is retained; whitespace after its encoded
|
|
673
|
+
control adds no extra line mark. An encoded literal `\/` uses a separate
|
|
674
|
+
invisible mark and decodes to `\/`, keeping it distinct from a line ending.
|
|
668
675
|
|
|
669
676
|
#### Directions and speakers
|
|
670
677
|
|
package/docs/commands.md
CHANGED
|
@@ -2574,6 +2574,12 @@ overrides the rules and the ignore list; an option the file cannot mean is
|
|
|
2574
2574
|
refused as `file:line:column: error:` and nothing is linted. See
|
|
2575
2575
|
[Project setup](project-setup.md) for the accepted keys.
|
|
2576
2576
|
|
|
2577
|
+
Closed `.verse` div bodies and inline `.verse` quotation contents are excluded
|
|
2578
|
+
from Markdown rules and inline lint directives. Their authored bytes stay
|
|
2579
|
+
unchanged under `--fix`, including their line endings. The div's opening and
|
|
2580
|
+
closing lines and surrounding prose are still checked, with diagnostics at
|
|
2581
|
+
their original source locations. Verse markup validation remains active.
|
|
2582
|
+
|
|
2577
2583
|
**OPTIONS**
|
|
2578
2584
|
|
|
2579
2585
|
| Positional | Type | Default | Description |
|
package/docs/encoding-core.md
CHANGED
|
@@ -136,9 +136,15 @@ encoding has its Roman forms checked against the lettered one only.
|
|
|
136
136
|
English names under a language's `data.names.en` are not encoded and are not
|
|
137
137
|
checked against any encoding.
|
|
138
138
|
|
|
139
|
-
Every encoding inherits the shared standard's
|
|
139
|
+
Every encoding inherits the shared standard's Roman punctuation
|
|
140
140
|
mappings, verse and name controls, and literal boundaries. `‼` marks an
|
|
141
|
-
imperative and `!` an exclamation.
|
|
141
|
+
imperative and `!` an exclamation. Configured language runs require sentence
|
|
142
|
+
text before each terminal (`.`, `?`, `‼`, `!`). Validation reports an opening
|
|
143
|
+
terminal at its original source position, including escaped marks. Whitespace,
|
|
144
|
+
indentation and verse boundaries preserve sentence state; opening quotations
|
|
145
|
+
start their own sentence, and closing quotations contribute their enclosed
|
|
146
|
+
text to the surrounding sentence. Names, literal codage and independent
|
|
147
|
+
language children count as text. An explicit mapping can give a standard
|
|
142
148
|
source its own font glyph. Other punctuation needs its own mapping.
|
|
143
149
|
|
|
144
150
|
## Conversion API
|
|
@@ -195,8 +201,16 @@ of the opening, closing or escape control are quoted with U+E0115; decoding
|
|
|
195
201
|
unquotes them before reading their codes. Unknown codes, malformed escapes,
|
|
196
202
|
nested markers and unmatched markers are errors.
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
The shared `\/` row writes the nonprinting U+E010F literal-slash mark and
|
|
205
|
+
decodes it to `\/`; `/` writes the distinct U+E010C line mark. Verse projection
|
|
206
|
+
retains the escape until encoding, including inside inline quotations. A row
|
|
207
|
+
program without a literal-slash row reports a source finding instead of
|
|
208
|
+
substituting a line control. Ordinary mappings cannot emit U+E010F.
|
|
209
|
+
|
|
210
|
+
`{verse: true}` handles line, stanza and canto structure. The encoder receives
|
|
211
|
+
`***` for a canto and decodes U+E010E back to `***`. Whitespace before the canto
|
|
212
|
+
still writes its stanza mark; following source whitespace adds no line mark.
|
|
213
|
+
The Markdown renderer additionally writes a boundary after every content line, including
|
|
200
214
|
the final line before a stanza or canto. Roman line layout is retained.
|
|
201
215
|
|
|
202
216
|
The internal row conformance fixtures cover their captured source programs;
|
|
@@ -99,8 +99,11 @@ are supplied automatically. A language that lists uppercase letters accepts
|
|
|
99
99
|
them; the build changes no authored case.
|
|
100
100
|
|
|
101
101
|
The terminals are `.` for a statement, `?` for a question, `‼` for an imperative
|
|
102
|
-
and `!` for an exclamation. None is required. A
|
|
103
|
-
|
|
102
|
+
and `!` for an exclamation. None is required. A terminal needs preceding text
|
|
103
|
+
in its sentence; `! Vesun` is a finding at `!`, while `Vesun!` is valid.
|
|
104
|
+
Whitespace, indentation, verse boundaries and a preceding terminal do not
|
|
105
|
+
supply sentence text. A comma pauses within a sentence; use it instead of `;`
|
|
106
|
+
or `:`, and use a period between complete sentences.
|
|
104
107
|
Quotation pairs alternate `«…»`, `‹…›`, then `«…»` at successive depths.
|
|
105
108
|
English double quotes, `<<` and `>>` are findings. A closing quote is not a
|
|
106
109
|
terminal for the enclosing sentence.
|
|
@@ -117,6 +120,9 @@ blank lines or `//` end stanzas, and `--- {.canto}` after a blank line ends a ca
|
|
|
117
120
|
slash is written `\/`. Indentation at a logical line boundary is layout,
|
|
118
121
|
not a punctuation character added to the encoded words.
|
|
119
122
|
|
|
123
|
+
An encoded literal slash has its own invisible mark, distinct from the verse
|
|
124
|
+
line mark, and decodes to `\/`. It never ends the logical line.
|
|
125
|
+
|
|
120
126
|
A `.name` child in a language run reaches the encoding with standard `{…}`
|
|
121
127
|
name boundaries. An `.emph` child contributes the same letters with emphasis
|
|
122
128
|
presentation. Both inherit the surrounding native writing when they state no
|
|
@@ -495,8 +501,10 @@ The keys are checked where they sit:
|
|
|
495
501
|
|
|
496
502
|
A `.verse` div holds inline Markdown and explicit verse structure. It is opaque
|
|
497
503
|
to lists, headings, quotes and code-block syntax. Its source newlines join unless
|
|
498
|
-
`/` ends a logical line. The authoring formatter preserves the
|
|
499
|
-
|
|
504
|
+
`/` ends a logical line. The authoring formatter preserves the div's text,
|
|
505
|
+
spacing and attributes, and also preserves inline `.verse` quotations. Quote
|
|
506
|
+
and list containers remain intact, table columns use the quotation's authored
|
|
507
|
+
width, and verse follows the same configured line endings as surrounding prose.
|
|
500
508
|
|
|
501
509
|
```markdown
|
|
502
510
|
::: {.verse lang=vyl font=semaron-kuriale enc=vylsemaronenc}
|
|
@@ -505,11 +513,19 @@ Lokazor lonak./
|
|
|
505
513
|
:::
|
|
506
514
|
```
|
|
507
515
|
|
|
516
|
+
Both `:::verse` and `::: {.verse}` inherit the enclosing div's language when
|
|
517
|
+
no `lang=` is stated. A link can select its label's language with trailing
|
|
518
|
+
attributes: `[[lore-longtunnel|Zebra Quartz]]{lang=en}`. This overrides the verse
|
|
519
|
+
language for the label while preserving the destination, including in an encoded
|
|
520
|
+
original's script and Roman columns.
|
|
521
|
+
|
|
508
522
|
Quoted verse is a span within prose, such as
|
|
509
523
|
`[Lokazor lonak zaron./..Lokamen lonak zaron.//]{.verse lang=vyl}`.
|
|
510
524
|
A concrete `enc=` sets a quotation in script only, continuously in the prose.
|
|
511
525
|
Its `/` and `//` become the encoding's verse marks; only a div has paired script
|
|
512
|
-
and Roman columns.
|
|
526
|
+
and Roman columns. Each stanza ending contributes its control, including
|
|
527
|
+
consecutive `//` marks and a final `//`. An unencoded quotation keeps Roman
|
|
528
|
+
line and stanza layout.
|
|
513
529
|
Quotations take no `work=`, `form=`, `scan=`, labels or canto breaks.
|
|
514
530
|
A div's canto separator can carry a heading and an anchor:
|
|
515
531
|
|
|
@@ -526,6 +542,9 @@ The title is shown once as heading text; the encoder receives its Roman `***`
|
|
|
526
542
|
canto control. Authored `***` inside verse is a finding. The canto stays within
|
|
527
543
|
the work and does not open a separate journal page.
|
|
528
544
|
|
|
545
|
+
The blank line before a canto retains its stanza mark in the encoded text.
|
|
546
|
+
Whitespace after the canto control adds no extra line mark.
|
|
547
|
+
|
|
529
548
|
### Versions of a work
|
|
530
549
|
|
|
531
550
|
`form=epic` records the author's literary form without inferring layout or metre.
|
|
@@ -615,7 +634,7 @@ the second beats and, in brackets, with all of them:
|
|
|
615
634
|
> ``this line has 4 beats (5 with secondary beats); the metre `hexameter` has 6``
|
|
616
635
|
|
|
617
636
|
**The check is author-side.** Readers see nothing of it: the rendered div
|
|
618
|
-
carries `data-
|
|
637
|
+
carries `data-scan` with the metre's name and nothing a reader sees. A div
|
|
619
638
|
without `scan=` is not counted. `scan=` is refused where it cannot mean
|
|
620
639
|
anything, each an error:
|
|
621
640
|
|
|
@@ -1994,9 +1994,9 @@ not.
|
|
|
1994
1994
|
|
|
1995
1995
|
##### Name-marking findings
|
|
1996
1996
|
|
|
1997
|
-
`package-build lint` reports
|
|
1998
|
-
|
|
1999
|
-
code, comments and embeds are outside
|
|
1997
|
+
`package-build lint` reports one name-marking warning in authored body prose.
|
|
1998
|
+
It leaves a valid package passing. Headings, frontmatter, tables, infoboxes,
|
|
1999
|
+
code, comments and embeds are outside this check.
|
|
2000
2000
|
|
|
2001
2001
|
1. A `.name` span without a `ref` whose whole text matches an indexed
|
|
2002
2002
|
`name.full` or a form its `name` lists, ignoring case, should be a link or
|
|
@@ -2025,8 +2025,7 @@ valid inside emphasis:
|
|
|
2025
2025
|
The name is `Joe Blow`; the surrounding phrase is emphasis.
|
|
2026
2026
|
|
|
2027
2027
|
Each finding is one warning at its authored `file:line:column`, naming the
|
|
2028
|
-
full name or explicit label.
|
|
2029
|
-
occurrence.
|
|
2028
|
+
full name or explicit label.
|
|
2030
2029
|
|
|
2031
2030
|
An optional `data-type` on a `.name` span states what the named thing is:
|
|
2032
2031
|
|
|
@@ -114,12 +114,21 @@ const CONTAINER_PREFIX = /^(?:[ \t]{0,3}>[ \t]?|[ \t]{0,3}(?:[-*+]|\d{1,9}[.)])[
|
|
|
114
114
|
*/
|
|
115
115
|
export function misplacedFootnoteDefinitions(source) {
|
|
116
116
|
const lines = String(source ?? "").split("\n");
|
|
117
|
+
const verseLines = new Set(
|
|
118
|
+
verseRegions(String(source ?? "")).flatMap((region) =>
|
|
119
|
+
Array.from({ length: region.end - region.start + 1 }, (_, i) => region.start + i),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
117
122
|
const errors = [];
|
|
118
123
|
let fence = null;
|
|
119
124
|
// Inside the continuation of a genuine top-level definition: a blank
|
|
120
125
|
// line or a four-space indent belongs to it, not to a new mistake.
|
|
121
126
|
let continuation = false;
|
|
122
127
|
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
if (verseLines.has(i)) {
|
|
129
|
+
continuation = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
123
132
|
const line = lines[i];
|
|
124
133
|
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
125
134
|
if (fence) {
|
|
@@ -1202,10 +1202,26 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1202
1202
|
.map((span) => ({
|
|
1203
1203
|
bodyStart: span.start + 1,
|
|
1204
1204
|
bodyEnd: span.contentEnd,
|
|
1205
|
-
body: span.
|
|
1205
|
+
body: text.slice(span.start + 1, span.contentEnd),
|
|
1206
1206
|
model: span.verseModel,
|
|
1207
1207
|
})),
|
|
1208
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
|
+
}));
|
|
1209
1225
|
for (const poem of verseContexts) {
|
|
1210
1226
|
const bodyStart = poem.bodyStart;
|
|
1211
1227
|
for (const [from, to] of poem.model.layout)
|
|
@@ -1281,6 +1297,9 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1281
1297
|
.filter((reference) => reference.from >= from && reference.from < to)
|
|
1282
1298
|
.map((reference) => [reference.from, reference.to]),
|
|
1283
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]),
|
|
1284
1303
|
...verseContexts
|
|
1285
1304
|
.filter(
|
|
1286
1305
|
(region) =>
|
|
@@ -1525,8 +1544,15 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1525
1544
|
|
|
1526
1545
|
const first = construct.line0 + 1;
|
|
1527
1546
|
const last = construct.last;
|
|
1528
|
-
|
|
1529
|
-
|
|
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;
|
|
1530
1556
|
detail.body = [from, to];
|
|
1531
1557
|
detail.holes = holesIn(from, to);
|
|
1532
1558
|
|
|
@@ -1561,7 +1587,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1561
1587
|
said(
|
|
1562
1588
|
top,
|
|
1563
1589
|
token.info.trim().split(/\s/)[0] === "verse" ?
|
|
1564
|
-
"a verse div"
|
|
1590
|
+
"a code fence named `verse`; write a `::: {.verse}` div"
|
|
1565
1591
|
: "a code fence",
|
|
1566
1592
|
);
|
|
1567
1593
|
else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
|
|
@@ -1629,7 +1655,12 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1629
1655
|
...noteReferences
|
|
1630
1656
|
.filter((reference) => reference.from >= bounds[0] && reference.from < bounds[1])
|
|
1631
1657
|
.map((reference) => [reference.from, reference.to]),
|
|
1632
|
-
...[...verseMarkers].filter(
|
|
1658
|
+
...[...verseMarkers].filter(
|
|
1659
|
+
([start]) =>
|
|
1660
|
+
start >= bounds[0] &&
|
|
1661
|
+
start < bounds[1] &&
|
|
1662
|
+
!referenceSpans.some((span) => start === span.start),
|
|
1663
|
+
),
|
|
1633
1664
|
...verseContexts
|
|
1634
1665
|
.filter(
|
|
1635
1666
|
(region) =>
|
|
@@ -1648,7 +1679,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1648
1679
|
const projectionOptions = {
|
|
1649
1680
|
from: bounds[0],
|
|
1650
1681
|
to: bounds[1],
|
|
1651
|
-
spans,
|
|
1682
|
+
spans: [...spans, ...referenceSpans],
|
|
1652
1683
|
exclude: excluded,
|
|
1653
1684
|
includeSpan: (span) =>
|
|
1654
1685
|
spanDetails.has(span.start) && ownerOf(spanDetails.get(span.start)) === owner,
|
|
@@ -1744,12 +1775,25 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1744
1775
|
}
|
|
1745
1776
|
if (verse) {
|
|
1746
1777
|
for (const boundary of projection.boundaries) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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;
|
|
1753
1797
|
if (target === undefined) continue;
|
|
1754
1798
|
pieces.push({
|
|
1755
1799
|
from: "",
|
|
@@ -1798,6 +1842,17 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1798
1842
|
});
|
|
1799
1843
|
}
|
|
1800
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
|
+
|
|
1801
1856
|
const firstEncodingRun =
|
|
1802
1857
|
active.find(native) ??
|
|
1803
1858
|
rawReferences.find((reference) => native(reference.construct))?.construct;
|
|
@@ -2142,7 +2197,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2142
2197
|
...span,
|
|
2143
2198
|
inline: true,
|
|
2144
2199
|
model: span.verseModel,
|
|
2145
|
-
body: span.
|
|
2200
|
+
body: text.slice(span.start + 1, span.contentEnd),
|
|
2146
2201
|
bodyStart: span.start + 1,
|
|
2147
2202
|
bodyEnd: span.contentEnd,
|
|
2148
2203
|
})),
|
|
@@ -2302,11 +2357,19 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2302
2357
|
if (attributes !== undefined) {
|
|
2303
2358
|
const open = lines[run.start].indexOf("{");
|
|
2304
2359
|
const close = lines[run.start].lastIndexOf("}");
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
+
});
|
|
2310
2373
|
}
|
|
2311
2374
|
if (detail.plain && detail.body)
|
|
2312
2375
|
for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
|
|
@@ -2395,7 +2458,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2395
2458
|
.map((span) => ({
|
|
2396
2459
|
...span,
|
|
2397
2460
|
inline: true,
|
|
2398
|
-
body: span.
|
|
2461
|
+
body: markdown.slice(span.start + 1, span.contentEnd),
|
|
2399
2462
|
bodyStart: span.start + 1,
|
|
2400
2463
|
bodyEnd: span.contentEnd,
|
|
2401
2464
|
})),
|
|
@@ -2469,7 +2532,10 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2469
2532
|
}
|
|
2470
2533
|
for (const group of model.groups) {
|
|
2471
2534
|
if (group.cantoBreak) {
|
|
2472
|
-
group.text =
|
|
2535
|
+
group.text =
|
|
2536
|
+
detail.plan?.pieces.find(
|
|
2537
|
+
(piece) => piece.boundary === "canto" && piece.offset === base + group.from,
|
|
2538
|
+
)?.to ?? "";
|
|
2473
2539
|
continue;
|
|
2474
2540
|
}
|
|
2475
2541
|
for (const line of group.lines) {
|
|
@@ -2499,10 +2565,25 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2499
2565
|
classes,
|
|
2500
2566
|
values: Object.entries(values).filter(([key]) => key !== "lang"),
|
|
2501
2567
|
});
|
|
2502
|
-
const
|
|
2503
|
-
.
|
|
2504
|
-
|
|
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
|
+
)
|
|
2505
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)
|
|
2506
2587
|
.join("");
|
|
2507
2588
|
pairedEdits.push({
|
|
2508
2589
|
from: block.start,
|
|
@@ -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);
|
package/engine/content-spans.mjs
CHANGED
|
@@ -67,7 +67,6 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false, verse
|
|
|
67
67
|
state.src[state.src.indexOf("]", start + 2) + 1] === "]") ||
|
|
68
68
|
/@[A-Za-z]+$/.test(state.src.slice(0, start)) ||
|
|
69
69
|
state.src[start + 1] === "^" ||
|
|
70
|
-
state.src[start + 1] === "!" ||
|
|
71
70
|
["!", "]"].includes(state.src[start - 1]) ||
|
|
72
71
|
(state.src[start - 1] === "[" && state.linkLevel === 0)
|
|
73
72
|
)
|
|
@@ -75,6 +74,7 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false, verse
|
|
|
75
74
|
const end = state.md.helpers.parseLinkLabel(state, start, false);
|
|
76
75
|
if (
|
|
77
76
|
end < 0 ||
|
|
77
|
+
(state.src[start + 1] === "!" && state.src[end + 1] !== "{") ||
|
|
78
78
|
["(", "["].includes(state.src[end + 1]) ||
|
|
79
79
|
(state.src[start + 1] === "[" && state.src[end + 1] !== "{")
|
|
80
80
|
)
|
|
@@ -215,6 +215,7 @@ export function scanSpans(source) {
|
|
|
215
215
|
tokens = parser.parse(text, env),
|
|
216
216
|
spans = [],
|
|
217
217
|
errors = [];
|
|
218
|
+
const sourcePositions = new Map();
|
|
218
219
|
const lines = text.split("\n"),
|
|
219
220
|
starts = [];
|
|
220
221
|
let offset = 0;
|
|
@@ -244,6 +245,14 @@ export function scanSpans(source) {
|
|
|
244
245
|
if (children[close].type === "span_open") depth++;
|
|
245
246
|
if (children[close].type === "span_close" && --depth === 0) break;
|
|
246
247
|
}
|
|
248
|
+
if ((child.attrGet("class") ?? "").split(/\s+/).includes("verse"))
|
|
249
|
+
sourcePositions.set(
|
|
250
|
+
start,
|
|
251
|
+
Array.from(
|
|
252
|
+
{ length: child.meta.text.length + 1 },
|
|
253
|
+
(_, index) => position(child.meta.start + 1 + index) - start - 1,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
247
256
|
spans.push({
|
|
248
257
|
id: child.attrGet("id") ?? "",
|
|
249
258
|
line,
|
|
@@ -360,10 +369,34 @@ export function scanSpans(source) {
|
|
|
360
369
|
([key, value]) => key === "class" && value.split(/\s+/).includes("verse"),
|
|
361
370
|
)
|
|
362
371
|
) {
|
|
363
|
-
|
|
372
|
+
const model = parseVerse(span.text, {
|
|
364
373
|
inline: true,
|
|
365
374
|
footnotes: footnoteLabels(text),
|
|
366
375
|
});
|
|
376
|
+
// The parser removes container prefixes and normalizes CRLF. Map
|
|
377
|
+
// every logical range back through its original source positions.
|
|
378
|
+
const positions = sourcePositions.get(span.start);
|
|
379
|
+
const at = (offset) => positions[offset];
|
|
380
|
+
const ranges = new Set([
|
|
381
|
+
...model.groups,
|
|
382
|
+
...model.cantos.flatMap((canto) => canto.stanzas),
|
|
383
|
+
...model.groups.flatMap((group) => (group.cantoBreak ? [] : group.lines)),
|
|
384
|
+
...model.notes,
|
|
385
|
+
]);
|
|
386
|
+
for (const range of ranges) {
|
|
387
|
+
range.from = at(range.from);
|
|
388
|
+
range.to = at(range.to);
|
|
389
|
+
if ("offsets" in range) range.offsets = range.offsets.map(at);
|
|
390
|
+
}
|
|
391
|
+
for (const boundary of model.boundaries)
|
|
392
|
+
boundary.sourceOffset = at(boundary.sourceOffset);
|
|
393
|
+
for (const finding of model.errors) finding.offset = at(finding.offset);
|
|
394
|
+
model.layout = model.layout.map(([from, to]) => [at(from), at(to)]);
|
|
395
|
+
for (let index = 1; index < positions.length; index++)
|
|
396
|
+
if (positions[index] > positions[index - 1] + 1)
|
|
397
|
+
model.layout.push([positions[index - 1] + 1, positions[index]]);
|
|
398
|
+
model.layout.sort((a, b) => a[0] - b[0]);
|
|
399
|
+
span.verseModel = model;
|
|
367
400
|
errors.push(...inlineVerseFindings(span, text));
|
|
368
401
|
}
|
|
369
402
|
spans.sort((a, b) => a.start - b.start);
|