1 import React from 'react'
2 import {Box, Text, Button, Octicon, themeGet} from '@primer/react'
3 import {Highlight, themes, Prism} from 'prism-react-renderer'
4 import styled from 'styled-components'
5 import {CheckIcon, CopyIcon} from '@primer/octicons-react'
6 import copyToClipboard from 'copy-to-clipboard'
7 import {announce} from '../util/aria-live'
8 ;(typeof global !== 'undefined' ? global : window).Prism = Prism
9 require('prismjs/components/prism-bash')
10
11 const ClipboardCopy = ({value, ...props}) => {
12 const [copied, setCopied] = React.useState(false)
13
14 React.useEffect(() => {
15 const timeout = setTimeout(() => {
16 if (copied) {
17 setCopied(false)
18 }
19 }, 1000)
20
21 return () => clearTimeout(timeout)
22 }, [copied])
23
24 return (
25 <Button
26 {...props}
27 aria-label="Copy to clipboard"
28 onClick={() => {
29 copyToClipboard(value)
30 setCopied(true)
31 announce(`Copied to clipboard`)
32 }}
33 >
34 <Octicon icon={copied ? CheckIcon : CopyIcon} sx={{color: copied ? 'success.fg' : 'fg.muted'}} />
35 </Button>
36 )
37 }
38
39 export const InlineCode = styled.code`
40 padding: 0.2em 0.4em;
41 font-family: ${themeGet('fonts.mono')};
42 font-size: 85%;
43 background-color: ${themeGet('colors.neutral.muted')};
44 border-radius: ${themeGet('radii.2')};
45 `
46
47 const MonoText = props => <Text sx={{fontFamily: 'mono', fontSize: 1}} {...props} />
48
49 const CodeBlock = ({children, code, className, style}) => (
50 <Box
51 sx={{
52 // Make <pre> adjust to the width of the container
53 // https://stackoverflow.com/a/14406386
54 display: 'table',
55 tableLayout: 'fixed',
56 width: '100%',
57 mb: 3,
58 }}
59 >
60 <Box
61 style={style}
62 sx={{
63 ...(code ? {display: 'flex', justifyContent: 'space-between', flexDirection: 'row-reverse'} : {}),
64 borderRadius: 2,
65 borderStyle: 'solid',
66 borderWidth: 1,
67 borderColor: 'border.muted',
68 }}
69 >
70 {code ? (
71 <ClipboardCopy
72 value={code}
73 sx={{
74 borderRadius: 0,
75 borderStyle: 'solid',
76 borderWidth: 1,
77 borderColor: 'border.muted',
78 marginTop: '-1px',
79 marginRight: '-1px',
80 borderTopRightRadius: 2,
81 borderBottomLeftRadius: 2,
82 }}
83 />
84 ) : null}
85 <Box sx={{m: 0, p: 3, overflowX: 'auto'}}>
86 <Box as="pre" className={className} tabIndex={0} sx={{m: 0}}>
87 {children}
88 </Box>
89 </Box>
90 </Box>
91 </Box>
92 )
93
94 function Code({className = '', prompt, children}) {
95 if (prompt) {
96 return (
97 <CodeBlock style={themes.github.plain}>
98 <MonoText>{children}</MonoText>
99 </CodeBlock>
100 )
101 }
102
103 const code = children.trim()
104 const isBlock = className.startsWith('language-') || code.includes('\n')
105
106 if (!isBlock) {
107 return <InlineCode className={className}>{code}</InlineCode>
108 }
109
110 return (
111 <Highlight code={code} language={className.replace(/language-/, '') || 'bash'} theme={themes.github}>
112 {({className: highlightClassName, style, tokens, getLineProps, getTokenProps}) => (
113 <CodeBlock className={highlightClassName} style={style} code={code}>
114 {tokens.map((line, i) => (
115 <Box key={i} {...getLineProps({line, key: i})}>
116 {line.map((token, key) => (
117 <MonoText
118 key={key}
119 {...{
120 ...getTokenProps({token, key}),
121 style:
122 getTokenProps({token, key}).className === 'token comment'
123 ? {...getTokenProps({token, key}).style, color: '#747458'}
124 : getTokenProps({token, key}).style,
125 }}
126 />
127 ))}
128 </Box>
129 ))}
130 </CodeBlock>
131 )}
132 </Highlight>
133 )
134 }
135
136 export default Code