1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 resolveInlineCodeFileLinkMeta,
5 resolveMarkdownFileLinkMeta,
6 rewriteMarkdownFileUriHref,
7 shouldOpenMarkdownFileLinkInBrowserByDefault,
8 shouldOpenMarkdownFileLinkInEditor,
9} from "./markdown-links";
10
11describe("shouldOpenMarkdownFileLinkInEditor", () => {
12 it("uses command-click on macOS", () => {
13 expect(shouldOpenMarkdownFileLinkInEditor({ metaKey: true, ctrlKey: false }, "MacIntel")).toBe(
14 true,
15 );
16 expect(shouldOpenMarkdownFileLinkInEditor({ metaKey: false, ctrlKey: true }, "MacIntel")).toBe(
17 false,
18 );
19 });
20
21 it("uses control-click on other platforms", () => {
22 expect(
23 shouldOpenMarkdownFileLinkInEditor({ metaKey: false, ctrlKey: true }, "Linux x86_64"),
24 ).toBe(true);
25 expect(
26 shouldOpenMarkdownFileLinkInEditor({ metaKey: true, ctrlKey: false }, "Linux x86_64"),
27 ).toBe(false);
28 });
29});
30
31describe("shouldOpenMarkdownFileLinkInBrowserByDefault", () => {
32 it("keeps PDFs browser-first while source files open in the file viewer", () => {
33 expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.pdf")).toBe(true);
34 expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.PDF?download=1")).toBe(true);
35 expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.html")).toBe(false);
36 expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.xml")).toBe(false);
37 });
38});
39
40describe("rewriteMarkdownFileUriHref", () => {
41 it("rewrites file uri hrefs into direct path hrefs", () => {
42 expect(rewriteMarkdownFileUriHref("file:///Users/julius/project/src/main.ts#L42")).toBe(
43 "/Users/julius/project/src/main.ts#L42",
44 );
45 });
46
47 it("preserves encoded octets so file paths are decoded only once later", () => {
48 expect(rewriteMarkdownFileUriHref("file:///Users/julius/project/file%2520name.md")).toBe(
49 "/Users/julius/project/file%2520name.md",
50 );
51 });
52
53 it("normalizes file uri hrefs for windows drive paths", () => {
54 expect(
55 rewriteMarkdownFileUriHref(
56 "file:///D:/Programme/t3code/apps/web/src/components/chat/OpenInPicker.tsx#L69",
57 ),
58 ).toBe("D:/Programme/t3code/apps/web/src/components/chat/OpenInPicker.tsx#L69");
59 });
60
61 it("preserves file uri authorities as windows UNC paths", () => {
62 expect(rewriteMarkdownFileUriHref("file://server/share/workspace-image.svg")).toBe(
63 "\\\\server\\share\\workspace-image.svg",
64 );
65 });
66
67 it("treats a localhost file uri as a local path", () => {
68 expect(rewriteMarkdownFileUriHref("file://localhost/home/me/notes.md")).toBe(
69 "/home/me/notes.md",
70 );
71 });
72
73 it("unwraps angle-bracketed file uri hrefs", () => {
74 expect(
75 rewriteMarkdownFileUriHref(" <file:///D:/Programme/t3code/apps/web/src/markdown-links.ts> "),
76 ).toBe("D:/Programme/t3code/apps/web/src/markdown-links.ts");
77 });
78});
79
80describe("relative links inside a rendered host file", () => {
81 it("anchor to the file's directory while workspace membership follows cwd", () => {
82 const meta = resolveMarkdownFileLinkMeta("appendix.md", "/repo", "/tmp/report");
83 expect(meta).toMatchObject({
84 filePath: "/tmp/report/appendix.md",
85 workspaceRelativePath: null,
86 });
87 const inline = resolveInlineCodeFileLinkMeta("Makefile:12", "/repo", "/tmp/report");
88 expect(inline).toMatchObject({ filePath: "/tmp/report/Makefile", line: 12 });
89 expect(resolveMarkdownFileLinkMeta("src/main.ts", "/repo", "/repo/docs")).toMatchObject({
90 filePath: "/repo/docs/src/main.ts",
91 workspaceRelativePath: "docs/src/main.ts",
92 });
93 });
94
95 it("resolve multi-segment inline code from the workspace root in a workspace file", () => {
96 expect(
97 resolveInlineCodeFileLinkMeta("docs/ai/design.md", "/repo", "/repo/docs/ai"),
98 ).toMatchObject({ filePath: "/repo/docs/ai/design.md" });
99 expect(
100 resolveInlineCodeFileLinkMeta("src/index.ts:4", "/repo", "/repo/packages/a"),
101 ).toMatchObject({ filePath: "/repo/src/index.ts", line: 4 });
102 });
103
104 it("keep sibling and explicitly relative inline code beside the file", () => {
105 expect(resolveInlineCodeFileLinkMeta("design.md:12", "/repo", "/repo/docs/ai")).toMatchObject({
106 filePath: "/repo/docs/ai/design.md",
107 line: 12,
108 });
109 expect(
110 resolveInlineCodeFileLinkMeta("./src/index.ts", "/repo", "/repo/packages/a"),
111 ).toMatchObject({ filePath: "/repo/packages/a/./src/index.ts" });
112 expect(resolveInlineCodeFileLinkMeta("../b/notes.md", "/repo", "/repo/docs/a")).toMatchObject({
113 filePath: "/repo/docs/a/../b/notes.md",
114 });
115 });
116
117 it("keep multi-segment inline code beside a file outside the workspace", () => {
118 expect(resolveInlineCodeFileLinkMeta("src/main.ts", "/repo", "/tmp/report")).toMatchObject({
119 filePath: "/tmp/report/src/main.ts",
120 workspaceRelativePath: null,
121 });
122 });
123});
124
125describe("resolveInlineCodeFileLinkMeta", () => {
126 it("links relative paths with file extensions", () => {
127 expect(
128 resolveInlineCodeFileLinkMeta("docs/internals/workspace-layout.md", "/Users/julius/project"),
129 ).toMatchObject({
130 targetPath: "/Users/julius/project/docs/internals/workspace-layout.md",
131 basename: "workspace-layout.md",
132 });
133 });
134
135 it("links absolute posix paths", () => {
136 expect(resolveInlineCodeFileLinkMeta("/Users/julius/project/AGENTS.md")).toMatchObject({
137 targetPath: "/Users/julius/project/AGENTS.md",
138 });
139 expect(resolveInlineCodeFileLinkMeta("/usr/local/bin/tool")).toMatchObject({
140 targetPath: "/usr/local/bin/tool",
141 });
142 expect(resolveInlineCodeFileLinkMeta("/workspace/Makefile")).toMatchObject({
143 basename: "Makefile",
144 });
145 expect(resolveInlineCodeFileLinkMeta("/chat/settings")).toBeNull();
146 });
147
148 it("links windows drive paths", () => {
149 expect(resolveInlineCodeFileLinkMeta("C:\\Users\\mike\\project\\src\\main.ts")).toMatchObject({
150 basename: "main.ts",
151 });
152 });
153
154 it("links relative paths with line positions", () => {
155 expect(
156 resolveInlineCodeFileLinkMeta("src/processRunner.ts:71", "/Users/julius/project"),
157 ).toMatchObject({
158 targetPath: "/Users/julius/project/src/processRunner.ts:71",
159 line: 71,
160 });
161 });
162
163 it("links bare filenames only when a line suffix marks them as file references", () => {
164 expect(resolveInlineCodeFileLinkMeta("script.ts:10", "/Users/julius/project")).toMatchObject({
165 targetPath: "/Users/julius/project/script.ts:10",
166 line: 10,
167 });
168 expect(resolveInlineCodeFileLinkMeta("AGENTS.md", "/Users/julius/project")).toBeNull();
169 });
170
171 it("links extensionless bare filenames with a line suffix", () => {
172 expect(resolveInlineCodeFileLinkMeta("Makefile:12", "/Users/julius/project")).toMatchObject({
173 targetPath: "/Users/julius/project/Makefile:12",
174 basename: "Makefile",
175 line: 12,
176 });
177 expect(resolveInlineCodeFileLinkMeta("Dockerfile:8:2", "/Users/julius/project")).toMatchObject({
178 line: 8,
179 column: 2,
180 });
181 expect(resolveInlineCodeFileLinkMeta("Makefile:12")).toBeNull();
182 });
183
184 it("does not treat arbitrary name:digits shapes as files", () => {
185 expect(resolveInlineCodeFileLinkMeta("error:1", "/Users/julius/project")).toBeNull();
186 expect(resolveInlineCodeFileLinkMeta("TODO:12", "/Users/julius/project")).toBeNull();
187 expect(resolveInlineCodeFileLinkMeta("exit:0", "/Users/julius/project")).toBeNull();
188 expect(resolveInlineCodeFileLinkMeta("port:3000", "/Users/julius/project")).toBeNull();
189 expect(resolveInlineCodeFileLinkMeta("http:80", "/Users/julius/project")).toBeNull();
190 });
191
192 it("links dot-prefixed relative paths without extensions", () => {
193 expect(
194 resolveInlineCodeFileLinkMeta("./scripts/deploy", "/Users/julius/project"),
195 ).toMatchObject({
196 basename: "deploy",
197 });
198 });
199
200 it("links relative windows-style paths by normalizing backslashes", () => {
201 expect(resolveInlineCodeFileLinkMeta("src\\main.ts", "/Users/julius/project")).toMatchObject({
202 targetPath: "/Users/julius/project/src/main.ts",
203 basename: "main.ts",
204 });
205 expect(
206 resolveInlineCodeFileLinkMeta(".\\scripts\\deploy", "/Users/julius/project"),
207 ).toMatchObject({
208 basename: "deploy",
209 });
210 });
211
212 it("ignores hosts, ports, and versions", () => {
213 expect(resolveInlineCodeFileLinkMeta("127.0.0.1:3000", "/Users/julius/project")).toBeNull();
214 expect(resolveInlineCodeFileLinkMeta("localhost:3000", "/Users/julius/project")).toBeNull();
215 expect(
216 resolveInlineCodeFileLinkMeta("example.com/index.html", "/Users/julius/project"),
217 ).toBeNull();
218 expect(resolveInlineCodeFileLinkMeta("example.com:8080", "/Users/julius/project")).toBeNull();
219 expect(resolveInlineCodeFileLinkMeta("10.0.0.1:80:1", "/Users/julius/project")).toBeNull();
220 expect(
221 resolveInlineCodeFileLinkMeta("localhost/index.html", "/Users/julius/project"),
222 ).toBeNull();
223 expect(
224 resolveInlineCodeFileLinkMeta("example.uk/index.html", "/Users/julius/project"),
225 ).toBeNull();
226 });
227
228 it("still links files whose extension merely resembles a tld", () => {
229 expect(resolveInlineCodeFileLinkMeta("script.ts:10", "/Users/julius/project")).not.toBeNull();
230 expect(resolveInlineCodeFileLinkMeta("src/setup.sh:3", "/Users/julius/project")).not.toBeNull();
231 expect(resolveInlineCodeFileLinkMeta("Makefile.in:12", "/Users/julius/project")).not.toBeNull();
232 expect(
233 resolveInlineCodeFileLinkMeta("conf.d/nginx.conf", "/Users/julius/project"),
234 ).not.toBeNull();
235 });
236
237 it("prefers file over country-code host when a line suffix is present", () => {
238 expect(resolveInlineCodeFileLinkMeta("script.pl:10", "/Users/julius/project")).toMatchObject({
239 targetPath: "/Users/julius/project/script.pl:10",
240 line: 10,
241 });
242 expect(resolveInlineCodeFileLinkMeta("model.pt:3", "/Users/julius/project")).not.toBeNull();
243 expect(
244 resolveInlineCodeFileLinkMeta("example.pl/index.html", "/Users/julius/project"),
245 ).toBeNull();
246 expect(resolveInlineCodeFileLinkMeta("example.com:8080", "/Users/julius/project")).toBeNull();
247 });
248
249 it("ignores commands, flags, and expressions", () => {
250 expect(resolveInlineCodeFileLinkMeta("git worktree list --porcelain")).toBeNull();
251 expect(resolveInlineCodeFileLinkMeta("node.meta", "/Users/julius/project")).toBeNull();
252 expect(resolveInlineCodeFileLinkMeta("pnpm install", "/Users/julius/project")).toBeNull();
253 expect(resolveInlineCodeFileLinkMeta("src/**/*.ts", "/Users/julius/project")).toBeNull();
254 });
255
256 it("ignores extension-less relative segments like git refs and directories", () => {
257 expect(resolveInlineCodeFileLinkMeta("origin/main", "/Users/julius/project")).toBeNull();
258 expect(resolveInlineCodeFileLinkMeta("apps/web", "/Users/julius/project")).toBeNull();
259 });
260
261 it("ignores external urls", () => {
262 expect(resolveInlineCodeFileLinkMeta("https://example.com/docs.html")).toBeNull();
263 });
264
265 it("ignores relative paths without a cwd to resolve against", () => {
266 expect(resolveInlineCodeFileLinkMeta("docs/internals/workspace-layout.md")).toBeNull();
267 });
268});
269
270describe("directory paths with a trailing separator", () => {
271 it("keeps the final segment for a POSIX directory path", () => {
272 expect(resolveMarkdownFileLinkMeta("/tmp/favicons/", "/repo/project")).toMatchObject({
273 basename: "favicons",
274 });
275 });
276
277 it("keeps the final segment for a Windows directory path", () => {
278 expect(
279 resolveMarkdownFileLinkMeta("C:\\Users\\kelchm\\.claude\\", "/repo/project"),
280 ).toMatchObject({ basename: ".claude" });
281 });
282
283 it("matches the label of the same path without a trailing separator", () => {
284 const withSlash = resolveMarkdownFileLinkMeta("/tmp/favicons/", "/repo/project");
285 const withoutSlash = resolveMarkdownFileLinkMeta("/tmp/favicons", "/repo/project");
286 expect(withSlash?.basename).toBe(withoutSlash?.basename);
287 });
288
289 it("does not produce an empty label for the filesystem root", () => {
290 const meta = resolveMarkdownFileLinkMeta("/tmp/", "/repo/project");
291 expect(meta?.basename).not.toBe("");
292 });
293});
294
295it("routes the project-root code link to the workspace explorer", () => {
296 const cwd = "/Users/saphid/.t3/worktrees/ov2-standalone-20260918";
297 expect(resolveInlineCodeFileLinkMeta(cwd, cwd)).toMatchObject({
298 workspaceRelativePath: ".",
299 filePath: cwd,
300 });
301});