forked from nodejs/nodejs.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
next.mdx.shiki.mjs
203 lines (164 loc) · 6.55 KB
/
next.mdx.shiki.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
'use strict';
import classNames from 'classnames';
import { toString } from 'hast-util-to-string';
import { SKIP, visit } from 'unist-util-visit';
import { getShiki, highlightToHast } from './util/getHighlighter';
// This is what Remark will use as prefix within a <pre> className
// to attribute the current language of the <pre> element
const languagePrefix = 'language-';
/**
* Retrieve the value for the given meta key.
*
* @example - Returns "CommonJS"
* getMetaParameter('displayName="CommonJS"', 'displayName');
*
* @param {any} meta - The meta parameter.
* @param {string} key - The key to retrieve the value.
*
* @return {string | undefined} - The value related to the given key.
*/
function getMetaParameter(meta, key) {
if (typeof meta !== 'string') {
return;
}
const matches = meta.match(new RegExp(`${key}="(?<parameter>[^"]*)"`));
const parameter = matches?.groups.parameter;
return parameter !== undefined && parameter.length > 0
? parameter
: undefined;
}
/**
* @typedef {import('unist').Node} Node
* @property {string} tagName
* @property {Array<import('unist').Node>} children
*/
/**
* Checks if the given node is a valid code element.
*
* @param {import('unist').Node} node - The node to be verified.
*
* @return {boolean} - True when it is a valid code element, false otherwise.
*/
function isCodeBlock(node) {
return Boolean(
node?.tagName === 'pre' && node?.children[0].tagName === 'code'
);
}
export default function rehypeShikiji() {
return async function (tree) {
// We do a top-level await, since the Unist-tree visitor
// is synchronous, and it makes more sense to do a top-level
// await, rather than an await inside the visitor function
const memoizedShiki = await getShiki();
visit(tree, 'element', (_, index, parent) => {
const languages = [];
const displayNames = [];
const codeTabsChildren = [];
let defaultTab = '0';
let currentIndex = index;
while (isCodeBlock(parent?.children[currentIndex])) {
const codeElement = parent?.children[currentIndex].children[0];
const displayName = getMetaParameter(
codeElement.data?.meta,
'displayName'
);
// We should get the language name from the class name
if (codeElement.properties.className?.length) {
const className = codeElement.properties.className.join(' ');
const matches = className.match(/language-(?<language>.*)/);
languages.push(matches?.groups.language ?? 'text');
}
// Map the display names of each variant for the CodeTab
displayNames.push(displayName?.replaceAll('|', '') ?? '');
codeTabsChildren.push(parent?.children[currentIndex]);
// If `active="true"` is provided in a CodeBox
// then the default selected entry of the CodeTabs will be the desired entry
const specificActive = getMetaParameter(
codeElement.data?.meta,
'active'
);
if (specificActive === 'true') {
defaultTab = String(codeTabsChildren.length - 1);
}
const nextNode = parent?.children[currentIndex + 1];
// If the CodeBoxes are on the root tree the next Element will be
// an empty text element so we should skip it
currentIndex += nextNode && nextNode?.type === 'text' ? 2 : 1;
}
if (codeTabsChildren.length >= 2) {
const codeTabElement = {
type: 'element',
tagName: 'CodeTabs',
children: codeTabsChildren,
properties: {
languages: languages.join('|'),
displayNames: displayNames.join('|'),
defaultTab,
},
};
// This removes all the original Code Elements and adds a new CodeTab Element
// at the original start of the first Code Element
parent.children.splice(index, currentIndex - index, codeTabElement);
// Prevent visiting the code block children and for the next N Elements
// since all of them belong to this CodeTabs Element
return [SKIP];
}
});
visit(tree, 'element', (node, index, parent) => {
// We only want to process <pre>...</pre> elements
if (!parent || index == null || node.tagName !== 'pre') {
return;
}
// We want the contents of the <pre> element, hence we attempt to get the first child
const preElement = node.children[0];
// If thereÄs nothing inside the <pre> element... What are we doing here?
if (!preElement || !preElement.properties) {
return;
}
// Ensure that we're not visiting a <code> element but it's inner contents
// (keep iterating further down until we reach where we want)
if (preElement.type !== 'element' || preElement.tagName !== 'code') {
return;
}
// Get the <pre> element class names
const preClassNames = preElement.properties.className;
// The current classnames should be an array and it should have a length
if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
return;
}
// We want to retrieve the language class name from the class names
const codeLanguage = preClassNames.find(
c => typeof c === 'string' && c.startsWith(languagePrefix)
);
// If we didn't find any `language-` classname then we shouldn't highlight
if (typeof codeLanguage !== 'string') {
return;
}
// Retrieve the whole <pre> contents as a parsed DOM string
const preElementContents = toString(preElement);
// Grabs the relevant alias/name of the language
const languageId = codeLanguage.slice(languagePrefix.length);
// Parses the <pre> contents and returns a HAST tree with the highlighted code
const { children } = highlightToHast(memoizedShiki)(
preElementContents,
languageId
);
// Adds the original language back to the <pre> element
children[0].properties.class = classNames(
children[0].properties.class,
codeLanguage
);
const showCopyButton = getMetaParameter(
preElement.data?.meta,
'showCopyButton'
);
// Adds a Copy Button to the CodeBox if requested as an additional parameter
// And avoids setting the property (overriding) if undefined or invalid value
if (showCopyButton && ['true', 'false'].includes(showCopyButton)) {
children[0].properties.showCopyButton = showCopyButton;
}
// Replaces the <pre> element with the updated one
parent.children.splice(index, 1, ...children);
});
};
}