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