import { processNotionIcon } from "./icon"; import { env } from "cloudflare:workers"; import { getNavigation } from "./navigation"; const NOTION_API_BASE = "https://api.notion.com/v1"; const NOTION_VERSION = "2025-09-03"; // or your preferred version const iconColorSubstitutions = [ ["#D44C47", "#FF453A"], ["#55534E", "#FFFFFF"], ["#448361", "#32D74B"], ["#337ea9", "#0A84FF"], ["#9065B0", "#BF5AF2"], ["#CB912F", "#FFD60A"], ["#C14C8A", "#FF375F"], ["#d9730d", "#FF9F0A"], ]; async function notionFetch(endpoint, options = {}) { const res = await fetch(`${NOTION_API_BASE}${endpoint}`, { headers: { Authorization: `Bearer ${env.NOTION_AUTH}`, "Notion-Version": NOTION_VERSION, "Content-Type": "application/json", }, ...options, }); if (!res.ok) { const text = await res.text(); throw new Error( `Notion API error: ${res.status} ${res.statusText} - ${text}` ); } return res.json(); } export async function addToNotionDataSource( properties, dataSourceId, icon = null ) { console.log("Adding to Notion data source..."); console.log("Properties:", properties); const pageParams = {}; const notionProperties = {}; if (icon != null) { pageParams["icon"] = { type: "external", external: { url: icon, }, }; } for (const [key, value] of Object.entries(properties)) { if (key === "Email" && value != "") { notionProperties[key] = { email: value }; } else if (key === "Name" && value != "") { notionProperties[key] = { title: [{ text: { content: value } }] }; } else if (key === "Features") { notionProperties[key] = { multi_select: value.map((feature) => ({ name: feature, })), }; } else if (key === "Message" && value != "") { pageParams["children"] = [ { object: "block", type: "paragraph", paragraph: { rich_text: [ { type: "text", text: { content: value, }, }, ], }, }, ]; } else if (typeof value === "string" && value != "") { notionProperties[key] = { rich_text: [{ text: { content: value } }], }; } else if (typeof value === "number" && value != undefined) { notionProperties[key] = { number: value }; } else if (value && typeof value === "object" && !Array.isArray(value)) { notionProperties[key] = value; } } try { const response = await notionFetch("/pages", { method: "POST", body: JSON.stringify({ parent: { data_source_id: dataSourceId }, ...pageParams, properties: notionProperties, }), }); console.log("Added to Notion DB!"); return response; } catch (error) { console.error("Failed to add to Notion:", error); throw new Error("Failed to add to Notion"); } } export async function queryNotionDataSource(dataSourceId, queryParams = {}) { console.log(`Fetching data source: ${dataSourceId} from Notion API...`); try { const body = JSON.stringify(queryParams); const result = await notionFetch(`/data_sources/${dataSourceId}/query`, { method: "POST", body, }); console.log("Fetched data source:", dataSourceId); return result.results; } catch (error) { console.error(`Error fetching Notion data source ${dataSourceId}:`, error); throw error; } } export async function getNotionDatabase(databaseId) { console.log(`Fetching database: ${databaseId} from Notion API...`); try { const result = await notionFetch(`/databases/${databaseId}`, { method: "GET", }); return result; } catch (error) { console.error(`Error fetching Notion database ${databaseId}:`, error); throw error; } } export async function getNotionDataSource(dataSourceId) { console.log(`Fetching data source: ${dataSourceId} from Notion API...`); try { const result = await notionFetch(`/data_sources/${dataSourceId}`, { method: "GET", }); return result; } catch (error) { console.error(`Error fetching Notion data source ${dataSourceId}:`, error); throw error; } } export async function getNotionPage(pageId) { console.log(`Fetching page: ${pageId} from Notion API...`); try { const result = await notionFetch(`/pages/${pageId}`, { method: "GET", }); return result; } catch (error) { console.error(`Error fetching Notion page ${pageId}:`, error); throw error; } } export async function updateNotionPage( pageId, properties, archive = false, trash = false ) { console.log(`Updating page: ${pageId} in Notion API...`); const notionProperties = {}; for (const [key, value] of Object.entries(properties)) { if (key === "Email" && value != "") { notionProperties[key] = { email: value }; } else if (key === "Name") { notionProperties[key] = { title: [{ text: { content: value } }] }; } else if (key === "Features") { notionProperties[key] = { multi_select: value.map((feature) => ({ name: feature, })), }; } else if (typeof value === "string" && value != "") { notionProperties[key] = { rich_text: [{ text: { content: value } }], }; } else if (typeof value === "number" && value != undefined) { notionProperties[key] = { number: value }; } else if (value && typeof value === "object" && !Array.isArray(value)) { notionProperties[key] = value; } } try { const result = await notionFetch(`/pages/${pageId}`, { method: "PATCH", body: JSON.stringify({ properties: notionProperties, archived: archive, in_trash: trash, }), }); return result; } catch (error) { console.error(`Error updating Notion page ${pageId}:`, error); throw error; } } export async function getNotionBlocks(pageId, queryParams = {}) { console.log(`Fetching blocks for page: ${pageId} from Notion API...`); try { const result = await notionFetch( `/blocks/${pageId}/children?${new URLSearchParams(queryParams)}` ); return result.results; } catch (error) { console.error(`Error fetching Notion page ${pageId}:`, error); throw error; } } export async function getNotionSVGIcon(url) { try { console.log(`Fetching SVG icon from: ${url}`); const response = await fetch(url); if (!response.ok) throw new Error(`Failed to fetch SVG: ${response.statusText}`); let svgText = await response.text(); iconColorSubstitutions.forEach(([searchColor, replaceColor]) => { svgText = svgText.replaceAll(searchColor, replaceColor); }); return svgText; } catch (error) { console.error(error); return null; } } // Transform Notion blocks to content format export async function transformContent(env, pageId) { try { console.log(`Transforming content for page: ${pageId}`); const navigationItems = await getNavigation(env); // Fetch blocks from Notion const blocks = await getNotionBlocks(pageId); const content = []; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const blockType = block.type; // Group consecutive list items if ( blockType === "bulleted_list_item" || blockType === "numbered_list_item" ) { const isNumbered = blockType === "numbered_list_item"; const children = []; // Collect consecutive list items of the same type while ( i < blocks.length && blocks[i].type === (isNumbered ? "numbered_list_item" : "bulleted_list_item") ) { children.push({ type: "listItem", text: await extractRichText( isNumbered ? blocks[i].numbered_list_item.rich_text : blocks[i].bulleted_list_item.rich_text, navigationItems ), }); i++; } // Step back one index because the for loop will increment it i--; content.push({ type: "list", ordered: isNumbered, children, }); continue; } switch (blockType) { case "heading_1": content.push({ type: "title1", text: await extractRichText( block.heading_1.rich_text, navigationItems ), }); break; case "heading_2": content.push({ type: "title2", text: await extractRichText( block.heading_2.rich_text, navigationItems ), }); break; case "heading_3": content.push({ type: "title3", text: await extractRichText( block.heading_3.rich_text, navigationItems ), }); break; case "paragraph": const paragraphText = await extractRichText( block.paragraph.rich_text, navigationItems ); if (paragraphText.trim()) { content.push({ type: "paragraph", text: paragraphText, }); } break; case "divider": content.push({ type: "divider", }); break; case "image": content.push({ type: "image", url: block.image.external?.url || block.image.file?.url, caption: block.image.caption ? await extractRichText(block.image.caption, navigationItems) : null, }); break; case "quote": content.push({ type: "quote", text: await extractRichText(block.quote.rich_text, navigationItems), }); break; case "code": content.push({ type: "code", text: await extractRichText(block.code.rich_text, navigationItems), language: block.code.language, }); break; case "bookmark": content.push({ type: "button", text: await extractRichText( block.bookmark.caption, navigationItems ), url: block.bookmark.url, }); break; case "callout": content.push({ type: "callout", text: await extractRichText( block.callout.rich_text, navigationItems ), icon: block.callout.icon?.emoji || null, }); break; case "link_to_page": const linkedNavigationItem = getItemByNotionId( navigationItems, block.link_to_page.page_id ); content.push({ type: "button", url: `/${linkedNavigationItem.slug}`, text: linkedNavigationItem.name, icon: linkedNavigationItem.icon, }); break; default: // For unsupported block types, try to extract text if available console.log(`Unsupported block type: ${blockType}`); console.log(JSON.stringify(block)); break; } } return content; } catch (error) { console.error(`Error transforming page content for ${pageId}:`, error); // Return fallback content if transformation fails return [ { type: "paragraph", text: "Content could not be loaded at this time.", }, ]; } } function processLink(text, href, navigationItems) { const icon = ` `; var pageIdNoDashes = null; if (href.startsWith("/")) { pageIdNoDashes = href.split("/").pop().replaceAll("#", ""); } if (href.includes("notion.so/")) { if (href.includes("-")) { pageIdNoDashes = href.split("-").pop().replaceAll("#", ""); } else { pageIdNoDashes = href.split("/").pop().replaceAll("#", ""); } } const textSpan = `${text}`; const iconSpan = `${icon}`; if (pageIdNoDashes != null) { const linkedNavigationItem = getItemByNotionId( navigationItems, pageIdNoDashes ); return `${textSpan}`; } else { return `${textSpan}${iconSpan}`; } } // Helper function to extract rich text content export async function extractRichText(richTextArray, navigationItems) { if (!richTextArray || !Array.isArray(richTextArray)) { return ""; } const textPieces = await Promise.all( richTextArray.map(async (textObj) => { // Handle custom emoji mentions if ( textObj.type === "mention" && textObj.mention?.type === "custom_emoji" && textObj.mention.custom_emoji?.url ) { const emoji = await processNotionIcon( textObj.mention.custom_emoji, "th-inline-emoji" ); return emoji; } let text = textObj.plain_text || ""; // Handle formatting if (textObj.annotations) { const annotations = textObj.annotations; if (annotations.bold) { text = `${text}`; } if (annotations.italic) { text = `${text}`; } if (annotations.underline) { text = `${text}`; } if (annotations.strikethrough) { text = `${text}`; } if (annotations.code) { text = `${text}`; } } // Handle links if (textObj.href) { text = processLink(text, textObj.href, navigationItems); } return text; }) ); return textPieces.join(""); } // Utility: safely extract plain text from a Notion title or rich_text field export function getPlainText(field) { if (!field) return ""; if (field.type === "title" || field.type === "rich_text") { return field[field.type].map((t) => t.plain_text).join("") || ""; } return ""; } // Utility: convert a string to camelCase export function toCamelCase(str = "") { return str .replace(/[^a-zA-Z0-9 ]/g, " ") // remove non-alphanumeric except spaces .trim() .split(/\s+/) // split on spaces .map((word, index) => { if (index === 0) return word.toLowerCase(); return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .join(""); } // Moved from properties.js: buildNewCache, now buildListCache export function buildListCache( cachedList, { added = [], updated = [], deleted = [] } ) { // Remove deleted and items that will be updated const filtered = cachedList.filter( (p) => !deleted.some((d) => d.notionId === p.notionId) && !updated.some((u) => u.notionId === p.notionId) ); return [...filtered, ...updated, ...added]; } export function getItemByNotionId(items, notionId) { const itemWithDashes = items.find((item) => item.notionId === notionId); if (itemWithDashes) { return itemWithDashes; } const itemWithoutDashes = items.find( (item) => item.notionId.replaceAll("-", "") === notionId.replaceAll("-", "") ); if (itemWithoutDashes) { return itemWithoutDashes; } return null; }