@meowdown/core 0.77.0 → 0.78.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/dist/index.d.ts +47 -46
- package/dist/index.js +271 -1372
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { LEZER_NODE_IDS, checkRoundTrip, collectInlineElements, getAutolinkHref, gfmParser, isSpaceChar, matchEmbed, parseInline, parseMarkdownAst, parseXPostId, serializeMarkdownAst } from "@meowdown/markdown";
|
|
1
2
|
import { Priority, Priority as Priority$1, createMarkBuilders, createNodeBuilders, defaultBlockAt, defineBaseCommands, defineBaseKeymap, defineClipboardSerializer, defineCommands, defineHistory, defineKeymap, defineMarkSpec, defineMarkView, defineNodeAttr, defineNodeSpec, definePlugin, findMarkRange, getMarkRange, getMarkType, getNodeType, isAllSelection, isApple, isAtBlockStart, isNodeSelection, isTextSelection, setBlockType, toggleNode, union, unsetBlockType, withPriority, withPriority as withPriority$1, withSkipCodeBlock } from "@prosekit/core";
|
|
2
3
|
import { defineCodeBlock, defineCodeBlockHighlight, defineCodeBlockPreviewPlugin, isCodeBlockPreviewHiddenDecoration } from "@prosekit/extensions/code-block";
|
|
3
4
|
import { definePlaceholder, definePlaceholder as definePlaceholder$1 } from "@prosekit/extensions/placeholder";
|
|
4
|
-
import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@prosekit/pm/state";
|
|
5
5
|
import { defineSearchCommands, defineSearchQuery, defineSearchStatusHandler, getSearchStatus, getSearchStatus as getSearchStatus$1 } from "@prosekit/extensions/search";
|
|
6
|
-
import { LEZER_NODE_IDS, collectInlineElements, getAutolinkHref, gfmBlockOnlyParser, gfmParser, isSpaceChar, matchEmbed, parseInline, parseXPostId } from "@meowdown/markdown";
|
|
7
6
|
import { isElementLike, isHTMLElement, isObject, once } from "@ocavue/utils";
|
|
8
7
|
import { defineBlockquote } from "@prosekit/extensions/blockquote";
|
|
9
8
|
import { defineDoc } from "@prosekit/extensions/doc";
|
|
@@ -11,6 +10,7 @@ import { defineGapCursor } from "@prosekit/extensions/gap-cursor";
|
|
|
11
10
|
import { defineModClickPrevention } from "@prosekit/extensions/mod-click-prevention";
|
|
12
11
|
import { defineText } from "@prosekit/extensions/text";
|
|
13
12
|
import { defineVirtualSelection } from "@prosekit/extensions/virtual-selection";
|
|
13
|
+
import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@prosekit/pm/state";
|
|
14
14
|
import { Decoration, DecorationSet } from "@prosekit/pm/view";
|
|
15
15
|
import { NO_BREAK_SPACE } from "unicode-by-name";
|
|
16
16
|
import { defaultHandlers } from "hast-util-to-mdast";
|
|
@@ -48,18 +48,6 @@ import { createListRenderingPlugin, createSafariInputMethodWorkaroundPlugin, cre
|
|
|
48
48
|
import { defineTableCellSpec, defineTableCommands, defineTableDropIndicator, defineTableEditingPlugin, defineTableHeaderCellSpec, defineTableRowSpec, defineTableSpec, deleteTable, isCellSelection } from "@prosekit/extensions/table";
|
|
49
49
|
import { InputRule } from "@prosekit/pm/inputrules";
|
|
50
50
|
|
|
51
|
-
//#region src/extensions/readonly.ts
|
|
52
|
-
function defineReadonly(getReadonly = () => true) {
|
|
53
|
-
return definePlugin(new Plugin({ props: { editable: (state) => !getReadonly(state) } }));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/extensions/node-names.ts
|
|
58
|
-
function isNodeOfType(node, name) {
|
|
59
|
-
return node.type.name === name;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
//#endregion
|
|
63
51
|
//#region src/utils/composition.ts
|
|
64
52
|
const COMPOSITION_TAIL_MS = 50;
|
|
65
53
|
let compositionEndedAt = -1;
|
|
@@ -633,6 +621,12 @@ function defineAtomMarkNavigation({ marks }) {
|
|
|
633
621
|
}), Priority$1.high), definePlugin(createCaretSnapPlugin(marks)), definePlugin(createSelectionPlugin(marks)));
|
|
634
622
|
}
|
|
635
623
|
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/extensions/node-names.ts
|
|
626
|
+
function isNodeOfType(node, name) {
|
|
627
|
+
return node.type.name === name;
|
|
628
|
+
}
|
|
629
|
+
|
|
636
630
|
//#endregion
|
|
637
631
|
//#region src/extensions/bullet-after-heading.ts
|
|
638
632
|
/**
|
|
@@ -1382,724 +1376,150 @@ function definePlainTextPaste() {
|
|
|
1382
1376
|
}
|
|
1383
1377
|
|
|
1384
1378
|
//#endregion
|
|
1385
|
-
//#region src/converters/pm-to-
|
|
1386
|
-
/**
|
|
1387
|
-
* Convert a ProseMirror document into a Markdown string.
|
|
1388
|
-
*
|
|
1389
|
-
* Performance design:
|
|
1390
|
-
* - Output accumulates in a `string[]` buffer; joined once at the end.
|
|
1391
|
-
* Avoids per-block intermediate strings while keeping the
|
|
1392
|
-
* function-per-node-type readability of a switch dispatch.
|
|
1393
|
-
* - Indent stack lives as a mutable `linePrefix` on the buffer object,
|
|
1394
|
-
* restored via local variables across nested calls - no fresh
|
|
1395
|
-
* context objects per recursion.
|
|
1396
|
-
* - Inline content is walked directly (not via `node.textContent`) to
|
|
1397
|
-
* skip one intermediate string allocation per leaf block.
|
|
1398
|
-
* - Backtick fence width and cell escaping use single linear loops, no
|
|
1399
|
-
* regex on the hot path.
|
|
1400
|
-
*/
|
|
1401
|
-
function docToMarkdown(node, options = {}) {
|
|
1402
|
-
const out = new MdOut();
|
|
1403
|
-
if (options.frontmatter) emitFrontmatter(node.attrs.frontmatter, out);
|
|
1404
|
-
const child = node.childCount === 1 ? node.child(0) : void 0;
|
|
1405
|
-
if (!(child && isNodeOfType(child, "paragraph") && child.childCount === 0)) emit$1(node, out);
|
|
1406
|
-
return out.finish();
|
|
1407
|
-
}
|
|
1408
|
-
/**
|
|
1409
|
-
* Emit the document's YAML frontmatter (stored as a `doc` attribute) as a
|
|
1410
|
-
* leading `---\n{body}\n---` block. `null` (the default) emits nothing; an
|
|
1411
|
-
* empty body emits `---\n---` with no middle blank line.
|
|
1412
|
-
*/
|
|
1413
|
-
function emitFrontmatter(body, out) {
|
|
1414
|
-
if (body === null) return;
|
|
1415
|
-
out.write("---");
|
|
1416
|
-
out.write("\n");
|
|
1417
|
-
if (body !== "") {
|
|
1418
|
-
out.write(body);
|
|
1419
|
-
out.write("\n");
|
|
1420
|
-
}
|
|
1421
|
-
out.write("---");
|
|
1422
|
-
out.closeBlock();
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* Heading prefixes indexed by level (1..6). Index 0 is a sentinel.
|
|
1426
|
-
*/
|
|
1427
|
-
const HEADING_PREFIX = [
|
|
1428
|
-
"",
|
|
1429
|
-
"# ",
|
|
1430
|
-
"## ",
|
|
1431
|
-
"### ",
|
|
1432
|
-
"#### ",
|
|
1433
|
-
"##### ",
|
|
1434
|
-
"###### "
|
|
1435
|
-
];
|
|
1436
|
-
function emitHeading(node, out) {
|
|
1437
|
-
const attrs = node.attrs;
|
|
1438
|
-
const underline = attrs.setextUnderline;
|
|
1439
|
-
if (underline != null && node.content.size > 0 && attrs.level <= 2) {
|
|
1440
|
-
emitInlineChildren(node, out);
|
|
1441
|
-
const underlineChar = attrs.level === 1 ? "=" : "-";
|
|
1442
|
-
out.write("\n" + underlineChar.repeat(Math.max(1, underline)));
|
|
1443
|
-
out.closeBlock();
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
const prefix = HEADING_PREFIX[attrs.level] ?? "# ";
|
|
1447
|
-
out.write(node.content.size > 0 ? prefix : prefix.slice(0, -1));
|
|
1448
|
-
emitInlineChildren(node, out);
|
|
1449
|
-
const closingHashes = attrs.closingHashes;
|
|
1450
|
-
if (closingHashes != null && closingHashes > 0) out.write(" " + "#".repeat(closingHashes));
|
|
1451
|
-
out.closeBlock();
|
|
1452
|
-
}
|
|
1453
|
-
var MdOut = class {
|
|
1454
|
-
constructor() {
|
|
1455
|
-
this.parts = [];
|
|
1456
|
-
this.linePrefix = "";
|
|
1457
|
-
this.pendingFirst = null;
|
|
1458
|
-
this.atLineStart = true;
|
|
1459
|
-
this.deferredBlankPrefix = null;
|
|
1460
|
-
this.contentEnd = 0;
|
|
1461
|
-
}
|
|
1462
|
-
/**
|
|
1463
|
-
* Write `text`, opening each embedded line with the current line prefix.
|
|
1464
|
-
*
|
|
1465
|
-
* `lazyLines` lets a line the prefix would change the meaning of - a setext
|
|
1466
|
-
* underline, a tab-indented line under a blockquote, a block opener - go out
|
|
1467
|
-
* as a lazy continuation instead (see `continuationPrefix`). Markdown keeps
|
|
1468
|
-
* such a line as text only when it arrives lazily, and the container picks
|
|
1469
|
-
* the paragraph up again on the next line. Only paragraph-like text may ask
|
|
1470
|
-
* for this: an unprefixed line would fall out of a code block, an html
|
|
1471
|
-
* comment, or an HTML block.
|
|
1472
|
-
*/
|
|
1473
|
-
write(text, lazyLines = false) {
|
|
1474
|
-
if (text === "") return;
|
|
1475
|
-
this.emitDeferredBlankLine();
|
|
1476
|
-
if (this.atLineStart) {
|
|
1477
|
-
if (this.pendingFirst !== null && isThematicBreak(this.pendingFirst + text)) this.breakMarkerLine();
|
|
1478
|
-
this.parts.push(this.pendingFirst ?? this.linePrefix);
|
|
1479
|
-
this.pendingFirst = null;
|
|
1480
|
-
this.atLineStart = false;
|
|
1481
|
-
}
|
|
1482
|
-
if (!text.includes("\n")) {
|
|
1483
|
-
this.parts.push(text);
|
|
1484
|
-
return;
|
|
1485
|
-
}
|
|
1486
|
-
const lines = text.split("\n");
|
|
1487
|
-
const lazy = lazyLines && this.linePrefix !== "";
|
|
1488
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1489
|
-
const line = lines[i];
|
|
1490
|
-
if (i > 0) this.parts.push("\n", lazy ? continuationPrefix(line, this.linePrefix) : this.linePrefix);
|
|
1491
|
-
if (line !== "") this.parts.push(line);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* End a block that owns no line of its own (an empty paragraph): it is one
|
|
1496
|
-
* blank line. After another block, the blank line that block owes is this
|
|
1497
|
-
* one, and the next block is owed a fresh one, so each empty block adds a
|
|
1498
|
-
* blank line to the separator. With nothing owed (the start of the output)
|
|
1499
|
-
* the line is written directly and nothing is owed after it: a leading empty
|
|
1500
|
-
* block is a blank line, not a separator. A marker-bearing block (an empty
|
|
1501
|
-
* list item's `- `, an empty quote line's `>`) writes its marker as the line.
|
|
1502
|
-
*/
|
|
1503
|
-
closeEmptyBlock() {
|
|
1504
|
-
if (!this.atLineStart) {
|
|
1505
|
-
this.closeBlock();
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
if (this.pendingFirst !== null) {
|
|
1509
|
-
this.emitDeferredBlankLine();
|
|
1510
|
-
const marker = this.pendingFirst;
|
|
1511
|
-
this.parts.push(marker.endsWith("] ") ? marker : marker.trimEnd(), "\n");
|
|
1512
|
-
this.pendingFirst = null;
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
if (this.deferredBlankPrefix === null) {
|
|
1516
|
-
this.parts.push(this.linePrefix.trimEnd(), "\n");
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
this.emitDeferredBlankLine();
|
|
1520
|
-
this.deferredBlankPrefix = this.linePrefix;
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* End the current block; the next write gets a blank line before it.
|
|
1524
|
-
*/
|
|
1525
|
-
closeBlock() {
|
|
1526
|
-
if (this.atLineStart && this.pendingFirst !== null) {
|
|
1527
|
-
this.emitDeferredBlankLine();
|
|
1528
|
-
const marker = this.pendingFirst;
|
|
1529
|
-
this.parts.push(marker.endsWith("] ") ? marker : marker.trimEnd());
|
|
1530
|
-
this.pendingFirst = null;
|
|
1531
|
-
this.atLineStart = false;
|
|
1532
|
-
}
|
|
1533
|
-
if (!this.atLineStart) {
|
|
1534
|
-
this.parts.push("\n");
|
|
1535
|
-
this.contentEnd = this.parts.length;
|
|
1536
|
-
}
|
|
1537
|
-
this.atLineStart = true;
|
|
1538
|
-
this.deferredBlankPrefix = this.linePrefix;
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* Give the markers pending on this line a line of their own, so what comes
|
|
1542
|
-
* next opens a new one. Unlike `closeBlock` this owes no blank line: the
|
|
1543
|
-
* items are still part of the same tight list.
|
|
1544
|
-
*/
|
|
1545
|
-
breakMarkerLine() {
|
|
1546
|
-
if (this.pendingFirst === null) return;
|
|
1547
|
-
this.emitDeferredBlankLine();
|
|
1548
|
-
this.parts.push(this.pendingFirst.trimEnd(), "\n");
|
|
1549
|
-
this.pendingFirst = null;
|
|
1550
|
-
this.atLineStart = true;
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Cancel the blank line deferred by the last `closeBlock`, so the next
|
|
1554
|
-
* write starts directly on the following line. Used between the blocks of
|
|
1555
|
-
* a tight list, where markdown separates items (and an item's paragraph
|
|
1556
|
-
* from its nested list) with a single newline.
|
|
1557
|
-
*/
|
|
1558
|
-
suppressBlank() {
|
|
1559
|
-
this.deferredBlankPrefix = null;
|
|
1560
|
-
}
|
|
1561
|
-
get atContentStart() {
|
|
1562
|
-
return this.atLineStart && !this.pendingFirst?.endsWith("] ");
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Run `fn` with `linePrefix` extended by `continuation`.
|
|
1566
|
-
* If `firstLine` is given, it replaces the prefix on the NEXT line only -
|
|
1567
|
-
* used for list items where the marker (`- `) only appears on line 1.
|
|
1568
|
-
* Composes with any outer one-shot prefix: a blockquote inside a list
|
|
1569
|
-
* item should emit "- > " on the first line, not just "> ".
|
|
1570
|
-
*/
|
|
1571
|
-
withPrefix(continuation, firstLine, fn) {
|
|
1572
|
-
const savedLine = this.linePrefix;
|
|
1573
|
-
const savedFirst = this.pendingFirst;
|
|
1574
|
-
this.linePrefix = savedLine + continuation;
|
|
1575
|
-
if (firstLine !== null) {
|
|
1576
|
-
const base = savedFirst ?? savedLine;
|
|
1577
|
-
if (savedFirst !== null && isThematicBreak(base + firstLine)) {
|
|
1578
|
-
this.breakMarkerLine();
|
|
1579
|
-
this.pendingFirst = savedLine + firstLine;
|
|
1580
|
-
} else this.pendingFirst = base + firstLine;
|
|
1581
|
-
}
|
|
1582
|
-
fn();
|
|
1583
|
-
this.linePrefix = savedLine;
|
|
1584
|
-
this.pendingFirst = firstLine !== null ? null : savedFirst;
|
|
1585
|
-
}
|
|
1586
|
-
finish() {
|
|
1587
|
-
const text = this.parts.slice(0, this.contentEnd).join("");
|
|
1588
|
-
let cut = text.length;
|
|
1589
|
-
for (let i = text.length - 1; i >= 0; i--) {
|
|
1590
|
-
const code = text.charCodeAt(i);
|
|
1591
|
-
if (code === 10) cut = i;
|
|
1592
|
-
else if (code !== 32 && code !== 9) break;
|
|
1593
|
-
}
|
|
1594
|
-
return (cut === 0 ? "" : text.slice(0, cut) + "\n") + this.parts.slice(this.contentEnd).join("") || "\n";
|
|
1595
|
-
}
|
|
1596
|
-
emitDeferredBlankLine() {
|
|
1597
|
-
const prefix = this.deferredBlankPrefix;
|
|
1598
|
-
if (prefix === null) return;
|
|
1599
|
-
this.parts.push(prefix.trimEnd(), "\n");
|
|
1600
|
-
this.deferredBlankPrefix = null;
|
|
1601
|
-
}
|
|
1602
|
-
};
|
|
1603
|
-
function emit$1(node, out) {
|
|
1604
|
-
switch (node.type.name) {
|
|
1605
|
-
case "doc":
|
|
1606
|
-
emitBlockChildren(node, out);
|
|
1607
|
-
return;
|
|
1608
|
-
case "paragraph":
|
|
1609
|
-
if (node.childCount === 0) {
|
|
1610
|
-
out.closeEmptyBlock();
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
emitInlineChildren(node, out);
|
|
1614
|
-
out.closeBlock();
|
|
1615
|
-
return;
|
|
1616
|
-
case "heading":
|
|
1617
|
-
emitHeading(node, out);
|
|
1618
|
-
return;
|
|
1619
|
-
case "blockquote":
|
|
1620
|
-
out.withPrefix("> ", "> ", () => emitBlockChildren(node, out));
|
|
1621
|
-
out.closeBlock();
|
|
1622
|
-
return;
|
|
1623
|
-
case "list":
|
|
1624
|
-
emitList(node, out, isTightItem(node));
|
|
1625
|
-
return;
|
|
1626
|
-
case "codeBlock":
|
|
1627
|
-
emitCodeBlock(node, out);
|
|
1628
|
-
return;
|
|
1629
|
-
case "horizontalRule": {
|
|
1630
|
-
const { marker } = node.attrs;
|
|
1631
|
-
out.write(marker || "---");
|
|
1632
|
-
out.closeBlock();
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
case "htmlComment": {
|
|
1636
|
-
const { content } = node.attrs;
|
|
1637
|
-
out.write(content);
|
|
1638
|
-
out.closeBlock();
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
case "table":
|
|
1642
|
-
emitTable(node, out);
|
|
1643
|
-
return;
|
|
1644
|
-
case "text":
|
|
1645
|
-
if (node.text) out.write(node.text);
|
|
1646
|
-
return;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1379
|
+
//#region src/converters/pm-to-ast.ts
|
|
1649
1380
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
* list ("run") whose tightness is decided once for the whole run, matching
|
|
1652
|
-
* CommonMark's list-wide loose/tight semantics.
|
|
1653
|
-
*
|
|
1654
|
-
* `tightItem` is true when `node` is a list item inside a tight run: its
|
|
1655
|
-
* blocks (a paragraph followed by nested lists) are then separated by single
|
|
1656
|
-
* newlines instead of blank lines.
|
|
1657
|
-
*/
|
|
1658
|
-
function emitBlockChildren(node, out, tightItem = false) {
|
|
1659
|
-
const count = node.childCount;
|
|
1660
|
-
let index = 0;
|
|
1661
|
-
while (index < count) {
|
|
1662
|
-
const child = node.child(index);
|
|
1663
|
-
if (!isNodeOfType(child, "list")) {
|
|
1664
|
-
if (tightItem && index > 0) out.suppressBlank();
|
|
1665
|
-
emit$1(child, out);
|
|
1666
|
-
index++;
|
|
1667
|
-
continue;
|
|
1668
|
-
}
|
|
1669
|
-
let runEnd = index + 1;
|
|
1670
|
-
while (runEnd < count && isNodeOfType(node.child(runEnd), "list")) runEnd++;
|
|
1671
|
-
const tightRun = isTightRun(node, index, runEnd);
|
|
1672
|
-
for (let item = index; item < runEnd; item++) {
|
|
1673
|
-
const child = node.child(item);
|
|
1674
|
-
const isRunStart = item === index;
|
|
1675
|
-
const tight = isRunStart ? tightItem && index > 0 : tightRun;
|
|
1676
|
-
const carriesOn = !isRunStart && listMarkerChar(node.child(item - 1)) === listMarkerChar(child);
|
|
1677
|
-
if (tight && (carriesOn || canInterruptParagraph(child))) out.suppressBlank();
|
|
1678
|
-
emitList(child, out, tightRun);
|
|
1679
|
-
}
|
|
1680
|
-
index = runEnd;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
/**
|
|
1684
|
-
* Whether `list`'s marker line still opens an item when the line above it is a
|
|
1685
|
-
* paragraph. CommonMark lets a list interrupt a paragraph only when the item
|
|
1686
|
-
* carries content and, if ordered, is numbered 1; anything else reads as more of
|
|
1687
|
-
* the paragraph, so the blank line before it is what keeps it a list at all.
|
|
1381
|
+
* Read persisted block data. Editor marks do not add Markdown delimiters.
|
|
1688
1382
|
*/
|
|
1689
|
-
function
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
*/
|
|
1699
|
-
function listMarkerChar(node) {
|
|
1700
|
-
const { kind, marker, collapsed } = node.attrs;
|
|
1701
|
-
if (kind === "ordered") return marker === ")" ? ")" : ".";
|
|
1702
|
-
if (kind === "task") return marker === "+" ? "+" : marker === "*" ? "*" : "-";
|
|
1703
|
-
return collapsed ? "+" : marker === "*" ? "*" : "-";
|
|
1704
|
-
}
|
|
1705
|
-
/**
|
|
1706
|
-
* A run of sibling `list` nodes serializes tight iff every item is "simple":
|
|
1707
|
-
* at most one leading paragraph, then only nested lists. Any other shape
|
|
1708
|
-
* (multiple paragraphs, a blockquote, a code block, …) needs blank-line
|
|
1709
|
-
* separation inside the item, which per CommonMark makes the whole list
|
|
1710
|
-
* loose.
|
|
1711
|
-
*/
|
|
1712
|
-
function isTightRun(parent, from, to) {
|
|
1713
|
-
for (let i = from; i < to; i++) if (!isTightItem(parent.child(i))) return false;
|
|
1714
|
-
return true;
|
|
1715
|
-
}
|
|
1716
|
-
function isTightItem(item) {
|
|
1717
|
-
const count = item.childCount;
|
|
1718
|
-
for (let i = 0; i < count; i++) {
|
|
1719
|
-
const typeName = item.child(i).type.name;
|
|
1720
|
-
if (typeName === "list") continue;
|
|
1721
|
-
if (typeName === "paragraph" && i === 0) continue;
|
|
1722
|
-
return false;
|
|
1723
|
-
}
|
|
1724
|
-
return true;
|
|
1383
|
+
function docToAst(node) {
|
|
1384
|
+
if (isNodeOfType(node, "doc")) return {
|
|
1385
|
+
type: "document",
|
|
1386
|
+
frontmatter: node.attrs.frontmatter ?? void 0,
|
|
1387
|
+
children: readBlocks(node)
|
|
1388
|
+
};
|
|
1389
|
+
if (isNodeOfType(node, "tableRow")) return readTableRow(node);
|
|
1390
|
+
if (isNodeOfType(node, "tableCell") || isNodeOfType(node, "tableHeaderCell")) return readTableCell(node);
|
|
1391
|
+
return readBlock(node);
|
|
1725
1392
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
* `prefix` is the default. A line the full prefix would change the meaning of -
|
|
1729
|
-
* a setext underline, a tab whose columns the prefix shrinks below the four
|
|
1730
|
-
* that kept it inert, a block opener the source kept inert with indentation the
|
|
1731
|
-
* parser dedented away - needs a lazy spelling instead: without its `>` marker
|
|
1732
|
-
* the line can only continue the paragraph, because no block may start on a
|
|
1733
|
-
* lazy line.
|
|
1734
|
-
*
|
|
1735
|
-
* The lazy spelling carries `lazyIndent(prefix)`, the columns the parser
|
|
1736
|
-
* dedents an unmarked line by, so the text comes back exactly. It goes out bare
|
|
1737
|
-
* only when the dedent would leave it alone anyway and its own whitespace
|
|
1738
|
-
* measures four columns flush left (or it is a setext underline, which nothing
|
|
1739
|
-
* at the top level turns back into an underline).
|
|
1740
|
-
*/
|
|
1741
|
-
function continuationPrefix(line, prefix) {
|
|
1742
|
-
if (measureIndent(line, prefix.length) >= 4) return prefix;
|
|
1743
|
-
const shrunk = measureIndent(line, 0) >= 4;
|
|
1744
|
-
const underline = isSetextUnderline(line);
|
|
1745
|
-
const opens = !shrunk && lineOpensBlock(line);
|
|
1746
|
-
if (!shrunk && !underline && !opens) return prefix;
|
|
1747
|
-
const lazy = lazyIndent(prefix);
|
|
1748
|
-
return !opens && dedentKeepsLine(line, lazy.length) ? "" : lazy;
|
|
1749
|
-
}
|
|
1750
|
-
/**
|
|
1751
|
-
* The lazy spelling's indent: the whitespace the containers write after the
|
|
1752
|
-
* last `>` marker, which is also the columns the parser dedents an unmarked
|
|
1753
|
-
* continuation line by (`sliceColumn` in `md-to-pm.ts`). The single space
|
|
1754
|
-
* directly after the `>` belongs to the marker, not to the indent. A prefix
|
|
1755
|
-
* with no `>` to drop has flush left as its only lazy spelling: its columns
|
|
1756
|
-
* are the list's own indent, and writing them would just re-match the item.
|
|
1757
|
-
*/
|
|
1758
|
-
function lazyIndent(prefix) {
|
|
1759
|
-
let start = prefix.length;
|
|
1760
|
-
while (start > 0 && prefix.charCodeAt(start - 1) === 32) start--;
|
|
1761
|
-
if (start === 0 || prefix.charCodeAt(start - 1) !== 62) return "";
|
|
1762
|
-
return prefix.slice(start + 1);
|
|
1763
|
-
}
|
|
1764
|
-
/**
|
|
1765
|
-
* Whether the parser's dedent (`sliceColumn` at `column`) returns `line`
|
|
1766
|
-
* unchanged: its leading whitespace stops short of the column, or a tab in it
|
|
1767
|
-
* reaches past. Such a line round-trips byte-exact when written bare.
|
|
1768
|
-
*/
|
|
1769
|
-
function dedentKeepsLine(line, column) {
|
|
1770
|
-
let col = 0;
|
|
1771
|
-
for (let index = 0; col < column; index++) {
|
|
1772
|
-
const code = line.charCodeAt(index);
|
|
1773
|
-
if (code === 32) col += 1;
|
|
1774
|
-
else if (code === 9) {
|
|
1775
|
-
const width = 4 - col % 4;
|
|
1776
|
-
if (col + width > column) return true;
|
|
1777
|
-
col += width;
|
|
1778
|
-
} else return true;
|
|
1779
|
-
}
|
|
1780
|
-
return false;
|
|
1393
|
+
function readBlocks(node) {
|
|
1394
|
+
return node.content.content.map(readBlock);
|
|
1781
1395
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
/^<\?/,
|
|
1792
|
-
/^<![A-Z]/,
|
|
1793
|
-
/^<!\[CDATA\[/,
|
|
1794
|
-
/^<\/?(?: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,
|
|
1795
|
-
/^(?:<\/[a-z][\w-]*\s*>|<[a-z][\w-]*(\s+[a-z:_][\w.-]*(?:\s*=\s*(?:[^\s"'=<>`]+|'[^']*'|"[^"]*"))?)*\s*>)\s*$/i
|
|
1796
|
-
];
|
|
1797
|
-
/**
|
|
1798
|
-
* Whether `text`'s first line opens an HTML block. The last pattern (a
|
|
1799
|
-
* complete tag) cannot interrupt a paragraph, but a lazy spelling of such a
|
|
1800
|
-
* line round-trips all the same, so one check serves both callers.
|
|
1801
|
-
*/
|
|
1802
|
-
function opensHTMLBlock(text) {
|
|
1803
|
-
if (text.charCodeAt(0) !== 60) return false;
|
|
1804
|
-
const lineEnd = text.indexOf("\n");
|
|
1805
|
-
const line = lineEnd < 0 ? text : text.slice(0, lineEnd);
|
|
1806
|
-
for (const pattern of HTML_BLOCK_OPEN) if (pattern.test(line)) return true;
|
|
1807
|
-
return false;
|
|
1808
|
-
}
|
|
1809
|
-
/**
|
|
1810
|
-
* Whether `line`, at the start of a container's content, opens a block instead
|
|
1811
|
-
* of continuing the paragraph above it: a blockquote, an ATX heading, a fence,
|
|
1812
|
-
* `$$` math, an HTML block, a thematic break, a bullet item (even an empty
|
|
1813
|
-
* one), and an ordered item that carries content and is numbered 1.
|
|
1814
|
-
*/
|
|
1815
|
-
function lineOpensBlock(line) {
|
|
1816
|
-
let index = 0;
|
|
1817
|
-
while (isSpaceOrTab(line.charCodeAt(index))) index++;
|
|
1818
|
-
const first = line.charCodeAt(index);
|
|
1819
|
-
switch (first) {
|
|
1820
|
-
case 62: return true;
|
|
1821
|
-
case 60: return opensHTMLBlock(index === 0 ? line : line.slice(index));
|
|
1822
|
-
case 35: {
|
|
1823
|
-
let end = index + 1;
|
|
1824
|
-
while (line.charCodeAt(end) === 35) end++;
|
|
1825
|
-
return end - index <= 6 && (end === line.length || isSpaceOrTab(line.charCodeAt(end)));
|
|
1396
|
+
function readBlock(node) {
|
|
1397
|
+
switch (node.type.name) {
|
|
1398
|
+
case "paragraph": {
|
|
1399
|
+
const inline = readInline(node);
|
|
1400
|
+
return {
|
|
1401
|
+
type: "paragraph",
|
|
1402
|
+
value: inline.value,
|
|
1403
|
+
segments: inline.segments
|
|
1404
|
+
};
|
|
1826
1405
|
}
|
|
1827
|
-
case
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1406
|
+
case "heading": {
|
|
1407
|
+
const attrs = node.attrs;
|
|
1408
|
+
const inline = readInline(node);
|
|
1409
|
+
return {
|
|
1410
|
+
type: "heading",
|
|
1411
|
+
level: attrs.level,
|
|
1412
|
+
setextUnderline: attrs.setextUnderline ?? void 0,
|
|
1413
|
+
closingHashes: attrs.closingHashes ?? void 0,
|
|
1414
|
+
value: inline.value,
|
|
1415
|
+
segments: inline.segments
|
|
1416
|
+
};
|
|
1832
1417
|
}
|
|
1833
|
-
case
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
case
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1418
|
+
case "blockquote": return {
|
|
1419
|
+
type: "blockquote",
|
|
1420
|
+
children: readBlocks(node)
|
|
1421
|
+
};
|
|
1422
|
+
case "list": {
|
|
1423
|
+
const attrs = node.attrs;
|
|
1424
|
+
return {
|
|
1425
|
+
type: "listItem",
|
|
1426
|
+
kind: attrs.kind === "ordered" || attrs.kind === "task" ? attrs.kind : "bullet",
|
|
1427
|
+
order: attrs.order ?? void 0,
|
|
1428
|
+
checked: !!attrs.checked,
|
|
1429
|
+
collapsed: !!attrs.collapsed,
|
|
1430
|
+
marker: attrs.marker ?? void 0,
|
|
1431
|
+
taskMarker: attrs.taskMarker ?? void 0,
|
|
1432
|
+
markerGap: attrs.markerGap,
|
|
1433
|
+
children: readBlocks(node)
|
|
1434
|
+
};
|
|
1841
1435
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
* `start`, where a tab reaches the next multiple of four.
|
|
1871
|
-
*/
|
|
1872
|
-
function measureIndent(line, start) {
|
|
1873
|
-
let col = start;
|
|
1874
|
-
for (let i = 0; i < line.length; i++) {
|
|
1875
|
-
const code = line.charCodeAt(i);
|
|
1876
|
-
if (code === 32) col += 1;
|
|
1877
|
-
else if (code === 9) col += 4 - col % 4;
|
|
1878
|
-
else break;
|
|
1436
|
+
case "codeBlock": {
|
|
1437
|
+
const attrs = node.attrs;
|
|
1438
|
+
return {
|
|
1439
|
+
type: "codeBlock",
|
|
1440
|
+
language: attrs.language || "",
|
|
1441
|
+
fenceStyle: attrs.fenceStyle ?? void 0,
|
|
1442
|
+
fenceLength: attrs.fenceLength ?? void 0,
|
|
1443
|
+
value: node.textContent
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
case "horizontalRule": return {
|
|
1447
|
+
type: "horizontalRule",
|
|
1448
|
+
marker: node.attrs.marker ?? void 0
|
|
1449
|
+
};
|
|
1450
|
+
case "htmlComment": return {
|
|
1451
|
+
type: "htmlComment",
|
|
1452
|
+
value: node.attrs.content
|
|
1453
|
+
};
|
|
1454
|
+
case "table": return readTable(node);
|
|
1455
|
+
case "text": return {
|
|
1456
|
+
type: "text",
|
|
1457
|
+
value: node.text ?? ""
|
|
1458
|
+
};
|
|
1459
|
+
default: return {
|
|
1460
|
+
type: "ignored",
|
|
1461
|
+
value: node.textContent,
|
|
1462
|
+
hasContent: node.content.size > 0
|
|
1463
|
+
};
|
|
1879
1464
|
}
|
|
1880
|
-
return col - start;
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Whether `line` is a run of one character, `=` or `-`, and so would read as a
|
|
1884
|
-
* setext underline under a container's line prefix. Surrounding whitespace does
|
|
1885
|
-
* not save it: an underline may carry any indentation the prefix leaves under
|
|
1886
|
-
* four columns, and any trailing spaces at all.
|
|
1887
|
-
*/
|
|
1888
|
-
function isSetextUnderline(line) {
|
|
1889
|
-
let start = 0;
|
|
1890
|
-
while (start < line.length && isSpaceChar(line.charCodeAt(start))) start++;
|
|
1891
|
-
const first = line.charCodeAt(start);
|
|
1892
|
-
if (first !== 61 && first !== 45) return false;
|
|
1893
|
-
let end = line.length;
|
|
1894
|
-
while (isSpaceChar(line.charCodeAt(end - 1))) end--;
|
|
1895
|
-
for (let i = start + 1; i < end; i++) if (line.charCodeAt(i) !== first) return false;
|
|
1896
|
-
return true;
|
|
1897
1465
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
const
|
|
1906
|
-
if (count ===
|
|
1907
|
-
|
|
1908
|
-
|
|
1466
|
+
function readInline(node) {
|
|
1467
|
+
const content = node.content.content;
|
|
1468
|
+
const count = content.length;
|
|
1469
|
+
if (count === 0) return {
|
|
1470
|
+
value: "",
|
|
1471
|
+
segments: void 0
|
|
1472
|
+
};
|
|
1473
|
+
const first = node.child(0);
|
|
1474
|
+
if (count === 1 && first.isText) return {
|
|
1475
|
+
value: first.text ?? "",
|
|
1476
|
+
segments: void 0
|
|
1477
|
+
};
|
|
1478
|
+
const chunks = new Array(count);
|
|
1479
|
+
let value = "";
|
|
1909
1480
|
for (let i = 0; i < count; i++) {
|
|
1910
|
-
const child =
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
function emitList(node, out, tight) {
|
|
1915
|
-
const { kind, order, taskMarker, markerGap, checked } = node.attrs;
|
|
1916
|
-
const markerChar = listMarkerChar(node);
|
|
1917
|
-
const checkMark = taskMarker === "X" ? "X" : "x";
|
|
1918
|
-
const gap = Math.min(Math.max(markerGap ?? 1, 1), 4);
|
|
1919
|
-
const prefix = `${kind === "ordered" ? `${order ?? 1}${markerChar}` : markerChar}${" ".repeat(gap)}`;
|
|
1920
|
-
const outputMarker = kind === "task" ? `${prefix}[${checked ? checkMark : " "}] ` : prefix;
|
|
1921
|
-
const continuation = " ".repeat(prefix.length);
|
|
1922
|
-
out.withPrefix(continuation, outputMarker, () => emitBlockChildren(node, out, tight));
|
|
1923
|
-
out.closeBlock();
|
|
1924
|
-
}
|
|
1925
|
-
/**
|
|
1926
|
-
* Whether a line of list markers reads as a thematic break: three or more `-`
|
|
1927
|
-
* or `*`, all the same character, with nothing but spaces, tabs and blockquote
|
|
1928
|
-
* markers around them.
|
|
1929
|
-
*/
|
|
1930
|
-
function isThematicBreak(line) {
|
|
1931
|
-
let breakChar = 0;
|
|
1932
|
-
let count = 0;
|
|
1933
|
-
for (let i = 0; i < line.length; i++) {
|
|
1934
|
-
const code = line.charCodeAt(i);
|
|
1935
|
-
if (code === 10) break;
|
|
1936
|
-
if (code === 32 || code === 9) continue;
|
|
1937
|
-
if (code === 62 && count === 0) continue;
|
|
1938
|
-
if (code !== 45 && code !== 42) return false;
|
|
1939
|
-
if (breakChar === 0) breakChar = code;
|
|
1940
|
-
else if (code !== breakChar) return false;
|
|
1941
|
-
count++;
|
|
1942
|
-
}
|
|
1943
|
-
return count >= 3;
|
|
1944
|
-
}
|
|
1945
|
-
function emitCodeBlock(node, out) {
|
|
1946
|
-
const attrs = node.attrs;
|
|
1947
|
-
const language = attrs.language || "";
|
|
1948
|
-
const code = node.textContent;
|
|
1949
|
-
if (attrs.fenceStyle === "indented" && !language) {
|
|
1950
|
-
const indentedCode = toIndentedCode(code);
|
|
1951
|
-
if (indentedCode != null) {
|
|
1952
|
-
out.write(indentedCode);
|
|
1953
|
-
out.closeBlock();
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
if (attrs.fenceStyle === "dollar" && language === "math" && !hasDollarFenceLine(code)) {
|
|
1958
|
-
out.write("$$");
|
|
1959
|
-
out.write("\n");
|
|
1960
|
-
if (code) {
|
|
1961
|
-
out.write(code);
|
|
1962
|
-
out.write("\n");
|
|
1963
|
-
}
|
|
1964
|
-
out.write("$$");
|
|
1965
|
-
out.closeBlock();
|
|
1966
|
-
return;
|
|
1481
|
+
const child = content[i];
|
|
1482
|
+
const text = child.isText ? child.text : void 0;
|
|
1483
|
+
chunks[i] = text;
|
|
1484
|
+
if (text) value += text;
|
|
1967
1485
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
out.write(code);
|
|
1975
|
-
out.write("\n");
|
|
1976
|
-
}
|
|
1977
|
-
out.write(fence);
|
|
1978
|
-
out.closeBlock();
|
|
1979
|
-
}
|
|
1980
|
-
/**
|
|
1981
|
-
* The narrowest fence that can hold `code`: wider than every line of it that
|
|
1982
|
-
* would close the fence early, and never under CommonMark's minimum of three. A
|
|
1983
|
-
* closing fence is a run of the fence character alone on its line; a run with
|
|
1984
|
-
* anything else on the line (`` a ``` ``, `` ``` x ``) or four columns in closes
|
|
1985
|
-
* nothing, so the fence holds it as it is.
|
|
1986
|
-
*/
|
|
1987
|
-
function minFenceLength(code, tilde) {
|
|
1988
|
-
const fenceChar = tilde ? 126 : 96;
|
|
1989
|
-
let longest = 2;
|
|
1990
|
-
let lineStart = 0;
|
|
1991
|
-
while (lineStart <= code.length) {
|
|
1992
|
-
let lineEnd = code.indexOf("\n", lineStart);
|
|
1993
|
-
if (lineEnd < 0) lineEnd = code.length;
|
|
1994
|
-
let index = lineStart;
|
|
1995
|
-
while (index < lineEnd && code.charCodeAt(index) === 32) index++;
|
|
1996
|
-
if (index - lineStart < 4) {
|
|
1997
|
-
const runStart = index;
|
|
1998
|
-
while (index < lineEnd && code.charCodeAt(index) === fenceChar) index++;
|
|
1999
|
-
const run = index - runStart;
|
|
2000
|
-
while (index < lineEnd && isSpaceOrTab(code.charCodeAt(index))) index++;
|
|
2001
|
-
if (index === lineEnd && run > longest) longest = run;
|
|
1486
|
+
return {
|
|
1487
|
+
value,
|
|
1488
|
+
segments: {
|
|
1489
|
+
value,
|
|
1490
|
+
chunks,
|
|
1491
|
+
textContent: node.textContent
|
|
2002
1492
|
}
|
|
2003
|
-
|
|
2004
|
-
}
|
|
2005
|
-
return longest + 1;
|
|
1493
|
+
};
|
|
2006
1494
|
}
|
|
2007
|
-
function
|
|
2008
|
-
return
|
|
1495
|
+
function readTable(node) {
|
|
1496
|
+
return {
|
|
1497
|
+
type: "table",
|
|
1498
|
+
children: node.content.content.map(readTableRow)
|
|
1499
|
+
};
|
|
2009
1500
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
function canIndentCode(code) {
|
|
2016
|
-
return code !== "" && !code.startsWith("\n") && !code.endsWith("\n");
|
|
1501
|
+
function readTableRow(node) {
|
|
1502
|
+
return {
|
|
1503
|
+
type: "tableRow",
|
|
1504
|
+
children: node.content.content.map(readTableCell)
|
|
1505
|
+
};
|
|
2017
1506
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
function toIndentedCode(code) {
|
|
2026
|
-
if (!canIndentCode(code)) return void 0;
|
|
2027
|
-
const lines = code.split("\n");
|
|
2028
|
-
for (let i = 0; i < lines.length; i++) if (lines[i] !== "") lines[i] = ` ${lines[i]}`;
|
|
2029
|
-
return lines.join("\n");
|
|
1507
|
+
function readTableCell(node) {
|
|
1508
|
+
return {
|
|
1509
|
+
type: "tableCell",
|
|
1510
|
+
header: isNodeOfType(node, "tableHeaderCell"),
|
|
1511
|
+
align: node.attrs.align ?? void 0,
|
|
1512
|
+
children: readBlocks(node)
|
|
1513
|
+
};
|
|
2030
1514
|
}
|
|
1515
|
+
|
|
1516
|
+
//#endregion
|
|
1517
|
+
//#region src/converters/pm-to-md.ts
|
|
2031
1518
|
/**
|
|
2032
|
-
*
|
|
1519
|
+
* Convert persisted editor content to Markdown through the shared block serializer.
|
|
2033
1520
|
*/
|
|
2034
|
-
function
|
|
2035
|
-
return
|
|
2036
|
-
}
|
|
2037
|
-
function emitTable(node, out) {
|
|
2038
|
-
const rowCount = node.childCount;
|
|
2039
|
-
if (rowCount === 0) return;
|
|
2040
|
-
const rows = [];
|
|
2041
|
-
let colCount = 0;
|
|
2042
|
-
let headerIdx = -1;
|
|
2043
|
-
for (let r = 0; r < rowCount; r++) {
|
|
2044
|
-
const row = node.child(r);
|
|
2045
|
-
const cells = [];
|
|
2046
|
-
let isHeaderRow = false;
|
|
2047
|
-
for (let c = 0; c < row.childCount; c++) {
|
|
2048
|
-
const cell = row.child(c);
|
|
2049
|
-
if (isNodeOfType(cell, "tableHeaderCell")) isHeaderRow = true;
|
|
2050
|
-
cells.push(extractCellText(cell));
|
|
2051
|
-
}
|
|
2052
|
-
if (isHeaderRow && headerIdx < 0) headerIdx = r;
|
|
2053
|
-
if (cells.length > colCount) colCount = cells.length;
|
|
2054
|
-
rows.push(cells);
|
|
2055
|
-
}
|
|
2056
|
-
if (colCount === 0) return;
|
|
2057
|
-
const alignmentRow = node.child(headerIdx >= 0 ? headerIdx : 0);
|
|
2058
|
-
const delimiters = [];
|
|
2059
|
-
for (let c = 0; c < colCount; c++) {
|
|
2060
|
-
const cell = c < alignmentRow.childCount ? alignmentRow.child(c) : void 0;
|
|
2061
|
-
const align = cell ? cell.attrs.align : void 0;
|
|
2062
|
-
delimiters.push(formatDelimiter(align));
|
|
2063
|
-
}
|
|
2064
|
-
const separator = "| " + delimiters.join(" | ") + " |";
|
|
2065
|
-
const headRow = headerIdx >= 0 ? rows[headerIdx] : new Array(colCount).fill("");
|
|
2066
|
-
out.write(formatTableRow(headRow, colCount));
|
|
2067
|
-
out.write("\n");
|
|
2068
|
-
out.write(separator);
|
|
2069
|
-
for (let r = 0; r < rowCount; r++) {
|
|
2070
|
-
if (r === headerIdx) continue;
|
|
2071
|
-
out.write("\n");
|
|
2072
|
-
out.write(formatTableRow(rows[r], colCount));
|
|
2073
|
-
}
|
|
2074
|
-
out.closeBlock();
|
|
2075
|
-
}
|
|
2076
|
-
function formatDelimiter(align) {
|
|
2077
|
-
switch (align) {
|
|
2078
|
-
case "left": return ":--";
|
|
2079
|
-
case "center": return ":-:";
|
|
2080
|
-
case "right": return "--:";
|
|
2081
|
-
default: return "---";
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
function formatTableRow(cells, colCount) {
|
|
2085
|
-
let s = "|";
|
|
2086
|
-
for (let c = 0; c < colCount; c++) s += " " + (cells[c] ?? "") + " |";
|
|
2087
|
-
return s;
|
|
2088
|
-
}
|
|
2089
|
-
/**
|
|
2090
|
-
* Trim cell text and escape pipes / collapse newlines into spaces.
|
|
2091
|
-
*
|
|
2092
|
-
* Why trim: the forward parser (`markdownToDoc`) calls `.trim()`
|
|
2093
|
-
* on cell text, matching GFM's documented behavior. We must do the same
|
|
2094
|
-
* here for round-trip stability.
|
|
2095
|
-
*
|
|
2096
|
-
* Fast path: if the trimmed text contains no `|` or `\n`, return it as-is
|
|
2097
|
-
* with no further allocation.
|
|
2098
|
-
*/
|
|
2099
|
-
function extractCellText(cell) {
|
|
2100
|
-
const raw = cell.textContent.trim();
|
|
2101
|
-
if (!raw.includes("|") && !raw.includes("\n")) return raw;
|
|
2102
|
-
return raw.replaceAll("|", String.raw`\|`).replaceAll("\n", " ");
|
|
1521
|
+
function docToMarkdown(node, options = {}) {
|
|
1522
|
+
return serializeMarkdownAst(docToAst(node), options);
|
|
2103
1523
|
}
|
|
2104
1524
|
|
|
2105
1525
|
//#endregion
|
|
@@ -2566,6 +1986,7 @@ var ConfigController = class {
|
|
|
2566
1986
|
};
|
|
2567
1987
|
const refreshKeys = /* @__PURE__ */ new Set([
|
|
2568
1988
|
"markMode",
|
|
1989
|
+
"referenceDefinitions",
|
|
2569
1990
|
"placeholder",
|
|
2570
1991
|
"readOnly",
|
|
2571
1992
|
"spellCheck",
|
|
@@ -5099,6 +4520,75 @@ function emit(out, from, to, marks) {
|
|
|
5099
4520
|
]);
|
|
5100
4521
|
}
|
|
5101
4522
|
|
|
4523
|
+
//#endregion
|
|
4524
|
+
//#region src/converters/paragraph.ts
|
|
4525
|
+
/**
|
|
4526
|
+
* Build one paragraph without interpreting block-opening Markdown.
|
|
4527
|
+
*/
|
|
4528
|
+
function paragraphMarkdownToDoc(markdown, nodes = getNodeBuilders()) {
|
|
4529
|
+
return astToDoc({
|
|
4530
|
+
type: "document",
|
|
4531
|
+
children: [{
|
|
4532
|
+
type: "paragraph",
|
|
4533
|
+
value: markdown.replaceAll(/\r\n?/g, "\n").replaceAll(/\n[ \t]*\n+/g, "\n")
|
|
4534
|
+
}]
|
|
4535
|
+
}, nodes);
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4538
|
+
* Flatten textblocks into one paragraph's source content.
|
|
4539
|
+
*/
|
|
4540
|
+
function docToParagraphMarkdown(doc) {
|
|
4541
|
+
const paragraphs = [];
|
|
4542
|
+
doc.descendants((node) => {
|
|
4543
|
+
if (!node.isTextblock) return true;
|
|
4544
|
+
paragraphs.push(node.textContent);
|
|
4545
|
+
return false;
|
|
4546
|
+
});
|
|
4547
|
+
return paragraphs.join("\n").replaceAll(/\r\n?/g, "\n").replaceAll(/\n[ \t]*\n+/g, "\n");
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
//#endregion
|
|
4551
|
+
//#region src/extensions/single-paragraph.ts
|
|
4552
|
+
const singleParagraphPluginKey = new PluginKey("single-paragraph");
|
|
4553
|
+
/**
|
|
4554
|
+
* Keep editing and pasted content within one paragraph.
|
|
4555
|
+
* Opt into this extension for inline Markdown fields such as task paragraphs.
|
|
4556
|
+
* Register it alongside `defineMeowdown`, seed content with
|
|
4557
|
+
* `paragraphMarkdownToDoc`, and read edits with `docToParagraphMarkdown`.
|
|
4558
|
+
* Ordinary note editors should keep their full document behavior.
|
|
4559
|
+
*
|
|
4560
|
+
* Text input stays literal, before block input rules can consume prefixes.
|
|
4561
|
+
* Transactions and pasted blocks flatten to paragraph text separated by soft
|
|
4562
|
+
* lines. Reference definitions come from `referenceDefinitions` in the editor
|
|
4563
|
+
* config; definition-looking content in the field remains visible literal text.
|
|
4564
|
+
* Enter/submit, focus, and persistence remain the caller's responsibility.
|
|
4565
|
+
*
|
|
4566
|
+
* @example
|
|
4567
|
+
* const editor = createEditor({
|
|
4568
|
+
* extension: union(defineMeowdown({ referenceDefinitions }), defineSingleParagraph()),
|
|
4569
|
+
* defaultContent: paragraphMarkdownToDoc('Buy **milk**'),
|
|
4570
|
+
* })
|
|
4571
|
+
* const markdown = docToParagraphMarkdown(editor.state.doc)
|
|
4572
|
+
*/
|
|
4573
|
+
function defineSingleParagraph() {
|
|
4574
|
+
return withPriority$1(definePlugin(new Plugin({
|
|
4575
|
+
key: singleParagraphPluginKey,
|
|
4576
|
+
props: { handleTextInput(view, from, to, text) {
|
|
4577
|
+
view.dispatch(view.state.tr.insertText(text, from, to));
|
|
4578
|
+
return true;
|
|
4579
|
+
} },
|
|
4580
|
+
appendTransaction(transactions, _oldState, state) {
|
|
4581
|
+
if (!transactions.some((transaction) => transaction.docChanged)) return;
|
|
4582
|
+
const markdown = docToParagraphMarkdown(state.doc);
|
|
4583
|
+
if (state.doc.childCount === 1 && isNodeOfType(state.doc.child(0), "paragraph") && state.doc.child(0).textContent === markdown) return;
|
|
4584
|
+
const doc = paragraphMarkdownToDoc(markdown, getNodeBuildersForSchema(state.schema));
|
|
4585
|
+
const position = Math.min(state.selection.head, doc.content.size - 1);
|
|
4586
|
+
const transaction = state.tr.replaceWith(0, state.doc.content.size, doc.content);
|
|
4587
|
+
return transaction.setSelection(TextSelection.create(transaction.doc, Math.max(1, position)));
|
|
4588
|
+
}
|
|
4589
|
+
})), Priority$1.highest);
|
|
4590
|
+
}
|
|
4591
|
+
|
|
5102
4592
|
//#endregion
|
|
5103
4593
|
//#region src/extensions/inline-mark-plugin.ts
|
|
5104
4594
|
const META_KEY = "inline-marks-applied";
|
|
@@ -5195,7 +4685,7 @@ function createInlineMarkPlugin(getOptions) {
|
|
|
5195
4685
|
const touchesRange = pos <= range.to && pos + node.nodeSize >= range.from;
|
|
5196
4686
|
const dependsOnChange = cached == null || setsIntersect(cached.referencedKeys, changedKeys);
|
|
5197
4687
|
if (!touchesRange && !dependsOnChange) return false;
|
|
5198
|
-
const nodeChunks = chunksForTextblock(node, pos + 1, state.schema, options, references, changedKeys, isReferenceDefinitionNode(node, parent, index));
|
|
4688
|
+
const nodeChunks = chunksForTextblock(node, pos + 1, state.schema, options, references, changedKeys, !singleParagraphPluginKey.get(state) && isReferenceDefinitionNode(node, parent, index));
|
|
5199
4689
|
if (nodeChunks.length > 0) chunks.push(...nodeChunks);
|
|
5200
4690
|
const updated = chunkCache.get(node);
|
|
5201
4691
|
if (updated != null) processed.push({
|
|
@@ -5217,29 +4707,43 @@ function createInlineMarkPlugin(getOptions) {
|
|
|
5217
4707
|
if (node?.isTextblock) chunkCache.set(node, cached);
|
|
5218
4708
|
}
|
|
5219
4709
|
}
|
|
4710
|
+
function collectReferences(doc, external, state) {
|
|
4711
|
+
const local = singleParagraphPluginKey.get(state) ? {
|
|
4712
|
+
nodes: /* @__PURE__ */ new Set(),
|
|
4713
|
+
definitions: /* @__PURE__ */ new Map()
|
|
4714
|
+
} : collectReferenceDefinitions(doc);
|
|
4715
|
+
return external ? {
|
|
4716
|
+
nodes: local.nodes,
|
|
4717
|
+
definitions: new Map([...external, ...local.definitions])
|
|
4718
|
+
} : local;
|
|
4719
|
+
}
|
|
5220
4720
|
return new Plugin({
|
|
5221
4721
|
key: pluginKey,
|
|
5222
4722
|
state: {
|
|
5223
4723
|
init(_config, state) {
|
|
5224
4724
|
return {
|
|
5225
|
-
|
|
4725
|
+
externalDefinitions: getOptions?.(state)?.referenceDefinitions,
|
|
4726
|
+
references: collectReferences(state.doc, getOptions?.(state)?.referenceDefinitions, state),
|
|
5226
4727
|
pendingReferenceKeys: emptyReferenceKeys
|
|
5227
4728
|
};
|
|
5228
4729
|
},
|
|
5229
|
-
apply(transaction, value,
|
|
4730
|
+
apply(transaction, value, oldState, newState) {
|
|
5230
4731
|
if (transaction.getMeta(RESTYLE_KEY) === true) return value.pendingReferenceKeys.size === 0 ? value : {
|
|
5231
|
-
|
|
4732
|
+
...value,
|
|
5232
4733
|
pendingReferenceKeys: emptyReferenceKeys
|
|
5233
4734
|
};
|
|
5234
4735
|
if (transaction.getMeta(META_KEY)) return value;
|
|
5235
|
-
const
|
|
4736
|
+
const externalDefinitions = getOptions?.(oldState)?.referenceDefinitions;
|
|
4737
|
+
const references = singleParagraphPluginKey.get(oldState) || externalDefinitions !== value.externalDefinitions || externalDefinitions && transaction.docChanged ? collectReferences(newState.doc, externalDefinitions, newState) : updateReferenceDefinitions(value.references, transaction, newState.doc);
|
|
5236
4738
|
if (references === value.references) return value;
|
|
5237
4739
|
const changedKeys = getChangedReferenceKeys(value.references.definitions, references.definitions);
|
|
5238
4740
|
if (changedKeys.size === 0) return {
|
|
4741
|
+
externalDefinitions,
|
|
5239
4742
|
references,
|
|
5240
4743
|
pendingReferenceKeys: value.pendingReferenceKeys
|
|
5241
4744
|
};
|
|
5242
4745
|
return {
|
|
4746
|
+
externalDefinitions,
|
|
5243
4747
|
references,
|
|
5244
4748
|
pendingReferenceKeys: mergeReferenceKeys(value.pendingReferenceKeys, changedKeys)
|
|
5245
4749
|
};
|
|
@@ -7171,6 +6675,12 @@ function definePendingReplacementHandler(handler) {
|
|
|
7171
6675
|
} }) }));
|
|
7172
6676
|
}
|
|
7173
6677
|
|
|
6678
|
+
//#endregion
|
|
6679
|
+
//#region src/extensions/readonly.ts
|
|
6680
|
+
function defineReadonly(getReadonly = () => true) {
|
|
6681
|
+
return definePlugin(new Plugin({ props: { editable: (state) => !getReadonly(state) } }));
|
|
6682
|
+
}
|
|
6683
|
+
|
|
7174
6684
|
//#endregion
|
|
7175
6685
|
//#region src/utils/caret-coords.ts
|
|
7176
6686
|
/**
|
|
@@ -7845,6 +7355,46 @@ function getNodeBuildersForSchema(schema) {
|
|
|
7845
7355
|
return builders;
|
|
7846
7356
|
}
|
|
7847
7357
|
|
|
7358
|
+
//#endregion
|
|
7359
|
+
//#region src/converters/ast-to-pm.ts
|
|
7360
|
+
/**
|
|
7361
|
+
* Build nodes with the caller's schema; Markdown inline syntax remains literal text.
|
|
7362
|
+
*/
|
|
7363
|
+
function astToDoc(node, nodes = getNodeBuilders()) {
|
|
7364
|
+
const children = node.children?.map((child) => astToDoc(child, nodes)) || [];
|
|
7365
|
+
switch (node.type) {
|
|
7366
|
+
case "document": return nodes.doc(node.frontmatter == null ? {} : { frontmatter: node.frontmatter }, children);
|
|
7367
|
+
case "paragraph": return nodes.paragraph(node.value);
|
|
7368
|
+
case "heading": return nodes.heading({
|
|
7369
|
+
level: node.level,
|
|
7370
|
+
setextUnderline: node.setextUnderline ?? null,
|
|
7371
|
+
closingHashes: node.closingHashes ?? null
|
|
7372
|
+
}, node.value);
|
|
7373
|
+
case "blockquote": return nodes.blockquote(children);
|
|
7374
|
+
case "listItem": return nodes.list({
|
|
7375
|
+
kind: node.kind,
|
|
7376
|
+
order: node.order ?? null,
|
|
7377
|
+
checked: node.checked,
|
|
7378
|
+
collapsed: node.collapsed,
|
|
7379
|
+
marker: node.marker ?? null,
|
|
7380
|
+
taskMarker: node.taskMarker,
|
|
7381
|
+
markerGap: node.markerGap
|
|
7382
|
+
}, children);
|
|
7383
|
+
case "codeBlock": return nodes.codeBlock({
|
|
7384
|
+
language: node.language,
|
|
7385
|
+
fenceStyle: node.fenceStyle ?? null,
|
|
7386
|
+
fenceLength: node.fenceLength ?? null
|
|
7387
|
+
}, node.value);
|
|
7388
|
+
case "horizontalRule": return nodes.horizontalRule({ marker: node.marker ?? null });
|
|
7389
|
+
case "htmlComment": return nodes.htmlComment({ content: node.value });
|
|
7390
|
+
case "table": return nodes.table(children);
|
|
7391
|
+
case "tableRow": return nodes.tableRow(children);
|
|
7392
|
+
case "tableCell": return node.header ? nodes.tableHeaderCell({ align: node.align ?? null }, children) : nodes.tableCell({ align: node.align ?? null }, children);
|
|
7393
|
+
case "text": return nodes.text(node.value);
|
|
7394
|
+
case "ignored": throw new Error("Cannot reconstruct an unsupported editor node from Markdown");
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7848
7398
|
//#endregion
|
|
7849
7399
|
//#region src/converters/md-to-pm.ts
|
|
7850
7400
|
/**
|
|
@@ -7862,658 +7412,7 @@ function getNodeBuildersForSchema(schema) {
|
|
|
7862
7412
|
* inline-code characters survive verbatim.
|
|
7863
7413
|
*/
|
|
7864
7414
|
function markdownToDoc(markdown, options = {}) {
|
|
7865
|
-
|
|
7866
|
-
let frontmatterBody;
|
|
7867
|
-
let rest = markdown;
|
|
7868
|
-
if (frontmatter) {
|
|
7869
|
-
const [body, matchLength] = matchFrontmatter(markdown);
|
|
7870
|
-
frontmatterBody = body;
|
|
7871
|
-
if (matchLength) rest = markdown.slice(matchLength);
|
|
7872
|
-
}
|
|
7873
|
-
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest, 0, rest !== markdown);
|
|
7874
|
-
return nodes.doc(frontmatterBody === void 0 ? {} : { frontmatter: frontmatterBody }, blocks);
|
|
7875
|
-
}
|
|
7876
|
-
/**
|
|
7877
|
-
* Matches a leading YAML frontmatter block: a `---` fence at offset 0, a body,
|
|
7878
|
-
* and a closing `---` fence, each fence being exactly three dashes followed by
|
|
7879
|
-
* optional spaces or tabs. Returns the body (the lines between the fences,
|
|
7880
|
-
* joined by `\n`, without a trailing newline) and the length of the matched
|
|
7881
|
-
* region, or undefined when there is no terminated frontmatter block (a lone `---`
|
|
7882
|
-
* with no closing fence stays a thematic break).
|
|
7883
|
-
*/
|
|
7884
|
-
const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?\n)?---[ \t]*(?:\r?\n|$)/;
|
|
7885
|
-
function matchFrontmatter(markdown) {
|
|
7886
|
-
const match = FRONTMATTER_RE.exec(markdown);
|
|
7887
|
-
if (!match) return [];
|
|
7888
|
-
return [(match[1] ?? "").replace(/\r?\n$/, ""), match[0].length];
|
|
7889
|
-
}
|
|
7890
|
-
/**
|
|
7891
|
-
* Walk the document's children, converting each block-level child and
|
|
7892
|
-
* flattening any node converter that returns multiple siblings (lists are the
|
|
7893
|
-
* main case). Blank lines before the first block and after the last one are
|
|
7894
|
-
* content too, one empty paragraph each; when a frontmatter block was peeled
|
|
7895
|
-
* off ahead of `text`, the first leading blank line is that block's separator.
|
|
7896
|
-
*/
|
|
7897
|
-
function collectBlocks(nodes, cursor, text, column, afterFrontmatter) {
|
|
7898
|
-
const out = [];
|
|
7899
|
-
if (!cursor.firstChild()) {
|
|
7900
|
-
appendEmptyParagraphs(out, nodes, Math.max(1, countNewlines(text, 0, text.length)));
|
|
7901
|
-
return out;
|
|
7902
|
-
}
|
|
7903
|
-
appendEmptyParagraphs(out, nodes, countNewlines(text, 0, cursor.from) - (afterFrontmatter ? 1 : 0));
|
|
7904
|
-
let previousTo;
|
|
7905
|
-
do {
|
|
7906
|
-
if (previousTo != null) appendGapParagraphs(out, nodes, text, previousTo, cursor.from);
|
|
7907
|
-
previousTo = cursor.to;
|
|
7908
|
-
appendBlocks(out, nodes, convertBlock(nodes, cursor, text, column));
|
|
7909
|
-
} while (cursor.nextSibling());
|
|
7910
|
-
cursor.parent();
|
|
7911
|
-
const end = text.endsWith("\n") ? text.length - 1 : text.length;
|
|
7912
|
-
appendEmptyParagraphs(out, nodes, countNewlines(text, previousTo, end));
|
|
7913
|
-
return out;
|
|
7914
|
-
}
|
|
7915
|
-
/**
|
|
7916
|
-
* Append `blocks`, dropping the indented spelling from a code block that lands
|
|
7917
|
-
* right after a list: four columns of indentation would put it inside the item
|
|
7918
|
-
* above instead of after it, so it can only be written as a fence.
|
|
7919
|
-
*/
|
|
7920
|
-
function appendBlocks(out, nodes, blocks) {
|
|
7921
|
-
for (const block of blocks) {
|
|
7922
|
-
const attrs = block.attrs;
|
|
7923
|
-
if (attrs.fenceStyle === "indented" && isNodeOfType(block, "codeBlock") && out.length > 0 && isNodeOfType(out[out.length - 1], "list")) {
|
|
7924
|
-
out.push(nodes.codeBlock({
|
|
7925
|
-
language: attrs.language,
|
|
7926
|
-
fenceStyle: null,
|
|
7927
|
-
fenceLength: attrs.fenceLength
|
|
7928
|
-
}, block.textContent));
|
|
7929
|
-
continue;
|
|
7930
|
-
}
|
|
7931
|
-
out.push(block);
|
|
7932
|
-
}
|
|
7933
|
-
}
|
|
7934
|
-
/**
|
|
7935
|
-
* Blank lines between two sibling blocks are content: a run of K blank lines
|
|
7936
|
-
* is one block separator plus K-1 empty paragraphs. The gap slice between the
|
|
7937
|
-
* siblings' ranges holds only line terminators and structural prefixes
|
|
7938
|
-
* (indent, blockquote `>`), so counting newlines is enough - the gap has K+1
|
|
7939
|
-
* of them (the previous block's own terminator plus one per blank line).
|
|
7940
|
-
*/
|
|
7941
|
-
function appendGapParagraphs(out, nodes, text, gapFrom, gapTo) {
|
|
7942
|
-
appendEmptyParagraphs(out, nodes, countNewlines(text, gapFrom, gapTo) - 2);
|
|
7943
|
-
}
|
|
7944
|
-
function appendEmptyParagraphs(out, nodes, count) {
|
|
7945
|
-
for (let i = 0; i < count; i++) out.push(nodes.paragraph());
|
|
7946
|
-
}
|
|
7947
|
-
function countNewlines(text, from, to) {
|
|
7948
|
-
let count = 0;
|
|
7949
|
-
for (let i = from; i < to; i++) if (text.charCodeAt(i) === 10) count++;
|
|
7950
|
-
return count;
|
|
7951
|
-
}
|
|
7952
|
-
function convertBlock(nodes, cursor, text, column) {
|
|
7953
|
-
switch (cursor.type.id) {
|
|
7954
|
-
case LEZER_NODE_IDS.ATXHeading1: return [convertHeading(nodes, cursor, text, column, 1, false)];
|
|
7955
|
-
case LEZER_NODE_IDS.ATXHeading2: return [convertHeading(nodes, cursor, text, column, 2, false)];
|
|
7956
|
-
case LEZER_NODE_IDS.ATXHeading3: return [convertHeading(nodes, cursor, text, column, 3, false)];
|
|
7957
|
-
case LEZER_NODE_IDS.ATXHeading4: return [convertHeading(nodes, cursor, text, column, 4, false)];
|
|
7958
|
-
case LEZER_NODE_IDS.ATXHeading5: return [convertHeading(nodes, cursor, text, column, 5, false)];
|
|
7959
|
-
case LEZER_NODE_IDS.ATXHeading6: return [convertHeading(nodes, cursor, text, column, 6, false)];
|
|
7960
|
-
case LEZER_NODE_IDS.SetextHeading1: return [convertHeading(nodes, cursor, text, column, 1, true)];
|
|
7961
|
-
case LEZER_NODE_IDS.SetextHeading2: return [convertHeading(nodes, cursor, text, column, 2, true)];
|
|
7962
|
-
case LEZER_NODE_IDS.Paragraph: return [convertParagraph(nodes, cursor, text, column)];
|
|
7963
|
-
case LEZER_NODE_IDS.LinkReference: return [convertParagraph(nodes, cursor, text, column)];
|
|
7964
|
-
case LEZER_NODE_IDS.CommentBlock: return [convertHTMLComment(nodes, cursor, text, column)];
|
|
7965
|
-
case LEZER_NODE_IDS.HTMLBlock:
|
|
7966
|
-
case LEZER_NODE_IDS.ProcessingInstructionBlock: return [convertParagraph(nodes, cursor, text, column)];
|
|
7967
|
-
case LEZER_NODE_IDS.Blockquote: return [convertBlockquote(nodes, cursor, text)];
|
|
7968
|
-
case LEZER_NODE_IDS.BulletList: return convertList(nodes, cursor, text, column, "bullet");
|
|
7969
|
-
case LEZER_NODE_IDS.OrderedList: return convertList(nodes, cursor, text, column, "ordered");
|
|
7970
|
-
case LEZER_NODE_IDS.FencedCode:
|
|
7971
|
-
case LEZER_NODE_IDS.CodeBlock: return [convertCodeBlock(nodes, cursor, text)];
|
|
7972
|
-
case LEZER_NODE_IDS.BlockMath: return [convertBlockMath(nodes, cursor, text)];
|
|
7973
|
-
case LEZER_NODE_IDS.HorizontalRule: {
|
|
7974
|
-
const marker = text.slice(cursor.from, cursor.to).trimEnd();
|
|
7975
|
-
return [nodes.horizontalRule({ marker: marker === "---" ? null : marker })];
|
|
7976
|
-
}
|
|
7977
|
-
case LEZER_NODE_IDS.Table: return [convertTable(nodes, cursor, text)];
|
|
7978
|
-
case LEZER_NODE_IDS.Task: return [convertParagraph(nodes, cursor, text, column)];
|
|
7979
|
-
default:
|
|
7980
|
-
if (text.slice(cursor.from, cursor.to).trim() === "") return [];
|
|
7981
|
-
console.warn(`[meowdown] unsupported lezer block "${cursor.type.name}"`);
|
|
7982
|
-
return [convertParagraph(nodes, cursor, text, column)];
|
|
7983
|
-
}
|
|
7984
|
-
}
|
|
7985
|
-
function convertHeading(nodes, cursor, text, column, level, isSetext) {
|
|
7986
|
-
const headingFrom = cursor.from;
|
|
7987
|
-
let contentStart = cursor.from;
|
|
7988
|
-
let contentEnd = cursor.to;
|
|
7989
|
-
let trailingMarkFrom = -1;
|
|
7990
|
-
let trailingMarkTo = -1;
|
|
7991
|
-
if (cursor.firstChild()) {
|
|
7992
|
-
if (cursor.type.id === LEZER_NODE_IDS.HeaderMark && cursor.from === headingFrom) contentStart = cursor.to;
|
|
7993
|
-
let lastId = -1;
|
|
7994
|
-
let lastFrom = -1;
|
|
7995
|
-
let lastTo = -1;
|
|
7996
|
-
do {
|
|
7997
|
-
lastId = cursor.type.id;
|
|
7998
|
-
lastFrom = cursor.from;
|
|
7999
|
-
lastTo = cursor.to;
|
|
8000
|
-
} while (cursor.nextSibling());
|
|
8001
|
-
if (lastId === LEZER_NODE_IDS.HeaderMark && lastFrom > contentStart) {
|
|
8002
|
-
contentEnd = isSetext ? text.lastIndexOf("\n", lastFrom - 1) : lastFrom;
|
|
8003
|
-
trailingMarkFrom = lastFrom;
|
|
8004
|
-
trailingMarkTo = lastTo;
|
|
8005
|
-
}
|
|
8006
|
-
cursor.parent();
|
|
8007
|
-
}
|
|
8008
|
-
const raw = dedentContinuation(readLeafText(cursor, text, contentStart, contentEnd), column);
|
|
8009
|
-
const content = isSetext ? raw : trimGap(raw);
|
|
8010
|
-
const setextUnderline = isSetext ? countUnderlineChars(text, trailingMarkFrom, trailingMarkTo) || 1 : null;
|
|
8011
|
-
const closingHashes = !isSetext && trailingMarkFrom >= 0 ? countHashChars(text, trailingMarkFrom, trailingMarkTo) || null : null;
|
|
8012
|
-
return nodes.heading({
|
|
8013
|
-
level,
|
|
8014
|
-
setextUnderline,
|
|
8015
|
-
closingHashes
|
|
8016
|
-
}, content);
|
|
8017
|
-
}
|
|
8018
|
-
/**
|
|
8019
|
-
* Trim the whitespace lezer's marker gap can contain: space, tab and the line
|
|
8020
|
-
* ends (`space` in `@lezer/markdown`, matching `isSpaceChar`). A plain
|
|
8021
|
-
* `String.prototype.trim` would also take a no-break or an ideographic space,
|
|
8022
|
-
* which lezer reads as heading text - and a `#` left standing next to one flips
|
|
8023
|
-
* into a closing run on the way back (`# #\u{A0}`).
|
|
8024
|
-
*/
|
|
8025
|
-
function trimGap(text) {
|
|
8026
|
-
let start = 0;
|
|
8027
|
-
let end = text.length;
|
|
8028
|
-
while (start < end && isSpaceChar(text.charCodeAt(start))) start++;
|
|
8029
|
-
while (end > start && isSpaceChar(text.charCodeAt(end - 1))) end--;
|
|
8030
|
-
return text.slice(start, end);
|
|
8031
|
-
}
|
|
8032
|
-
/**
|
|
8033
|
-
* Count the `=` / `-` characters in a setext underline run.
|
|
8034
|
-
*/
|
|
8035
|
-
function countUnderlineChars(text, from, to) {
|
|
8036
|
-
if (from < 0) return 0;
|
|
8037
|
-
let count = 0;
|
|
8038
|
-
for (let i = from; i < to; i++) {
|
|
8039
|
-
const code = text.charCodeAt(i);
|
|
8040
|
-
if (code === 61 || code === 45) count++;
|
|
8041
|
-
}
|
|
8042
|
-
return count;
|
|
8043
|
-
}
|
|
8044
|
-
/**
|
|
8045
|
-
* Count the `#` characters between `from` and `to`.
|
|
8046
|
-
*/
|
|
8047
|
-
function countHashChars(text, from, to) {
|
|
8048
|
-
if (from < 0) return 0;
|
|
8049
|
-
let count = 0;
|
|
8050
|
-
for (let i = from; i < to; i++) if (text.charCodeAt(i) === 35) count++;
|
|
8051
|
-
return count;
|
|
8052
|
-
}
|
|
8053
|
-
/**
|
|
8054
|
-
* The column at which content begins on the line containing `from` (i.e. the
|
|
8055
|
-
* enclosing container's content column). Columns count a tab as a CommonMark
|
|
8056
|
-
* tab stop of 4 (`4 - col % 4`), matching how lezer measures indentation.
|
|
8057
|
-
*/
|
|
8058
|
-
function measureContentColumn(text, from) {
|
|
8059
|
-
const lineStart = text.lastIndexOf("\n", from - 1) + 1;
|
|
8060
|
-
let col = 0;
|
|
8061
|
-
for (let index = lineStart; index < from; index++) col += text.charCodeAt(index) === 9 ? 4 - col % 4 : 1;
|
|
8062
|
-
return col;
|
|
8063
|
-
}
|
|
8064
|
-
/**
|
|
8065
|
-
* Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4`
|
|
8066
|
-
* columns. Whitespace the container never wrote is left alone: a tab that would
|
|
8067
|
-
* reach past `column` stands for more columns than the container takes, and a
|
|
8068
|
-
* line whose indentation stops short of `column` was written lazily, without any
|
|
8069
|
-
* prefix at all.
|
|
8070
|
-
*/
|
|
8071
|
-
function sliceColumn(line, column) {
|
|
8072
|
-
let col = 0;
|
|
8073
|
-
let index = 0;
|
|
8074
|
-
while (col < column) {
|
|
8075
|
-
const code = line.charCodeAt(index);
|
|
8076
|
-
if (code === 32) col += 1;
|
|
8077
|
-
else if (code === 9) {
|
|
8078
|
-
const width = 4 - col % 4;
|
|
8079
|
-
if (col + width > column) break;
|
|
8080
|
-
col += width;
|
|
8081
|
-
} else return line;
|
|
8082
|
-
index++;
|
|
8083
|
-
}
|
|
8084
|
-
return line.slice(index);
|
|
8085
|
-
}
|
|
8086
|
-
/**
|
|
8087
|
-
* Strip a leaf block's structural continuation indent.
|
|
8088
|
-
*
|
|
8089
|
-
* lezer keeps the indent of a multi-line block's continuation lines inside the
|
|
8090
|
-
* source span (its `scrub` pads each line's container prefix to equal-width
|
|
8091
|
-
* whitespace to preserve positions). CommonMark and lezer require every
|
|
8092
|
-
* continuation line to be indented to the same content `column`, which equals
|
|
8093
|
-
* the block's first-line column. The first line is already past its indent, so
|
|
8094
|
-
* only lines 2..n are dedented. Returns `content` untouched at column 0 (a
|
|
8095
|
-
* top-level block) or when there is no continuation line.
|
|
8096
|
-
*/
|
|
8097
|
-
function dedentContinuation(content, column) {
|
|
8098
|
-
if (column === 0 || !content.includes("\n")) return content;
|
|
8099
|
-
return content.split("\n").map((line, index) => index === 0 ? line : sliceColumn(line, column)).join("\n");
|
|
8100
|
-
}
|
|
8101
|
-
/**
|
|
8102
|
-
* The blockquote markers inside `from`..`to`, as a flat run of `from, to` pairs
|
|
8103
|
-
* in document order. A marker can sit at any depth: a paragraph carries its own,
|
|
8104
|
-
* while a link reference's lands inside the `LinkLabel` that spans the break.
|
|
8105
|
-
* The cursor ends where it started.
|
|
8106
|
-
*/
|
|
8107
|
-
function collectQuoteMarks(cursor, marks, from, to) {
|
|
8108
|
-
if (!cursor.firstChild()) return;
|
|
8109
|
-
do {
|
|
8110
|
-
if (cursor.from >= to) break;
|
|
8111
|
-
if (cursor.to <= from) continue;
|
|
8112
|
-
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) {
|
|
8113
|
-
marks.push(cursor.from, cursor.to);
|
|
8114
|
-
continue;
|
|
8115
|
-
}
|
|
8116
|
-
collectQuoteMarks(cursor, marks, from, to);
|
|
8117
|
-
} while (cursor.nextSibling());
|
|
8118
|
-
cursor.parent();
|
|
8119
|
-
}
|
|
8120
|
-
/**
|
|
8121
|
-
* The source between `from` and `to`, minus the blockquote markers inside it.
|
|
8122
|
-
*
|
|
8123
|
-
* In block-only parsing a leaf block has no inline children, with one
|
|
8124
|
-
* exception: lezer leaves the `QuoteMark` of every continuation line of a
|
|
8125
|
-
* multi-line block (`> l1\n> l2`) inside the block's own span. A marker takes
|
|
8126
|
-
* the whole line prefix with it, the single space after it included, exactly as
|
|
8127
|
-
* the serializer's own prefix puts it back. Only a space: a tab after the marker
|
|
8128
|
-
* stands for the columns up to the next tab stop, more than the prefix writes
|
|
8129
|
-
* back, so it stays in the text as the indentation it is. A line with no marker
|
|
8130
|
-
* carried no prefix at all - it is a lazy continuation, and every column on it
|
|
8131
|
-
* is its own. Markers outside `from`..`to` (an ATX heading's own marks, a setext
|
|
8132
|
-
* underline's line) are left alone. The cursor ends where it started.
|
|
8133
|
-
*/
|
|
8134
|
-
function readLeafText(cursor, text, from, to) {
|
|
8135
|
-
const lineBreak = text.indexOf("\n", from);
|
|
8136
|
-
if (lineBreak < 0 || lineBreak >= to) return text.slice(from, to);
|
|
8137
|
-
const marks = [];
|
|
8138
|
-
collectQuoteMarks(cursor, marks, from, to);
|
|
8139
|
-
let content = "";
|
|
8140
|
-
let pos = from;
|
|
8141
|
-
for (let index = 0; index < marks.length; index += 2) {
|
|
8142
|
-
content += text.slice(pos, Math.max(pos, text.lastIndexOf("\n", marks[index]) + 1));
|
|
8143
|
-
pos = marks[index + 1];
|
|
8144
|
-
if (text.charCodeAt(pos) === 32) pos += 1;
|
|
8145
|
-
}
|
|
8146
|
-
return trimTrailingBlankLines(content + text.slice(pos, to));
|
|
8147
|
-
}
|
|
8148
|
-
/**
|
|
8149
|
-
* Drop the blank lines a block ends with, the line break in front of them
|
|
8150
|
-
* included. A raw HTML or processing-instruction block with no closing sequence
|
|
8151
|
-
* runs its span to the end of the document and swallows them; a leaf block's
|
|
8152
|
-
* text ends where its last line does, which is where the serializer ends its
|
|
8153
|
-
* own output (`MdOut.finish`), so recording more would lose it on the way back.
|
|
8154
|
-
*/
|
|
8155
|
-
function trimTrailingBlankLines(content) {
|
|
8156
|
-
let end = content.length;
|
|
8157
|
-
for (let index = end - 1; index >= 0; index--) {
|
|
8158
|
-
const code = content.charCodeAt(index);
|
|
8159
|
-
if (code === 10) end = index;
|
|
8160
|
-
else if (code !== 32 && code !== 9) break;
|
|
8161
|
-
}
|
|
8162
|
-
return content.slice(0, end);
|
|
8163
|
-
}
|
|
8164
|
-
/**
|
|
8165
|
-
* Build a paragraph from raw markdown content, dedenting continuation lines so
|
|
8166
|
-
* the serializer's own line prefix does not double the indent. A soft line break
|
|
8167
|
-
* stays a literal `\n` in a single text node; the paragraph spec's
|
|
8168
|
-
* `whitespace: 'pre'` keeps a DOM re-read from folding it to a space.
|
|
8169
|
-
*/
|
|
8170
|
-
function buildParagraph(nodes, content, column) {
|
|
8171
|
-
return nodes.paragraph(dedentContinuation(content, column));
|
|
8172
|
-
}
|
|
8173
|
-
function convertParagraph(nodes, cursor, text, column) {
|
|
8174
|
-
return buildParagraph(nodes, readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
8175
|
-
}
|
|
8176
|
-
/**
|
|
8177
|
-
* Build the invisible `htmlComment` node from a `CommentBlock`. The raw comment
|
|
8178
|
-
* (delimiters included) is kept verbatim on the node's `content` attribute;
|
|
8179
|
-
* continuation lines are dedented like a paragraph's so the serializer's own
|
|
8180
|
-
* line prefix re-applies the container indent instead of doubling it.
|
|
8181
|
-
*/
|
|
8182
|
-
function convertHTMLComment(nodes, cursor, text, column) {
|
|
8183
|
-
const content = dedentContinuation(readLeafText(cursor, text, cursor.from, cursor.to), column);
|
|
8184
|
-
return nodes.htmlComment({ content });
|
|
8185
|
-
}
|
|
8186
|
-
/**
|
|
8187
|
-
* A blockquote's children start at column 0: every column an enclosing container
|
|
8188
|
-
* wrote sits in front of the `> ` marker on the line, and comes off with it.
|
|
8189
|
-
*
|
|
8190
|
-
* Blank quote lines before the first block and after the last one are empty
|
|
8191
|
-
* paragraphs, like the document's own edges. The quote's range ends on its last
|
|
8192
|
-
* line rather than after it, so every newline in the trailing slice is a blank
|
|
8193
|
-
* line, and a quote made of markers only has one more line than newlines.
|
|
8194
|
-
*/
|
|
8195
|
-
function convertBlockquote(nodes, cursor, text) {
|
|
8196
|
-
const content = [];
|
|
8197
|
-
const from = cursor.from;
|
|
8198
|
-
const to = cursor.to;
|
|
8199
|
-
let previousTo;
|
|
8200
|
-
if (cursor.firstChild()) {
|
|
8201
|
-
do {
|
|
8202
|
-
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
8203
|
-
if (previousTo == null) appendEmptyParagraphs(content, nodes, countNewlines(text, from, cursor.from));
|
|
8204
|
-
else appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
8205
|
-
previousTo = cursor.to;
|
|
8206
|
-
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, 0));
|
|
8207
|
-
} while (cursor.nextSibling());
|
|
8208
|
-
cursor.parent();
|
|
8209
|
-
}
|
|
8210
|
-
appendEmptyParagraphs(content, nodes, previousTo == null ? countNewlines(text, from, to) + 1 : countNewlines(text, previousTo, to));
|
|
8211
|
-
return nodes.blockquote(content);
|
|
8212
|
-
}
|
|
8213
|
-
function convertList(nodes, cursor, text, column, kind) {
|
|
8214
|
-
const out = [];
|
|
8215
|
-
if (cursor.firstChild()) {
|
|
8216
|
-
let previousContentEnd;
|
|
8217
|
-
do {
|
|
8218
|
-
if (cursor.type.id !== LEZER_NODE_IDS.ListItem) continue;
|
|
8219
|
-
if (previousContentEnd != null) appendGapParagraphs(out, nodes, text, previousContentEnd, cursor.from);
|
|
8220
|
-
const [item, contentEnd] = convertListItem(nodes, cursor, text, column, kind);
|
|
8221
|
-
out.push(item);
|
|
8222
|
-
previousContentEnd = contentEnd;
|
|
8223
|
-
} while (cursor.nextSibling());
|
|
8224
|
-
cursor.parent();
|
|
8225
|
-
}
|
|
8226
|
-
return out;
|
|
8227
|
-
}
|
|
8228
|
-
/**
|
|
8229
|
-
* The marker style at `cursor`, plus the start number of an ordered item.
|
|
8230
|
-
*/
|
|
8231
|
-
function readListMark(cursor, text, kind) {
|
|
8232
|
-
if (kind === "ordered") {
|
|
8233
|
-
const delimiterCode = text.charCodeAt(cursor.to - 1);
|
|
8234
|
-
let marker;
|
|
8235
|
-
if (delimiterCode === 41) marker = ")";
|
|
8236
|
-
else if (delimiterCode === 46) marker = ".";
|
|
8237
|
-
const number = Number.parseInt(text.slice(cursor.from, cursor.to), 10);
|
|
8238
|
-
return {
|
|
8239
|
-
marker,
|
|
8240
|
-
order: Number.isFinite(number) ? number : 1
|
|
8241
|
-
};
|
|
8242
|
-
}
|
|
8243
|
-
const code = text.charCodeAt(cursor.from);
|
|
8244
|
-
return {
|
|
8245
|
-
marker: code === 42 ? "*" : code === 43 ? "+" : "-",
|
|
8246
|
-
order: void 0
|
|
8247
|
-
};
|
|
8248
|
-
}
|
|
8249
|
-
/**
|
|
8250
|
-
* A GFM `Task` leaf (`[ ] text` / `[x] text`, after the list mark). The checkbox
|
|
8251
|
-
* becomes the item's own attributes; the text after it becomes its paragraph.
|
|
8252
|
-
*/
|
|
8253
|
-
function convertTaskItem(nodes, cursor, text, column) {
|
|
8254
|
-
let taskStart = cursor.from;
|
|
8255
|
-
const taskEnd = cursor.to;
|
|
8256
|
-
let checked = false;
|
|
8257
|
-
let taskMarker;
|
|
8258
|
-
if (cursor.firstChild()) {
|
|
8259
|
-
if (cursor.type.id === LEZER_NODE_IDS.TaskMarker) {
|
|
8260
|
-
const taskMarkerCode = text.charCodeAt(cursor.from + 1);
|
|
8261
|
-
if (taskMarkerCode === 120) {
|
|
8262
|
-
checked = true;
|
|
8263
|
-
taskMarker = "x";
|
|
8264
|
-
} else if (taskMarkerCode === 88) {
|
|
8265
|
-
checked = true;
|
|
8266
|
-
taskMarker = "X";
|
|
8267
|
-
}
|
|
8268
|
-
taskStart = cursor.to;
|
|
8269
|
-
}
|
|
8270
|
-
cursor.parent();
|
|
8271
|
-
}
|
|
8272
|
-
if (isSpaceChar(text.charCodeAt(taskStart))) taskStart += 1;
|
|
8273
|
-
const paragraph = buildParagraph(nodes, readLeafText(cursor, text, taskStart, taskEnd), column);
|
|
8274
|
-
return {
|
|
8275
|
-
checked,
|
|
8276
|
-
taskMarker,
|
|
8277
|
-
paragraph
|
|
8278
|
-
};
|
|
8279
|
-
}
|
|
8280
|
-
/**
|
|
8281
|
-
* The gap between a list marker ending at `markTo` and the item's first content
|
|
8282
|
-
* at `contentFrom`. Only content that opens on the marker's own line measures a
|
|
8283
|
-
* gap; an item whose content starts on the next line takes the canonical single
|
|
8284
|
-
* space, the column its own continuation lines are indented to. A gap of 5+ is
|
|
8285
|
-
* indented code (a different node, so the content's column would be the code
|
|
8286
|
-
* block's), and 1 is the canonical default; only a 2-4 space gap is a faithful,
|
|
8287
|
-
* content-preserving variation.
|
|
8288
|
-
*/
|
|
8289
|
-
function measureMarkerGap(text, contentFrom, markTo, markEndColumn) {
|
|
8290
|
-
const gap = markTo != null && text.lastIndexOf("\n", contentFrom - 1) < markTo ? measureContentColumn(text, contentFrom) - markEndColumn : 1;
|
|
8291
|
-
return gap >= 2 && gap <= 4 ? gap : 1;
|
|
8292
|
-
}
|
|
8293
|
-
function convertListItem(nodes, cursor, text, column, kind) {
|
|
8294
|
-
const content = [];
|
|
8295
|
-
let taskChecked;
|
|
8296
|
-
let taskMarker;
|
|
8297
|
-
let order;
|
|
8298
|
-
let marker;
|
|
8299
|
-
let markWidth = 1;
|
|
8300
|
-
let markTo;
|
|
8301
|
-
let markEndColumn = 0;
|
|
8302
|
-
let markerGap = 1;
|
|
8303
|
-
let contentColumn = column + markWidth + markerGap;
|
|
8304
|
-
let previousTo;
|
|
8305
|
-
if (cursor.firstChild()) {
|
|
8306
|
-
do {
|
|
8307
|
-
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
8308
|
-
if (cursor.type.id === LEZER_NODE_IDS.ListMark) {
|
|
8309
|
-
const listMark = readListMark(cursor, text, kind);
|
|
8310
|
-
marker = listMark.marker;
|
|
8311
|
-
order = listMark.order;
|
|
8312
|
-
markWidth = cursor.to - cursor.from;
|
|
8313
|
-
markTo = cursor.to;
|
|
8314
|
-
markEndColumn = measureContentColumn(text, cursor.to);
|
|
8315
|
-
continue;
|
|
8316
|
-
}
|
|
8317
|
-
if (previousTo == null) {
|
|
8318
|
-
markerGap = measureMarkerGap(text, cursor.from, markTo, markEndColumn);
|
|
8319
|
-
contentColumn = column + markWidth + markerGap;
|
|
8320
|
-
} else appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
8321
|
-
previousTo = cursor.to;
|
|
8322
|
-
if (kind === "bullet" && cursor.type.id === LEZER_NODE_IDS.Task) {
|
|
8323
|
-
const task = convertTaskItem(nodes, cursor, text, contentColumn);
|
|
8324
|
-
taskChecked = task.checked;
|
|
8325
|
-
taskMarker = task.taskMarker;
|
|
8326
|
-
content.push(task.paragraph);
|
|
8327
|
-
continue;
|
|
8328
|
-
}
|
|
8329
|
-
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, contentColumn));
|
|
8330
|
-
} while (cursor.nextSibling());
|
|
8331
|
-
cursor.parent();
|
|
8332
|
-
}
|
|
8333
|
-
const isTask = taskChecked != null;
|
|
8334
|
-
const collapsed = !isTask && kind === "bullet" && marker === "+";
|
|
8335
|
-
const attrs = {
|
|
8336
|
-
kind: isTask ? "task" : kind,
|
|
8337
|
-
order: kind === "ordered" ? order ?? 1 : null,
|
|
8338
|
-
checked: taskChecked ?? false,
|
|
8339
|
-
collapsed,
|
|
8340
|
-
marker: collapsed ? null : marker,
|
|
8341
|
-
taskMarker,
|
|
8342
|
-
markerGap
|
|
8343
|
-
};
|
|
8344
|
-
return [nodes.list(attrs, content), previousTo ?? cursor.to];
|
|
8345
|
-
}
|
|
8346
|
-
function convertCodeBlock(nodes, cursor, text) {
|
|
8347
|
-
const indented = cursor.type.id === LEZER_NODE_IDS.CodeBlock;
|
|
8348
|
-
const blockTo = cursor.to;
|
|
8349
|
-
let language = "";
|
|
8350
|
-
let code = "";
|
|
8351
|
-
let fenceStyle = indented ? "indented" : null;
|
|
8352
|
-
let fenceLength = null;
|
|
8353
|
-
let sawOpeningMark = false;
|
|
8354
|
-
let pos = cursor.from;
|
|
8355
|
-
if (cursor.firstChild()) {
|
|
8356
|
-
do {
|
|
8357
|
-
code += uncoveredCode(text, pos, cursor.from);
|
|
8358
|
-
pos = cursor.to;
|
|
8359
|
-
switch (cursor.type.id) {
|
|
8360
|
-
case LEZER_NODE_IDS.CodeMark:
|
|
8361
|
-
if (sawOpeningMark) break;
|
|
8362
|
-
sawOpeningMark = true;
|
|
8363
|
-
if (text.charCodeAt(cursor.from) === 126) fenceStyle = "tilde";
|
|
8364
|
-
fenceLength = cursor.to - cursor.from;
|
|
8365
|
-
break;
|
|
8366
|
-
case LEZER_NODE_IDS.CodeInfo:
|
|
8367
|
-
language = text.slice(cursor.from, cursor.to);
|
|
8368
|
-
break;
|
|
8369
|
-
case LEZER_NODE_IDS.CodeText: code += text.slice(cursor.from, cursor.to);
|
|
8370
|
-
}
|
|
8371
|
-
} while (cursor.nextSibling());
|
|
8372
|
-
cursor.parent();
|
|
8373
|
-
code += uncoveredCode(text, pos, blockTo);
|
|
8374
|
-
}
|
|
8375
|
-
if (fenceStyle === "indented" && !canIndentCode(code)) fenceStyle = null;
|
|
8376
|
-
if (fenceLength != null && fenceLength <= minFenceLength(code, fenceStyle === "tilde")) fenceLength = null;
|
|
8377
|
-
return nodes.codeBlock({
|
|
8378
|
-
language,
|
|
8379
|
-
fenceStyle,
|
|
8380
|
-
fenceLength
|
|
8381
|
-
}, code);
|
|
8382
|
-
}
|
|
8383
|
-
/**
|
|
8384
|
-
* Text inside a code block that no child node covers is the whitespace in
|
|
8385
|
-
* front of a line's content: indentation, marker spaces, line breaks. The one
|
|
8386
|
-
* exception is a quote marker indented with a tab (`>\t \n\t>2`), where lezer
|
|
8387
|
-
* leaves the content after the marker uncovered too, so anything from a gap's
|
|
8388
|
-
* first non-whitespace character on is code the child list dropped.
|
|
8389
|
-
*/
|
|
8390
|
-
function uncoveredCode(text, from, to) {
|
|
8391
|
-
for (let index = from; index < to; index++) if (!isSpaceChar(text.charCodeAt(index))) return text.slice(index, to);
|
|
8392
|
-
return "";
|
|
8393
|
-
}
|
|
8394
|
-
/**
|
|
8395
|
-
* A `$$` display math block is a code block whose `language` is `math`; the
|
|
8396
|
-
* `dollar` fence style makes it serialize back to `$$` fences.
|
|
8397
|
-
*/
|
|
8398
|
-
function convertBlockMath(nodes, cursor, text) {
|
|
8399
|
-
let code = "";
|
|
8400
|
-
if (cursor.firstChild()) {
|
|
8401
|
-
do
|
|
8402
|
-
if (cursor.type.id === LEZER_NODE_IDS.CodeText) code += text.slice(cursor.from, cursor.to);
|
|
8403
|
-
while (cursor.nextSibling());
|
|
8404
|
-
cursor.parent();
|
|
8405
|
-
}
|
|
8406
|
-
return nodes.codeBlock({
|
|
8407
|
-
language: "math",
|
|
8408
|
-
fenceStyle: "dollar",
|
|
8409
|
-
fenceLength: null
|
|
8410
|
-
}, code);
|
|
8411
|
-
}
|
|
8412
|
-
function convertTable(nodes, cursor, text) {
|
|
8413
|
-
let aligns = [];
|
|
8414
|
-
if (cursor.firstChild()) {
|
|
8415
|
-
do
|
|
8416
|
-
if (cursor.type.id === LEZER_NODE_IDS.TableDelimiter) aligns = parseDelimiterAligns(text.slice(cursor.from, cursor.to));
|
|
8417
|
-
while (cursor.nextSibling());
|
|
8418
|
-
cursor.parent();
|
|
8419
|
-
}
|
|
8420
|
-
const rows = [];
|
|
8421
|
-
let columnCount = aligns.length;
|
|
8422
|
-
if (cursor.firstChild()) {
|
|
8423
|
-
do {
|
|
8424
|
-
const id = cursor.type.id;
|
|
8425
|
-
if (id !== LEZER_NODE_IDS.TableHeader && id !== LEZER_NODE_IDS.TableRow) continue;
|
|
8426
|
-
const cells = readTableCells(cursor, text);
|
|
8427
|
-
if (cells.length > columnCount) columnCount = cells.length;
|
|
8428
|
-
rows.push({
|
|
8429
|
-
isHeader: id === LEZER_NODE_IDS.TableHeader,
|
|
8430
|
-
cells
|
|
8431
|
-
});
|
|
8432
|
-
} while (cursor.nextSibling());
|
|
8433
|
-
cursor.parent();
|
|
8434
|
-
}
|
|
8435
|
-
return nodes.table(rows.map(({ isHeader, cells }) => {
|
|
8436
|
-
const built = [];
|
|
8437
|
-
for (let column = 0; column < columnCount; column++) {
|
|
8438
|
-
const attrs = { align: aligns[column] ?? null };
|
|
8439
|
-
const paragraph = nodes.paragraph(cells[column] ?? "");
|
|
8440
|
-
built.push(isHeader ? nodes.tableHeaderCell(attrs, paragraph) : nodes.tableCell(attrs, paragraph));
|
|
8441
|
-
}
|
|
8442
|
-
return nodes.tableRow(built);
|
|
8443
|
-
}));
|
|
8444
|
-
}
|
|
8445
|
-
function parseDelimiterAligns(separator) {
|
|
8446
|
-
return separator.split("|").map((segment) => segment.trim()).filter((segment) => segment !== "").map((segment) => {
|
|
8447
|
-
const left = segment.startsWith(":");
|
|
8448
|
-
const right = segment.endsWith(":");
|
|
8449
|
-
if (left && right) return "center";
|
|
8450
|
-
if (left) return "left";
|
|
8451
|
-
if (right) return "right";
|
|
8452
|
-
return null;
|
|
8453
|
-
});
|
|
8454
|
-
}
|
|
8455
|
-
/**
|
|
8456
|
-
* A row's cell texts, indexed by column. `@lezer/markdown` emits no `TableCell`
|
|
8457
|
-
* for an empty cell, so the column comes from the pipes counted so far, not
|
|
8458
|
-
* from the cells seen so far.
|
|
8459
|
-
*/
|
|
8460
|
-
function readTableCells(cursor, text) {
|
|
8461
|
-
const cellTexts = [];
|
|
8462
|
-
if (!cursor.firstChild()) return cellTexts;
|
|
8463
|
-
const hasLeadingPipe = cursor.type.id === LEZER_NODE_IDS.TableDelimiter;
|
|
8464
|
-
let delimiterCount = 0;
|
|
8465
|
-
do {
|
|
8466
|
-
if (cursor.type.id === LEZER_NODE_IDS.TableDelimiter) {
|
|
8467
|
-
delimiterCount++;
|
|
8468
|
-
continue;
|
|
8469
|
-
}
|
|
8470
|
-
if (cursor.type.id !== LEZER_NODE_IDS.TableCell) continue;
|
|
8471
|
-
const column = delimiterCount - (hasLeadingPipe ? 1 : 0);
|
|
8472
|
-
if (column < 0) continue;
|
|
8473
|
-
while (cellTexts.length <= column) cellTexts.push("");
|
|
8474
|
-
cellTexts[column] = text.slice(cursor.from, cursor.to).trim().replaceAll(String.raw`\|`, "|");
|
|
8475
|
-
} while (cursor.nextSibling());
|
|
8476
|
-
cursor.parent();
|
|
8477
|
-
return cellTexts;
|
|
8478
|
-
}
|
|
8479
|
-
|
|
8480
|
-
//#endregion
|
|
8481
|
-
//#region src/converters/check-roundtrip.ts
|
|
8482
|
-
function trimTrailingNewlines(text) {
|
|
8483
|
-
let end = text.length;
|
|
8484
|
-
while (end > 0 && text.charCodeAt(end - 1) === 10) end--;
|
|
8485
|
-
return text.slice(0, end);
|
|
8486
|
-
}
|
|
8487
|
-
/**
|
|
8488
|
-
* Classify how `markdown` survives the editor's parse-then-serialize round trip.
|
|
8489
|
-
*/
|
|
8490
|
-
function checkRoundTrip(markdown, options = {}) {
|
|
8491
|
-
const { frontmatter } = options;
|
|
8492
|
-
const doc = markdownToDoc(markdown, { frontmatter });
|
|
8493
|
-
let serialized = docToMarkdown(doc, { frontmatter });
|
|
8494
|
-
if (markdown === serialized) return "exact";
|
|
8495
|
-
markdown = trimTrailingNewlines(markdown);
|
|
8496
|
-
serialized = trimTrailingNewlines(serialized);
|
|
8497
|
-
if (markdown === serialized) return "exact";
|
|
8498
|
-
markdown = removeMarkdownStructure(markdown);
|
|
8499
|
-
serialized = removeMarkdownStructure(serialized);
|
|
8500
|
-
if (markdown === serialized) return "normalizing";
|
|
8501
|
-
markdown = removeNumberZeroPrefix(markdown);
|
|
8502
|
-
serialized = removeNumberZeroPrefix(serialized);
|
|
8503
|
-
if (markdown === serialized) return "normalizing";
|
|
8504
|
-
markdown = removeMathCodeBlockLanguage(markdown);
|
|
8505
|
-
serialized = removeMathCodeBlockLanguage(serialized);
|
|
8506
|
-
if (markdown === serialized) return "normalizing";
|
|
8507
|
-
return "lossy";
|
|
8508
|
-
}
|
|
8509
|
-
function removeMarkdownStructure(markdown) {
|
|
8510
|
-
return markdown.replaceAll(/[-+=_:|\\/[\]<>()`~$*#\s!]+/gu, "");
|
|
8511
|
-
}
|
|
8512
|
-
function removeNumberZeroPrefix(markdown) {
|
|
8513
|
-
return markdown.replaceAll(/0+/gu, "");
|
|
8514
|
-
}
|
|
8515
|
-
function removeMathCodeBlockLanguage(markdown) {
|
|
8516
|
-
return markdown.replaceAll(/math|latex/giu, "");
|
|
7415
|
+
return astToDoc(parseMarkdownAst(markdown, options), options.nodes);
|
|
8517
7416
|
}
|
|
8518
7417
|
|
|
8519
7418
|
//#endregion
|
|
@@ -9176,4 +8075,4 @@ function getVirtualElementFromRange(view, range) {
|
|
|
9176
8075
|
}
|
|
9177
8076
|
|
|
9178
8077
|
//#endregion
|
|
9179
|
-
export { EDITOR_KEY_BINDINGS, Priority, buildFileMarkdown, checkRoundTrip, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defaultResolveXPost, defaultResolveYouTubeVideo, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineCodeBlockSyntaxHighlight, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLComment, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkCommands, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMath, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, defineXPostMediaClickHandler, defineYouTubeVideoClickHandler, dismissLinkHover, docToMarkdown, formatFileSize, formatSizedWikiEmbed, getCodeTokens, getEditorConfig, getFileKind, getIsComposing, getLinkText, getLinkUnitAt, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunks, inlineTextToMarkChunksWithContext, insertLink, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isMarkOfType, isModEvent, isNodeOfType, isReferenceDefinitionNode, isSelectionInTableCell, loadKaTeX, markdownToDoc, normalizeHref, parsePostEmbedSnapshot, parseWikiEmbed, removeLink, renderMathInto, updateEditorConfig, updateLink, wikiEmbedBasename, withPriority };
|
|
8078
|
+
export { EDITOR_KEY_BINDINGS, Priority, buildFileMarkdown, checkRoundTrip, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defaultResolveXPost, defaultResolveYouTubeVideo, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineCodeBlockSyntaxHighlight, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLComment, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkCommands, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMath, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSingleParagraph, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, defineXPostMediaClickHandler, defineYouTubeVideoClickHandler, dismissLinkHover, docToMarkdown, docToParagraphMarkdown, formatFileSize, formatSizedWikiEmbed, getCodeTokens, getEditorConfig, getFileKind, getIsComposing, getLinkText, getLinkUnitAt, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunks, inlineTextToMarkChunksWithContext, insertLink, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isMarkOfType, isModEvent, isNodeOfType, isReferenceDefinitionNode, isSelectionInTableCell, loadKaTeX, markdownToDoc, normalizeHref, paragraphMarkdownToDoc, parsePostEmbedSnapshot, parseWikiEmbed, removeLink, renderMathInto, updateEditorConfig, updateLink, wikiEmbedBasename, withPriority };
|