fumadocs-twoslash
Version:
Typescript Twoslash Integration for Fumadocs
121 lines (120 loc) • 3.8 kB
JavaScript
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 };