Repository: nodejs/nodejs.dev
Pull request #3249 (2023-03-01 22:12:24)

feat(codebox): adding possibility to switch between lang
## Description

Adding possibility to switch between lang.
The label lang it's obligatory in lower case. It's more beautiful (personally) and it's better for dyslexia.
Modifying about content (```javascript -> ``js).

## Todo list:

* [X] Switch lang
* [x] Update unit test
* [X] Modifying about content (```javascript -> ``js)
* [X] Add possibility to use `sync|async` in "Lang" of code box

### Demo:

**code**:

```md
  '''cjs|esm
  const { createServer } = require('http');
  -------
  import { createServer } from 'http';
  '''
```

https://user-images.githubusercontent.com/97875033/222957220-b5ff2021-4e3e-4468-9703-4df51ba219e6.mov

### Feature unsupported

```md
  '''sync|async
  CODE sync
  -------
  CODE async
  '''
```

## Related Issues

Related #2844 (feature of switching not for parsing of dock)


AugustinMauroy: **Need help:**
*When I click to switch to another language (on the button with the writing language) for the first time nothing happens. I have to click twice to switch and then everything is fine.
It is as if the code had to heat up.
* Design: I find that if you don't know it's a switcher you can't see it. Any ideas?
codecov-commenter: # [Codecov](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Patch coverage has no change and project coverage change: **`-0.51`** :warning:
> Comparison is base [(`fd57b87`)](https://codecov.io/gh/nodejs/nodejs.dev/commit/fd57b871c2b1ad2f6fb812ed066dec074d76db22?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 66.02% compared to head [(`8517b46`)](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 65.52%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3249      +/-   ##
==========================================
- Coverage   66.02%   65.52%   -0.51%     
==========================================
  Files         118      145      +27     
  Lines        1351     1749     +398     
  Branches      342      407      +65     
==========================================
+ Hits          892     1146     +254     
- Misses        422      558     +136     
- Partials       37       45       +8     
```


| [Impacted Files](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/templates/blog.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RlbXBsYXRlcy9ibG9nLnRzeA==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [src/hooks/useNodeJsContributorsApi.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2hvb2tzL3VzZU5vZGVKc0NvbnRyaWJ1dG9yc0FwaS50c3g=) | `73.61% <0.00%> (-26.39%)` | :arrow_down: |
| [src/hooks/useDetectOs.ts](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2hvb2tzL3VzZURldGVjdE9zLnRz) | `80.00% <0.00%> (-20.00%)` | :arrow_down: |
| [util-node/getNodeReleasesData.js](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dXRpbC1ub2RlL2dldE5vZGVSZWxlYXNlc0RhdGEuanM=) | `91.30% <0.00%> (-6.32%)` | :arrow_down: |
| [src/\_\_fixtures\_\_/page.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL19fZml4dHVyZXNfXy9wYWdlLnRzeA==) | `94.11% <0.00%> (-5.89%)` | :arrow_down: |
| [src/pages/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3BhZ2VzL2luZGV4LnRzeA==) | `100.00% <0.00%> (ø)` | |
| [src/templates/api.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RlbXBsYXRlcy9hcGkudHN4) | `0.00% <0.00%> (ø)` | |
| [src/templates/learn.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RlbXBsYXRlcy9sZWFybi50c3g=) | `100.00% <0.00%> (ø)` | |
| [util-node/createSlug.js](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dXRpbC1ub2RlL2NyZWF0ZVNsdWcuanM=) | `100.00% <0.00%> (ø)` | |
| [util-node/getNvmData.js](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dXRpbC1ub2RlL2dldE52bURhdGEuanM=) | `100.00% <0.00%> (ø)` | |
| ... and [187 more](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/nodejs/nodejs.dev/pull/3249?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

github-actions[bot]: Please find a preview at: https://staging.nodejs.dev/3249/
ovflowd: Superb work, @AugustinMauroy. It took a while, but we got to the right direction :)

Patch:
diff --git a/content/about/about.en.md b/content/about/about.en.md
index 656e3ce0..691a6ac7 100644
--- a/content/about/about.en.md
+++ b/content/about/about.en.md
@@ -11,7 +11,7 @@ scalable network applications. In the following "hello world" example, many
 connections can be handled concurrently. Upon each connection, the callback is
 fired, but if there is no work to be done, Node.js will sleep.
 
-```javascript
+```js
 const http = require('http');
 
 const hostname = '127.0.0.1';
diff --git a/content/about/about.ja.md b/content/about/about.ja.md
index 48deb2b2..8794d909 100644
--- a/content/about/about.ja.md
+++ b/content/about/about.ja.md
@@ -8,7 +8,7 @@ category: about
 
 Node.js は、非同期のイベントドリブン JavaScript ランタイムとして、スケーラブルなネットワークアプリケーションを構築するために設計されています。次の "hello world" の例では、多くの接続を同時に処理できます。接続を並列に処理することができます。各接続の際に、コールバックが呼び出されます。しかし、何もすることがない場合は、Node.js はスリープします。
 
-```javascript
+```js
 const http = require('http');
 
 const hostname = '127.0.0.1';
diff --git a/content/about/about.pt.md b/content/about/about.pt.md
index 0e739f85..1c887439 100644
--- a/content/about/about.pt.md
+++ b/content/about/about.pt.md
@@ -8,7 +8,7 @@ category: about
 
 Como executor de JavaScript orientado a eventos assíncronos, a Node está desenhada para construir aplicações de rede escaláveis. No seguinte exemplo "hello world" ou "olá mundo" em Português, várias conexões podem ser manipuladas simultaneamente. Sobre cada conexão, uma função de resposta é disparada, mas se não existe nada a ser feito, a Node.js adormecerá.
 
-```javascript
+```js
 const http = require('http');
 
 const hostname = '127.0.0.1';
diff --git a/src/components/ArticleComponents/Codebox/index.module.scss b/src/components/ArticleComponents/Codebox/index.module.scss
index 0cd2be98..bdbea3cb 100644
--- a/src/components/ArticleComponents/Codebox/index.module.scss
+++ b/src/components/ArticleComponents/Codebox/index.module.scss
@@ -41,8 +41,8 @@
     flex-direction: row;
     justify-content: space-between;
 
-    span,
-    button {
+    .lang,
+    .copy {
       align-items: center;
       display: inherit;
       font-size: var(--font-size-code);
@@ -50,14 +50,23 @@
       justify-content: center;
       width: 86px;
     }
-
-    span {
+    .langBox {
       background-color: var(--black4);
       border-radius: 0 0 0.3rem 0;
-      color: var(--black9);
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      width: 100px;
+
+      .lang {
+        background-color: var(--black4);
+        border-width: 0;
+        color: var(--black9);
+        width: max-content;
+      }
     }
 
-    button {
+    .copy {
       background-color: var(--black9);
       border-radius: 0 0 0 0.3rem;
       border-width: 0;
diff --git a/src/components/ArticleComponents/Codebox/index.tsx b/src/components/ArticleComponents/Codebox/index.tsx
index 442cb626..97a24923 100644
--- a/src/components/ArticleComponents/Codebox/index.tsx
+++ b/src/components/ArticleComponents/Codebox/index.tsx
@@ -15,54 +15,59 @@ interface Props {
   };
 }
 
-const replaceLanguages = (language: string) =>
+export const replaceLabelLanguages = (language: string) =>
+  language.replace(/console/i, 'bash');
+
+export const replaceLanguages = (language: string) =>
   language
     .replace(/mjs|cjs|javascript/i, 'js')
     .replace(/console|shell/i, 'bash');
 
-const replaceLabelLanguages = (language: string) =>
-  language
-    .replace(/javascript/i, 'cjs')
-    .replace(/console|shell/i, 'bash')
-    .toUpperCase();
-
 const Codebox = ({ children: { props } }: Props): JSX.Element => {
   const [parsedCode, setParsedCode] = useState('');
   const [copied, copyText] = useCopyToClipboard();
-
+  const [langIndex, setLangIndex] = useState(0);
   // eslint-disable-next-line react/prop-types
   const className = props.className || 'text';
-
   // Language Matches in class
   const matches = className.match(/language-(?<lang>.*)/);
-
-  // Language name from classname
-  const language = matches?.groups?.lang || 'text';
-
-  // Actual Code into a stringified format
-  const stringCode = props.children?.toString() || '';
+  const languageOptions = (matches?.groups?.lang || 'text').split('|');
+  const language = languageOptions[langIndex];
+  const codeArray = props.children
+    ? props.children.toString().split('--------------\n')
+    : [''];
 
   const handleCopyCode = async (event: React.MouseEvent<HTMLButtonElement>) => {
     event.preventDefault();
-    copyText(stringCode);
+    copyText(codeArray[langIndex]);
   };
 
   useEffect(() => {
     const parsedLanguage = replaceLanguages(language);
-
     const prismLanguage = languages[parsedLanguage] || languages.text;
 
     setParsedCode(
-      sanitize(highlight(stringCode, prismLanguage, parsedLanguage))
+      sanitize(highlight(codeArray[langIndex], prismLanguage, parsedLanguage))
     );
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
+  }, [langIndex]);
 
   return (
     <pre className={classnames(styles.pre, replaceLanguages(className))}>
       <div className={styles.top}>
-        <span>{replaceLabelLanguages(language)}</span>
-        <button type="button" onClick={handleCopyCode}>
+        <div className={styles.langBox}>
+          {languageOptions.map((lang, index) => (
+            <button
+              type="button"
+              key={lang}
+              className={styles.lang}
+              onClick={() => setLangIndex(index)}
+            >
+              {replaceLabelLanguages(lang.toLowerCase())}
+            </button>
+          ))}
+        </div>
+        <button type="button" className={styles.copy} onClick={handleCopyCode}>
           <FormattedMessage id="components.codeBox.copy" values={{ copied }} />
         </button>
       </div>


Test patch:
diff --git a/src/components/ArticleComponents/Codebox/__tests__/__snapshots__/index.test.tsx.snap b/src/components/ArticleComponents/Codebox/__tests__/__snapshots__/index.test.tsx.snap
index 069bec52..7133d858 100644
--- a/src/components/ArticleComponents/Codebox/__tests__/__snapshots__/index.test.tsx.snap
+++ b/src/components/ArticleComponents/Codebox/__tests__/__snapshots__/index.test.tsx.snap
@@ -1,17 +1,147 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Codebox component renders correctly 1`] = `
+exports[`Codebox component (multiple langs) renders correctly 1`] = `
 <div>
   <pre
