import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview"; export interface FileBreadcrumb { readonly label: string; readonly path: string; readonly kind: "project" | "directory" | "file";} function isWindowsAbsolutePath(value: string): boolean { return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");} /** A file route holding an absolute path shows a host file outside the workspace. */export function isAbsolutePath(value: string): boolean { return value.startsWith("/") || isWindowsAbsolutePath(value);} /** Route segments that `normalizeRoutePath` joins back into the same path, root included. */export function fileRoutePathSegments(path: string): string[] { const segments = path.split("/").filter((segment) => segment.length > 0); return path.startsWith("/") ? ["", ...segments] : segments;} function isWindowsPathStyle(value: string): boolean { return isWindowsAbsolutePath(value) || /^[A-Za-z]:\\/.test(value);} function joinPath(base: string, next: string, separator: "/" | "\\"): string { const cleanBase = base.replace(/[\\/]+$/, ""); if (separator === "\\") { return `${cleanBase}\\${next.replaceAll("/", "\\")}`; } return `${cleanBase}/${next.replace(/^\/+/, "")}`;} export function basename(path: string): string { const parts = path.split(/[\\/]/).filter(Boolean); return parts.at(-1) ?? path;} export function resolveWorkspaceFilePath(cwd: string, relativePath: string): string { if (isAbsolutePath(relativePath)) { return relativePath; } const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/"; return joinPath(cwd, relativePath, separator);} function normalizeRelativePath(value: string): string | null { const segments: string[] = []; for (const segment of value.replaceAll("\\", "/").split("/")) { if (segment.length === 0 || segment === ".") { continue; } if (segment === "..") { if (segments.length === 0) { return null; } segments.pop(); continue; } segments.push(segment); } return segments.length > 0 ? segments.join("/") : null;} export function resolveWorkspaceRelativeFilePath( workspaceRoot: string | null | undefined, targetPath: string,): string | null { if (!isAbsolutePath(targetPath)) { if (targetPath.startsWith("~/") || targetPath.startsWith("~\\")) { return null; } return normalizeRelativePath(targetPath); } if (!workspaceRoot) { return null; } const normalizedTarget = targetPath.replaceAll("\\", "/"); const normalizedRoot = workspaceRoot.replaceAll("\\", "/").replace(/\/+$/, ""); const caseInsensitive = isWindowsAbsolutePath(targetPath) || isWindowsAbsolutePath(workspaceRoot); const comparableTarget = caseInsensitive ? normalizedTarget.toLowerCase() : normalizedTarget; const comparableRoot = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot; if (!comparableTarget.startsWith(`${comparableRoot}/`)) { return null; } const relativePath = normalizedTarget.slice(normalizedRoot.length + 1); // `/repo/../x` starts with the root but escapes it. if (relativePath.split("/").includes("..")) { return null; } return normalizeRelativePath(relativePath);} export function isVideoPreviewFile(path: string): boolean { return isWorkspaceVideoPreviewPath(path);} export function isSvgImagePreviewFile(path: string): boolean { return /\.svg$/i.test(path.split(/[?#]/, 1)[0] ?? "");} export function isMarkdownPreviewFile(path: string): boolean { return /\.(?:md|mdx)$/i.test(path.split(/[?#]/, 1)[0] ?? "");} export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] { const parts = relativePath.split("/").filter(Boolean); return [ { label: projectName, path: "", kind: "project" }, ...parts.map((part, index) => ({ label: part, path: parts.slice(0, index + 1).join("/"), kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const), })), ];}
import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview";
export interface FileBreadcrumb {
readonly label: string;
readonly path: string;
readonly kind: "project" | "directory" | "file";
}
function isWindowsAbsolutePath(value: string): boolean {
return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
}
/** A file route holding an absolute path shows a host file outside the workspace. */
export function isAbsolutePath(value: string): boolean {
return value.startsWith("/") || isWindowsAbsolutePath(value);
}
/** Route segments that `normalizeRoutePath` joins back into the same path, root included. */
export function fileRoutePathSegments(path: string): string[] {
const segments = path.split("/").filter((segment) => segment.length > 0);
return path.startsWith("/") ? ["", ...segments] : segments;
}
function isWindowsPathStyle(value: string): boolean {
return isWindowsAbsolutePath(value) || /^[A-Za-z]:\\/.test(value);
}
function joinPath(base: string, next: string, separator: "/" | "\\"): string {
const cleanBase = base.replace(/[\\/]+$/, "");
if (separator === "\\") {
return `${cleanBase}\\${next.replaceAll("/", "\\")}`;
}
return `${cleanBase}/${next.replace(/^\/+/, "")}`;
}
export function basename(path: string): string {
const parts = path.split(/[\\/]/).filter(Boolean);
return parts.at(-1) ?? path;
}
export function resolveWorkspaceFilePath(cwd: string, relativePath: string): string {
if (isAbsolutePath(relativePath)) {
return relativePath;
}
const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/";
return joinPath(cwd, relativePath, separator);
}
function normalizeRelativePath(value: string): string | null {
const segments: string[] = [];
for (const segment of value.replaceAll("\\", "/").split("/")) {
if (segment.length === 0 || segment === ".") {
continue;
}
if (segment === "..") {
if (segments.length === 0) {
return null;
}
segments.pop();
continue;
}
segments.push(segment);
}
return segments.length > 0 ? segments.join("/") : null;
}
export function resolveWorkspaceRelativeFilePath(
workspaceRoot: string | null | undefined,
targetPath: string,
): string | null {
if (!isAbsolutePath(targetPath)) {
if (targetPath.startsWith("~/") || targetPath.startsWith("~\\")) {
return null;
}
return normalizeRelativePath(targetPath);
}
if (!workspaceRoot) {
return null;
}
const normalizedTarget = targetPath.replaceAll("\\", "/");
const normalizedRoot = workspaceRoot.replaceAll("\\", "/").replace(/\/+$/, "");
const caseInsensitive = isWindowsAbsolutePath(targetPath) || isWindowsAbsolutePath(workspaceRoot);
const comparableTarget = caseInsensitive ? normalizedTarget.toLowerCase() : normalizedTarget;
const comparableRoot = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot;
if (!comparableTarget.startsWith(`${comparableRoot}/`)) {
return null;
}
const relativePath = normalizedTarget.slice(normalizedRoot.length + 1);
// `/repo/../x` starts with the root but escapes it.
if (relativePath.split("/").includes("..")) {
return null;
}
return normalizeRelativePath(relativePath);
}
export function isVideoPreviewFile(path: string): boolean {
return isWorkspaceVideoPreviewPath(path);
}
export function isSvgImagePreviewFile(path: string): boolean {
return /\.svg$/i.test(path.split(/[?#]/, 1)[0] ?? "");
}
export function isMarkdownPreviewFile(path: string): boolean {
return /\.(?:md|mdx)$/i.test(path.split(/[?#]/, 1)[0] ?? "");
}
export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] {
const parts = relativePath.split("/").filter(Boolean);
return [
{ label: projectName, path: "", kind: "project" },
...parts.map((part, index) => ({
label: part,
path: parts.slice(0, index + 1).join("/"),
kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const),
})),
];
}