n8n-nodes-notion-advanced 1.2.27-beta → 1.2.29-beta

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.
@@ -55,6 +55,14 @@ export declare class NotionAITool implements INodeType {
55
55
  static processNestedHtmlInListItem(content: string): string;
56
56
  static convertInlineHtmlToMarkdown(content: string): string;
57
57
  static processNestedList(listContent: string, listType: 'bulleted_list_item' | 'numbered_list_item', blocks: IDataObject[]): void;
58
+ static extractListItemsWithBranching(content: string): Array<{
59
+ text: string;
60
+ children: Array<{
61
+ type: string;
62
+ content: string;
63
+ }>;
64
+ }>;
65
+ static extractListItems(content: string): string[];
58
66
  static getCalloutEmoji(type: string): string;
59
67
  static getCalloutColor(type: string): string;
60
68
  static parseBasicMarkdown(text: string): IDataObject[];
@@ -745,40 +745,77 @@ class NotionAITool {
745
745
  return charIndex;
746
746
  }
747
747
  }
748
- // Enhanced hierarchical XML tree structure that catches ALL XML content
748
+ // Enhanced hierarchical XML tree structure using depth-aware parsing
749
749
  static buildXMLTree(content, tagProcessors) {
750
750
  var _a;
751
751
  const allMatches = [];
752
- const processedRanges = [];
753
- // Step 1: Collect all XML tags with specific processors
752
+ // Step 1: Use depth-aware parsing for each tag processor
754
753
  tagProcessors.forEach(({ regex, blockCreator, listProcessor }) => {
755
754
  var _a;
756
- const globalRegex = new RegExp(regex.source, 'gis');
757
- let match;
758
- while ((match = globalRegex.exec(content)) !== null) {
759
- const tagName = ((_a = match[0].match(/<(\w+)/)) === null || _a === void 0 ? void 0 : _a[1]) || 'unknown';
760
- const xmlNode = {
761
- id: `${tagName}_${match.index}_${Date.now()}_${Math.random()}`,
762
- tagName,
763
- start: match.index,
764
- end: match.index + match[0].length,
765
- match: match[0],
766
- processor: blockCreator,
767
- groups: match.slice(1),
768
- children: [],
769
- depth: 0,
770
- innerContent: match[0],
771
- replacement: undefined,
772
- listProcessor
773
- };
774
- allMatches.push(xmlNode);
775
- processedRanges.push({ start: xmlNode.start, end: xmlNode.end });
755
+ const tagPattern = (_a = regex.source.match(/<(\w+)/)) === null || _a === void 0 ? void 0 : _a[1];
756
+ if (!tagPattern)
757
+ return;
758
+ // Find all opening tags of this type
759
+ let pos = 0;
760
+ while (pos < content.length) {
761
+ const openTagStart = content.indexOf(`<${tagPattern}`, pos);
762
+ if (openTagStart === -1)
763
+ break;
764
+ const openTagEnd = content.indexOf('>', openTagStart);
765
+ if (openTagEnd === -1)
766
+ break;
767
+ // Find matching closing tag using depth tracking
768
+ let depth = 1;
769
+ let searchPos = openTagEnd + 1;
770
+ let closeTagStart = -1;
771
+ const openPattern = `<${tagPattern}`;
772
+ const closePattern = `</${tagPattern}>`;
773
+ while (searchPos < content.length && depth > 0) {
774
+ const nextOpen = content.indexOf(openPattern, searchPos);
775
+ const nextClose = content.indexOf(closePattern, searchPos);
776
+ if (nextClose === -1)
777
+ break;
778
+ if (nextOpen !== -1 && nextOpen < nextClose) {
779
+ // Found nested opening tag
780
+ depth++;
781
+ searchPos = nextOpen + openPattern.length;
782
+ }
783
+ else {
784
+ // Found closing tag
785
+ depth--;
786
+ if (depth === 0) {
787
+ closeTagStart = nextClose;
788
+ break;
789
+ }
790
+ searchPos = nextClose + closePattern.length;
791
+ }
792
+ }
793
+ if (closeTagStart !== -1) {
794
+ const fullMatch = content.substring(openTagStart, closeTagStart + closePattern.length);
795
+ const innerContent = content.substring(openTagEnd + 1, closeTagStart);
796
+ const xmlNode = {
797
+ id: `${tagPattern}_${openTagStart}_${Date.now()}_${Math.random()}`,
798
+ tagName: tagPattern,
799
+ start: openTagStart,
800
+ end: closeTagStart + closePattern.length,
801
+ match: fullMatch,
802
+ processor: blockCreator,
803
+ groups: [innerContent], // For list processors, group[0] is the inner content
804
+ children: [],
805
+ depth: 0,
806
+ innerContent,
807
+ replacement: undefined,
808
+ listProcessor
809
+ };
810
+ allMatches.push(xmlNode);
811
+ }
812
+ pos = openTagEnd + 1;
776
813
  }
777
814
  });
778
815
  // Step 2: Catch ANY remaining XML/HTML tags that weren't processed by specific processors
779
- // This prevents ANY XML content from falling through to traditional processing
780
816
  const genericXmlRegex = /<[^>]+>[\s\S]*?<\/[^>]+>|<[^>]+\/>/gis;
781
817
  let genericMatch;
818
+ const processedRanges = allMatches.map(node => ({ start: node.start, end: node.end }));
782
819
  while ((genericMatch = genericXmlRegex.exec(content)) !== null) {
783
820
  const matchStart = genericMatch.index;
784
821
  const matchEnd = genericMatch.index + genericMatch[0].length;
@@ -792,7 +829,7 @@ class NotionAITool {
792
829
  start: matchStart,
793
830
  end: matchEnd,
794
831
  match: genericMatch[0],
795
- processor: () => null, // Generic processor that just removes the content
832
+ processor: () => null,
796
833
  groups: [],
797
834
  children: [],
798
835
  depth: 0,
@@ -801,12 +838,11 @@ class NotionAITool {
801
838
  listProcessor: undefined
802
839
  };
803
840
  allMatches.push(xmlNode);
804
- processedRanges.push({ start: matchStart, end: matchEnd });
805
841
  }
806
842
  }
807
843
  // Sort by start position to maintain document order
808
844
  allMatches.sort((a, b) => a.start - b.start);
809
- // Build parent-child relationships while preserving ordering
845
+ // Build parent-child relationships
810
846
  const rootNodes = [];
811
847
  const nodeStack = [];
812
848
  for (const node of allMatches) {
@@ -826,7 +862,7 @@ class NotionAITool {
826
862
  // This is a root node
827
863
  rootNodes.push(node);
828
864
  }
829
- // Only push self-contained tags to stack (not self-closing)
865
+ // Push to stack for potential children
830
866
  if (!node.match.endsWith('/>') && node.match.includes('</')) {
831
867
  nodeStack.push(node);
832
868
  }
@@ -837,7 +873,34 @@ class NotionAITool {
837
873
  static processXMLTreeDepthFirst(nodes, blocks, placeholderCounter) {
838
874
  const replacements = new Map();
839
875
  const processNode = (node) => {
840
- // First, process all children depth-first
876
+ // For lists, use the original match content to preserve structure
877
+ // This is the only case where we skip child processing to avoid fragmentation
878
+ if (node.listProcessor && (node.tagName === 'ul' || node.tagName === 'ol')) {
879
+ try {
880
+ // Extract inner content directly from the original match
881
+ const tagName = node.tagName.toLowerCase();
882
+ const openTagRegex = new RegExp(`^<${tagName}[^>]*>`, 'i');
883
+ const closeTagRegex = new RegExp(`</${tagName}>$`, 'i');
884
+ let innerContent = node.match;
885
+ const openMatch = node.match.match(openTagRegex);
886
+ const closeMatch = node.match.match(closeTagRegex);
887
+ if (openMatch && closeMatch) {
888
+ const openTag = openMatch[0];
889
+ const closeTag = closeMatch[0];
890
+ const startIndex = openTag.length;
891
+ const endIndex = node.match.length - closeTag.length;
892
+ innerContent = node.match.substring(startIndex, endIndex);
893
+ }
894
+ // Process the list with complete content
895
+ node.listProcessor(innerContent, blocks);
896
+ return ''; // Remove completely - no placeholder needed
897
+ }
898
+ catch (error) {
899
+ console.warn(`Error processing list node ${node.tagName}:`, error);
900
+ return ''; // Remove even on error to prevent artifacts
901
+ }
902
+ }
903
+ // For non-list nodes, process children first (normal hierarchical processing)
841
904
  for (const child of node.children) {
842
905
  const childReplacement = processNode(child);
843
906
  replacements.set(child.id, childReplacement);
@@ -876,11 +939,6 @@ class NotionAITool {
876
939
  }
877
940
  // Process this node with updated inner content
878
941
  try {
879
- // Handle special list processors
880
- if (node.listProcessor && (node.tagName === 'ul' || node.tagName === 'ol')) {
881
- node.listProcessor(innerContent, blocks);
882
- return ''; // Remove completely - no placeholder needed
883
- }
884
942
  // Use blockCreator to create the block
885
943
  const block = node.processor(...node.groups);
886
944
  if (block) {
@@ -1157,23 +1215,9 @@ class NotionAITool {
1157
1215
  };
1158
1216
  }
1159
1217
  },
1160
- // Standalone list items (only if not already processed in lists): <li>content</li>
1161
- {
1162
- regex: /<li\s*[^>]*>(.*?)<\/li>/gis,
1163
- blockCreator: (content) => {
1164
- if (content.trim()) {
1165
- // Convert HTML to markdown first, then parse to rich text
1166
- const markdownContent = NotionAITool.convertInlineHtmlToMarkdown(content.trim());
1167
- return {
1168
- type: 'bulleted_list_item',
1169
- bulleted_list_item: {
1170
- rich_text: NotionAITool.parseBasicMarkdown(markdownContent),
1171
- },
1172
- };
1173
- }
1174
- return null;
1175
- }
1176
- },
1218
+ // REMOVED: Standalone <li> processor
1219
+ // <li> tags should ONLY be processed within <ul>/<ol> contexts via the list processors above
1220
+ // Having a standalone <li> processor causes XML fragments and double processing
1177
1221
  // Line breaks: <br/> or <br>
1178
1222
  {
1179
1223
  regex: /<br\s*\/?>/gis,
@@ -1399,61 +1443,28 @@ class NotionAITool {
1399
1443
  if (!processed)
1400
1444
  return '';
1401
1445
  try {
1402
- // Handle multiple segments separated by HTML block elements
1403
- const segments = [];
1404
- // Split by block-level HTML elements like <p>, <div>, etc.
1405
- const blockElements = /<(p|div|h[1-6]|blockquote)\s*[^>]*>.*?<\/\1>/gis;
1406
- let lastIndex = 0;
1407
- let match;
1408
- const blockMatches = [];
1409
- while ((match = blockElements.exec(processed)) !== null) {
1410
- blockMatches.push({
1411
- start: match.index,
1412
- end: match.index + match[0].length,
1413
- content: match[0],
1414
- tag: match[1]
1415
- });
1416
- }
1417
- // Sort matches by position
1418
- blockMatches.sort((a, b) => a.start - b.start);
1419
- // Process text segments between block elements
1420
- blockMatches.forEach((blockMatch, index) => {
1421
- // Add text before this block element
1422
- if (blockMatch.start > lastIndex) {
1423
- const beforeText = processed.substring(lastIndex, blockMatch.start).trim();
1424
- if (beforeText) {
1425
- segments.push(NotionAITool.convertInlineHtmlToMarkdown(beforeText));
1426
- }
1427
- }
1428
- // Process content inside block element
1429
- const innerContent = blockMatch.content.replace(new RegExp(`^<${blockMatch.tag}[^>]*>`, 'i'), '')
1430
- .replace(new RegExp(`</${blockMatch.tag}>$`, 'i'), '')
1431
- .trim();
1432
- if (innerContent) {
1433
- segments.push(NotionAITool.convertInlineHtmlToMarkdown(innerContent));
1434
- }
1435
- lastIndex = blockMatch.end;
1436
- });
1437
- // Add remaining text after last block element
1438
- if (lastIndex < processed.length) {
1439
- const remainingText = processed.substring(lastIndex).trim();
1440
- if (remainingText) {
1441
- segments.push(NotionAITool.convertInlineHtmlToMarkdown(remainingText));
1442
- }
1443
- }
1444
- // If no block elements were found, process the whole content
1445
- if (blockMatches.length === 0) {
1446
- segments.push(NotionAITool.convertInlineHtmlToMarkdown(processed));
1447
- }
1448
- // Join segments with space and clean up
1449
- const result = segments.filter(s => s.trim()).join(' ').trim();
1450
- // Final cleanup of any remaining artifacts
1451
- return result.replace(/\s+/g, ' ').trim();
1446
+ // First, aggressively remove any nested list tags and their content
1447
+ // This prevents XML fragments from appearing in the final content
1448
+ processed = processed.replace(/<[uo]l\s*[^>]*>[\s\S]*?<\/[uo]l>/gi, '');
1449
+ // Remove any standalone list item tags that might be left behind
1450
+ processed = processed.replace(/<\/?li\s*[^>]*>/gi, '');
1451
+ // Remove any other common list-related fragments
1452
+ processed = processed.replace(/<\/?[uo]l\s*[^>]*>/gi, '');
1453
+ // Simple cleanup - just remove remaining HTML tags and clean whitespace
1454
+ // Avoid convertInlineHtmlToMarkdown to prevent duplication issues
1455
+ const result = processed
1456
+ .replace(/<[^>]*>/g, ' ') // Remove any remaining HTML tags
1457
+ .replace(/\s+/g, ' ') // Clean up whitespace
1458
+ .trim();
1459
+ return result;
1452
1460
  }
1453
1461
  catch (error) {
1454
1462
  console.warn('Error processing nested HTML in list item:', error);
1455
- // Fallback: just remove HTML tags and return text
1456
- return processed.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
1463
+ // Fallback: aggressively remove all HTML tags and return clean text
1464
+ return processed
1465
+ .replace(/<[^>]*>/g, ' ')
1466
+ .replace(/\s+/g, ' ')
1467
+ .trim();
1457
1468
  }
1458
1469
  }
1459
1470
  // Helper function to convert inline HTML to markdown
@@ -1488,75 +1499,32 @@ class NotionAITool {
1488
1499
  processed = processed.replace(/\s+/g, ' ').trim();
1489
1500
  return processed;
1490
1501
  }
1491
- // Helper function to process nested lists and flatten them for Notion
1502
+ // Helper function to process lists using branch-based approach
1503
+ // Each <ul> and <ol> represents a new branch that contains children
1492
1504
  static processNestedList(listContent, listType, blocks) {
1493
1505
  try {
1494
- // More robust list item extraction using regex
1495
- const liRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
1496
- let match;
1497
- while ((match = liRegex.exec(listContent)) !== null) {
1498
- let itemContent = match[1].trim();
1499
- if (!itemContent)
1506
+ // Process each <li> element as a potential branch point
1507
+ const listItems = NotionAITool.extractListItemsWithBranching(listContent);
1508
+ for (const item of listItems) {
1509
+ if (!item.text && !item.children.length)
1500
1510
  continue;
1501
- // Check if this item contains nested lists
1502
- const hasNestedList = /<[uo]l\s*[^>]*>/i.test(itemContent);
1503
- if (hasNestedList) {
1504
- // Split content into parts: before nested list, nested list, after nested list
1505
- const parts = itemContent.split(/(<[uo]l\s*[^>]*>[\s\S]*?<\/[uo]l>)/i);
1506
- // Process the main content (before nested list)
1507
- const mainContent = parts[0] ? parts[0].trim() : '';
1508
- if (mainContent) {
1509
- const cleanContent = NotionAITool.processNestedHtmlInListItem(mainContent);
1510
- if (cleanContent) {
1511
- blocks.push({
1512
- type: listType,
1513
- [listType]: {
1514
- rich_text: NotionAITool.parseBasicMarkdown(cleanContent),
1515
- },
1516
- });
1517
- }
1518
- }
1519
- // Process nested lists
1520
- for (let i = 1; i < parts.length; i += 2) {
1521
- const nestedListHtml = parts[i];
1522
- if (nestedListHtml) {
1523
- const nestedListMatch = nestedListHtml.match(/<([uo]l)\s*[^>]*>([\s\S]*?)<\/\1>/i);
1524
- if (nestedListMatch) {
1525
- const [, nestedListTag, nestedContent] = nestedListMatch;
1526
- const nestedListType = nestedListTag === 'ul' ? 'bulleted_list_item' : 'numbered_list_item';
1527
- // Recursively process nested list
1528
- NotionAITool.processNestedList(nestedContent, nestedListType, blocks);
1529
- }
1530
- }
1531
- }
1532
- // Process any content after nested lists
1533
- if (parts.length > 2) {
1534
- const afterContent = parts.slice(2).join('').trim();
1535
- if (afterContent) {
1536
- const cleanContent = NotionAITool.processNestedHtmlInListItem(afterContent);
1537
- if (cleanContent) {
1538
- blocks.push({
1539
- type: listType,
1540
- [listType]: {
1541
- rich_text: NotionAITool.parseBasicMarkdown(cleanContent),
1542
- },
1543
- });
1544
- }
1545
- }
1546
- }
1547
- }
1548
- else {
1549
- // Simple item without nested lists
1550
- const cleanContent = NotionAITool.processNestedHtmlInListItem(itemContent);
1551
- if (cleanContent) {
1511
+ // Create list item for the parent text (if any)
1512
+ if (item.text && item.text.trim()) {
1513
+ const cleanText = NotionAITool.processNestedHtmlInListItem(item.text);
1514
+ if (cleanText) {
1552
1515
  blocks.push({
1553
1516
  type: listType,
1554
1517
  [listType]: {
1555
- rich_text: NotionAITool.parseBasicMarkdown(cleanContent),
1518
+ rich_text: NotionAITool.parseBasicMarkdown(cleanText),
1556
1519
  },
1557
1520
  });
1558
1521
  }
1559
1522
  }
1523
+ // Process each child branch
1524
+ for (const child of item.children) {
1525
+ const childListType = child.type === 'ul' ? 'bulleted_list_item' : 'numbered_list_item';
1526
+ NotionAITool.processNestedList(child.content, childListType, blocks);
1527
+ }
1560
1528
  }
1561
1529
  }
1562
1530
  catch (error) {
@@ -1570,6 +1538,220 @@ class NotionAITool {
1570
1538
  });
1571
1539
  }
1572
1540
  }
1541
+ // Extract list items with proper branching structure - only process top-level <li> tags
1542
+ static extractListItemsWithBranching(content) {
1543
+ const items = [];
1544
+ let pos = 0;
1545
+ while (pos < content.length) {
1546
+ // Find next <li> tag at the current level
1547
+ const liStart = content.indexOf('<li', pos);
1548
+ if (liStart === -1)
1549
+ break;
1550
+ const liOpenEnd = content.indexOf('>', liStart);
1551
+ if (liOpenEnd === -1)
1552
+ break;
1553
+ // Find the matching </li> using proper depth tracking for nested tags
1554
+ let depth = 0;
1555
+ let searchPos = liOpenEnd + 1; // Start after the opening <li> tag
1556
+ let liEnd = -1;
1557
+ while (searchPos < content.length) {
1558
+ const nextLiOpen = content.indexOf('<li', searchPos);
1559
+ const nextLiClose = content.indexOf('</li>', searchPos);
1560
+ // Handle case where no more closing tags
1561
+ if (nextLiClose === -1)
1562
+ break;
1563
+ // If there's an opening tag before the next closing tag
1564
+ if (nextLiOpen !== -1 && nextLiOpen < nextLiClose) {
1565
+ depth++;
1566
+ searchPos = nextLiOpen + 3; // Move past '<li'
1567
+ }
1568
+ else {
1569
+ // Found a closing tag
1570
+ if (depth === 0) {
1571
+ // This is our matching closing tag
1572
+ liEnd = nextLiClose;
1573
+ break;
1574
+ }
1575
+ else {
1576
+ // This closing tag belongs to a nested li
1577
+ depth--;
1578
+ searchPos = nextLiClose + 5; // Move past '</li>'
1579
+ }
1580
+ }
1581
+ }
1582
+ if (liEnd === -1) {
1583
+ // No matching closing tag found
1584
+ pos = liOpenEnd + 1;
1585
+ continue;
1586
+ }
1587
+ // Extract the content between <li> and </li>
1588
+ const fullItemContent = content.substring(liOpenEnd + 1, liEnd);
1589
+ if (!fullItemContent.trim()) {
1590
+ pos = liEnd + 5;
1591
+ continue;
1592
+ }
1593
+ const item = { text: '', children: [] };
1594
+ // Process the content to separate text from nested lists
1595
+ let contentPos = 0;
1596
+ let textParts = [];
1597
+ while (contentPos < fullItemContent.length) {
1598
+ // Look for the next nested list (ul or ol)
1599
+ const nextUlStart = fullItemContent.indexOf('<ul', contentPos);
1600
+ const nextOlStart = fullItemContent.indexOf('<ol', contentPos);
1601
+ let nextListStart = -1;
1602
+ let listType = '';
1603
+ if (nextUlStart !== -1 && (nextOlStart === -1 || nextUlStart < nextOlStart)) {
1604
+ nextListStart = nextUlStart;
1605
+ listType = 'ul';
1606
+ }
1607
+ else if (nextOlStart !== -1) {
1608
+ nextListStart = nextOlStart;
1609
+ listType = 'ol';
1610
+ }
1611
+ if (nextListStart === -1) {
1612
+ // No more nested lists - add remaining text
1613
+ const remainingText = fullItemContent.substring(contentPos);
1614
+ if (remainingText.trim()) {
1615
+ textParts.push(remainingText);
1616
+ }
1617
+ break;
1618
+ }
1619
+ // Add text before the nested list
1620
+ const textBefore = fullItemContent.substring(contentPos, nextListStart);
1621
+ if (textBefore.trim()) {
1622
+ textParts.push(textBefore);
1623
+ }
1624
+ // Find the end of this nested list
1625
+ const listOpenEnd = fullItemContent.indexOf('>', nextListStart);
1626
+ if (listOpenEnd === -1) {
1627
+ // Malformed list tag
1628
+ textParts.push(fullItemContent.substring(contentPos));
1629
+ break;
1630
+ }
1631
+ // Track depth to find the matching closing tag
1632
+ let listDepth = 1;
1633
+ let listSearchPos = listOpenEnd + 1;
1634
+ let listEnd = -1;
1635
+ const openTag = `<${listType}`;
1636
+ const closeTag = `</${listType}>`;
1637
+ while (listSearchPos < fullItemContent.length && listDepth > 0) {
1638
+ const nextListOpen = fullItemContent.indexOf(openTag, listSearchPos);
1639
+ const nextListClose = fullItemContent.indexOf(closeTag, listSearchPos);
1640
+ if (nextListClose === -1)
1641
+ break;
1642
+ if (nextListOpen !== -1 && nextListOpen < nextListClose) {
1643
+ listDepth++;
1644
+ listSearchPos = nextListOpen + openTag.length;
1645
+ }
1646
+ else {
1647
+ listDepth--;
1648
+ if (listDepth === 0) {
1649
+ listEnd = nextListClose + closeTag.length;
1650
+ break;
1651
+ }
1652
+ listSearchPos = nextListClose + closeTag.length;
1653
+ }
1654
+ }
1655
+ if (listEnd !== -1) {
1656
+ // Extract the content between <ul>/<ol> and </ul>/<ol>
1657
+ const listContent = fullItemContent.substring(listOpenEnd + 1, listEnd - closeTag.length);
1658
+ item.children.push({
1659
+ type: listType,
1660
+ content: listContent
1661
+ });
1662
+ contentPos = listEnd;
1663
+ }
1664
+ else {
1665
+ // Malformed nested list - treat remaining as text
1666
+ textParts.push(fullItemContent.substring(contentPos));
1667
+ break;
1668
+ }
1669
+ }
1670
+ // Combine all text parts and clean them
1671
+ if (textParts.length > 0) {
1672
+ const combinedText = textParts.join(' ').trim();
1673
+ const cleanText = NotionAITool.processNestedHtmlInListItem(combinedText);
1674
+ if (cleanText) {
1675
+ item.text = cleanText;
1676
+ }
1677
+ }
1678
+ // Only add items that have either text or children
1679
+ if (item.text.trim() || item.children.length > 0) {
1680
+ items.push(item);
1681
+ }
1682
+ pos = liEnd + 5; // Move past </li>
1683
+ }
1684
+ return items;
1685
+ }
1686
+ // Helper function to properly extract list items handling nested <li> tags
1687
+ static extractListItems(content) {
1688
+ const items = [];
1689
+ // Use a more robust regex approach that respects nesting
1690
+ // This regex captures the complete <li>...</li> blocks including nested content
1691
+ const liRegex = /<li[^>]*>((?:[^<]|<(?!\/li>))*?(?:<[uo]l[^>]*>[\s\S]*?<\/[uo]l>(?:[^<]|<(?!\/li>))*?)*?)<\/li>/gi;
1692
+ let match;
1693
+ while ((match = liRegex.exec(content)) !== null) {
1694
+ const itemContent = match[1];
1695
+ if (itemContent && itemContent.trim()) {
1696
+ items.push(itemContent.trim());
1697
+ }
1698
+ }
1699
+ // Fallback to the old depth-tracking method if regex fails
1700
+ if (items.length === 0) {
1701
+ let currentPos = 0;
1702
+ while (currentPos < content.length) {
1703
+ // Find the next <li> opening tag
1704
+ const liStart = content.indexOf('<li', currentPos);
1705
+ if (liStart === -1)
1706
+ break;
1707
+ // Find the end of the opening tag
1708
+ const openTagEnd = content.indexOf('>', liStart);
1709
+ if (openTagEnd === -1)
1710
+ break;
1711
+ // Now find the matching closing </li> tag accounting for nesting
1712
+ let depth = 1;
1713
+ let pos = openTagEnd + 1;
1714
+ let itemEnd = -1;
1715
+ while (pos < content.length && depth > 0) {
1716
+ const nextLiOpen = content.indexOf('<li', pos);
1717
+ const nextLiClose = content.indexOf('</li>', pos);
1718
+ // If no more closing tags, we're done
1719
+ if (nextLiClose === -1)
1720
+ break;
1721
+ // If there's an opening tag before the next closing tag, increase depth
1722
+ if (nextLiOpen !== -1 && nextLiOpen < nextLiClose) {
1723
+ depth++;
1724
+ pos = nextLiOpen + 3; // Move past '<li'
1725
+ }
1726
+ else {
1727
+ // Found a closing tag
1728
+ depth--;
1729
+ if (depth === 0) {
1730
+ itemEnd = nextLiClose + 5; // Include the '</li>'
1731
+ break;
1732
+ }
1733
+ else {
1734
+ pos = nextLiClose + 5; // Move past '</li>'
1735
+ }
1736
+ }
1737
+ }
1738
+ if (itemEnd !== -1) {
1739
+ // Extract the content between <li...> and </li>
1740
+ const fullMatch = content.substring(liStart, itemEnd);
1741
+ const innerMatch = fullMatch.match(/<li[^>]*>([\s\S]*)<\/li>$/);
1742
+ if (innerMatch) {
1743
+ items.push(innerMatch[1]);
1744
+ }
1745
+ currentPos = itemEnd;
1746
+ }
1747
+ else {
1748
+ // Malformed HTML, skip this tag
1749
+ currentPos = openTagEnd + 1;
1750
+ }
1751
+ }
1752
+ }
1753
+ return items;
1754
+ }
1573
1755
  // Helper function to get callout emoji based on type
1574
1756
  static getCalloutEmoji(type) {
1575
1757
  const emojiMap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-notion-advanced",
3
- "version": "1.2.27-beta",
3
+ "version": "1.2.29-beta",
4
4
  "description": "Advanced n8n Notion nodes: Full-featured workflow node + AI Agent Tool for intelligent Notion automation with 25+ block types (BETA)",
5
5
  "scripts": {},
6
6
  "files": [