twoslash-python
Version:
Twoslash generator for Python - Enhance your Python documentation with TypeScript-like type information
490 lines • 19.3 kB
JavaScript
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