diff --git a/electron/main/localFileUrl.ts b/electron/main/localFileUrl.ts index 1775c9d5..7f9c8e57 100644 --- a/electron/main/localFileUrl.ts +++ b/electron/main/localFileUrl.ts @@ -29,6 +29,11 @@ export function filePathFromLocalFileUrl(url: string): string { if (parsed.hostname === 'preview' && queryPath) return queryPath; const pathname = decodeURIComponent(parsed.pathname); + // Navigable local HTML uses a fixed host with the absolute filesystem path + // in the pathname. Unlike `localfile:///Users/...`, this keeps Chromium from + // treating the first path segment as a lower-cased hostname when resolving a + // relative link from an injected element. + if (parsed.hostname === 'preview') return pathname; if (parsed.hostname) return `/${parsed.hostname}${pathname}`; return pathname; } diff --git a/src/lib/htmlLocalAssets.ts b/src/lib/htmlLocalAssets.ts index 29ea3f6b..1bd6c95a 100644 --- a/src/lib/htmlLocalAssets.ts +++ b/src/lib/htmlLocalAssets.ts @@ -63,22 +63,32 @@ function encodePathSegments(filePath: string): string { export function toLocalFileUrl(filePath: string): string { if ( - filePath.startsWith('localfile://') || - filePath.startsWith('file://') || filePath.startsWith('http://') || filePath.startsWith('https://') || filePath.startsWith('blob:') || filePath.startsWith('data:') ) { - const normalized = - filePath.startsWith('file://') && !filePath.startsWith('localfile://') - ? filePath.replace(/^file:\/\//, 'localfile://') - : filePath; - return normalized.endsWith('/') ? normalized : `${normalized}/`; + return filePath.endsWith('/') ? filePath : `${filePath}/`; } - const encodedPath = encodePathSegments(filePath); - const localFileUrl = `localfile://${encodedPath}`; + let localPath = filePath; + if (filePath.startsWith('localfile:') || filePath.startsWith('file:')) { + const parsed = new URL(filePath); + const queryPath = parsed.searchParams.get('path'); + if (parsed.hostname === 'preview' && queryPath) { + localPath = queryPath; + } else if (!parsed.hostname || parsed.hostname === 'preview') { + localPath = decodeURIComponent(parsed.pathname); + } else { + localPath = `/${parsed.hostname}${decodeURIComponent(parsed.pathname)}`; + } + } + + const encodedPath = encodePathSegments(localPath); + const rootedPath = encodedPath.startsWith('/') + ? encodedPath + : `/${encodedPath}`; + const localFileUrl = `localfile://preview${rootedPath}`; return localFileUrl.endsWith('/') ? localFileUrl : `${localFileUrl}/`; } diff --git a/test/unit/electron/localFileUrl.test.ts b/test/unit/electron/localFileUrl.test.ts index 8171e1ac..f59462b1 100644 --- a/test/unit/electron/localFileUrl.test.ts +++ b/test/unit/electron/localFileUrl.test.ts @@ -24,6 +24,17 @@ describe('filePathFromLocalFileUrl', () => { ).toBe('/Users/Example User/Report.pdf'); }); + it('preserves a navigable fixed-host pathname after relative resolution', () => { + const linkedUrl = new URL( + '../%E7%AC%AC7%E8%AF%BE%E6%97%B6/index.html', + 'localfile://preview/Users/Example%20User/project/%E7%AC%AC%E4%B8%80%E8%AF%BE%E6%97%B6/' + ); + + expect(filePathFromLocalFileUrl(linkedUrl.toString())).toBe( + '/Users/Example User/project/第7课时/index.html' + ); + }); + it('reconstructs legacy macOS URLs whose first segment became a host', () => { expect( filePathFromLocalFileUrl('localfile://users/4pmtong/report.pdf') diff --git a/test/unit/lib/htmlLocalAssets.test.ts b/test/unit/lib/htmlLocalAssets.test.ts index fdc89a1e..77f8d622 100644 --- a/test/unit/lib/htmlLocalAssets.test.ts +++ b/test/unit/lib/htmlLocalAssets.test.ts @@ -24,19 +24,27 @@ import { describe('toLocalFileUrl', () => { it('converts absolute unix paths to localfile base hrefs', () => { expect(toLocalFileUrl('/Users/test/canvas_map')).toBe( - 'localfile:///Users/test/canvas_map/' + 'localfile://preview/Users/test/canvas_map/' ); }); - it('preserves existing localfile urls and trailing slash', () => { + it('upgrades existing localfile urls to the navigable fixed-host format', () => { expect(toLocalFileUrl('localfile:///Users/test/canvas_map/')).toBe( - 'localfile:///Users/test/canvas_map/' + 'localfile://preview/Users/test/canvas_map/' ); }); + it('converts query-based preview urls to the navigable fixed-host format', () => { + expect( + toLocalFileUrl( + 'localfile://preview/?path=%2FUsers%2FExample%20User%2Fcanvas_map' + ) + ).toBe('localfile://preview/Users/Example%20User/canvas_map/'); + }); + it('emits standard localfile urls for Windows drive paths', () => { expect(toLocalFileUrl('C:\\Users\\test\\canvas_map')).toBe( - 'localfile:///C:/Users/test/canvas_map/' + 'localfile://preview/C:/Users/test/canvas_map/' ); }); });