248 lines · 8.3 KB
import MarkdownIt from "markdown-it";
import type { MarkdownIt as MarkdownRenderer, StateCore } from "markdown-it";
import { highlight } from "sugar-high";
import { FilterXSS, safeAttrValue } from "xss";
import { linkTextReferences } from "./references";
import { camoUrl } from "./camo-sign";

export type MarkdownContext = {
  owner: string;
  repo: string;
  ref: string;
  /** Directory of the document inside the repo, used to resolve relative links. */
  basePath: string;
};

const ATTACHMENT_PREFIX = "/attachments/";
const ABSOLUTE_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
/** GitHub's prefix for user-supplied ids/anchors, so content cannot clobber `window.<id>` or app anchors. */
const USER_CONTENT_PREFIX = "user-content-";
const EXTERNAL_URL = /^(?:https?:)?\/\//i;
const DATA_IMAGE = /^data:image\/(png|gif|jpe?g|webp);/i;

/** External images go through the signed same-origin proxy; relative, data: and attachment URLs stay as they are. */
function imageSource(url: string): string {
  if (!EXTERNAL_URL.test(url)) return url;
  return camoUrl(url.startsWith("//") ? `https:${url}` : url) ?? "";
}

function userContentId(value: string): string {
  const cleaned = value.trim();
  return cleaned.length === 0 || cleaned.startsWith(USER_CONTENT_PREFIX) ? cleaned : `${USER_CONTENT_PREFIX}${cleaned}`;
}

/** Same-document anchors must point at the prefixed ids this renderer emits. */
function anchorHref(url: string): string {
  if (!url.startsWith("#") || url.length === 1) return url;
  return `#${userContentId(url.slice(1))}`;
}

function createRenderer(): MarkdownRenderer {