UNPKG

svem

Version:

Svelte in Markdown preprocessor

183 lines (182 loc) 5.67 kB
import { transformerNotationDiff, transformerRenderWhitespace } from "@shikijs/transformers"; import { visit } from "unist-util-visit"; import { unified } from "unified"; import { getSingletonHighlighter } from "shiki"; import { escapeHtml } from "./html.js"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; import ClipboardIcon from "../icons/clipboard.js"; import rehypeSanitize from "rehype-sanitize"; import rehypePresetMinify from "rehype-preset-minify"; const inlineLang = /\{:[a-zA-Z0-9_]+}$/; const remarkCodeHighlight = (options) => { const { themes, langs, allowCopy = true } = options ?? {}; return async (tree) => { const hg = await getSingletonHighlighter({ langs, themes: Object.entries(themes).map(([, name]) => name) }); visit(tree, "code", (node) => { if (typeof node.attributes?.lang === "string") { node.lang = node.attributes.lang; delete node.attributes.lang; } if (!node.lang) { node.lang = "text"; } if (options?.transform) { node.value = options.transform(node.value); } const output = highlight(hg, node.value, { lang: node.lang, themes }); const block = createWrapper(output, node.lang, node.attributes, allowCopy ? node.value : void 0); Object.assign(node, block); }); visit(tree, "inlineCode", (node) => { if (!node.value) return; node.type = "html-code"; if (!inlineLang.test(node.value)) { node.value = `<code class="inline-code">${escapeHtml(node.value)}</code>`; return; } const pattern = node.value.match(inlineLang)?.[0] ?? ""; if (pattern) { const lang = pattern.slice(2, -1); node.value = highlight(hg, node.value.replace(pattern, "").trim(), { lang, themes }); node.value = node.value.replace(/<pre class="shiki/, '<pre class="shiki shiki-inline'); } }); }; }; const createWrapper = (code, lang, attributes, rawCode) => { const { title, ...props } = attributes; const rawCodeId = `${(/* @__PURE__ */ new Date()).getTime()}-${Math.random().toString(36).slice(2)}`; return { type: "html-node", tagName: "div", attributes: { ...props, class: "code-block-outer" }, children: [ { type: "html-node", tagName: "div", attributes: { class: "code-block-inner" }, children: [ { type: "html-node", tagName: "div", attributes: { class: "code-block", standalone: !title }, children: [ { type: "html-node", tagName: "div", attributes: { class: "code-block-header", standalone: !title }, children: [ { type: "html-node", tagName: "span", value: title ?? "", attributes: { class: "code-block-title" } }, { type: "html-node", tagName: "div", attributes: { class: "code-block-actions" }, children: [ { type: "html-node", tagName: "span", value: lang ?? "text", attributes: { class: "code-block-lang" } }, { type: "html-node", tagName: "button", value: ClipboardIcon, attributes: { class: "code-block-copy", hidden: !rawCode, title: "Copy code", "aria-label": "Copy code", "data-raw-code-copy": rawCodeId } } ] } ] }, { type: "html-code", value: code, attributes: {} }, { type: "html-node", tagName: "div", value: rawCode, escape: true, attributes: { "data-raw-code": rawCodeId, hidden: true } } ] } ] } ] }; }; function highlight(shiki, code, options) { const { lang, themes } = options ?? {}; const transformers = [transformerRenderWhitespace()]; if (options.diffOptions !== false) { transformers.push(transformerNotationDiff(options.diffOptions)); } const output = shiki.codeToHtml(code, { lang, themes, transformers }); const hast = unified().use(rehypeParse).use(rehypeSanitize).parse(output); escapeNodes(hast.children ?? []); return unified().use(rehypePresetMinify).use(rehypeStringify, { allowDangerousHtml: true }).stringify(hast); } function escapeNodes(nodes) { for (const node of nodes) { if (node.type === "element" && Array.isArray(node.children)) { escapeNodes(node.children); } else if (node.type === "text") { node.value = escapeHtml(node.value ?? ""); } } } export { createWrapper, highlight, remarkCodeHighlight };