-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Expand file tree
/
Copy pathindex.tsx
More file actions
131 lines (112 loc) · 3.58 KB
/
index.tsx
File metadata and controls
131 lines (112 loc) · 3.58 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
'use client';
import {
CodeBracketIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
import { useTranslations } from 'next-intl';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { Fragment, isValidElement, useRef } from 'react';
import Button from '@/components/Common/Button';
import { useCopyToClipboard, useNotification } from '@/hooks';
import styles from './index.module.css';
// Transforms a code element with plain text content into a more structured
// format for rendering with line numbers
const transformCode = <T extends ReactElement<PropsWithChildren>>(
code: T,
language: string
): ReactElement<HTMLElement> | T => {
if (!isValidElement(code)) {
// Early return when the `CodeBox` child is not a valid element since the
// type is a ReactNode, and can assume any value
return code;
}
const content = code.props?.children;
if (code.type !== 'code' || typeof content !== 'string') {
// There is no need to transform an element that is not a code element or
// a content that is not a string
return code;
}
// Note that since we use `.split` we will have an extra entry
// being an empty string, so we need to remove it
const lines = content.split('\n');
const extraStyle = language.length === 0 ? { fontFamily: 'monospace' } : {};
return (
<code style={extraStyle}>
{lines
.flatMap((line, lineIndex) => {
const columns = line.split(' ');
return [
<span key={lineIndex} className="line">
{columns.map((column, columnIndex) => (
<Fragment key={columnIndex}>
<span>{column}</span>
{columnIndex < columns.length - 1 && <span> </span>}
</Fragment>
))}
</span>,
// Add a break line so the text content is formatted correctly
// when copying to clipboard
'\n',
];
})
// Here we remove that empty line from before and
// the last flatMap entry which is an `\n`
.slice(0, -2)}
</code>
);
};
type CodeBoxProps = {
language: string;
showCopyButton?: boolean;
className?: string;
};
const CodeBox: FC<PropsWithChildren<CodeBoxProps>> = ({
children,
language,
showCopyButton = true,
className,
}) => {
const ref = useRef<HTMLPreElement>(null);
const notify = useNotification();
const [, copyToClipboard] = useCopyToClipboard();
const t = useTranslations();
const onCopy = async () => {
if (ref.current?.textContent) {
copyToClipboard(ref.current.textContent);
notify({
duration: 3000,
message: (
<div className={styles.notification}>
<CodeBracketIcon className={styles.icon} />
{t('components.common.codebox.copied')}
</div>
),
});
}
};
return (
<div className={styles.root}>
<pre
ref={ref}
className={classNames(styles.content, className)}
tabIndex={0}
dir="ltr"
>
{transformCode(children as ReactElement<PropsWithChildren>, language)}
</pre>
{language && (
<div className={styles.footer}>
<span className={styles.language}>{language}</span>
{showCopyButton && (
<Button kind="neutral" className={styles.action} onClick={onCopy}>
<DocumentDuplicateIcon className={styles.icon} />
{t('components.common.codebox.copy')}
</Button>
)}
</div>
)}
</div>
);
};
export default CodeBox;