fix local HTML relative-link previews

This commit is contained in:
4pmtong 2026-08-07 19:54:38 +08:00
parent 3300374c8b
commit f5d26eabff
4 changed files with 47 additions and 13 deletions

View file

@ -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 <base> element.
if (parsed.hostname === 'preview') return pathname;
if (parsed.hostname) return `/${parsed.hostname}${pathname}`;
return pathname;
}

View file

@ -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}/`;
}

View file

@ -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')

View file

@ -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/'
);
});
});