106 lines · 3.8 KB
import type { MentionMap } from "./mentions";

const NAME = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})";
const REPO = "[A-Za-z0-9._-]{1,100}";
const TEAM = "[A-Za-z0-9][A-Za-z0-9_-]{0,99}";
const SHA = "[0-9a-f]{7,40}";
const SHORT_SHA = 7;

/** One pass over plain text so inserted anchors are never re-scanned by a later pattern. */
const REFERENCE = new RegExp(
  `(?<prefix>^|[\\s(])(?:${[
    `@(?<teamOrg>${NAME})\\/(?<team>${TEAM})`,
    `@(?<user>${NAME})`,
    `(?<issueOwner>${NAME})\\/(?<issueRepo>${REPO})#(?<issueNumber>\\d+)`,
    `(?<commitOwner>${NAME})\\/(?<commitRepo>${REPO})@(?<commitSha>${SHA})`,
    `#(?<localNumber>\\d+)`,
    `(?<localSha>${SHA})`,
  ].join("|")})\\b`,
  "g",
);

type Groups = Partial<Record<string, string>>;

function escapeHtml(value: string): string {
  return value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
}

function anchor(href: string, text: string, kind: string, ref: string, className = ""): string {
  const classAttr = className ? ` class="${className}"` : "";
  return `<a href="${escapeHtml(href)}"${classAttr} data-hovercard="${kind}" data-ref="${escapeHtml(ref)}">${escapeHtml(text)}</a>`;
}

/**
 * Links `@user`, `@org/team`, `#123`, `owner/repo#123`, `owner/repo@sha` and bare commit SHAs in a text
 * segment of rendered HTML. Anchors carry `data-hovercard` so the client can show previews.
 */
export function linkTextReferences(
  text: string,
  context: { owner: string; repo: string } | null,
  mentions: MentionMap | null = null,