1import { fileBasename, workspaceRelativeFilePath } from "@t3tools/shared/path";
2import {
3 inlineCodeFilePathCandidate,
4 isRelativeFilePath,
5 normalizeMarkdownLinkDestination,
6 resolveMarkdownFileLinkTarget,
7} from "@t3tools/shared/markdownLinks";
8import { parseFileUrlHref, splitFilePathPosition } from "@t3tools/shared/fileLinks";
9
10import { formatWorkspaceRelativePath } from "./filePathDisplay";
11import { isTerminalLinkActivation } from "./terminal-links";
12
13export interface MarkdownFileLinkMeta {
14 filePath: string;
15 targetPath: string;
16 displayPath: string;
17 workspaceRelativePath: string | null;
18 basename: string;
19 line?: number;
20 column?: number;
21}
22
23export function shouldOpenMarkdownFileLinkInEditor(
24 event: Pick<MouseEvent, "metaKey" | "ctrlKey">,
25 platform?: string,
26): boolean {
27 return isTerminalLinkActivation(event, platform);
28}
29
30export function shouldOpenMarkdownFileLinkInBrowserByDefault(path: string): boolean {
31 return /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? "");
32}
33
34export function rewriteMarkdownFileUriHref(href: string | undefined): string | null {
35 if (!href) return null;
36 const target = parseFileUrlHref(normalizeMarkdownLinkDestination(href));
37 return target ? `${target.path}${target.hash}` : null;
38}
39
40/**
41 * Inline code spans mostly hold identifiers, commands, and refs (`node.meta`,
42 * `origin/main`) rather than deliberate link destinations, so auto-linking
43 * them demands stronger path evidence than an explicit markdown link does:
44 * an unambiguous path prefix, a file extension, or a :line suffix.
45 */
46export function resolveInlineCodeFileLinkMeta(
47 codeText: string,
48 cwd?: string,
49 baseDir: string | undefined = cwd,
50): MarkdownFileLinkMeta | null {
51 const candidate = inlineCodeFilePathCandidate(codeText);
52 if (candidate === null) return null;
53
54 return resolveMarkdownFileLinkMeta(
55 candidate,
56 cwd,
57 inlineCodePathNamesFromWorkspaceRoot(candidate, cwd, baseDir) ? cwd : baseDir,
58 );
59}
60
61/**
62 * Prose in a workspace file names other files from the repo root (`docs/ai/design.md`),
63 * unlike an explicit link. Single-segment names (`design.md:12`) and `./`, `../`
64 * paths still read as siblings, and files outside the workspace keep their own base.
65 */
66function inlineCodePathNamesFromWorkspaceRoot(
67 candidate: string,
68 cwd: string | undefined,
69 baseDir: string | undefined,
70): boolean {
71 if (!cwd || !baseDir || !isRelativeFilePath(candidate)) return false;
72 if (/^(?:~|\.{1,2})\//.test(candidate)) return false;
73 if (!splitFilePathPosition(candidate).path.includes("/")) return false;
74 return workspaceRelativeFilePath(baseDir, cwd) !== null;
75}
76
77export function resolveMarkdownFileLinkMeta(
78 href: string | undefined,
79 cwd?: string,
80 baseDir: string | undefined = cwd,
81): MarkdownFileLinkMeta | null {
82 const targetPath = resolveMarkdownFileLinkTarget(href, cwd, baseDir);
83 if (!targetPath) return null;
84 return buildFileLinkMetaFromTarget(targetPath, cwd);
85}
86
87function buildFileLinkMetaFromTarget(targetPath: string, cwd?: string): MarkdownFileLinkMeta {
88 const { path, line, column } = splitFilePathPosition(targetPath);
89 return {
90 filePath: path,
91 targetPath,
92 displayPath: formatWorkspaceRelativePath(targetPath, cwd),
93 workspaceRelativePath: workspaceRelativeFilePath(path, cwd),
94 basename: fileBasename(path),
95 ...(line !== undefined ? { line } : {}),
96 ...(column !== undefined ? { column } : {}),
97 };
98}