/** * test/unit-image-paths.mjs * * 单元测试:图片路径提取与本地路径识别 * 运行方式:node test/unit-image-paths.mjs */ let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(` ✅ ${name}`); passed++; } catch (e) { console.error(` ❌ ${name}`); console.error(` ${e.message}`); failed++; } } function assert(condition, msg) { if (!condition) throw new Error(msg || 'Assertion failed'); } function assertEqual(a, b, msg) { const as = JSON.stringify(a), bs = JSON.stringify(b); if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`); } function normalizeFileUrlToLocalPath(url) { if (!url.startsWith('file:///')) return url; const rawPath = url.slice('file:///'.length); let decodedPath = rawPath; try { decodedPath = decodeURIComponent(rawPath); } catch { // 忽略非法编码,保留原始路径 } return /^[A-Za-z]:[\\/]/.test(decodedPath) ? decodedPath : '/' + decodedPath; } function extractImageUrlsFromText(text) { const urls = []; const fileRe = /file:\/\/\/([^\s"')\]]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg))/gi; for (const m of text.matchAll(fileRe)) { const normalizedPath = normalizeFileUrlToLocalPath(`file:///${m[1]}`); urls.push(normalizedPath); } const httpRe = /(https?:\/\/[^\s"')\]]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"')\]]*)?)/gi; for (const m of text.matchAll(httpRe)) { if (!urls.includes(m[1])) urls.push(m[1]); } const localRe = /(?:^|[\s"'(\[,:])((?:\/(?!\/)|[A-Za-z]:[\\/])[^\s"')\]]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg))/gi; for (const m of text.matchAll(localRe)) { const localPath = m[1].trim(); const fullMatch = m[0]; const matchStart = m.index ?? 0; const pathOffsetInMatch = fullMatch.lastIndexOf(localPath); const pathStart = matchStart + Math.max(pathOffsetInMatch, 0); const beforePath = text.slice(Math.max(0, pathStart - 12), pathStart); if (/file:\/\/\/[A-Za-z]:$/i.test(beforePath)) continue; if (localPath.startsWith('//')) continue; if (!urls.includes(localPath)) urls.push(localPath); } return [...new Set(urls)]; } function isLocalPath(imageUrl) { return /^(\/|~\/|[A-Za-z]:[\\/])/.test(imageUrl); } console.log('\n📦 [1] 协议相对 URL 排除\n'); test('不提取 //example.com/image.jpg', () => { const text = 'look //example.com/image.jpg and https://example.com/real.jpg'; const urls = extractImageUrlsFromText(text); assertEqual(urls, ['https://example.com/real.jpg']); }); console.log('\n📦 [2] file:// Windows 路径归一化\n'); test('file:///C:/Users/name/a.jpg → C:/Users/name/a.jpg', () => { const text = 'please inspect file:///C:/Users/name/a.jpg'; const urls = extractImageUrlsFromText(text); assertEqual(urls, ['C:/Users/name/a.jpg']); }); test('file:///Users/name/a.jpg → /Users/name/a.jpg', () => { const text = 'please inspect file:///Users/name/a.jpg'; const urls = extractImageUrlsFromText(text); assertEqual(urls, ['/Users/name/a.jpg']); }); test('直接 image block 的 file:// URL 也能归一化', () => { assertEqual( normalizeFileUrlToLocalPath('file:///C:/Users/name/a.jpg'), 'C:/Users/name/a.jpg' ); assertEqual( normalizeFileUrlToLocalPath('file:///Users/name/a.jpg'), '/Users/name/a.jpg' ); }); console.log('\n📦 [3] Windows 本地路径识别\n'); test('提取 C:\\Users\\name\\a.jpg', () => { const text = '看看这张图 C:\\Users\\name\\a.jpg'; const urls = extractImageUrlsFromText(text); assertEqual(urls, ['C:\\Users\\name\\a.jpg']); }); test('提取 C:/Users/name/a.jpg', () => { const text = '看看这张图 C:/Users/name/a.jpg'; const urls = extractImageUrlsFromText(text); assertEqual(urls, ['C:/Users/name/a.jpg']); }); test('Windows 路径被视为本地文件', () => { assert(isLocalPath('C:\\Users\\name\\a.jpg'), 'backslash path should be local'); assert(isLocalPath('C:/Users/name/a.jpg'), 'slash path should be local'); assert(isLocalPath(normalizeFileUrlToLocalPath('file:///C:/Users/name/a.jpg')), 'normalized file URL should be local'); assert(isLocalPath(normalizeFileUrlToLocalPath('file:///Users/name/a.jpg')), 'normalized unix file URL should be local'); }); console.log('\n' + '═'.repeat(55)); console.log(` 结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`); console.log('═'.repeat(55) + '\n'); if (failed > 0) process.exit(1);