113 lines · 3.8 KB
import MarkdownIt from "markdown-it";
import type { MarkdownIt as MarkdownRenderer, StateCore } from "markdown-it";
import { highlight } from "sugar-high";

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

const ABSOLUTE_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
const ISSUE_REFERENCE = /(^|[\s(])#(\d+)\b/g;
const MENTION = /(^|[\s(])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\b/g;

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

function createRenderer(): MarkdownRenderer {
  const md = new MarkdownIt({
    html: false,
    linkify: true,
    typographer: false,
    highlight: (code) => `<pre class="sh-code"><code>${highlight(code)}</code></pre>`,
  });
  md.validateLink = (url) =>
    !/^(javascript|vbscript|data):/i.test(url.trim()) || /^data:image\/(png|gif|jpe?g|webp);/i.test(url.trim());
  return md;
}

const renderer = createRenderer();

function resolveRepoPath(basePath: string, target: string): string {
  const parts = (target.startsWith("/") ? [] : basePath.split("/").filter(Boolean)).concat(target.split("/"));
  const out: string[] = [];
  for (const part of parts) {
    if (!part || part === ".") continue;
    if (part === "..") out.pop();