1import { css } from '@emotion/core'; 2import { theme } from '@expo/styleguide'; 3import { Language, Prism } from 'prism-react-renderer'; 4import * as React from 'react'; 5 6import { installLanguages } from './languages'; 7 8import * as Constants from '~/constants/theme'; 9 10installLanguages(Prism); 11 12const attributes = { 13 'data-text': true, 14}; 15 16const STYLES_CODE_BLOCK = css` 17 color: ${theme.text.default}; 18 font-family: ${Constants.fontFamilies.mono}; 19 font-size: 13px; 20 line-height: 20px; 21 white-space: inherit; 22 padding: 0px; 23 margin: 0px; 24 25 .code-annotation { 26 transition: 200ms ease all; 27 transition-property: text-shadow, opacity; 28 text-shadow: ${theme.highlight.emphasis} 0px 0px 10px, ${theme.highlight.emphasis} 0px 0px 10px, 29 ${theme.highlight.emphasis} 0px 0px 10px, ${theme.highlight.emphasis} 0px 0px 10px; 30 } 31 32 .code-annotation:hover { 33 cursor: pointer; 34 animation: none; 35 opacity: 0.8; 36 } 37 38 .code-hidden { 39 display: none; 40 } 41 42 .code-placeholder { 43 opacity: 0.5; 44 } 45`; 46 47const STYLES_INLINE_CODE = css` 48 color: ${theme.text.default}; 49 font-family: ${Constants.fontFamilies.mono}; 50 font-size: 0.825em; 51 white-space: pre-wrap; 52 display: inline; 53 padding: 2px 4px; 54 line-height: 170%; 55 max-width: 100%; 56 57 word-wrap: break-word; 58 background-color: ${theme.background.secondary}; 59 border: 1px solid ${theme.border.default}; 60 border-radius: 4px; 61 vertical-align: middle; 62 overflow-x: auto; 63 64 /* Disable Safari from adding border when used within a (perma)link */ 65 a & { 66 border-color: ${theme.border.default}; 67 } 68`; 69 70const STYLES_CODE_CONTAINER = css` 71 border: 1px solid ${theme.border.default}; 72 padding: 16px; 73 margin: 16px 0; 74 white-space: pre; 75 overflow: auto; 76 -webkit-overflow-scrolling: touch; 77 background-color: ${theme.background.secondary}; 78 line-height: 120%; 79 border-radius: 4px; 80`; 81 82type Props = { 83 className?: string; 84}; 85 86export class Code extends React.Component<Props> { 87 componentDidMount() { 88 this.runTippy(); 89 } 90 91 componentDidUpdate() { 92 this.runTippy(); 93 } 94 95 private runTippy() { 96 if (process.browser) { 97 global.tippy('.code-annotation', { 98 theme: 'expo', 99 placement: 'top', 100 arrow: true, 101 arrowType: 'round', 102 interactive: true, 103 distance: 20, 104 }); 105 } 106 } 107 108 private escapeHtml(text: string) { 109 return text.replace(/"/g, '"'); 110 } 111 112 private replaceCommentsWithAnnotations(value: string) { 113 return value 114 .replace(/<span class="token comment">\/\* @info (.*?)\*\/<\/span>\s*/g, (match, content) => { 115 return `<span class="code-annotation" title="${this.escapeHtml(content)}">`; 116 }) 117 .replace(/<span class="token comment">\/\* @hide (.*?)\*\/<\/span>\s*/g, (match, content) => { 118 return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 119 content 120 )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 121 }) 122 .replace(/<span class="token comment">\/\* @end \*\/<\/span>(\n *)?/g, '</span></span>'); 123 } 124 125 render() { 126 let html = this.props.children?.toString() || ''; 127 // mdx will add the class `language-foo` to codeblocks with the tag `foo` 128 // if this class is present, we want to slice out `language-` 129 let lang = this.props.className && this.props.className.slice(9).toLowerCase(); 130 131 // Allow for code blocks without a language. 132 if (lang) { 133 // sh isn't supported, use Use sh to match js, and ts 134 if (lang in remapLanguages) { 135 lang = remapLanguages[lang]; 136 } 137 138 const grammar = Prism.languages[lang as keyof typeof Prism.languages]; 139 if (!grammar) { 140 throw new Error(`docs currently do not support language: ${lang}`); 141 } 142 143 html = Prism.highlight(html, grammar, lang as Language); 144 html = this.replaceCommentsWithAnnotations(html); 145 } 146 147 // Remove leading newline if it exists (because inside <pre> all whitespace is dislayed as is by the browser, and 148 // sometimes, Prism adds a newline before the code) 149 if (html.startsWith('\n')) { 150 html = html.replace('\n', ''); 151 } 152 153 return ( 154 <pre css={STYLES_CODE_CONTAINER} {...attributes}> 155 <code css={STYLES_CODE_BLOCK} dangerouslySetInnerHTML={{ __html: html }} /> 156 </pre> 157 ); 158 } 159} 160 161const remapLanguages: Record<string, string> = { 162 'objective-c': 'objc', 163 sh: 'bash', 164 rb: 'ruby', 165}; 166 167export const InlineCode: React.FC = ({ children }) => ( 168 <code css={STYLES_INLINE_CODE} className="inline"> 169 {children} 170 </code> 171); 172