import type { MarkdownIt, Token } from "markdown-it"; const YOUTUBE_RE = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[?&]\S*)?$/; const TWEET_RE = /^https?:\/\/(?:www\.)?(?:x|twitter)\.com\/[^/]+\/status\/\d+/; // A lone image is the shape Obsidian embeds with; anything alongside it is a real image. function loneImage(inline: Token): Token | null { const only = inline.children?.length === 1 ? inline.children[0] : null; return only?.type === "image" ? only : null; } // videoId is regex-bound to 11 url-safe chars, so the interpolated iframe src can't break out. function iframeHtml(videoId: string): string { return `
\n`; } // Obsidian embeds a video with the image form, ``; a bare URL alone // in a paragraph is Lichen's own syntax. Both mean the same thing, so both become the // iframe. Tweets deliberately become a plain link instead: an x.com embed is a third-party // frame, and `frame-src` (src/lib/response.ts) allows youtube-nocookie only. export function embedPlugin(mdi: MarkdownIt): void { mdi.core.ruler.push("embed", (state) => { const tokens = state.tokens; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token?.type !== "paragraph_open") continue; const inline = tokens[i + 1]; const close = tokens[i + 2]; if (inline?.type !== "inline" || close?.type !== "paragraph_close") continue; const image = loneImage(inline); const url = String(image?.attrGet("src") ?? inline.content.trim()); const match = YOUTUBE_RE.exec(url); if (match) { const htmlToken = new state.Token("html_block", "", 0); htmlToken.content = iframeHtml(match[1] as string); tokens.splice(i, 3, htmlToken); // p_open + inline + p_close → html_block continue; } if (image && TWEET_RE.test(url)) { const linkOpen = new state.Token("link_open", "a", 1); linkOpen.attrSet("href", url); const text = new state.Token("text", "", 0); text.content = url; inline.children = [ linkOpen, text, new state.Token("link_close", "a", -1), ]; } } }); }