diff --git a/lib/rehype-plugins.js b/lib/rehype-plugins.js index 27894bb9..c42619ef 100644 --- a/lib/rehype-plugins.js +++ b/lib/rehype-plugins.js @@ -1,7 +1,8 @@ const memoize = require('micro-memoize'); -const { rehypeVscode } = require('unified-vscode'); +// const { rehypeVscode } = require('unified-vscode'); +const { rehypeShikiTwoslash } = require('./rehype-shiki-twoslash'); const rehypeImgs = require('./rehype-image-size'); -const rehypePlugins = [memoize(rehypeVscode), rehypeImgs]; +const rehypePlugins = [rehypeShikiTwoslash, rehypeImgs]; module.exports = { rehypePlugins }; diff --git a/lib/rehype-shiki-twoslash.js b/lib/rehype-shiki-twoslash.js new file mode 100644 index 00000000..cb496d47 --- /dev/null +++ b/lib/rehype-shiki-twoslash.js @@ -0,0 +1,146 @@ +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; diff --git a/package.json b/package.json index 497b1683..37936485 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "remark-squeeze-paragraphs": "^4.0.0", "remark-unwrap-images": "2.0.0", "remark-vscode": "^1.0.0-beta.2", + "shiki-twoslash": "^1.4.1", "strip-markdown": "^4.0.0", "stylis": "^4.0.6", "swr": "^0.3.11", diff --git a/src/pages/understand-stacks/local-development.md b/src/pages/understand-stacks/local-development.md index b70e388e..54f29e3a 100644 --- a/src/pages/understand-stacks/local-development.md +++ b/src/pages/understand-stacks/local-development.md @@ -31,7 +31,7 @@ This guide helps you understand how to set up and run a mocknet for local develo ```bash cp sample.env .env ``` - + 3. Start the Mocknet: ```bash diff --git a/yarn.lock b/yarn.lock index ea5a02f4..49d8ff73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2176,6 +2176,22 @@ "@typescript-eslint/types" "4.9.0" eslint-visitor-keys "^2.0.0" +"@typescript/twoslash@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@typescript/twoslash/-/twoslash-1.1.7.tgz#9fc5709f37940f2deda396b74e503c6c33c54d6e" + integrity sha512-+oASPajHbUpmwsZgf0/ioBn9vjIodAO4c0na2nMLWxBKDMGAo16m8uEFtxjIrEYp6l+h3rBvHchSFucv1/qcNQ== + dependencies: + "@typescript/vfs" "1.3.4" + debug "^4.1.1" + lz-string "^1.4.4" + +"@typescript/vfs@1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@typescript/vfs/-/vfs-1.3.4.tgz#07f89d2114f6e29255d589395ed7f3b4af8a00c2" + integrity sha512-RbyJiaAGQPIcAGWFa3jAXSuAexU4BFiDRF1g3hy7LmRqfNpYlTQWGXjcrOaVZjJ8YkkpuwG0FcsYvtWQpd9igQ== + dependencies: + debug "^4.1.1" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -6103,6 +6119,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + make-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -6911,7 +6932,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onigasm@^2.2.1: +onigasm@^2.2.1, onigasm@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== @@ -8378,6 +8399,24 @@ shell-quote@1.7.2: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shiki-twoslash@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/shiki-twoslash/-/shiki-twoslash-1.4.1.tgz#cab11b13214c51f88bed733512feb2654da0716e" + integrity sha512-PB55xKMbiDwiOc9OoJAsxsdKURsqqmHrc8hL0gsRuiJrcN+iz4NupKYwxzp+VzhBkKAZMG8YXwt//XithLpGJQ== + dependencies: + "@typescript/twoslash" "1.1.7" + "@typescript/vfs" "1.3.4" + shiki "^0.9.3" + typescript ">3" + +shiki@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.3.tgz#7bf7bcf3ed50ca525ec89cc09254abce4264d5ca" + integrity sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA== + dependencies: + onigasm "^2.2.5" + vscode-textmate "^5.2.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -9205,6 +9244,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@>3: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== + typescript@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" @@ -9613,6 +9657,11 @@ vm-browserify@1.1.2, vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-textmate@^5.2.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== + "vscode-textmate@https://github.com/octref/vscode-textmate": version "4.0.1" resolved "https://github.com/octref/vscode-textmate#e65aabe2227febda7beaad31dd0fca1228c5ddf3"