import type { PreviewTarget } from '@/store/preview' const HTML_EXTENSIONS = new Set(['.htm', '.html']) const IMAGE_EXTENSIONS = new Set(['.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp']) const LANGUAGE_BY_EXT: Record = { '.c': 'c', '.conf': 'ini', '.cpp': 'cpp', '.css': 'css', '.csv': 'csv', '.go': 'go', '.graphql': 'graphql', '.h': 'c', '.hpp': 'cpp', '.html': 'html', '.java': 'java', '.js': 'javascript', '.json': 'json', '.jsx': 'jsx', '.log': 'text', '.lua': 'lua', '.md': 'markdown', '.mjs': 'javascript', '.py': 'python', '.rb': 'ruby', '.rs': 'rust', '.sh': 'shell', '.sql': 'sql', '.svg': 'xml', '.toml': 'toml', '.ts': 'typescript', '.tsx': 'tsx', '.txt': 'text', '.xml': 'xml', '.yaml': 'yaml', '.yml': 'yaml', '.zsh': 'shell' } function basename(value: string) { return value.split(/[\\/]/).filter(Boolean).pop() || value } function extension(value: string) { const clean = value.split(/[?#]/, 1)[0] || value const idx = clean.lastIndexOf('.') return idx >= 0 ? clean.slice(idx).toLowerCase() : '' } function joinPath(base: string, rel: string) { if (!base) {return rel} return `${base.replace(/\/+$/, '')}/${rel.replace(/^\.?\//, '')}` } function pathToFileUrl(path: string) { const encoded = path.split('/').map(part => encodeURIComponent(part)).join('/') return `file://${encoded.startsWith('/') ? encoded : `/${encoded}`}` } export function localPreviewTarget(rawTarget: string, cwd?: string | null): PreviewTarget | null { const raw = rawTarget.trim().replace(/^`|`$/g, '') if (!raw) {return null} if (/^https?:\/\//i.test(raw)) { return { kind: 'url', label: basename(raw), source: raw, url: raw } } let path = raw if (/^file:\/\//i.test(raw)) { try { path = decodeURIComponent(new URL(raw).pathname) } catch { path = raw.replace(/^file:\/\//i, '') } } else if (!raw.startsWith('/') && cwd) { path = joinPath(cwd, raw) } const ext = extension(path) const isHtml = HTML_EXTENSIONS.has(ext) const isImage = IMAGE_EXTENSIONS.has(ext) return { kind: 'file', label: basename(path), language: LANGUAGE_BY_EXT[ext] || 'text', path, // Renderer fallback can't stat/sniff without reading; assume text unless // image/html extension says otherwise. LocalFilePreview still guards // binary/large files when readFileText/readFileDataUrl returns metadata. previewKind: isHtml ? 'html' : isImage ? 'image' : 'text', source: raw, url: pathToFileUrl(path) } } export async function normalizeOrLocalPreviewTarget(rawTarget: string, cwd?: string | null): Promise { try { const normalized = await window.hermesDesktop?.normalizePreviewTarget?.(rawTarget, cwd || undefined) if (normalized) { return normalized } } catch { // Running Electron may still have the old HTML-only preview IPC. Fall // through to renderer-side local classification so text/images still open. } return localPreviewTarget(rawTarget, cwd) }