/** * Markdown rewrites for export (issue #65, ADR 0009). A page's cached Markdown * stores images as `![alt]()` and wikilinks as `[[slug]]` / * `[[slug|text]]`; neither is portable as-is, so each export target rewrites * them: the pond ZIP to relative files, an office document to inline data. */ // A wikilink token — `[[slug]]` or `[[slug|display text]]`. Only real wikilink // nodes serialize this way (literal brackets in text are escaped), so matching // the raw token is safe. const WIKILINK = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; // A Markdown image — `![alt](src)`. In cached page Markdown `src` is always a // bare attachment id (no parentheses), so the group is unambiguous. const IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; /** The image attachment ids referenced by a page's Markdown, in order. */ export function imageFileIds(markdown: string): string[] { const ids: string[] = []; for (const match of markdown.matchAll(IMAGE)) ids.push(match[2]!); return ids; } /** * Rewrite for the pond ZIP: a wikilink becomes a relative link to the target * page's `.md` file when that page is in the export (readable), else its plain * display text; an image source becomes a relative path into `media/`. */ export function markdownForZip( markdown: string, readableSlugs: Set, mediaNameById: Map, ): string { return markdown .replace(WIKILINK, (_whole, slug: string, text?: string) => { const label = (text ?? slug).trim(); return readableSlugs.has(slug) ? `[${label}](${encodeURIComponent(slug)}.md)` : label; }) .replace(IMAGE, (whole, alt: string, src: string) => { const name = mediaNameById.get(src); return name ? `![${alt}](media/${name})` : whole; }); } /** * Rewrite for a standalone office document: a wikilink becomes plain display * text (there is no target document to link to), and an image source becomes an * inline `data:` URI so pandoc embeds the bytes. */ export function markdownForDocument(markdown: string, dataUriById: Map): string { return markdown .replace(WIKILINK, (_whole, slug: string, text?: string) => (text ?? slug).trim()) .replace(IMAGE, (whole, alt: string, src: string) => { const uri = dataUriById.get(src); return uri ? `![${alt}](${uri})` : whole; }); } const EXTENSION_BY_MIME: Readonly> = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', }; /** File extension for an attachment's stored bytes, from its MIME type. */ export function imageExtension(mimeType: string): string { return EXTENSION_BY_MIME[mimeType] ?? 'bin'; }