1import type { FilesystemEntryMetadata } from "@t3tools/contracts";
2import { filePreviewDelimiter } from "@t3tools/shared/delimitedPreview";
3import { joinPath, resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
4import {
5 isWorkspaceAudioPreviewPath,
6 isWorkspaceVideoPreviewPath,
7} from "@t3tools/shared/filePreview";
8
9function isWindowsAbsolutePath(value: string): boolean {
10 return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
11}
12
13export function isAbsolutePath(value: string): boolean {
14 return value.startsWith("/") || isWindowsAbsolutePath(value);
15}
16
17export function fileRoutePathSegments(path: string): string[] {
18 const segments = path.split("/").filter((segment) => segment.length > 0);
19 return path.startsWith("/") ? ["", ...segments] : segments;
20}
21
22function isWindowsPathStyle(value: string): boolean {
23 return isWindowsAbsolutePath(value) || /^[A-Za-z]:\\/.test(value);
24}
25
26export function basename(path: string): string {
27 const parts = path.split(/[\\/]/).filter(Boolean);
28 return parts.at(-1) ?? path;
29}
30
31export function resolveWorkspaceFilePath(cwd: string, relativePath: string): string {
32 if (isAbsolutePath(relativePath)) {
33 return relativePath;
34 }
35
36 const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/";
37 return joinPath(cwd, relativePath, separator);
38}
39
40function normalizeRelativePath(value: string): string | null {
41 const segments: string[] = [];
42 for (const segment of value.replaceAll("\\", "/").split("/")) {
43 if (segment.length === 0 || segment === ".") {
44 continue;
45 }
46 if (segment === "..") {
47 if (segments.length === 0) {
48 return null;
49 }
50 segments.pop();
51 continue;
52 }
53 segments.push(segment);
54 }
55 return segments.length > 0 ? segments.join("/") : null;
56}
57
58export function resolveFileLinkPath(
59 workspaceRoot: string | null | undefined,
60 targetPath: string,
61): string | null {
62 if (targetPath.startsWith("~/") || targetPath.startsWith("~\\")) {
63 if (!workspaceRoot) return null;
64 const expanded = resolvePathLinkTarget(targetPath, workspaceRoot);
65 return isAbsolutePath(expanded) ? expanded : null;
66 }
67 return isAbsolutePath(targetPath) ? targetPath : normalizeRelativePath(targetPath);
68}
69
70export function resolveWorkspaceRelativeFilePath(
71 workspaceRoot: string | null | undefined,
72 targetPath: string,
73): string | null {
74 const resolved = resolveFileLinkPath(workspaceRoot, targetPath);
75 if (resolved === null) return null;
76 targetPath = resolved;
77 if (!isAbsolutePath(targetPath)) return targetPath;
78 if (!workspaceRoot) {
79 return null;
80 }
81
82 const normalizedTarget = targetPath.replaceAll("\\", "/");
83 const normalizedRoot = workspaceRoot.replaceAll("\\", "/").replace(/\/+$/, "");
84 const caseInsensitive = isWindowsAbsolutePath(targetPath) || isWindowsAbsolutePath(workspaceRoot);
85 const comparableTarget = caseInsensitive ? normalizedTarget.toLowerCase() : normalizedTarget;
86 const comparableRoot = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot;
87 if (!comparableTarget.startsWith(`${comparableRoot}/`)) {
88 return null;
89 }
90
91 const relativePath = normalizedTarget.slice(normalizedRoot.length + 1);
92 if (relativePath.split("/").includes("..")) {
93 return null;
94 }
95 return normalizeRelativePath(relativePath);
96}
97
98export function isVideoPreviewFile(path: string): boolean {
99 return isWorkspaceVideoPreviewPath(path);
100}
101
102export function isAudioPreviewFile(path: string): boolean {
103 return isWorkspaceAudioPreviewPath(path);
104}
105
106export function isSvgImagePreviewFile(path: string): boolean {
107 return /\.svg$/i.test(path);
108}
109
110export function isMarkdownPreviewFile(path: string): boolean {
111 return /\.(?:md|mdx)$/i.test(path);
112}
113
114export function needsWorkspaceFileContents(input: {
115 readonly relativePath: string | null;
116 readonly metadataKind: FilesystemEntryMetadata["kind"] | undefined;
117 readonly activeMode: "preview" | "source";
118}): boolean {
119 const path = input.relativePath;
120 return (
121 input.metadataKind !== "directory" &&
122 path !== null &&
123 !isVideoPreviewFile(path) &&
124 !isAudioPreviewFile(path) &&
125 (input.activeMode === "source" ||
126 isMarkdownPreviewFile(path) ||
127 filePreviewDelimiter({ name: path }) !== null)
128 );
129}
130
131export function fileHeaderSubtitle(projectName: string, relativePath: string): string {
132 const parentDir = relativePath.slice(
133 0,
134 Math.max(relativePath.lastIndexOf("/"), relativePath.lastIndexOf("\\"), 0),
135 );
136 return isAbsolutePath(relativePath)
137 ? parentDir
138 : [projectName, parentDir].filter(Boolean).join(" · ");
139}