UNPKG

@shikijs/vitepress-twoslash

Version:

Enable Twoslash support in VitePress

239 lines (234 loc) 7 kB
import { createTransformerFactory } from '@shikijs/twoslash/core'; import { removeTwoslashNotations } from 'twoslash'; import { createTwoslasher } from 'twoslash-vue'; import { rendererRich } from '@shikijs/twoslash'; export { defaultHoverInfoProcessor } from '@shikijs/twoslash'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { gfmFromMarkdown } from 'mdast-util-gfm'; import { toHast, defaultHandlers } from 'mdast-util-to-hast'; function rendererFloatingVue(options = {}) { const { classCopyIgnore = "vp-copy-ignore", classFloatingPanel = "twoslash-floating", classMarkdown = "vp-doc", floatingVueTheme = "twoslash", floatingVueThemeQuery = "twoslash-query", floatingVueThemeCompletion = "twoslash-completion" } = options.floatingVue || {}; const { errorRendering = "line" } = options; const hoverBasicProps = { "class": "twoslash-hover", "popper-class": ["shiki", classFloatingPanel, classCopyIgnore].join(" "), "theme": floatingVueTheme }; const rich = rendererRich({ classExtra: classCopyIgnore, ...options, renderMarkdown, renderMarkdownInline, hast: { hoverToken: { tagName: "v-menu", properties: hoverBasicProps }, hoverCompose: compose, queryToken: { tagName: "v-menu", properties: { ...hoverBasicProps, ":shown": "true", "theme": floatingVueThemeQuery } }, queryCompose: compose, popupDocs: { class: `twoslash-popup-docs ${classMarkdown}` }, popupDocsTags: { class: `twoslash-popup-docs twoslash-popup-docs-tags ${classMarkdown}` }, popupError: { class: `twoslash-popup-error ${classMarkdown}` }, errorToken: errorRendering === "line" ? void 0 : { tagName: "v-menu", properties: { ...hoverBasicProps, class: "twoslash-error twoslash-error-hover" } }, errorCompose: compose, completionCompose({ popup, cursor }) { return [ { type: "element", tagName: "v-menu", properties: { "popper-class": ["shiki twoslash-completion", classCopyIgnore, classFloatingPanel], "theme": floatingVueThemeCompletion, ":shown": "true" }, children: [ cursor, { type: "element", tagName: "template", properties: { "v-slot:popper": "{}" }, content: { type: "root", children: [vPre(popup)] } } ] } ]; } } }); return rich; } function vPre(el) { if (el.type === "element") { el.properties = el.properties || {}; el.properties["v-pre"] = ""; } return el; } function renderMarkdown(md) { const mdast = fromMarkdown( md.replace(/\{@link ([^}]*)\}/g, "$1"), // replace jsdoc links { mdastExtensions: [gfmFromMarkdown()] } ); return toHast( mdast, { handlers: { code: (state, node) => { const lang = node.lang || ""; if (lang) { return { type: "element", tagName: "code", properties: {}, children: this.codeToHast( node.value, { ...this.options, transformers: [], lang, structure: node.value.trim().includes("\n") ? "classic" : "inline" } ).children }; } return defaultHandlers.code(state, node); } } } ).children; } function renderMarkdownInline(md, context) { if (context === "tag:param") md = md.replace(/^([\w$-]+)/, "`$1` "); const children = renderMarkdown.call(this, md); if (children.length === 1 && children[0].type === "element" && children[0].tagName === "p") return children[0].children; return children; } function compose(parts) { if (parts.token.type === "element" && parts.token.children.length < 1) { const classes = parts.token.properties.class || ""; parts.token.properties.class = `${classes} twoslash-error-empty`; } return [ { type: "element", tagName: "span", properties: {}, children: [parts.token] }, { type: "element", tagName: "template", properties: { "v-slot:popper": "{}" }, content: { type: "root", children: [vPre(parts.popup)] }, children: [] } ]; } function transformerTwoslash(options = {}) { const { explicitTrigger = true, typesCache } = options; const onError = (error, code) => { const isCI = typeof process !== "undefined" && process?.env?.CI; const isDev = typeof process !== "undefined" && process?.env?.NODE_ENV === "development"; const shouldThrow = (options.throws || isCI || !isDev) && options.throws !== false; console.error(` -------- Twoslash error in code: -------- ${code.split(/\n/g).slice(0, 15).join("\n").trim()} -------- `); if (shouldThrow) throw error; else console.error(error); removeTwoslashNotations(code); }; const defaultTwoslasher = createTwoslasher(options.twoslashOptions); let twoslasher = defaultTwoslasher; if (typesCache) { twoslasher = (code, extension, options2) => { const cached = typesCache.read(code); if (cached) return cached; const twoslashResult = defaultTwoslasher(code, extension, options2); typesCache.write(code, twoslashResult); return twoslashResult; }; twoslasher.getCacheMap = defaultTwoslasher.getCacheMap; } const twoslash = createTransformerFactory(twoslasher)({ langs: ["ts", "tsx", "js", "jsx", "json", "vue"], renderer: rendererFloatingVue(options), onTwoslashError: onError, onShikiError: onError, ...options, explicitTrigger }); const trigger = explicitTrigger instanceof RegExp ? explicitTrigger : /\btwoslash\b/; typesCache?.init?.(); return { ...twoslash, name: "@shikijs/vitepress-twoslash", preprocess(code, options2) { const cleanup = options2.transformers?.find((i) => i.name === "vitepress:clean-up"); if (cleanup) options2.transformers?.splice(options2.transformers.indexOf(cleanup), 1); if (!explicitTrigger || options2.meta?.__raw?.match(trigger)) { const vPre = options2.transformers?.find((i) => i.name === "vitepress:v-pre"); if (vPre) options2.transformers?.splice(options2.transformers.indexOf(vPre), 1); } return twoslash.preprocess.call(this, code, options2); }, postprocess(html) { if (this.meta.twoslash) return html.replace(/\{/g, "&#123;"); } }; } export { rendererFloatingVue, transformerTwoslash };