feat(desktop): add classifyPreviewLink classifier

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 12:34:54 +08:00
parent 36a3e4c018
commit 8c4e8073ab
2 changed files with 74 additions and 0 deletions

View File

@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { classifyPreviewLink } from './previewLinkRouter'
describe('classifyPreviewLink', () => {
it('classifies loopback urls as browser-localhost', () => {
expect(classifyPreviewLink('http://localhost:5173/').kind).toBe('browser-localhost')
expect(classifyPreviewLink('http://127.0.0.1:8080/x').kind).toBe('browser-localhost')
})
it('classifies html file paths as browser-file', () => {
expect(classifyPreviewLink('file:///Users/x/index.html').kind).toBe('browser-file')
expect(classifyPreviewLink('/Users/x/page.htm').kind).toBe('browser-file')
expect(classifyPreviewLink('./out/index.html').kind).toBe('browser-file')
})
it('classifies relative previewable docs as file-preview', () => {
expect(classifyPreviewLink('docs/report.md').kind).toBe('file-preview')
expect(classifyPreviewLink('src/app.ts').kind).toBe('file-preview')
})
it('classifies remote http(s) as remote', () => {
expect(classifyPreviewLink('https://example.com').kind).toBe('remote')
})
it('ignores anchors and empty', () => {
expect(classifyPreviewLink('#section').kind).toBe('ignored')
expect(classifyPreviewLink('').kind).toBe('ignored')
})
it('exposes a normalized path for file kinds', () => {
expect(classifyPreviewLink('file:///Users/x/index.html').path).toBe('/Users/x/index.html')
expect(classifyPreviewLink('docs/report.md').path).toBe('docs/report.md')
})
})

View File

@ -0,0 +1,45 @@
import { isLoopbackHostname } from './desktopRuntime'
export type PreviewLinkKind =
| 'browser-localhost'
| 'browser-file'
| 'file-preview'
| 'remote'
| 'ignored'
export type PreviewLinkClass = { kind: PreviewLinkKind; path?: string; url?: string }
const PREVIEWABLE_EXT = new Set(['md', 'markdown', 'txt', 'ts', 'tsx', 'js', 'jsx', 'json', 'css', 'py', 'rs', 'go', 'java', 'sh', 'yml', 'yaml', 'png', 'jpg', 'jpeg', 'gif', 'svg'])
function extOf(p: string): string {
const m = /\.([a-z0-9]+)(?:[?#].*)?$/i.exec(p)
const g = m?.[1]
return g ? g.toLowerCase() : ''
}
export function classifyPreviewLink(href: string): PreviewLinkClass {
const raw = href.trim()
if (!raw || raw.startsWith('#')) return { kind: 'ignored' }
try {
const u = new URL(raw)
if (u.protocol === 'http:' || u.protocol === 'https:') {
return isLoopbackHostname(u.hostname)
? { kind: 'browser-localhost', url: u.toString() }
: { kind: 'remote', url: u.toString() }
}
if (u.protocol === 'file:') {
return { kind: 'browser-file', path: decodeURIComponent(u.pathname) }
}
return { kind: 'ignored' }
} catch {
// not an absolute URL → treat as a path
}
const ext = extOf(raw)
if (ext === 'html' || ext === 'htm') return { kind: 'browser-file', path: raw }
if (PREVIEWABLE_EXT.has(ext)) {
return raw.startsWith('/') ? { kind: 'browser-file', path: raw } : { kind: 'file-preview', path: raw }
}
return { kind: 'ignored' }
}