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 38const STYLES_INLINE_CODE = css` 39 color: ${Constants.expoColors.gray[900]}; 40 font-family: ${Constants.fontFamilies.mono}; 41 font-size: 0.825em; 42 white-space: pre-wrap; 43 display: inline; 44 padding: 2px 4px; 45 line-height: 170%; 46 max-width: 100%; 47 48 word-wrap: break-word; 49 background-color: ${Constants.expoColors.gray[100]}; 50 border: 1px solid ${Constants.expoColors.gray[250]}; 51 border-radius: 4px; 52 vertical-align: middle; 53 overflow-x: scroll; 54`; 55 56const STYLES_CODE_CONTAINER = css` 57 border: 1px solid ${Constants.expoColors.gray[250]}; 58 padding: 16px; 59 margin: 16px 0; 60 white-space: pre; 61 overflow: auto; 62 -webkit-overflow-scrolling: touch; 63 background-color: ${Constants.expoColors.gray[100]}; 64 line-height: 120%; 65 border-radius: 4px; 66`; 67 68type Props = { 69 className?: string; 70}; 71 72export class Code extends React.Component<Props> { 73 componentDidMount() { 74 this.runTippy(); 75 } 76 77 componentDidUpdate() { 78 this.runTippy(); 79 } 80 81 private runTippy() { 82 if (process.browser) { 83 global.tippy('.code-annotation', { 84 theme: 'expo', 85 placement: 'top', 86 arrow: true, 87 arrowType: 'round', 88 interactive: true, 89 distance: 20, 90 }); 91 } 92 } 93 94 private escapeHtml(text: string) { 95 return text.replace(/"/g, '"'); 96 } 97 98 private replaceCommentsWithAnnotations(value: string) { 99 return value 100 .replace(/<span class="token comment">\/\* @info (.*?)\*\/<\/span>\s*/g, (match, content) => { 101 return `<span class="code-annotation" title="${this.escapeHtml(content)}">`; 102 }) 103 .replace(/<span class="token comment">\/\* @end \*\/<\/span>(\n *)?/g, '</span>'); 104 } 105 106 render() { 107 let html = this.props.children?.toString() || ''; 108 // mdx will add the class `language-foo` to codeblocks with the tag `foo` 109 // if this class is present, we want to slice out `language-` 110 let lang = this.props.className && this.props.className.slice(9).toLowerCase(); 111 112 // Allow for code blocks without a language. 113 if (lang) { 114 // sh isn't supported, use Use sh to match js, and ts 115 if (lang in remapLanguages) { 116 lang = remapLanguages[lang]; 117 } 118 119 const grammar = Prism.languages[lang as keyof typeof Prism.languages]; 120 if (!grammar) { 121 throw new Error(`docs currently do not support language: ${lang}`); 122 } 123 124 html = Prism.highlight(html, grammar, lang as Language); 125 html = this.replaceCommentsWithAnnotations(html); 126 } 127 128 // Remove leading newline if it exists (because inside <pre> all whitespace is dislayed as is by the browser, and 129 // sometimes, Prism adds a newline before the code) 130 if (html.startsWith('\n')) { 131 html = html.replace('\n', ''); 132 } 133 134 return ( 135 <pre css={STYLES_CODE_CONTAINER} {...attributes}> 136 <code css={STYLES_CODE_BLOCK} dangerouslySetInnerHTML={{ __html: html }} /> 137 </pre> 138 ); 139 } 140} 141 142const remapLanguages: Record<string, string> = { 143 'objective-c': 'objc', 144 sh: 'bash', 145 rb: 'ruby', 146}; 147 148export const InlineCode: React.FC = ({ children }) => ( 149 <code css={STYLES_INLINE_CODE} className="inline"> 150 {children} 151 </code> 152); 153