File size: 4,681 Bytes
097fb32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * 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);