import Prism from "prismjs"; import "prismjs/components/prism-lua.js"; import "prismjs/components/prism-markdown.js"; import { escape as escapeHtml } from "@std/html"; import { mangle } from "marked-mangle"; import GitHubSlugger from "github-slugger"; import * as Marked from "marked"; const slugger = new GitHubSlugger(); Marked.marked.use(mangle()); const ADMISSION_REG = /^\[(info|warn|tip)\]:\s/; export interface MarkdownHeading { id: string; html: string; } class DefaultRenderer extends Marked.Renderer { headings: MarkdownHeading[] = []; override text( token: Marked.Tokens.Text | Marked.Tokens.Escape | Marked.Tokens.Tag, ): string { if ( token.type === "text" && "tokens" in token && token.tokens !== undefined ) { return this.parser.parseInline(token.tokens); } // Smartypants typography enhancement return token.text .replaceAll("...", "…") .replaceAll("--", "—") .replaceAll("---", "–") .replaceAll(/(\w)'(\w)/g, "$1’$2") .replaceAll(/s'/g, "s’") .replaceAll("'", "’") .replaceAll(/["](.*?)["]/g, "“$1”") .replaceAll(/"(.*?)"/g, "“$1”") .replaceAll(/['](.*?)[']/g, "‘$1’"); } override heading({ tokens, depth, raw, }: Marked.Tokens.Heading): string { const slug = slugger.slug(raw); const text = this.parser.parseInline(tokens); this.headings.push({ id: slug, html: text }); return `${text}`; } override link({ href, title, tokens }: Marked.Tokens.Link) { const text = this.parser.parseInline(tokens); const titleAttr = title ? ` title="${title}"` : ""; if (href.startsWith("#")) { return `${text}`; } return `${text}`; } override image({ href, text, title }: Marked.Tokens.Image) { return `${text ?? `; } override code({ lang: info, text }: Marked.Tokens.Code): string { // format: tsx // format: tsx my/file.ts // format: tsx "This is my title" let lang = ""; let title = ""; const match = info?.match(/^([\w_-]+)\s*(.*)?$/); if (match) { lang = match[1].toLocaleLowerCase(); title = match[2] ?? ""; } let out = `
`; if (title) { out += `
${title ? escapeHtml(String(title)) : " "}
`; } const grammar = lang && Object.hasOwnProperty.call(Prism.languages, lang) ? Prism.languages[lang] : undefined; if (grammar === undefined) { out += `
${escapeHtml(text)}
`; } else { const html = Prism.highlight(text, grammar, lang); out += `
${html}
`; } out += `
`; return out; } override blockquote({ text, tokens }: Marked.Tokens.Blockquote): string { const match = text.match(ADMISSION_REG); if (match) { const label: Record = { tip: "Tip", warn: "Warning", info: "Info", }; Marked.walkTokens(tokens, (token) => { if (token.type === "text" && token.text.startsWith(match[0])) { token.text = token.text.slice(match[0].length); } }); const type = match[1]; const icon = ``; return `
\n${icon}${ label[type] }${Marked.parser(tokens)}
\n`; } return `
\n${Marked.parser(tokens)}
\n`; } } export interface MarkdownHeading { id: string; html: string; } export interface MarkdownOptions { inline?: boolean; } export function renderMarkdown( input: string, opts: MarkdownOptions = {}, ): { headings: MarkdownHeading[]; html: string } { const renderer = new DefaultRenderer(); const markedOpts: Marked.MarkedOptions = { gfm: true, renderer, }; const html = opts.inline ? Marked.parseInline(input, markedOpts) as string : Marked.parse(input, markedOpts) as string; return { headings: renderer.headings, html }; }