import MarkdownIt from "markdown-it"; import { type Html, trusted } from "./html.ts"; import { pathUrl } from "./layout.ts"; /** Where a Markdown file lives, so its relative links and images can point at the repo's raw files. */ export type MarkdownBase = { repo: string; dir: string; at: string | null }; const SCHEME = /^([a-z][a-z0-9+.-]*):/i; const ALLOWED_SCHEMES = ["http", "https", "mailto"]; // §25: raw HTML is off, so markdown-it's own escaping is the whole sanitizer. const md = new MarkdownIt({ html: false, linkify: true }); md.validateLink = (url) => { const scheme = SCHEME.exec(url.trim()); return !scheme || ALLOWED_SCHEMES.includes((scheme[1] as string).toLowerCase()); }; md.core.ruler.push("kepr_raw_links", (state) => { const base = state.env as MarkdownBase; const visit = (tokens: typeof state.tokens) => { for (const token of tokens) { const attr = token.type === "link_open" ? "href" : token.type === "image" ? "src" : null; const url = attr && token.attrGet(attr); if (attr && typeof url === "string") token.attrSet(attr, rawUrl(url, base)); if (token.children) visit(token.children); } }; visit(state.tokens); }); function rawUrl(url: string, base: MarkdownBase): string { if (SCHEME.test(url) || url.startsWith("#") || url.startsWith("//")) return url; // A leading `/` means the repo root, as it does on a code host. const resolved = new URL(url, `http://repo/${base.dir ? `${pathUrl(base.dir)}/` : ""}`); const at = base.at ? `?at=${base.at}` : ""; return `/${base.repo}/raw${resolved.pathname}${at}${resolved.hash}`; } export function renderMarkdown(source: string, base: MarkdownBase): Html { return trusted(md.render(source, base)); } /** The README's first paragraph (§21.4), as markup and as plain text for link previews. */ export function firstParagraph( source: string, base: MarkdownBase, ): { html: Html; text: string } | null { const tokens = md.parse(source, base); const index = tokens.findIndex((token) => token.type === "paragraph_open"); const inline = tokens[index + 1]; if (index < 0 || !inline?.children) return null; const text = inline.children .filter((token) => token.type === "text" || token.type === "code_inline") .map((token) => token.content) .join(""); return { html: trusted(md.renderer.renderInline(inline.children, md.options, base)), text }; } /** One line of Markdown with links and images flattened away, for a row that is itself a link. */ export function renderLine(source: string, base: MarkdownBase): Html { const tokens = md.parseInline(source, base)[0]?.children ?? []; const kept = tokens.filter((token) => !/^(link_open|link_close|image)$/.test(token.type)); return trusted(md.renderer.renderInline(kept, md.options, base)); }