225 lines · 7.6 KB · TypeScript
RawDownload
export function getFileExtension(path: string): string {  const lastDot = path.lastIndexOf(".");  const lastSlash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));  if (lastDot <= lastSlash + 1) return "";  return path.slice(lastDot + 1);} const WINDOWS_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/;const EXPLICIT_URL_SCHEME = /^[A-Za-z][A-Za-z\d+.-]*:/;const MARKDOWN_ESCAPABLE = new Set(`!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~ \t`.split("")); export type LinkTarget =  | { kind: "internal"; path: string; anchor?: string }  | { kind: "same-doc-anchor"; anchor: string }  | { kind: "external-url"; url: string }  | { kind: "external-path"; path: string }; function splitLinkHref(href: string): string {  const hashIndex = href.indexOf("#");  const queryIndex = href.indexOf("?");  const splitIndex =    hashIndex === -1 ? queryIndex : queryIndex === -1 ? hashIndex : Math.min(hashIndex, queryIndex);  return splitIndex === -1 ? href : href.slice(0, splitIndex);} function extractAnchor(href: string): string | undefined {  const hashIndex = href.indexOf("#");  if (hashIndex === -1) return undefined;  const anchor = href.slice(hashIndex + 1);  return anchor ? decodeLinkPath(anchor) : undefined;} export function decodeLinkPath(path: string): string {  try {    return decodeURIComponent(path);  } catch {    return path;  }} function unescapeMarkdownDestination(path: string): string {  let result = "";  for (let index = 0; index < path.length; index++) {    const char = path[index]!;    const next = path[index + 1];    if (char === "\\" && next && MARKDOWN_ESCAPABLE.has(next)) {      result += next;      index++;      continue;    }    result += char;  }  return result;} export function normalizeMarkdownDestination(destination: string): string {  let normalized = destination.trim();  if (normalized.startsWith("<") && normalized.endsWith(">")) {    normalized = normalized.slice(1, -1).trim();  }  return unescapeMarkdownDestination(normalized);} export function normalizeLocalMarkdownDestination(destination: string): string {  return decodeLinkPath(normalizeMarkdownDestination(destination));} export function formatMarkdownDestination(destination: string): string {  if (!/[\s<>]/.test(destination)) return destination;  const escaped = destination.replace(/</g, "%3C").replace(/>/g, "%3E");  return `<${escaped}>`;} export function normalizePath(path: string): string {  const normalized = path.replace(/\\/g, "/");  const isWindowsAbsolute = WINDOWS_ABSOLUTE_PATH.test(normalized);  const hasLeadingSlash = normalized.startsWith("/");  const parts = normalized.split("/");  const stack: string[] = [];   for (const part of parts) {    if (!part || part === ".") continue;    if (part === "..") {      const last = stack[stack.length - 1];      if (last && last !== "..") {        if (isWindowsAbsolute || hasLeadingSlash || !WINDOWS_ABSOLUTE_PATH.test(last)) {          stack.pop();          continue;        }      }      if (!isWindowsAbsolute && !hasLeadingSlash) stack.push(part);      continue;    }    stack.push(part);  }   if (isWindowsAbsolute) {    const [drive, ...rest] = stack;    return rest.length === 0 ? `${drive}/` : `${drive}/${rest.join("/")}`;  }   if (hasLeadingSlash) {    return stack.length === 0 ? "/" : `/${stack.join("/")}`;  }   return stack.join("/");} function resolvePath(baseDir: string, target: string): string {  if (WINDOWS_ABSOLUTE_PATH.test(target) || target.startsWith("/")) {    return normalizePath(target);  }  return normalizePath(`${baseDir.replace(/\/$/, "")}/${target}`);} function isPathInsideRoot(path: string, root: string): boolean {  const normalizedPath = normalizePath(path);  const normalizedRoot = normalizePath(root).replace(/\/$/, "");  return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);} export type FileExistsFn = (path: string) => boolean | Promise<boolean>; export async function resolveLinkTarget(  href: string,  currentFilePath: string,  workspaceRoot?: string | null,  fileExists?: FileExistsFn,): Promise<LinkTarget | null> {  const trimmed = normalizeMarkdownDestination(href);  if (!trimmed) return null;  if (trimmed.startsWith("#")) {    const anchor = extractAnchor(trimmed);    return anchor ? { kind: "same-doc-anchor", anchor } : null;  }  if (EXPLICIT_URL_SCHEME.test(trimmed) && !WINDOWS_ABSOLUTE_PATH.test(trimmed)) {    return { kind: "external-url", url: trimmed };  }   const target = splitLinkHref(trimmed);  if (!target) return null;   const anchor = extractAnchor(trimmed);  const decodedTarget = decodeLinkPath(target);  const resolvedPath = resolvePath(getParentDir(currentFilePath), decodedTarget);   const extension = getFileExtension(resolvedPath).toLowerCase();  const isMarkdown = extension === "md" || extension === "markdown";   if (isMarkdown && (!workspaceRoot || isPathInsideRoot(resolvedPath, workspaceRoot))) {    return { kind: "internal", path: resolvedPath, ...(anchor ? { anchor } : {}) };  }   // Probe for markdown files when the resolved path has no recognized extension.  // For doc-corpus conventions (Hugo, Docusaurus, MDN, …), extensionless  // absolute paths like `/docs/foo/bar/` are site-root-relative, so we probe  // both their workspace-root mapping and their filesystem mapping.  if (extension === "" && fileExists) {    const bases: string[] = [resolvedPath.replace(/\/+$/, "")];    const isPosixAbsolute =      decodedTarget.startsWith("/") && !WINDOWS_ABSOLUTE_PATH.test(decodedTarget);    if (isPosixAbsolute && workspaceRoot) {      const rootRelative = normalizePath(        `${workspaceRoot.replace(/\/$/, "")}${decodedTarget}`,      ).replace(/\/+$/, "");      if (!bases.includes(rootRelative)) bases.push(rootRelative);    }     for (const base of bases) {      const candidates = [        `${base}.md`,        `${base}.markdown`,        `${base}/index.md`,        `${base}/index.markdown`,        `${base}/README.md`,      ];      for (const candidate of candidates) {        if (workspaceRoot && !isPathInsideRoot(candidate, workspaceRoot)) continue;        if (await fileExists(candidate)) {          return { kind: "internal", path: candidate, ...(anchor ? { anchor } : {}) };        }      }    }  }   return { kind: "external-path", path: resolvedPath };} export function getFileStem(path: string): string {  const name = getFileName(path);  const lastDot = name.lastIndexOf(".");  if (lastDot <= 0) return name;  return name.slice(0, lastDot);} export function getFileName(path: string): string {  const lastSlash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));  return path.slice(lastSlash + 1);} export function getRelativePath(fullPath: string, root: string): string {  const normalized = fullPath.replace(/\\/g, "/");  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/$/, "");  if (normalized.startsWith(normalizedRoot + "/")) {    return normalized.slice(normalizedRoot.length + 1);  }  return normalized;} export function getParentDir(path: string): string {  const normalized = path.replace(/\\/g, "/");  const lastSlash = normalized.lastIndexOf("/");  if (lastSlash <= 0) return "/";  return normalized.slice(0, lastSlash);} export function resolveImagePath(imageSrc: string, markdownDir: string): string {  const normalizedSrc = normalizeMarkdownDestination(imageSrc);  if (normalizedSrc.startsWith("http://") || normalizedSrc.startsWith("https://")) {    return normalizedSrc;  }  const localSrc = decodeLinkPath(normalizedSrc);  if (localSrc.startsWith("/")) return localSrc;  return `${markdownDir.replace(/\/$/, "")}/${localSrc}`;}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t