-    class="pre language-html"
+    class="pre language-js|language-js"
   >
     <div
       class="top"
     >
-      <span>
-        HTML
+      <div
+        class="langBox"
+      >
+        <button
+          class="lang"
+          type="button"
+        >
+          js
+        </button>
+        <button
+          class="lang"
+          type="button"
+        >
+          language-js
+        </button>
+      </div>
+      <button
+        class="copy"
+        type="button"
+      >
+        copy
+      </button>
+    </div>
+    <div
+      class="content"
+    >
+      <span
+        class="token keyword"
+      >
+        const
+      </span>
+       http 
+      <span
+        class="token operator"
+      >
+        =
+      </span>
+       
+      <span
+        class="token function"
+      >
+        require
+      </span>
+      <span
+        class="token punctuation"
+      >
+        (
+      </span>
+      <span
+        class="token string"
+      >
+        'http'
+      </span>
+      <span
+        class="token punctuation"
+      >
+        )
+      </span>
+      <span
+        class="token punctuation"
+      >
+        ;
+      </span>
+      
+  
+      <span
+        class="token operator"
+      >
+        --
+      </span>
+      <span
+        class="token operator"
+      >
+        --
+      </span>
+      <span
+        class="token operator"
+      >
+        --
+      </span>
+      <span
+        class="token operator"
+      >
+        -
       </span>
