295 lines · 9.8 KB
import MarkdownIt from "markdown-it";
import type { MentionMap } from "./mentions";
import type { MarkdownIt as MarkdownRenderer, StateCore } from "markdown-it";
import { highlight } from "sugar-high";
import { FilterXSS, friendlyAttrValue, safeAttrValue } from "xss";
import { linkTextReferences } from "./references";
import { camoUrl, isOwnHost } from "./camo-sign";
import { userContentOriginFor } from "./usercontent-origin";
import { env } from "cloudflare:workers";

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 LOCAL_BASES = ["https://local.invalid/", "http://local.invalid/"];

/**
 * Parsed like a browser would against an https and an http page, so `//host`, `http:host` (on https) and
 * `https:host` (on http) all count as leaving the page's origin.
 */
function isExternalUrl(value: string): boolean {
  return LOCAL_BASES.some((base) => {
    try {
      return new URL(value.trim(), base).hostname !== "local.invalid";
    } catch {
      return false;
    }
  });
}

function externalHref(value: string): string {
  for (const base of LOCAL_BASES) {