1import { css } from '@emotion/react'; 2import { SerializedStyles } from '@emotion/serialize'; 3import { theme, typography } from '@expo/styleguide'; 4import { Language, Prism } from 'prism-react-renderer'; 5import * as React from 'react'; 6import tippy, { roundArrow } from 'tippy.js'; 7 8import { installLanguages } from './languages'; 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: ${typography.fontFaces.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: ${typography.fontFaces.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 tippy('.code-annotation', { 97 allowHTML: true, 98 theme: 'expo', 99 placement: 'top', 100 arrow: roundArrow, 101 interactive: true, 102 offset: [0, 20], 103 appendTo: document.body, 104 }); 105 } 106 107 private escapeHtml(text: string) { 108 return text.replace(/"/g, '"'); 109 } 110 111 private replaceXmlCommentsWithAnnotations(value: string) { 112 return value 113 .replace( 114 /<span class="token comment"><!-- @info (.*?)--><\/span>\s*/g, 115 (match, content) => { 116 return `<span class="code-annotation" data-tippy-content="${this.escapeHtml(content)}">`; 117 } 118 ) 119 .replace( 120 /<span class="token comment"><!-- @hide (.*?)--><\/span>\s*/g, 121 (match, content) => { 122 return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 123 content 124 )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 125 } 126 ) 127 .replace(/<span class="token comment"><!-- @end --><\/span>(\n *)?/g, '</span></span>'); 128 } 129 130 private replaceHashCommentsWithAnnotations(value: string) { 131 return value 132 .replace(/<span class="token comment"># @info (.*?)#<\/span>\s*/g, (match, content) => { 133 return `<span class="code-annotation" data-tippy-content="${this.escapeHtml(content)}">`; 134 }) 135 .replace(/<span class="token comment"># @hide (.*?)#<\/span>\s*/g, (match, content) => { 136 return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 137 content 138 )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 139 }) 140 .replace(/<span class="token comment"># @end #<\/span>(\n *)?/g, '</span></span>'); 141 } 142 143 private replaceSlashCommentsWithAnnotations(value: string) { 144 return value 145 .replace(/<span class="token comment">\/\* @info (.*?)\*\/<\/span>\s*/g, (match, content) => { 146 return `<span class="code-annotation" data-tippy-content="${this.escapeHtml(content)}">`; 147 }) 148 .replace(/<span class="token comment">\/\* @hide (.*?)\*\/<\/span>\s*/g, (match, content) => { 149 return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 150 content 151 )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 152 }) 153 .replace(/<span class="token comment">\/\* @end \*\/<\/span>(\n *)?/g, '</span></span>'); 154 } 155 156 render() { 157 let html = this.props.children?.toString() || ''; 158 // mdx will add the class `language-foo` to codeblocks with the tag `foo` 159 // if this class is present, we want to slice out `language-` 160 let lang = this.props.className && this.props.className.slice(9).toLowerCase(); 161 162 // Allow for code blocks without a language. 163 if (lang) { 164 // sh isn't supported, use sh to match js, and ts 165 if (lang in remapLanguages) { 166 lang = remapLanguages[lang]; 167 } 168 169 const grammar = Prism.languages[lang as keyof typeof Prism.languages]; 170 if (!grammar) { 171 throw new Error(`docs currently do not support language: ${lang}`); 172 } 173 174 html = Prism.highlight(html, grammar, lang as Language); 175 if (['properties', 'ruby'].includes(lang)) { 176 html = this.replaceHashCommentsWithAnnotations(html); 177 } else if (['xml', 'html'].includes(lang)) { 178 html = this.replaceXmlCommentsWithAnnotations(html); 179 } else { 180 html = this.replaceSlashCommentsWithAnnotations(html); 181 } 182 } 183 184 // Remove leading newline if it exists (because inside <pre> all whitespace is displayed as is by the browser, and 185 // sometimes, Prism adds a newline before the code) 186 if (html.startsWith('\n')) { 187 html = html.replace('\n', ''); 188 } 189 190 return ( 191 <pre css={STYLES_CODE_CONTAINER} {...attributes}> 192 <code css={STYLES_CODE_BLOCK} dangerouslySetInnerHTML={{ __html: html }} /> 193 </pre> 194 ); 195 } 196} 197 198const remapLanguages: Record<string, string> = { 199 'objective-c': 'objc', 200 sh: 'bash', 201 rb: 'ruby', 202}; 203 204export const InlineCode: React.FC<{ customCss?: SerializedStyles }> = ({ children, customCss }) => ( 205 <code css={[STYLES_INLINE_CODE, customCss]} className="inline"> 206 {children} 207 </code> 208); 209