/*** EXPORT ------------------------------------------- ***/ export default (input: string): string => { const codeBlocks: { content: string; index: number; language: string; }[] = []; const codeBlockPattern = /```(\w*)[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/g; const inlineCode: string[] = []; const inlineCodePattern = /`([^`]+)`/g; const refPattern = /\s*\[(\w+)\]\s+<([^>]+)>$/gm; const refs = new Map(); /*** extract and replace code blocks with placeholders ***/ let processed = input.replace(codeBlockPattern, (_match, language, content) => { const index = codeBlocks.length; codeBlocks.push({ content: content.trimEnd(), index, language: language || "" }); return ``; }); /*** extract inline code ***/ processed = processed.replace(inlineCodePattern, (_match, content) => { const index = inlineCode.length; inlineCode.push(content); return ``; }); /*** extract reference definitions (images/links) ***/ for (const match of processed.matchAll(refPattern)) { const [, ref, url] = match; refs.set(ref, url); } /*** remove reference definitions before blockquote processing ***/ processed = processed.replace(refPattern, ""); /*** process blockquotes ***/ processed = processBlockquotes(processed); processed = processed .replace(/(\n){6}/g, "") /*** remove 6 lines from the top of the memo ***/ .replace("References", "") /*** remove reference to reference definitions from output ***/ .replace(/(\*\*|__)(.*?)\1/g, `$2`) /*** bold ***/ .replace(/(\*|_)(.*?)\1/g, "$2") /*** italic ***/ .replace(/(\~\~)(.*?)\1/g, `$2`) /*** strikethrough ***/ .replace(/'/g, "’") /*** fancy apostrophe ***/ .replace(/(:nbhyp:)/g, "‑") /*** non‑breaking hyphen ***/ // deno-lint-ignore no-irregular-whitespace .replace(/(:nbsp:)/g, " ") /*** non‑breaking space ***/ .replace(/(\.\.\.)/g, "…") /*** ellipsis ***/ .replace(/---/g, "
") .replace(/📸\[([^\]]+)\]\[(\w+)\]/g, (match, alt, ref) => { const url = refs.get(ref); return url ? `${alt}` : match; }) .replace(/📼\[([^\]]+)\]\[(\w+)\]/g, (match, alt, ref) => { const url = refs.get(ref); return url ? `` : match; }) .replace(/\[(\w+)\](?!\s+<|\()/g, (match, ref) => { const url = refs.get(ref); const isExternal = url && url.startsWith("http://") || url && url.startsWith("https://"); return url ? isExternal ? `[${ref}]` : `[${ref}]` : match; }) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => { const isExternal = url.startsWith("http://") || url.startsWith("https://"); return isExternal ? `${text}` : `${text}`; }); /*** restore inline code ***/ for (const block in inlineCode) { processed = processed.replace(``, `${escapeHtml(inlineCode[block])}`); } /*** restore code blocks ***/ for (const block of codeBlocks) { const langAttr = block.language ? ` data-language="${block.language}"` : ""; const html = `
\n${escapeHtml(block.content)}\n\n
`; processed = processed.replace(``, html); } // console.log(processed); /*** for troubleshooting ***/ return processed.trimEnd(); } /*** HELPER ------------------------------------------- ***/ function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function processBlockquotes(input: string): string { const lines = input.split("\n"); const result: string[] = []; let quoteBuffer: string[] = []; const flushQuote = () => { if (quoteBuffer.length > 0) { /*** special indentation to fit memo/remark formatting ***/ result.push(`
\n ${quoteBuffer.join("\n ")}\n\n
`); quoteBuffer = []; // TODO // : handle links within blockquotes (WM-032) } }; for (const line of lines) { /*** preserve code block placeholders ***/ if (line.includes("