/* Copyright 2026 Marimo. All rights reserved. */ import type { TypedString } from "./typed"; export type FilePath = TypedString<"FilePath">; // Windows drive-letter prefix like `C:\`, `d:/`, `Z:\`. const WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:[/\\]/; // URI scheme prefix like `s3://`, `gs://`, `http://`, `file://`. const URI_SCHEME_PREFIX = /^[A-Za-z][\dA-Za-z+.-]*:\/\//; export const Paths = { isAbsolute: (path: string): boolean => { return ( path.startsWith("/") || path.startsWith("\\") || WINDOWS_DRIVE_PREFIX.test(path) || URI_SCHEME_PREFIX.test(path) ); }, dirname: (path: string) => { return PathBuilder.guessDeliminator(path).dirname(path as FilePath); }, basename: (path: string) => { return PathBuilder.guessDeliminator(path).basename(path as FilePath); }, rest: (path: string, root: string) => { return PathBuilder.guessDeliminator(path).rest( path as FilePath, root as FilePath, ); }, extension: (filename: string): string => { const parts = filename.split("."); if (parts.length === 1) { return ""; } return parts.at(-1) ?? ""; }, }; export class PathBuilder { public readonly deliminator: string; constructor(deliminator: "/" | "\\") { this.deliminator = deliminator; } static guessDeliminator(path: string): PathBuilder { return path.includes("/") ? new PathBuilder("/") : new PathBuilder("\\"); } join(...paths: string[]): FilePath { return paths.filter(Boolean).join(this.deliminator) as FilePath; } basename(path: FilePath): FilePath { const parts = path.split(this.deliminator); return (parts.pop() ?? "") as FilePath; } rest(path: FilePath, root: FilePath): FilePath { const pathParts = path.split(this.deliminator); const rootParts = root.split(this.deliminator); let i = 0; for (; i < pathParts.length && i < rootParts.length; ++i) { if (pathParts[i] !== rootParts[i]) { break; } } return pathParts.slice(i).join(this.deliminator) as FilePath; } dirname(path: FilePath): FilePath { const parts = path.split(this.deliminator); parts.pop(); return parts.join(this.deliminator) as FilePath; } }