UNPKG

twoslash-python

Version:

Twoslash generator for Python - Enhance your Python documentation with TypeScript-like type information

490 lines 19.3 kB
import { ShikiTwoslashError } from '@shikijs/twoslash/core'; import { defaultCompletionIcons, defaultCustomTagIcons } from '@shikijs/twoslash/core'; import { gfmFromMarkdown } from 'mdast-util-gfm'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { defaultHandlers, toHast } from 'mdast-util-to-hast'; function renderMarkdown(md) { const mdast = fromMarkdown(md.replace(/{@link ([^}]*)}/g, '$1'), // replace jsdoc links { mdastExtensions: [gfmFromMarkdown()], }); const hast = toHast(mdast, { handlers: { code: ((state, node) => { const lang = node.lang || ''; if (lang) { const result = this.codeToHast(node.value, { ...this.options, transformers: [], lang, }); return result.children?.[0] || defaultHandlers.code(state, node); } return defaultHandlers.code(state, node); }), }, }); if (!hast || !('children' in hast)) { return []; } return hast.children; } function renderMarkdownInline(md, context) { if (context === 'tag:param') md = md.replace(/^([\w$-]+)/, '`$1` '); const children = renderMarkdown.call(this, md); if (children && children.length === 1 && children[0] && children[0].type === 'element' && children[0].tagName === 'p') return children[0].children; return children; } function extend(extension, node) { if (!extension) return node; return { ...node, tagName: extension.tagName ?? node.tagName, properties: { ...node.properties, class: extension.class || node.properties?.class, ...extension.properties, }, children: extension.children?.(node.children) ?? node.children, }; } function renderMarkdownPassThrough(markdown) { return [ { type: 'text', value: markdown, }, ]; } /** * An alternative renderer that providers better prefixed class names, * with syntax highlight for the info text. */ export function rendererRichPython(options = {}) { const { completionIcons = defaultCompletionIcons, customTagIcons = defaultCustomTagIcons, processHoverInfo = defaultHoverInfoProcessor, processHoverDocs = (docs) => docs, classExtra = '', errorRendering = 'line', queryRendering = 'popup', renderMarkdown = renderMarkdownPassThrough, renderMarkdownInline = renderMarkdownPassThrough, hast, } = options; function highlightPopupContent(info) { if (!info.text) return []; const content = processHoverInfo(info.text); if (!content || content === 'any') return []; const popupContents = []; const typeCode = { type: 'element', tagName: 'code', properties: {}, children: this.codeToHast(content, { ...this.options, meta: {}, transformers: [], lang: 'python', structure: content.trim().includes('\n') ? 'classic' : 'inline', }).children, }; typeCode.properties.class = 'twoslash-popup-code'; popupContents.push(extend(hast?.popupTypes, typeCode)); if (info.docs) { const docs = processHoverDocs(info.docs) ?? info.docs; if (docs) { console.log('docs', docs); const children = renderMarkdown.call(this, docs); popupContents.push(extend(hast?.popupDocs, { type: 'element', tagName: 'div', properties: { class: 'twoslash-popup-docs' }, children, })); } } if (info.tags?.length) { popupContents.push(extend(hast?.popupDocsTags, { type: 'element', tagName: 'div', properties: { class: 'twoslash-popup-docs twoslash-popup-docs-tags', }, children: info.tags.map((tag) => ({ type: 'element', tagName: 'span', properties: { class: `twoslash-popup-docs-tag`, }, children: [ { type: 'element', tagName: 'span', properties: { class: 'twoslash-popup-docs-tag-name', }, children: [ { type: 'text', value: `@${tag[0]}`, }, ], }, ...(tag[1] ? [ { type: 'element', tagName: 'span', properties: { class: 'twoslash-popup-docs-tag-value', }, children: renderMarkdownInline.call(this, tag[1], `tag:${tag[0]}`), }, ] : []), ], })), })); } return popupContents; } return { nodeStaticInfo(info, node) { const themedContent = highlightPopupContent.call(this, info); if (!themedContent.length) return node; const popup = extend(hast?.hoverPopup, { type: 'element', tagName: 'span', properties: { class: ['twoslash-popup-container', classExtra].filter(Boolean).join(' '), }, children: themedContent, }); return extend(hast?.hoverToken, { type: 'element', tagName: 'span', properties: { class: 'twoslash-hover', }, children: hast?.hoverCompose ? hast?.hoverCompose({ popup, token: node }) : [popup, node], }); }, nodeQuery(query, node) { if (!query.text) return {}; const themedContent = highlightPopupContent.call(this, query); if (queryRendering !== 'popup') { return extend(hast?.queryToken, { type: 'element', tagName: 'span', properties: { class: 'twoslash-hover', }, children: [node], }); } const popup = extend(hast?.queryPopup, { type: 'element', tagName: 'span', properties: { class: ['twoslash-popup-container', classExtra].filter(Boolean).join(' '), }, children: [ { type: 'element', tagName: 'div', properties: { class: 'twoslash-popup-arrow' }, children: [], }, ...themedContent, ], }); return extend(hast?.queryToken, { type: 'element', tagName: 'span', properties: { class: 'twoslash-hover twoslash-query-presisted', }, children: hast?.queryCompose ? hast?.queryCompose({ popup, token: node }) : [popup, node], }); }, nodeCompletion(query, node) { if (node.type !== 'text') throw new ShikiTwoslashError(`Renderer hook nodeCompletion only works on text nodes, got ${node.type}`); const items = query.completions.map((i) => { const kind = i.kind || 'default'; const isDeprecated = 'kindModifiers' in i && typeof i.kindModifiers === 'string' && i.kindModifiers?.split(',').includes('deprecated'); return { type: 'element', tagName: 'li', properties: {}, children: [ ...(completionIcons ? [ { type: 'element', tagName: 'span', properties: { class: `twoslash-completions-icon completions-${kind.replace(/\s/g, '-')}`, }, children: [completionIcons[kind] || completionIcons.property].filter(Boolean), }, ] : []), { type: 'element', tagName: 'span', properties: { class: isDeprecated ? 'deprecated' : undefined, }, children: [ { type: 'element', tagName: 'span', properties: { class: 'twoslash-completions-matched' }, children: [ { type: 'text', value: i.name.startsWith(query.completionsPrefix) ? query.completionsPrefix : '', }, ], }, { type: 'element', tagName: 'span', properties: { class: 'twoslash-completions-unmatched' }, children: [ { type: 'text', value: i.name.startsWith(query.completionsPrefix) ? i.name.slice(query.completionsPrefix.length || 0) : i.name, }, ], }, ], }, ], }; }); const cursor = extend(hast?.completionCursor, { type: 'element', tagName: 'span', properties: { class: ['twoslash-completion-cursor', classExtra].filter(Boolean).join(' '), }, children: [], }); const popup = extend(hast?.completionPopup, { type: 'element', tagName: 'ul', properties: { class: ['twoslash-completion-list', classExtra].filter(Boolean).join(' '), }, children: items, }); const children = []; if (node.value) children.push({ type: 'text', value: node.value }); if (hast?.completionCompose) { children.push(...hast.completionCompose({ popup, cursor })); } else { children.push({ ...cursor, children: [popup], }); } return extend(hast?.completionToken, { type: 'element', tagName: 'span', properties: {}, children, }); }, nodesError(error, children) { if (errorRendering !== 'hover') { return [ extend(hast?.errorToken, { type: 'element', tagName: 'span', properties: { class: [`twoslash-error`, getErrorLevelClass(error)].filter(Boolean).join(' '), }, children, }), ]; } const popup = extend(hast?.errorPopup, { type: 'element', tagName: 'span', properties: { class: ['twoslash-popup-container', classExtra].filter(Boolean).join(' '), }, children: [ extend(hast?.popupError, { type: 'element', tagName: 'div', properties: { class: 'twoslash-popup-error', }, children: renderMarkdown.call(this, error.text), }), ], }); const token = { type: 'element', tagName: 'span', children, properties: {}, }; return [ extend(hast?.errorToken, { type: 'element', tagName: 'span', properties: { class: `twoslash-error twoslash-error-hover ${getErrorLevelClass(error)}`, }, children: hast?.errorCompose ? hast?.errorCompose({ popup, token }) : [popup, token], }), ]; }, lineQuery(query, node) { if (queryRendering !== 'line') return []; const themedContent = highlightPopupContent.call(this, query); const targetNode = node?.type === 'element' ? node.children[0] : undefined; const targetText = targetNode?.type === 'text' ? targetNode.value : ''; const offset = Math.max(0, (query.character || 0) + Math.floor(targetText.length / 2) - 2); return [ { type: 'element', tagName: 'div', properties: { class: ['twoslash-meta-line twoslash-query-line', classExtra].filter(Boolean).join(' '), }, children: [ { type: 'text', value: ' '.repeat(offset) }, { type: 'element', tagName: 'span', properties: { class: ['twoslash-popup-container', classExtra].filter(Boolean).join(' '), }, children: [ { type: 'element', tagName: 'div', properties: { class: 'twoslash-popup-arrow' }, children: [], }, ...themedContent, ], }, ], }, ]; }, lineError(error) { if (errorRendering !== 'line') return []; return [ { type: 'element', tagName: 'div', properties: { class: ['twoslash-meta-line twoslash-error-line', getErrorLevelClass(error), classExtra] .filter(Boolean) .join(' '), }, children: [ { type: 'text', value: error.text, }, ], }, ]; }, lineCustomTag(tag) { return [ { type: 'element', tagName: 'div', properties: { class: [`twoslash-tag-line twoslash-tag-${tag.name}-line`, classExtra] .filter(Boolean) .join(' '), }, children: [ ...(customTagIcons ? [ { type: 'element', tagName: 'span', properties: { class: `twoslash-tag-icon tag-${tag.name}-icon` }, children: [customTagIcons[tag.name]].filter(Boolean), }, ] : []), { type: 'text', value: tag.text || '', }, ], }, ]; }, nodesHighlight(highlight, nodes) { return [ extend(hast?.nodesHighlight, { type: 'element', tagName: 'span', properties: { class: 'twoslash-highlighted', }, children: nodes, }), ]; }, }; } const regexType = /^[A-Z]\w*(<[^>]*>)?:/; const regexFunction = /^\w*\(/; /** * The default hover info processor, which will do some basic cleanup */ export function defaultHoverInfoProcessor(type) { let content = type // remove leading `(property)` or `(method)` on each line .replace(/^\(([\w-]+)\)\s+/gm, '') // remove import statement .replace(/\nimport .*$/, '') // remove interface or namespace lines with only the name .replace(/^(interface|namespace) \w+$/gm, '') .trim(); // Add `type` or `function` keyword if needed if (content.match(regexType)) content = `type ${content}`; else if (content.match(regexFunction)) content = `function ${content}`; return content; } function getErrorLevelClass(error) { switch (error.level) { case 'warning': return 'twoslash-error-level-warning'; case 'suggestion': return 'twoslash-error-level-suggestion'; case 'message': return 'twoslash-error-level-message'; default: return ''; } } //# sourceMappingURL=renderer.js.map