fix: preserve windows markdown link fragments (#4261)

This commit is contained in:
Neil
2026-05-31 10:02:59 -07:00
committed by GitHub
parent 9b5625c528
commit fba03068e3
5 changed files with 56 additions and 4 deletions
@@ -55,6 +55,20 @@ describe('resolveMarkdownLinkTarget', () => {
})
})
it('extracts hash line anchors from Windows drive-letter absolute .md links', () => {
const r = resolveMarkdownLinkTarget(
'C:\\repo\\docs\\guide.md#L10',
'C:\\repo\\docs\\note.md',
'C:\\repo'
)
expect(r).toMatchObject({
kind: 'markdown',
absolutePath: 'C:/repo/docs/guide.md',
relativePath: 'docs/guide.md',
line: 10
})
})
it('extracts line from #L10', () => {
const r = resolveMarkdownLinkTarget('./guide.md#L10', SOURCE, ROOT)
expect(r).toMatchObject({ kind: 'markdown', line: 10, column: undefined })
@@ -1,4 +1,8 @@
import { filesystemPathToFileUri, fileUriToFilesystemPath } from '../../../../shared/file-uri-path'
import {
filesystemPathHrefToFileUri,
filesystemPathToFileUri,
fileUriToFilesystemPath
} from '../../../../shared/file-uri-path'
import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path'
// Pure classifier for markdown link targets. Called by the link-activation
@@ -112,7 +116,7 @@ function resolveRelativeToSource(rawHref: string, sourceFilePath: string): URL |
if (isWindowsAbsolutePathLike(rawHref)) {
// Why: URL treats `C:\...` as a custom `c:` scheme unless the Windows
// absolute path is first converted to the file URL form used downstream.
return new URL(filesystemPathToFileUri(rawHref))
return new URL(filesystemPathHrefToFileUri(rawHref))
}
return new URL(rawHref, toFileUrl(sourceFilePath))
} catch {
@@ -16,6 +16,12 @@ describe('getMarkdownPreviewLinkTarget', () => {
)
})
it('preserves hash fragments on Windows drive-letter absolute links', () => {
expect(
getMarkdownPreviewLinkTarget('C:\\repo\\docs\\guide.md#L10', '/repo/docs/README.md')
).toBe('file:///C:/repo/docs/guide.md#L10')
})
it('preserves external links', () => {
expect(getMarkdownPreviewLinkTarget('https://example.com/docs', '/repo/docs/README.md')).toBe(
'https://example.com/docs'
@@ -1,4 +1,8 @@
import { filesystemPathToFileUri, fileUriToFilesystemPath } from '../../../../shared/file-uri-path'
import {
filesystemPathHrefToFileUri,
filesystemPathToFileUri,
fileUriToFilesystemPath
} from '../../../../shared/file-uri-path'
import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path'
function toFileUrl(filePath: string): string {
@@ -14,7 +18,7 @@ export function resolveMarkdownPreviewHref(rawUrl: string, filePath: string): UR
if (isWindowsAbsolutePathLike(rawUrl)) {
// Why: URL treats `C:\...` as a custom `c:` scheme unless we first
// normalize the drive path into the file URL form markdown previews use.
return new URL(filesystemPathToFileUri(rawUrl))
return new URL(filesystemPathHrefToFileUri(rawUrl))
}
return new URL(rawUrl, toFileUrl(filePath))
} catch {
+24
View File
@@ -28,6 +28,30 @@ export function filesystemPathToFileUri(filePath: string): string {
return normalizedPath.startsWith('/') ? `file://${encodedPath}` : `file:///${encodedPath}`
}
export function filesystemPathHrefToFileUri(filePathHref: string): string {
const suffixIndex = filePathHref.search(/[?#]/)
if (suffixIndex === -1) {
return filesystemPathToFileUri(filePathHref)
}
const pathPart = filePathHref.slice(0, suffixIndex)
const suffix = filePathHref.slice(suffixIndex)
const url = new URL(filesystemPathToFileUri(pathPart))
if (suffix.startsWith('#')) {
// Why: markdown href fragments like `#L10` should stay URL fragments,
// not become `%23L10` inside the Windows filesystem path.
url.hash = suffix
return url.toString()
}
const hashIndex = suffix.indexOf('#')
url.search = hashIndex === -1 ? suffix : suffix.slice(0, hashIndex)
if (hashIndex !== -1) {
url.hash = suffix.slice(hashIndex)
}
return url.toString()
}
export function fileUriToFilesystemPath(url: URL): string | null {
if (url.protocol !== 'file:') {
return null