import { parseDocument } from "yaml"; import { assertPageIcon, type PageIcon } from "../notion-as-code/page-icon.js"; export type AppPageContent = { content?: string; customBlockKey?: string; icon?: PageIcon; }; /** Read APP.md metadata without changing the Markdown body. */ export function parseAppPage(markdown: string): AppPageContent { const opening = /^(?:\uFEFF)?---[ \t]*\r?\n/u.exec(markdown); if (!opening) { return { content: markdown }; } const remainder = markdown.slice(opening[0].length); const closing = /^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/mu.exec(remainder); if (!closing) { throw new Error("APP.md frontmatter must end with --- or ... on its own line."); } const content = remainder.slice(closing.index + closing[0].length); const metadata = readFrontmatterMetadata(remainder.slice(0, closing.index)); if (metadata.customBlockKey !== undefined) { if (content.trim()) { throw new Error("APP.md cannot have body content when custom_block is set."); } return metadata; } return { content, ...metadata }; } function readFrontmatterMetadata(source: string): Omit { try { const document = parseDocument(source, { schema: "core" }); const error = document.errors[0] ?? document.warnings[0]; if (error) { throw error; } // Frontmatter needs no aliases; reject them to prevent expansion of untrusted YAML. const metadata: unknown = document.toJS({ maxAliasCount: 0 }); if (metadata === null) { return {}; } if (typeof metadata !== "object" || Array.isArray(metadata)) { throw new Error("Metadata must be a YAML mapping."); } const result: Omit = {}; if ("custom_block" in metadata) { if (typeof metadata.custom_block !== "string" || !metadata.custom_block.trim()) { throw new Error("custom_block must be a non-empty string."); } result.customBlockKey = metadata.custom_block; } if (!("icon" in metadata)) { return result; } const icon = typeof metadata.icon === "string" ? { type: "emoji", emoji: metadata.icon } : metadata.icon; assertPageIcon(icon); return { ...result, icon }; } catch (error) { throw new Error( `Invalid APP.md frontmatter: ${error instanceof Error ? error.message : String(error)}`, { cause: error }, ); } }