From c06cf88a28287b0246451ab857087e62f0ffee9b Mon Sep 17 00:00:00 2001 From: PCloud Date: Mon, 24 Jun 2024 08:28:19 +0200 Subject: [PATCH] refactor: merge Notion-To-Markdown with Notion-Hugo (#239) --- package-lock.json | 34 ++- package.json | 3 +- src/markdown/md.ts | 267 ++++++++++++++++++++ src/markdown/notion-to-md.ts | 458 +++++++++++++++++++++++++++++++++++ src/markdown/notion.ts | 77 ++++++ src/markdown/types.ts | 23 ++ src/render.ts | 4 +- 7 files changed, 843 insertions(+), 23 deletions(-) create mode 100644 src/markdown/md.ts create mode 100644 src/markdown/notion-to-md.ts create mode 100644 src/markdown/notion.ts create mode 100644 src/markdown/types.ts diff --git a/package-lock.json b/package-lock.json index 1715ad4..d20511d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,15 @@ "license": "GPL-3.0-only", "dependencies": { "@notionhq/client": "^2.2.15", - "@pclouddev/notion-to-markdown": "^2.7.17", "dotenv": "^16.4.5", "front-matter": "^4.0.2", "fs-extra": "^11.2.0", + "markdown-table": "^2.0.0", "yaml": "^2.4.2" }, "devDependencies": { "@types/fs-extra": "^11.0.4", + "@types/markdown-table": "^2.0.0", "@types/node": "^20.13.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", @@ -77,17 +78,6 @@ "node": ">=12" } }, - "node_modules/@pclouddev/notion-to-markdown": { - "version": "2.7.17", - "resolved": "https://registry.npmjs.org/@pclouddev/notion-to-markdown/-/notion-to-markdown-2.7.17.tgz", - "integrity": "sha512-9GnREgda0xG4kyF6HvQCnTEtPlIfL6XulLPTK49JK6P1T10j9Ssm31CUJ5JATKLNINDgGHFO1JlFrpHpi6c/6w==", - "dependencies": { - "markdown-table": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -131,6 +121,12 @@ "@types/node": "*" } }, + "node_modules/@types/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-fVZN/DRjZvjuk+lo7ovlI/ZycS51gpYU5vw5EcFeqkcX6lucQ+UWgEOH2O4KJHkSck4DHAY7D7CkVLD0wzc5qw==", + "dev": true + }, "node_modules/@types/node": { "version": "20.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", @@ -547,14 +543,6 @@ "node-fetch": "^2.6.1" } }, - "@pclouddev/notion-to-markdown": { - "version": "2.7.17", - "resolved": "https://registry.npmjs.org/@pclouddev/notion-to-markdown/-/notion-to-markdown-2.7.17.tgz", - "integrity": "sha512-9GnREgda0xG4kyF6HvQCnTEtPlIfL6XulLPTK49JK6P1T10j9Ssm31CUJ5JATKLNINDgGHFO1JlFrpHpi6c/6w==", - "requires": { - "markdown-table": "^2.0.0" - } - }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -598,6 +586,12 @@ "@types/node": "*" } }, + "@types/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-fVZN/DRjZvjuk+lo7ovlI/ZycS51gpYU5vw5EcFeqkcX6lucQ+UWgEOH2O4KJHkSck4DHAY7D7CkVLD0wzc5qw==", + "dev": true + }, "@types/node": { "version": "20.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", diff --git a/package.json b/package.json index babeaee..180c917 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,15 @@ "author": "HEIGE-PCloud", "dependencies": { "@notionhq/client": "^2.2.15", - "@pclouddev/notion-to-markdown": "^2.7.17", "dotenv": "^16.4.5", "front-matter": "^4.0.2", "fs-extra": "^11.2.0", + "markdown-table": "^2.0.0", "yaml": "^2.4.2" }, "devDependencies": { "@types/fs-extra": "^11.0.4", + "@types/markdown-table": "^2.0.0", "@types/node": "^20.13.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", diff --git a/src/markdown/md.ts b/src/markdown/md.ts new file mode 100644 index 0000000..a371090 --- /dev/null +++ b/src/markdown/md.ts @@ -0,0 +1,267 @@ +import markdownTable from "markdown-table"; +import { + AudioBlockObjectResponse, + EquationRichTextItemResponse, + MentionRichTextItemResponse, + PdfBlockObjectResponse, + RichTextItemResponse, + TextRichTextItemResponse, + VideoBlockObjectResponse, +} from "@notionhq/client/build/src/api-endpoints"; +import { CalloutIcon } from "./types"; +import { getPageRelrefFromId } from "./notion"; +import { Client, isFullUser } from "@notionhq/client"; +export const inlineCode = (text: string) => { + return `\`${text}\``; +}; + +export const bold = (text: string) => { + return `**${text}**`; +}; + +export const italic = (text: string) => { + return `_${text}_`; +}; + +export const strikethrough = (text: string) => { + return `~~${text}~~`; +}; + +export const underline = (text: string) => { + return `${text}`; +}; + +export const link = (text: string, href: string) => { + return `[${text}](${href})`; +}; + +export const codeBlock = (text: string, language?: string) => { + if (language === "plain text") language = "text"; + + return `\`\`\`${language} +${text} +\`\`\``; +}; + +export const heading1 = (text: string) => { + return `# ${text}`; +}; + +export const heading2 = (text: string) => { + return `## ${text}`; +}; + +export const heading3 = (text: string) => { + return `### ${text}`; +}; + +export const quote = (text: string) => { + // the replace is done to handle multiple lines + return `> ${text.replace(/\n/g, " \n> ")}`; +}; + +export const callout = (text: string, icon?: CalloutIcon) => { + let emoji: string | undefined; + if (icon?.type === "emoji") { + emoji = icon.emoji; + } + + // the replace is done to handle multiple lines + return `> ${emoji ? emoji + " " : ""}${text.replace(/\n/g, " \n> ")}`; +}; + +export const bullet = (text: string, count?: number) => { + let renderText = text.trim(); + return count ? `${count}. ${renderText}` : `- ${renderText}`; +}; + +export const todo = (text: string, checked: boolean) => { + return checked ? `- [x] ${text}` : `- [ ] ${text}`; +}; + +export const image = (alt: string, href: string) => { + return `![${alt}](${href})`; +}; + +export const addTabSpace = (text: string, n = 0) => { + const tab = " "; + for (let i = 0; i < n; i++) { + if (text.includes("\n")) { + const multiLineText = text.split(/(?<=\n)/).join(tab); + text = tab + multiLineText; + } else text = tab + text; + } + return text; +}; + +export const divider = () => { + return "---"; +}; + +export const toggle = (summary?: string, children?: string) => { + if (!summary) return children || ""; + return `
+ ${summary} + +${children || ""} + +
`; +}; + +export const table = (cells: string[][]) => { + return markdownTable(cells); +}; + +export const plainText = (textArray: RichTextItemResponse[]) => { + return textArray.map((text) => text.plain_text).join(""); +}; + +/** + * Block equation + * Format: \[ expression \] + * @param expression + * @returns + */ +export const equation = (expression: string) => { + return `\\[${expression}\\]`; +}; + +function textRichText(text: TextRichTextItemResponse): string { + const annotations = text.annotations; + let content = text.text.content; + if (annotations.bold) { + content = bold(content); + } + if (annotations.code) { + content = inlineCode(content); + } + if (annotations.italic) { + content = italic(content); + } + if (annotations.strikethrough) { + content = strikethrough(content); + } + if (annotations.underline) { + content = underline(content); + } + if (text.href) { + content = link(content, text.href); + } + return content; +} + +/** + * Inline equation + * Format: \( expression \) + * @param text + * @returns + */ +function equationRichText(text: EquationRichTextItemResponse): string { + return `\\(${text.equation.expression}\\)`; +} + +async function mentionRichText( + text: MentionRichTextItemResponse, + notion: Client +): Promise { + const mention = text.mention; + switch (mention.type) { + case "page": { + const pageId = mention.page.id; + const { title, relref } = await getPageRelrefFromId(pageId, notion); + return link(title, relref); + } + case "user": { + const userId = mention.user.id; + try { + const user = await notion.users.retrieve({ user_id: userId }); + if (user.name) { + return `@${user.name}`; + } + } catch (error) { + console.warn(`Failed to retrieve user with id ${userId}`); + } + return ""; + } + case "date": { + const date = mention.date; + const dateEnd = date.end ? ` -> ${date.end}` : ""; + const timeZone = date.time_zone ? ` (${date.time_zone})` : ""; + return `@${date.start}${dateEnd}${timeZone}`; + } + case "link_preview": { + const linkPreview = mention.link_preview; + return link(linkPreview.url, linkPreview.url); + } + case "template_mention": { + // https://developers.notion.com/reference/rich-text#template-mention-type-object + // Hide the template button + return ""; + } + case "database": { + console.warn("[Warn] Database mention is not supported"); + return ""; + } + } +} + +export async function richText( + textArray: RichTextItemResponse[], + notion: Client +) { + return ( + await Promise.all( + textArray.map(async (text) => { + if (text.type === "text") { + return textRichText(text); + } else if (text.type === "equation") { + return equationRichText(text); + } else if (text.type === "mention") { + return await mentionRichText(text, notion); + } + }) + ) + ).join(""); +} + +export const video = (block: VideoBlockObjectResponse) => { + const videoBlock = block.video; + if (videoBlock.type === "file") { + return htmlVideo(videoBlock.file.url); + } + const url = videoBlock.external.url; + if (url.startsWith("https://www.youtube.com/")) { + /* + YouTube video links that include embed or watch. + E.g. https://www.youtube.com/watch?v=[id], https://www.youtube.com/embed/[id] + */ + // get last 11 characters of the url as the video id + const videoId = url.slice(-11); + return ``; + } + return htmlVideo(url); +}; + +function htmlVideo(url: string) { + return ``; +} + +export const pdf = (block: PdfBlockObjectResponse) => { + const pdfBlock = block.pdf; + const url = + pdfBlock.type === "file" ? pdfBlock.file.url : pdfBlock.external.url; + return ``; +}; + +export const audio = (block: AudioBlockObjectResponse) => { + const audioBlock = block.audio; + const url = + audioBlock.type === "file" ? audioBlock.file.url : audioBlock.external.url; + return ``; +}; diff --git a/src/markdown/notion-to-md.ts b/src/markdown/notion-to-md.ts new file mode 100644 index 0000000..9d4b3bc --- /dev/null +++ b/src/markdown/notion-to-md.ts @@ -0,0 +1,458 @@ +import { Client, isFullBlock } from "@notionhq/client"; +import { + GetBlockResponse, + RichTextItemResponse, +} from "@notionhq/client/build/src/api-endpoints"; +import { CustomTransformer, MdBlock, NotionToMarkdownOptions } from "./types"; +import * as md from "./md"; +import { getBlockChildren, getPageRelrefFromId } from "./notion"; +import { plainText } from "./md"; + +/** + * Converts a Notion page to Markdown. + */ +export class NotionToMarkdown { + private notionClient: Client; + private customTransformers: Record; + private richText: (textArray: RichTextItemResponse[]) => Promise; + private unsupportedTransformer: (type: string) => string = () => ""; + constructor(options: NotionToMarkdownOptions) { + this.notionClient = options.notionClient; + this.customTransformers = {}; + this.richText = (textArray: RichTextItemResponse[]) => + md.richText(textArray, this.notionClient); + } + setCustomTransformer( + type: string, + transformer: CustomTransformer + ): NotionToMarkdown { + this.customTransformers[type] = transformer; + + return this; + } + setCustomRichTextTransformer( + transformer: ( + textArray: RichTextItemResponse[], + notion: Client + ) => Promise + ) { + this.richText = (textArray: RichTextItemResponse[]) => + transformer(textArray, this.notionClient); + return this; + } + setUnsupportedTransformer(transformer: (type: string) => string) { + this.unsupportedTransformer = transformer; + return this; + } + /** + * Converts Markdown Blocks to string + * @param {MdBlock[]} mdBlocks - Array of markdown blocks + * @param {number} nestingLevel - Defines max depth of nesting + * @returns {string} - Returns markdown string + */ + toMarkdownString(mdBlocks: MdBlock[] = [], nestingLevel: number = 0): string { + let mdString = ""; + mdBlocks.forEach((mdBlocks) => { + // process parent blocks + if (mdBlocks.parent) { + if ( + mdBlocks.type !== "to_do" && + mdBlocks.type !== "bulleted_list_item" && + mdBlocks.type !== "numbered_list_item" + ) { + // add extra line breaks non list blocks + mdString += `\n${md.addTabSpace(mdBlocks.parent, nestingLevel)}\n\n`; + } else { + mdString += `${md.addTabSpace(mdBlocks.parent, nestingLevel)}\n`; + } + } + + // process child blocks + if (mdBlocks.children && mdBlocks.children.length > 0) { + if (mdBlocks.type === "synced_block") { + mdString += this.toMarkdownString(mdBlocks.children, nestingLevel); + } else { + mdString += this.toMarkdownString( + mdBlocks.children, + nestingLevel + 1 + ); + } + } + }); + return mdString; + } + + /** + * Retrieves Notion Blocks based on ID and converts them to Markdown Blocks + * @param {string} id - notion page id (not database id) + * @param {number} totalPage - Retrieve block children request number, page_size Maximum = totalPage * 100 (Default=null) + * @returns {Promise} - List of markdown blocks + */ + async pageToMarkdown( + id: string, + totalPage: number | null = null + ): Promise { + if (!this.notionClient) { + throw new Error( + "notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md" + ); + } + + const blocks = await getBlockChildren(this.notionClient, id, totalPage); + + const parsedData = await this.blocksToMarkdown(blocks); + return parsedData; + } + + /** + * Converts list of Notion Blocks to Markdown Blocks + * @param {ListBlockChildrenResponseResults | undefined} blocks - List of notion blocks + * @param {number} totalPage - Retrieve block children request number, page_size Maximum = totalPage * 100 + * @param {MdBlock[]} mdBlocks - Defines max depth of nesting + * @returns {Promise} - Array of markdown blocks with their children + */ + async blocksToMarkdown( + blocks?: GetBlockResponse[], + totalPage: number | null = null, + mdBlocks: MdBlock[] = [] + ): Promise { + if (!this.notionClient) { + throw new Error( + "notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md" + ); + } + + if (!blocks) return mdBlocks; + + for (const block of blocks) { + if (!isFullBlock(block)) continue; + let expiry_time: string | undefined = undefined; + if (block.type === "pdf" && block.pdf.type === "file") { + expiry_time = block.pdf.file.expiry_time; + } + if (block.type === "image" && block.image.type === "file") { + expiry_time = block.image.file.expiry_time; + } + if (block.type === "video" && block.video.type === "file") { + expiry_time = block.video.file.expiry_time; + } + if (block.type === "file" && block.file.type === "file") { + expiry_time = block.file.file.expiry_time; + } + + if ( + block.has_children && + block.type !== "column_list" && + block.type !== "toggle" && + block.type !== "callout" && + block.type !== "quote" + ) { + let child_blocks = await getBlockChildren( + this.notionClient, + block.id, + totalPage + ); + + mdBlocks.push({ + type: block.type, + parent: await this.blockToMarkdown(block), + children: [], + expiry_time, + }); + + await this.blocksToMarkdown( + child_blocks, + totalPage, + mdBlocks[mdBlocks.length - 1].children + ); + continue; + } + let tmp = await this.blockToMarkdown(block); + mdBlocks.push({ + type: block.type, + parent: tmp, + children: [], + expiry_time, + }); + } + return mdBlocks; + } + + /** + * Converts a Notion Block to a Markdown Block + * @param block - single notion block + * @returns corresponding markdown string of the passed block + */ + async blockToMarkdown(block: GetBlockResponse): Promise { + if (typeof block !== "object" || !("type" in block)) return ""; + + const { type } = block; + if (type in this.customTransformers && !!this.customTransformers[type]) + return await this.customTransformers[type](block); + switch (type) { + case "image": { + const image = block.image; + const url = + image.type === "external" ? image.external.url : image.file.url; + return md.image(plainText(image.caption), url); + } + case "divider": { + return md.divider(); + } + + case "equation": { + return md.equation(block.equation.expression); + } + + case "video": + return md.video(block); + case "pdf": + return md.pdf(block); + case "file": { + const file = block.file; + const link = + file.type === "external" ? file.external.url : file.file.url; + return md.link(file.name, link); + } + case "bookmark": { + const bookmark = block.bookmark; + const caption = + bookmark.caption.length > 0 + ? await this.richText(bookmark.caption) + : bookmark.url; + return md.link(caption, bookmark.url); + } + + case "link_to_page": { + const linkToPage = block.link_to_page; + if (linkToPage.type === "page_id") { + const { title, relref } = await getPageRelrefFromId( + linkToPage.page_id, + this.notionClient + ); + return md.link(title, relref); + } else if (linkToPage.type === "comment_id") { + console.warn("Unsupported link_to_page type: comment_id"); + return ""; + } else if (linkToPage.type === "database_id") { + console.warn("Unsupported link_to_page type: database_id"); + return ""; + } + break; + } + case "embed": { + const embed = block.embed; + const title = embed.caption.length > 0 ? plainText(embed.caption) : embed.url; + return md.link(title, embed.url); + } + case "link_preview": { + const linkPreview = block.link_preview; + return md.link(linkPreview.url, linkPreview.url); + } + case "child_page": + case "child_database": + { + let blockContent; + let title: string = type; + if (type === "child_page") { + blockContent = { url: block.id }; + title = block.child_page.title; + } + + if (type === "child_database") { + blockContent = { url: block.id }; + title = block.child_database.title || "child_database"; + } + + if (blockContent) return md.link(title, blockContent.url); + } + break; + + case "table": { + const { id, has_children } = block; + let tableArr: string[][] = []; + if (has_children) { + const tableRows = await getBlockChildren(this.notionClient, id, 100); + // console.log(">>", tableRows); + let rowsPromise = tableRows?.map(async (row) => { + const { type } = row as any; + const cells = (row as any)[type]["cells"]; + + /** + * this is more like a hack since matching the type text was + * difficult. So converting each cell to paragraph type to + * reuse the blockToMarkdown function + */ + let cellStringPromise = cells.map( + async (cell: any) => + await this.blockToMarkdown({ + type: "paragraph", + paragraph: { rich_text: cell }, + } as GetBlockResponse) + ); + + const cellStringArr = await Promise.all(cellStringPromise); + // console.log("~~", cellStringArr); + tableArr.push(cellStringArr); + // console.log(tableArr); + }); + await Promise.all(rowsPromise || []); + } + return md.table(tableArr); + } + + case "column_list": { + const { id, has_children } = block; + + if (!has_children) return ""; + + const column_list_children = await getBlockChildren( + this.notionClient, + id, + 100 + ); + + let column_list_promise = column_list_children.map( + async (column) => await this.blockToMarkdown(column) + ); + + let column_list: string[] = await Promise.all(column_list_promise); + + return column_list.join("\n\n"); + } + + case "column": { + const { id, has_children } = block; + if (!has_children) return ""; + + const column_children = await getBlockChildren( + this.notionClient, + id, + 100 + ); + + const column_children_promise = column_children.map( + async (column_child) => await this.blockToMarkdown(column_child) + ); + + let column: string[] = await Promise.all(column_children_promise); + return column.join("\n\n"); + } + + case "toggle": { + const { id, has_children } = block; + + const toggle_summary = block.toggle.rich_text[0]?.plain_text; + + // empty toggle + if (!has_children) { + return md.toggle(toggle_summary); + } + + const toggle_children_object = await getBlockChildren( + this.notionClient, + id, + 100 + ); + + // parse children blocks to md object + const toggle_children = await this.blocksToMarkdown( + toggle_children_object + ); + + // convert children md object to md string + const toggle_children_md_string = + this.toMarkdownString(toggle_children); + + return md.toggle(toggle_summary, toggle_children_md_string); + } + + case "paragraph": + return await this.richText(block.paragraph.rich_text); + case "heading_1": + return md.heading1(await this.richText(block.heading_1.rich_text)); + case "heading_2": + return md.heading2(await this.richText(block.heading_2.rich_text)); + case "heading_3": + return md.heading3(await this.richText(block.heading_3.rich_text)); + case "bulleted_list_item": + return md.bullet( + await this.richText(block.bulleted_list_item.rich_text) + ); + case "numbered_list_item": + return md.bullet( + await this.richText(block.numbered_list_item.rich_text), + 1 + ); + case "to_do": + return md.todo( + await this.richText(block.to_do.rich_text), + block.to_do.checked + ); + case "code": + return md.codeBlock( + plainText(block.code.rich_text), + block.code.language + ); + case "callout": + const { id, has_children } = block; + const callout_text = await this.richText(block.callout.rich_text); + if (!has_children) return md.callout(callout_text, block.callout.icon); + + let callout_string = ""; + + const callout_children_object = await getBlockChildren( + this.notionClient, + id, + 100 + ); + + // parse children blocks to md object + const callout_children = await this.blocksToMarkdown( + callout_children_object + ); + + callout_string += `${callout_text}\n`; + callout_children.map((child) => { + callout_string += `${child.parent}\n\n`; + }); + + return md.callout(callout_string.trim(), block.callout.icon); + case "quote": + const quote_text = await this.richText(block.quote.rich_text); + if (!block.has_children) return md.quote(quote_text); + let quote_string = ""; + const quote_children_object = await getBlockChildren( + this.notionClient, + block.id, + 100 + ); + const quote_children = await this.blocksToMarkdown( + quote_children_object + ); + + quote_string += `${quote_text}\n`; + quote_children.map((child) => { + quote_string += `${child.parent}\n\n`; + }); + + return md.quote(quote_string.trim()); + + case "audio": + return md.audio(block); + case "template": + case "synced_block": + case "child_page": + case "child_database": + case "column": + case "link_preview": + case "column_list": + case "link_to_page": + case "breadcrumb": + case "unsupported": + case "table_of_contents": + return this.unsupportedTransformer(type); + } + return ""; + } +} diff --git a/src/markdown/notion.ts b/src/markdown/notion.ts new file mode 100644 index 0000000..1e54216 --- /dev/null +++ b/src/markdown/notion.ts @@ -0,0 +1,77 @@ +import { Client, isFullPage } from "@notionhq/client"; +import { + GetBlockResponse, + ListBlockChildrenResponse, + PageObjectResponse, +} from "@notionhq/client/build/src/api-endpoints"; +import { plainText } from "./md"; + +export const getBlockChildren = async ( + notionClient: Client, + block_id: string, + totalPage: number | null +) => { + try { + let results: GetBlockResponse[] = []; + let pageCount = 0; + let start_cursor = undefined; + + do { + const response: ListBlockChildrenResponse = + await notionClient.blocks.children.list({ + start_cursor, + block_id, + }); + results.push(...response.results); + + start_cursor = response.next_cursor; + pageCount += 1; + } while ( + start_cursor != null && + (totalPage == null || pageCount < totalPage) + ); + + return results; + } catch (e) { + console.log(e); + return []; + } +}; + +export function getPageTitle(page: PageObjectResponse): string { + const title = page.properties.Name ?? page.properties.title; + if (title.type === "title") { + return plainText(title.title); + } + throw Error( + `page.properties.Name has type ${title.type} instead of title. The underlying Notion API might has changed, please report an issue to the author.` + ); +} + +export function getFileName(title: any, page_id: any): string { + return ( + title.replaceAll(" ", "-").replace(/--+/g, "-") + + "-" + + page_id.replaceAll("-", "") + + ".md" + ); +} + +export const getPageRelrefFromId = async ( + pageId: string, + notion: Client +): Promise<{ + title: string; + relref: string; +}> => { + const page = await notion.pages.retrieve({ page_id: pageId }); // throw if failed + if (!isFullPage(page)) { + throw Error( + `The pages.retrieve endpoint failed to return a full page for ${pageId}.` + ); + } + const title = getPageTitle(page); + const fileName = getFileName(title, page.id); + const relref = `{{% relref "${fileName}" %}}`; + return { title, relref }; +}; diff --git a/src/markdown/types.ts b/src/markdown/types.ts new file mode 100644 index 0000000..d72bcf4 --- /dev/null +++ b/src/markdown/types.ts @@ -0,0 +1,23 @@ +import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; +import { Client } from "@notionhq/client"; + +export interface NotionToMarkdownOptions { + notionClient: Client; +} + +export type MdBlock = { + type?: string; + parent: string; + children: MdBlock[]; + expiry_time?: string; +}; + +export type CalloutIcon = + | { type: "emoji"; emoji: string } + | { type: "external"; external: { url: string } } + | { type: "file"; file: { url: string; expiry_time: string } } + | null; + +export type CustomTransformer = ( + block: GetBlockResponse +) => string | Promise; diff --git a/src/render.ts b/src/render.ts index fcf5d59..861dbab 100644 --- a/src/render.ts +++ b/src/render.ts @@ -7,12 +7,12 @@ import { import { PageObjectResponse, } from "@notionhq/client/build/src/api-endpoints"; -import { NotionToMarkdown } from "@pclouddev/notion-to-markdown"; +import { NotionToMarkdown } from "./markdown/notion-to-md"; import YAML from "yaml"; import { sh } from "./sh"; import { DatabaseMount, PageMount } from "./config"; import { getPageTitle, getCoverLink, getFileName } from "./helpers"; -import { MdBlock } from "@pclouddev/notion-to-markdown/build/types"; +import { MdBlock } from "./markdown/types"; import path from "path"; import { getContentFile } from "./file";