UNPKG

n8n-nodes-notion-advanced

Version:

Advanced n8n Notion nodes: Full-featured workflow node + AI Agent Tool for intelligent Notion automation with 25+ block types (BETA)

1,076 lines (1,075 loc) 109 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NotionAITool = void 0; const n8n_workflow_1 = require("n8n-workflow"); const NotionUtils_1 = require("./NotionUtils"); class NotionAITool { constructor() { this.description = { displayName: 'Notion AI Tool', name: 'notionAiTool', icon: 'file:notion.svg', group: ['ai'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'AI-powered tool for creating and managing Notion content. Designed for use with AI Agent Nodes.', defaults: { name: 'Notion AI Tool', }, inputs: ['main'], outputs: ['main'], usableAsTool: true, codex: { categories: ['Productivity', 'AI', 'Documentation'], subcategories: { 'Productivity': ['Notion', 'Knowledge Management'], 'AI': ['AI Agent Tools', 'Natural Language Processing'], 'Documentation': ['Page Creation', 'Content Management'] }, resources: { primaryDocumentation: [ { url: 'https://github.com/AZ-IT-US/n8n-notion-advanced-node#ai-tool-usage', }, ], }, alias: ['notion', 'productivity', 'ai-tool', 'pages', 'database'], }, credentials: [ { name: 'notionApi', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Create Page with Content', value: 'createPageWithContent', description: 'Create a new Notion page with structured content including text, headings, lists, and formatting', action: 'Create a Notion page with content', }, { name: 'Add Content to Page', value: 'addContentToPage', description: 'Append new content blocks (paragraphs, headings, lists, etc.) to an existing Notion page', action: 'Add content to existing page', }, { name: 'Search and Retrieve Pages', value: 'searchPages', description: 'Search for Notion pages by title, content, or properties and retrieve their information', action: 'Search and retrieve pages', }, { name: 'Update Page Properties', value: 'updatePageProperties', description: 'Update page title, properties, status, tags, or other metadata', action: 'Update page properties', }, { name: 'Create Database Entry', value: 'createDatabaseEntry', description: 'Create a new entry in a Notion database with specified properties and values', action: 'Create database entry', }, { name: 'Query Database', value: 'queryDatabase', description: 'Search and filter database entries based on criteria and retrieve matching records', action: 'Query database', }, ], default: 'createPageWithContent', }, // CREATE PAGE WITH CONTENT { displayName: 'Page Title', name: 'pageTitle', type: 'string', required: true, displayOptions: { show: { operation: ['createPageWithContent'], }, }, default: '', description: 'The title of the new page to create', }, { displayName: 'Parent Page/Database ID', name: 'parentId', type: 'string', required: true, displayOptions: { show: { operation: ['createPageWithContent', 'createDatabaseEntry'], }, }, default: '', description: 'ID of the parent page or database where this should be created. Can be a Notion URL or page ID.', }, { displayName: 'Content', name: 'content', type: 'string', typeOptions: { rows: 6, }, displayOptions: { show: { operation: ['createPageWithContent', 'addContentToPage'], }, }, default: '', description: 'The content to add. Use natural language - AI will structure it into appropriate blocks (headings, paragraphs, lists, etc.)', placeholder: 'Example:\n# Main Heading\nThis is a paragraph with **bold** and *italic* text.\n\n## Subheading\n- First bullet point\n- Second bullet point\n\n> This is a quote block', }, // ADD CONTENT TO PAGE { displayName: 'Target Page ID', name: 'targetPageId', type: 'string', required: true, displayOptions: { show: { operation: ['addContentToPage', 'updatePageProperties'], }, }, default: '', description: 'ID or URL of the existing page to modify', }, // SEARCH PAGES { displayName: 'Search Query', name: 'searchQuery', type: 'string', displayOptions: { show: { operation: ['searchPages'], }, }, default: '', description: 'Search terms to find pages. Leave empty to get all pages.', }, // UPDATE PAGE PROPERTIES { displayName: 'Properties to Update', name: 'propertiesToUpdate', type: 'string', typeOptions: { rows: 4, }, displayOptions: { show: { operation: ['updatePageProperties'], }, }, default: '', description: 'Properties to update in JSON format or natural language. Example: {"status": "In Progress", "priority": "High"} or "Set status to Done and priority to Low"', }, // DATABASE OPERATIONS { displayName: 'Database ID', name: 'databaseId', type: 'string', required: true, displayOptions: { show: { operation: ['queryDatabase'], }, }, default: '', description: 'ID or URL of the database to query', }, { displayName: 'Entry Properties', name: 'entryProperties', type: 'string', typeOptions: { rows: 4, }, displayOptions: { show: { operation: ['createDatabaseEntry'], }, }, default: '', description: 'Properties for the new database entry in JSON format or natural language description', }, { displayName: 'Query Filter', name: 'queryFilter', type: 'string', displayOptions: { show: { operation: ['queryDatabase'], }, }, default: '', description: 'Filter criteria in natural language (e.g., "status is Done and priority is High") or JSON format', }, // COMMON OPTIONS { displayName: 'Additional Options', name: 'additionalOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'Icon', name: 'icon', type: 'string', default: '', description: 'Emoji icon for the page (e.g., 📝, 🎯, 📊)', }, { displayName: 'Cover Image URL', name: 'coverUrl', type: 'string', default: '', description: 'URL of cover image for the page', }, { displayName: 'Max Results', name: 'maxResults', type: 'number', default: 20, description: 'Maximum number of results to return (1-100)', }, ], }, ], }; } async execute() { const items = this.getInputData(); const responseData = []; // Validate credentials const isValid = await NotionUtils_1.validateCredentials.call(this); if (!isValid) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid Notion API credentials'); } for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); let result; switch (operation) { case 'createPageWithContent': result = await NotionAITool.createPageWithContent(this, i); break; case 'addContentToPage': result = await NotionAITool.addContentToPage(this, i); break; case 'searchPages': result = await NotionAITool.searchPages(this, i); break; case 'updatePageProperties': result = await NotionAITool.updatePageProperties(this, i); break; case 'createDatabaseEntry': result = await NotionAITool.createDatabaseEntry(this, i); break; case 'queryDatabase': result = await NotionAITool.queryDatabase(this, i); break; default: throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`); } responseData.push({ operation, success: true, ...result, }); } catch (error) { if (this.continueOnFail()) { responseData.push({ error: error.message, success: false, }); } else { throw error; } } } return [this.helpers.returnJsonArray(responseData)]; } // Helper method to support both camelCase and underscore parameter names for AI agent compatibility static getFlexibleParameter(executeFunctions, itemIndex, primaryName, alternativeNames = [], defaultValue) { try { // First try the primary (camelCase) parameter name return executeFunctions.getNodeParameter(primaryName, itemIndex, defaultValue); } catch (error) { // If that fails, try each alternative name for (const altName of alternativeNames) { try { return executeFunctions.getNodeParameter(altName, itemIndex, defaultValue); } catch (altError) { // Continue to next alternative } } // If all parameter names fail, return default value or throw original error if (defaultValue !== undefined) { return defaultValue; } throw error; } } static async createPageWithContent(executeFunctions, itemIndex) { // Support both camelCase and underscore parameter names for AI agent compatibility const pageTitle = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'pageTitle', ['Page_Title', 'page_title']); const parentId = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'parentId', ['Parent_Page_Database_ID', 'parent_id', 'parentPageDatabaseId']); const content = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'content', ['Content'], ''); const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); const resolvedParentId = await NotionUtils_1.resolvePageId.call(executeFunctions, parentId); // Create the page first const pageBody = { parent: { page_id: resolvedParentId }, properties: { title: { title: [(0, NotionUtils_1.createRichText)(pageTitle)], }, }, }; // Add icon and cover if provided if (additionalOptions.icon) { pageBody.icon = { type: 'emoji', emoji: additionalOptions.icon }; } if (additionalOptions.coverUrl) { pageBody.cover = { type: 'external', external: { url: additionalOptions.coverUrl } }; } const page = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'POST', '/pages', pageBody); // If content is provided, add it to the page if (content) { const blocks = NotionAITool.parseContentToBlocks(content); if (blocks.length > 0) { await NotionUtils_1.notionApiRequest.call(executeFunctions, 'PATCH', `/blocks/${page.id}/children`, { children: blocks, }); } } return { pageId: page.id, title: pageTitle, url: page.url, message: `Created page "${pageTitle}" with content`, }; } static async addContentToPage(executeFunctions, itemIndex) { const targetPageId = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'targetPageId', ['Target_Page_ID', 'target_page_id']); const content = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'content', ['Content']); const resolvedPageId = await NotionUtils_1.resolvePageId.call(executeFunctions, targetPageId); const blocks = NotionAITool.parseContentToBlocks(content); if (blocks.length === 0) { throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), 'No valid content blocks found to add'); } const result = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'PATCH', `/blocks/${resolvedPageId}/children`, { children: blocks, }); return { pageId: resolvedPageId, blocksAdded: blocks.length, message: `Added ${blocks.length} content blocks to page`, result, }; } static async searchPages(executeFunctions, itemIndex) { var _a, _b; const searchQuery = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'searchQuery', ['Search_Query', 'search_query'], ''); const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); const maxResults = additionalOptions.maxResults || 20; const body = { page_size: Math.min(maxResults, 100), }; if (searchQuery) { body.query = searchQuery; } body.filter = { property: 'object', value: 'page', }; const response = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'POST', '/search', body); return { totalResults: ((_a = response.results) === null || _a === void 0 ? void 0 : _a.length) || 0, pages: response.results || [], message: `Found ${((_b = response.results) === null || _b === void 0 ? void 0 : _b.length) || 0} pages`, }; } static async updatePageProperties(executeFunctions, itemIndex) { const targetPageId = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'targetPageId', ['Target_Page_ID', 'target_page_id']); const propertiesToUpdate = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'propertiesToUpdate', ['Properties_To_Update', 'properties_to_update']); const resolvedPageId = await NotionUtils_1.resolvePageId.call(executeFunctions, targetPageId); const properties = NotionAITool.parsePropertiesToUpdate(propertiesToUpdate); const result = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'PATCH', `/pages/${resolvedPageId}`, { properties, }); return { pageId: resolvedPageId, updatedProperties: Object.keys(properties), message: `Updated ${Object.keys(properties).length} properties`, result, }; } static async createDatabaseEntry(executeFunctions, itemIndex) { const parentId = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'parentId', ['Parent_Page_Database_ID', 'parent_id', 'parentPageDatabaseId']); const entryProperties = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'entryProperties', ['Entry_Properties', 'entry_properties']); const resolvedParentId = await NotionUtils_1.resolvePageId.call(executeFunctions, parentId); const properties = NotionAITool.parsePropertiesToUpdate(entryProperties); const result = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'POST', '/pages', { parent: { database_id: resolvedParentId }, properties, }); return { entryId: result.id, databaseId: resolvedParentId, message: 'Created new database entry', result, }; } static async queryDatabase(executeFunctions, itemIndex) { var _a, _b; const databaseId = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'databaseId', ['Database_ID', 'database_id']); const queryFilter = NotionAITool.getFlexibleParameter(executeFunctions, itemIndex, 'queryFilter', ['Query_Filter', 'query_filter'], ''); const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); const maxResults = additionalOptions.maxResults || 20; const resolvedDatabaseId = await NotionUtils_1.resolvePageId.call(executeFunctions, databaseId); const body = { page_size: Math.min(maxResults, 100), }; if (queryFilter) { try { body.filter = JSON.parse(queryFilter); } catch { // If not JSON, create a simple text filter body.filter = { property: 'Name', title: { contains: queryFilter, }, }; } } const response = await NotionUtils_1.notionApiRequest.call(executeFunctions, 'POST', `/databases/${resolvedDatabaseId}/query`, body); return { databaseId: resolvedDatabaseId, totalResults: ((_a = response.results) === null || _a === void 0 ? void 0 : _a.length) || 0, entries: response.results || [], message: `Found ${((_b = response.results) === null || _b === void 0 ? void 0 : _b.length) || 0} database entries`, }; } static parseContentToBlocks(content) { const blocks = []; // Handle both actual newlines and escaped \n characters const normalizedContent = content.replace(/\\n/g, '\n'); // First, process XML-like tags for reliable parsing const processedContent = NotionAITool.processXmlTags(normalizedContent, blocks); // Then process remaining content with traditional markdown patterns const lines = processedContent.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip completely empty lines and placeholder artifacts if (!trimmedLine || /__BLOCK_\d+__/.test(trimmedLine) || /^\d+__$/.test(trimmedLine)) continue; // Skip lines that contain ANY XML/HTML tag patterns (to prevent double processing) // This is a comprehensive check to ensure NO XML content gets processed twice const hasAnyXmlTags = ( // Basic XML/HTML tag detection /<[^>]+>/.test(trimmedLine) || // HTML-encoded tags /&lt;[^&]+&gt;/.test(trimmedLine) || // Any opening or closing XML/HTML tags /<\/?[a-zA-Z][a-zA-Z0-9]*[^>]*>/.test(trimmedLine) || // Self-closing tags /<[a-zA-Z][a-zA-Z0-9]*[^>]*\/>/.test(trimmedLine) || // Common XML/HTML tag names (comprehensive list) /<\/?(?:h[1-6]|p|div|span|ul|ol|li|strong|b|em|i|code|pre|blockquote|callout|todo|image|embed|bookmark|equation|toggle|quote|divider|br|a|u|s|del|mark)\b[^>]*>/i.test(trimmedLine) || // Specific attribute patterns /(?:type|src|href|alt|language|checked)="[^"]*"/.test(trimmedLine) || // Any line that looks like it contains XML structure /^\s*<[^>]+>.*<\/[^>]+>\s*$/.test(trimmedLine) || // Lines that start or end with XML tags /^\s*<[^>]+>/.test(trimmedLine) || /<\/[^>]+>\s*$/.test(trimmedLine)); if (hasAnyXmlTags) { continue; // Skip ALL lines containing XML content to prevent double processing } // Traditional markdown patterns (for backwards compatibility) if (trimmedLine.startsWith('# ')) { blocks.push({ type: 'heading_1', heading_1: { rich_text: [(0, NotionUtils_1.createRichText)(trimmedLine.substring(2).trim())], }, }); } else if (trimmedLine.startsWith('## ')) { blocks.push({ type: 'heading_2', heading_2: { rich_text: [(0, NotionUtils_1.createRichText)(trimmedLine.substring(3).trim())], }, }); } else if (trimmedLine.startsWith('### ')) { blocks.push({ type: 'heading_3', heading_3: { rich_text: [(0, NotionUtils_1.createRichText)(trimmedLine.substring(4).trim())], }, }); } else if (trimmedLine === '---' || trimmedLine === '***') { blocks.push({ type: 'divider', divider: {}, }); } else if (trimmedLine.includes('[!') && trimmedLine.startsWith('>')) { // Callout blocks: > [!info] content const calloutMatch = trimmedLine.match(/^>\s*\[!(\w+)\]\s*(.*)/i); if (calloutMatch) { const [, calloutType, text] = calloutMatch; const emoji = NotionAITool.getCalloutEmoji(calloutType.toLowerCase()); const color = NotionAITool.getCalloutColor(calloutType.toLowerCase()); blocks.push({ type: 'callout', callout: { rich_text: NotionAITool.parseBasicMarkdown(text), icon: { type: 'emoji', emoji }, color: color, }, }); } else { blocks.push({ type: 'quote', quote: { rich_text: NotionAITool.parseBasicMarkdown(trimmedLine.substring(1).trim()), }, }); } } else if (trimmedLine.startsWith('![') && trimmedLine.includes('](') && trimmedLine.endsWith(')')) { // Image: ![alt text](url) const match = trimmedLine.match(/^!\[(.*?)\]\((.*?)\)$/); if (match) { const [, altText, url] = match; blocks.push({ type: 'image', image: { type: 'external', external: { url }, caption: altText ? NotionAITool.parseBasicMarkdown(altText) : [], }, }); } } else if (trimmedLine.startsWith('$$') && trimmedLine.endsWith('$$') && trimmedLine.length > 4) { // Equation: $$equation$$ const equation = trimmedLine.substring(2, trimmedLine.length - 2).trim(); blocks.push({ type: 'equation', equation: { expression: equation, }, }); } else if ((trimmedLine.startsWith('http://') || trimmedLine.startsWith('https://')) && !trimmedLine.includes(' ')) { // Check if it's a video URL for embed, otherwise bookmark const videoPatterns = [ /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:vimeo\.com\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:twitch\.tv\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:loom\.com\/share\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:figma\.com\/)/i, /(?:https?:\/\/)?(?:www\.)?(?:miro\.com\/)/i, /(?:https?:\/\/)?(?:codepen\.io\/)/i ]; const isEmbeddableUrl = videoPatterns.some(pattern => pattern.test(trimmedLine)); if (isEmbeddableUrl) { blocks.push({ type: 'embed', embed: { url: trimmedLine, }, }); } else { blocks.push({ type: 'bookmark', bookmark: { url: trimmedLine, }, }); } } else if (trimmedLine.startsWith('- [') && (trimmedLine.includes('[ ]') || trimmedLine.includes('[x]') || trimmedLine.includes('[X]'))) { // To-do list items: - [ ] or - [x] or - [X] const isChecked = trimmedLine.includes('[x]') || trimmedLine.includes('[X]'); const text = trimmedLine.replace(/^-\s*\[[ xX]\]\s*/, '').trim(); blocks.push({ type: 'to_do', to_do: { rich_text: NotionAITool.parseBasicMarkdown(text), checked: isChecked, }, }); } else if (trimmedLine.startsWith('- ') && !trimmedLine.startsWith('- [')) { // Bullet list items: - item (but not todos) const listText = trimmedLine.substring(2).trim(); blocks.push({ type: 'bulleted_list_item', bulleted_list_item: { rich_text: NotionAITool.parseBasicMarkdown(listText), }, }); } else if (/^\d+\.\s/.test(trimmedLine)) { // Numbered list items: 1. item const listText = trimmedLine.replace(/^\d+\.\s/, '').trim(); blocks.push({ type: 'numbered_list_item', numbered_list_item: { rich_text: NotionAITool.parseBasicMarkdown(listText), }, }); } else if (trimmedLine.startsWith('> ') && !trimmedLine.includes('[!')) { // Quote block (but not callout) blocks.push({ type: 'quote', quote: { rich_text: NotionAITool.parseBasicMarkdown(trimmedLine.substring(2).trim()), }, }); } else if (trimmedLine.startsWith('```')) { // Handle code blocks const language = trimmedLine.substring(3).trim() || 'plain text'; const codeLines = []; i++; // Skip the opening ``` // Collect all code lines until closing ``` while (i < lines.length && !lines[i].trim().startsWith('```')) { codeLines.push(lines[i]); i++; } blocks.push({ type: 'code', code: { rich_text: [(0, NotionUtils_1.createRichText)(codeLines.join('\n'))], language: language === 'plain text' ? 'plain_text' : language, }, }); } else { // Regular paragraph - handle basic markdown formatting const richText = NotionAITool.parseBasicMarkdown(trimmedLine); blocks.push({ type: 'paragraph', paragraph: { rich_text: richText, }, }); } } return blocks; } // Helper function to resolve overlapping tag matches static resolveOverlaps(matches) { const resolved = []; const sorted = matches.sort((a, b) => { if (a.start !== b.start) return a.start - b.start; return (b.end - b.start) - (a.end - a.start); // Prefer longer matches }); for (const match of sorted) { const hasOverlap = resolved.some(existing => (match.start < existing.end && match.end > existing.start)); if (!hasOverlap) { resolved.push(match); } } return resolved; } // Helper function to validate XML tag structure static validateXmlTag(match, tagName) { try { // Basic validation for well-formed tags const openTag = new RegExp(`<${tagName}[^>]*>`, 'i'); const closeTag = new RegExp(`</${tagName}>`, 'i'); if (!openTag.test(match) || !closeTag.test(match)) { console.warn(`Malformed XML tag detected: ${match.substring(0, 50)}...`); return false; } return true; } catch (error) { console.warn(`Error validating XML tag: ${error}`); return false; } } // Helper function for optimized string replacement static optimizedReplace(content, matches) { if (matches.length === 0) return content; const parts = []; let lastIndex = 0; matches.forEach(({ start, end, replacement }) => { parts.push(content.substring(lastIndex, start)); parts.push(replacement); lastIndex = end; }); parts.push(content.substring(lastIndex)); return parts.join(''); } // Helper function for Unicode-safe position calculation static getUtf8BytePosition(str, charIndex) { try { return Buffer.from(str.substring(0, charIndex), 'utf8').length; } catch (error) { // Fallback to character index if Buffer operations fail return charIndex; } } // Enhanced hierarchical XML tree structure using depth-aware parsing static buildXMLTree(content, tagProcessors) { var _a; const allMatches = []; // Step 1: Use regex-based parsing to properly extract capture groups, then enhance with depth-aware structure tagProcessors.forEach(({ regex, blockCreator, listProcessor }) => { var _a; const globalRegex = new RegExp(regex.source, 'gis'); let match; while ((match = globalRegex.exec(content)) !== null) { const fullMatch = match[0]; const matchStart = match.index; const matchEnd = match.index + fullMatch.length; // Extract tag name for identification const tagPattern = ((_a = regex.source.match(/<(\w+)/)) === null || _a === void 0 ? void 0 : _a[1]) || 'unknown'; // Extract inner content (between opening and closing tags) let innerContent = ''; try { const openTagRegex = new RegExp(`^<${tagPattern}[^>]*>`, 'i'); const closeTagRegex = new RegExp(`</${tagPattern}>$`, 'i'); const openMatch = fullMatch.match(openTagRegex); const closeMatch = fullMatch.match(closeTagRegex); if (openMatch && closeMatch) { const openTag = openMatch[0]; const closeTag = closeMatch[0]; const startIndex = openTag.length; const endIndex = fullMatch.length - closeTag.length; innerContent = fullMatch.substring(startIndex, endIndex); } else { // Fallback for self-closing or malformed tags innerContent = fullMatch.replace(/^<[^>]*>/, '').replace(/<\/[^>]*>$/, ''); } } catch (error) { console.warn(`Error extracting inner content for ${tagPattern}:`, error); innerContent = fullMatch; } const xmlNode = { id: `${tagPattern}_${matchStart}_${Date.now()}_${Math.random()}`, tagName: tagPattern, start: matchStart, end: matchEnd, match: fullMatch, processor: blockCreator, groups: match.slice(1), // Proper regex capture groups (excluding full match) children: [], depth: 0, innerContent, replacement: undefined, listProcessor }; allMatches.push(xmlNode); } }); // Step 2: Catch ANY remaining XML/HTML tags that weren't processed by specific processors const genericXmlRegex = /<[^>]+>[\s\S]*?<\/[^>]+>|<[^>]+\/>/gis; let genericMatch; const processedRanges = allMatches.map(node => ({ start: node.start, end: node.end })); while ((genericMatch = genericXmlRegex.exec(content)) !== null) { const matchStart = genericMatch.index; const matchEnd = genericMatch.index + genericMatch[0].length; // Check if this match overlaps with any already processed range const hasOverlap = processedRanges.some(range => (matchStart < range.end && matchEnd > range.start)); if (!hasOverlap) { const tagName = ((_a = genericMatch[0].match(/<(\w+)/)) === null || _a === void 0 ? void 0 : _a[1]) || 'generic'; const xmlNode = { id: `${tagName}_${matchStart}_${Date.now()}_${Math.random()}`, tagName, start: matchStart, end: matchEnd, match: genericMatch[0], processor: () => null, groups: [], children: [], depth: 0, innerContent: genericMatch[0], replacement: undefined, listProcessor: undefined }; allMatches.push(xmlNode); } } // Sort by start position to maintain document order allMatches.sort((a, b) => a.start - b.start); // Build parent-child relationships const rootNodes = []; const nodeStack = []; for (const node of allMatches) { // Pop nodes from stack that don't contain this node while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1].end <= node.start) { nodeStack.pop(); } // Set depth based on stack size node.depth = nodeStack.length; // If there's a parent on the stack, add this as its child if (nodeStack.length > 0) { const parent = nodeStack[nodeStack.length - 1]; node.parent = parent; parent.children.push(node); } else { // This is a root node rootNodes.push(node); } // Push to stack for potential children if (!node.match.endsWith('/>') && node.match.includes('</')) { nodeStack.push(node); } } return rootNodes; } // Convert XML tree to HierarchyNode structure for cleaner processing static xmlTreeToHierarchy(nodes) { const hierarchyNodes = []; const processNode = (xmlNode) => { try { // Process children first const childHierarchyNodes = []; for (const child of xmlNode.children) { const childHierarchy = processNode(child); if (childHierarchy) { childHierarchyNodes.push(childHierarchy); } } // For list processors, handle them specially if (xmlNode.listProcessor && (xmlNode.tagName === 'ul' || xmlNode.tagName === 'ol')) { // Extract inner content and calculate offset const tagName = xmlNode.tagName.toLowerCase(); const openTagRegex = new RegExp(`^<${tagName}[^>]*>`, 'i'); const closeTagRegex = new RegExp(`</${tagName}>$`, 'i'); let innerContent = xmlNode.match; let contentStartOffset = 0; const openMatch = xmlNode.match.match(openTagRegex); const closeMatch = xmlNode.match.match(closeTagRegex); if (openMatch && closeMatch) { const openTag = openMatch[0]; const closeTag = closeMatch[0]; const startIndex = openTag.length; const endIndex = xmlNode.match.length - closeTag.length; innerContent = xmlNode.match.substring(startIndex, endIndex); contentStartOffset = xmlNode.start + startIndex; // Absolute position where list content starts } // Adjust child hierarchy node positions to be relative to list content const adjustedChildNodes = childHierarchyNodes.map(child => { var _a; return ({ ...child, metadata: { ...child.metadata, sourcePosition: ((_a = child.metadata) === null || _a === void 0 ? void 0 : _a.sourcePosition) !== undefined ? child.metadata.sourcePosition - contentStartOffset : undefined } }); }); // Build hierarchy structure for the list const listHierarchy = NotionAITool.buildListHierarchy(innerContent, xmlNode.tagName === 'ul' ? 'bulleted_list_item' : 'numbered_list_item', adjustedChildNodes); return listHierarchy; } // For regular nodes, create block and attach children const block = xmlNode.processor(...xmlNode.groups); if (!block) return null; const hierarchyNode = { block, children: childHierarchyNodes, metadata: { sourcePosition: xmlNode.start, xmlNodeId: xmlNode.id, tagName: xmlNode.tagName } }; return hierarchyNode; } catch (error) { console.warn(`Error processing XML node ${xmlNode.tagName}:`, error); return null; } }; // Process all root nodes for (const rootNode of nodes) { const hierarchyNode = processNode(rootNode); if (hierarchyNode) { if (hierarchyNode.block) { hierarchyNodes.push(hierarchyNode); } else if (hierarchyNode.children.length > 0) { // If it's a list container, add its children directly hierarchyNodes.push(...hierarchyNode.children); } } } return hierarchyNodes; } // Convert HierarchyNode structure to final Notion blocks static hierarchyToNotionBlocks(hierarchy) { return hierarchy.map(node => { const block = { ...node.block }; if (node.children.length > 0) { const blockData = block[block.type]; if (blockData && typeof blockData === 'object') { // Check if this block type can have children const childSupportingTypes = ['bulleted_list_item', 'numbered_list_item', 'toggle', 'quote', 'callout']; if (childSupportingTypes.includes(block.type)) { blockData.children = NotionAITool.hierarchyToNotionBlocks(node.children); } } } return block; }); } // Process XML tree using the new hierarchy system static processXMLTreeDepthFirst(nodes, blocks, placeholderCounter) { const replacements = new Map(); try { // Convert XML tree to hierarchy structure const hierarchy = NotionAITool.xmlTreeToHierarchy(nodes); // Convert hierarchy to final Notion blocks const finalBlocks = NotionAITool.hierarchyToNotionBlocks(hierarchy); // Add all blocks to the output blocks.push(...finalBlocks); // Mark all nodes as processed (empty replacement) const markProcessed = (nodeList) => { nodeList.forEach(node => { replacements.set(node.id, ''); markProcessed(node.children); }); }; markProcessed(nodes); } catch (error) { console.warn('Error in hierarchy processing, falling back to legacy processing:', error); // Fallback to simple processing if hierarchy fails nodes.forEach(node => { try { const block = node.processor(...node.groups); if (block) { blocks.push(block); } replacements.set(node.id, ''); } catch (nodeError) { console.warn(`Error processing fallback node ${node.tagName}:`, nodeError); replacements.set(node.id, ''); } }); } return replacements; } // Apply hierarchical replacements to content static applyHierarchicalReplacements(content, nodes, replacements) { let processedContent = content; // Sort nodes by start position in reverse order to avoid position shifts const allNodes = this.getAllNodesFromTree(nodes); allNodes.sort((a, b) => b.start - a.start); // Apply replacements from end to beginning for (const node of allNodes) { const replacement = replacements.get(node.id); if (replacement !== undefined) { processedContent = processedContent.substring(0, node.start) + replacement + processedContent.substring(node.end); } } return processedContent; } // Helper function to get all nodes from tree (flattened) static getAllNodesFromTree(nodes) { const allNodes = []; const collectNodes = (nodeList) => { for (const node of nodeList) { allNodes.push(node); collectNodes(node.children); } }; collectNodes(nodes); return allNodes; } // New hierarchical XML-like tag processing function static processXmlTags(content, blocks) { let processedContent = content; // First, decode HTML entities to proper XML tags processedContent = NotionAITool.decodeHtmlEntities(processedContent); // Use simple sequential placeholder format: __BLOCK_N__ let placeholderCounter = 1; // Start from 1 for cleaner numbering // Debug mode for development const DEBUG_ORDERING = process.env.NODE_ENV === 'development'; // Define all tag processors const tagProcessors = [ // Callouts: <callout type="info">content</callout> { regex: /<callout\s*(?:type="([^"]*)")?\s*>(.*?)<\/callout>/gis, blockCreator: (type = 'info', content) => { const emoji = NotionAITool.getCalloutEmoji(type.toLowerCase()); const color = NotionAITool.getCalloutColor(type.toLowerCase()); return { type: 'callout', callout: { rich_text: NotionAITool.parseBasicMarkdown(content.trim()), icon: { type: 'emoji', emoji }, color: color, }, }; } }, // Code blocks: <code language="javascript">content</code> { regex: /<code\s*(?:language="([^"]*)")?\s*>(.*?)<\/code>/gis, blockCreator: (language = 'plain_text', content) => { return { type: 'code', code: { rich_text: [(0, NotionUtils_1.createRichText)(content.trim())], language: language === 'plain text' ? 'plain_text' : language, }, }; } }, // Images: <image src="url" alt="description">caption</image> { regex: /<image\s+src="([^"]*)"(?:\s+alt="([^"]*)")?\s*>(.*?)<\/image>/gis, blockCreator: (src, alt = '', caption = '') => { const captionText = caption.trim() || alt; return { type: 'image', image: { type: 'external',