File size: 4,357 Bytes
eb1202c | 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | export function stripFileProtocol(input: string) {
if (!input.startsWith("file://")) return input
return input.slice("file://".length)
}
export function stripQueryAndHash(input: string) {
const hashIndex = input.indexOf("#")
const queryIndex = input.indexOf("?")
if (hashIndex !== -1 && queryIndex !== -1) {
return input.slice(0, Math.min(hashIndex, queryIndex))
}
if (hashIndex !== -1) return input.slice(0, hashIndex)
if (queryIndex !== -1) return input.slice(0, queryIndex)
return input
}
export function unquoteGitPath(input: string) {
if (!input.startsWith('"')) return input
if (!input.endsWith('"')) return input
const body = input.slice(1, -1)
const bytes: number[] = []
for (let i = 0; i < body.length; i++) {
const char = body[i]!
if (char !== "\\") {
bytes.push(char.charCodeAt(0))
continue
}
const next = body[i + 1]
if (!next) {
bytes.push("\\".charCodeAt(0))
continue
}
if (next >= "0" && next <= "7") {
const chunk = body.slice(i + 1, i + 4)
const match = chunk.match(/^[0-7]{1,3}/)
if (!match) {
bytes.push(next.charCodeAt(0))
i++
continue
}
bytes.push(parseInt(match[0], 8))
i += match[0].length
continue
}
const escaped =
next === "n"
? "\n"
: next === "r"
? "\r"
: next === "t"
? "\t"
: next === "b"
? "\b"
: next === "f"
? "\f"
: next === "v"
? "\v"
: next === "\\" || next === '"'
? next
: undefined
bytes.push((escaped ?? next).charCodeAt(0))
i++
}
return new TextDecoder().decode(new Uint8Array(bytes))
}
export function decodeFilePath(input: string) {
try {
return decodeURIComponent(input)
} catch {
return input
}
}
export function encodeFilePath(filepath: string): string {
// Normalize Windows paths: convert backslashes to forward slashes
let normalized = filepath.replace(/\\/g, "/")
// Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
if (/^[A-Za-z]:/.test(normalized)) {
normalized = "/" + normalized
}
// Encode each path segment (preserving forward slashes as path separators)
// Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
// can reliably detect drives.
return normalized
.split("/")
.map((segment, index) => {
if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
return encodeURIComponent(segment)
})
.join("/")
}
export function createPathHelpers(scope: () => string) {
const normalize = (input: string) => {
const root = scope()
let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
// Separator-agnostic prefix stripping for Cygwin/native Windows compatibility
// Only case-insensitive on Windows (drive letter or UNC paths)
const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\")
const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/")
const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/")
if (
canonPath.startsWith(canonRoot) &&
(canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/")
) {
// Slice from original path to preserve native separators
path = path.slice(root.length)
}
if (path.startsWith("./") || path.startsWith(".\\")) {
path = path.slice(2)
}
if (path.startsWith("/") || path.startsWith("\\")) {
path = path.slice(1)
}
return path
}
const tab = (input: string) => {
const path = normalize(input)
return `file://${encodeFilePath(path)}`
}
const pathFromTab = (tabValue: string) => {
if (!tabValue.startsWith("file://")) return
return normalize(tabValue)
}
const normalizeDir = (input: string) => {
const path = normalize(input)
const root = scope()
const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\")
return (windows ? path.replace(/\\/g, "/") : path).replace(/\/+$/, "")
}
return {
normalize,
tab,
pathFromTab,
normalizeDir,
}
}
|