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, 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 } : {}), }; }