The Node.js® Website
at main 3.4 kB view raw
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;