/** * Shortcode + auto-URL-embed preprocessing for the rich (content) * composition. Moved verbatim from the old RichMarkdownRenderer — the * `{{youtube:...}}` / `{% youtube %}` / thumbnail-link / auto-URL grammar * is authored-content SSOT; chat surfaces never run this. */ export const processShortcodes = (content: string): string => { let processedContent = content; // Escape values interpolated into the raw HTML `data-*` attributes generated below. // With rehypeRaw enabled, an unescaped `"`/`<`/`>` in a URL or id could break out of // the attribute and inject markup, so every interpolated embed value goes through this. const escapeAttr = (value: string) => value .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); // First, process explicit shortcodes processedContent = processedContent // YouTube embeds: {{youtube:VIDEO_ID}} .replace(/\{\{youtube:([^}]+)\}\}/g, (match, videoId) => { return `\n\n
\n\n`; }) // Markdoc-style YouTube: {% youtube id="VIDEO_ID" /%} or {% youtube id="VIDEO_ID" title="..." /%} .replace(/\{%\s*youtube\s+id="([^"]+)"(?:\s+title="[^"]*")?\s*\/?%\}/g, (match, videoId) => { return `\n\n\n\n`; }) /** * SHORTCODE: YouTube Thumbnail Link (RECOMMENDED - GitHub + Flamingo Compatible) * * SYNTAX: [](https://www.youtube.com/watch?v=VIDEO_ID) * - On GitHub: renders as a clickable thumbnail image linking to YouTube * - On Flamingo: converts to a full embedded YouTube player * Use this format for docs that must work on BOTH GitHub and Flamingo; * {{youtube:ID}} / {% youtube id="ID" /%} are Flamingo-only. */ .replace(/\[!\[([^\]]*)\]\(https?:\/\/img\.youtube\.com\/vi\/([a-zA-Z0-9_-]+)\/[^)]+\)\]\(https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)[^)]*\)/g, (match, altText, thumbId, videoId) => { return `\n\n\n\n`; }) // Reddit embeds: {{reddit:POST_URL}} .replace(/\{\{reddit:([^}]+)\}\}/g, (match, urlOrId) => { const postUrl = urlOrId.trim(); const fullUrl = postUrl.startsWith('http') ? postUrl : `https://reddit.com/r/${postUrl}`; return `\n\n\n\n`; }) // Twitter/X embeds: {{tweet:TWEET_URL}} or {{twitter:TWEET_URL}} .replace(/\{\{(?:tweet|twitter):([^}]+)\}\}/g, (match, urlOrId) => { const tweetInput = urlOrId.trim(); const tweetUrl = tweetInput.startsWith('http') ? tweetInput : `https://twitter.com/twitter/status/${tweetInput}`; return `\n\n\n\n`; }) // Figma by file key (spec grammar): {{figma:FILE_KEY[:NODE_ID]}} → rewritten to the // canonical URL form so the {{figma:URL}} rule below owns the rendering. .replace(/\{\{figma:(?!https?:)([A-Za-z0-9]+)(?::([0-9]+[-:][0-9]+))?\}\}/g, (match, key, node) => { const nodeParam = node ? `?node-id=${String(node).replace(':', '-')}` : ''; return `{{figma:https://www.figma.com/design/${key}${nodeParam}}}`; }) // Figma embeds: {{figma:URL}} .replace(/\{\{figma:([^}]+)\}\}/g, (match, url) => { return `\n\n\n\n`; }) // Claude artifact / Claude Design: {{claude-artifact:URL}} / {{claude-design:URL}} // — the same shape as figma, so a Claude link is a markdown BLOCK wherever // markdown renders, never a bespoke card one surface hand-assembles. // `{{claude-artifact:URL}}` or `{{claude-artifact:URL|Name}}` — the optional // name after the pipe is the ONLY source of a real title (claude.ai serves // one og:title for every artifact), so the block carries it through. .replace(/\{\{claude-(artifact|design):([^|}]+)(?:\|([^}]*))?\}\}/g, (match, kind, url, title) => { const titleAttr = title && title.trim() ? ` data-title="${escapeAttr(title.trim())}"` : ''; return `\n\n\n\n`; }) // LinkedIn embeds: {{linkedin:POST_URL}} .replace(/\{\{linkedin:([^}]+)\}\}/g, (match, url) => { return `\n\n\n\n`; }) // Link previews: {{link:URL}} .replace(/\{\{link:([^}]+)\}\}/g, (match, url) => { return `\n\n\n\n`; }); // Next, auto-detect standalone URLs (but NOT those already in markdown links or code blocks) // Step 1: Temporarily replace code blocks to protect them const codeBlocks: string[] = []; processedContent = processedContent.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => { const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; codeBlocks.push(match); return placeholder; }); // Step 1.5: Strip well-formed `` elements from prose. // Pasted social embed markup ships a loader script alongside its markup // (Reddit: `…` + embed.reddit.com // widgets.js; Twitter: platform.twitter.com widgets.js). Scripts NEVER // execute on this surface — the sanitize stack strips real ones — but the // engine's text pre-pass escapes the tag first, so it used to render as // VISIBLE source text under every embed. Runs after Step 1 so script tags // inside code fences / inline code stay literal sample code; an UNCLOSED // opener is left alone (falls through to today's escape-to-text behavior, // which is the safe degrade). Quantifiers are hard-bounded (ReDoS). processedContent = processedContent.replace( /