diff --git a/apps/site/pages/en/about/index.mdx b/apps/site/pages/en/about/index.mdx index 9a8688f2a1824..229942e03f3b5 100644 --- a/apps/site/pages/en/about/index.mdx +++ b/apps/site/pages/en/about/index.mdx @@ -44,6 +44,14 @@ server.listen(port, hostname, () => { }); ``` +```cjs +const type = 'commonjs'; +``` + +```mjs +const type = 'module'; +``` + This is in contrast to today's more common concurrency model, in which OS threads are employed. Thread-based networking is relatively inefficient and very difficult to use. Furthermore, users of Node.js are free from worries of diff --git a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs index 7ab0fcaa6e1b0..c1cd35e7ac491 100644 --- a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs +++ b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs @@ -19,7 +19,7 @@ mock.module('shiki/themes/nord.mjs', { defaultExport: { name: 'nord', colors: { 'editor.background': '#2e3440' } }, }); -describe('createHighlighter', async () => { +describe('createHighlighter', { concurrency: true }, async () => { const { createHighlighter } = await import('../highlighter.mjs'); describe('getLanguageDisplayName', () => { diff --git a/packages/rehype-shiki/src/__tests__/plugin.test.mjs b/packages/rehype-shiki/src/__tests__/plugin.test.mjs index 11f545ad28ba3..5e335177ebab8 100644 --- a/packages/rehype-shiki/src/__tests__/plugin.test.mjs +++ b/packages/rehype-shiki/src/__tests__/plugin.test.mjs @@ -1,36 +1,826 @@ import assert from 'node:assert/strict'; import { describe, it, mock } from 'node:test'; +import rehypeShikiji from '../plugin.mjs'; // Simplified mocks - only mock what's actually needed mock.module('../index.mjs', { - namedExports: { highlightToHast: mock.fn(() => ({ children: [] })) }, -}); + namedExports: { + highlightToHast: mock.fn((code, lang) => { + if (!lang) throw new Error('Language is required'); -mock.module('classnames', { - defaultExport: (...args) => args.filter(Boolean).join(' '), + return { + children: [ + { + type: 'element', + tagName: 'code', + properties: { className: [`language-${lang}`] }, + children: [{ type: 'text', value: code }], + }, + ], + } + } + ), + } }); -mock.module('hast-util-to-string', { - namedExports: { toString: () => 'code' }, -}); +describe('rehypeShikiji', { concurrency: true }, async () => { + it('creates CodeTabs for JS/TS code blocks', () => { + const treeToTransform = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JS"' }, + properties: { className: ['language-js'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="TS"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + }; -const mockVisit = mock.fn(); -mock.module('unist-util-visit', { - namedExports: { visit: mockVisit, SKIP: Symbol() }, -}); + rehypeShikiji()(treeToTransform); + + assert.strictEqual( + treeToTransform.children.length, + 1, + 'Should group JS/TS blocks into a single CodeTabs' + ); + const codeTabs = treeToTransform.children[0]; + assert.strictEqual(codeTabs.tagName, 'CodeTabs', 'Should create a CodeTabs element'); + assert.deepStrictEqual( + codeTabs.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JS"' }, + properties: { className: ['language-js'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="TS"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + 'CodeTabs should contain both JS and TS code blocks' + ); + assert.deepStrictEqual( + codeTabs.properties, + { + languages: 'js|ts', + displayNames: 'JS|TS', + }, + 'CodeTabs should have correct properties' + ); + }); + + it('creates CodeTabs for CJS/ESM code blocks', () => { + const treeToTransform = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(treeToTransform); + + assert.strictEqual( + treeToTransform.children.length, + 1, + 'Should group CJS/ESM blocks into a single CodeTabs' + ); + const codeTabs = treeToTransform.children[0]; + assert.strictEqual(codeTabs.tagName, 'CodeTabs', 'Should create a CodeTabs element'); + assert.deepStrictEqual( + codeTabs.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + 'CodeTabs should contain both CJS and ESM code blocks' + ); + assert.deepStrictEqual( + codeTabs.properties, + { + languages: 'cjs|esm', + displayNames: 'CJS|ESM', + }, + 'CodeTabs should have correct properties' + ); + + const treeToTransformbis = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(treeToTransformbis); -describe('rehypeShikiji', async () => { - const { default: rehypeShikiji } = await import('../plugin.mjs'); - const mockTree = { type: 'root', children: [] }; + assert.strictEqual( + treeToTransformbis.children.length, + 1, + 'Should group ESM/CJS blocks into a single CodeTabs' + ); + const codeTabsBis = treeToTransformbis.children[0]; + assert.strictEqual(codeTabsBis.tagName, 'CodeTabs', 'Should create a CodeTabs element'); + assert.deepStrictEqual( + codeTabsBis.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + ], + 'CodeTabs should contain both ESM and CJS code blocks' + ); + assert.deepStrictEqual( + codeTabsBis.properties, + { + languages: 'esm|cjs', + displayNames: 'ESM|CJS', + }, + 'CodeTabs should have correct properties' + ); + }); + + it('If there are a sequence of codeblock of CJS/ESM, it should create pairs of CodeTabs', () => { + const parent = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }; - it('calls visit twice', () => { - mockVisit.mock.resetCalls(); - rehypeShikiji()(mockTree); - assert.strictEqual(mockVisit.mock.calls.length, 2); + rehypeShikiji()(parent); + + assert.strictEqual( + parent.children.length, + 2, + 'Should create two CodeTabs groups' + ); + + // Check first CodeTabs group + const firstGroup = parent.children[0]; + assert.deepStrictEqual( + firstGroup, + { + type: 'element', // Added type property + tagName: 'CodeTabs', + properties: { + languages: 'cjs|esm', + displayNames: 'CJS|ESM', + }, + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }, + 'Group 1 should be a CodeTabs group with correct structure' + ); + + // Check second CodeTabs group + const secondGroup = parent.children[1]; + assert.deepStrictEqual( + secondGroup, + { + type: 'element', // Added type property + tagName: 'CodeTabs', + properties: { + languages: 'cjs|esm', + displayNames: 'CJS|ESM', + }, + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, // Ensure this is expected + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, // Ensure this is expected + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }, + 'Group 2 should be a CodeTabs group with correct structure' + ); }); - it('creates CodeTabs for multiple code blocks', () => { + it("if it isn't a sequence of cjs/esm codeblock, it should not pair them", () => { const parent = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="TS"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(parent); + + assert.strictEqual( + parent.children.length, + 3, + 'Should not create CodeTabs groups' + ); + + assert.deepStrictEqual( + parent.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="CJS"' }, + properties: { className: ['language-cjs'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="TS"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + 'Children should remain as individual pre/code blocks' + ); + + const parentBis = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="package.json"' }, + properties: { className: ['language-json'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="workflow"' }, + properties: { className: ['language-yaml'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="mod.ts"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(parentBis); + + assert.strictEqual( + parentBis.children.length, + 3, + 'Should not create CodeTabs groups for different languages' + ); + + assert.deepStrictEqual( + parentBis.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="package.json"' }, + properties: { className: ['language-json'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="workflow"' }, + properties: { className: ['language-yaml'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="mod.ts"' }, + properties: { className: ['language-ts'] }, + }, + ], + }, + ], + 'Children should remain as individual pre/code blocks for different languages' + ); + }); + + it('should not create CodeTabs for non-JS/TS/CJS/ESM code blocks', () => { + const treeToTransform = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="YAML"' }, + properties: { className: ['language-yaml'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JS"' }, + properties: { className: ['language-js'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JSON"' }, + properties: { className: ['language-json'] }, + }, + ], + } + ], + }; + + rehypeShikiji()(treeToTransform); + + assert.strictEqual( + treeToTransform.children.length, + 3, + 'Should not group non-JS/TS/CJS/ESM blocks into CodeTabs' + ); + assert.deepStrictEqual( + treeToTransform.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="YAML"' }, + properties: { className: ['language-yaml'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JS"' }, + properties: { className: ['language-js'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JSON"' }, + properties: { className: ['language-json'] }, + }, + ], + } + ], + 'Children should remain as individual pre/code blocks for non-JS/TS/CJS/ESM languages' + ); + }); + + it("should create CodeTabs for a sequence n snippet of ESM/CJS code blocks", () => { + const parent = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(parent); + + + assert.deepStrictEqual( + parent.children[0], + { + tagName: 'CodeTabs', + type: 'element', + properties: { + languages: 'esm|esm|esm|esm', + displayNames: 'ESM|ESM|ESM|ESM', + }, + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="ESM"' }, + properties: { className: ['language-esm'] }, + }, + ], + }, + ], + }, + 'Should create a single CodeTabs with all ESM code blocks' + ); + assert.strictEqual( + parent.children.length, + 1, + 'Should create two CodeTabs groups for CJS/ESM pairs' + ); + }) + + it("should create CodeTabs for a sequence n snippet of text code blocks", () => { + const parent = { + type: 'root', + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 1"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 2"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 3"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + ], + }; + + rehypeShikiji()(parent); + + assert.deepStrictEqual( + parent.children[0], + { + tagName: 'CodeTabs', + type: 'element', + properties: { + languages: 'text|text|text', + displayNames: 'Text 1|Text 2|Text 3', + }, + children: [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 1"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 2"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="Text 3"' }, + properties: { className: ['language-text'] }, + }, + ], + }, + ], + }, + 'Should create a single CodeTabs with all text code blocks' + ); + assert.strictEqual( + parent.children.length, + 1, + 'Should create a single CodeTabs group for text code blocks' + ); + }); + + it("should have a copy button for each code block", () => { + const treeToTransform = { + type: 'root', + data: { + meta: 'showCopyButton="true"' + }, children: [ { tagName: 'pre', @@ -55,13 +845,34 @@ describe('rehypeShikiji', async () => { ], }; - mockVisit.mock.mockImplementation((tree, selector, visitor) => { - if (selector === 'element') { - visitor(parent.children[0], 0, parent); - } - }); + rehypeShikiji()(treeToTransform); - rehypeShikiji()(mockTree); - assert.ok(parent.children.some(child => child.tagName === 'CodeTabs')); + assert.strictEqual(treeToTransform.type, 'root'); + assert.strictEqual(treeToTransform.children.length, 2); + assert.deepStrictEqual( + treeToTransform.children, + [ + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="JS"' }, + properties: { className: ['language-js'] }, + } + ] + }, + { + tagName: 'pre', + children: [ + { + tagName: 'code', + data: { meta: 'displayName="TS"' }, + properties: { className: ['language-ts'] }, + } + ] + }, + ], + ); }); }); diff --git a/packages/rehype-shiki/src/plugin.mjs b/packages/rehype-shiki/src/plugin.mjs index c07416a4db326..01ed9222f998c 100644 --- a/packages/rehype-shiki/src/plugin.mjs +++ b/packages/rehype-shiki/src/plugin.mjs @@ -16,7 +16,7 @@ const languagePrefix = 'language-'; * @example - Returns "CommonJS" * getMetaParameter('displayName="CommonJS"', 'displayName'); * - * @param {any} meta - The meta parameter. + * @param {unknown} meta - The meta parameter. * @param {string} key - The key to retrieve the value. * * @return {string | undefined} - The value related to the given key. @@ -34,12 +34,6 @@ function getMetaParameter(meta, key) { : undefined; } -/** - * @typedef {import('unist').Node} Node - * @property {string} tagName - * @property {Array} children - */ - /** * Checks if the given node is a valid code element. * @@ -53,9 +47,28 @@ function isCodeBlock(node) { ); } +/** + * @param {string} className - The class name to extract the language from. + * @returns {string} - The language extracted from the class name. + */ +function getLanguageFromClassName(className) { + const matches = className.match(new RegExp(`${languagePrefix}(?.*)`)); + return matches?.groups.language ?? 'text'; +} + +/** + * This plugin converts CodeBox elements to CodeTabs and highlights code blocks. + * sequence of language should be: + * ESM/CJS || CJS/ESM => 1 codeTabs + * ESM/CJS/ESM/CJS => 2 codeTabs + * TS/JS || JS/TS => 1 codeTabs + * TS/ESM/JS || JS/TS/ESM => 1 codeTabs + * YAML/JSON/... => 1 codeTabs (n lambda languages) + */ export default function rehypeShikiji() { return function (tree) { - visit(tree, 'element', (_, index, parent) => { + // 1. Convert CodeBox elements to CodeTabs + visit(tree, 'element', (element, index, parent) => { const languages = []; const displayNames = []; const codeTabsChildren = []; @@ -73,10 +86,9 @@ export default function rehypeShikiji() { // 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-(?.*)/); - - languages.push(matches?.groups.language ?? 'text'); + languages.push( + getLanguageFromClassName(codeElement.properties.className[0]) + ); } // Map the display names of each variant for the CodeTab @@ -102,6 +114,7 @@ export default function rehypeShikiji() { currentIndex += nextNode && nextNode?.type === 'text' ? 2 : 1; } + // @todo(@AugustinMauroy): add logic to handle sequences of languages if (codeTabsChildren.length >= 2) { const codeTabElement = { type: 'element', @@ -124,6 +137,7 @@ export default function rehypeShikiji() { } }); + // 2. Convert
 elements with a language className to highlighted code
     visit(tree, 'element', (node, index, parent) => {
       // We only want to process 
...
elements if (!parent || index == null || node.tagName !== 'pre') { @@ -185,6 +199,9 @@ export default function rehypeShikiji() { // 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)) { + throw new Error( + 'The `showCopyButton` meta parameter is deprecated. Use `show-copy-button` instead.' + ); children[0].properties.showCopyButton = showCopyButton; }