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 replaceCommentsWithAnnotations(value: string) { 113 return value 114 .replace(/<span class="token comment">\/\* @info (.*?)\*\/<\/span>\s*/g, (match, content) => { 115 return `<span class="code-annotation" data-tippy-content="${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