175 lines · 4.9 KB · TypeScript
RawDownload
import { isMacPlatform } from "./lib/utils"; export type TerminalLinkKind = "url" | "path"; export interface TerminalLinkMatch {  kind: TerminalLinkKind;  text: string;  start: number;  end: number;} const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/g;const FILE_PATH_PATTERN =  /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/; function trimClosingDelimiters(value: string): string {  let output = value.replace(TRAILING_PUNCTUATION_PATTERN, "");  if (output.length === 0) return output;   const trimUnbalanced = (open: string, close: string) => {    while (output.endsWith(close)) {      const opens = output.split(open).length - 1;      const closes = output.split(close).length - 1;      if (opens >= closes) return;      output = output.slice(0, -1);    }  };   trimUnbalanced("(", ")");  trimUnbalanced("[", "]");  trimUnbalanced("{", "}");  return output;} function overlaps(a: { start: number; end: number }, b: { start: number; end: number }): boolean {  return a.start < b.end && b.start < a.end;} function collectMatches(  line: string,  kind: TerminalLinkKind,  pattern: RegExp,  existing: TerminalLinkMatch[],): TerminalLinkMatch[] {  const matches: TerminalLinkMatch[] = [];  pattern.lastIndex = 0;   for (const rawMatch of line.matchAll(pattern)) {    const raw = rawMatch[0];    const start = rawMatch.index ?? -1;    if (start < 0 || raw.length === 0) continue;     const trimmed = trimClosingDelimiters(raw);    if (trimmed.length === 0) continue;    if (kind === "path" && /^https?:\/\//i.test(trimmed)) continue;     const candidate: TerminalLinkMatch = {      kind,      text: trimmed,      start,      end: start + trimmed.length,    };     const collides = [...existing, ...matches].some((other) => overlaps(candidate, other));    if (collides) continue;     matches.push(candidate);  }   return matches;} function isWindowsAbsolutePath(value: string): boolean {  return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");} function isAbsolutePath(value: string): boolean {  return value.startsWith("/") || isWindowsAbsolutePath(value);} function isWindowsPathStyle(value: string): boolean {  return isWindowsAbsolutePath(value) || /[A-Za-z]:\\/.test(value);} function joinPath(base: string, next: string, separator: "/" | "\\"): string {  const cleanBase = base.replace(/[\\/]+$/, "");  if (separator === "\\") {    return `${cleanBase}\\${next.replaceAll("/", "\\")}`;  }  return `${cleanBase}/${next.replace(/^\/+/, "")}`;} function inferHomeFromCwd(cwd: string): string | undefined {  const posixUser = cwd.match(/^\/Users\/([^/]+)/);  if (posixUser?.[1]) {    return `/Users/${posixUser[1]}`;  }   const posixHome = cwd.match(/^\/home\/([^/]+)/);  if (posixHome?.[1]) {    return `/home/${posixHome[1]}`;  }   const windowsUser = cwd.match(/^([A-Za-z]:\\Users\\[^\\]+)/);  if (windowsUser?.[1]) {    return windowsUser[1];  }   return undefined;} function splitPathAndPosition(value: string): {  path: string;  line: string | undefined;  column: string | undefined;} {  let path = value;  let column: string | undefined;  let line: string | undefined;   const columnMatch = path.match(/:(\d+)$/);  if (!columnMatch?.[1]) {    return { path, line: undefined, column: undefined };  }   column = columnMatch[1];  path = path.slice(0, -columnMatch[0].length);   const lineMatch = path.match(/:(\d+)$/);  if (lineMatch?.[1]) {    line = lineMatch[1];    path = path.slice(0, -lineMatch[0].length);  } else {    line = column;    column = undefined;  }   return { path, line, column };} export function extractTerminalLinks(line: string): TerminalLinkMatch[] {  const urlMatches = collectMatches(line, "url", URL_PATTERN, []);  const pathMatches = collectMatches(line, "path", FILE_PATH_PATTERN, urlMatches);  return [...urlMatches, ...pathMatches].toSorted((a, b) => a.start - b.start);} export function isTerminalLinkActivation(  event: Pick<MouseEvent, "metaKey" | "ctrlKey">,  platform = typeof navigator === "undefined" ? "" : navigator.platform,): boolean {  if (platform.length === 0) return false;  return isMacPlatform(platform)    ? event.metaKey && !event.ctrlKey    : event.ctrlKey && !event.metaKey;} export function resolvePathLinkTarget(rawPath: string, cwd: string): string {  const { path, line, column } = splitPathAndPosition(rawPath);   let resolvedPath = path;  if (path.startsWith("~/")) {    const home = inferHomeFromCwd(cwd);    if (home) {      const separator: "/" | "\\" = isWindowsPathStyle(home) ? "\\" : "/";      resolvedPath = joinPath(home, path.slice(2), separator);    }  } else if (!isAbsolutePath(path)) {    const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/";    resolvedPath = joinPath(cwd, path, separator);  }   if (!line) return resolvedPath;  return `${resolvedPath}:${line}${column ? `:${column}` : ""}`;}