@@ -18,33 +18,38 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
1818 const [ showCopied , setShowCopied ] = useState ( false ) ;
1919 const codeRef = useRef < HTMLDivElement > ( null ) ;
2020
21- const { copyCodeOnClick} = useCopyCodeCleaner ( codeRef , {
22- cleanDiffMarkers : true ,
23- language,
24- } ) ;
25-
2621 // Show the copy button after js has loaded
2722 // otherwise the copy button will not work
2823 const [ showCopyButton , setShowCopyButton ] = useState ( false ) ;
2924 useEffect ( ( ) => {
3025 setShowCopyButton ( true ) ;
3126 } , [ ] ) ;
3227
33- const handleCopyOnClick = async ( ) => {
34- const success = await copyCodeOnClick ( ) ;
28+ useCleanSnippetInClipboard ( codeRef , { language} ) ;
29+
30+ async function copyCodeOnClick ( ) {
31+ if ( codeRef . current === null ) {
32+ return ;
33+ }
34+
35+ const code = cleanCodeSnippet ( codeRef . current . innerText , { language} ) ;
3536
36- if ( success ) {
37+ try {
38+ await navigator . clipboard . writeText ( code ) ;
3739 setShowCopied ( true ) ;
3840 setTimeout ( ( ) => setShowCopied ( false ) , 1200 ) ;
41+ } catch ( error ) {
42+ // eslint-disable-next-line no-console
43+ console . error ( 'Failed to copy:' , error ) ;
3944 }
40- } ;
45+ }
4146
4247 return (
4348 < div className = { styles [ 'code-block' ] } >
4449 < div className = { styles [ 'code-actions' ] } >
4550 < code className = { styles . filename } > { filename } </ code >
4651 { showCopyButton && (
47- < button className = { styles . copy } onClick = { handleCopyOnClick } >
52+ < button className = { styles . copy } onClick = { copyCodeOnClick } >
4853 < Clipboard size = { 16 } />
4954 </ button >
5055 ) }
@@ -70,40 +75,58 @@ const REGEX = {
7075} ;
7176
7277/**
73- * A custom hook that handles cleaning text when copying from code blocks
78+ * Cleans a code snippet by removing diff markers (+ or -) and bash prompts.
79+ *
80+ * @internal Only exported for testing
81+ */
82+ export function cleanCodeSnippet ( rawCodeSnippet : string , options ?: CleanCopyOptions ) {
83+ const language = options ?. language ;
84+ const cleanDiffMarkers = options ?. cleanDiffMarkers ?? true ;
85+ const cleanBashPrompt = options ?. cleanBashPrompt ?? true ;
86+
87+ let cleanedSnippet = rawCodeSnippet . replace ( REGEX . CONSECUTIVE_NEWLINES , '\n' ) ;
88+
89+ if ( cleanDiffMarkers ) {
90+ cleanedSnippet = cleanedSnippet . replace ( REGEX . DIFF_MARKERS , '' ) ;
91+ }
92+
93+ if ( cleanBashPrompt && ( language === 'bash' || language === 'shell' ) ) {
94+ // Split into lines, clean each line, then rejoin
95+ cleanedSnippet = cleanedSnippet
96+ . split ( '\n' )
97+ . map ( line => {
98+ const match = line . match ( REGEX . BASH_PROMPT ) ;
99+ return match ? line . substring ( match [ 0 ] . length ) : line ;
100+ } )
101+ . filter ( line => line . trim ( ) !== '' ) // Remove empty lines
102+ . join ( '\n' ) ;
103+ }
104+
105+ return cleanedSnippet ;
106+ }
107+
108+ /**
109+ * A custom hook that handles cleaning text when manually copying code to clipboard
110+ *
74111 * @param codeRef - Reference to the code element
75112 * @param options - Configuration options for cleaning
76113 */
77- export function useCopyCodeCleaner (
114+ export function useCleanSnippetInClipboard (
78115 codeRef : RefObject < HTMLElement > ,
79116 options : CleanCopyOptions = { }
80117) {
81118 const { cleanDiffMarkers = true , cleanBashPrompt = true , language = '' } = options ;
82119
83- /**
84- * Effect, which cleans the snippet when the user manually copies it to their clipboard
85- */
86120 useEffect ( ( ) => {
87121 const handleUserCopyEvent = ( event : ClipboardEvent ) => {
88122 if ( ! codeRef . current || ! event . clipboardData ) return ;
89123
90124 const selection = window . getSelection ( ) ?. toString ( ) || '' ;
91125
92126 if ( selection ) {
93- let cleanedText = selection ;
94-
95- if ( cleanDiffMarkers ) {
96- cleanedText = cleanedText . replace ( REGEX . DIFF_MARKERS , '' ) ;
97- }
98-
99- if ( cleanBashPrompt && ( language === 'bash' || language === 'shell' ) ) {
100- const match = cleanedText . match ( REGEX . BASH_PROMPT ) ;
101- if ( match ) {
102- cleanedText = cleanedText . substring ( match [ 0 ] . length ) ;
103- }
104- }
127+ const cleanedSnippet = cleanCodeSnippet ( selection , options ) ;
105128
106- event . clipboardData . setData ( 'text/plain' , cleanedText ) ;
129+ event . clipboardData . setData ( 'text/plain' , cleanedSnippet ) ;
107130 event . preventDefault ( ) ;
108131 }
109132 } ;
@@ -118,40 +141,5 @@ export function useCopyCodeCleaner(
118141 codeElement . removeEventListener ( 'copy' , handleUserCopyEvent as EventListener ) ;
119142 }
120143 } ;
121- } , [ codeRef , cleanDiffMarkers , language , cleanBashPrompt ] ) ;
122-
123- /**
124- * Function for copying code when clicking on "copy code".
125- *
126- * @returns Whether code was copied successfully
127- */
128- const copyCodeOnClick = async ( ) : Promise < boolean > => {
129- if ( codeRef . current === null ) {
130- return false ;
131- }
132-
133- let code = codeRef . current . innerText . replace ( REGEX . CONSECUTIVE_NEWLINES , '\n' ) ;
134-
135- if ( cleanBashPrompt && ( language === 'bash' || language === 'shell' ) ) {
136- const match = code . match ( REGEX . BASH_PROMPT ) ;
137- if ( match ) {
138- code = code . substring ( match [ 0 ] . length ) ;
139- }
140- }
141-
142- if ( cleanDiffMarkers ) {
143- code = code . replace ( REGEX . DIFF_MARKERS , '' ) ;
144- }
145-
146- try {
147- await navigator . clipboard . writeText ( code ) ;
148- return true ;
149- } catch ( error ) {
150- // eslint-disable-next-line no-console
151- console . error ( 'Failed to copy:' , error ) ;
152- return false ;
153- }
154- } ;
155-
156- return { copyCodeOnClick} ;
144+ } , [ codeRef , cleanDiffMarkers , language , cleanBashPrompt , options ] ) ;
157145}
0 commit comments