markdown-parser 0.1.0 → 0.1.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.d.ts CHANGED
@@ -41,7 +41,6 @@ type InlineNode = TextNode | CodeSpanNode | HardBreakNode | SoftBreakNode | Stro
41
41
  declare class MarkdownParser {
42
42
  private splitter;
43
43
  private root;
44
- private nextLineIndex;
45
44
  private nextNodeIndex;
46
45
  private referenceDefinitions;
47
46
  parse(input: string, options?: {
@@ -50,6 +49,8 @@ declare class MarkdownParser {
50
49
  private parseLine;
51
50
  private parseReferenceLinkDefinitions;
52
51
  private convertInternalBlockToPublicBlock;
52
+ get experimental_partialNodes(): BlockNode[];
53
+ private convertInternalBlockToPartialBlock;
53
54
  }
54
55
  interface TableNode {
55
56
  type: "table";
@@ -83,9 +84,7 @@ interface BlockquoteNode {
83
84
  type ListNode = {
84
85
  type: "list";
85
86
  tight: boolean;
86
- items: Array<{
87
- children: Array<BlockNode>;
88
- }>;
87
+ items: Array<ListItemNode>;
89
88
  } & ({
90
89
  kind: "ordered";
91
90
  start: number;
@@ -93,6 +92,10 @@ type ListNode = {
93
92
  kind: "unordered";
94
93
  marker: string;
95
94
  });
95
+ type ListItemNode = {
96
+ type: "list-item";
97
+ children: Array<BlockNode>;
98
+ };
96
99
  interface HtmlBlockNode {
97
100
  type: "html-block";
98
101
  content: string;
package/dist/index.js CHANGED
@@ -1140,12 +1140,10 @@ const AUTOLINK_REGEX = /^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/i;
1140
1140
  class MarkdownParser {
1141
1141
  parse(input, options) {
1142
1142
  const stream = options?.stream ?? false;
1143
- const lines = this.splitter.split(input, {
1143
+ for (const line of this.splitter.split(input, {
1144
1144
  stream
1145
- });
1146
- for (const line of lines){
1145
+ })){
1147
1146
  this.parseLine(line.replace(/\0/g, "\uFFFD"));
1148
- this.nextLineIndex++;
1149
1147
  }
1150
1148
  // If stream is false, we need to finalize all remaining open blocks before returning the nodes.
1151
1149
  if (!stream) {
@@ -1165,9 +1163,9 @@ class MarkdownParser {
1165
1163
  return nodes;
1166
1164
  }
1167
1165
  parseLine(line) {
1168
- const lineIndex = this.nextLineIndex;
1169
1166
  // We start from the root node and descend through the rightmost path of the current node until we find the last open node that the current line continues.
1170
1167
  let lastMatchedNode = this.root;
1168
+ const lists = []; // Tracks contiguous list items in an empty line
1171
1169
  while(true){
1172
1170
  if (!("children" in lastMatchedNode)) break;
1173
1171
  // If there are no children in the current node or if the last/rightmost child of the current node is not open, we do not traverse any further, in which case the last matched node is the current node.
@@ -1181,7 +1179,6 @@ class MarkdownParser {
1181
1179
  if (blockquote !== null) {
1182
1180
  line = blockquote.content;
1183
1181
  lastMatchedNode = child;
1184
- child.endLineIndex = lineIndex;
1185
1182
  continue;
1186
1183
  } else {
1187
1184
  break;
@@ -1192,8 +1189,13 @@ class MarkdownParser {
1192
1189
  if (item === undefined || item.isClosed) break;
1193
1190
  if (isLineEmpty(line)) {
1194
1191
  const lastItemChild = item.children.at(-1);
1192
+ lists.push(item);
1195
1193
  // If the current line is empty and there are no children in the list item, we close the list item.
1196
1194
  if (lastItemChild === undefined) {
1195
+ // Mark all contiguous list items as having a pending blank line
1196
+ for (const list of lists){
1197
+ list.hasPendingBlankLine = true;
1198
+ }
1197
1199
  closeRightmostPath(child);
1198
1200
  return;
1199
1201
  } else {
@@ -1221,11 +1223,9 @@ class MarkdownParser {
1221
1223
  marker,
1222
1224
  numOfMarkers
1223
1225
  })) {
1224
- child.endLineIndex = lineIndex;
1225
1226
  child.isClosed = true;
1226
1227
  return;
1227
1228
  } else {
1228
- child.endLineIndex = lineIndex;
1229
1229
  child.lines.push(sliceLeadingIndent(line, child.indentLevel));
1230
1230
  return;
1231
1231
  }
@@ -1234,11 +1234,9 @@ class MarkdownParser {
1234
1234
  if (child.type === "indented-code-block") {
1235
1235
  // We continue the indented code block if the current line is indented enough or if it's empty.
1236
1236
  if (isIndentedCodeLine(line)) {
1237
- child.endLineIndex = lineIndex;
1238
1237
  child.lines.push(sliceLeadingIndent(line, 4));
1239
1238
  return;
1240
1239
  } else if (isLineEmpty(line)) {
1241
- child.endLineIndex = lineIndex;
1242
1240
  child.lines.push("");
1243
1241
  return;
1244
1242
  } else {
@@ -1249,10 +1247,12 @@ class MarkdownParser {
1249
1247
  if (child.type === "html-block") {
1250
1248
  // If the HTML block can be interrupted by a blank line and the current line is empty, we mark the HTML block as closed.
1251
1249
  if (child.canBeInterruptedByBlankLine && isLineEmpty(line)) {
1250
+ for (const list of lists){
1251
+ list.hasPendingBlankLine = true;
1252
+ }
1252
1253
  child.isClosed = true;
1253
1254
  return;
1254
1255
  }
1255
- child.endLineIndex = lineIndex;
1256
1256
  child.lines.push(line);
1257
1257
  // If the HTML block ends with the current line, we mark the HTML block as closed.
1258
1258
  if (child.endPattern?.test(line.trim())) {
@@ -1265,6 +1265,10 @@ class MarkdownParser {
1265
1265
  if (child.type === "paragraph" || child.type === "table") {
1266
1266
  // If the current line is empty, we mark the paragraph or table node as closed and exit as the line has been fully processed and doesn't require any further processing.
1267
1267
  if (isLineEmpty(line)) {
1268
+ // If the parent of the paragraph or table node is a list item, we mark it as having a pending blank line.
1269
+ for (const list of lists){
1270
+ list.hasPendingBlankLine = true;
1271
+ }
1268
1272
  child.isClosed = true;
1269
1273
  return;
1270
1274
  } else {
@@ -1272,6 +1276,20 @@ class MarkdownParser {
1272
1276
  }
1273
1277
  }
1274
1278
  }
1279
+ // If the last matched node is a list item and it has a pending blank line, we mark the parent list as loose.
1280
+ if (lastMatchedNode.type === "list-item" && lastMatchedNode.hasPendingBlankLine) {
1281
+ lastMatchedNode.parent.isTight = false;
1282
+ let node = lastMatchedNode;
1283
+ while(node !== null){
1284
+ if (node.type === "list-item") {
1285
+ node.hasPendingBlankLine = false;
1286
+ }
1287
+ node = node.parent;
1288
+ }
1289
+ }
1290
+ for (const list of lists){
1291
+ list.hasPendingBlankLine = true;
1292
+ }
1275
1293
  // Represents the node where newly matched nodes are expected to be added. This may not always be true for exceptional cases, like paragraph continuation.
1276
1294
  let currentContainer = lastMatchedNode;
1277
1295
  while(true){
@@ -1282,9 +1300,7 @@ class MarkdownParser {
1282
1300
  type: "blockquote",
1283
1301
  children: [],
1284
1302
  isClosed: false,
1285
- parent: currentContainer,
1286
- startLineIndex: lineIndex,
1287
- endLineIndex: lineIndex
1303
+ parent: currentContainer
1288
1304
  };
1289
1305
  addNode(node);
1290
1306
  currentContainer = node;
@@ -1299,9 +1315,7 @@ class MarkdownParser {
1299
1315
  level: heading.level,
1300
1316
  content: heading.content,
1301
1317
  isClosed: true,
1302
- parent: currentContainer,
1303
- startLineIndex: lineIndex,
1304
- endLineIndex: lineIndex
1318
+ parent: currentContainer
1305
1319
  });
1306
1320
  break;
1307
1321
  }
@@ -1316,9 +1330,7 @@ class MarkdownParser {
1316
1330
  info: fencedCode.info,
1317
1331
  lines: [],
1318
1332
  isClosed: false,
1319
- parent: currentContainer,
1320
- startLineIndex: lineIndex,
1321
- endLineIndex: lineIndex
1333
+ parent: currentContainer
1322
1334
  });
1323
1335
  break;
1324
1336
  }
@@ -1333,9 +1345,7 @@ class MarkdownParser {
1333
1345
  line
1334
1346
  ],
1335
1347
  isClosed: false,
1336
- parent: currentContainer,
1337
- startLineIndex: lineIndex,
1338
- endLineIndex: lineIndex
1348
+ parent: currentContainer
1339
1349
  };
1340
1350
  if (htmlBlock.canInterruptParagraph) {
1341
1351
  addNode(node);
@@ -1380,30 +1390,25 @@ class MarkdownParser {
1380
1390
  rows: []
1381
1391
  },
1382
1392
  isClosed: false,
1383
- parent: currentContainer,
1384
- startLineIndex: lineIndex,
1385
- endLineIndex: lineIndex
1393
+ parent: currentContainer
1386
1394
  });
1387
1395
  break;
1388
1396
  }
1389
1397
  // Setext heading
1390
1398
  const heading = parseSetextHeading(line);
1391
1399
  if (heading !== null) {
1392
- let lines = lastChild.lines;
1393
- const originalNumOfLines = lines.length;
1394
1400
  // Parse link reference definitions (if any) from the paragraph lines
1395
- let definition = parseLinkReferenceDefinition(lines);
1396
- while(definition !== null){
1397
- const { label, href, title } = definition.definition;
1401
+ const result = parseLinkReferenceDefinitions(lastChild.lines);
1402
+ for (const definition of result.definitions){
1403
+ const { label, href, title } = definition;
1398
1404
  if (!this.referenceDefinitions.has(label)) {
1399
1405
  this.referenceDefinitions.set(label, {
1400
1406
  href,
1401
1407
  title
1402
1408
  });
1403
1409
  }
1404
- lines = lines.slice(definition.nextLineIndex);
1405
- definition = parseLinkReferenceDefinition(lines);
1406
1410
  }
1411
+ const lines = lastChild.lines.slice(result.nextLineIndex);
1407
1412
  // If there are remaining lines after parsing link reference definitions, we create a new heading node with the remaining lines
1408
1413
  if (lines.length > 0) {
1409
1414
  // Remove the current paragraph node from the container node as it'll replaced by a heading node
@@ -1413,9 +1418,7 @@ class MarkdownParser {
1413
1418
  level: heading.level,
1414
1419
  content: lines.join("\n").trim(),
1415
1420
  isClosed: true,
1416
- parent: currentContainer,
1417
- startLineIndex: lastChild.startLineIndex - (originalNumOfLines - lines.length),
1418
- endLineIndex: lineIndex
1421
+ parent: currentContainer
1419
1422
  });
1420
1423
  break;
1421
1424
  } else {
@@ -1429,9 +1432,7 @@ class MarkdownParser {
1429
1432
  addNode({
1430
1433
  type: "thematic-break",
1431
1434
  isClosed: true,
1432
- parent: currentContainer,
1433
- startLineIndex: lineIndex,
1434
- endLineIndex: lineIndex
1435
+ parent: currentContainer
1435
1436
  });
1436
1437
  break;
1437
1438
  }
@@ -1442,7 +1443,6 @@ class MarkdownParser {
1442
1443
  const lastChild = currentContainer.children.at(-1);
1443
1444
  if (lastChild?.type === "paragraph" && !lastChild.isClosed) {
1444
1445
  if (isLineEmpty(list.content) || list.kind === "ordered" && list.value !== 1) {
1445
- lastChild.endLineIndex = lineIndex;
1446
1446
  lastChild.lines.push(line);
1447
1447
  break;
1448
1448
  }
@@ -1461,10 +1461,8 @@ class MarkdownParser {
1461
1461
  numOfColumns: list.numOfColumns,
1462
1462
  children: [],
1463
1463
  isClosed: false,
1464
- parent: currentContainer,
1465
- startLineIndex: lineIndex,
1466
- endLineIndex: lineIndex,
1467
- isTight: true
1464
+ isTight: true,
1465
+ parent: currentContainer
1468
1466
  };
1469
1467
  addNode(parent);
1470
1468
  }
@@ -1479,23 +1477,24 @@ class MarkdownParser {
1479
1477
  marker: list.marker,
1480
1478
  children: [],
1481
1479
  isClosed: false,
1482
- parent: currentContainer,
1483
1480
  numOfColumns: list.numOfColumns,
1484
- startLineIndex: lineIndex,
1485
- endLineIndex: lineIndex,
1486
- isTight: true
1481
+ isTight: true,
1482
+ parent: currentContainer
1487
1483
  };
1488
1484
  addNode(parent);
1489
1485
  }
1490
1486
  }
1487
+ // If the last child of the parent list is marked as having a pending blank line, we mark this list as loose (i.e., not tight)
1488
+ if (parent.children.at(-1)?.hasPendingBlankLine) {
1489
+ parent.isTight = false;
1490
+ }
1491
1491
  parent.numOfColumns = list.numOfColumns;
1492
1492
  const item = {
1493
1493
  type: "list-item",
1494
1494
  children: [],
1495
1495
  isClosed: false,
1496
- parent: parent,
1497
- startLineIndex: lineIndex,
1498
- endLineIndex: lineIndex
1496
+ hasPendingBlankLine: false,
1497
+ parent: parent
1499
1498
  };
1500
1499
  addNode(item);
1501
1500
  currentContainer = item;
@@ -1511,9 +1510,7 @@ class MarkdownParser {
1511
1510
  sliceLeadingIndent(line, 4)
1512
1511
  ],
1513
1512
  isClosed: false,
1514
- parent: currentContainer,
1515
- startLineIndex: lineIndex,
1516
- endLineIndex: lineIndex
1513
+ parent: currentContainer
1517
1514
  });
1518
1515
  break;
1519
1516
  }
@@ -1523,7 +1520,6 @@ class MarkdownParser {
1523
1520
  }
1524
1521
  // Table continuation
1525
1522
  if (lastChild?.type === "table" && !lastChild.isClosed) {
1526
- lastChild.endLineIndex = lineIndex;
1527
1523
  const numOfColumns = lastChild.head.cells.length;
1528
1524
  let cells = parseTableRow(line);
1529
1525
  if (cells.length > numOfColumns) {
@@ -1543,7 +1539,6 @@ class MarkdownParser {
1543
1539
  }
1544
1540
  // Paragraph
1545
1541
  if (latestOpenNode?.type === "paragraph") {
1546
- latestOpenNode.endLineIndex = lineIndex;
1547
1542
  latestOpenNode.lines.push(line);
1548
1543
  break;
1549
1544
  } else {
@@ -1553,9 +1548,7 @@ class MarkdownParser {
1553
1548
  line
1554
1549
  ],
1555
1550
  isClosed: false,
1556
- parent: currentContainer,
1557
- startLineIndex: lineIndex,
1558
- endLineIndex: lineIndex
1551
+ parent: currentContainer
1559
1552
  });
1560
1553
  break;
1561
1554
  }
@@ -1568,19 +1561,17 @@ class MarkdownParser {
1568
1561
  // If a block is not closed/finalized yet, we do not parse reference link definitions for it
1569
1562
  if (!block.isClosed) break;
1570
1563
  if (block.type === "paragraph") {
1571
- let lines = block.lines;
1572
- let definition = parseLinkReferenceDefinition(lines);
1573
- while(definition !== null){
1574
- const { label, href, title } = definition.definition;
1564
+ const result = parseLinkReferenceDefinitions(block.lines);
1565
+ for (const definition of result.definitions){
1566
+ const { label, href, title } = definition;
1575
1567
  if (!this.referenceDefinitions.has(label)) {
1576
1568
  this.referenceDefinitions.set(label, {
1577
1569
  href,
1578
1570
  title
1579
1571
  });
1580
1572
  }
1581
- lines = lines.slice(definition.nextLineIndex);
1582
- definition = parseLinkReferenceDefinition(lines);
1583
1573
  }
1574
+ const lines = block.lines.slice(result.nextLineIndex);
1584
1575
  if (lines.length > 0) {
1585
1576
  block.lines = lines;
1586
1577
  } else {
@@ -1659,6 +1650,7 @@ class MarkdownParser {
1659
1650
  case "list":
1660
1651
  {
1661
1652
  const items = block.children.map((item)=>({
1653
+ type: "list-item",
1662
1654
  children: item.children.map((child)=>this.convertInternalBlockToPublicBlock(child))
1663
1655
  }));
1664
1656
  if (block.kind === "ordered") {
@@ -1705,6 +1697,106 @@ class MarkdownParser {
1705
1697
  }
1706
1698
  }
1707
1699
  }
1700
+ get experimental_partialNodes() {
1701
+ return this.root.children.map((child)=>this.convertInternalBlockToPartialBlock(child)).filter((child)=>child !== null);
1702
+ }
1703
+ convertInternalBlockToPartialBlock(block) {
1704
+ switch(block.type){
1705
+ case "paragraph":
1706
+ {
1707
+ if (!block.isClosed) return null;
1708
+ return this.convertInternalBlockToPublicBlock(block);
1709
+ }
1710
+ case "heading":
1711
+ {
1712
+ return this.convertInternalBlockToPublicBlock(block);
1713
+ }
1714
+ case "thematic-break":
1715
+ {
1716
+ return this.convertInternalBlockToPublicBlock(block);
1717
+ }
1718
+ case "fenced-code-block":
1719
+ {
1720
+ let content = "";
1721
+ if (block.isClosed) {
1722
+ content = block.lines.length > 0 ? block.lines.join("\n") + "\n" : "";
1723
+ } else {
1724
+ content = block.lines.length > 0 ? block.lines.join("\n") : "";
1725
+ }
1726
+ return {
1727
+ type: "code-block",
1728
+ content: content,
1729
+ info: block.info
1730
+ };
1731
+ }
1732
+ case "indented-code-block":
1733
+ {
1734
+ let startIndex = 0;
1735
+ let endIndex = block.lines.length - 1;
1736
+ while(startIndex < endIndex){
1737
+ const line = block.lines[startIndex];
1738
+ if (line === undefined) break;
1739
+ if (!isLineEmpty(line)) break;
1740
+ startIndex++;
1741
+ }
1742
+ while(endIndex > startIndex){
1743
+ const line = block.lines[endIndex];
1744
+ if (line === undefined) break;
1745
+ if (!isLineEmpty(line)) break;
1746
+ endIndex--;
1747
+ }
1748
+ let content = "";
1749
+ if (block.isClosed) {
1750
+ content = block.lines.slice(startIndex, endIndex + 1).join("\n") + "\n";
1751
+ } else {
1752
+ content = block.lines.slice(startIndex, endIndex + 1).join("\n");
1753
+ }
1754
+ return {
1755
+ type: "code-block",
1756
+ content: content
1757
+ };
1758
+ }
1759
+ case "html-block":
1760
+ {
1761
+ return this.convertInternalBlockToPublicBlock(block);
1762
+ }
1763
+ case "table":
1764
+ {
1765
+ return this.convertInternalBlockToPublicBlock(block);
1766
+ }
1767
+ case "blockquote":
1768
+ {
1769
+ return {
1770
+ type: "blockquote",
1771
+ children: block.children.map((child)=>this.convertInternalBlockToPartialBlock(child)).filter((child)=>child !== null)
1772
+ };
1773
+ }
1774
+ case "list":
1775
+ {
1776
+ const items = block.children.map((item)=>({
1777
+ type: "list-item",
1778
+ children: item.children.map((child)=>this.convertInternalBlockToPartialBlock(child)).filter((child)=>child !== null)
1779
+ }));
1780
+ if (block.kind === "ordered") {
1781
+ return {
1782
+ type: "list",
1783
+ kind: "ordered",
1784
+ start: block.start,
1785
+ tight: block.isTight,
1786
+ items: items
1787
+ };
1788
+ } else {
1789
+ return {
1790
+ type: "list",
1791
+ kind: "unordered",
1792
+ marker: block.marker,
1793
+ tight: block.isTight,
1794
+ items: items
1795
+ };
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1708
1800
  constructor(){
1709
1801
  this.splitter = new LineSplitter();
1710
1802
  this.root = {
@@ -1712,17 +1804,13 @@ class MarkdownParser {
1712
1804
  children: [],
1713
1805
  parent: null
1714
1806
  };
1715
- this.nextLineIndex = 0;
1716
1807
  this.nextNodeIndex = 0;
1717
1808
  this.referenceDefinitions = new Map();
1718
1809
  }
1719
1810
  }
1720
1811
  function addNode(node) {
1721
- // Ensure we don't have any dangling "open" nodes on the last-child chain before inserting a new sibling at this level.
1722
1812
  closeRightmostPath(node.parent);
1723
- // Insert the new node as the next child of the parent.
1724
1813
  if (node.type === "list-item") {
1725
- // This 'if' condition exists only for TypeScript's type narrowing purposes.
1726
1814
  node.parent.children.push(node);
1727
1815
  } else {
1728
1816
  node.parent.children.push(node);
@@ -1738,61 +1826,20 @@ function addNode(node) {
1738
1826
  * @param parent The parent/container node whose last-child chain will be traversed and closed.
1739
1827
  * @param options.endLineIndex The end line index to update the nodes with.
1740
1828
  */ function closeRightmostPath(parent) {
1741
- // Find the deepest open node on the rightmost path of the parent container. Then, we close the node, traverse up the parent chain, closing the parent nodes until we reach the root node.
1742
- // You'll notice that this is not the most efficient way to do this, because we basically travel to the leaf from the parent and then back up to the parent. But, it is required for list tightness calculation.
1743
- // We could definitely optimize this by keeping track of the deepest open node from parsing the last line, but that would require a lot of additional state management, which is not worth the complexity for now.
1744
- let currentNode = getDeepestOpenNodeOnRightmostPath(parent);
1745
- while(currentNode !== parent){
1746
- if (currentNode === null) break;
1747
- currentNode.isClosed = true;
1748
- if (currentNode.type === "list") {
1749
- currentNode.isTight = isListTight(currentNode);
1750
- }
1751
- switch(currentNode.type){
1752
- case "blockquote":
1753
- case "list":
1754
- case "list-item":
1755
- {
1756
- // When closing a container node, we update the start and end line indices based on their first and last child's start and end line indices.
1757
- const firstChild = currentNode.children.at(0);
1758
- const lastChild = currentNode.children.at(-1);
1759
- if (firstChild !== undefined) {
1760
- currentNode.startLineIndex = Math.min(currentNode.startLineIndex, firstChild.startLineIndex);
1761
- }
1762
- if (lastChild !== undefined) {
1763
- currentNode.endLineIndex = Math.max(currentNode.endLineIndex, lastChild.endLineIndex);
1764
- }
1765
- break;
1766
- }
1767
- }
1768
- if (currentNode.parent?.type === "root") break;
1769
- currentNode = currentNode.parent;
1770
- }
1771
- }
1772
- function isListTight(list) {
1773
- // 1) Check if there are gaps between list items
1774
- const items = list.children;
1775
- for(let i = 0; i < items.length - 1; i++){
1776
- const a = items[i];
1777
- const b = items[i + 1];
1778
- if (a === undefined || b === undefined) break;
1779
- if (a.endLineIndex < b.startLineIndex - 1) {
1780
- return false;
1781
- }
1782
- }
1783
- // 2) Check if there are gaps between block children inside each item
1784
- for (const item of items){
1785
- const blocks = item.children;
1786
- for(let j = 0; j < blocks.length - 1; j++){
1787
- const a = blocks[j];
1788
- const b = blocks[j + 1];
1789
- if (a === undefined || b === undefined) break;
1790
- if (a.endLineIndex < b.startLineIndex - 1) {
1791
- return false;
1792
- }
1829
+ // Walk down the rightmost/last-child chain and close any still-open nodes until we hit a closed one (or run out).
1830
+ // Start at the last child of the parent; we'll keep descending into the last child of certain container nodes (e.g., blockquotes).
1831
+ let child = parent.children.at(-1);
1832
+ while(child !== undefined){
1833
+ // If we've hit a node that is already closed, everything below it on this rightmost chain is assumed to be closed too, so we can stop early.
1834
+ if (child.isClosed) break;
1835
+ child.isClosed = true;
1836
+ // If the child is a container node, we continue descending into the last child of the container node.
1837
+ if (child.type === "blockquote" || child.type === "list" || child.type === "list-item") {
1838
+ child = child.children.at(-1);
1839
+ } else {
1840
+ break;
1793
1841
  }
1794
1842
  }
1795
- return true;
1796
1843
  }
1797
1844
  function getDeepestOpenNodeOnRightmostPath(node) {
1798
1845
  const child = node.children.at(-1);
@@ -2307,10 +2354,28 @@ function parseTableRow(line) {
2307
2354
  }
2308
2355
  return cells;
2309
2356
  }
2310
- function parseLinkReferenceDefinition(lines) {
2311
- const firstLine = lines[0];
2357
+ function parseLinkReferenceDefinitions(lines) {
2358
+ const definitions = [];
2359
+ let nextLineIndex = 0;
2360
+ let definition = _parseLinkReferenceDefinition(lines, {
2361
+ startLineIndex: nextLineIndex
2362
+ });
2363
+ while(definition !== null){
2364
+ definitions.push(definition.definition);
2365
+ nextLineIndex = definition.nextLineIndex;
2366
+ definition = _parseLinkReferenceDefinition(lines, {
2367
+ startLineIndex: nextLineIndex
2368
+ });
2369
+ }
2370
+ return {
2371
+ definitions,
2372
+ nextLineIndex
2373
+ };
2374
+ }
2375
+ function _parseLinkReferenceDefinition(lines, options) {
2376
+ const firstLine = lines[options.startLineIndex];
2312
2377
  if (firstLine === undefined) return null;
2313
- let nextLineIndex = 1;
2378
+ let nextLineIndex = options.startLineIndex + 1;
2314
2379
  let content = firstLine.trim() + "\n";
2315
2380
  // A link reference definition must start with a left bracket '['
2316
2381
  if (content.charAt(0) !== "[") return null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "markdown-parser",
3
3
  "description": "A streaming-capable markdown parser.",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,