import type { Note } from '@/lib/types' const MD_IMG = /!\[[^\]]*\]\(([^)\s]+)\)/g const HTML_IMG = /]+src=["']([^"']+)["'][^>]*>/gi /** SSR-safe HTML entity decode so excerpt text matches server and client. */ function decodeHtmlEntities(text: string): string { return text .replace(/ /gi, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/(?:�*39;|')/g, "'") .replace(/&#(\d+);/g, (_, code: string) => { const n = Number(code) return n > 0 && n < 0x110000 ? String.fromCodePoint(n) : `&#${code};` }) .replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => { const n = parseInt(hex, 16) return n > 0 && n < 0x110000 ? String.fromCodePoint(n) : `&#x${hex};` }) } /** * Plain-text preview for list view (light markdown stripping). */ export function stripMarkdownPreview(raw: string, maxLen = 180): string { if (!raw?.trim()) return '' const t = raw .replace(/^#{1,6}\s+/gm, '') .replace(/```[\s\S]*?```/g, ' ') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/\[(.+?)]\([^)]+\)/g, '$1') .replace(/^\s*[-*+]\s+/gm, '') .replace(/^\s*\d+\.\s+/gm, '') .replace(/\n+/g, ' ') .replace(/\s+/g, ' ') .trim() if (t.length > maxLen) { return `${t.slice(0, maxLen).trim()}…` } return t } export function getNoteDisplayTitle(note: { title: string | null; content: string; type: string }, untitled: string): string { const title = note.title?.trim() if (title) return title if (note.type === 'checklist') { const line = note.content?.split('\n').find((l) => l.trim()) if (line) return stripMarkdownPreview(line, 80) || untitled } const preview = stripMarkdownPreview(note.content || '', 100) return preview || untitled } export function getNoteFeedImage(note: Note): string | null { const first = note.images?.[0]?.trim() if (first) return first const content = note.content || '' MD_IMG.lastIndex = 0 const md = MD_IMG.exec(content) if (md?.[1]) return md[1] HTML_IMG.lastIndex = 0 const html = HTML_IMG.exec(content) if (html?.[1]) return html[1] return null } /** Plain excerpt for collection cards (HTML + markdown noise stripped). */ export function getNotePlainExcerpt(note: Note, maxLen = 240): string { let raw = note.content || '' raw = raw.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ') raw = raw.replace(/<[^>]+>/g, ' ') raw = decodeHtmlEntities(raw) return stripMarkdownPreview(raw, maxLen) } /** Adapte un SVG miniature (viewBox 4:3 liste) pour remplir une vignette carte 16:10. */ export function prepareNoteIllustrationForGrid(svg: string): string { return svg.replace(/]*)>/i, (_, attrs: string) => { let next = attrs.replace(/\s(width|height)=["'][^"']*["']/gi, '') if (/preserveAspectRatio=/i.test(next)) { next = next.replace(/preserveAspectRatio=["'][^"']*["']/i, 'preserveAspectRatio="xMidYMid slice"') } else { next += ' preserveAspectRatio="xMidYMid slice"' } return `` }) }