diff --git a/GUI/src/components/Markdowify/index.tsx b/GUI/src/components/Markdowify/index.tsx index c98f1240..e4b19252 100644 --- a/GUI/src/components/Markdowify/index.tsx +++ b/GUI/src/components/Markdowify/index.tsx @@ -7,6 +7,27 @@ interface MarkdownifyProps { sanitizeLinks?: boolean; } +const isValidImageUrl = (s: string): boolean => { + try { + const u = new URL(s); + if (!/^(https?|data):$/i.test(u.protocol)) return false; + if (s.startsWith('data:image/')) { + return /^data:image\/(png|jpe?g|gif|webp|svg\+xml|bmp);(base64,|charset=utf-8;)/i.test(s); + } + const path = u.pathname.toLowerCase(); + const search = u.search.toLowerCase(); + if (/\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff?|avif|heic|heif|apng)([?#]|$)/i.test(path)) { + return true; + } + if (/\/(images?|img|photos?|pictures?|media|uploads?|thumb|avatar)\//i.test(path)) { + return true; + } + return /[?&](format|type|image|img|photo)=(png|jpe?g|gif|webp|svg|ico)/i.test(search); + } catch { + return false; + } +}; + const LinkPreview: React.FC<{ href: string; children: React.ReactNode; @@ -27,6 +48,14 @@ const LinkPreview: React.FC<{ ); } + if (!isValidImageUrl(href)) { + return ( + + {children} + + ); + } + return hasError ? ( {children} @@ -53,10 +82,22 @@ function formatMessage(message?: string): string { .replaceAll(/\\?\$v\w*/g, '') .replaceAll(/\\?\$g\w*/g, ''); - return filteredMessage - .replaceAll(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) + const dataImagePattern = /((?:^|\s))(data:image\/[a-z0-9+]+;[^)\s]+)/gi; + const finalMessage = filteredMessage.replaceAll( + dataImagePattern, + (_, prefix, dataUrl) => `${prefix}[image](${dataUrl})`, + ); + + return finalMessage + .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) + .replaceAll('&', '&') + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") .replaceAll(/(^|\n)(\d{4})\.\s/g, (match, prefix, year) => { - const remainingText = filteredMessage.substring(filteredMessage.indexOf(match) + match.length); + const remainingText = finalMessage.substring(finalMessage.indexOf(match) + match.length); const sentenceEnd = remainingText.indexOf('\n\n'); if (sentenceEnd !== -1) { const currentSentence = remainingText.substring(0, sentenceEnd); @@ -66,7 +107,8 @@ function formatMessage(message?: string): string { } return `${prefix}${year}\\. `; }) - .replace(/(?<=\n)\d+\.\s/g, hasSpecialFormat(filteredMessage) ? '\n\n$&' : '$&'); + .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(finalMessage) ? '\n\n$&' : '$&') + .replaceAll(/^(\s+)/g, (match) => match.replaceAll(' ', ' ')); } const Markdownify: React.FC = ({ message, sanitizeLinks = false }) => (