markdown.ts

2.9 KB · raw

1import MarkdownIt from "markdown-it";2import { type Html, trusted } from "./html.ts";3import { pathUrl } from "./layout.ts";45/** Where a Markdown file lives, so its relative links and images can point at the repo's raw files. */6export type MarkdownBase = { repo: string; dir: string; at: string | null };78const SCHEME = /^([a-z][a-z0-9+.-]*):/i;9const ALLOWED_SCHEMES = ["http", "https", "mailto"];1011// §25: raw HTML is off, so markdown-it's own escaping is the whole sanitizer.12const md = new MarkdownIt({ html: false, linkify: true });13md.validateLink = (url) => {14	const scheme = SCHEME.exec(url.trim());15	return !scheme || ALLOWED_SCHEMES.includes((scheme[1] as string).toLowerCase());16};17md.core.ruler.push("kepr_raw_links", (state) => {18	const base = state.env as MarkdownBase;19	// Kepr's own pages (docs, support) have no repo, so their links stay as written.20	if (!base.repo) return;21	const visit = (tokens: typeof state.tokens) => {22		for (const token of tokens) {23			const attr = token.type === "link_open" ? "href" : token.type === "image" ? "src" : null;24			const url = attr && token.attrGet(attr);25			if (attr && typeof url === "string") token.attrSet(attr, rawUrl(url, base));26			if (token.children) visit(token.children);27		}28	};29	visit(state.tokens);30});3132function rawUrl(url: string, base: MarkdownBase): string {33	if (SCHEME.test(url) || url.startsWith("#") || url.startsWith("//")) return url;34	// A leading `/` means the repo root, as it does on a code host.35	const resolved = new URL(url, `http://repo/${base.dir ? `${pathUrl(base.dir)}/` : ""}`);36	const at = base.at ? `?at=${base.at}` : "";37	return `/${base.repo}/raw${resolved.pathname}${at}${resolved.hash}`;38}3940export function renderMarkdown(source: string, base: MarkdownBase): Html {41	return trusted(md.render(source, base));42}4344/** One of Kepr's own Markdown pages, such as the docs (§21.10). */45export function renderSiteMarkdown(source: string): Html {46	return trusted(md.render(source, {}));47}4849/**50 * A paragraph of Markdown source (the README's first paragraph, found as the Atlas finds it) as51 * inline markup, and as plain text for link previews.52 */53export function renderParagraph(source: string, base: MarkdownBase): { html: Html; text: string } {54	const children = md.parseInline(source, base)[0]?.children ?? [];55	const text = children56		.map((token) =>57			token.type === "text" || token.type === "code_inline"58				? token.content59				: token.type === "softbreak"60					? " "61					: "",62		)63		.join("");64	return { html: trusted(md.renderer.renderInline(children, md.options, base)), text };65}6667/** One line of Markdown with links and images flattened away, for a row that is itself a link. */68export function renderLine(source: string, base: MarkdownBase): Html {69	const tokens = md.parseInline(source, base)[0]?.children ?? [];70	const kept = tokens.filter((token) => !/^(link_open|link_close|image)$/.test(token.type));71	return trusted(md.renderer.renderInline(kept, md.options, base));72}