ai-knowledge-hub
Version:
MCP server that provides unified access to organizational knowledge across multiple platforms (local docs, Guru, Notion)
375 lines • 11.2 kB
JavaScript
/**
* Notion block builder utilities
* Creates properly formatted Notion blocks from content
*/
/**
* Create rich text array from plain text with optional formatting
*/
export function createRichText(content, formatting) {
if (!content) {
return [];
}
const richText = {
type: 'text',
text: {
content,
link: formatting?.link ?? null,
},
annotations: {
bold: formatting?.bold ?? false,
italic: formatting?.italic ?? false,
strikethrough: formatting?.strikethrough ?? false,
underline: false,
code: formatting?.code ?? false,
color: formatting?.color ?? 'default',
},
plain_text: content,
href: formatting?.link?.url ?? null,
};
return [richText];
}
/**
* Create rich text from markdown nodes with formatting
*/
export function createRichTextFromNodes(nodes) {
const richTextArray = [];
for (const node of nodes) {
if (node.type === 'text' && node.content !== null && node.content !== undefined && node.content.length > 0) {
const formatting = {
bold: node.bold,
italic: node.italic,
strikethrough: node.strikethrough,
code: node.code,
link: node.link,
};
richTextArray.push(...createRichText(node.content, formatting));
}
}
return richTextArray.length > 0 ? richTextArray : createRichText('');
}
/**
* Build a paragraph block
*/
export function buildParagraphBlock(content, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: richText,
color: formatting?.color ?? 'default',
},
};
}
/**
* Build a heading block (H1, H2, or H3)
*/
export function buildHeadingBlock(content, level, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
const headingType = `heading_${level}`;
return {
object: 'block',
type: headingType,
[headingType]: {
rich_text: richText,
color: formatting?.color ?? 'default',
is_toggleable: false,
},
};
}
/**
* Build a bulleted list item block
*/
export function buildBulletedListItemBlock(content, children, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'bulleted_list_item',
bulleted_list_item: {
rich_text: richText,
color: formatting?.color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a numbered list item block
*/
export function buildNumberedListItemBlock(content, children, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'numbered_list_item',
numbered_list_item: {
rich_text: richText,
color: formatting?.color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a to-do list item block (for task lists)
*/
export function buildToDoBlock(content, checked = false, children, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'to_do',
to_do: {
rich_text: richText,
checked,
color: formatting?.color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a code block
*/
export function buildCodeBlock(code, language, caption) {
return {
object: 'block',
type: 'code',
code: {
caption: caption !== null && caption !== undefined && caption.length > 0 ? createRichText(caption) : [],
rich_text: createRichText(code),
language: language ?? 'plain text',
},
};
}
/**
* Build a quote block
*/
export function buildQuoteBlock(content, children, formatting) {
const richText = typeof content === 'string'
? createRichText(content, formatting)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'quote',
quote: {
rich_text: richText,
color: formatting?.color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a divider block (horizontal rule)
*/
export function buildDividerBlock() {
return {
object: 'block',
type: 'divider',
divider: {},
};
}
/**
* Build a callout block (used for special formatting)
*/
export function buildCalloutBlock(content, icon, color, children) {
const richText = typeof content === 'string'
? createRichText(content)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'callout',
callout: {
rich_text: richText,
icon: icon !== null && icon !== undefined && icon.length > 0 ? {
type: 'emoji',
emoji: icon,
} : {
type: 'emoji',
emoji: '💡',
},
color: color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a toggle block (collapsible content)
*/
export function buildToggleBlock(content, children, formatting) {
const richText = typeof content === 'string'
? createRichText(content)
: createRichTextFromNodes(content);
return {
object: 'block',
type: 'toggle',
toggle: {
rich_text: richText,
color: formatting?.color ?? 'default',
children: children ?? [],
},
};
}
/**
* Build a table block
* Note: Notion tables require table_row blocks as children within the table block
*/
export function buildTableBlock(rows, hasColumnHeader = true, hasRowHeader = false) {
if (rows.length === 0) {
return [];
}
const tableWidth = rows[0].length;
// Create table row blocks as children
const tableRows = rows.map(row => {
// Each cell should be an array of rich text objects
const cells = row.map(cellContent => createRichText(cellContent));
return {
object: 'block',
type: 'table_row',
table_row: {
cells,
},
};
});
// Create the table block with rows as children
const tableBlock = {
object: 'block',
type: 'table',
table: {
table_width: tableWidth,
has_column_header: hasColumnHeader,
has_row_header: hasRowHeader,
children: tableRows,
},
};
// Return only the table block (rows are nested as children)
return [tableBlock];
}
/**
* Build a table block from MarkdownNode arrays (supports rich text in cells)
* Note: Notion tables require table_row blocks as children within the table block
*/
export function buildTableBlockFromNodes(rows, hasColumnHeader = true, hasRowHeader = false) {
if (rows.length === 0) {
return [];
}
const tableWidth = rows[0].length;
// Create table row blocks as children
const tableRows = rows.map(row => {
// Each cell should be an array of rich text objects
const cells = row.map(cellNode => {
// If cell has children (rich text), use those; otherwise use content
if (cellNode.children && cellNode.children.length > 0) {
return createRichTextFromNodes(cellNode.children);
}
else {
return createRichText(cellNode.content ?? '');
}
});
return {
object: 'block',
type: 'table_row',
table_row: {
cells,
},
};
});
// Create the table block with rows as children
const tableBlock = {
object: 'block',
type: 'table',
table: {
table_width: tableWidth,
has_column_header: hasColumnHeader,
has_row_header: hasRowHeader,
children: tableRows,
},
};
// Return only the table block (rows are nested as children)
return [tableBlock];
}
/**
* Build an image block
*/
export function buildImageBlock(url, caption) {
const isExternal = url.startsWith('http://') || url.startsWith('https://');
return {
object: 'block',
type: 'image',
image: {
type: isExternal ? 'external' : 'file',
[isExternal ? 'external' : 'file']: {
url: url,
},
caption: caption !== null && caption !== undefined ? createRichText(caption) : [],
},
};
}
/**
* Build an embed block
*/
export function buildEmbedBlock(url, caption) {
return {
object: 'block',
type: 'embed',
embed: {
url,
caption: caption !== null && caption !== undefined && caption.length > 0 ? createRichText(caption) : [],
},
};
}
/**
* Build a bookmark block
*/
export function buildBookmarkBlock(url, caption) {
return {
object: 'block',
type: 'bookmark',
bookmark: {
url,
caption: caption !== null && caption !== undefined && caption.length > 0 ? createRichText(caption) : [],
},
};
}
/**
* Utility function to determine appropriate list block type
*/
export function buildListItemBlock(content, listType, checked, children, formatting) {
switch (listType) {
case 'bulleted':
return buildBulletedListItemBlock(content, children, formatting);
case 'numbered':
return buildNumberedListItemBlock(content, children, formatting);
case 'todo':
return buildToDoBlock(content, checked ?? false, children, formatting);
default:
return buildBulletedListItemBlock(content, children, formatting);
}
}
/**
* Utility function to normalize heading levels for Notion (max H3)
*/
export function normalizeHeadingLevel(level) {
if (level <= 1) {
return 1;
}
if (level === 2) {
return 2;
}
return 3; // H4, H5, H6 all become H3 in Notion
}
/**
* Create a fallback block for unsupported content
*/
export function buildFallbackBlock(content, originalType) {
const fallbackContent = originalType !== null && originalType !== undefined && originalType.length > 0
? `[Unsupported ${originalType}]: ${content}`
: content;
return buildParagraphBlock(fallbackContent, { color: 'gray' });
}
//# sourceMappingURL=notion-blocks.js.map