@meowdown/core 0.65.2 → 0.65.4
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/dist/index.d.ts +69 -68
- package/dist/index.js +644 -185
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -43,6 +43,12 @@ import { closeHistory } from "@prosekit/pm/history";
|
|
|
43
43
|
import { registerResizableHandleElement, registerResizableRootElement } from "@prosekit/web/resizable";
|
|
44
44
|
import { InputRule } from "@prosekit/pm/inputrules";
|
|
45
45
|
|
|
46
|
+
//#region src/extensions/node-names.ts
|
|
47
|
+
function isNodeOfType(node, name) {
|
|
48
|
+
return node.type.name === name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
46
52
|
//#region src/utils/composition.ts
|
|
47
53
|
const COMPOSITION_TAIL_MS = 50;
|
|
48
54
|
let compositionEndedAt = -1;
|
|
@@ -920,12 +926,6 @@ function atomUnitToDOM(atom, sourceText) {
|
|
|
920
926
|
}
|
|
921
927
|
}
|
|
922
928
|
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region src/extensions/node-names.ts
|
|
925
|
-
function isNodeOfType(node, name) {
|
|
926
|
-
return node.type.name === name;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
929
|
//#endregion
|
|
930
930
|
//#region src/extensions/heading.ts
|
|
931
931
|
/**
|
|
@@ -1294,27 +1294,6 @@ function definePlainTextPaste() {
|
|
|
1294
1294
|
}));
|
|
1295
1295
|
}
|
|
1296
1296
|
|
|
1297
|
-
//#endregion
|
|
1298
|
-
//#region src/utils/backticks.ts
|
|
1299
|
-
/**
|
|
1300
|
-
* Length of the longest run of `charCode` in `text`, at least `min`.
|
|
1301
|
-
*/
|
|
1302
|
-
function longestCharRun(text, charCode, min = 0) {
|
|
1303
|
-
let longest = min;
|
|
1304
|
-
let run = 0;
|
|
1305
|
-
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === charCode) {
|
|
1306
|
-
run++;
|
|
1307
|
-
if (run > longest) longest = run;
|
|
1308
|
-
} else run = 0;
|
|
1309
|
-
return longest;
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* Length of the longest run of backticks in `text`, at least `min`.
|
|
1313
|
-
*/
|
|
1314
|
-
function longestBacktickRun(text, min = 0) {
|
|
1315
|
-
return longestCharRun(text, 96, min);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1297
|
//#endregion
|
|
1319
1298
|
//#region src/converters/pm-to-md.ts
|
|
1320
1299
|
/**
|
|
@@ -1376,7 +1355,8 @@ function emitHeading(node, out) {
|
|
|
1376
1355
|
out.closeBlock();
|
|
1377
1356
|
return;
|
|
1378
1357
|
}
|
|
1379
|
-
|
|
1358
|
+
const prefix = HEADING_PREFIX[attrs.level] ?? "# ";
|
|
1359
|
+
out.write(node.content.size > 0 ? prefix : prefix.slice(0, -1));
|
|
1380
1360
|
emitInlineChildren(node, out);
|
|
1381
1361
|
const closingHashes = attrs.closingHashes;
|
|
1382
1362
|
if (closingHashes != null && closingHashes > 0) out.write(" " + "#".repeat(closingHashes));
|
|
@@ -1390,10 +1370,22 @@ var MdOut = class {
|
|
|
1390
1370
|
this.atLineStart = true;
|
|
1391
1371
|
this.deferredBlankPrefix = null;
|
|
1392
1372
|
}
|
|
1393
|
-
|
|
1373
|
+
/**
|
|
1374
|
+
* Write `text`, opening each embedded line with the current line prefix.
|
|
1375
|
+
*
|
|
1376
|
+
* `lazyLines` lets a line the prefix would change the meaning of - a setext
|
|
1377
|
+
* underline, a tab-indented line under a blockquote, a block opener - go out
|
|
1378
|
+
* as a lazy continuation instead (see `continuationPrefix`). Markdown keeps
|
|
1379
|
+
* such a line as text only when it arrives lazily, and the container picks
|
|
1380
|
+
* the paragraph up again on the next line. Only paragraph-like text may ask
|
|
1381
|
+
* for this: an unprefixed line would fall out of a code block, an html
|
|
1382
|
+
* comment, or an HTML block.
|
|
1383
|
+
*/
|
|
1384
|
+
write(text, lazyLines = false) {
|
|
1394
1385
|
if (text === "") return;
|
|
1395
1386
|
this.emitDeferredBlankLine();
|
|
1396
1387
|
if (this.atLineStart) {
|
|
1388
|
+
if (this.pendingFirst !== null && isThematicBreak(this.pendingFirst + text)) this.breakMarkerLine();
|
|
1397
1389
|
this.parts.push(this.pendingFirst ?? this.linePrefix);
|
|
1398
1390
|
this.pendingFirst = null;
|
|
1399
1391
|
this.atLineStart = false;
|
|
@@ -1403,9 +1395,11 @@ var MdOut = class {
|
|
|
1403
1395
|
return;
|
|
1404
1396
|
}
|
|
1405
1397
|
const lines = text.split("\n");
|
|
1398
|
+
const lazy = lazyLines && this.linePrefix !== "";
|
|
1406
1399
|
for (let i = 0; i < lines.length; i++) {
|
|
1407
|
-
|
|
1408
|
-
if (
|
|
1400
|
+
const line = lines[i];
|
|
1401
|
+
if (i > 0) this.parts.push("\n", lazy ? continuationPrefix(line, this.linePrefix) : this.linePrefix);
|
|
1402
|
+
if (line !== "") this.parts.push(line);
|
|
1409
1403
|
}
|
|
1410
1404
|
}
|
|
1411
1405
|
/**
|
|
@@ -1432,7 +1426,8 @@ var MdOut = class {
|
|
|
1432
1426
|
closeBlock() {
|
|
1433
1427
|
if (this.atLineStart && this.pendingFirst !== null) {
|
|
1434
1428
|
this.emitDeferredBlankLine();
|
|
1435
|
-
this.
|
|
1429
|
+
const marker = this.pendingFirst;
|
|
1430
|
+
this.parts.push(marker.endsWith("] ") ? marker : marker.trimEnd());
|
|
1436
1431
|
this.pendingFirst = null;
|
|
1437
1432
|
this.atLineStart = false;
|
|
1438
1433
|
}
|
|
@@ -1441,6 +1436,18 @@ var MdOut = class {
|
|
|
1441
1436
|
this.deferredBlankPrefix = this.linePrefix;
|
|
1442
1437
|
}
|
|
1443
1438
|
/**
|
|
1439
|
+
* Give the markers pending on this line a line of their own, so what comes
|
|
1440
|
+
* next opens a new one. Unlike `closeBlock` this owes no blank line: the
|
|
1441
|
+
* items are still part of the same tight list.
|
|
1442
|
+
*/
|
|
1443
|
+
breakMarkerLine() {
|
|
1444
|
+
if (this.pendingFirst === null) return;
|
|
1445
|
+
this.emitDeferredBlankLine();
|
|
1446
|
+
this.parts.push(this.pendingFirst.trimEnd(), "\n");
|
|
1447
|
+
this.pendingFirst = null;
|
|
1448
|
+
this.atLineStart = true;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1444
1451
|
* Cancel the blank line deferred by the last `closeBlock`, so the next
|
|
1445
1452
|
* write starts directly on the following line. Used between the blocks of
|
|
1446
1453
|
* a tight list, where markdown separates items (and an item's paragraph
|
|
@@ -1462,14 +1469,24 @@ var MdOut = class {
|
|
|
1462
1469
|
this.linePrefix = savedLine + continuation;
|
|
1463
1470
|
if (firstLine !== null) {
|
|
1464
1471
|
const base = savedFirst ?? savedLine;
|
|
1465
|
-
|
|
1472
|
+
if (savedFirst !== null && isThematicBreak(base + firstLine)) {
|
|
1473
|
+
this.breakMarkerLine();
|
|
1474
|
+
this.pendingFirst = savedLine + firstLine;
|
|
1475
|
+
} else this.pendingFirst = base + firstLine;
|
|
1466
1476
|
}
|
|
1467
1477
|
fn();
|
|
1468
1478
|
this.linePrefix = savedLine;
|
|
1469
1479
|
this.pendingFirst = firstLine !== null ? null : savedFirst;
|
|
1470
1480
|
}
|
|
1471
1481
|
finish() {
|
|
1472
|
-
|
|
1482
|
+
const text = this.parts.join("");
|
|
1483
|
+
let cut = text.length;
|
|
1484
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
1485
|
+
const code = text.charCodeAt(i);
|
|
1486
|
+
if (code === 10) cut = i;
|
|
1487
|
+
else if (code !== 32 && code !== 9) break;
|
|
1488
|
+
}
|
|
1489
|
+
return text.slice(0, cut) + "\n";
|
|
1473
1490
|
}
|
|
1474
1491
|
emitDeferredBlankLine() {
|
|
1475
1492
|
const prefix = this.deferredBlankPrefix;
|
|
@@ -1548,13 +1565,39 @@ function emitBlockChildren(node, out, tightItem = false) {
|
|
|
1548
1565
|
while (runEnd < count && isNodeOfType(node.child(runEnd), "list")) runEnd++;
|
|
1549
1566
|
const tightRun = isTightRun(node, index, runEnd);
|
|
1550
1567
|
for (let item = index; item < runEnd; item++) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1568
|
+
const child = node.child(item);
|
|
1569
|
+
const isRunStart = item === index;
|
|
1570
|
+
const tight = isRunStart ? tightItem && index > 0 : tightRun;
|
|
1571
|
+
const carriesOn = !isRunStart && listMarkerChar(node.child(item - 1)) === listMarkerChar(child);
|
|
1572
|
+
if (tight && (carriesOn || canInterruptParagraph(child))) out.suppressBlank();
|
|
1573
|
+
emitList(child, out, tightRun);
|
|
1553
1574
|
}
|
|
1554
1575
|
index = runEnd;
|
|
1555
1576
|
}
|
|
1556
1577
|
}
|
|
1557
1578
|
/**
|
|
1579
|
+
* Whether `list`'s marker line still opens an item when the line above it is a
|
|
1580
|
+
* paragraph. CommonMark lets a list interrupt a paragraph only when the item
|
|
1581
|
+
* carries content and, if ordered, is numbered 1; anything else reads as more of
|
|
1582
|
+
* the paragraph, so the blank line before it is what keeps it a list at all.
|
|
1583
|
+
*/
|
|
1584
|
+
function canInterruptParagraph(list) {
|
|
1585
|
+
const attrs = list.attrs;
|
|
1586
|
+
if (attrs.kind === "ordered" && (attrs.order ?? 1) !== 1) return false;
|
|
1587
|
+
return attrs.kind === "task" || list.childCount > 0 && list.child(0).content.size > 0;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* The character a list item's marker ends with: the bullet itself, or the
|
|
1591
|
+
* delimiter after an ordered item's number. A markdown list runs for as long as
|
|
1592
|
+
* this stays the same, and a different one opens a new list.
|
|
1593
|
+
*/
|
|
1594
|
+
function listMarkerChar(node) {
|
|
1595
|
+
const { kind, marker, collapsed } = node.attrs;
|
|
1596
|
+
if (kind === "ordered") return marker === ")" ? ")" : ".";
|
|
1597
|
+
if (kind === "task") return marker === "+" ? "+" : marker === "*" ? "*" : "-";
|
|
1598
|
+
return collapsed ? "+" : marker === "*" ? "*" : "-";
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1558
1601
|
* A run of sibling `list` nodes serializes tight iff every item is "simple":
|
|
1559
1602
|
* at most one leading paragraph, then only nested lists. Any other shape
|
|
1560
1603
|
* (multiple paragraphs, a blockquote, a code block, …) needs blank-line
|
|
@@ -1576,6 +1619,177 @@ function isTightItem(item) {
|
|
|
1576
1619
|
return true;
|
|
1577
1620
|
}
|
|
1578
1621
|
/**
|
|
1622
|
+
* The prefix a leaf's continuation line goes out with. The container's full
|
|
1623
|
+
* `prefix` is the default. A line the full prefix would change the meaning of -
|
|
1624
|
+
* a setext underline, a tab whose columns the prefix shrinks below the four
|
|
1625
|
+
* that kept it inert, a block opener the source kept inert with indentation the
|
|
1626
|
+
* parser dedented away - needs a lazy spelling instead: without its `>` marker
|
|
1627
|
+
* the line can only continue the paragraph, because no block may start on a
|
|
1628
|
+
* lazy line.
|
|
1629
|
+
*
|
|
1630
|
+
* The lazy spelling carries `lazyIndent(prefix)`, the columns the parser
|
|
1631
|
+
* dedents an unmarked line by, so the text comes back exactly. It goes out bare
|
|
1632
|
+
* only when the dedent would leave it alone anyway and its own whitespace
|
|
1633
|
+
* measures four columns flush left (or it is a setext underline, which nothing
|
|
1634
|
+
* at the top level turns back into an underline).
|
|
1635
|
+
*/
|
|
1636
|
+
function continuationPrefix(line, prefix) {
|
|
1637
|
+
if (measureIndent(line, prefix.length) >= 4) return prefix;
|
|
1638
|
+
const shrunk = measureIndent(line, 0) >= 4;
|
|
1639
|
+
const underline = isSetextUnderline(line);
|
|
1640
|
+
const opens = !shrunk && lineOpensBlock(line);
|
|
1641
|
+
if (!shrunk && !underline && !opens) return prefix;
|
|
1642
|
+
const lazy = lazyIndent(prefix);
|
|
1643
|
+
return !opens && dedentKeepsLine(line, lazy.length) ? "" : lazy;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The lazy spelling's indent: the whitespace the containers write after the
|
|
1647
|
+
* last `>` marker, which is also the columns the parser dedents an unmarked
|
|
1648
|
+
* continuation line by (`sliceColumn` in `md-to-pm.ts`). The single space
|
|
1649
|
+
* directly after the `>` belongs to the marker, not to the indent. A prefix
|
|
1650
|
+
* with no `>` to drop has flush left as its only lazy spelling: its columns
|
|
1651
|
+
* are the list's own indent, and writing them would just re-match the item.
|
|
1652
|
+
*/
|
|
1653
|
+
function lazyIndent(prefix) {
|
|
1654
|
+
let start = prefix.length;
|
|
1655
|
+
while (start > 0 && prefix.charCodeAt(start - 1) === 32) start--;
|
|
1656
|
+
if (start === 0 || prefix.charCodeAt(start - 1) !== 62) return "";
|
|
1657
|
+
return prefix.slice(start + 1);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Whether the parser's dedent (`sliceColumn` at `column`) returns `line`
|
|
1661
|
+
* unchanged: its leading whitespace stops short of the column, or a tab in it
|
|
1662
|
+
* reaches past. Such a line round-trips byte-exact when written bare.
|
|
1663
|
+
*/
|
|
1664
|
+
function dedentKeepsLine(line, column) {
|
|
1665
|
+
let col = 0;
|
|
1666
|
+
for (let index = 0; col < column; index++) {
|
|
1667
|
+
const code = line.charCodeAt(index);
|
|
1668
|
+
if (code === 32) col += 1;
|
|
1669
|
+
else if (code === 9) {
|
|
1670
|
+
const width = 4 - col % 4;
|
|
1671
|
+
if (col + width > column) return true;
|
|
1672
|
+
col += width;
|
|
1673
|
+
} else return true;
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* The line patterns that open an HTML block, the openers of `HTMLBlockStyle`
|
|
1679
|
+
* in `@lezer/markdown`: script/pre/style, a comment, a processing instruction,
|
|
1680
|
+
* a declaration, CDATA, a known block-level tag, and a complete tag alone on
|
|
1681
|
+
* its line. Leading whitespace is stripped before matching.
|
|
1682
|
+
*/
|
|
1683
|
+
const HTML_BLOCK_OPEN = [
|
|
1684
|
+
/^<(?:script|pre|style)(?:\s|>|$)/i,
|
|
1685
|
+
/^<!--/,
|
|
1686
|
+
/^<\?/,
|
|
1687
|
+
/^<![A-Z]/,
|
|
1688
|
+
/^<!\[CDATA\[/,
|
|
1689
|
+
/^<\/?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\/?>|$)/i,
|
|
1690
|
+
/^(?:<\/[a-z][\w-]*\s*>|<[a-z][\w-]*(\s+[a-z:_][\w.-]*(?:\s*=\s*(?:[^\s"'=<>`]+|'[^']*'|"[^"]*"))?)*\s*>)\s*$/i
|
|
1691
|
+
];
|
|
1692
|
+
/**
|
|
1693
|
+
* Whether `text`'s first line opens an HTML block. The last pattern (a
|
|
1694
|
+
* complete tag) cannot interrupt a paragraph, but a lazy spelling of such a
|
|
1695
|
+
* line round-trips all the same, so one check serves both callers.
|
|
1696
|
+
*/
|
|
1697
|
+
function opensHTMLBlock(text) {
|
|
1698
|
+
const lineEnd = text.indexOf("\n");
|
|
1699
|
+
const line = lineEnd < 0 ? text : text.slice(0, lineEnd);
|
|
1700
|
+
for (const pattern of HTML_BLOCK_OPEN) if (pattern.test(line)) return true;
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Whether `line`, at the start of a container's content, opens a block instead
|
|
1705
|
+
* of continuing the paragraph above it: a blockquote, an ATX heading, a fence,
|
|
1706
|
+
* `$$` math, an HTML block, a thematic break, a bullet item (even an empty
|
|
1707
|
+
* one), and an ordered item that carries content and is numbered 1.
|
|
1708
|
+
*/
|
|
1709
|
+
function lineOpensBlock(line) {
|
|
1710
|
+
let index = 0;
|
|
1711
|
+
while (isSpaceOrTab(line.charCodeAt(index))) index++;
|
|
1712
|
+
const first = line.charCodeAt(index);
|
|
1713
|
+
switch (first) {
|
|
1714
|
+
case 62: return true;
|
|
1715
|
+
case 60: return opensHTMLBlock(index === 0 ? line : line.slice(index));
|
|
1716
|
+
case 35: {
|
|
1717
|
+
let end = index + 1;
|
|
1718
|
+
while (line.charCodeAt(end) === 35) end++;
|
|
1719
|
+
return end - index <= 6 && (end === line.length || isSpaceOrTab(line.charCodeAt(end)));
|
|
1720
|
+
}
|
|
1721
|
+
case 96:
|
|
1722
|
+
case 126: {
|
|
1723
|
+
let end = index + 1;
|
|
1724
|
+
while (line.charCodeAt(end) === first) end++;
|
|
1725
|
+
return end - index >= 3;
|
|
1726
|
+
}
|
|
1727
|
+
case 36: return line.charCodeAt(index + 1) === 36;
|
|
1728
|
+
case 95: return isUnderscoreBreak(line, index);
|
|
1729
|
+
case 42:
|
|
1730
|
+
case 45:
|
|
1731
|
+
case 43: return isThematicBreak(line) || index + 1 >= line.length || isSpaceOrTab(line.charCodeAt(index + 1));
|
|
1732
|
+
case 49: {
|
|
1733
|
+
const delimiter = line.charCodeAt(index + 1);
|
|
1734
|
+
return (delimiter === 46 || delimiter === 41) && startsNonEmptyItem(line, index + 1);
|
|
1735
|
+
}
|
|
1736
|
+
default: return false;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* A `___` thematic break: three or more underscores with nothing but spaces
|
|
1741
|
+
* and tabs between them. (`isThematicBreak` covers `-` and `*`, the two break
|
|
1742
|
+
* characters that double as list bullets.)
|
|
1743
|
+
*/
|
|
1744
|
+
function isUnderscoreBreak(line, index) {
|
|
1745
|
+
let count = 0;
|
|
1746
|
+
for (; index < line.length; index++) {
|
|
1747
|
+
const code = line.charCodeAt(index);
|
|
1748
|
+
if (code === 95) count++;
|
|
1749
|
+
else if (!isSpaceOrTab(code)) return false;
|
|
1750
|
+
}
|
|
1751
|
+
return count >= 3;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Whether the list delimiter at `index` is followed by the gap and content
|
|
1755
|
+
* (`1. x`) that let an ordered item interrupt a paragraph.
|
|
1756
|
+
*/
|
|
1757
|
+
function startsNonEmptyItem(line, index) {
|
|
1758
|
+
if (!isSpaceOrTab(line.charCodeAt(index + 1))) return false;
|
|
1759
|
+
for (let i = index + 2; i < line.length; i++) if (!isSpaceOrTab(line.charCodeAt(i))) return true;
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* The columns of leading whitespace `line` stands for when it starts at column
|
|
1764
|
+
* `start`, where a tab reaches the next multiple of four.
|
|
1765
|
+
*/
|
|
1766
|
+
function measureIndent(line, start) {
|
|
1767
|
+
let col = start;
|
|
1768
|
+
for (let i = 0; i < line.length; i++) {
|
|
1769
|
+
const code = line.charCodeAt(i);
|
|
1770
|
+
if (code === 32) col += 1;
|
|
1771
|
+
else if (code === 9) col += 4 - col % 4;
|
|
1772
|
+
else break;
|
|
1773
|
+
}
|
|
1774
|
+
return col - start;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Whether `line` is a run of one character, `=` or `-`, and so would read as a
|
|
1778
|
+
* setext underline under a container's line prefix. Surrounding whitespace does
|
|
1779
|
+
* not save it: an underline may carry any indentation the prefix leaves under
|
|
1780
|
+
* four columns, and any trailing spaces at all.
|
|
1781
|
+
*/
|
|
1782
|
+
function isSetextUnderline(line) {
|
|
1783
|
+
let start = 0;
|
|
1784
|
+
while (start < line.length && isSpaceChar(line.charCodeAt(start))) start++;
|
|
1785
|
+
const first = line.charCodeAt(start);
|
|
1786
|
+
if (first !== 61 && first !== 45) return false;
|
|
1787
|
+
let end = line.length;
|
|
1788
|
+
while (isSpaceChar(line.charCodeAt(end - 1))) end--;
|
|
1789
|
+
for (let i = start + 1; i < end; i++) if (line.charCodeAt(i) !== first) return false;
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1579
1793
|
* Walk inline children writing text directly. The schema has no marks, so
|
|
1580
1794
|
* every inline child is currently a text node - but going through this
|
|
1581
1795
|
* loop instead of `node.textContent` avoids one intermediate string
|
|
@@ -1583,23 +1797,45 @@ function isTightItem(item) {
|
|
|
1583
1797
|
*/
|
|
1584
1798
|
function emitInlineChildren(node, out) {
|
|
1585
1799
|
const count = node.childCount;
|
|
1800
|
+
if (count === 0) return;
|
|
1801
|
+
const first = node.child(0).text;
|
|
1802
|
+
const lazy = first == null || first.charCodeAt(0) !== 60 || !opensHTMLBlock(first);
|
|
1586
1803
|
for (let i = 0; i < count; i++) {
|
|
1587
1804
|
const child = node.child(i);
|
|
1588
|
-
if (child.isText && child.text) out.write(child.text);
|
|
1805
|
+
if (child.isText && child.text) out.write(child.text, lazy);
|
|
1589
1806
|
}
|
|
1590
1807
|
}
|
|
1591
1808
|
function emitList(node, out, tight) {
|
|
1592
|
-
const { kind,
|
|
1593
|
-
const
|
|
1594
|
-
const orderMarker = marker === ")" ? ")" : ".";
|
|
1809
|
+
const { kind, order, taskMarker, markerGap, checked } = node.attrs;
|
|
1810
|
+
const markerChar = listMarkerChar(node);
|
|
1595
1811
|
const checkMark = taskMarker === "X" ? "X" : "x";
|
|
1596
1812
|
const gap = Math.min(Math.max(markerGap ?? 1, 1), 4);
|
|
1597
|
-
const prefix = `${kind === "ordered" ? `${order ?? 1}${
|
|
1813
|
+
const prefix = `${kind === "ordered" ? `${order ?? 1}${markerChar}` : markerChar}${" ".repeat(gap)}`;
|
|
1598
1814
|
const outputMarker = kind === "task" ? `${prefix}[${checked ? checkMark : " "}] ` : prefix;
|
|
1599
1815
|
const continuation = " ".repeat(prefix.length);
|
|
1600
1816
|
out.withPrefix(continuation, outputMarker, () => emitBlockChildren(node, out, tight));
|
|
1601
1817
|
out.closeBlock();
|
|
1602
1818
|
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Whether a line of list markers reads as a thematic break: three or more `-`
|
|
1821
|
+
* or `*`, all the same character, with nothing but spaces, tabs and blockquote
|
|
1822
|
+
* markers around them.
|
|
1823
|
+
*/
|
|
1824
|
+
function isThematicBreak(line) {
|
|
1825
|
+
let breakChar = 0;
|
|
1826
|
+
let count = 0;
|
|
1827
|
+
for (let i = 0; i < line.length; i++) {
|
|
1828
|
+
const code = line.charCodeAt(i);
|
|
1829
|
+
if (code === 10) break;
|
|
1830
|
+
if (code === 32 || code === 9) continue;
|
|
1831
|
+
if (code === 62 && count === 0) continue;
|
|
1832
|
+
if (code !== 45 && code !== 42) return false;
|
|
1833
|
+
if (breakChar === 0) breakChar = code;
|
|
1834
|
+
else if (code !== breakChar) return false;
|
|
1835
|
+
count++;
|
|
1836
|
+
}
|
|
1837
|
+
return count >= 3;
|
|
1838
|
+
}
|
|
1603
1839
|
function emitCodeBlock(node, out) {
|
|
1604
1840
|
const attrs = node.attrs;
|
|
1605
1841
|
const language = attrs.language || "";
|
|
@@ -1624,10 +1860,9 @@ function emitCodeBlock(node, out) {
|
|
|
1624
1860
|
return;
|
|
1625
1861
|
}
|
|
1626
1862
|
const tilde = attrs.fenceStyle === "tilde";
|
|
1627
|
-
const
|
|
1628
|
-
const fence = (tilde ? "~" : "`").repeat(Math.max(attrs.fenceLength ?? 0, minWidth));
|
|
1863
|
+
const fence = (tilde ? "~" : "`").repeat(Math.max(attrs.fenceLength ?? 0, minFenceLength(code, tilde)));
|
|
1629
1864
|
out.write(fence);
|
|
1630
|
-
if (language) out.write(language);
|
|
1865
|
+
if (language) out.write(language.startsWith(tilde ? "~" : "`") ? " " + language : language);
|
|
1631
1866
|
out.write("\n");
|
|
1632
1867
|
if (code) {
|
|
1633
1868
|
out.write(code);
|
|
@@ -1637,16 +1872,53 @@ function emitCodeBlock(node, out) {
|
|
|
1637
1872
|
out.closeBlock();
|
|
1638
1873
|
}
|
|
1639
1874
|
/**
|
|
1875
|
+
* The narrowest fence that can hold `code`: wider than every line of it that
|
|
1876
|
+
* would close the fence early, and never under CommonMark's minimum of three. A
|
|
1877
|
+
* closing fence is a run of the fence character alone on its line; a run with
|
|
1878
|
+
* anything else on the line (`` a ``` ``, `` ``` x ``) or four columns in closes
|
|
1879
|
+
* nothing, so the fence holds it as it is.
|
|
1880
|
+
*/
|
|
1881
|
+
function minFenceLength(code, tilde) {
|
|
1882
|
+
const fenceChar = tilde ? 126 : 96;
|
|
1883
|
+
let longest = 2;
|
|
1884
|
+
let lineStart = 0;
|
|
1885
|
+
while (lineStart <= code.length) {
|
|
1886
|
+
let lineEnd = code.indexOf("\n", lineStart);
|
|
1887
|
+
if (lineEnd < 0) lineEnd = code.length;
|
|
1888
|
+
let index = lineStart;
|
|
1889
|
+
while (index < lineEnd && code.charCodeAt(index) === 32) index++;
|
|
1890
|
+
if (index - lineStart < 4) {
|
|
1891
|
+
const runStart = index;
|
|
1892
|
+
while (index < lineEnd && code.charCodeAt(index) === fenceChar) index++;
|
|
1893
|
+
const run = index - runStart;
|
|
1894
|
+
while (index < lineEnd && isSpaceOrTab(code.charCodeAt(index))) index++;
|
|
1895
|
+
if (index === lineEnd && run > longest) longest = run;
|
|
1896
|
+
}
|
|
1897
|
+
lineStart = lineEnd + 1;
|
|
1898
|
+
}
|
|
1899
|
+
return longest + 1;
|
|
1900
|
+
}
|
|
1901
|
+
function isSpaceOrTab(char) {
|
|
1902
|
+
return char === 32 || char === 9;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Whether indentation can spell `code` as an indented code block. There is no
|
|
1906
|
+
* line to carry the four columns of a block with no content at all, and none to
|
|
1907
|
+
* carry a blank line at either end.
|
|
1908
|
+
*/
|
|
1909
|
+
function canIndentCode(code) {
|
|
1910
|
+
return code !== "" && !code.startsWith("\n") && !code.endsWith("\n");
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1640
1913
|
* Indent `code` for an indented code block, or return `undefined` for shapes
|
|
1641
|
-
* the indented form cannot express
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1914
|
+
* the indented form cannot express, which fall back to a fence. Blank interior
|
|
1915
|
+
* lines stay empty so a round-trip adds no trailing whitespace; `MdOut.write`
|
|
1916
|
+
* still prepends the enclosing `linePrefix` (blockquote or list continuation)
|
|
1917
|
+
* per line.
|
|
1645
1918
|
*/
|
|
1646
1919
|
function toIndentedCode(code) {
|
|
1647
|
-
if (code
|
|
1920
|
+
if (!canIndentCode(code)) return void 0;
|
|
1648
1921
|
const lines = code.split("\n");
|
|
1649
|
-
if (lines[0] === "" || lines[lines.length - 1] === "") return void 0;
|
|
1650
1922
|
for (let i = 0; i < lines.length; i++) if (lines[i] !== "") lines[i] = ` ${lines[i]}`;
|
|
1651
1923
|
return lines.join("\n");
|
|
1652
1924
|
}
|
|
@@ -2477,8 +2749,10 @@ function applySequentialChunks(doc, chunks) {
|
|
|
2477
2749
|
rebuilt = [];
|
|
2478
2750
|
for (let seen = 0; seen < index; seen++) rebuilt.push(node.child(seen));
|
|
2479
2751
|
}
|
|
2480
|
-
if (rebuilt != null)
|
|
2481
|
-
|
|
2752
|
+
if (rebuilt != null) {
|
|
2753
|
+
if (Array.isArray(replacement)) rebuilt.push(...replacement);
|
|
2754
|
+
else rebuilt.push(replacement);
|
|
2755
|
+
}
|
|
2482
2756
|
childFrom = childTo;
|
|
2483
2757
|
}
|
|
2484
2758
|
return rebuilt == null ? void 0 : Fragment.fromArray(rebuilt);
|
|
@@ -3817,6 +4091,27 @@ function defineInlineMarks() {
|
|
|
3817
4091
|
return union(defineMdMark(), defineMdEm(), defineMdStrong(), defineMdCode(), defineMdLinkText(), defineMdLinkUri(), defineMdLinkTitle(), defineMdDel(), defineMdHighlight(), defineMdTag(), defineMdWikilink(), defineMdImage(), defineMdFile(), defineMdMath(), defineMdPack());
|
|
3818
4092
|
}
|
|
3819
4093
|
|
|
4094
|
+
//#endregion
|
|
4095
|
+
//#region src/utils/backticks.ts
|
|
4096
|
+
/**
|
|
4097
|
+
* Length of the longest run of `charCode` in `text`, at least `min`.
|
|
4098
|
+
*/
|
|
4099
|
+
function longestCharRun(text, charCode, min = 0) {
|
|
4100
|
+
let longest = min;
|
|
4101
|
+
let run = 0;
|
|
4102
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === charCode) {
|
|
4103
|
+
run++;
|
|
4104
|
+
if (run > longest) longest = run;
|
|
4105
|
+
} else run = 0;
|
|
4106
|
+
return longest;
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Length of the longest run of backticks in `text`, at least `min`.
|
|
4110
|
+
*/
|
|
4111
|
+
function longestBacktickRun(text, min = 0) {
|
|
4112
|
+
return longestCharRun(text, 96, min);
|
|
4113
|
+
}
|
|
4114
|
+
|
|
3820
4115
|
//#endregion
|
|
3821
4116
|
//#region src/extensions/inline-toggle.ts
|
|
3822
4117
|
const TOGGLE_SPECS = {
|
|
@@ -5512,20 +5807,22 @@ function scrollRectIntoView(view, rect, startDOM) {
|
|
|
5512
5807
|
else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom")) moveY = rect.bottom - rect.top > bounding.bottom - bounding.top ? rect.top + getSide(scrollMargin, "top") - bounding.top : rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom");
|
|
5513
5808
|
if (rect.left < bounding.left + getSide(scrollThreshold, "left")) moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left"));
|
|
5514
5809
|
else if (rect.right > bounding.right - getSide(scrollThreshold, "right")) moveX = rect.right - bounding.right + getSide(scrollMargin, "right");
|
|
5515
|
-
if (moveX || moveY)
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5810
|
+
if (moveX || moveY) {
|
|
5811
|
+
if (atTop) doc.defaultView?.scrollBy(moveX, moveY);
|
|
5812
|
+
else {
|
|
5813
|
+
const startX = elt.scrollLeft;
|
|
5814
|
+
const startY = elt.scrollTop;
|
|
5815
|
+
if (moveY) elt.scrollTop += moveY;
|
|
5816
|
+
if (moveX) elt.scrollLeft += moveX;
|
|
5817
|
+
const dX = elt.scrollLeft - startX;
|
|
5818
|
+
const dY = elt.scrollTop - startY;
|
|
5819
|
+
rect = {
|
|
5820
|
+
left: rect.left - dX,
|
|
5821
|
+
top: rect.top - dY,
|
|
5822
|
+
right: rect.right - dX,
|
|
5823
|
+
bottom: rect.bottom - dY
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5529
5826
|
}
|
|
5530
5827
|
const pos = atTop ? "fixed" : getComputedStyle(elt).position;
|
|
5531
5828
|
if (/^(?:fixed|sticky)$/.test(pos)) break;
|
|
@@ -5696,7 +5993,7 @@ function markdownToDoc(markdown, options = {}) {
|
|
|
5696
5993
|
frontmatterBody = body;
|
|
5697
5994
|
if (matchLength) rest = markdown.slice(matchLength);
|
|
5698
5995
|
}
|
|
5699
|
-
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest);
|
|
5996
|
+
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest, 0);
|
|
5700
5997
|
return nodes.doc(frontmatterBody === void 0 ? {} : { frontmatter: frontmatterBody }, blocks);
|
|
5701
5998
|
}
|
|
5702
5999
|
/**
|
|
@@ -5718,19 +6015,38 @@ function matchFrontmatter(markdown) {
|
|
|
5718
6015
|
* and flattening any node converter that returns multiple siblings
|
|
5719
6016
|
* (lists are the main case).
|
|
5720
6017
|
*/
|
|
5721
|
-
function collectBlocks(nodes, cursor, text) {
|
|
6018
|
+
function collectBlocks(nodes, cursor, text, column) {
|
|
5722
6019
|
const out = [];
|
|
5723
6020
|
if (!cursor.firstChild()) return out;
|
|
5724
6021
|
let previousTo;
|
|
5725
6022
|
do {
|
|
5726
6023
|
if (previousTo != null) appendGapParagraphs(out, nodes, text, previousTo, cursor.from);
|
|
5727
6024
|
previousTo = cursor.to;
|
|
5728
|
-
out
|
|
6025
|
+
appendBlocks(out, nodes, convertBlock(nodes, cursor, text, column));
|
|
5729
6026
|
} while (cursor.nextSibling());
|
|
5730
6027
|
cursor.parent();
|
|
5731
6028
|
return out;
|
|
5732
6029
|
}
|
|
5733
6030
|
/**
|
|
6031
|
+
* Append `blocks`, dropping the indented spelling from a code block that lands
|
|
6032
|
+
* right after a list: four columns of indentation would put it inside the item
|
|
6033
|
+
* above instead of after it, so it can only be written as a fence.
|
|
6034
|
+
*/
|
|
6035
|
+
function appendBlocks(out, nodes, blocks) {
|
|
6036
|
+
for (const block of blocks) {
|
|
6037
|
+
const attrs = block.attrs;
|
|
6038
|
+
if (attrs.fenceStyle === "indented" && isNodeOfType(block, "codeBlock") && out.length > 0 && isNodeOfType(out[out.length - 1], "list")) {
|
|
6039
|
+
out.push(nodes.codeBlock({
|
|
6040
|
+
language: attrs.language,
|
|
6041
|
+
fenceStyle: null,
|
|
6042
|
+
fenceLength: attrs.fenceLength
|
|
6043
|
+
}, block.textContent));
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
out.push(block);
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
/**
|
|
5734
6050
|
* Blank lines between two sibling blocks are content: a run of K blank lines
|
|
5735
6051
|
* is one block separator plus K-1 empty paragraphs. The gap slice between the
|
|
5736
6052
|
* siblings' ranges holds only line terminators and structural prefixes
|
|
@@ -5742,24 +6058,24 @@ function appendGapParagraphs(out, nodes, text, gapFrom, gapTo) {
|
|
|
5742
6058
|
for (let i = gapFrom; i < gapTo; i++) if (text.charCodeAt(i) === 10) newlineCount++;
|
|
5743
6059
|
for (let i = 2; i < newlineCount; i++) out.push(nodes.paragraph());
|
|
5744
6060
|
}
|
|
5745
|
-
function convertBlock(nodes, cursor, text) {
|
|
6061
|
+
function convertBlock(nodes, cursor, text, column) {
|
|
5746
6062
|
switch (cursor.type.id) {
|
|
5747
|
-
case LEZER_NODE_IDS.ATXHeading1: return [convertHeading(nodes, cursor, text, 1, false)];
|
|
5748
|
-
case LEZER_NODE_IDS.ATXHeading2: return [convertHeading(nodes, cursor, text, 2, false)];
|
|
5749
|
-
case LEZER_NODE_IDS.ATXHeading3: return [convertHeading(nodes, cursor, text, 3, false)];
|
|
5750
|
-
case LEZER_NODE_IDS.ATXHeading4: return [convertHeading(nodes, cursor, text, 4, false)];
|
|
5751
|
-
case LEZER_NODE_IDS.ATXHeading5: return [convertHeading(nodes, cursor, text, 5, false)];
|
|
5752
|
-
case LEZER_NODE_IDS.ATXHeading6: return [convertHeading(nodes, cursor, text, 6, false)];
|
|
5753
|
-
case LEZER_NODE_IDS.SetextHeading1: return [convertHeading(nodes, cursor, text, 1, true)];
|
|
5754
|
-
case LEZER_NODE_IDS.SetextHeading2: return [convertHeading(nodes, cursor, text, 2, true)];
|
|
5755
|
-
case LEZER_NODE_IDS.Paragraph: return [convertParagraph(nodes, cursor, text)];
|
|
5756
|
-
case LEZER_NODE_IDS.LinkReference: return [convertParagraph(nodes, cursor, text)];
|
|
5757
|
-
case LEZER_NODE_IDS.CommentBlock: return [convertHTMLComment(nodes, cursor, text)];
|
|
6063
|
+
case LEZER_NODE_IDS.ATXHeading1: return [convertHeading(nodes, cursor, text, column, 1, false)];
|
|
6064
|
+
case LEZER_NODE_IDS.ATXHeading2: return [convertHeading(nodes, cursor, text, column, 2, false)];
|
|
6065
|
+
case LEZER_NODE_IDS.ATXHeading3: return [convertHeading(nodes, cursor, text, column, 3, false)];
|
|
6066
|
+
case LEZER_NODE_IDS.ATXHeading4: return [convertHeading(nodes, cursor, text, column, 4, false)];
|
|
6067
|
+
case LEZER_NODE_IDS.ATXHeading5: return [convertHeading(nodes, cursor, text, column, 5, false)];
|
|
6068
|
+
case LEZER_NODE_IDS.ATXHeading6: return [convertHeading(nodes, cursor, text, column, 6, false)];
|
|
6069
|
+
case LEZER_NODE_IDS.SetextHeading1: return [convertHeading(nodes, cursor, text, column, 1, true)];
|
|
6070
|
+
case LEZER_NODE_IDS.SetextHeading2: return [convertHeading(nodes, cursor, text, column, 2, true)];
|
|
6071
|
+
case LEZER_NODE_IDS.Paragraph: return [convertParagraph(nodes, cursor, text, column)];
|
|
6072
|
+
case LEZER_NODE_IDS.LinkReference: return [convertParagraph(nodes, cursor, text, column)];
|
|
6073
|
+
case LEZER_NODE_IDS.CommentBlock: return [convertHTMLComment(nodes, cursor, text, column)];
|
|
5758
6074
|
case LEZER_NODE_IDS.HTMLBlock:
|
|
5759
|
-
case LEZER_NODE_IDS.ProcessingInstructionBlock: return [convertParagraph(nodes, cursor, text)];
|
|
6075
|
+
case LEZER_NODE_IDS.ProcessingInstructionBlock: return [convertParagraph(nodes, cursor, text, column)];
|
|
5760
6076
|
case LEZER_NODE_IDS.Blockquote: return [convertBlockquote(nodes, cursor, text)];
|
|
5761
|
-
case LEZER_NODE_IDS.BulletList: return convertList(nodes, cursor, text, "bullet");
|
|
5762
|
-
case LEZER_NODE_IDS.OrderedList: return convertList(nodes, cursor, text, "ordered");
|
|
6077
|
+
case LEZER_NODE_IDS.BulletList: return convertList(nodes, cursor, text, column, "bullet");
|
|
6078
|
+
case LEZER_NODE_IDS.OrderedList: return convertList(nodes, cursor, text, column, "ordered");
|
|
5763
6079
|
case LEZER_NODE_IDS.FencedCode:
|
|
5764
6080
|
case LEZER_NODE_IDS.CodeBlock: return [convertCodeBlock(nodes, cursor, text)];
|
|
5765
6081
|
case LEZER_NODE_IDS.BlockMath: return [convertBlockMath(nodes, cursor, text)];
|
|
@@ -5768,14 +6084,14 @@ function convertBlock(nodes, cursor, text) {
|
|
|
5768
6084
|
return [nodes.horizontalRule({ marker: marker === "---" ? null : marker })];
|
|
5769
6085
|
}
|
|
5770
6086
|
case LEZER_NODE_IDS.Table: return [convertTable(nodes, cursor, text)];
|
|
5771
|
-
case LEZER_NODE_IDS.Task: return [convertParagraph(nodes, cursor, text)];
|
|
6087
|
+
case LEZER_NODE_IDS.Task: return [convertParagraph(nodes, cursor, text, column)];
|
|
5772
6088
|
default:
|
|
5773
6089
|
if (text.slice(cursor.from, cursor.to).trim() === "") return [];
|
|
5774
6090
|
console.warn(`[meowdown] unsupported lezer block "${cursor.type.name}"`);
|
|
5775
|
-
return [convertParagraph(nodes, cursor, text)];
|
|
6091
|
+
return [convertParagraph(nodes, cursor, text, column)];
|
|
5776
6092
|
}
|
|
5777
6093
|
}
|
|
5778
|
-
function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
6094
|
+
function convertHeading(nodes, cursor, text, column, level, isSetext) {
|
|
5779
6095
|
const headingFrom = cursor.from;
|
|
5780
6096
|
let contentStart = cursor.from;
|
|
5781
6097
|
let contentEnd = cursor.to;
|
|
@@ -5792,13 +6108,14 @@ function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
|
5792
6108
|
lastTo = cursor.to;
|
|
5793
6109
|
} while (cursor.nextSibling());
|
|
5794
6110
|
if (lastId === LEZER_NODE_IDS.HeaderMark && lastFrom > contentStart) {
|
|
5795
|
-
contentEnd = lastFrom;
|
|
6111
|
+
contentEnd = isSetext ? text.lastIndexOf("\n", lastFrom - 1) : lastFrom;
|
|
5796
6112
|
trailingMarkFrom = lastFrom;
|
|
5797
6113
|
trailingMarkTo = lastTo;
|
|
5798
6114
|
}
|
|
5799
6115
|
cursor.parent();
|
|
5800
6116
|
}
|
|
5801
|
-
const
|
|
6117
|
+
const raw = dedentContinuation(readLeafText(cursor, text, contentStart, contentEnd), column);
|
|
6118
|
+
const content = isSetext ? raw : trimGap(raw);
|
|
5802
6119
|
const setextUnderline = isSetext ? countUnderlineChars(text, trailingMarkFrom, trailingMarkTo) || 1 : null;
|
|
5803
6120
|
const closingHashes = !isSetext && trailingMarkFrom >= 0 ? countHashChars(text, trailingMarkFrom, trailingMarkTo) || null : null;
|
|
5804
6121
|
return nodes.heading({
|
|
@@ -5808,6 +6125,20 @@ function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
|
5808
6125
|
}, content);
|
|
5809
6126
|
}
|
|
5810
6127
|
/**
|
|
6128
|
+
* Trim the whitespace lezer's marker gap can contain: space, tab and the line
|
|
6129
|
+
* ends (`space` in `@lezer/markdown`, matching `isSpaceChar`). A plain
|
|
6130
|
+
* `String.prototype.trim` would also take a no-break or an ideographic space,
|
|
6131
|
+
* which lezer reads as heading text - and a `#` left standing next to one flips
|
|
6132
|
+
* into a closing run on the way back (`# #\u{A0}`).
|
|
6133
|
+
*/
|
|
6134
|
+
function trimGap(text) {
|
|
6135
|
+
let start = 0;
|
|
6136
|
+
let end = text.length;
|
|
6137
|
+
while (start < end && isSpaceChar(text.charCodeAt(start))) start++;
|
|
6138
|
+
while (end > start && isSpaceChar(text.charCodeAt(end - 1))) end--;
|
|
6139
|
+
return text.slice(start, end);
|
|
6140
|
+
}
|
|
6141
|
+
/**
|
|
5811
6142
|
* Count the `=` / `-` characters in a setext underline run.
|
|
5812
6143
|
*/
|
|
5813
6144
|
function countUnderlineChars(text, from, to) {
|
|
@@ -5840,16 +6171,23 @@ function measureContentColumn(text, from) {
|
|
|
5840
6171
|
return col;
|
|
5841
6172
|
}
|
|
5842
6173
|
/**
|
|
5843
|
-
* Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4`
|
|
6174
|
+
* Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4`
|
|
6175
|
+
* columns. Whitespace the container never wrote is left alone: a tab that would
|
|
6176
|
+
* reach past `column` stands for more columns than the container takes, and a
|
|
6177
|
+
* line whose indentation stops short of `column` was written lazily, without any
|
|
6178
|
+
* prefix at all.
|
|
5844
6179
|
*/
|
|
5845
6180
|
function sliceColumn(line, column) {
|
|
5846
6181
|
let col = 0;
|
|
5847
6182
|
let index = 0;
|
|
5848
|
-
while (
|
|
6183
|
+
while (col < column) {
|
|
5849
6184
|
const code = line.charCodeAt(index);
|
|
5850
6185
|
if (code === 32) col += 1;
|
|
5851
|
-
else if (code === 9)
|
|
5852
|
-
|
|
6186
|
+
else if (code === 9) {
|
|
6187
|
+
const width = 4 - col % 4;
|
|
6188
|
+
if (col + width > column) break;
|
|
6189
|
+
col += width;
|
|
6190
|
+
} else return line;
|
|
5853
6191
|
index++;
|
|
5854
6192
|
}
|
|
5855
6193
|
return line.slice(index);
|
|
@@ -5870,6 +6208,69 @@ function dedentContinuation(content, column) {
|
|
|
5870
6208
|
return content.split("\n").map((line, index) => index === 0 ? line : sliceColumn(line, column)).join("\n");
|
|
5871
6209
|
}
|
|
5872
6210
|
/**
|
|
6211
|
+
* The blockquote markers inside `from`..`to`, as a flat run of `from, to` pairs
|
|
6212
|
+
* in document order. A marker can sit at any depth: a paragraph carries its own,
|
|
6213
|
+
* while a link reference's lands inside the `LinkLabel` that spans the break.
|
|
6214
|
+
* The cursor ends where it started.
|
|
6215
|
+
*/
|
|
6216
|
+
function collectQuoteMarks(cursor, marks, from, to) {
|
|
6217
|
+
if (!cursor.firstChild()) return;
|
|
6218
|
+
do {
|
|
6219
|
+
if (cursor.from >= to) break;
|
|
6220
|
+
if (cursor.to <= from) continue;
|
|
6221
|
+
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) {
|
|
6222
|
+
marks.push(cursor.from, cursor.to);
|
|
6223
|
+
continue;
|
|
6224
|
+
}
|
|
6225
|
+
collectQuoteMarks(cursor, marks, from, to);
|
|
6226
|
+
} while (cursor.nextSibling());
|
|
6227
|
+
cursor.parent();
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* The source between `from` and `to`, minus the blockquote markers inside it.
|
|
6231
|
+
*
|
|
6232
|
+
* In block-only parsing a leaf block has no inline children, with one
|
|
6233
|
+
* exception: lezer leaves the `QuoteMark` of every continuation line of a
|
|
6234
|
+
* multi-line block (`> l1\n> l2`) inside the block's own span. A marker takes
|
|
6235
|
+
* the whole line prefix with it, the single space after it included, exactly as
|
|
6236
|
+
* the serializer's own prefix puts it back. Only a space: a tab after the marker
|
|
6237
|
+
* stands for the columns up to the next tab stop, more than the prefix writes
|
|
6238
|
+
* back, so it stays in the text as the indentation it is. A line with no marker
|
|
6239
|
+
* carried no prefix at all - it is a lazy continuation, and every column on it
|
|
6240
|
+
* is its own. Markers outside `from`..`to` (an ATX heading's own marks, a setext
|
|
6241
|
+
* underline's line) are left alone. The cursor ends where it started.
|
|
6242
|
+
*/
|
|
6243
|
+
function readLeafText(cursor, text, from, to) {
|
|
6244
|
+
const lineBreak = text.indexOf("\n", from);
|
|
6245
|
+
if (lineBreak < 0 || lineBreak >= to) return text.slice(from, to);
|
|
6246
|
+
const marks = [];
|
|
6247
|
+
collectQuoteMarks(cursor, marks, from, to);
|
|
6248
|
+
let content = "";
|
|
6249
|
+
let pos = from;
|
|
6250
|
+
for (let index = 0; index < marks.length; index += 2) {
|
|
6251
|
+
content += text.slice(pos, Math.max(pos, text.lastIndexOf("\n", marks[index]) + 1));
|
|
6252
|
+
pos = marks[index + 1];
|
|
6253
|
+
if (text.charCodeAt(pos) === 32) pos += 1;
|
|
6254
|
+
}
|
|
6255
|
+
return trimTrailingBlankLines(content + text.slice(pos, to));
|
|
6256
|
+
}
|
|
6257
|
+
/**
|
|
6258
|
+
* Drop the blank lines a block ends with, the line break in front of them
|
|
6259
|
+
* included. A raw HTML or processing-instruction block with no closing sequence
|
|
6260
|
+
* runs its span to the end of the document and swallows them; a leaf block's
|
|
6261
|
+
* text ends where its last line does, which is where the serializer ends its
|
|
6262
|
+
* own output (`MdOut.finish`), so recording more would lose it on the way back.
|
|
6263
|
+
*/
|
|
6264
|
+
function trimTrailingBlankLines(content) {
|
|
6265
|
+
let end = content.length;
|
|
6266
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
6267
|
+
const code = content.charCodeAt(index);
|
|
6268
|
+
if (code === 10) end = index;
|
|
6269
|
+
else if (code !== 32 && code !== 9) break;
|
|
6270
|
+
}
|
|
6271
|
+
return content.slice(0, end);
|
|
6272
|
+
}
|
|
6273
|
+
/**
|
|
5873
6274
|
* Build a paragraph from raw markdown content, dedenting continuation lines so
|
|
5874
6275
|
* the serializer's own line prefix does not double the indent. A soft line break
|
|
5875
6276
|
* stays a literal `\n` in a single text node; the paragraph spec's
|
|
@@ -5878,25 +6279,8 @@ function dedentContinuation(content, column) {
|
|
|
5878
6279
|
function buildParagraph(nodes, content, column) {
|
|
5879
6280
|
return nodes.paragraph(dedentContinuation(content, column));
|
|
5880
6281
|
}
|
|
5881
|
-
function convertParagraph(nodes, cursor, text) {
|
|
5882
|
-
|
|
5883
|
-
const to = cursor.to;
|
|
5884
|
-
const column = measureContentColumn(text, from);
|
|
5885
|
-
if (cursor.firstChild()) {
|
|
5886
|
-
let content = "";
|
|
5887
|
-
let pos = from;
|
|
5888
|
-
do
|
|
5889
|
-
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) {
|
|
5890
|
-
content += text.slice(pos, cursor.from);
|
|
5891
|
-
pos = cursor.to;
|
|
5892
|
-
if (isSpaceChar(text.charCodeAt(pos))) pos += 1;
|
|
5893
|
-
}
|
|
5894
|
-
while (cursor.nextSibling());
|
|
5895
|
-
cursor.parent();
|
|
5896
|
-
content += text.slice(pos, to);
|
|
5897
|
-
return buildParagraph(nodes, content, column);
|
|
5898
|
-
}
|
|
5899
|
-
return buildParagraph(nodes, text.slice(from, to), column);
|
|
6282
|
+
function convertParagraph(nodes, cursor, text, column) {
|
|
6283
|
+
return buildParagraph(nodes, readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
5900
6284
|
}
|
|
5901
6285
|
/**
|
|
5902
6286
|
* Build the invisible `htmlComment` node from a `CommentBlock`. The raw comment
|
|
@@ -5904,11 +6288,14 @@ function convertParagraph(nodes, cursor, text) {
|
|
|
5904
6288
|
* continuation lines are dedented like a paragraph's so the serializer's own
|
|
5905
6289
|
* line prefix re-applies the container indent instead of doubling it.
|
|
5906
6290
|
*/
|
|
5907
|
-
function convertHTMLComment(nodes, cursor, text) {
|
|
5908
|
-
const
|
|
5909
|
-
const content = dedentContinuation(text.slice(cursor.from, cursor.to), column);
|
|
6291
|
+
function convertHTMLComment(nodes, cursor, text, column) {
|
|
6292
|
+
const content = dedentContinuation(readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
5910
6293
|
return nodes.htmlComment({ content });
|
|
5911
6294
|
}
|
|
6295
|
+
/**
|
|
6296
|
+
* A blockquote's children start at column 0: every column an enclosing container
|
|
6297
|
+
* wrote sits in front of the `> ` marker on the line, and comes off with it.
|
|
6298
|
+
*/
|
|
5912
6299
|
function convertBlockquote(nodes, cursor, text) {
|
|
5913
6300
|
const content = [];
|
|
5914
6301
|
if (cursor.firstChild()) {
|
|
@@ -5917,17 +6304,17 @@ function convertBlockquote(nodes, cursor, text) {
|
|
|
5917
6304
|
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
5918
6305
|
if (previousTo != null) appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
5919
6306
|
previousTo = cursor.to;
|
|
5920
|
-
content
|
|
6307
|
+
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, 0));
|
|
5921
6308
|
} while (cursor.nextSibling());
|
|
5922
6309
|
cursor.parent();
|
|
5923
6310
|
}
|
|
5924
6311
|
return nodes.blockquote(content);
|
|
5925
6312
|
}
|
|
5926
|
-
function convertList(nodes, cursor, text, kind) {
|
|
6313
|
+
function convertList(nodes, cursor, text, column, kind) {
|
|
5927
6314
|
const items = [];
|
|
5928
6315
|
if (cursor.firstChild()) {
|
|
5929
6316
|
do
|
|
5930
|
-
if (cursor.type.id === LEZER_NODE_IDS.ListItem) items.push(convertListItem(nodes, cursor, text, kind));
|
|
6317
|
+
if (cursor.type.id === LEZER_NODE_IDS.ListItem) items.push(convertListItem(nodes, cursor, text, column, kind));
|
|
5931
6318
|
while (cursor.nextSibling());
|
|
5932
6319
|
cursor.parent();
|
|
5933
6320
|
}
|
|
@@ -5958,7 +6345,7 @@ function readListMark(cursor, text, kind) {
|
|
|
5958
6345
|
* A GFM `Task` leaf (`[ ] text` / `[x] text`, after the list mark). The checkbox
|
|
5959
6346
|
* becomes the item's own attributes; the text after it becomes its paragraph.
|
|
5960
6347
|
*/
|
|
5961
|
-
function convertTaskItem(nodes, cursor, text) {
|
|
6348
|
+
function convertTaskItem(nodes, cursor, text, column) {
|
|
5962
6349
|
let taskStart = cursor.from;
|
|
5963
6350
|
const taskEnd = cursor.to;
|
|
5964
6351
|
let checked = false;
|
|
@@ -5978,43 +6365,54 @@ function convertTaskItem(nodes, cursor, text) {
|
|
|
5978
6365
|
cursor.parent();
|
|
5979
6366
|
}
|
|
5980
6367
|
if (isSpaceChar(text.charCodeAt(taskStart))) taskStart += 1;
|
|
5981
|
-
const paragraph = buildParagraph(nodes, text.slice(taskStart, taskEnd),
|
|
6368
|
+
const paragraph = buildParagraph(nodes, text.slice(taskStart, taskEnd), column);
|
|
5982
6369
|
return {
|
|
5983
6370
|
checked,
|
|
5984
6371
|
taskMarker,
|
|
5985
6372
|
paragraph
|
|
5986
6373
|
};
|
|
5987
6374
|
}
|
|
5988
|
-
function convertListItem(nodes, cursor, text, kind) {
|
|
6375
|
+
function convertListItem(nodes, cursor, text, column, kind) {
|
|
5989
6376
|
const content = [];
|
|
5990
6377
|
let taskChecked;
|
|
5991
6378
|
let taskMarker;
|
|
5992
6379
|
let order;
|
|
5993
6380
|
let marker;
|
|
5994
|
-
let
|
|
5995
|
-
let
|
|
6381
|
+
let markWidth = 1;
|
|
6382
|
+
let markTo;
|
|
6383
|
+
let markEndColumn = 0;
|
|
6384
|
+
let markerGap = 1;
|
|
6385
|
+
let contentColumn = column + markWidth + markerGap;
|
|
6386
|
+
let sawContent = false;
|
|
5996
6387
|
if (cursor.firstChild()) {
|
|
5997
6388
|
do {
|
|
5998
|
-
if (cursor.type.id
|
|
6389
|
+
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
5999
6390
|
if (cursor.type.id === LEZER_NODE_IDS.ListMark) {
|
|
6000
6391
|
const listMark = readListMark(cursor, text, kind);
|
|
6001
6392
|
marker = listMark.marker;
|
|
6002
6393
|
order = listMark.order;
|
|
6394
|
+
markWidth = cursor.to - cursor.from;
|
|
6395
|
+
markTo = cursor.to;
|
|
6003
6396
|
markEndColumn = measureContentColumn(text, cursor.to);
|
|
6004
6397
|
continue;
|
|
6005
6398
|
}
|
|
6399
|
+
if (!sawContent) {
|
|
6400
|
+
sawContent = true;
|
|
6401
|
+
const gap = markTo != null && text.lastIndexOf("\n", cursor.from - 1) < markTo ? measureContentColumn(text, cursor.from) - markEndColumn : 1;
|
|
6402
|
+
markerGap = gap >= 2 && gap <= 4 ? gap : 1;
|
|
6403
|
+
contentColumn = column + markWidth + markerGap;
|
|
6404
|
+
}
|
|
6006
6405
|
if (kind === "bullet" && cursor.type.id === LEZER_NODE_IDS.Task) {
|
|
6007
|
-
const task = convertTaskItem(nodes, cursor, text);
|
|
6406
|
+
const task = convertTaskItem(nodes, cursor, text, contentColumn);
|
|
6008
6407
|
taskChecked = task.checked;
|
|
6009
6408
|
taskMarker = task.taskMarker;
|
|
6010
6409
|
content.push(task.paragraph);
|
|
6011
6410
|
continue;
|
|
6012
6411
|
}
|
|
6013
|
-
content
|
|
6412
|
+
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, contentColumn));
|
|
6014
6413
|
} while (cursor.nextSibling());
|
|
6015
6414
|
cursor.parent();
|
|
6016
6415
|
}
|
|
6017
|
-
const gap = firstContentColumn != null && markEndColumn != null ? firstContentColumn - markEndColumn : 1;
|
|
6018
6416
|
const isTask = taskChecked != null;
|
|
6019
6417
|
const collapsed = !isTask && kind === "bullet" && marker === "+";
|
|
6020
6418
|
const attrs = {
|
|
@@ -6024,36 +6422,41 @@ function convertListItem(nodes, cursor, text, kind) {
|
|
|
6024
6422
|
collapsed,
|
|
6025
6423
|
marker: collapsed ? null : marker,
|
|
6026
6424
|
taskMarker,
|
|
6027
|
-
markerGap
|
|
6425
|
+
markerGap
|
|
6028
6426
|
};
|
|
6029
6427
|
return nodes.list(attrs, content);
|
|
6030
6428
|
}
|
|
6031
6429
|
function convertCodeBlock(nodes, cursor, text) {
|
|
6032
6430
|
const indented = cursor.type.id === LEZER_NODE_IDS.CodeBlock;
|
|
6431
|
+
const blockTo = cursor.to;
|
|
6033
6432
|
let language = "";
|
|
6034
6433
|
let code = "";
|
|
6035
6434
|
let fenceStyle = indented ? "indented" : null;
|
|
6036
6435
|
let fenceLength = null;
|
|
6037
6436
|
let sawOpeningMark = false;
|
|
6437
|
+
let pos = cursor.from;
|
|
6038
6438
|
if (cursor.firstChild()) {
|
|
6039
|
-
do
|
|
6439
|
+
do {
|
|
6440
|
+
code += uncoveredCode(text, pos, cursor.from);
|
|
6441
|
+
pos = cursor.to;
|
|
6040
6442
|
switch (cursor.type.id) {
|
|
6041
|
-
case LEZER_NODE_IDS.CodeMark:
|
|
6443
|
+
case LEZER_NODE_IDS.CodeMark:
|
|
6042
6444
|
if (sawOpeningMark) break;
|
|
6043
6445
|
sawOpeningMark = true;
|
|
6044
6446
|
if (text.charCodeAt(cursor.from) === 126) fenceStyle = "tilde";
|
|
6045
|
-
|
|
6046
|
-
if (markLength > 3) fenceLength = markLength;
|
|
6447
|
+
fenceLength = cursor.to - cursor.from;
|
|
6047
6448
|
break;
|
|
6048
|
-
}
|
|
6049
6449
|
case LEZER_NODE_IDS.CodeInfo:
|
|
6050
6450
|
language = text.slice(cursor.from, cursor.to);
|
|
6051
6451
|
break;
|
|
6052
6452
|
case LEZER_NODE_IDS.CodeText: code += text.slice(cursor.from, cursor.to);
|
|
6053
6453
|
}
|
|
6054
|
-
while (cursor.nextSibling());
|
|
6454
|
+
} while (cursor.nextSibling());
|
|
6055
6455
|
cursor.parent();
|
|
6456
|
+
code += uncoveredCode(text, pos, blockTo);
|
|
6056
6457
|
}
|
|
6458
|
+
if (fenceStyle === "indented" && !canIndentCode(code)) fenceStyle = null;
|
|
6459
|
+
if (fenceLength != null && fenceLength <= minFenceLength(code, fenceStyle === "tilde")) fenceLength = null;
|
|
6057
6460
|
return nodes.codeBlock({
|
|
6058
6461
|
language,
|
|
6059
6462
|
fenceStyle,
|
|
@@ -6061,6 +6464,17 @@ function convertCodeBlock(nodes, cursor, text) {
|
|
|
6061
6464
|
}, code);
|
|
6062
6465
|
}
|
|
6063
6466
|
/**
|
|
6467
|
+
* Text inside a code block that no child node covers is the whitespace in
|
|
6468
|
+
* front of a line's content: indentation, marker spaces, line breaks. The one
|
|
6469
|
+
* exception is a quote marker indented with a tab (`>\t \n\t>2`), where lezer
|
|
6470
|
+
* leaves the content after the marker uncovered too, so anything from a gap's
|
|
6471
|
+
* first non-whitespace character on is code the child list dropped.
|
|
6472
|
+
*/
|
|
6473
|
+
function uncoveredCode(text, from, to) {
|
|
6474
|
+
for (let index = from; index < to; index++) if (!isSpaceChar(text.charCodeAt(index))) return text.slice(index, to);
|
|
6475
|
+
return "";
|
|
6476
|
+
}
|
|
6477
|
+
/**
|
|
6064
6478
|
* A `$$` display math block is a code block whose `language` is `math`; the
|
|
6065
6479
|
* `dollar` fence style makes it serialize back to `$$` fences.
|
|
6066
6480
|
*/
|
|
@@ -6087,15 +6501,29 @@ function convertTable(nodes, cursor, text) {
|
|
|
6087
6501
|
cursor.parent();
|
|
6088
6502
|
}
|
|
6089
6503
|
const rows = [];
|
|
6504
|
+
let columnCount = aligns.length;
|
|
6090
6505
|
if (cursor.firstChild()) {
|
|
6091
6506
|
do {
|
|
6092
6507
|
const id = cursor.type.id;
|
|
6093
|
-
if (id
|
|
6094
|
-
|
|
6508
|
+
if (id !== LEZER_NODE_IDS.TableHeader && id !== LEZER_NODE_IDS.TableRow) continue;
|
|
6509
|
+
const cells = readTableCells(cursor, text);
|
|
6510
|
+
if (cells.length > columnCount) columnCount = cells.length;
|
|
6511
|
+
rows.push({
|
|
6512
|
+
isHeader: id === LEZER_NODE_IDS.TableHeader,
|
|
6513
|
+
cells
|
|
6514
|
+
});
|
|
6095
6515
|
} while (cursor.nextSibling());
|
|
6096
6516
|
cursor.parent();
|
|
6097
6517
|
}
|
|
6098
|
-
return nodes.table(rows)
|
|
6518
|
+
return nodes.table(rows.map(({ isHeader, cells }) => {
|
|
6519
|
+
const built = [];
|
|
6520
|
+
for (let column = 0; column < columnCount; column++) {
|
|
6521
|
+
const attrs = { align: aligns[column] ?? null };
|
|
6522
|
+
const paragraph = nodes.paragraph(cells[column] ?? "");
|
|
6523
|
+
built.push(isHeader ? nodes.tableHeaderCell(attrs, paragraph) : nodes.tableCell(attrs, paragraph));
|
|
6524
|
+
}
|
|
6525
|
+
return nodes.tableRow(built);
|
|
6526
|
+
}));
|
|
6099
6527
|
}
|
|
6100
6528
|
function parseDelimiterAligns(separator) {
|
|
6101
6529
|
return separator.split("|").map((segment) => segment.trim()).filter((segment) => segment !== "").map((segment) => {
|
|
@@ -6107,71 +6535,100 @@ function parseDelimiterAligns(separator) {
|
|
|
6107
6535
|
return null;
|
|
6108
6536
|
});
|
|
6109
6537
|
}
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
const
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6538
|
+
/**
|
|
6539
|
+
* A row's cell texts, indexed by column. `@lezer/markdown` emits no `TableCell`
|
|
6540
|
+
* for an empty cell, so the column comes from the pipes counted so far, not
|
|
6541
|
+
* from the cells seen so far.
|
|
6542
|
+
*/
|
|
6543
|
+
function readTableCells(cursor, text) {
|
|
6544
|
+
const cellTexts = [];
|
|
6545
|
+
if (!cursor.firstChild()) return cellTexts;
|
|
6546
|
+
const hasLeadingPipe = cursor.type.id === LEZER_NODE_IDS.TableDelimiter;
|
|
6547
|
+
let delimiterCount = 0;
|
|
6548
|
+
do {
|
|
6549
|
+
if (cursor.type.id === LEZER_NODE_IDS.TableDelimiter) {
|
|
6550
|
+
delimiterCount++;
|
|
6551
|
+
continue;
|
|
6552
|
+
}
|
|
6553
|
+
if (cursor.type.id !== LEZER_NODE_IDS.TableCell) continue;
|
|
6554
|
+
const column = delimiterCount - (hasLeadingPipe ? 1 : 0);
|
|
6555
|
+
if (column < 0) continue;
|
|
6556
|
+
while (cellTexts.length <= column) cellTexts.push("");
|
|
6557
|
+
cellTexts[column] = text.slice(cursor.from, cursor.to).trim().replaceAll(String.raw`\|`, "|");
|
|
6558
|
+
} while (cursor.nextSibling());
|
|
6559
|
+
cursor.parent();
|
|
6560
|
+
return cellTexts;
|
|
6131
6561
|
}
|
|
6132
6562
|
|
|
6133
6563
|
//#endregion
|
|
6134
6564
|
//#region src/converters/check-roundtrip.ts
|
|
6135
6565
|
function trimTrailingNewlines(text) {
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
return /^[\s>]*$/u.test(line);
|
|
6566
|
+
let end = text.length;
|
|
6567
|
+
while (end > 0 && text.charCodeAt(end - 1) === 10) end--;
|
|
6568
|
+
return text.slice(0, end);
|
|
6140
6569
|
}
|
|
6141
|
-
|
|
6142
|
-
|
|
6570
|
+
const CONTAINER_PREFIX_RE = /^(?:[\s>]|[-+*](?=\s|$)|\d{1,9}[.)](?=\s|$))+/u;
|
|
6571
|
+
function stripWhitespace(line) {
|
|
6572
|
+
return line.replaceAll(/\s/gu, "");
|
|
6143
6573
|
}
|
|
6144
|
-
function
|
|
6145
|
-
|
|
6574
|
+
function shortenFenceRun(line) {
|
|
6575
|
+
const fence = line.charCodeAt(0);
|
|
6576
|
+
if (fence !== 96 && fence !== 126) return line;
|
|
6577
|
+
let width = 1;
|
|
6578
|
+
while (line.charCodeAt(width) === fence) width++;
|
|
6579
|
+
return width > 3 ? line.slice(0, 3) + line.slice(width) : line;
|
|
6146
6580
|
}
|
|
6147
6581
|
const DELIMITER_CELL_RE = /^:?-+:?$/u;
|
|
6148
|
-
function
|
|
6149
|
-
|
|
6150
|
-
const alignsRight = cell.endsWith(":");
|
|
6151
|
-
if (alignsLeft && alignsRight) return ":-:";
|
|
6152
|
-
if (alignsLeft) return ":--";
|
|
6153
|
-
if (alignsRight) return "--:";
|
|
6154
|
-
return "---";
|
|
6582
|
+
function reduceCell(cell) {
|
|
6583
|
+
return stripWhitespace(shortenFenceRun(cell.replace(CONTAINER_PREFIX_RE, "")));
|
|
6155
6584
|
}
|
|
6156
6585
|
function canonicalizeTableRow(line) {
|
|
6157
6586
|
if (!line.includes("|")) return void 0;
|
|
6158
|
-
const
|
|
6159
|
-
|
|
6160
|
-
return
|
|
6587
|
+
const cells = line.replace(/^\s*\|/u, "").replace(/\|\s*$/u, "").split("|").map(reduceCell);
|
|
6588
|
+
if (cells.every((cell) => cell === "" || DELIMITER_CELL_RE.test(cell))) return "";
|
|
6589
|
+
return cells.filter((cell) => cell !== "").join("|");
|
|
6590
|
+
}
|
|
6591
|
+
/**
|
|
6592
|
+
* The lines of `text` that carry content, each reduced to that content. A line
|
|
6593
|
+
* left empty by dropping its markers, whitespace, and table structure (a blank
|
|
6594
|
+
* line, a bare `>`, a list marker whose content is on the next line, a
|
|
6595
|
+
* delimiter row or a lone delimiter cell) carries none and is skipped.
|
|
6596
|
+
*/
|
|
6597
|
+
function contentLines(text) {
|
|
6598
|
+
const lines = [];
|
|
6599
|
+
for (const line of text.split("\n")) {
|
|
6600
|
+
const bare = line.replace(CONTAINER_PREFIX_RE, "");
|
|
6601
|
+
const content = canonicalizeTableRow(bare) ?? stripWhitespace(shortenFenceRun(bare));
|
|
6602
|
+
if (content !== "" && !DELIMITER_CELL_RE.test(content)) lines.push(content);
|
|
6603
|
+
}
|
|
6604
|
+
return lines;
|
|
6161
6605
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6606
|
+
/**
|
|
6607
|
+
* Whether `wanted` appears in `found` as an ordered subsequence.
|
|
6608
|
+
*
|
|
6609
|
+
* The serializer may write a line the source never had: an unterminated fenced
|
|
6610
|
+
* block gets the closing fence it was missing. Such a line carries no content,
|
|
6611
|
+
* which the document comparison proves - so only a content line that fails to
|
|
6612
|
+
* come back out is loss.
|
|
6613
|
+
*/
|
|
6614
|
+
function containsInOrder(found, wanted) {
|
|
6615
|
+
let index = 0;
|
|
6616
|
+
for (const line of found) {
|
|
6617
|
+
if (index === wanted.length) return true;
|
|
6618
|
+
if (line === wanted[index]) index++;
|
|
6619
|
+
}
|
|
6620
|
+
return index === wanted.length;
|
|
6164
6621
|
}
|
|
6165
6622
|
/**
|
|
6166
6623
|
* Classify how `markdown` survives the editor's parse-then-serialize round trip.
|
|
6167
6624
|
*/
|
|
6168
6625
|
function checkRoundTrip(markdown, options = {}) {
|
|
6169
|
-
const
|
|
6170
|
-
const
|
|
6626
|
+
const { frontmatter } = options;
|
|
6627
|
+
const doc = markdownToDoc(markdown, { frontmatter });
|
|
6628
|
+
const serialized = docToMarkdown(doc, { frontmatter });
|
|
6171
6629
|
if (trimTrailingNewlines(serialized) === trimTrailingNewlines(markdown)) return "exact";
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
return before.length === after.length && before.every((line, i) => normalizeLine(line) === normalizeLine(after[i])) ? "normalizing" : "lossy";
|
|
6630
|
+
if (!containsInOrder(contentLines(serialized), contentLines(markdown))) return "lossy";
|
|
6631
|
+
return markdownToDoc(serialized, { frontmatter }).eq(doc) ? "normalizing" : "lossy";
|
|
6175
6632
|
}
|
|
6176
6633
|
|
|
6177
6634
|
//#endregion
|
|
@@ -7296,8 +7753,10 @@ var ImageMarkView = class {
|
|
|
7296
7753
|
if (next.src !== previous.src) return false;
|
|
7297
7754
|
this.#attrs = next;
|
|
7298
7755
|
if (this.#image && next.alt !== previous.alt) this.#image.alt = next.alt;
|
|
7299
|
-
if (this.#resizableRoot && (next.width !== previous.width || next.height !== previous.height))
|
|
7300
|
-
|
|
7756
|
+
if (this.#resizableRoot && (next.width !== previous.width || next.height !== previous.height)) {
|
|
7757
|
+
if (this.#image) applyImageDisplaySize(this.#resizableRoot, this.#image, next.width, next.height);
|
|
7758
|
+
else applySize(this.#resizableRoot, next.width, next.height);
|
|
7759
|
+
}
|
|
7301
7760
|
if (this.#tweetIframe && next.height !== previous.height) applyTweetHeight(this.#tweetIframe, next.height);
|
|
7302
7761
|
return true;
|
|
7303
7762
|
}
|