notion-helper
Version:
A library of functions for working more easily with the Notion API
1,535 lines (1,531 loc) • 190 kB
JavaScript
// 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