+      
+  
+      <span
+        class="token keyword"
+      >
+        import
+      </span>
+       http 
+      <span
+        class="token keyword"
+      >
+        from
+      </span>
+       
+      <span
+        class="token string"
+      >
+        'http'
+      </span>
+      <span
+        class="token punctuation"
+      >
+        ;
+      </span>
+    </div>
+  </pre>
+</div>
+`;
+
+exports[`Codebox component (one lang) renders correctly 1`] = `
+<div>
+  <pre
+    class="pre language-js"
+  >
+    <div
+      class="top"
+    >
+      <div
+        class="langBox"
+      >
+        <button
+          class="lang"
+          type="button"
+        >
+          js
+        </button>
+      </div>
       <button
+        class="copy"
         type="button"
       >
         copy
@@ -20,7 +150,191 @@ exports[`Codebox component renders correctly 1`] = `
     <div
       class="content"
     >
-      [object Object]
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      span 
+      <span
+        class="token keyword"
+      >
+        class
+      </span>
+      <span
+        class="token operator"
+      >
+        =
+      </span>
+      <span
+        class="token string"
+      >
+        "token keyword"
+      </span>
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+      <span
+        class="token keyword"
+      >
+        const
+      </span>
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      <span
+        class="token operator"
+      >
+        /
+      </span>
+      span
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+       a 
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      span 
+      <span
+        class="token keyword"
+      >
+        class
+      </span>
+      <span
+        class="token operator"
+      >
+        =
+      </span>
+      <span
+        class="token string"
+      >
+        "token operator"
+      </span>
+      <span
+        class="token operator"
+      >
+        &gt;=
+      </span>
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      <span
+        class="token operator"
+      >
+        /
+      </span>
+      span
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+       
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      span 
+      <span
+        class="token keyword"
+      >
+        class
+      </span>
+      <span
+        class="token operator"
+      >
+        =
+      </span>
+      <span
+        class="token string"
+      >
+        "token number"
+      </span>
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+      <span
+        class="token number"
+      >
+        1
+      </span>
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      <span
+        class="token operator"
+      >
+        /
+      </span>
+      span
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      span 
+      <span
+        class="token keyword"
+      >
+        class
+      </span>
+      <span
+        class="token operator"
+      >
+        =
+      </span>
+      <span
+        class="token string"
+      >
+        "token punctuation"
+      </span>
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
+      <span
+        class="token punctuation"
+      >
+        ;
+      </span>
+      <span
+        class="token operator"
+      >
+        &lt;
+      </span>
+      <span
+        class="token operator"
+      >
+        /
+      </span>
+      span
+      <span
+        class="token operator"
+      >
+        &gt;
+      </span>
     </div>
   </pre>
 </div>
diff --git a/src/components/ArticleComponents/Codebox/__tests__/index.test.tsx b/src/components/ArticleComponents/Codebox/__tests__/index.test.tsx
index e467286d..4f6e3a00 100644
--- a/src/components/ArticleComponents/Codebox/__tests__/index.test.tsx
+++ b/src/components/ArticleComponents/Codebox/__tests__/index.test.tsx
@@ -1,8 +1,10 @@
 import React from 'react';
 import userEvent from '@testing-library/user-event';
 import { render, screen } from '@testing-library/react';
+import { highlight, languages } from 'prismjs';
+import { sanitize } from 'isomorphic-dompurify';
 
-import Codebox from '../index';
+import Codebox, { replaceLabelLanguages, replaceLanguages } from '../index';
 
 Object.assign(navigator, {
   clipboard: {
@@ -16,14 +18,30 @@ afterEach(() => {
   jest.clearAllMocks();
 });
 
-describe('Codebox component', (): void => {
+describe('Replacer tests', (): void => {
+  it('replaceLabelLanguages', (): void => {
+    expect(replaceLabelLanguages('language-console')).toBe('language-bash');
+  });
+
+  it('replaceLanguages', (): void => {
+    expect(replaceLanguages('language-mjs')).toBe('language-js');
+    expect(replaceLanguages('language-cjs')).toBe('language-js');
+    expect(replaceLanguages('language-javascript')).toBe('language-js');
+    expect(replaceLanguages('language-console')).toBe('language-bash');
+    expect(replaceLanguages('language-shell')).toBe('language-bash');
+  });
+});
+
+describe('Codebox component (one lang)', (): void => {
+  const code = 'const a = 1;';
+  const textToCopy = sanitize(highlight(code, languages.js, 'js'));
+
   it('renders correctly', (): void => {
-    const textToCopy = <p>text to be copy</p>;
     const { container } = render(
       <Codebox>
         {{
           props: {
-            className: 'language-html',
+            className: 'language-js',
             children: textToCopy,
           },
         }}
@@ -33,13 +51,11 @@ describe('Codebox component', (): void => {
   });
 
   it('renders correctly', async () => {
-    const textToCopy = <p>text to be copy</p>;
-
     render(
       <Codebox>
         {{
           props: {
-            className: 'language-html',
+            className: 'language-js',
             children: textToCopy,
           },
         }}
@@ -57,3 +73,63 @@ describe('Codebox component', (): void => {
     expect(navigatorClipboardSpy).toHaveBeenCalledWith(textToCopy.toString());
   });
 });
+
+describe('Codebox component (multiple langs)', (): void => {
+  const code = `const http = require('http');
+  -------
+  import http from 'http';`;
+
+  it('renders correctly', (): void => {
+    const { container } = render(
+      <Codebox>
+        {{
+          props: {
+            className: 'language-js|language-js',
+            children: code,
+          },
+        }}
+      </Codebox>
+    );
+    expect(container).toMatchSnapshot();
+  });
+
+  it('switch between languages', async () => {
+    render(
+      <Codebox>
+        {{
+          props: {
+            className: 'language-cjs|language-mjs',
+            children: code,
+          },
+        }}
+      </Codebox>
+    );
+
+    const buttonElement = screen.getByText('cjs');
+    userEvent.click(buttonElement);
+
+    await screen.findByText('cjs');
+
+    expect(screen.getByText('cjs')).toBeInTheDocument();
+  });
+
+  it('switch between sync and async', async () => {
+    render(
+      <Codebox>
+        {{
+          props: {
+            className: 'language-async|language-sync',
+            children: code,
+          },
+        }}
+      </Codebox>
+    );
+
+    const buttonElement = screen.getByText('async');
+    userEvent.click(buttonElement);
+
+    await screen.findByText('async');
+
+    expect(screen.getByText('async')).toBeInTheDocument();
+  });
+});