UNPKG

@sveltek/unplugins

Version:

Sveltek's Unified plugins for Markdown preprocessor.

205 lines (198 loc) 4.77 kB
import { visit } from "unist-util-visit"; export * from "unist-util-visit" //#region src/remark/toc/index.ts /** * A custom `Remark` plugin that creates `Table of Content` (Toc). * * Automatically adds a link with the appropriate attributes to the headings. * * It also stores Toc items to `frontmatter` for easy access. * * @example * * ```ts * import { svelteMarkdown } from '@sveltek/markdown' * import { remarkToc } from '@sveltek/unplugins' * * svelteMarkdown({ * plugins: { * remark: [remarkToc] * } * }) * ``` * * Or with options: * * ```js * svelteMarkdown({ * plugins: { * remark: [[remarkToc, { depth: 3 }]] * } * }) * ``` */ 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; }; }; //#endregion //#region src/remark/reading-stats/stats.ts 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` }; }; //#endregion //#region src/remark/reading-stats/index.ts /** * A custom `Remark` plugin that creates `Reading Stats`. * * Stores reading details to `frontmatter` for easy access. * * @example * * ```ts * import { svelteMarkdown } from '@sveltek/markdown' * import { remarkReadingStats } from '@sveltek/unplugins' * * svelteMarkdown({ * plugins: { * remark: [remarkReadingStats] * } * }) * ``` * * Or with options: * * ```js * svelteMarkdown({ * plugins: { * remark: [[remarkReadingStats, { wordsPerMinute: 300 }]] * } * }) * ``` */ 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 }); }; }; //#endregion //#region src/rehype/shiki/utils.ts const isFunction = (v) => v instanceof Function; //#endregion //#region src/rehype/shiki/index.ts /** * A custom `Rehype` plugin for `Shiki`. * * @example * * ```js * import { svelteMarkdown } from '@sveltek/markdown' * import { rehypeShiki } from '@sveltek/unplugins' * * svelteMarkdown({ * plugins: { * rehype: [rehypeShiki] * } * }) * ``` * * Or with options: * * ```js * svelteMarkdown({ * plugins: { * rehype: [[rehypeShiki, { theme: 'github-light-default' }]] * } * }) * ``` */ 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 { codeToHtml } = await getSingletonHighlighter({ ...highlighter, themes: highlighter?.themes || themes && Object.values(themes) || [defaultTheme], langs: highlighter?.langs || defaultLangs }); 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 }; return rehypeHighlight.call(this, highlightOptions)?.(tree, file, () => {}); }; }; //#endregion export { rehypeShiki, remarkReadingStats, remarkToc };