import { fileBasename, formatFilePathPosition, inlineCodeFilePathCandidate, isRelativeFilePath, normalizeMarkdownLinkDestination, parseFileUrlHref, parseMarkdownFileLink, safeDecodeURIComponent, splitFilePathPosition, workspaceRelativeFilePath,} from "@t3tools/client-runtime/markdown-links"; import { formatWorkspaceRelativePath } from "./filePathDisplay";import { isTerminalLinkActivation, resolvePathLinkTarget } from "./terminal-links"; export { normalizeMarkdownLinkDestination }; const MARKDOWN_LINK_HREF_PATTERN = /\[[^\]]*]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)/g; export interface MarkdownFileLinkMeta { filePath: string; targetPath: string; displayPath: string; workspaceRelativePath: string | null; basename: string; line?: number; column?: number;} export function extractMarkdownLinkHrefs(markdown: string): string[] { const hrefs: string[] = []; for (const match of markdown.matchAll(MARKDOWN_LINK_HREF_PATTERN)) { const href = (match[1] ?? match[2])?.trim(); if (href) hrefs.push(href); } return hrefs;} export function shouldOpenMarkdownFileLinkInEditor( event: Pick<MouseEvent, "metaKey" | "ctrlKey">, platform?: string,): boolean { return isTerminalLinkActivation(event, platform);} export function shouldOpenMarkdownFileLinkInBrowserByDefault(path: string): boolean { return /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? "");} export function isWindowsDrivePathHref(href: string): boolean { return /^[A-Za-z]:[\\/]/.test(safeDecodeURIComponent(href));} export function rewriteMarkdownFileUriHref(href: string | undefined): string | null { if (!href) return null; const target = parseFileUrlHref(normalizeMarkdownLinkDestination(href)); return target ? `${target.path}${target.hash}` : null;} /** * `baseDir` anchors relative links; it defaults to the workspace root and is the * file's own directory when rendering a markdown file. `cwd` stays the workspace * root so the result still knows whether the target is inside it. */export function resolveMarkdownFileLinkTarget( href: string | undefined, cwd?: string, baseDir: string | undefined = cwd,): string | null { if (!href) return null; const target = parseMarkdownFileLink(href); if (!target) return null; const pathWithPosition = formatFilePathPosition(target); if (!isRelativeFilePath(pathWithPosition)) return pathWithPosition; if (!baseDir) return null; return resolvePathLinkTarget(pathWithPosition, baseDir);} /** * Inline code spans mostly hold identifiers, commands, and refs (`node.meta`, * `origin/main`) rather than deliberate link destinations, so auto-linking * them demands stronger path evidence than an explicit markdown link does: * an unambiguous path prefix, a file extension, or a :line suffix. */export function resolveInlineCodeFileLinkMeta( codeText: string, cwd?: string, baseDir: string | undefined = cwd,): MarkdownFileLinkMeta | null { const candidate = inlineCodeFilePathCandidate(codeText); if (candidate === null) return null; return resolveMarkdownFileLinkMeta(candidate, cwd, baseDir);} export function resolveMarkdownFileLinkMeta( href: string | undefined, cwd?: string, baseDir: string | undefined = cwd,): MarkdownFileLinkMeta | null { const targetPath = resolveMarkdownFileLinkTarget(href, cwd, baseDir); if (!targetPath) return null; return buildFileLinkMetaFromTarget(targetPath, cwd);} function buildFileLinkMetaFromTarget(targetPath: string, cwd?: string): MarkdownFileLinkMeta { const { path, line, column } = splitFilePathPosition(targetPath); return { filePath: path, targetPath, displayPath: formatWorkspaceRelativePath(targetPath, cwd), workspaceRelativePath: workspaceRelativeFilePath(path, cwd), basename: fileBasename(path), ...(line !== undefined ? { line } : {}), ...(column !== undefined ? { column } : {}), };}
import {
fileBasename,
formatFilePathPosition,
inlineCodeFilePathCandidate,
isRelativeFilePath,
normalizeMarkdownLinkDestination,
parseFileUrlHref,
parseMarkdownFileLink,
safeDecodeURIComponent,
splitFilePathPosition,
workspaceRelativeFilePath,
} from "@t3tools/client-runtime/markdown-links";
import { formatWorkspaceRelativePath } from "./filePathDisplay";
import { isTerminalLinkActivation, resolvePathLinkTarget } from "./terminal-links";
export { normalizeMarkdownLinkDestination };
const MARKDOWN_LINK_HREF_PATTERN =
/\[[^\]]*]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)/g;
export interface MarkdownFileLinkMeta {
filePath: string;
targetPath: string;
displayPath: string;
workspaceRelativePath: string | null;
basename: string;
line?: number;
column?: number;
}
export function extractMarkdownLinkHrefs(markdown: string): string[] {
const hrefs: string[] = [];
for (const match of markdown.matchAll(MARKDOWN_LINK_HREF_PATTERN)) {
const href = (match[1] ?? match[2])?.trim();
if (href) hrefs.push(href);
}
return hrefs;
}
export function shouldOpenMarkdownFileLinkInEditor(
event: Pick<MouseEvent, "metaKey" | "ctrlKey">,
platform?: string,
): boolean {
return isTerminalLinkActivation(event, platform);
}
export function shouldOpenMarkdownFileLinkInBrowserByDefault(path: string): boolean {
return /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? "");
}
export function isWindowsDrivePathHref(href: string): boolean {
return /^[A-Za-z]:[\\/]/.test(safeDecodeURIComponent(href));
}
export function rewriteMarkdownFileUriHref(href: string | undefined): string | null {
if (!href) return null;
const target = parseFileUrlHref(normalizeMarkdownLinkDestination(href));
return target ? `${target.path}${target.hash}` : null;
}
/**
* `baseDir` anchors relative links; it defaults to the workspace root and is the
* file's own directory when rendering a markdown file. `cwd` stays the workspace
* root so the result still knows whether the target is inside it.
*/
export function resolveMarkdownFileLinkTarget(
href: string | undefined,
cwd?: string,
baseDir: string | undefined = cwd,
): string | null {
if (!href) return null;
const target = parseMarkdownFileLink(href);
if (!target) return null;
const pathWithPosition = formatFilePathPosition(target);
if (!isRelativeFilePath(pathWithPosition)) return pathWithPosition;
if (!baseDir) return null;
return resolvePathLinkTarget(pathWithPosition, baseDir);
}
/**
* Inline code spans mostly hold identifiers, commands, and refs (`node.meta`,
* `origin/main`) rather than deliberate link destinations, so auto-linking
* them demands stronger path evidence than an explicit markdown link does:
* an unambiguous path prefix, a file extension, or a :line suffix.
*/
export function resolveInlineCodeFileLinkMeta(
codeText: string,
cwd?: string,
baseDir: string | undefined = cwd,
): MarkdownFileLinkMeta | null {
const candidate = inlineCodeFilePathCandidate(codeText);
if (candidate === null) return null;
return resolveMarkdownFileLinkMeta(candidate, cwd, baseDir);
}
export function resolveMarkdownFileLinkMeta(
href: string | undefined,
cwd?: string,
baseDir: string | undefined = cwd,
): MarkdownFileLinkMeta | null {
const targetPath = resolveMarkdownFileLinkTarget(href, cwd, baseDir);
if (!targetPath) return null;
return buildFileLinkMetaFromTarget(targetPath, cwd);
}
function buildFileLinkMetaFromTarget(targetPath: string, cwd?: string): MarkdownFileLinkMeta {
const { path, line, column } = splitFilePathPosition(targetPath);
return {
filePath: path,
targetPath,
displayPath: formatWorkspaceRelativePath(targetPath, cwd),
workspaceRelativePath: workspaceRelativeFilePath(path, cwd),
basename: fileBasename(path),
...(line !== undefined ? { line } : {}),
...(column !== undefined ? { column } : {}),
};
}