|
| 1 | +const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "svg", "avif"] as const; |
| 2 | +const VIDEO_EXTENSIONS = ["mp4", "webm", "mov", "m4v", "ogv"] as const; |
| 3 | +const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "flac"] as const; |
| 4 | + |
| 5 | +export type MediaKind = "image" | "video" | "audio"; |
| 6 | + |
| 7 | +export function classifyMediaUrl(url: string | undefined | null): MediaKind | null { |
| 8 | + if (!url) return null; |
| 9 | + let pathname: string; |
| 10 | + try { |
| 11 | + pathname = new URL(url, "https://placeholder.local").pathname; |
| 12 | + } catch { |
| 13 | + return null; |
| 14 | + } |
| 15 | + const lower = pathname.toLowerCase(); |
| 16 | + const ext = lower.split(".").pop(); |
| 17 | + if (!ext) return null; |
| 18 | + if ((IMAGE_EXTENSIONS as readonly string[]).includes(ext)) return "image"; |
| 19 | + if ((VIDEO_EXTENSIONS as readonly string[]).includes(ext)) return "video"; |
| 20 | + if ((AUDIO_EXTENSIONS as readonly string[]).includes(ext)) return "audio"; |
| 21 | + return null; |
| 22 | +} |
| 23 | + |
| 24 | +export interface MediaMatch { |
| 25 | + url: string; |
| 26 | + kind: MediaKind; |
| 27 | +} |
| 28 | + |
| 29 | +const IMAGE_MARKDOWN = /!\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/; |
| 30 | +const LINK_MARKDOWN = /\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/g; |
| 31 | +const BARE_URL = /(https?:\/\/[^\s)]+)/g; |
| 32 | + |
| 33 | +export function extractFirstMedia(markdown: string | undefined | null): MediaMatch | null { |
| 34 | + if (!markdown) return null; |
| 35 | + |
| 36 | + const img = markdown.match(IMAGE_MARKDOWN); |
| 37 | + if (img?.[1]) { |
| 38 | + const kind = classifyMediaUrl(img[1]) ?? "image"; |
| 39 | + return { url: img[1], kind }; |
| 40 | + } |
| 41 | + |
| 42 | + for (const match of markdown.matchAll(LINK_MARKDOWN)) { |
| 43 | + const kind = classifyMediaUrl(match[1]); |
| 44 | + if (kind) return { url: match[1], kind }; |
| 45 | + } |
| 46 | + |
| 47 | + for (const match of markdown.matchAll(BARE_URL)) { |
| 48 | + const kind = classifyMediaUrl(match[1]); |
| 49 | + if (kind) return { url: match[1], kind }; |
| 50 | + } |
| 51 | + |
| 52 | + return null; |
| 53 | +} |
| 54 | + |
| 55 | +export function stripMarkdown(markdown: string, maxLength = 180): string { |
| 56 | + const withoutImages = markdown.replace(/!\[[^\]]*\]\([^)]+\)/g, ""); |
| 57 | + const withoutLinks = withoutImages.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); |
| 58 | + const withoutMarks = withoutLinks |
| 59 | + .replace(/[#>*_`~]+/g, " ") |
| 60 | + .replace(/\s+/g, " ") |
| 61 | + .trim(); |
| 62 | + if (withoutMarks.length <= maxLength) return withoutMarks; |
| 63 | + return `${withoutMarks.slice(0, maxLength).trimEnd()}…`; |
| 64 | +} |
0 commit comments