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() const prefix = root.endsWith("/") ? root : root + "/" let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))) if (path.startsWith(prefix)) { path = path.slice(prefix.length) } if (path.startsWith(root)) { path = path.slice(root.length) } if (path.startsWith("./")) { path = path.slice(2) } if (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) => normalize(input).replace(/\/+$/, "") return { normalize, tab, pathFromTab, normalizeDir, } }