@sveltek/unplugins
Version:
Sveltek's Unified plugins for Markdown preprocessor.
103 lines (96 loc) • 3.31 kB
JavaScript
import { visit } from 'unist-util-visit';
const remarkToc = (options = {}) => {
const { depth = 3, links = true } = options;
return (tree, vfile) => {
const frontmatter = vfile.data.frontmatter;
const toc = [];
let i = 0;
visit(tree, "heading", (node) => {
const [child] = node.children;
let value = "";
let id = "";
if (child.type === "text") {
value = child.value;
id = value.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
}
if (links) {
node.children = [];
node.children.push({
type: "link",
url: `#${id}`,
children: [{ type: "text", value }]
});
}
const data = node.data || (node.data = {});
const props = data.hProperties || (data.hProperties = {});
if (node.depth > 1 && node.depth <= depth) {
if (toc.some((h) => h.id === id)) id = `${id}-${i + 1}`;
if (!props.id) props.id = id;
toc.push({ id, depth: node.depth, value });
}
});
frontmatter.toc = toc;
};
};
const countWords = (text) => (text.match(/\S+/g) || []).length;
const readingStats = (text, { wordsPerMinute = 200 } = {}) => {
const words = countWords(text);
const min = words / wordsPerMinute;
const minutes = min < 1 ? 1 : Math.round(min * 10) / 10;
return { minutes, words, text: `${minutes} min read` };
};
const remarkReadingStats = (options = {}) => {
const { wordsPerMinute } = options;
return (tree, vfile) => {
const frontmatter = vfile.data.frontmatter;
let text = "";
visit(tree, "text", (node) => {
text += node.value;
});
frontmatter.readingStats = readingStats(text, { wordsPerMinute });
};
};
const isFunction = (v) => v instanceof Function;
const rehypeShiki = function(options = {}) {
const {
theme,
themes,
langs,
highlighter,
codeToHtml: codeHtmlOptions,
root = (node) => {
node.tagName = "div";
}
} = options;
const defaultTheme = theme || "github-dark-default";
const defaultLangs = langs || ["javascript", "typescript", "svelte"];
return async (tree, file) => {
const { rehypeHighlight } = await import('@sveltek/markdown');
const { getSingletonHighlighter } = await import('shiki');
const shikiHighlighter = getSingletonHighlighter({
...highlighter,
themes: highlighter?.themes || themes && Object.values(themes) || [defaultTheme],
langs: highlighter?.langs || defaultLangs
});
const { codeToHtml } = await shikiHighlighter;
const highlightOptions = {
highlighter: async ({ code, lang, meta }) => {
const parsedMeta = options.parseMeta?.(meta);
if (parsedMeta) meta = parsedMeta;
const codeHtml = isFunction(codeHtmlOptions) ? codeHtmlOptions({ code, lang, meta }) : codeHtmlOptions;
if (code) {
return codeToHtml(code, {
...codeHtml,
lang: lang || "text",
...themes ? { themes: codeHtml?.themes || themes } : { theme: codeHtml?.theme || defaultTheme }
});
}
},
root
};
const transformer = rehypeHighlight.call(this, highlightOptions);
return transformer?.(tree, file, () => {
});
};
};
export { rehypeShiki, remarkReadingStats, remarkToc };