Skip to content

Commit 86db365

Browse files
committed
feat(shiki): add twoslash support
1 parent fd6b7e6 commit 86db365

File tree

11 files changed

+135
-65
lines changed

11 files changed

+135
-65
lines changed

apps/site/next.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ const nextConfig = {
9696
'shiki',
9797
],
9898
},
99+
// TODO(@avivkeller): Why can't this be used without this config?
100+
serverExternalPackages: ['twoslash'],
99101
// If we're building for the Cloudflare deployment we want to set
100102
// an appropriate deploymentId (needed for skew protection)
101103
// TODO: The `OPEN_NEXT_CLOUDFLARE` environment variable is being

apps/site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"semver": "~7.7.2",
7474
"sval": "^0.6.3",
7575
"tailwindcss": "catalog:",
76+
"twoslash": "^0.3.4",
7677
"vfile": "~6.0.3",
7778
"vfile-matter": "~5.0.1"
7879
},

apps/site/pages/en/about/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ scalable network applications. In the following "hello world" example, many
1010
connections can be handled concurrently. Upon each connection, the callback is
1111
fired, but if there is no work to be done, Node.js will sleep.
1212

13-
```cjs
13+
```cjs twoslash
1414
const { createServer } = require('node:http');
1515

1616
const hostname = '127.0.0.1';

apps/site/pages/en/learn/typescript/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Take a look at this code snippet and then we can unpack it together:
2222
Maintainers note: this code is duplicated in the next article, please keep them in sync
2323
-->
2424

