UNPKG

notion-helper

Version:

A library of functions for working more easily with the Notion API

1,535 lines (1,531 loc) 190 kB
// src/constants.mjs var CONSTANTS = { MAX_TEXT_LENGTH: 2e3, MAX_MENTION_LENGTH: 2e3, MAX_BLOCKS: 100, MAX_BLOCKS_REQUEST: 999, MAX_CHILD_ARRAY_DEPTH: 2, MAX_PAYLOAD_SIZE: 45e4, // 450kb MAX_EQUATION_LENGTH: 1e3, MAX_URL_LENGTH: 2e3, MAX_PHONE_NUMBER_LENGTH: 200, MAX_EMAIL_LENGTH: 200, MAX_MULTI_SELECT_COUNT: 100, MAX_RELATION_COUNT: 100, MAX_PEOPLE_COUNT: 100, IMAGE_SUPPORT: { FORMATS: [ "bmp", "gif", "heic", "jpeg", "jpg", "png", "svg", "tif", "tiff" ] }, VIDEO_SUPPORT: { FORMATS: [ "amv", "asf", "avi", "f4v", "flv", "gifv", "mkv", "mov", "mpg", "mpeg", "mpv", "mp4", "m4v", "qt", "wmv" ], SITES: [ "youtube.com" ] }, AUDIO_SUPPORT: { FORMATS: [ "mp3", "wav", "ogg", "mid", "midi", "wma", "aac", "m4a", "m4b" ] }, DOCUMENT_SUPPORT: { FORMATS: [ "pdf", "json", "txt" ] } }; var constants_default = CONSTANTS; // src/utils.mjs function isSingleEmoji(string) { const trimmedString = string.trim(); const regex = /^(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\p{Emoji_Modifier})?$/u; return regex.test(trimmedString); } function isValidURL(string) { validateStringLength({ string, type: "url" }); try { const url2 = new URL(string); return url2.protocol === "http:" || url2.protocol === "https:"; } catch (e) { return false; } } function isValidUUID(string) { const regex = /^[0-9a-fA-F]{8}(-?[0-9a-fA-F]{4}){3}-?[0-9a-fA-F]{12}$/; return regex.test(string); } function validateImageURL(url2) { try { const supportedFormats = constants_default.IMAGE_SUPPORT.FORMATS.join("|"); const formatRegex = new RegExp(`\\.(${supportedFormats})$`, "i"); return formatRegex.test(url2) && isValidURL(url2); } catch (e) { return false; } } function validateVideoURL(url2) { try { const supportedFormats = constants_default.VIDEO_SUPPORT.FORMATS.join("|"); const formatRegex = new RegExp(`\\.(${supportedFormats})$`, "i"); const supportedSites = constants_default.VIDEO_SUPPORT.SITES.join("|"); const siteRegex = new RegExp(`(${supportedSites})`, "i"); return (formatRegex.test(url2) || siteRegex.test(url2)) && isValidURL(url2); } catch (e) { return false; } } function validateAudioURL(url2) { try { const supportedFormats = constants_default.AUDIO_SUPPORT.FORMATS.join("|"); const formatRegex = new RegExp(`\\.(${supportedFormats})$`, "i"); return formatRegex.test(url2) && isValidURL(url2); } catch (e) { return false; } } function validatePDFURL(url2) { try { const formatRegex = new RegExp(`\\.pdf$`, "i"); return formatRegex.test(url2) && isValidURL(url2); } catch (e) { return false; } } function validateStringLength({ string, type, limit }) { if (typeof string !== "string") { console.warn(`Invalid input sent to validateStringLength(). Expected a string, got: ${typeof string}. String: ${string}. Type: ${type}. Limit: ${limit}.`); return; } let resolvedLimit = limit; if (typeof resolvedLimit !== "number" || resolvedLimit <= 0) { switch (type) { case "text": resolvedLimit = constants_default.MAX_TEXT_LENGTH; break; case "mention": resolvedLimit = constants_default.MAX_MENTION_LENGTH; break; case "equation": resolvedLimit = constants_default.MAX_EQUATION_LENGTH; break; case "url": resolvedLimit = constants_default.MAX_URL_LENGTH; break; case "email": resolvedLimit = constants_default.MAX_EMAIL_LENGTH; break; case "phone_number": resolvedLimit = constants_default.MAX_PHONE_NUMBER_LENGTH; break; default: resolvedLimit = void 0; } } if (typeof resolvedLimit === "number" && string.length > resolvedLimit) { const displayString = string.length > 500 ? string.slice(0, 500) + "...[truncated]" : string; console.warn(`String length is over the limit. String length: ${string.length}, Max length: ${resolvedLimit}. String: ${displayString}. Type: ${type}.`); } } function validateArrayLength({ array, type, limit }) { if (!Array.isArray(array)) { console.warn(`Invalid input sent to validateArrayLength(). Expected an array, got: ${typeof array}. Array: ${array}. Type: ${type}. Limit: ${limit}.`); return; } let resolvedLimit = limit; if (typeof resolvedLimit !== "number" || resolvedLimit <= 0) { switch (type) { case "relation": resolvedLimit = constants_default.MAX_RELATION_COUNT; break; case "multi_select": resolvedLimit = constants_default.MAX_MULTI_SELECT_COUNT; break; case "people": resolvedLimit = constants_default.MAX_PEOPLE_COUNT; break; default: resolvedLimit = void 0; } } if (typeof resolvedLimit === "number" && array.length > resolvedLimit) { const displayArray = array.length > 10 ? array.slice(0, 10) + "...[truncated]" : array; console.warn(`Array length is over the limit. Array length: ${array.length}, Max length: ${resolvedLimit}. Array: ${displayArray}. Type: ${type}.`); } } function enforceStringLength(string, limit) { if (typeof string !== "string") { console.error( "Invalid input sent to enforceStringLength(). Expected a string, got: ", string, typeof string ); throw new Error("Invalid input: Expected a string."); } const charLimit = constants_default.MAX_TEXT_LENGTH; const softLimit = limit && limit > 0 ? limit : charLimit * 0.8; if (string.length < charLimit) { return [string]; } else { let chunks = []; let currentIndex = 0; while (currentIndex < string.length) { let nextCutIndex = Math.min( currentIndex + softLimit, string.length ); let nextSpaceIndex = string.indexOf(" ", nextCutIndex); if (nextSpaceIndex === -1 || nextSpaceIndex - currentIndex > softLimit) { nextSpaceIndex = nextCutIndex; } while (nextSpaceIndex > 0 && string.charCodeAt(nextSpaceIndex - 1) >= 55296 && string.charCodeAt(nextSpaceIndex - 1) <= 56319) { nextSpaceIndex--; } chunks.push(string.substring(currentIndex, nextSpaceIndex)); currentIndex = nextSpaceIndex + 1; } return chunks; } } function validateDate(dateInput) { let date2; if (dateInput === null) { return null; } if (dateInput instanceof Date) { date2 = dateInput; } else if (typeof dateInput === "string") { date2 = new Date(dateInput); } else { console.warn(`Invalid input: Expected a Date object or string representing a date. Returning null.`); return null; } if (!isNaN(date2.getTime())) { const isoString = date2.toISOString(); if (typeof dateInput === "string" && !dateInput.includes(":") && !dateInput.includes("T")) { return isoString.split("T")[0]; } else { return isoString; } } else { console.warn(`Invalid date string or Date object provided. Returning null.`); return null; } } function getDepth(arr, level = 0) { if (!Array.isArray(arr) || arr.length === 0) { return level; } let maxDepth = level; for (let block2 of arr) { if (block2[block2.type].children) { const depth = getDepth(block2[block2.type].children, level + 1); maxDepth = Math.max(maxDepth, depth); } } return maxDepth; } function getTotalCount(arr) { if (!arr || arr?.length === 0) return 0; return arr.reduce( (acc, child) => { if (child[child.type].children) { return acc + 1 + getTotalCount(child[child.type].children); } return acc; }, 0 ); } function getLongestArray(arr, count = 0) { if (!Array.isArray(arr) || arr.length === 0) { return count; } let maxLength = Math.max(count, arr.length); for (let block2 of arr) { if (block2[block2.type].children) { const count2 = getLongestArray(block2[block2.type].children, maxLength); maxLength = Math.max(count2, maxLength); } } return maxLength; } function getPayloadSize(arr) { if (!arr || !Array.isArray(arr)) return 0; const size = arr.reduce((acc, block2) => { return acc + new TextEncoder().encode(JSON.stringify(block2)).length; }, 0); return size; } function validateAndSplitBlock(block2, limit) { if (!block2 || typeof block2 !== "object") { console.warn(`Invalid input sent to validateAndSplitBlock(). Expected a Notion block object, got: ${typeof block2}. Block: ${block2}.`); return []; } if (!block2.type) { console.warn(`Invalid Notion block: missing 'type' property. Block: ${JSON.stringify(block2)}.`); return []; } const blockContent = block2[block2.type]; if (!blockContent) { return [block2]; } function processRichTextArray(richTextArray, textLimit = limit) { const processedRichText = []; for (const richTextItem of richTextArray) { if (richTextItem.type === "text" && richTextItem.text && richTextItem.text.content) { const textChunks = enforceStringLength(richTextItem.text.content, textLimit); for (const chunk of textChunks) { processedRichText.push({ ...richTextItem, text: { ...richTextItem.text, content: chunk } }); } } else if (richTextItem.type === "equation" && richTextItem.equation && richTextItem.equation.expression) { const equationChunks = enforceStringLength(richTextItem.equation.expression, constants_default.MAX_EQUATION_LENGTH); for (const chunk of equationChunks) { processedRichText.push({ ...richTextItem, equation: { ...richTextItem.equation, expression: chunk } }); } } else { processedRichText.push(richTextItem); } } return processedRichText; } if (blockContent.rich_text) { const processedRichText = processRichTextArray(blockContent.rich_text); if (processedRichText.length <= constants_default.MAX_BLOCKS) { return [{ ...block2, [block2.type]: { ...blockContent, rich_text: processedRichText } }]; } const splitBlocks = []; const chunkSize = constants_default.MAX_BLOCKS; for (let i = 0; i < processedRichText.length; i += chunkSize) { const richTextChunk = processedRichText.slice(i, i + chunkSize); const isFirstBlock = i === 0; const newBlock = { type: block2.type, [block2.type]: { ...blockContent, rich_text: richTextChunk } }; if (blockContent.color) { newBlock[block2.type].color = blockContent.color; } if (isFirstBlock && blockContent.children) { newBlock[block2.type].children = blockContent.children; } splitBlocks.push(newBlock); } console.info(`Block split into ${splitBlocks.length} blocks due to rich text array exceeding MAX_BLOCKS limit. Original rich text count: ${blockContent.rich_text.length}, Processed count: ${processedRichText.length}.`); return splitBlocks; } const captionBlockTypes = ["image", "video", "audio", "file", "pdf", "code"]; if (captionBlockTypes.includes(block2.type) && blockContent.caption) { const processedCaption = processRichTextArray(blockContent.caption); if (processedCaption.length > constants_default.MAX_BLOCKS) { const truncatedCaption = processedCaption.slice(0, constants_default.MAX_BLOCKS); let previewText = ""; if (processedCaption[0] && processedCaption[0].type === "text" && processedCaption[0].text && processedCaption[0].text.content) { previewText = processedCaption[0].text.content; } else if (processedCaption[0] && processedCaption[0].type === "equation" && processedCaption[0].equation && processedCaption[0].equation.expression) { previewText = processedCaption[0].equation.expression; } const displayText = previewText.length > 500 ? previewText.slice(0, 500) + "...[truncated]" : previewText; console.warn(`Caption array exceeded MAX_BLOCKS limit (${constants_default.MAX_BLOCKS}). Truncated from ${processedCaption.length} to ${constants_default.MAX_BLOCKS} rich text objects. Block type: ${block2.type}. Preview: ${displayText}`); return [{ ...block2, [block2.type]: { ...blockContent, caption: truncatedCaption } }]; } return [{ ...block2, [block2.type]: { ...blockContent, caption: processedCaption } }]; } return [block2]; } function extractNotionPageId(url2) { if (!url2 || typeof url2 !== "string") return null; const [baseUrl] = url2.split("?"); const regex = /([a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})(?![a-f0-9])/i; const match = baseUrl.match(regex); if (!match) return null; return match[1].replace(/-/g, ""); } // src/rich-text.mjs var LOG_PREFIX = "buildRichTextObj"; var PREVIEW_MAX_LENGTH = 200; var DEFAULT_OVERFLOW_STRATEGY = "split"; var DEFAULT_INVALID_URL_STRATEGY = "warn"; var DEFAULT_INVALID_MENTION_STRATEGY = "warn"; var OVERFLOW_STRATEGIES = /* @__PURE__ */ new Set(["split", "truncate", "throw"]); var INVALID_URL_STRATEGIES = /* @__PURE__ */ new Set(["warn", "strip", "throw"]); var INVALID_MENTION_STRATEGIES = /* @__PURE__ */ new Set(["warn", "strip", "throw"]); var ALLOWED_ANNOTATION_KEYS = /* @__PURE__ */ new Set([ "bold", "italic", "underline", "strikethrough", "code", "color" ]); function buildRichTextObj(input, options = {}) { const isLegacyAnnotationsObject = arguments.length > 1 && options && typeof options === "object" && !Array.isArray(options) && Object.keys(options).length > 0 && Object.keys(options).every((key) => ALLOWED_ANNOTATION_KEYS.has(key)); if (isLegacyAnnotationsObject) { options = { annotations: options, url: arguments[2], type: arguments[3] || "text" }; } const { annotations = {}, url: url2, type = "text", overflow = DEFAULT_OVERFLOW_STRATEGY, onInvalidUrl = DEFAULT_INVALID_URL_STRATEGY, onInvalidMentionId = DEFAULT_INVALID_MENTION_STRATEGY } = options; const overflowStrategy = normalizeStrategy(overflow, OVERFLOW_STRATEGIES, DEFAULT_OVERFLOW_STRATEGY); const invalidUrlStrategy = normalizeStrategy(onInvalidUrl, INVALID_URL_STRATEGIES, DEFAULT_INVALID_URL_STRATEGY); const invalidMentionStrategy = normalizeStrategy(onInvalidMentionId, INVALID_MENTION_STRATEGIES, DEFAULT_INVALID_MENTION_STRATEGY); const sanitizedAnnotations = sanitizeAnnotations(annotations); let resolvedUrl = url2 ?? null; if (resolvedUrl) { resolvedUrl = sanitizeUrl(resolvedUrl, invalidUrlStrategy, input); } if (typeof input === "string") { const limit = getMaxLengthForType(type); const chunks = applyOverflowStrategy(input, { limit, strategy: overflowStrategy, type }); switch (type) { case "text": return chunks.map((content) => ({ type: "text", text: { content, link: resolvedUrl ? { url: resolvedUrl } : null }, annotations: { ...sanitizedAnnotations }, ...resolvedUrl ? { href: resolvedUrl } : {} })); case "equation": return chunks.map((expression) => ({ type: "equation", equation: { expression }, annotations: { ...sanitizedAnnotations }, ...resolvedUrl ? { href: resolvedUrl } : {} })); default: break; } } if (typeof input === "object") { if (type === "text" || !type) { return [ { type: "text", text: input } ]; } switch (type) { case "equation": { const expression = typeof input?.expression === "string" ? input.expression : null; if (expression) { const chunks = applyOverflowStrategy(expression, { limit: getMaxLengthForType("equation"), strategy: overflowStrategy, type: "equation" }); return chunks.map((segment) => ({ type: "equation", equation: { ...input, expression: segment }, annotations: { ...annotations }, ...resolvedUrl ? { href: resolvedUrl } : {} })); } return [ { type: "equation", equation: { ...input }, annotations: { ...sanitizedAnnotations }, ...resolvedUrl ? { href: resolvedUrl } : {} } ]; } case "mention": return processMentionInput(input, { annotations: sanitizedAnnotations, url: resolvedUrl, onInvalidMentionId: invalidMentionStrategy, overflow: overflowStrategy, onInvalidUrl: invalidUrlStrategy }); default: { const error2 = `Unsupported rich text type: ${type}`; console.error(error2); throw new Error(error2); } } } const error = `Invalid input sent to buildRichTextObj()`; console.error(error); throw new Error(error); } function mentionUser(userId, options = {}) { return buildRichTextObj( { type: "user", user: { id: userId } }, { type: "mention", ...options } ); } function mentionDate(date2, options = {}) { const dateObj = typeof date2 === "string" ? { start: date2 } : date2; if (!dateObj || !dateObj.start) { console.warn(`Invalid date. Date: ${date2}.`); } if (dateObj.end && !dateObj.start) { console.warn(`Invalid date. Date: ${date2}. End date provided without start date.`); } const validatedDateObj = { start: validateDate(dateObj.start), end: dateObj.end ? validateDate(dateObj.end) : null }; return buildRichTextObj( { type: "date", date: validatedDateObj }, { type: "mention", ...options } ); } function mentionDatabase(databaseId, options = {}) { return buildRichTextObj( { type: "database", database: { id: databaseId } }, { type: "mention", ...options } ); } function mentionPage(pageId2, options = {}) { return buildRichTextObj( { type: "page", page: { id: pageId2 } }, { type: "mention", ...options } ); } function enforceRichText(content) { if (!content) { return []; } if (Array.isArray(content)) { return content.flatMap( (item) => typeof item === "string" ? enforceRichText(item) : enforceRichTextObject(item) ); } if (typeof content === "string") { const strings = enforceStringLength(content); const richTextObjects = strings.flatMap((string) => { const isURL = isValidURL(string); const isBold = /^\*{2}[\s\S]*?\*{2}$/.test(string); const isItalic = /^[\*_]{1}[^\*_]{1}[\s\S]*?[^\*_]{1}[\*_]{1}$/.test(string); const isBoldItalic = /^\*{3}[\s\S]*?\*{3}$/.test(string); let plainString = string; if (isBold || isItalic || isBoldItalic) { plainString = string.replace(/^(\*|_)+|(\*|_)+$/g, ""); } return buildRichTextObj( plainString, { annotations: { bold: isBold || isBoldItalic, italic: isItalic || isBoldItalic }, url: isURL ? plainString : null } ); }); return richTextObjects; } if (typeof content === "number") { return buildRichTextObj(content.toString()); } if (typeof content === "object") { return [enforceRichTextObject(content)]; } console.warn(`Invalid input for rich text. Returning empty array.`); return []; } function enforceRichTextObject(obj) { if (typeof obj === "string") { return buildRichTextObj(obj)[0]; } if (obj?.type === "text" && obj?.text && typeof obj.text.content === "string") { validateStringLength({ string: obj.text.content, type: "text" }); return obj; } if (obj?.type === "equation" && typeof obj?.equation?.expression === "string") { validateStringLength({ string: obj.equation.expression, type: "equation" }); return obj; } if (obj?.type === "mention" && obj?.mention && typeof obj.mention === "object") { validateStringLength({ string: obj.mention.type, type: "mention" }); return obj; } if (obj?.type === "equation" && typeof obj?.expression === "string") { validateStringLength({ string: obj.expression, type: "equation" }); return { type: "equation", equation: { expression: obj.expression }, ...obj.annotations ? { annotations: obj.annotations } : {} }; } const mentionTypes = /* @__PURE__ */ new Set(["database", "date", "page", "user"]); if (mentionTypes.has(obj?.type)) { const { annotations, type, ...rest } = obj; return { type: "mention", mention: { type, [type]: rest }, ...annotations ? { annotations } : {} }; } console.warn(`Invalid rich text object. Returning empty rich text object.`); return buildRichTextObj("")[0]; } function normalizeStrategy(value, allowedStrategies, fallback) { if (!value || typeof value !== "string") { return fallback; } const normalized = value.toLowerCase(); if (allowedStrategies.has(normalized)) { return normalized; } console.warn(`[${LOG_PREFIX}] Unknown strategy "${value}". Falling back to "${fallback}".`); return fallback; } function createPreview(value, maxLength = PREVIEW_MAX_LENGTH) { if (value === null || value === void 0) { return ""; } const stringValue = typeof value === "string" ? value : String(value); return stringValue.length > maxLength ? `${stringValue.slice(0, maxLength)}...[truncated]` : stringValue; } function sanitizeAnnotations(input) { if (!input || typeof input !== "object") { return {}; } const sanitized = {}; for (const key of ALLOWED_ANNOTATION_KEYS) { if (Object.prototype.hasOwnProperty.call(input, key)) { sanitized[key] = input[key]; } } return sanitized; } function getMaxLengthForType(type) { switch (type) { case "equation": return constants_default.MAX_EQUATION_LENGTH; case "mention": return constants_default.MAX_MENTION_LENGTH; case "text": default: return constants_default.MAX_TEXT_LENGTH; } } function isHighSurrogate(codePoint) { return codePoint >= 55296 && codePoint <= 56319; } function splitStringByLimit(value, limit) { if (!limit || limit <= 0) { limit = constants_default.MAX_TEXT_LENGTH; } const segments = []; let index = 0; while (index < value.length) { let end = Math.min(index + limit, value.length); if (end < value.length) { const code2 = value.charCodeAt(end - 1); if (isHighSurrogate(code2)) { end += 1; if (end > value.length) { end = value.length; } } } if (end === index) { end = Math.min(index + limit, value.length); if (end === index) { end = Math.min(index + 1, value.length); } } segments.push(value.slice(index, end)); index = end; } return segments; } function applyOverflowStrategy(value, { limit, strategy, type }) { if (typeof value !== "string") { return [value]; } if (value.length <= limit) { return [value]; } const preview = createPreview(value); switch (strategy) { case "split": { const chunks = splitStringByLimit(value, limit); console.warn(`[${LOG_PREFIX}] Input for type "${type}" exceeded ${limit} characters. Strategy: split into ${chunks.length} chunk(s). Preview: ${preview}`); return chunks; } case "truncate": console.warn(`[${LOG_PREFIX}] Input for type "${type}" exceeded ${limit} characters. Strategy: truncate. Preview: ${preview}`); return [value.slice(0, limit)]; case "throw": throw new Error(`[${LOG_PREFIX}] Input for type "${type}" exceeded maximum length (${value.length} > ${limit}). Preview: ${preview}`); default: { console.warn(`[${LOG_PREFIX}] Unknown overflow strategy "${strategy}". Falling back to split.`); const chunks = splitStringByLimit(value, limit); console.warn(`[${LOG_PREFIX}] Input for type "${type}" exceeded ${limit} characters. Strategy: split into ${chunks.length} chunk(s). Preview: ${preview}`); return chunks; } } } function sanitizeUrl(url2, strategy, contextPreview) { if (!url2) { return null; } validateStringLength({ string: url2, type: "url" }); if (isValidURL(url2)) { return url2; } const preview = createPreview(contextPreview); switch (strategy) { case "warn": console.warn(`[${LOG_PREFIX}] Invalid URL "${url2}". Strategy: warn. Input preview: ${preview}`); return url2; case "strip": console.warn(`[${LOG_PREFIX}] Invalid URL "${url2}". Strategy: strip (link removed). Input preview: ${preview}`); return null; case "throw": throw new Error(`[${LOG_PREFIX}] Invalid URL "${url2}". Strategy: throw. Input preview: ${preview}`); default: console.warn(`[${LOG_PREFIX}] Unknown invalid URL strategy "${strategy}". Defaulting to warn.`); console.warn(`[${LOG_PREFIX}] Invalid URL "${url2}". Strategy: warn. Input preview: ${preview}`); return url2; } } function extractMentionId(mention) { if (!mention || typeof mention !== "object") { return null; } switch (mention.type) { case "user": return mention.user?.id ?? null; case "page": return mention.page?.id ?? null; case "database": return mention.database?.id ?? null; default: return null; } } function createMentionPlainText(mention) { if (!mention || typeof mention !== "object") { return ""; } switch (mention.type) { case "user": return "@User"; case "page": return "Page"; case "database": return "Database"; case "date": return mention.date?.start ?? "Date"; case "template_mention": return "Template"; default: return "Mention"; } } function processMentionInput(mention, { annotations, url: url2, onInvalidMentionId, overflow, onInvalidUrl }) { const sanitizedAnnotations = sanitizeAnnotations(annotations); if (!mention || typeof mention !== "object" || typeof mention.type !== "string") { console.warn(`[${LOG_PREFIX}] Invalid mention payload provided. Converting to empty text.`); return buildRichTextObj("", { annotations: sanitizedAnnotations, url: url2, overflow, onInvalidUrl, onInvalidMentionId }); } const mentionId = extractMentionId(mention); if (mentionId && !isValidUUID(mentionId)) { const preview = createPreview(mentionId); switch (onInvalidMentionId) { case "warn": console.warn(`[${LOG_PREFIX}] Invalid ${mention.type} ID "${mentionId}". Strategy: warn. Preview: ${preview}`); break; case "strip": console.warn(`[${LOG_PREFIX}] Invalid ${mention.type} ID "${mentionId}". Strategy: strip (converted to text). Preview: ${preview}`); return buildRichTextObj( `Invalid ${mention.type} mention (${preview})`, { annotations: sanitizedAnnotations, ...url2 ? { url: url2 } : {}, overflow, onInvalidUrl, onInvalidMentionId } ); case "throw": throw new Error(`[${LOG_PREFIX}] Invalid ${mention.type} ID "${mentionId}". Strategy: throw. Preview: ${preview}`); default: console.warn(`[${LOG_PREFIX}] Unknown invalid mention strategy "${onInvalidMentionId}". Defaulting to warn.`); console.warn(`[${LOG_PREFIX}] Invalid ${mention.type} ID "${mentionId}". Strategy: warn. Preview: ${preview}`); } } const plainText = createMentionPlainText(mention); return [ { type: "mention", mention, annotations: { ...sanitizedAnnotations }, ...plainText ? { plain_text: plainText } : {}, ...url2 ? { href: url2 } : {} } ]; } // src/emoji-and-files.mjs function setIcon(value) { if (typeof value !== "string") { return {}; } const isEmoji = isSingleEmoji(value); const isImageURL = validateImageURL(value); const isUUID = isValidUUID(value); if (isImageURL) { return createExternal(value); } else if (isEmoji) { return createEmoji(value); } else if (isUUID) { return createFile(value); } else { return void 0; } } function createExternal(url2) { return { type: "external", external: { url: url2 } }; } function createEmoji(emoji) { return { type: "emoji", emoji }; } function createFile(id) { return { type: "file_upload", file_upload: { id } }; } // src/blocks.mjs var block = { /** * Methods for audio blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ audio: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates an audio block. * * @function * @param {string|Object} options - A string representing the audio URL, a file upload ID, or an options object. * @param {string} options.url - The URL for the audio. * @param {string} options.id - The ID of the file upload. * @param {string|string[]|Array<Object>} [options.caption=[]] - The caption as a string, an array of strings, or an array of rich text objects. * @returns {Object|null} An audio block object compatible with Notion's API, or null if the URL/ID is invalid. * @example * // Use with an external URL * const externalAudio = block.audio.createBlock("https://thomasjfrank.com/wp-content/uploads/2025/05/output3.mp3"); * * // Use with a file upload ID * const fileUploadAudio = block.audio.createBlock("123e4567-e89b-12d3-a456-426614174000"); * * // Use with options object for external audio * const externalAudio = block.audio.createBlock({ * url: "https://thomasjfrank.com/wp-content/uploads/2025/05/output3.mp3", * caption: "Check out my mixtape, man." * }); * * // Use with options object for file upload * const fileUploadAudio = block.audio.createBlock({ * id: "123e4567-e89b-12d3-a456-426614174000", * caption: "Check out my mixtape, man." * }); */ createBlock: (options) => { let url2, id, caption; if (typeof options === "string") { url2 = options; caption = []; } else { ({ url: url2, id, caption = [] } = options); } let urlOrId = url2 || id; const isValidAudio = validateAudioURL(urlOrId) || isValidUUID(urlOrId); const isFileUpload = isValidUUID(urlOrId); const isExternal = isValidURL(urlOrId); const audioType = isFileUpload ? "file_upload" : isExternal ? "external" : "file"; if (!isValidAudio) { console.warn(`${urlOrId} is not a valid audio URL or file upload ID.`); } return isValidAudio ? { type: "audio", audio: { type: audioType, [audioType]: { [audioType === "file_upload" ? "id" : "url"]: urlOrId }, caption: enforceRichText(caption) } } : null; } }, /** * Methods for bookmark blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ bookmark: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates a bookmark block. * * @function * @param {string|Object} options - A string representing the URL, or an options object. * @param {string} options.url - The URL to be bookmarked. * @param {string|string[]|Array<Object>} [options.caption=[]] - The caption as a string, an array of strings, or an array of rich text objects. * @returns {Object} A bookmark block object compatible with Notion's API. * @example * // Use with just a URL * const simpleBookmark = block.bookmark.createBlock("https://www.flylighter.com"); * * // Use with options object * const complexBookmark = block.bookmark.createBlock({ * url: "https://www.flylighter.com", * caption: "Flylighter is a super-rad web clipper for Notion." * }); * * // Use with options object and array of strings for caption * const multiLineBookmark = block.bookmark.createBlock({ * url: "https://www.flylighter.com", * caption: ["Flylighter is a web clipper for Notion...", "...and Obsidian, too."] * }); */ createBlock: (options) => { let url2, caption; if (typeof options === "string") { url2 = options; caption = []; } else { ({ url: url2, caption = [] } = options); } return { type: "bookmark", bookmark: { url: url2, caption: enforceRichText(caption) } }; } }, /** * Methods for breadcrumb blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ breadcrumb: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates a breadcrumb block. * * @returns {Object} A breadcrumb block object. * * @example * // Create a breadcrumb block * const breadcrumbBlock = block.breadcrumb.createBlock(); */ createBlock: () => { return { type: "breadcrumb", breadcrumb: {} }; } }, /** * Methods for bulleted list item blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ bulleted_list_item: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: true, /** * Creates a bulleted list item block. * * @function * @param {string|string[]|Object} options - A string, an array of strings, or an options object representing the list item content. * @param {string|string[]|Array<Object>} [options.rich_text=[]] - The item's content as a string, an array of strings, or an array of rich text objects. * @param {Array<Object>} [options.children=[]] - An array of child block objects. * @param {string} [options.color="default"] - Color for the text. * @returns {Object} A bulleted list item block object compatible with Notion's API. * @example * // Use with a string * const simpleItem = block.bulleted_list_item.createBlock("Simple list item"); * * // Use with an array of strings * const multiLineItem = block.bulleted_list_item.createBlock(["Line 1", "Line 2"]); * * // Use with options object * const complexItem = block.bulleted_list_item.createBlock({ * rich_text: "Complex item", * color: "red", * children: [ * // Child blocks would go here * ] * }); */ createBlock: (options) => { let rich_text, children, color; if (typeof options === "string" || Array.isArray(options)) { rich_text = options; children = []; color = "default"; } else { ({ rich_text = [], children = [], color = "default" } = options); } return { type: "bulleted_list_item", bulleted_list_item: { rich_text: enforceRichText(rich_text), color, ...children.length > 0 && { children } } }; } }, /** * Methods for callout blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ callout: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: true, /** * Creates a callout block. * * @function * @param {string|string[]|Object} options - A string, an array of strings, or an options object representing the callout content. * @param {string|string[]|Array<Object>} [options.rich_text=[]] - The content as a string, an array of strings, or an array of rich text objects. * @param {string} [options.icon=""] - An optional icon value (URL for "external" or emoji character for "emoji"). * @param {Array<Object>} [options.children=[]] - An array of child block objects. * @param {string} [options.color="default"] - Color for the callout background. * @returns {Object} A callout block object compatible with Notion's API. * @example * // Use with a string * const simpleCallout = block.callout.createBlock("I though I told you never to come in here, McFly!"); * * // Use with options object * const complexCallout = block.callout.createBlock({ * rich_text: "Now make like a tree and get outta here.", * icon: "💡", * color: "blue_background", * children: [ * // Child blocks would go here * ] * }); */ createBlock: (options) => { let rich_text, icon2, children, color; if (typeof options === "string" || Array.isArray(options)) { rich_text = options; icon2 = ""; children = []; color = "default"; } else { ({ rich_text = [], icon: icon2 = "", children = [], color = "default" } = options); } return { type: "callout", callout: { rich_text: enforceRichText(rich_text), icon: setIcon(icon2), color, ...children.length > 0 && { children } } }; } }, /** * Methods for code blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ code: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates a code block. * * @function * @param {string|string[]|Object} options - A string or array of strings representing the code content, or an options object. * @param {string|string[]|Array<Object>} [options.rich_text=[]] - The code content as a string, an array of strings, or an array of rich text objects. * @param {string|string[]|Array<Object>} [options.caption=[]] - The caption as a string, an array of strings, or an array of rich text objects. * @param {string} [options.language="plain text"] - Programming language of the code block. * @returns {Object} A code block object compatible with Notion's API. * @example * // Use with a string * const simpleCode = block.code.createBlock("console.log('Give me all the bacon and eggs you have.');"); * * // Use with an array of strings (multi-line snippet) * const multiLineCode = block.code.createBlock([ * "let counter = 0;", * "counter += 1;" * ]); * * // Use with options object * const complexCode = block.code.createBlock({ * rich_text: "const name = 'Monkey D. Luffy'\n console.log(`My name is ${name} and I will be king of the pirates!`)", * language: "JavaScript", * caption: "A simple JavaScript greeting function" * }); */ createBlock: (options) => { let rich_text, caption, language; if (typeof options === "string" || Array.isArray(options)) { rich_text = options; caption = []; language = "plain text"; } else { ({ rich_text = [], caption = [], language = "plain text" } = options); } return { type: "code", code: { rich_text: enforceRichText(rich_text), caption: enforceRichText(caption), language } }; } }, /** * Methods for column list blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ column_list: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: true, /** * Creates a column list block. Column list blocks must have at least two column child blocks, each of which must have at least one child block - otherwise, the Notion API will throw an error. * * @see https://developers.notion.com/reference/block#column-list-and-column * * @param {(number|Array|undefined)} options - The options for creating the column list block. * @param {number} [options] - The number of columns to create. Each will contain an empty paragraph block. * @param {Array} [options] - An array where each element represents a column. Each element can be: * - A column object (will be added to the final column list's children array directly) * - A string (will create a column object containing a paragraph block with the string's value) * - An array (will create a column object containing the elements as children. Each element should be a string or a valid block object. Strings will be converted to paragraph blocks.) * * @returns {Object|null} The created column list block object, or null if invalid input is provided. * * @example * // Create a column list with 3 empty columns * const emptyColumns = block.column_list.createBlock(3); * * @example * // Create a column list with 2 columns, each containing a paragraph * const twoColumnList = block.column_list.createBlock(["First column", "Second column"]); * * @example * // Create a column list with mixed content * const mixedColumnList = block.column_list.createBlock([ * "Text in first column", * ["Paragraph 1 in second column", "Paragraph 2 in second column"], * { * type: "column", * column: {}, * children: [block.heading_2.createBlock("Custom heading in third column")] * } * ]); * * @example * // Create an empty column list * const emptyColumnList = block.column_list.createBlock(); */ createBlock: (options) => { if (typeof options === "string" || typeof options === "function") { return null; } if (!options || Array.isArray(options) && options.length === 0 || typeof options === "object" && Object.keys(options).length < 1) { return { type: "column_list", column_list: {} }; } if (typeof options === "number") { let columns = []; for (let i = 0; i < options; i++) { const column2 = { type: "column", column: { children: [block.paragraph.createBlock("")] } }; columns.push(column2); } return { type: "column_list", column_list: { children: columns } }; } if (Array.isArray(options)) { let columns = []; for (let option of options) { if (typeof option === "object" && option.hasOwnProperty("column")) { columns.push(option); } if (Array.isArray(option)) { let blocks = []; for (let singleBlock of option) { if (typeof singleBlock === "string") { const paragraph2 = block.paragraph.createBlock(singleBlock); blocks.push(paragraph2); } else if (typeof singleBlock === "object" && singleBlock !== null && !singleBlock.hasOwnProperty("column")) { blocks.push(singleBlock); } } const column2 = { type: "column", column: { children: blocks } }; columns.push(column2); } if (typeof option === "string") { const column2 = { type: "column", column: { children: [block.paragraph.createBlock(option)] } }; columns.push(column2); } if (!Array.isArray(option) && typeof option === "object" && !option.hasOwnProperty("column")) { const column2 = { type: "column", column: { children: [option] } }; columns.push(column2); } } return { type: "column_list", column_list: { children: columns } }; } } }, /** * Methods for column blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ column: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: true, /** * Creates a column block. A column block must have at least one non-column block in its children array, and the column block itself must be appended as a child block to a column_list block. * * @see https://developers.notion.com/reference/block#column-list-and-column * * @param {(Array|string|undefined)} options - The options for creating the column block. * @param {Array} [options] - An array where each element becomes a child block of the column. Each element should be: * - A block object (will be added directly to the column's children array) * - A string (will be converted to a paragraph block) * @param {string} [options] - A string that will be converted into a paragraph block within the column. * * @returns {Object} The created column block object. * * @example * // Create an empty column * const emptyColumn = block.column.createBlock(); * * @example * // Create a column with a single paragraph * const singleParagraphColumn = block.column.createBlock("This is a paragraph in a column"); * * @example * // Create a column with multiple blocks * const multiBlockColumn = block.column.createBlock([ * "First paragraph", * block.heading_2.createBlock("Heading in column"), * "Another paragraph" * ]); * * @example * // Create a column with custom blocks * const customBlocksColumn = block.column.createBlock([ * block.paragraph.createBlock("Custom paragraph"), * block.bulleted_list_item.createBlock("Bullet point in column"), * block.image.createBlock({ url: "https://example.com/image.jpg" }) * ]); */ createBlock: (options) => { if (!options || Array.isArray(options) && options.length === 0 || typeof options === "object" && Object.keys(options).length < 1) { return { type: "column", column: { children: [] } }; } if (typeof options === "string") { return { type: "column", column: { children: [block.paragraph.createBlock(options)] } }; } if (Array.isArray(options)) { let blocks = []; for (let singleBlock of options) { if (typeof singleBlock === "string") { const paragraph2 = block.paragraph.createBlock(singleBlock); blocks.push(paragraph2); } else if (typeof singleBlock === "object" && singleBlock !== null && !singleBlock.hasOwnProperty("column")) { blocks.push(singleBlock); } } return { type: "column", column: { children: blocks } }; } } }, /** * Methods for divider blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ divider: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates a divider block. * * @function * @returns {Object} A divider block object compatible with Notion's API. * @example * const divider = block.divider.createBlock(); */ createBlock: () => ({ type: "divider", divider: {} }) }, /** * Methods for embed blocks. * * @namespace * @property {boolean} supports_children - Indicates if the block supports child blocks. */ embed: { /** * Indicates if the block supports child blocks. * @type {boolean} */ supports_children: false, /** * Creates an embed block. * * @function * @param {string|Object} options - A string representing the URL to be embedded, or an options object. * @param {string} options.url - The URL to be embedded. * @returns {Object} An embed block object compatible with Notion's API. * @example * // Use with a string * const simpleEmbed = block.embed.createBlock("https://www.youtube.com/watch?v=ec5m6t77eYM"); * * // Use with options object * const complexEmbed = block.embed.createBlock({ * url: "https://www.youtube.com/watch?v=ec5m6t77eYM" * }); */ createBlock: (options) => { const url2 = typeof options === "string" ? options : options.url; return { type: "embed", embed: { url: url2 } }; } }, /** * Methods for file blocks. * * @namespace * @prop