UNPKG

fumadocs-twoslash

Version:

Typescript Twoslash Integration for Fumadocs

121 lines (120 loc) 3.8 kB
import { fromMarkdown } from "mdast-util-from-markdown"; import { gfmFromMarkdown } from "mdast-util-gfm"; import { defaultHandlers, toHast } from "mdast-util-to-hast"; import { ShikiError } from "shiki/core"; import { createTransformerFactory, rendererRich } from "@shikijs/twoslash"; import { createTwoslasher } from "twoslash"; //#region src/index.ts let cachedInstance; /** * This transformer **must** be used with the `rehype-code` plugin of Fumadocs. */ function transformerTwoslash(_options = {}) { const ignoreClass = "nd-copy-ignore"; const { twoslashOptions = {}, rendererRich: rendererOptions, ...rest } = _options; function lazyInstance() { function get() { return cachedInstance ??= createTwoslasher({ ...twoslashOptions, compilerOptions: { moduleResolution: 100, baseUrl: void 0, ...twoslashOptions.compilerOptions } }); } const wrapper = (...args) => get()(...args); wrapper.getCacheMap = () => get().getCacheMap(); return wrapper; } const renderer = rendererRich({ classExtra: ignoreClass, queryRendering: "line", renderMarkdown, renderMarkdownInline, ...rendererOptions, hast: { hoverToken: { tagName: "Popup" }, hoverPopup: { tagName: "PopupContent", properties: { class: ignoreClass } }, hoverCompose: ({ popup, token }) => [popup, { type: "element", tagName: "PopupTrigger", properties: {}, children: [token] }], popupDocs: { class: "prose twoslash-popup-docs" }, popupTypes: { tagName: "div", class: "twoslash shiki fd-codeblock prose-no-margin", children: (v) => { if (v.length === 1 && v[0].type === "element" && v[0].tagName === "pre") return v; return [{ type: "element", tagName: "code", properties: { class: "twoslash-popup-code" }, children: v }]; } }, popupDocsTags: { class: "prose twoslash-popup-docs twoslash-popup-docs-tags" }, nodesHighlight: { class: "highlighted-word twoslash-highlighted" }, ...rendererOptions?.hast } }); const fn = renderer.lineQuery; renderer.lineQuery = function(...args) { const result = fn.call(this, ...args); const child = result[0].children[0]; result[0].children[0] = { type: "element", tagName: "span", children: [child] }; return result; }; return createTransformerFactory(lazyInstance(), renderer)({ explicitTrigger: true, ...rest }); } function renderMarkdown(md) { const mdast = fromMarkdown(md.replace(/{@link (?<link>[^}]*)}/g, "$1"), { mdastExtensions: [gfmFromMarkdown()] }); const onCode = (lang, node) => { return this.codeToHast(node.value, { ...this.options, transformers: [], meta: node.meta ? { __raw: node.meta } : {}, lang }).children[0]; }; return toHast(mdast, { handlers: { code: (state, node) => { const lang = node.lang; if (!lang) return defaultHandlers.code(state, node); try { return onCode(lang, node); } catch (e) { const def = defaultHandlers.code(state, node); if (e instanceof ShikiError) { this.meta._fd_postprocess ??= []; this.meta._fd_postprocess.push(async ({ highlighter }) => { await highlighter.loadLanguage(lang); Object.assign(def, onCode(lang, node)); }); return def; } if (e instanceof Error) console.error(`[fumadocs-twoslash] encountered an error when highlighting codeblock in a Twoslash popup: ${e.message}`); return def; } } } }).children; } function renderMarkdownInline(md, context) { const text = context === "tag:param" ? md.replace(/^(?<link>[\w$-]+)/, "`$1` ") : md; const children = renderMarkdown.call(this, text); if (children.length === 1 && children[0].type === "element" && children[0].tagName === "p") return children[0].children; return children; } //#endregion export { transformerTwoslash };