You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

146 lines
3.8 KiB

const createShikiHighlighter = require('shiki-twoslash');
const memoize = require('micro-memoize');
const visit = require('unist-util-visit');
const pAll = require('p-all');
const hastToString = require('hast-util-to-string');
const DEFAULT_THEME = 'dark-plus';
function getTokenClassNames(token) {
let classNames;
let classNameString = '';
token.explanation.forEach(expl => {
const thing = expl.scopes.find(scope => scope.themeMatches.length > 0);
const themeMatches = thing && thing.themeMatches && thing.themeMatches[0];
const name = themeMatches && themeMatches.name;
const formatted =
name &&
name
.toString()
.split(', ')
.map(entry => entry.toLowerCase());
if (formatted) {
classNames = formatted;
}
});
classNames &&
classNames.length &&
classNames.forEach((className, index) => {
classNameString += `${slugify(className)}${index !== classNames.length - 1 ? ' ' : ''}`;
});
return classNameString === '' ? 'plain' : classNameString;
}
function removeEmptyLine(index, tree) {
const [item] = tree.slice(index * -1);
if (item && item.properties && item.properties.className.includes('empty')) {
tree.pop();
}
}
function codeLanguage(node) {
const className = node.properties.className || [];
let value;
for (const element of className) {
value = element;
if (value.slice(0, 9) === 'language-') {
return value.slice(9);
}
}
return 'bash';
}
const rehypeShikiTwoslash = options => {
let theme = options.theme || DEFAULT_THEME;
const generateHighlighter = memoize(async () => createShikiHighlighter({ theme, cache }), {
isPromise: true,
});
const highlight = async (node, lang) => {
const hash = generateHash(node);
return cache.wrap(hash, async () => {
const highlighter = await generateHighlighter();
return highlighter.codeToThemedTokens(node, lang);
});
};
function tokensToHast(lines) {
const tokenLineClassName = (options && options.tokenLineClassName) || 'token-line';
const tokenLineElement = (options && options.tokenLineElement) || 'span';
const tree = [];
for (const line of lines) {
if (line.length === 0) {
tree.push(
u('element', {
tagName: tokenLineElement,
properties: { className: `${tokenLineClassName} ${tokenLineClassName}__empty` },
})
);
} else {
const lineChildren = [];
for (const token of line) {
const className = getTokenClassNames(token);
lineChildren.push(
u(
'element',
{
tagName: 'span',
properties: { style: `color: ${token.color}`, className: `token ${className}` },
},
[u('text', token.content)]
)
);
}
tree.push(
u(
'element',
{ tagName: tokenLineElement, properties: { className: tokenLineClassName } },
lineChildren
)
);
}
}
removeEmptyLine(2, tree);
removeEmptyLine(1, tree);
return tree;
}
async function transformer(tree) {
const nodes = [];
visit(tree, 'element', (node, index, parent) => {
if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
return;
} else {
nodes.push(node);
}
});
await pAll(
nodes.map(node => () => visitor(node)),
{ concurrency: 25 }
);
return tree;
}
async function visitor(node) {
const lang = codeLanguage(node);
const tokens = await highlight(hastToString(node), lang);
const tree = tokensToHast(tokens);
node.properties.lines = tokens.length - 1;
node.properties.lang = lang;
node.children = tree;
}
return transformer;
};
module.exports = rehypeShikiTwoslash;