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