import { escapeHtml } from './utils'; import { ImageIcon, VideoIcon } from './constants/icons'; export interface MediaBlock { type: 'image' | 'video' | 'image-placeholder' | 'video-placeholder'; html: string; startLine: number; endLine: number; } export interface MediaEmbedResult { blocks: MediaBlock[]; processedLines: Set; } const IMAGE_REGEX = /^\[image\(([^)]+)\)\]$/; const VIDEO_REGEX = /^\[youtube\(([^)]+)\)\]$/; const IMAGE_PLACEHOLDER_REGEX = /^<]+)>>$/; const VIDEO_PLACEHOLDER_REGEX = /^<]+)>>$/; const IFRAME_ATTRS = [ 'width="100%"', 'height="100%"', 'frameborder="0"', 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"', 'referrerpolicy="strict-origin-when-cross-origin"', 'allowfullscreen', ].join(' '); export function parseMediaEmbeds(lines: string[]): MediaEmbedResult { const blocks: MediaBlock[] = []; const processedLines = new Set(); lines.forEach((line, i) => { const trimmed = line.trim(); const imageMatch = trimmed.match(IMAGE_REGEX); if (imageMatch) { blocks.push(createMediaBlock('image', imageMatch[1], i)); processedLines.add(i); return; } const videoMatch = trimmed.match(VIDEO_REGEX); if (videoMatch) { blocks.push(createMediaBlock('video', videoMatch[1], i)); processedLines.add(i); return; } const imagePlaceholderMatch = trimmed.match(IMAGE_PLACEHOLDER_REGEX); if (imagePlaceholderMatch) { blocks.push(createPlaceholderBlock('image-placeholder', imagePlaceholderMatch[1], i)); processedLines.add(i); return; } const videoPlaceholderMatch = trimmed.match(VIDEO_PLACEHOLDER_REGEX); if (videoPlaceholderMatch) { blocks.push(createPlaceholderBlock('video-placeholder', videoPlaceholderMatch[1], i)); processedLines.add(i); } }); return { blocks, processedLines }; } function createMediaBlock( type: 'image' | 'video', paramString: string, lineIndex: number ): MediaBlock { const params = parseParams(paramString); const html = type === 'video' ? createVideoHtml(params) : createImageHtml(params); return { type, html, startLine: lineIndex, endLine: lineIndex, }; } function createVideoHtml(params: Record): string { const { id: videoId, width = '100%', privacy, start, end } = params; if (!videoId) { return createErrorHtml('video'); } const domain = privacy === 'false' ? 'youtube.com' : 'youtube-nocookie.com'; let url = `https://www.${domain}/embed/${videoId}`; const urlParams = []; if (start) urlParams.push(`start=${start}`); if (end) urlParams.push(`end=${end}`); if (urlParams.length) url += `?${urlParams.join('&')}`; const style = width !== '100%' ? ` style="width: ${width}"` : ''; return `
${VideoIcon} Loading
`; } function createImageHtml(params: Record): string { const { id: url, alt = '', width, height } = params; if (!url) { return createErrorHtml('image'); } const styles = buildStyles({ width: width || '100%', height }); return `
${escapeHtml(alt)}
${ImageIcon} Loading
`; } function parseParams(paramString: string): Record { const [id, ...pairs] = paramString.split(',').map(s => s.trim()); const params: Record = { id }; pairs.forEach(pair => { const [key, value] = pair.split('=').map(s => s.trim()); if (key && value) params[key] = value; }); return params; } function buildStyles(dimensions: { width?: string; height?: string }): string { return Object.entries(dimensions) .filter(([, value]) => value) .map(([prop, value]) => `${prop}: ${value.match(/px|%/) ? value : value + 'px'}`) .join('; '); } function createErrorHtml(type: 'video' | 'image'): string { const icon = type === 'video' ? VideoIcon : ImageIcon; return `
${icon} Failed to load ${type}
`; } function createPlaceholderBlock( type: 'image-placeholder' | 'video-placeholder', query: string, lineIndex: number ): MediaBlock { const isVideo = type === 'video-placeholder'; const icon = isVideo ? VideoIcon : ImageIcon; const mediaType = isVideo ? 'video' : 'image'; const placeholderId = generatePlaceholderId(type, query); const html = `
${icon} Loading
`; return { type, html, startLine: lineIndex, endLine: lineIndex, }; } function generatePlaceholderId(type: string, query: string): string { const prefix = type === 'video-placeholder' ? 'video-placeholder' : 'image-placeholder'; const hash = simpleHash(query); return `${prefix}-${hash}`; } function simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash).toString(36); }