@meowdown/core 0.65.1 → 0.65.3
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 +6 -5
- package/dist/index.js +613 -171
- package/package.json +8 -7
package/dist/index.d.ts
CHANGED
|
@@ -14,11 +14,12 @@ import { VirtualElement } from "@floating-ui/dom";
|
|
|
14
14
|
/**
|
|
15
15
|
* How faithfully markdown survives a parse-then-serialize round trip:
|
|
16
16
|
* - `exact`: byte-identical (modulo the trailing newline).
|
|
17
|
-
* - `normalizing`: bytes differ, but only as layout the parser
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* table delimiter row rewritten to canonical dashes).
|
|
21
|
-
* - `lossy`: content changed - a
|
|
17
|
+
* - `normalizing`: bytes differ, but only as layout the parser reads back
|
|
18
|
+
* through - every content line survives and the output re-parses to the same
|
|
19
|
+
* document (e.g. a lazy continuation re-indented to its canonical column, or
|
|
20
|
+
* a table delimiter row rewritten to canonical dashes).
|
|
21
|
+
* - `lossy`: content changed - a content line differs or disappeared, or the
|
|
22
|
+
* output re-parses to a different document.
|
|
22
23
|
*/
|
|
23
24
|
type RoundTripFidelity = 'exact' | 'normalizing' | 'lossy';
|
|
24
25
|
/**
|
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;
|
|
@@ -440,13 +446,14 @@ function getSelectedAtomRange(state) {
|
|
|
440
446
|
return getSelectedRange(state, ATOM_SOURCE_MARK_NAMES);
|
|
441
447
|
}
|
|
442
448
|
function findSelectionAcrossBlockBoundary(state, pos, markNames, direction) {
|
|
449
|
+
const unitMarkNames = [...markNames, "mdPack"];
|
|
443
450
|
const $pos = state.doc.resolve(pos);
|
|
444
451
|
if (!(direction === -1 ? $pos.parentOffset === 0 : $pos.parentOffset === $pos.parent.content.size) || $pos.depth === 0) return;
|
|
445
|
-
const nearUnit = direction === -1 ? getMarkRangeAfter(state, pos,
|
|
452
|
+
const nearUnit = direction === -1 ? getMarkRangeAfter(state, pos, unitMarkNames) : getMarkRangeBefore(state, pos, unitMarkNames);
|
|
446
453
|
const boundary = direction === -1 ? $pos.before() : $pos.after();
|
|
447
454
|
const target = Selection.findFrom(state.doc.resolve(boundary), direction);
|
|
448
455
|
if (!target) return;
|
|
449
|
-
const farUnit = isTextSelection(target) ? direction === -1 ? getMarkRangeBefore(state, target.head,
|
|
456
|
+
const farUnit = isTextSelection(target) ? direction === -1 ? getMarkRangeBefore(state, target.head, unitMarkNames) : getMarkRangeAfter(state, target.head, unitMarkNames) : void 0;
|
|
450
457
|
if (nearUnit == null && farUnit == null) return;
|
|
451
458
|
return target;
|
|
452
459
|
}
|
|
@@ -919,12 +926,6 @@ function atomUnitToDOM(atom, sourceText) {
|
|
|
919
926
|
}
|
|
920
927
|
}
|
|
921
928
|
|
|
922
|
-
//#endregion
|
|
923
|
-
//#region src/extensions/node-names.ts
|
|
924
|
-
function isNodeOfType(node, name) {
|
|
925
|
-
return node.type.name === name;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
929
|
//#endregion
|
|
929
930
|
//#region src/extensions/heading.ts
|
|
930
931
|
/**
|
|
@@ -1293,27 +1294,6 @@ function definePlainTextPaste() {
|
|
|
1293
1294
|
}));
|
|
1294
1295
|
}
|
|
1295
1296
|
|
|
1296
|
-
//#endregion
|
|
1297
|
-
//#region src/utils/backticks.ts
|
|
1298
|
-
/**
|
|
1299
|
-
* Length of the longest run of `charCode` in `text`, at least `min`.
|
|
1300
|
-
*/
|
|
1301
|
-
function longestCharRun(text, charCode, min = 0) {
|
|
1302
|
-
let longest = min;
|
|
1303
|
-
let run = 0;
|
|
1304
|
-
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === charCode) {
|
|
1305
|
-
run++;
|
|
1306
|
-
if (run > longest) longest = run;
|
|
1307
|
-
} else run = 0;
|
|
1308
|
-
return longest;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Length of the longest run of backticks in `text`, at least `min`.
|
|
1312
|
-
*/
|
|
1313
|
-
function longestBacktickRun(text, min = 0) {
|
|
1314
|
-
return longestCharRun(text, 96, min);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
1297
|
//#endregion
|
|
1318
1298
|
//#region src/converters/pm-to-md.ts
|
|
1319
1299
|
/**
|
|
@@ -1375,7 +1355,8 @@ function emitHeading(node, out) {
|
|
|
1375
1355
|
out.closeBlock();
|
|
1376
1356
|
return;
|
|
1377
1357
|
}
|
|
1378
|
-
|
|
1358
|
+
const prefix = HEADING_PREFIX[attrs.level] ?? "# ";
|
|
1359
|
+
out.write(node.content.size > 0 ? prefix : prefix.slice(0, -1));
|
|
1379
1360
|
emitInlineChildren(node, out);
|
|
1380
1361
|
const closingHashes = attrs.closingHashes;
|
|
1381
1362
|
if (closingHashes != null && closingHashes > 0) out.write(" " + "#".repeat(closingHashes));
|
|
@@ -1389,10 +1370,22 @@ var MdOut = class {
|
|
|
1389
1370
|
this.atLineStart = true;
|
|
1390
1371
|
this.deferredBlankPrefix = null;
|
|
1391
1372
|
}
|
|
1392
|
-
|
|
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) {
|
|
1393
1385
|
if (text === "") return;
|
|
1394
1386
|
this.emitDeferredBlankLine();
|
|
1395
1387
|
if (this.atLineStart) {
|
|
1388
|
+
if (this.pendingFirst !== null && isThematicBreak(this.pendingFirst + text)) this.breakMarkerLine();
|
|
1396
1389
|
this.parts.push(this.pendingFirst ?? this.linePrefix);
|
|
1397
1390
|
this.pendingFirst = null;
|
|
1398
1391
|
this.atLineStart = false;
|
|
@@ -1402,9 +1395,11 @@ var MdOut = class {
|
|
|
1402
1395
|
return;
|
|
1403
1396
|
}
|
|
1404
1397
|
const lines = text.split("\n");
|
|
1398
|
+
const lazy = lazyLines && this.linePrefix !== "";
|
|
1405
1399
|
for (let i = 0; i < lines.length; i++) {
|
|
1406
|
-
|
|
1407
|
-
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);
|
|
1408
1403
|
}
|
|
1409
1404
|
}
|
|
1410
1405
|
/**
|
|
@@ -1431,7 +1426,8 @@ var MdOut = class {
|
|
|
1431
1426
|
closeBlock() {
|
|
1432
1427
|
if (this.atLineStart && this.pendingFirst !== null) {
|
|
1433
1428
|
this.emitDeferredBlankLine();
|
|
1434
|
-
this.
|
|
1429
|
+
const marker = this.pendingFirst;
|
|
1430
|
+
this.parts.push(marker.endsWith("] ") ? marker : marker.trimEnd());
|
|
1435
1431
|
this.pendingFirst = null;
|
|
1436
1432
|
this.atLineStart = false;
|
|
1437
1433
|
}
|
|
@@ -1440,6 +1436,18 @@ var MdOut = class {
|
|
|
1440
1436
|
this.deferredBlankPrefix = this.linePrefix;
|
|
1441
1437
|
}
|
|
1442
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
|
+
/**
|
|
1443
1451
|
* Cancel the blank line deferred by the last `closeBlock`, so the next
|
|
1444
1452
|
* write starts directly on the following line. Used between the blocks of
|
|
1445
1453
|
* a tight list, where markdown separates items (and an item's paragraph
|
|
@@ -1461,14 +1469,24 @@ var MdOut = class {
|
|
|
1461
1469
|
this.linePrefix = savedLine + continuation;
|
|
1462
1470
|
if (firstLine !== null) {
|
|
1463
1471
|
const base = savedFirst ?? savedLine;
|
|
1464
|
-
|
|
1472
|
+
if (savedFirst !== null && isThematicBreak(base + firstLine)) {
|
|
1473
|
+
this.breakMarkerLine();
|
|
1474
|
+
this.pendingFirst = savedLine + firstLine;
|
|
1475
|
+
} else this.pendingFirst = base + firstLine;
|
|
1465
1476
|
}
|
|
1466
1477
|
fn();
|
|
1467
1478
|
this.linePrefix = savedLine;
|
|
1468
1479
|
this.pendingFirst = firstLine !== null ? null : savedFirst;
|
|
1469
1480
|
}
|
|
1470
1481
|
finish() {
|
|
1471
|
-
|
|
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";
|
|
1472
1490
|
}
|
|
1473
1491
|
emitDeferredBlankLine() {
|
|
1474
1492
|
const prefix = this.deferredBlankPrefix;
|
|
@@ -1547,13 +1565,39 @@ function emitBlockChildren(node, out, tightItem = false) {
|
|
|
1547
1565
|
while (runEnd < count && isNodeOfType(node.child(runEnd), "list")) runEnd++;
|
|
1548
1566
|
const tightRun = isTightRun(node, index, runEnd);
|
|
1549
1567
|
for (let item = index; item < runEnd; item++) {
|
|
1550
|
-
|
|
1551
|
-
|
|
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);
|
|
1552
1574
|
}
|
|
1553
1575
|
index = runEnd;
|
|
1554
1576
|
}
|
|
1555
1577
|
}
|
|
1556
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
|
+
/**
|
|
1557
1601
|
* A run of sibling `list` nodes serializes tight iff every item is "simple":
|
|
1558
1602
|
* at most one leading paragraph, then only nested lists. Any other shape
|
|
1559
1603
|
* (multiple paragraphs, a blockquote, a code block, …) needs blank-line
|
|
@@ -1575,6 +1619,177 @@ function isTightItem(item) {
|
|
|
1575
1619
|
return true;
|
|
1576
1620
|
}
|
|
1577
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
|
+
/**
|
|
1578
1793
|
* Walk inline children writing text directly. The schema has no marks, so
|
|
1579
1794
|
* every inline child is currently a text node - but going through this
|
|
1580
1795
|
* loop instead of `node.textContent` avoids one intermediate string
|
|
@@ -1582,23 +1797,45 @@ function isTightItem(item) {
|
|
|
1582
1797
|
*/
|
|
1583
1798
|
function emitInlineChildren(node, out) {
|
|
1584
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);
|
|
1585
1803
|
for (let i = 0; i < count; i++) {
|
|
1586
1804
|
const child = node.child(i);
|
|
1587
|
-
if (child.isText && child.text) out.write(child.text);
|
|
1805
|
+
if (child.isText && child.text) out.write(child.text, lazy);
|
|
1588
1806
|
}
|
|
1589
1807
|
}
|
|
1590
1808
|
function emitList(node, out, tight) {
|
|
1591
|
-
const { kind,
|
|
1592
|
-
const
|
|
1593
|
-
const orderMarker = marker === ")" ? ")" : ".";
|
|
1809
|
+
const { kind, order, taskMarker, markerGap, checked } = node.attrs;
|
|
1810
|
+
const markerChar = listMarkerChar(node);
|
|
1594
1811
|
const checkMark = taskMarker === "X" ? "X" : "x";
|
|
1595
1812
|
const gap = Math.min(Math.max(markerGap ?? 1, 1), 4);
|
|
1596
|
-
const prefix = `${kind === "ordered" ? `${order ?? 1}${
|
|
1813
|
+
const prefix = `${kind === "ordered" ? `${order ?? 1}${markerChar}` : markerChar}${" ".repeat(gap)}`;
|
|
1597
1814
|
const outputMarker = kind === "task" ? `${prefix}[${checked ? checkMark : " "}] ` : prefix;
|
|
1598
1815
|
const continuation = " ".repeat(prefix.length);
|
|
1599
1816
|
out.withPrefix(continuation, outputMarker, () => emitBlockChildren(node, out, tight));
|
|
1600
1817
|
out.closeBlock();
|
|
1601
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
|
+
}
|
|
1602
1839
|
function emitCodeBlock(node, out) {
|
|
1603
1840
|
const attrs = node.attrs;
|
|
1604
1841
|
const language = attrs.language || "";
|
|
@@ -1623,10 +1860,9 @@ function emitCodeBlock(node, out) {
|
|
|
1623
1860
|
return;
|
|
1624
1861
|
}
|
|
1625
1862
|
const tilde = attrs.fenceStyle === "tilde";
|
|
1626
|
-
const
|
|
1627
|
-
const fence = (tilde ? "~" : "`").repeat(Math.max(attrs.fenceLength ?? 0, minWidth));
|
|
1863
|
+
const fence = (tilde ? "~" : "`").repeat(Math.max(attrs.fenceLength ?? 0, minFenceLength(code, tilde)));
|
|
1628
1864
|
out.write(fence);
|
|
1629
|
-
if (language) out.write(language);
|
|
1865
|
+
if (language) out.write(language.startsWith(tilde ? "~" : "`") ? " " + language : language);
|
|
1630
1866
|
out.write("\n");
|
|
1631
1867
|
if (code) {
|
|
1632
1868
|
out.write(code);
|
|
@@ -1636,16 +1872,53 @@ function emitCodeBlock(node, out) {
|
|
|
1636
1872
|
out.closeBlock();
|
|
1637
1873
|
}
|
|
1638
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
|
+
/**
|
|
1639
1913
|
* Indent `code` for an indented code block, or return `undefined` for shapes
|
|
1640
|
-
* the indented form cannot express
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
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.
|
|
1644
1918
|
*/
|
|
1645
1919
|
function toIndentedCode(code) {
|
|
1646
|
-
if (code
|
|
1920
|
+
if (!canIndentCode(code)) return void 0;
|
|
1647
1921
|
const lines = code.split("\n");
|
|
1648
|
-
if (lines[0] === "" || lines[lines.length - 1] === "") return void 0;
|
|
1649
1922
|
for (let i = 0; i < lines.length; i++) if (lines[i] !== "") lines[i] = ` ${lines[i]}`;
|
|
1650
1923
|
return lines.join("\n");
|
|
1651
1924
|
}
|
|
@@ -3816,6 +4089,27 @@ function defineInlineMarks() {
|
|
|
3816
4089
|
return union(defineMdMark(), defineMdEm(), defineMdStrong(), defineMdCode(), defineMdLinkText(), defineMdLinkUri(), defineMdLinkTitle(), defineMdDel(), defineMdHighlight(), defineMdTag(), defineMdWikilink(), defineMdImage(), defineMdFile(), defineMdMath(), defineMdPack());
|
|
3817
4090
|
}
|
|
3818
4091
|
|
|
4092
|
+
//#endregion
|
|
4093
|
+
//#region src/utils/backticks.ts
|
|
4094
|
+
/**
|
|
4095
|
+
* Length of the longest run of `charCode` in `text`, at least `min`.
|
|
4096
|
+
*/
|
|
4097
|
+
function longestCharRun(text, charCode, min = 0) {
|
|
4098
|
+
let longest = min;
|
|
4099
|
+
let run = 0;
|
|
4100
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === charCode) {
|
|
4101
|
+
run++;
|
|
4102
|
+
if (run > longest) longest = run;
|
|
4103
|
+
} else run = 0;
|
|
4104
|
+
return longest;
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Length of the longest run of backticks in `text`, at least `min`.
|
|
4108
|
+
*/
|
|
4109
|
+
function longestBacktickRun(text, min = 0) {
|
|
4110
|
+
return longestCharRun(text, 96, min);
|
|
4111
|
+
}
|
|
4112
|
+
|
|
3819
4113
|
//#endregion
|
|
3820
4114
|
//#region src/extensions/inline-toggle.ts
|
|
3821
4115
|
const TOGGLE_SPECS = {
|
|
@@ -5695,7 +5989,7 @@ function markdownToDoc(markdown, options = {}) {
|
|
|
5695
5989
|
frontmatterBody = body;
|
|
5696
5990
|
if (matchLength) rest = markdown.slice(matchLength);
|
|
5697
5991
|
}
|
|
5698
|
-
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest);
|
|
5992
|
+
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest, 0);
|
|
5699
5993
|
return nodes.doc(frontmatterBody === void 0 ? {} : { frontmatter: frontmatterBody }, blocks);
|
|
5700
5994
|
}
|
|
5701
5995
|
/**
|
|
@@ -5717,19 +6011,38 @@ function matchFrontmatter(markdown) {
|
|
|
5717
6011
|
* and flattening any node converter that returns multiple siblings
|
|
5718
6012
|
* (lists are the main case).
|
|
5719
6013
|
*/
|
|
5720
|
-
function collectBlocks(nodes, cursor, text) {
|
|
6014
|
+
function collectBlocks(nodes, cursor, text, column) {
|
|
5721
6015
|
const out = [];
|
|
5722
6016
|
if (!cursor.firstChild()) return out;
|
|
5723
6017
|
let previousTo;
|
|
5724
6018
|
do {
|
|
5725
6019
|
if (previousTo != null) appendGapParagraphs(out, nodes, text, previousTo, cursor.from);
|
|
5726
6020
|
previousTo = cursor.to;
|
|
5727
|
-
out
|
|
6021
|
+
appendBlocks(out, nodes, convertBlock(nodes, cursor, text, column));
|
|
5728
6022
|
} while (cursor.nextSibling());
|
|
5729
6023
|
cursor.parent();
|
|
5730
6024
|
return out;
|
|
5731
6025
|
}
|
|
5732
6026
|
/**
|
|
6027
|
+
* Append `blocks`, dropping the indented spelling from a code block that lands
|
|
6028
|
+
* right after a list: four columns of indentation would put it inside the item
|
|
6029
|
+
* above instead of after it, so it can only be written as a fence.
|
|
6030
|
+
*/
|
|
6031
|
+
function appendBlocks(out, nodes, blocks) {
|
|
6032
|
+
for (const block of blocks) {
|
|
6033
|
+
const attrs = block.attrs;
|
|
6034
|
+
if (attrs.fenceStyle === "indented" && isNodeOfType(block, "codeBlock") && out.length > 0 && isNodeOfType(out[out.length - 1], "list")) {
|
|
6035
|
+
out.push(nodes.codeBlock({
|
|
6036
|
+
language: attrs.language,
|
|
6037
|
+
fenceStyle: null,
|
|
6038
|
+
fenceLength: attrs.fenceLength
|
|
6039
|
+
}, block.textContent));
|
|
6040
|
+
continue;
|
|
6041
|
+
}
|
|
6042
|
+
out.push(block);
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
/**
|
|
5733
6046
|
* Blank lines between two sibling blocks are content: a run of K blank lines
|
|
5734
6047
|
* is one block separator plus K-1 empty paragraphs. The gap slice between the
|
|
5735
6048
|
* siblings' ranges holds only line terminators and structural prefixes
|
|
@@ -5741,24 +6054,24 @@ function appendGapParagraphs(out, nodes, text, gapFrom, gapTo) {
|
|
|
5741
6054
|
for (let i = gapFrom; i < gapTo; i++) if (text.charCodeAt(i) === 10) newlineCount++;
|
|
5742
6055
|
for (let i = 2; i < newlineCount; i++) out.push(nodes.paragraph());
|
|
5743
6056
|
}
|
|
5744
|
-
function convertBlock(nodes, cursor, text) {
|
|
6057
|
+
function convertBlock(nodes, cursor, text, column) {
|
|
5745
6058
|
switch (cursor.type.id) {
|
|
5746
|
-
case LEZER_NODE_IDS.ATXHeading1: return [convertHeading(nodes, cursor, text, 1, false)];
|
|
5747
|
-
case LEZER_NODE_IDS.ATXHeading2: return [convertHeading(nodes, cursor, text, 2, false)];
|
|
5748
|
-
case LEZER_NODE_IDS.ATXHeading3: return [convertHeading(nodes, cursor, text, 3, false)];
|
|
5749
|
-
case LEZER_NODE_IDS.ATXHeading4: return [convertHeading(nodes, cursor, text, 4, false)];
|
|
5750
|
-
case LEZER_NODE_IDS.ATXHeading5: return [convertHeading(nodes, cursor, text, 5, false)];
|
|
5751
|
-
case LEZER_NODE_IDS.ATXHeading6: return [convertHeading(nodes, cursor, text, 6, false)];
|
|
5752
|
-
case LEZER_NODE_IDS.SetextHeading1: return [convertHeading(nodes, cursor, text, 1, true)];
|
|
5753
|
-
case LEZER_NODE_IDS.SetextHeading2: return [convertHeading(nodes, cursor, text, 2, true)];
|
|
5754
|
-
case LEZER_NODE_IDS.Paragraph: return [convertParagraph(nodes, cursor, text)];
|
|
5755
|
-
case LEZER_NODE_IDS.LinkReference: return [convertParagraph(nodes, cursor, text)];
|
|
5756
|
-
case LEZER_NODE_IDS.CommentBlock: return [convertHTMLComment(nodes, cursor, text)];
|
|
6059
|
+
case LEZER_NODE_IDS.ATXHeading1: return [convertHeading(nodes, cursor, text, column, 1, false)];
|
|
6060
|
+
case LEZER_NODE_IDS.ATXHeading2: return [convertHeading(nodes, cursor, text, column, 2, false)];
|
|
6061
|
+
case LEZER_NODE_IDS.ATXHeading3: return [convertHeading(nodes, cursor, text, column, 3, false)];
|
|
6062
|
+
case LEZER_NODE_IDS.ATXHeading4: return [convertHeading(nodes, cursor, text, column, 4, false)];
|
|
6063
|
+
case LEZER_NODE_IDS.ATXHeading5: return [convertHeading(nodes, cursor, text, column, 5, false)];
|
|
6064
|
+
case LEZER_NODE_IDS.ATXHeading6: return [convertHeading(nodes, cursor, text, column, 6, false)];
|
|
6065
|
+
case LEZER_NODE_IDS.SetextHeading1: return [convertHeading(nodes, cursor, text, column, 1, true)];
|
|
6066
|
+
case LEZER_NODE_IDS.SetextHeading2: return [convertHeading(nodes, cursor, text, column, 2, true)];
|
|
6067
|
+
case LEZER_NODE_IDS.Paragraph: return [convertParagraph(nodes, cursor, text, column)];
|
|
6068
|
+
case LEZER_NODE_IDS.LinkReference: return [convertParagraph(nodes, cursor, text, column)];
|
|
6069
|
+
case LEZER_NODE_IDS.CommentBlock: return [convertHTMLComment(nodes, cursor, text, column)];
|
|
5757
6070
|
case LEZER_NODE_IDS.HTMLBlock:
|
|
5758
|
-
case LEZER_NODE_IDS.ProcessingInstructionBlock: return [convertParagraph(nodes, cursor, text)];
|
|
6071
|
+
case LEZER_NODE_IDS.ProcessingInstructionBlock: return [convertParagraph(nodes, cursor, text, column)];
|
|
5759
6072
|
case LEZER_NODE_IDS.Blockquote: return [convertBlockquote(nodes, cursor, text)];
|
|
5760
|
-
case LEZER_NODE_IDS.BulletList: return convertList(nodes, cursor, text, "bullet");
|
|
5761
|
-
case LEZER_NODE_IDS.OrderedList: return convertList(nodes, cursor, text, "ordered");
|
|
6073
|
+
case LEZER_NODE_IDS.BulletList: return convertList(nodes, cursor, text, column, "bullet");
|
|
6074
|
+
case LEZER_NODE_IDS.OrderedList: return convertList(nodes, cursor, text, column, "ordered");
|
|
5762
6075
|
case LEZER_NODE_IDS.FencedCode:
|
|
5763
6076
|
case LEZER_NODE_IDS.CodeBlock: return [convertCodeBlock(nodes, cursor, text)];
|
|
5764
6077
|
case LEZER_NODE_IDS.BlockMath: return [convertBlockMath(nodes, cursor, text)];
|
|
@@ -5767,14 +6080,14 @@ function convertBlock(nodes, cursor, text) {
|
|
|
5767
6080
|
return [nodes.horizontalRule({ marker: marker === "---" ? null : marker })];
|
|
5768
6081
|
}
|
|
5769
6082
|
case LEZER_NODE_IDS.Table: return [convertTable(nodes, cursor, text)];
|
|
5770
|
-
case LEZER_NODE_IDS.Task: return [convertParagraph(nodes, cursor, text)];
|
|
6083
|
+
case LEZER_NODE_IDS.Task: return [convertParagraph(nodes, cursor, text, column)];
|
|
5771
6084
|
default:
|
|
5772
6085
|
if (text.slice(cursor.from, cursor.to).trim() === "") return [];
|
|
5773
6086
|
console.warn(`[meowdown] unsupported lezer block "${cursor.type.name}"`);
|
|
5774
|
-
return [convertParagraph(nodes, cursor, text)];
|
|
6087
|
+
return [convertParagraph(nodes, cursor, text, column)];
|
|
5775
6088
|
}
|
|
5776
6089
|
}
|
|
5777
|
-
function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
6090
|
+
function convertHeading(nodes, cursor, text, column, level, isSetext) {
|
|
5778
6091
|
const headingFrom = cursor.from;
|
|
5779
6092
|
let contentStart = cursor.from;
|
|
5780
6093
|
let contentEnd = cursor.to;
|
|
@@ -5791,13 +6104,14 @@ function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
|
5791
6104
|
lastTo = cursor.to;
|
|
5792
6105
|
} while (cursor.nextSibling());
|
|
5793
6106
|
if (lastId === LEZER_NODE_IDS.HeaderMark && lastFrom > contentStart) {
|
|
5794
|
-
contentEnd = lastFrom;
|
|
6107
|
+
contentEnd = isSetext ? text.lastIndexOf("\n", lastFrom - 1) : lastFrom;
|
|
5795
6108
|
trailingMarkFrom = lastFrom;
|
|
5796
6109
|
trailingMarkTo = lastTo;
|
|
5797
6110
|
}
|
|
5798
6111
|
cursor.parent();
|
|
5799
6112
|
}
|
|
5800
|
-
const
|
|
6113
|
+
const raw = dedentContinuation(readLeafText(cursor, text, contentStart, contentEnd), column);
|
|
6114
|
+
const content = isSetext ? raw : trimGap(raw);
|
|
5801
6115
|
const setextUnderline = isSetext ? countUnderlineChars(text, trailingMarkFrom, trailingMarkTo) || 1 : null;
|
|
5802
6116
|
const closingHashes = !isSetext && trailingMarkFrom >= 0 ? countHashChars(text, trailingMarkFrom, trailingMarkTo) || null : null;
|
|
5803
6117
|
return nodes.heading({
|
|
@@ -5807,6 +6121,20 @@ function convertHeading(nodes, cursor, text, level, isSetext) {
|
|
|
5807
6121
|
}, content);
|
|
5808
6122
|
}
|
|
5809
6123
|
/**
|
|
6124
|
+
* Trim the whitespace lezer's marker gap can contain: space, tab and the line
|
|
6125
|
+
* ends (`space` in `@lezer/markdown`, matching `isSpaceChar`). A plain
|
|
6126
|
+
* `String.prototype.trim` would also take a no-break or an ideographic space,
|
|
6127
|
+
* which lezer reads as heading text - and a `#` left standing next to one flips
|
|
6128
|
+
* into a closing run on the way back (`# #\u{A0}`).
|
|
6129
|
+
*/
|
|
6130
|
+
function trimGap(text) {
|
|
6131
|
+
let start = 0;
|
|
6132
|
+
let end = text.length;
|
|
6133
|
+
while (start < end && isSpaceChar(text.charCodeAt(start))) start++;
|
|
6134
|
+
while (end > start && isSpaceChar(text.charCodeAt(end - 1))) end--;
|
|
6135
|
+
return text.slice(start, end);
|
|
6136
|
+
}
|
|
6137
|
+
/**
|
|
5810
6138
|
* Count the `=` / `-` characters in a setext underline run.
|
|
5811
6139
|
*/
|
|
5812
6140
|
function countUnderlineChars(text, from, to) {
|
|
@@ -5839,16 +6167,23 @@ function measureContentColumn(text, from) {
|
|
|
5839
6167
|
return col;
|
|
5840
6168
|
}
|
|
5841
6169
|
/**
|
|
5842
|
-
* Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4`
|
|
6170
|
+
* Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4`
|
|
6171
|
+
* columns. Whitespace the container never wrote is left alone: a tab that would
|
|
6172
|
+
* reach past `column` stands for more columns than the container takes, and a
|
|
6173
|
+
* line whose indentation stops short of `column` was written lazily, without any
|
|
6174
|
+
* prefix at all.
|
|
5843
6175
|
*/
|
|
5844
6176
|
function sliceColumn(line, column) {
|
|
5845
6177
|
let col = 0;
|
|
5846
6178
|
let index = 0;
|
|
5847
|
-
while (
|
|
6179
|
+
while (col < column) {
|
|
5848
6180
|
const code = line.charCodeAt(index);
|
|
5849
6181
|
if (code === 32) col += 1;
|
|
5850
|
-
else if (code === 9)
|
|
5851
|
-
|
|
6182
|
+
else if (code === 9) {
|
|
6183
|
+
const width = 4 - col % 4;
|
|
6184
|
+
if (col + width > column) break;
|
|
6185
|
+
col += width;
|
|
6186
|
+
} else return line;
|
|
5852
6187
|
index++;
|
|
5853
6188
|
}
|
|
5854
6189
|
return line.slice(index);
|
|
@@ -5869,6 +6204,69 @@ function dedentContinuation(content, column) {
|
|
|
5869
6204
|
return content.split("\n").map((line, index) => index === 0 ? line : sliceColumn(line, column)).join("\n");
|
|
5870
6205
|
}
|
|
5871
6206
|
/**
|
|
6207
|
+
* The blockquote markers inside `from`..`to`, as a flat run of `from, to` pairs
|
|
6208
|
+
* in document order. A marker can sit at any depth: a paragraph carries its own,
|
|
6209
|
+
* while a link reference's lands inside the `LinkLabel` that spans the break.
|
|
6210
|
+
* The cursor ends where it started.
|
|
6211
|
+
*/
|
|
6212
|
+
function collectQuoteMarks(cursor, marks, from, to) {
|
|
6213
|
+
if (!cursor.firstChild()) return;
|
|
6214
|
+
do {
|
|
6215
|
+
if (cursor.from >= to) break;
|
|
6216
|
+
if (cursor.to <= from) continue;
|
|
6217
|
+
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) {
|
|
6218
|
+
marks.push(cursor.from, cursor.to);
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
collectQuoteMarks(cursor, marks, from, to);
|
|
6222
|
+
} while (cursor.nextSibling());
|
|
6223
|
+
cursor.parent();
|
|
6224
|
+
}
|
|
6225
|
+
/**
|
|
6226
|
+
* The source between `from` and `to`, minus the blockquote markers inside it.
|
|
6227
|
+
*
|
|
6228
|
+
* In block-only parsing a leaf block has no inline children, with one
|
|
6229
|
+
* exception: lezer leaves the `QuoteMark` of every continuation line of a
|
|
6230
|
+
* multi-line block (`> l1\n> l2`) inside the block's own span. A marker takes
|
|
6231
|
+
* the whole line prefix with it, the single space after it included, exactly as
|
|
6232
|
+
* the serializer's own prefix puts it back. Only a space: a tab after the marker
|
|
6233
|
+
* stands for the columns up to the next tab stop, more than the prefix writes
|
|
6234
|
+
* back, so it stays in the text as the indentation it is. A line with no marker
|
|
6235
|
+
* carried no prefix at all - it is a lazy continuation, and every column on it
|
|
6236
|
+
* is its own. Markers outside `from`..`to` (an ATX heading's own marks, a setext
|
|
6237
|
+
* underline's line) are left alone. The cursor ends where it started.
|
|
6238
|
+
*/
|
|
6239
|
+
function readLeafText(cursor, text, from, to) {
|
|
6240
|
+
const lineBreak = text.indexOf("\n", from);
|
|
6241
|
+
if (lineBreak < 0 || lineBreak >= to) return text.slice(from, to);
|
|
6242
|
+
const marks = [];
|
|
6243
|
+
collectQuoteMarks(cursor, marks, from, to);
|
|
6244
|
+
let content = "";
|
|
6245
|
+
let pos = from;
|
|
6246
|
+
for (let index = 0; index < marks.length; index += 2) {
|
|
6247
|
+
content += text.slice(pos, Math.max(pos, text.lastIndexOf("\n", marks[index]) + 1));
|
|
6248
|
+
pos = marks[index + 1];
|
|
6249
|
+
if (text.charCodeAt(pos) === 32) pos += 1;
|
|
6250
|
+
}
|
|
6251
|
+
return trimTrailingBlankLines(content + text.slice(pos, to));
|
|
6252
|
+
}
|
|
6253
|
+
/**
|
|
6254
|
+
* Drop the blank lines a block ends with, the line break in front of them
|
|
6255
|
+
* included. A raw HTML or processing-instruction block with no closing sequence
|
|
6256
|
+
* runs its span to the end of the document and swallows them; a leaf block's
|
|
6257
|
+
* text ends where its last line does, which is where the serializer ends its
|
|
6258
|
+
* own output (`MdOut.finish`), so recording more would lose it on the way back.
|
|
6259
|
+
*/
|
|
6260
|
+
function trimTrailingBlankLines(content) {
|
|
6261
|
+
let end = content.length;
|
|
6262
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
6263
|
+
const code = content.charCodeAt(index);
|
|
6264
|
+
if (code === 10) end = index;
|
|
6265
|
+
else if (code !== 32 && code !== 9) break;
|
|
6266
|
+
}
|
|
6267
|
+
return content.slice(0, end);
|
|
6268
|
+
}
|
|
6269
|
+
/**
|
|
5872
6270
|
* Build a paragraph from raw markdown content, dedenting continuation lines so
|
|
5873
6271
|
* the serializer's own line prefix does not double the indent. A soft line break
|
|
5874
6272
|
* stays a literal `\n` in a single text node; the paragraph spec's
|
|
@@ -5877,25 +6275,8 @@ function dedentContinuation(content, column) {
|
|
|
5877
6275
|
function buildParagraph(nodes, content, column) {
|
|
5878
6276
|
return nodes.paragraph(dedentContinuation(content, column));
|
|
5879
6277
|
}
|
|
5880
|
-
function convertParagraph(nodes, cursor, text) {
|
|
5881
|
-
|
|
5882
|
-
const to = cursor.to;
|
|
5883
|
-
const column = measureContentColumn(text, from);
|
|
5884
|
-
if (cursor.firstChild()) {
|
|
5885
|
-
let content = "";
|
|
5886
|
-
let pos = from;
|
|
5887
|
-
do
|
|
5888
|
-
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) {
|
|
5889
|
-
content += text.slice(pos, cursor.from);
|
|
5890
|
-
pos = cursor.to;
|
|
5891
|
-
if (isSpaceChar(text.charCodeAt(pos))) pos += 1;
|
|
5892
|
-
}
|
|
5893
|
-
while (cursor.nextSibling());
|
|
5894
|
-
cursor.parent();
|
|
5895
|
-
content += text.slice(pos, to);
|
|
5896
|
-
return buildParagraph(nodes, content, column);
|
|
5897
|
-
}
|
|
5898
|
-
return buildParagraph(nodes, text.slice(from, to), column);
|
|
6278
|
+
function convertParagraph(nodes, cursor, text, column) {
|
|
6279
|
+
return buildParagraph(nodes, readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
5899
6280
|
}
|
|
5900
6281
|
/**
|
|
5901
6282
|
* Build the invisible `htmlComment` node from a `CommentBlock`. The raw comment
|
|
@@ -5903,11 +6284,14 @@ function convertParagraph(nodes, cursor, text) {
|
|
|
5903
6284
|
* continuation lines are dedented like a paragraph's so the serializer's own
|
|
5904
6285
|
* line prefix re-applies the container indent instead of doubling it.
|
|
5905
6286
|
*/
|
|
5906
|
-
function convertHTMLComment(nodes, cursor, text) {
|
|
5907
|
-
const
|
|
5908
|
-
const content = dedentContinuation(text.slice(cursor.from, cursor.to), column);
|
|
6287
|
+
function convertHTMLComment(nodes, cursor, text, column) {
|
|
6288
|
+
const content = dedentContinuation(readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
5909
6289
|
return nodes.htmlComment({ content });
|
|
5910
6290
|
}
|
|
6291
|
+
/**
|
|
6292
|
+
* A blockquote's children start at column 0: every column an enclosing container
|
|
6293
|
+
* wrote sits in front of the `> ` marker on the line, and comes off with it.
|
|
6294
|
+
*/
|
|
5911
6295
|
function convertBlockquote(nodes, cursor, text) {
|
|
5912
6296
|
const content = [];
|
|
5913
6297
|
if (cursor.firstChild()) {
|
|
@@ -5916,17 +6300,17 @@ function convertBlockquote(nodes, cursor, text) {
|
|
|
5916
6300
|
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
5917
6301
|
if (previousTo != null) appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
5918
6302
|
previousTo = cursor.to;
|
|
5919
|
-
content
|
|
6303
|
+
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, 0));
|
|
5920
6304
|
} while (cursor.nextSibling());
|
|
5921
6305
|
cursor.parent();
|
|
5922
6306
|
}
|
|
5923
6307
|
return nodes.blockquote(content);
|
|
5924
6308
|
}
|
|
5925
|
-
function convertList(nodes, cursor, text, kind) {
|
|
6309
|
+
function convertList(nodes, cursor, text, column, kind) {
|
|
5926
6310
|
const items = [];
|
|
5927
6311
|
if (cursor.firstChild()) {
|
|
5928
6312
|
do
|
|
5929
|
-
if (cursor.type.id === LEZER_NODE_IDS.ListItem) items.push(convertListItem(nodes, cursor, text, kind));
|
|
6313
|
+
if (cursor.type.id === LEZER_NODE_IDS.ListItem) items.push(convertListItem(nodes, cursor, text, column, kind));
|
|
5930
6314
|
while (cursor.nextSibling());
|
|
5931
6315
|
cursor.parent();
|
|
5932
6316
|
}
|
|
@@ -5957,7 +6341,7 @@ function readListMark(cursor, text, kind) {
|
|
|
5957
6341
|
* A GFM `Task` leaf (`[ ] text` / `[x] text`, after the list mark). The checkbox
|
|
5958
6342
|
* becomes the item's own attributes; the text after it becomes its paragraph.
|
|
5959
6343
|
*/
|
|
5960
|
-
function convertTaskItem(nodes, cursor, text) {
|
|
6344
|
+
function convertTaskItem(nodes, cursor, text, column) {
|
|
5961
6345
|
let taskStart = cursor.from;
|
|
5962
6346
|
const taskEnd = cursor.to;
|
|
5963
6347
|
let checked = false;
|
|
@@ -5977,43 +6361,54 @@ function convertTaskItem(nodes, cursor, text) {
|
|
|
5977
6361
|
cursor.parent();
|
|
5978
6362
|
}
|
|
5979
6363
|
if (isSpaceChar(text.charCodeAt(taskStart))) taskStart += 1;
|
|
5980
|
-
const paragraph = buildParagraph(nodes, text.slice(taskStart, taskEnd),
|
|
6364
|
+
const paragraph = buildParagraph(nodes, text.slice(taskStart, taskEnd), column);
|
|
5981
6365
|
return {
|
|
5982
6366
|
checked,
|
|
5983
6367
|
taskMarker,
|
|
5984
6368
|
paragraph
|
|
5985
6369
|
};
|
|
5986
6370
|
}
|
|
5987
|
-
function convertListItem(nodes, cursor, text, kind) {
|
|
6371
|
+
function convertListItem(nodes, cursor, text, column, kind) {
|
|
5988
6372
|
const content = [];
|
|
5989
6373
|
let taskChecked;
|
|
5990
6374
|
let taskMarker;
|
|
5991
6375
|
let order;
|
|
5992
6376
|
let marker;
|
|
5993
|
-
let
|
|
5994
|
-
let
|
|
6377
|
+
let markWidth = 1;
|
|
6378
|
+
let markTo;
|
|
6379
|
+
let markEndColumn = 0;
|
|
6380
|
+
let markerGap = 1;
|
|
6381
|
+
let contentColumn = column + markWidth + markerGap;
|
|
6382
|
+
let sawContent = false;
|
|
5995
6383
|
if (cursor.firstChild()) {
|
|
5996
6384
|
do {
|
|
5997
|
-
if (cursor.type.id
|
|
6385
|
+
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
5998
6386
|
if (cursor.type.id === LEZER_NODE_IDS.ListMark) {
|
|
5999
6387
|
const listMark = readListMark(cursor, text, kind);
|
|
6000
6388
|
marker = listMark.marker;
|
|
6001
6389
|
order = listMark.order;
|
|
6390
|
+
markWidth = cursor.to - cursor.from;
|
|
6391
|
+
markTo = cursor.to;
|
|
6002
6392
|
markEndColumn = measureContentColumn(text, cursor.to);
|
|
6003
6393
|
continue;
|
|
6004
6394
|
}
|
|
6395
|
+
if (!sawContent) {
|
|
6396
|
+
sawContent = true;
|
|
6397
|
+
const gap = markTo != null && text.lastIndexOf("\n", cursor.from - 1) < markTo ? measureContentColumn(text, cursor.from) - markEndColumn : 1;
|
|
6398
|
+
markerGap = gap >= 2 && gap <= 4 ? gap : 1;
|
|
6399
|
+
contentColumn = column + markWidth + markerGap;
|
|
6400
|
+
}
|
|
6005
6401
|
if (kind === "bullet" && cursor.type.id === LEZER_NODE_IDS.Task) {
|
|
6006
|
-
const task = convertTaskItem(nodes, cursor, text);
|
|
6402
|
+
const task = convertTaskItem(nodes, cursor, text, contentColumn);
|
|
6007
6403
|
taskChecked = task.checked;
|
|
6008
6404
|
taskMarker = task.taskMarker;
|
|
6009
6405
|
content.push(task.paragraph);
|
|
6010
6406
|
continue;
|
|
6011
6407
|
}
|
|
6012
|
-
content
|
|
6408
|
+
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, contentColumn));
|
|
6013
6409
|
} while (cursor.nextSibling());
|
|
6014
6410
|
cursor.parent();
|
|
6015
6411
|
}
|
|
6016
|
-
const gap = firstContentColumn != null && markEndColumn != null ? firstContentColumn - markEndColumn : 1;
|
|
6017
6412
|
const isTask = taskChecked != null;
|
|
6018
6413
|
const collapsed = !isTask && kind === "bullet" && marker === "+";
|
|
6019
6414
|
const attrs = {
|
|
@@ -6023,36 +6418,41 @@ function convertListItem(nodes, cursor, text, kind) {
|
|
|
6023
6418
|
collapsed,
|
|
6024
6419
|
marker: collapsed ? null : marker,
|
|
6025
6420
|
taskMarker,
|
|
6026
|
-
markerGap
|
|
6421
|
+
markerGap
|
|
6027
6422
|
};
|
|
6028
6423
|
return nodes.list(attrs, content);
|
|
6029
6424
|
}
|
|
6030
6425
|
function convertCodeBlock(nodes, cursor, text) {
|
|
6031
6426
|
const indented = cursor.type.id === LEZER_NODE_IDS.CodeBlock;
|
|
6427
|
+
const blockTo = cursor.to;
|
|
6032
6428
|
let language = "";
|
|
6033
6429
|
let code = "";
|
|
6034
6430
|
let fenceStyle = indented ? "indented" : null;
|
|
6035
6431
|
let fenceLength = null;
|
|
6036
6432
|
let sawOpeningMark = false;
|
|
6433
|
+
let pos = cursor.from;
|
|
6037
6434
|
if (cursor.firstChild()) {
|
|
6038
|
-
do
|
|
6435
|
+
do {
|
|
6436
|
+
code += uncoveredCode(text, pos, cursor.from);
|
|
6437
|
+
pos = cursor.to;
|
|
6039
6438
|
switch (cursor.type.id) {
|
|
6040
|
-
case LEZER_NODE_IDS.CodeMark:
|
|
6439
|
+
case LEZER_NODE_IDS.CodeMark:
|
|
6041
6440
|
if (sawOpeningMark) break;
|
|
6042
6441
|
sawOpeningMark = true;
|
|
6043
6442
|
if (text.charCodeAt(cursor.from) === 126) fenceStyle = "tilde";
|
|
6044
|
-
|
|
6045
|
-
if (markLength > 3) fenceLength = markLength;
|
|
6443
|
+
fenceLength = cursor.to - cursor.from;
|
|
6046
6444
|
break;
|
|
6047
|
-
}
|
|
6048
6445
|
case LEZER_NODE_IDS.CodeInfo:
|
|
6049
6446
|
language = text.slice(cursor.from, cursor.to);
|
|
6050
6447
|
break;
|
|
6051
6448
|
case LEZER_NODE_IDS.CodeText: code += text.slice(cursor.from, cursor.to);
|
|
6052
6449
|
}
|
|
6053
|
-
while (cursor.nextSibling());
|
|
6450
|
+
} while (cursor.nextSibling());
|
|
6054
6451
|
cursor.parent();
|
|
6452
|
+
code += uncoveredCode(text, pos, blockTo);
|
|
6055
6453
|
}
|
|
6454
|
+
if (fenceStyle === "indented" && !canIndentCode(code)) fenceStyle = null;
|
|
6455
|
+
if (fenceLength != null && fenceLength <= minFenceLength(code, fenceStyle === "tilde")) fenceLength = null;
|
|
6056
6456
|
return nodes.codeBlock({
|
|
6057
6457
|
language,
|
|
6058
6458
|
fenceStyle,
|
|
@@ -6060,6 +6460,17 @@ function convertCodeBlock(nodes, cursor, text) {
|
|
|
6060
6460
|
}, code);
|
|
6061
6461
|
}
|
|
6062
6462
|
/**
|
|
6463
|
+
* Text inside a code block that no child node covers is the whitespace in
|
|
6464
|
+
* front of a line's content: indentation, marker spaces, line breaks. The one
|
|
6465
|
+
* exception is a quote marker indented with a tab (`>\t \n\t>2`), where lezer
|
|
6466
|
+
* leaves the content after the marker uncovered too, so anything from a gap's
|
|
6467
|
+
* first non-whitespace character on is code the child list dropped.
|
|
6468
|
+
*/
|
|
6469
|
+
function uncoveredCode(text, from, to) {
|
|
6470
|
+
for (let index = from; index < to; index++) if (!isSpaceChar(text.charCodeAt(index))) return text.slice(index, to);
|
|
6471
|
+
return "";
|
|
6472
|
+
}
|
|
6473
|
+
/**
|
|
6063
6474
|
* A `$$` display math block is a code block whose `language` is `math`; the
|
|
6064
6475
|
* `dollar` fence style makes it serialize back to `$$` fences.
|
|
6065
6476
|
*/
|
|
@@ -6086,15 +6497,29 @@ function convertTable(nodes, cursor, text) {
|
|
|
6086
6497
|
cursor.parent();
|
|
6087
6498
|
}
|
|
6088
6499
|
const rows = [];
|
|
6500
|
+
let columnCount = aligns.length;
|
|
6089
6501
|
if (cursor.firstChild()) {
|
|
6090
6502
|
do {
|
|
6091
6503
|
const id = cursor.type.id;
|
|
6092
|
-
if (id
|
|
6093
|
-
|
|
6504
|
+
if (id !== LEZER_NODE_IDS.TableHeader && id !== LEZER_NODE_IDS.TableRow) continue;
|
|
6505
|
+
const cells = readTableCells(cursor, text);
|
|
6506
|
+
if (cells.length > columnCount) columnCount = cells.length;
|
|
6507
|
+
rows.push({
|
|
6508
|
+
isHeader: id === LEZER_NODE_IDS.TableHeader,
|
|
6509
|
+
cells
|
|
6510
|
+
});
|
|
6094
6511
|
} while (cursor.nextSibling());
|
|
6095
6512
|
cursor.parent();
|
|
6096
6513
|
}
|
|
6097
|
-
return nodes.table(rows)
|
|
6514
|
+
return nodes.table(rows.map(({ isHeader, cells }) => {
|
|
6515
|
+
const built = [];
|
|
6516
|
+
for (let column = 0; column < columnCount; column++) {
|
|
6517
|
+
const attrs = { align: aligns[column] ?? null };
|
|
6518
|
+
const paragraph = nodes.paragraph(cells[column] ?? "");
|
|
6519
|
+
built.push(isHeader ? nodes.tableHeaderCell(attrs, paragraph) : nodes.tableCell(attrs, paragraph));
|
|
6520
|
+
}
|
|
6521
|
+
return nodes.tableRow(built);
|
|
6522
|
+
}));
|
|
6098
6523
|
}
|
|
6099
6524
|
function parseDelimiterAligns(separator) {
|
|
6100
6525
|
return separator.split("|").map((segment) => segment.trim()).filter((segment) => segment !== "").map((segment) => {
|
|
@@ -6106,71 +6531,88 @@ function parseDelimiterAligns(separator) {
|
|
|
6106
6531
|
return null;
|
|
6107
6532
|
});
|
|
6108
6533
|
}
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
const
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6534
|
+
/**
|
|
6535
|
+
* A row's cell texts, indexed by column. `@lezer/markdown` emits no `TableCell`
|
|
6536
|
+
* for an empty cell, so the column comes from the pipes counted so far, not
|
|
6537
|
+
* from the cells seen so far.
|
|
6538
|
+
*/
|
|
6539
|
+
function readTableCells(cursor, text) {
|
|
6540
|
+
const cellTexts = [];
|
|
6541
|
+
if (!cursor.firstChild()) return cellTexts;
|
|
6542
|
+
const hasLeadingPipe = cursor.type.id === LEZER_NODE_IDS.TableDelimiter;
|
|
6543
|
+
let delimiterCount = 0;
|
|
6544
|
+
do {
|
|
6545
|
+
if (cursor.type.id === LEZER_NODE_IDS.TableDelimiter) {
|
|
6546
|
+
delimiterCount++;
|
|
6547
|
+
continue;
|
|
6548
|
+
}
|
|
6549
|
+
if (cursor.type.id !== LEZER_NODE_IDS.TableCell) continue;
|
|
6550
|
+
const column = delimiterCount - (hasLeadingPipe ? 1 : 0);
|
|
6551
|
+
if (column < 0) continue;
|
|
6552
|
+
while (cellTexts.length <= column) cellTexts.push("");
|
|
6553
|
+
cellTexts[column] = text.slice(cursor.from, cursor.to).trim().replaceAll(String.raw`\|`, "|");
|
|
6554
|
+
} while (cursor.nextSibling());
|
|
6555
|
+
cursor.parent();
|
|
6556
|
+
return cellTexts;
|
|
6130
6557
|
}
|
|
6131
6558
|
|
|
6132
6559
|
//#endregion
|
|
6133
6560
|
//#region src/converters/check-roundtrip.ts
|
|
6134
6561
|
function trimTrailingNewlines(text) {
|
|
6135
|
-
|
|
6562
|
+
let end = text.length;
|
|
6563
|
+
while (end > 0 && text.charCodeAt(end - 1) === 10) end--;
|
|
6564
|
+
return text.slice(0, end);
|
|
6136
6565
|
}
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
function nonBlankLines(text) {
|
|
6141
|
-
return text.split("\n").filter((line) => !isBlankLine(line));
|
|
6142
|
-
}
|
|
6143
|
-
function collapseWhitespace(line) {
|
|
6144
|
-
return line.trim().replaceAll(/\s+/gu, " ");
|
|
6566
|
+
const CONTAINER_PREFIX_RE = /^(?:[\s>]|[-+*](?=\s|$)|\d{1,9}[.)](?=\s|$))+/u;
|
|
6567
|
+
function stripWhitespace(line) {
|
|
6568
|
+
return line.replaceAll(/\s/gu, "");
|
|
6145
6569
|
}
|
|
6146
6570
|
const DELIMITER_CELL_RE = /^:?-+:?$/u;
|
|
6147
|
-
function canonicalizeDelimiterCell(cell) {
|
|
6148
|
-
const alignsLeft = cell.startsWith(":");
|
|
6149
|
-
const alignsRight = cell.endsWith(":");
|
|
6150
|
-
if (alignsLeft && alignsRight) return ":-:";
|
|
6151
|
-
if (alignsLeft) return ":--";
|
|
6152
|
-
if (alignsRight) return "--:";
|
|
6153
|
-
return "---";
|
|
6154
|
-
}
|
|
6155
6571
|
function canonicalizeTableRow(line) {
|
|
6156
6572
|
if (!line.includes("|")) return void 0;
|
|
6157
|
-
const
|
|
6158
|
-
|
|
6159
|
-
return
|
|
6573
|
+
const cells = line.replace(/^\|/u, "").replace(/\|$/u, "").split("|");
|
|
6574
|
+
if (cells.every((cell) => DELIMITER_CELL_RE.test(cell))) return "";
|
|
6575
|
+
return cells.filter((cell) => cell !== "").join("|");
|
|
6576
|
+
}
|
|
6577
|
+
/**
|
|
6578
|
+
* The lines of `text` that carry content, each reduced to that content. A line
|
|
6579
|
+
* left empty by dropping its markers and whitespace (a blank line, a bare `>`,
|
|
6580
|
+
* a list marker whose content is on the next line) carries none and is skipped.
|
|
6581
|
+
*/
|
|
6582
|
+
function contentLines(text) {
|
|
6583
|
+
const lines = [];
|
|
6584
|
+
for (const line of text.split("\n")) {
|
|
6585
|
+
const content = stripWhitespace(line.replace(CONTAINER_PREFIX_RE, ""));
|
|
6586
|
+
if (content !== "") lines.push(canonicalizeTableRow(content) ?? content);
|
|
6587
|
+
}
|
|
6588
|
+
return lines;
|
|
6160
6589
|
}
|
|
6161
|
-
|
|
6162
|
-
|
|
6590
|
+
/**
|
|
6591
|
+
* Whether `wanted` appears in `found` as an ordered subsequence.
|
|
6592
|
+
*
|
|
6593
|
+
* The serializer may write a line the source never had: an unterminated fenced
|
|
6594
|
+
* block gets the closing fence it was missing. Such a line carries no content,
|
|
6595
|
+
* which the document comparison proves - so only a content line that fails to
|
|
6596
|
+
* come back out is loss.
|
|
6597
|
+
*/
|
|
6598
|
+
function containsInOrder(found, wanted) {
|
|
6599
|
+
let index = 0;
|
|
6600
|
+
for (const line of found) {
|
|
6601
|
+
if (index === wanted.length) return true;
|
|
6602
|
+
if (line === wanted[index]) index++;
|
|
6603
|
+
}
|
|
6604
|
+
return index === wanted.length;
|
|
6163
6605
|
}
|
|
6164
6606
|
/**
|
|
6165
6607
|
* Classify how `markdown` survives the editor's parse-then-serialize round trip.
|
|
6166
6608
|
*/
|
|
6167
6609
|
function checkRoundTrip(markdown, options = {}) {
|
|
6168
|
-
const
|
|
6169
|
-
const
|
|
6610
|
+
const { frontmatter } = options;
|
|
6611
|
+
const doc = markdownToDoc(markdown, { frontmatter });
|
|
6612
|
+
const serialized = docToMarkdown(doc, { frontmatter });
|
|
6170
6613
|
if (trimTrailingNewlines(serialized) === trimTrailingNewlines(markdown)) return "exact";
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
return before.length === after.length && before.every((line, i) => normalizeLine(line) === normalizeLine(after[i])) ? "normalizing" : "lossy";
|
|
6614
|
+
if (!containsInOrder(contentLines(serialized), contentLines(markdown))) return "lossy";
|
|
6615
|
+
return markdownToDoc(serialized, { frontmatter }).eq(doc) ? "normalizing" : "lossy";
|
|
6174
6616
|
}
|
|
6175
6617
|
|
|
6176
6618
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meowdown/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.65.
|
|
4
|
+
"version": "0.65.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,24 +26,24 @@
|
|
|
26
26
|
"@floating-ui/dom": "^1.8.0",
|
|
27
27
|
"@lezer/highlight": "^1.2.3",
|
|
28
28
|
"@ocavue/utils": "^1.7.0",
|
|
29
|
-
"@prosekit/core": "^0.13.0
|
|
30
|
-
"@prosekit/extensions": "^0.18.0
|
|
31
|
-
"@prosekit/pm": "^0.1.19
|
|
32
|
-
"@prosekit/web": "^0.9.0
|
|
29
|
+
"@prosekit/core": "^0.13.0",
|
|
30
|
+
"@prosekit/extensions": "^0.18.0",
|
|
31
|
+
"@prosekit/pm": "^0.1.19",
|
|
32
|
+
"@prosekit/web": "^0.9.0",
|
|
33
33
|
"hast-util-to-mdast": "^10.1.2",
|
|
34
34
|
"katex": "^0.18.1",
|
|
35
35
|
"mdast-util-to-markdown": "^2.1.2",
|
|
36
36
|
"micromark-util-decode-string": "^2.0.1",
|
|
37
37
|
"micromark-util-normalize-identifier": "^2.0.1",
|
|
38
38
|
"prosemirror-flat-list": "^0.7.1",
|
|
39
|
-
"prosemirror-highlight": "^0.
|
|
39
|
+
"prosemirror-highlight": "^0.16.0",
|
|
40
40
|
"rehype-parse": "^9.0.1",
|
|
41
41
|
"rehype-remark": "^10.0.1",
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
43
43
|
"remark-stringify": "^11.0.0",
|
|
44
44
|
"unicode-by-name": "^0.2.0",
|
|
45
45
|
"unified": "^11.0.5",
|
|
46
|
-
"@meowdown/markdown": "^0.65.
|
|
46
|
+
"@meowdown/markdown": "^0.65.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@ocavue/tsconfig": "^0.7.1",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@vitest/browser-playwright": "^4.1.10",
|
|
54
54
|
"dedent": "^1.7.2",
|
|
55
55
|
"diffable-html-snapshot": "^0.3.0",
|
|
56
|
+
"fast-check": "^4.9.0",
|
|
56
57
|
"tsdown": "^0.23.0-beta.2",
|
|
57
58
|
"vitest": "^4.1.10",
|
|
58
59
|
"@meowdown/vitest": "0.0.0"
|