@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/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-md.ts
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
- * Emit block-level children. Consecutive `list` children form one markdown
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 canInterruptParagraph(list) {
1690
- const attrs = list.attrs;
1691
- if (attrs.kind === "ordered" && (attrs.order ?? 1) !== 1) return false;
1692
- return attrs.kind === "task" || list.childCount > 0 && list.child(0).content.size > 0;
1693
- }
1694
- /**
1695
- * The character a list item's marker ends with: the bullet itself, or the
1696
- * delimiter after an ordered item's number. A markdown list runs for as long as
1697
- * this stays the same, and a different one opens a new list.
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
- * The prefix a leaf's continuation line goes out with. The container's full
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
- * The line patterns that open an HTML block, the openers of `HTMLBlockStyle`
1784
- * in `@lezer/markdown`: script/pre/style, a comment, a processing instruction,
1785
- * a declaration, CDATA, a known block-level tag, and a complete tag alone on
1786
- * its line. Leading whitespace is stripped before matching.
1787
- */
1788
- const HTML_BLOCK_OPEN = [
1789
- /^<(?:script|pre|style)(?:\s|>|$)/i,
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 96:
1828
- case 126: {
1829
- let end = index + 1;
1830
- while (line.charCodeAt(end) === first) end++;
1831
- return end - index >= 3;
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 36: return line.charCodeAt(index + 1) === 36;
1834
- case 95: return isUnderscoreBreak(line, index);
1835
- case 42:
1836
- case 45:
1837
- case 43: return isThematicBreak(line) || index + 1 >= line.length || isSpaceOrTab(line.charCodeAt(index + 1));
1838
- case 49: {
1839
- const delimiter = line.charCodeAt(index + 1);
1840
- return (delimiter === 46 || delimiter === 41) && startsNonEmptyItem(line, index + 1);
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
- default: return false;
1843
- }
1844
- }
1845
- /**
1846
- * A `___` thematic break: three or more underscores with nothing but spaces
1847
- * and tabs between them. (`isThematicBreak` covers `-` and `*`, the two break
1848
- * characters that double as list bullets.)
1849
- */
1850
- function isUnderscoreBreak(line, index) {
1851
- let count = 0;
1852
- for (; index < line.length; index++) {
1853
- const code = line.charCodeAt(index);
1854
- if (code === 95) count++;
1855
- else if (!isSpaceOrTab(code)) return false;
1856
- }
1857
- return count >= 3;
1858
- }
1859
- /**
1860
- * Whether the list delimiter at `index` is followed by the gap and content
1861
- * (`1. x`) that let an ordered item interrupt a paragraph.
1862
- */
1863
- function startsNonEmptyItem(line, index) {
1864
- if (!isSpaceOrTab(line.charCodeAt(index + 1))) return false;
1865
- for (let i = index + 2; i < line.length; i++) if (!isSpaceOrTab(line.charCodeAt(i))) return true;
1866
- return false;
1867
- }
1868
- /**
1869
- * The columns of leading whitespace `line` stands for when it starts at column
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
- * Walk inline children writing text directly. The schema has no marks, so
1900
- * every inline child is currently a text node - but going through this
1901
- * loop instead of `node.textContent` avoids one intermediate string
1902
- * allocation per leaf block (paragraph / heading content).
1903
- */
1904
- function emitInlineChildren(node, out) {
1905
- const count = node.childCount;
1906
- if (count === 0) return;
1907
- const first = node.child(0).text;
1908
- const lazy = first == null || !out.atContentStart || !opensHTMLBlock(first);
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 = node.child(i);
1911
- if (child.isText && child.text) out.write(child.text, lazy);
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
- const tilde = attrs.fenceStyle === "tilde";
1969
- const fence = (tilde ? "~" : "`").repeat(Math.max(attrs.fenceLength ?? 0, minFenceLength(code, tilde)));
1970
- out.write(fence);
1971
- if (language) out.write(language.startsWith(tilde ? "~" : "`") ? " " + language : language);
1972
- out.write("\n");
1973
- if (code) {
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
- lineStart = lineEnd + 1;
2004
- }
2005
- return longest + 1;
1493
+ };
2006
1494
  }
2007
- function isSpaceOrTab(char) {
2008
- return char === 32 || char === 9;
1495
+ function readTable(node) {
1496
+ return {
1497
+ type: "table",
1498
+ children: node.content.content.map(readTableRow)
1499
+ };
2009
1500
  }
2010
- /**
2011
- * Whether indentation can spell `code` as an indented code block. There is no
2012
- * line to carry the four columns of a block with no content at all, and none to
2013
- * carry a blank line at either end.
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
- * Indent `code` for an indented code block, or return `undefined` for shapes
2020
- * the indented form cannot express, which fall back to a fence. Blank interior
2021
- * lines stay empty so a round-trip adds no trailing whitespace; `MdOut.write`
2022
- * still prepends the enclosing `linePrefix` (blockquote or list continuation)
2023
- * per line.
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
- * Whether any content line would read as a closing `$$` fence.
1519
+ * Convert persisted editor content to Markdown through the shared block serializer.
2033
1520
  */
2034
- function hasDollarFenceLine(code) {
2035
- return code.split("\n").some((line) => line.trim() === "$$");
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
- references: collectReferenceDefinitions(state.doc),
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, _oldState, newState) {
4730
+ apply(transaction, value, oldState, newState) {
5230
4731
  if (transaction.getMeta(RESTYLE_KEY) === true) return value.pendingReferenceKeys.size === 0 ? value : {
5231
- references: value.references,
4732
+ ...value,
5232
4733
  pendingReferenceKeys: emptyReferenceKeys
5233
4734
  };
5234
4735
  if (transaction.getMeta(META_KEY)) return value;
5235
- const references = updateReferenceDefinitions(value.references, transaction, newState.doc);
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
- const { nodes = getNodeBuilders(), frontmatter = false } = options;
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 };