The Node.js® Website
1'use client';
2
3import {
4 DocumentDuplicateIcon,
5 CodeBracketIcon,
6} from '@heroicons/react/24/outline';
7import { useTranslations } from 'next-intl';
8import type { FC, PropsWithChildren, ReactNode } from 'react';
9import { Fragment, isValidElement, useRef } from 'react';
10
11import Button from '@/components/Common/Button';
12import { useCopyToClipboard, useNotification } from '@/hooks';
13
14import styles from './index.module.css';
15
16// Transforms a code element with plain text content into a more structured
17// format for rendering with line numbers
18const transformCode = (code: ReactNode, language: string): ReactNode => {
19 if (!isValidElement(code)) {
20 // Early return when the `CodeBox` child is not a valid element since the
21 // type is a ReactNode, and can assume any value
22 return code;
23 }
24
25 const content = code.props?.children;
26
27 if (code.type !== 'code' || typeof content !== 'string') {
28 // There is no need to transform an element that is not a code element or
29 // a content that is not a string
30 return code;
31 }
32
33 // Note that since we use `.split` we will have an extra entry
34 // being an empty string, so we need to remove it
35 const lines = content.split('\n');
36
37 const extraStyle = language.length === 0 ? { fontFamily: 'monospace' } : {};
38
39 return (
40 <code style={extraStyle}>
41 {lines
42 .flatMap((line, lineIndex) => {
43 const columns = line.split(' ');
44
45 return [
46 <span key={lineIndex} className="line">
47 {columns.map((column, columnIndex) => (
48 <Fragment key={columnIndex}>
49 <span>{column}</span>
50 {columnIndex < columns.length - 1 && <span> </span>}
51 </Fragment>
52 ))}
53 </span>,
54 // Add a break line so the text content is formatted correctly
55 // when copying to clipboard
56 '\n',
57 ];
58 })
59 // Here we remove that empty line from before and
60 // the last flatMap entry which is an `\n`
61 .slice(0, -2)}
62 </code>
63 );
64};
65
66type CodeBoxProps = { language: string; showCopyButton?: boolean };
67
68const CodeBox: FC<PropsWithChildren<CodeBoxProps>> = ({
69 children,
70 language,
71 showCopyButton = true,
72}) => {
73 const ref = useRef<HTMLPreElement>(null);
74
75 const notify = useNotification();
76 const [, copyToClipboard] = useCopyToClipboard();
77 const t = useTranslations();
78
79 const onCopy = async () => {
80 if (ref.current?.textContent) {
81 copyToClipboard(ref.current.textContent);
82
83 notify({
84 duration: 3000,
85 message: (
86 <div className={styles.notification}>
87 <CodeBracketIcon className={styles.icon} />
88 {t('components.common.codebox.copied')}
89 </div>
90 ),
91 });
92 }
93 };
94
95 return (
96 <div className={styles.root}>
97 <pre ref={ref} className={styles.content} tabIndex={0}>
98 {transformCode(children, language)}
99 </pre>
100
101 {language && (
102 <div className={styles.footer}>
103 <span className={styles.language}>{language}</span>
104
105 {showCopyButton && (
106 <Button kind="neutral" className={styles.action} onClick={onCopy}>
107 <DocumentDuplicateIcon className={styles.icon} />
108 {t('components.common.codebox.copy')}
109 </Button>
110 )}
111 </div>
112 )}
113 </div>
114 );
115};
116
117export default CodeBox;