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
JavaScript
"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
/<[^&]+>/.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.endsWith(')')) {
// Image: 
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',