117 lines
3.0 KiB
TypeScript
117 lines
3.0 KiB
TypeScript
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<string, string> = {
|
|
'.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<PreviewTarget | null> {
|
|
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)
|
|
}
|