svem
Version:
Svelte in Markdown preprocessor
183 lines (182 loc) • 5.67 kB
JavaScript
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
};