25-
```ts
25+
```ts twoslash
2626
type User = {
2727
name: string;
2828
age: number;

apps/site/styles/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
*/
88

99
@import '@node-core/ui-components/styles/index.css';
10+
@import '@node-core/rehype-shiki/styles.css';
1011
@import './locales.css';

packages/rehype-shiki/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{
22
"name": "@node-core/rehype-shiki",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"type": "module",
55
"exports": {
66
".": "./src/index.mjs",
7+
"./styles.css": "./src/styles.css",
78
"./*": "./src/*.mjs"
89
},
910
"repository": {
@@ -23,6 +24,7 @@
2324
"@shikijs/core": "^3.8.1",
2425
"@shikijs/engine-javascript": "^3.8.1",
2526
"@shikijs/engine-oniguruma": "^3.8.1",
27+
"@shikijs/twoslash": "^3.12.1",
2628
"classnames": "catalog:",
2729
"hast-util-to-string": "^3.0.1",
2830
"shiki": "~3.8.1",

packages/rehype-shiki/src/highlighter.mjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const DEFAULT_THEME = {
1313
* Creates a syntax highlighter with utility functions
1414
* @param {import('@shikijs/core').HighlighterCoreOptions} options - Configuration options for the highlighter
1515
*/
16-
export const createHighlighter = options => {
16+
export const createHighlighter = ({ transformers, ...options }) => {
1717
const shiki = createHighlighterCoreSync({
1818
themes: [DEFAULT_THEME],
1919
...options,
@@ -37,11 +37,12 @@ export const createHighlighter = options => {
3737
*
3838
* @param {string} code - The code to highlight
3939
* @param {string} lang - The programming language to use for highlighting
40+
* @param {Record<string, any>} meta - Metadata
4041
* @returns {string} The inner HTML of the highlighted code
4142
*/
42-
const highlightToHtml = (code, lang) =>
43+
const highlightToHtml = (code, lang, meta = {}) =>
4344
shiki
44-
.codeToHtml(code, { lang, theme })
45+
.codeToHtml(code, { lang, theme, meta, transformers })
4546
// Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag
4647
// since our own CodeBox component handles the <code> tag, we just want to extract
4748
// the inner highlighted code to the CodeBox
@@ -52,9 +53,10 @@ export const createHighlighter = options => {
5253
*
5354
* @param {string} code - The code to highlight
5455
* @param {string} lang - The programming language to use for highlighting
56+
* @param {Record<string, any>} meta - Metadata
5557
*/
56-
const highlightToHast = (code, lang) =>
57-
shiki.codeToHast(code, { lang, theme });
58+
const highlightToHast = (code, lang, meta = {}) =>
59+
shiki.codeToHast(code, { lang, theme, meta, transformers });
5860

5961
return {
6062
shiki,

packages/rehype-shiki/src/index.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
22
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma';
3+
import { transformerTwoslash } from '@shikijs/twoslash';
34
import cLanguage from 'shiki/langs/c.mjs';
45
import coffeeScriptLanguage from 'shiki/langs/coffeescript.mjs';
56
import cPlusPlusLanguage from 'shiki/langs/cpp.mjs';
@@ -19,6 +20,11 @@ import { createHighlighter } from './highlighter.mjs';
1920

2021
const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } =
2122
createHighlighter({
23+
transformers: [
24+
transformerTwoslash({
25+
explicitTrigger: true,
26+
}),
27+
],
2228
// We use the faster WASM engine on the server instead of the web-optimized version.
2329
//
2430
// Currently we fall back to the JavaScript RegEx engine

packages/rehype-shiki/src/plugin.mjs

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,28 @@ import { highlightToHast } from './index.mjs';
1010
// to attribute the current language of the <pre> element
1111
const languagePrefix = 'language-';
1212

13+
// The regex to match metadata
14+
const rMeta = /(\w+)(?:=(?:"([^"]+)"|(\S+)))?/g;
15+
1316
/**
14-
* Retrieve the value for the given meta key.
15-
*
16-
* @example - Returns "CommonJS"
17-
* getMetaParameter('displayName="CommonJS"', 'displayName');
18-
*
19-
* @param {any} meta - The meta parameter.
20-
* @param {string} key - The key to retrieve the value.
21-
*
22-
* @return {string | undefined} - The value related to the given key.
17+
* Parses a fenced code block metadata string into a JavaScript object.
18+
* @param {string} meta - The metadata string from a Markdown code fence.
19+
* @returns {Record<string, string|boolean>} An object representing the metadata.
2320
*/
24-
function getMetaParameter(meta, key) {
25-
if (typeof meta !== 'string') {
26-
return;
21+
function parseMeta(meta) {
22+
const obj = { __raw: meta };
23+
24+
if (!meta) {
25+
return obj;
2726
}
2827

29-
const matches = meta.match(new RegExp(`${key}="(?<parameter>[^"]*)"`));
30-
const parameter = matches?.groups.parameter;
28+
let match;
29+
30+
while ((match = rMeta.exec(meta)) !== null) {
31+
obj[match[1]] = match[2] ?? match[3] ?? true;
32+
}
3133

32-
return parameter !== undefined && parameter.length > 0
33-
? parameter
34-
: undefined;
34+
return obj;
3535
}
3636

3737
/**
@@ -65,11 +65,7 @@ export default function rehypeShikiji() {
6565

6666
while (isCodeBlock(parent?.children[currentIndex])) {
6767
const codeElement = parent?.children[currentIndex].children[0];
68-
69-
const displayName = getMetaParameter(
70-
codeElement.data?.meta,
71-
'displayName'
72-
);
68+
const meta = parseMeta(codeElement.data?.meta);
7369

7470
// We should get the language name from the class name
7571
if (codeElement.properties.className?.length) {
@@ -80,18 +76,13 @@ export default function rehypeShikiji() {
8076
}
8177

8278
// Map the display names of each variant for the CodeTab
83-
displayNames.push(displayName?.replaceAll('|', '') ?? '');
79+
displayNames.push(meta.displayName?.replaceAll('|', '') ?? '');
8480

8581
codeTabsChildren.push(parent?.children[currentIndex]);
8682

8783
// If `active="true"` is provided in a CodeBox
8884
// then the default selected entry of the CodeTabs will be the desired entry
89-
const specificActive = getMetaParameter(
90-
codeElement.data?.meta,
91-
'active'
92-
);
93-
94-
if (specificActive === 'true') {
85+
if (meta.active === 'true') {
9586
defaultTab = String(codeTabsChildren.length - 1);
9687
}
9788

@@ -162,30 +153,35 @@ export default function rehypeShikiji() {
162153
return;
163154
}
164155

156+
// Get the metadata
157+
const meta = parseMeta(preElement.data?.meta);
158+
165159
// Retrieve the whole <pre> contents as a parsed DOM string
166160
const preElementContents = toString(preElement);
167161

168162
// Grabs the relevant alias/name of the language
169163
const languageId = codeLanguage.slice(languagePrefix.length);
170164

171165
// Parses the <pre> contents and returns a HAST tree with the highlighted code
172-
const { children } = highlightToHast(preElementContents, languageId);
166+
const { children } = highlightToHast(
167+
preElementContents,
168+
languageId,
169+
meta
170+
);
173171

174172
// Adds the original language back to the <pre> element
175173
children[0].properties.class = classNames(
176174
children[0].properties.class,
177175
codeLanguage
178176
);
179177

180-
const showCopyButton = getMetaParameter(
181-
preElement.data?.meta,
182-
'showCopyButton'
183-
);
184-
185178
// Adds a Copy Button to the CodeBox if requested as an additional parameter
186179
// And avoids setting the property (overriding) if undefined or invalid value
187-
if (showCopyButton && ['true', 'false'].includes(showCopyButton)) {
188-
children[0].properties.showCopyButton = showCopyButton;
180+
if (
181+
meta.showCopyButton &&
182+
['true', 'false'].includes(meta.showCopyButton)
183+
) {
184+
children[0].properties.showCopyButton = meta.showCopyButton;
189185
}
190186

191187
// Replaces the <pre> element with the updated one
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "@shikijs/twoslash/style-rich.css"

0 commit comments

Comments
 (0)