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