authorgravatar for 107891771+Michel-Liao@users.noreply.github.comMichel Liao <107891771+Michel-Liao@users.noreply.github.com> 2026-09-04 20:27:31-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 17:27:31-07:00
log8faf031c2dac4f147c43fe32e326b76678c20c94
tree76dd7e1da726673ffd236457e055153cee7aaabd
parented2bdbb278c4fedf6ce79d4925908b759f7fd992
signature Signed by PGP key B5690EEEBB952194

fix(web): respect case in POSIX file links (#9309)

Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com>

5 files changed, 83 insertions(+), 5 deletions(-)

apps/web/src/filePathDisplay.test.ts+6
......@@ -38,4 +38,10 @@ describe("formatWorkspaceRelativePath", () => {
3838 ),
3939 ).toBe("t3code/apps/web/src/session-logic.ts:501:9");
4040 });
41
42 it("keeps double-slash POSIX paths case-sensitive", () => {
43 expect(formatWorkspaceRelativePath("//tmp/project/probe.txt", "//tmp/Project")).toBe(
44 "//tmp/project/probe.txt",
45 );
46 });
4147});
apps/web/src/filePathDisplay.ts+7-3
......@@ -4,6 +4,7 @@ import {
44 splitFilePathPosition,
55 stripSlashPrefixedWindowsDrive,
66} from "@t3tools/client-runtime/markdown-links";
7import { isWindowsAbsolutePath } from "@t3tools/shared/path";
78
89function normalizePathSeparators(path: string): string {
910 return path.replaceAll("\\", "/");
......@@ -30,10 +31,13 @@ export function formatWorkspaceRelativePath(
3031 normalizePathSeparators(trimTrailingPathSeparators(workspaceRoot)),
3132 );
3233 const workspaceLabel = fileBasename(normalizedWorkspaceRoot);
33 const pathForCompare = normalizedPath.toLowerCase();
34 const workspaceForCompare = normalizedWorkspaceRoot.toLowerCase();
34 const caseInsensitive = isWindowsAbsolutePath(stripSlashPrefixedWindowsDrive(workspaceRoot));
35 const pathForCompare = caseInsensitive ? normalizedPath.toLowerCase() : normalizedPath;
36 const workspaceForCompare = caseInsensitive
37 ? normalizedWorkspaceRoot.toLowerCase()
38 : normalizedWorkspaceRoot;
3539 const workspaceWithSeparator = `${workspaceForCompare}/`;
36 const workspaceLabelWithSeparator = `${workspaceLabel.toLowerCase()}/`;
40 const workspaceLabelWithSeparator = `${caseInsensitive ? workspaceLabel.toLowerCase() : workspaceLabel}/`;
3741
3842 if (pathForCompare === workspaceForCompare) {
3943 displayPath = workspaceLabel;
apps/web/src/markdown-links.test.ts+55
......@@ -254,6 +254,61 @@ describe("resolveMarkdownFileLinkTarget", () => {
254254 });
255255 });
256256
257 it("does not classify a case-distinct POSIX sibling as a workspace file", () => {
258 expect(
259 resolveMarkdownFileLinkMeta(
260 "/tmp/t3code-case-test/project/probe.txt",
261 "/tmp/t3code-case-test/Project",
262 ),
263 ).toMatchObject({
264 displayPath: "/tmp/t3code-case-test/project/probe.txt",
265 workspaceRelativePath: null,
266 });
267 });
268
269 it("keeps Windows workspace comparisons case-insensitive", () => {
270 expect(
271 resolveMarkdownFileLinkMeta("C:/Users/MIKE/Project/src/main.ts", "c:/users/mike/project"),
272 ).toMatchObject({
273 displayPath: "project/src/main.ts",
274 workspaceRelativePath: "src/main.ts",
275 });
276 });
277
278 it("keeps drive-root workspace comparisons case-insensitive", () => {
279 expect(resolveMarkdownFileLinkMeta("C:/Users/MIKE/project.ts", "c:/")).toMatchObject({
280 displayPath: "c:/Users/MIKE/project.ts",
281 workspaceRelativePath: "Users/MIKE/project.ts",
282 });
283 });
284
285 it("keeps backslash UNC workspace comparisons case-insensitive", () => {
286 expect(
287 resolveMarkdownFileLinkMeta(
288 "\\\\server\\share\\PROJECT\\src\\main.ts",
289 "\\\\Server\\Share\\Project",
290 ),
291 ).toMatchObject({
292 displayPath: "Project/src/main.ts",
293 workspaceRelativePath: "src/main.ts",
294 });
295 });
296
297 it.each([
298 ["/tmp/repo/file.ts", "/", "tmp/repo/file.ts"],
299 ["C:/Users/MIKE/file.ts", "c:/", "Users/MIKE/file.ts"],
300 ["\\\\server\\SHARE\\file.ts", "\\\\Server\\Share\\", "file.ts"],
301 ["/tmp/repo/file.ts%20", "/tmp/repo", "file.ts "],
302 ])("preserves the preview target for %s in workspace %s", (href, cwd, workspaceRelativePath) => {
303 expect(resolveMarkdownFileLinkMeta(href, cwd)).toMatchObject({ workspaceRelativePath });
304 });
305
306 it("keeps an encoded final space in the absolute target", () => {
307 expect(resolveMarkdownFileLinkTarget("/tmp/repo/file.ts%20", "/tmp/repo")).toBe(
308 "/tmp/repo/file.ts ",
309 );
310 });
311
257312 it("normalizes slash-prefixed windows drive paths before resolving", () => {
258313 expect(
259314 resolveMarkdownFileLinkTarget(
packages/client-runtime/src/markdownLinks.test.ts+11-1
......@@ -160,7 +160,17 @@ describe("workspaceRelativeFilePath", () => {
160160 ["/repo/project/src/main.ts", "/repo/project/", "src/main.ts"],
161161 ["C:\\Users\\mike\\t3code\\apps\\web\\a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"],
162162 ["/C:/Users/mike/t3code/apps/web/a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"],
163 ["/Repo/Project/src/main.ts", "/repo/project", "src/main.ts"],
163 ["/Repo/Project/src/main.ts", "/repo/project", null],
164 ["/tmp/case/project/probe.txt", "/tmp/case/Project", null],
165 ["//tmp/case/project/probe.txt", "//tmp/case/Project", null],
166 ["/tmp/case/Project/probe.txt", "/tmp/case/Project", "probe.txt"],
167 ["C:/USERS/mike/t3code/main.ts", "c:/users/MIKE/t3code", "main.ts"],
168 ["/C:/USERS/mike/t3code/main.ts", "/c:/users/MIKE/t3code", "main.ts"],
169 ["\\\\server\\share\\PROJECT\\main.ts", "\\\\Server\\Share\\Project", "main.ts"],
170 ["/tmp/repo/file.ts", "/", "tmp/repo/file.ts"],
171 ["C:/Users/MIKE/main.ts", "c:/", "Users/MIKE/main.ts"],
172 ["\\\\server\\SHARE\\file.ts", "\\\\Server\\Share\\", "file.ts"],
173 ["/tmp/repo/file.ts ", "/tmp/repo", "file.ts "],
164174 ["/tmp/report.ts", "/repo/project", null],
165175 ["/repo/project-two/a.ts", "/repo/project", null],
166176 ["/repo/project/a.ts", undefined, null],
packages/client-runtime/src/markdownLinks.ts+4-1
......@@ -342,6 +342,9 @@ export function workspaceRelativeFilePath(
342342 const normalizedRoot = stripSlashPrefixedWindowsDrive(
343343 workspaceRoot.replaceAll("\\", "/"),
344344 ).replace(/\/+$/, "");
345 if (!normalizedPath.toLowerCase().startsWith(`${normalizedRoot.toLowerCase()}/`)) return null;
345 const caseInsensitive = isWindowsAbsolutePath(stripSlashPrefixedWindowsDrive(workspaceRoot));
346 const pathForCompare = caseInsensitive ? normalizedPath.toLowerCase() : normalizedPath;
347 const rootForCompare = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot;
348 if (!pathForCompare.startsWith(`${rootForCompare}/`)) return null;
346349 return normalizedPath.slice(normalizedRoot.length + 1);
347350}