import { defaultHoverInfoProcessor, rendererRich } from '@shikijs/twoslash' import type { RendererRichOptions, TwoslashRenderer } from '@shikijs/twoslash' import type { Element, ElementContent, Text } from 'hast' import type { ShikiTransformerContextCommon } from 'shiki' import { gfmFromMarkdown } from 'mdast-util-gfm' import { fromMarkdown } from 'mdast-util-from-markdown' import { defaultHandlers, toHast } from 'mdast-util-to-hast' export { defaultHoverInfoProcessor } export interface TwoslashFloatingVueOptions { classCopyIgnore?: string classFloatingPanel?: string classCode?: string classMarkdown?: string floatingVueTheme?: string floatingVueThemeQuery?: string floatingVueThemeCompletion?: string } export interface TwoslashFloatingVueRendererOptions extends RendererRichOptions { /** * Class and themes for floating-vue specific nodes */ floatingVue?: TwoslashFloatingVueOptions } export function rendererFloatingVue(options: TwoslashFloatingVueRendererOptions = {}): TwoslashRenderer { const { classCopyIgnore = 'vp-copy-ignore', classFloatingPanel = 'twoslash-floating', classCode = 'vp-code', classMarkdown = 'plume-content', 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' ? undefined : { 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 compose(parts: { token: Element | Text, popup: Element }): Element[] { 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 vPre(el: T): T { if (el.type === 'element') { el.properties = el.properties || {} el.properties['v-pre'] = '' } return el } function renderMarkdown(this: ShikiTransformerContextCommon, md: string): ElementContent[] { 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 this.codeToHast( node.value, { ...this.options, transformers: [], lang, }, ).children[0] as Element } return defaultHandlers.code(state, node) }, }, }, ) as Element).children } function renderMarkdownInline(this: ShikiTransformerContextCommon, md: string, context?: string): ElementContent[] { 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 }