forked from nodejs/doc-kit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode-tabs.mjs
More file actions
106 lines (84 loc) · 2.63 KB
/
code-tabs.mjs
File metadata and controls
106 lines (84 loc) · 2.63 KB
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
'use strict';
import { SKIP, visit } from 'unist-util-visit';
const languagePrefix = 'language-';
/**
* Checks if a HAST node is a <pre><code> code block.
*
* @param {import('hast').Node} node
* @returns {boolean}
*/
function isCodeBlock(node) {
return Boolean(
node?.tagName === 'pre' && node?.children[0].tagName === 'code'
);
}
/**
* Extracts the language identifier from a <code> element's className.
*
* @param {import('hast').Element} codeElement
* @returns {string}
*/
function getLanguage(codeElement) {
const className = codeElement.properties?.className;
if (!Array.isArray(className)) {
return 'text';
}
const langClass = className.find(
c => typeof c === 'string' && c.startsWith(languagePrefix)
);
return langClass ? langClass.slice(languagePrefix.length) : 'text';
}
/**
* A rehype plugin that assigns display names to consecutive code blocks
* sharing the same language, preventing ambiguous tab labels like "JS | JS".
*
* Must run before @node-core/rehype-shiki so that displayName metadata
* is available when CodeTabs are assembled.
*
* @type {import('unified').Plugin}
*/
export default function codeTabs() {
return function (tree) {
visit(tree, 'element', (node, index, parent) => {
if (!parent || index == null || !isCodeBlock(node)) {
return;
}
const group = [];
let currentIndex = index;
while (isCodeBlock(parent.children[currentIndex])) {
group.push(currentIndex);
const nextNode = parent.children[currentIndex + 1];
currentIndex += nextNode && nextNode.type === 'text' ? 2 : 1;
}
if (group.length < 2) {
return;
}
const languages = group.map(idx =>
getLanguage(parent.children[idx].children[0])
);
const counts = {};
for (const lang of languages) {
counts[lang] = (counts[lang] || 0) + 1;
}
// If no language appears more than once, rehype-shiki handles it fine
const hasDuplicates = Object.values(counts).some(c => c > 1);
if (!hasDuplicates) {
return;
}
// Assign display names like (1), (2) for duplicated languages
const counters = {};
for (let i = 0; i < group.length; i++) {
const lang = languages[i];
if (counts[lang] < 2) {
continue;
}
counters[lang] = (counters[lang] || 0) + 1;
const codeElement = parent.children[group[i]].children[0];
codeElement.data = codeElement.data || {};
codeElement.data.meta =
`${codeElement.data.meta || ''} displayName="(${counters[lang]})"`.trim();
}
return [SKIP];
});
};
}