@shikijs/vitepress-twoslash
Version:
Enable Twoslash support in VitePress
236 lines (231 loc) • 6.83 kB
JavaScript
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",
classCode = "vp-code",
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, classCode].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) {
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, "{");
}
};
}
export { rendererFloatingVue, transformerTwoslash };