15b6cd93dSBartosz Kaszubowskiimport { css } from '@emotion/react'; 2f4b1168bSBartosz Kaszubowskiimport { theme, typography } from '@expo/styleguide'; 3f4b1168bSBartosz Kaszubowskiimport { borderRadius, spacing } from '@expo/styleguide-base'; 4*46f023faSEvan Baconimport { FileCode01Icon, LayoutAlt01Icon, Server03Icon } from '@expo/styleguide-icons'; 5586106d6SBartłomiej Klocekimport { Language, Prism } from 'prism-react-renderer'; 6586106d6SBartłomiej Klocekimport * as React from 'react'; 7d87c9a81SAxel Delafosseimport tippy, { roundArrow } from 'tippy.js'; 8586106d6SBartłomiej Klocek 9586106d6SBartłomiej Klocekimport { installLanguages } from './languages'; 10586106d6SBartłomiej Klocek 1195548fffSBartosz Kaszubowskiimport { Snippet } from '~/ui/components/Snippet/Snippet'; 1295548fffSBartosz Kaszubowskiimport { SnippetContent } from '~/ui/components/Snippet/SnippetContent'; 1395548fffSBartosz Kaszubowskiimport { SnippetHeader } from '~/ui/components/Snippet/SnippetHeader'; 1495548fffSBartosz Kaszubowskiimport { CopyAction } from '~/ui/components/Snippet/actions/CopyAction'; 1514c78e61SJon Sampimport { CODE } from '~/ui/components/Text'; 1695548fffSBartosz Kaszubowski 173f609562SBartosz Kaszubowski// @ts-ignore Jest ESM issue https://github.com/facebook/jest/issues/9430 183f609562SBartosz Kaszubowskiconst { default: testTippy } = tippy; 193f609562SBartosz Kaszubowski 20586106d6SBartłomiej KlocekinstallLanguages(Prism); 21586106d6SBartłomiej Klocek 22586106d6SBartłomiej Klocekconst attributes = { 23586106d6SBartłomiej Klocek 'data-text': true, 24586106d6SBartłomiej Klocek}; 25586106d6SBartłomiej Klocek 26586106d6SBartłomiej Klocekconst STYLES_CODE_BLOCK = css` 27d7910c52SBartosz Kaszubowski ${typography.body.code}; 288bf05203SJon Samp color: ${theme.text.default}; 29586106d6SBartłomiej Klocek white-space: inherit; 30f8204ef0SBartosz Kaszubowski padding: 0; 31f8204ef0SBartosz Kaszubowski margin: 0; 32586106d6SBartłomiej Klocek 33586106d6SBartłomiej Klocek .code-annotation { 34586106d6SBartłomiej Klocek transition: 200ms ease all; 35586106d6SBartłomiej Klocek transition-property: text-shadow, opacity; 3614c78e61SJon Samp text-shadow: ${theme.palette.yellow7} 0 0 10px, ${theme.palette.yellow7} 0 0 10px, 3714c78e61SJon Samp ${theme.palette.yellow7} 0 0 10px, ${theme.palette.yellow7} 0 0 10px; 38586106d6SBartłomiej Klocek } 39586106d6SBartłomiej Klocek 404e0d47adSKim Brandwijk .code-annotation.with-tooltip:hover { 41586106d6SBartłomiej Klocek cursor: pointer; 42586106d6SBartłomiej Klocek animation: none; 43586106d6SBartłomiej Klocek opacity: 0.8; 44586106d6SBartłomiej Klocek } 4599e845b3SHein Rutjes 4699e845b3SHein Rutjes .code-hidden { 4799e845b3SHein Rutjes display: none; 4899e845b3SHein Rutjes } 4999e845b3SHein Rutjes 5099e845b3SHein Rutjes .code-placeholder { 5199e845b3SHein Rutjes opacity: 0.5; 5299e845b3SHein Rutjes } 53586106d6SBartłomiej Klocek`; 54586106d6SBartłomiej Klocek 5595548fffSBartosz Kaszubowskiconst STYLES_CODE_CONTAINER_BLOCK = css` 56b56e9353SBartosz Kaszubowski border: 1px solid ${theme.border.secondary}; 57586106d6SBartłomiej Klocek padding: 16px; 58586106d6SBartłomiej Klocek margin: 16px 0; 5914c78e61SJon Samp background-color: ${theme.background.subtle}; 6095548fffSBartosz Kaszubowski`; 6195548fffSBartosz Kaszubowski 6295548fffSBartosz Kaszubowskiconst STYLES_CODE_CONTAINER = css` 63586106d6SBartłomiej Klocek white-space: pre; 64586106d6SBartłomiej Klocek overflow: auto; 65586106d6SBartłomiej Klocek -webkit-overflow-scrolling: touch; 66586106d6SBartłomiej Klocek line-height: 120%; 6714c78e61SJon Samp border-radius: ${borderRadius.sm}px; 6836e3c417SBartosz Kaszubowski padding: ${spacing[4]}px; 69d9bd5b6cSBartosz Kaszubowski 70d9bd5b6cSBartosz Kaszubowski table &:last-child { 71d9bd5b6cSBartosz Kaszubowski margin-bottom: 0; 72d9bd5b6cSBartosz Kaszubowski } 73586106d6SBartłomiej Klocek`; 74586106d6SBartłomiej Klocek 75586106d6SBartłomiej Klocektype Props = { 76586106d6SBartłomiej Klocek className?: string; 77586106d6SBartłomiej Klocek}; 78586106d6SBartłomiej Klocek 7946dc8e76SBartosz Kaszubowskiexport function cleanCopyValue(value: string) { 8046dc8e76SBartosz Kaszubowski return value 8146dc8e76SBartosz Kaszubowski .replace(/\/\*\s?@(info[^*]+|end|hide[^*]+).?\*\//g, '') 8246dc8e76SBartosz Kaszubowski .replace(/#\s?@(info[^#]+|end|hide[^#]+).?#/g, '') 8346dc8e76SBartosz Kaszubowski .replace(/<!--\s?@(info[^<>]+|end|hide[^<>]+).?-->/g, '') 8446dc8e76SBartosz Kaszubowski .replace(/^ +\r?\n|\n +\r?$/gm, ''); 8546dc8e76SBartosz Kaszubowski} 8646dc8e76SBartosz Kaszubowski 87b8b69ebeSBartosz Kaszubowskiexport class Code extends React.Component<React.PropsWithChildren<Props>> { 88586106d6SBartłomiej Klocek componentDidMount() { 89586106d6SBartłomiej Klocek this.runTippy(); 90586106d6SBartłomiej Klocek } 91586106d6SBartłomiej Klocek 92586106d6SBartłomiej Klocek componentDidUpdate() { 93586106d6SBartłomiej Klocek this.runTippy(); 94586106d6SBartłomiej Klocek } 95586106d6SBartłomiej Klocek 96586106d6SBartłomiej Klocek private runTippy() { 973f609562SBartosz Kaszubowski const tippyFunc = testTippy || tippy; 983f609562SBartosz Kaszubowski tippyFunc('.code-annotation.with-tooltip', { 99d87c9a81SAxel Delafosse allowHTML: true, 100586106d6SBartłomiej Klocek theme: 'expo', 101586106d6SBartłomiej Klocek placement: 'top', 102d87c9a81SAxel Delafosse arrow: roundArrow, 103586106d6SBartłomiej Klocek interactive: true, 104d87c9a81SAxel Delafosse offset: [0, 20], 105d87c9a81SAxel Delafosse appendTo: document.body, 106586106d6SBartłomiej Klocek }); 107586106d6SBartłomiej Klocek } 108586106d6SBartłomiej Klocek 109586106d6SBartłomiej Klocek private escapeHtml(text: string) { 110586106d6SBartłomiej Klocek return text.replace(/"/g, '"'); 111586106d6SBartłomiej Klocek } 112586106d6SBartłomiej Klocek 113af3546daSEvan Bacon private replaceXmlCommentsWithAnnotations(value: string) { 114af3546daSEvan Bacon return value 115af3546daSEvan Bacon .replace( 116af3546daSEvan Bacon /<span class="token comment"><!-- @info (.*?)--><\/span>\s*/g, 117af3546daSEvan Bacon (match, content) => { 1184e0d47adSKim Brandwijk return content 1194e0d47adSKim Brandwijk ? `<span class="code-annotation with-tooltip" data-tippy-content="${this.escapeHtml( 1204e0d47adSKim Brandwijk content 1214e0d47adSKim Brandwijk )}">` 1224e0d47adSKim Brandwijk : '<span class="code-annotation">'; 123af3546daSEvan Bacon } 124af3546daSEvan Bacon ) 125af3546daSEvan Bacon .replace( 126af3546daSEvan Bacon /<span class="token comment"><!-- @hide (.*?)--><\/span>\s*/g, 127af3546daSEvan Bacon (match, content) => { 128af3546daSEvan Bacon return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 129af3546daSEvan Bacon content 130af3546daSEvan Bacon )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 131af3546daSEvan Bacon } 132af3546daSEvan Bacon ) 133b4195976SBartosz Kaszubowski .replace(/\s*<span class="token comment"><!-- @end --><\/span>/g, '</span>'); 134af3546daSEvan Bacon } 135af3546daSEvan Bacon 136af3546daSEvan Bacon private replaceHashCommentsWithAnnotations(value: string) { 137af3546daSEvan Bacon return value 138af3546daSEvan Bacon .replace(/<span class="token comment"># @info (.*?)#<\/span>\s*/g, (match, content) => { 1394e0d47adSKim Brandwijk return content 1404e0d47adSKim Brandwijk ? `<span class="code-annotation with-tooltip" data-tippy-content="${this.escapeHtml( 1414e0d47adSKim Brandwijk content 1424e0d47adSKim Brandwijk )}">` 1434e0d47adSKim Brandwijk : '<span class="code-annotation">'; 144af3546daSEvan Bacon }) 145af3546daSEvan Bacon .replace(/<span class="token comment"># @hide (.*?)#<\/span>\s*/g, (match, content) => { 146af3546daSEvan Bacon return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 147af3546daSEvan Bacon content 148af3546daSEvan Bacon )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 149af3546daSEvan Bacon }) 150b4195976SBartosz Kaszubowski .replace(/\s*<span class="token comment"># @end #<\/span>/g, '</span>'); 151af3546daSEvan Bacon } 152af3546daSEvan Bacon 153af3546daSEvan Bacon private replaceSlashCommentsWithAnnotations(value: string) { 154586106d6SBartłomiej Klocek return value 155586106d6SBartłomiej Klocek .replace(/<span class="token comment">\/\* @info (.*?)\*\/<\/span>\s*/g, (match, content) => { 1564e0d47adSKim Brandwijk return content 1574e0d47adSKim Brandwijk ? `<span class="code-annotation with-tooltip" data-tippy-content="${this.escapeHtml( 1584e0d47adSKim Brandwijk content 1594e0d47adSKim Brandwijk )}">` 1604e0d47adSKim Brandwijk : '<span class="code-annotation">'; 161586106d6SBartłomiej Klocek }) 16299e845b3SHein Rutjes .replace(/<span class="token comment">\/\* @hide (.*?)\*\/<\/span>\s*/g, (match, content) => { 16399e845b3SHein Rutjes return `<span><span class="code-hidden">%%placeholder-start%%</span><span class="code-placeholder">${this.escapeHtml( 16499e845b3SHein Rutjes content 16599e845b3SHein Rutjes )}</span><span class="code-hidden">%%placeholder-end%%</span><span class="code-hidden">`; 16699e845b3SHein Rutjes }) 167b4195976SBartosz Kaszubowski .replace(/\s*<span class="token comment">\/\* @end \*\/<\/span>/g, '</span>'); 168586106d6SBartłomiej Klocek } 169586106d6SBartłomiej Klocek 17095548fffSBartosz Kaszubowski private parseValue(value: string) { 17195548fffSBartosz Kaszubowski if (value.startsWith('@@@')) { 17295548fffSBartosz Kaszubowski const valueChunks = value.split('@@@'); 17395548fffSBartosz Kaszubowski return { 17495548fffSBartosz Kaszubowski title: valueChunks[1], 17595548fffSBartosz Kaszubowski value: valueChunks[2], 17695548fffSBartosz Kaszubowski }; 17795548fffSBartosz Kaszubowski } 17895548fffSBartosz Kaszubowski return { 17995548fffSBartosz Kaszubowski value, 18095548fffSBartosz Kaszubowski }; 18195548fffSBartosz Kaszubowski } 18295548fffSBartosz Kaszubowski 183586106d6SBartłomiej Klocek render() { 1843f609562SBartosz Kaszubowski // note(simek): MDX dropped `inlineCode` pseudo-tag, and we need to relay on `pre` and `code` now, 1853f609562SBartosz Kaszubowski // which results in this nesting mess, we should fix it in the future 1863c9a6b96SBartosz Kaszubowski const child = 1873c9a6b96SBartosz Kaszubowski this.props.className && this.props.className.startsWith('language') 1883f609562SBartosz Kaszubowski ? this 1893f609562SBartosz Kaszubowski : (React.Children.toArray(this.props.children)[0] as JSX.Element); 19095548fffSBartosz Kaszubowski 19195548fffSBartosz Kaszubowski const value = this.parseValue(child?.props?.children?.toString() || ''); 19295548fffSBartosz Kaszubowski let html = value.value; 1933f609562SBartosz Kaszubowski 194586106d6SBartłomiej Klocek // mdx will add the class `language-foo` to codeblocks with the tag `foo` 195586106d6SBartłomiej Klocek // if this class is present, we want to slice out `language-` 1963f609562SBartosz Kaszubowski let lang = child.props.className && child.props.className.slice(9).toLowerCase(); 197586106d6SBartłomiej Klocek 198586106d6SBartłomiej Klocek // Allow for code blocks without a language. 199586106d6SBartłomiej Klocek if (lang) { 200e0277979SBartosz Kaszubowski // sh isn't supported, use sh to match js, and ts 201586106d6SBartłomiej Klocek if (lang in remapLanguages) { 202586106d6SBartłomiej Klocek lang = remapLanguages[lang]; 203586106d6SBartłomiej Klocek } 204586106d6SBartłomiej Klocek 205586106d6SBartłomiej Klocek const grammar = Prism.languages[lang as keyof typeof Prism.languages]; 206586106d6SBartłomiej Klocek if (!grammar) { 207586106d6SBartłomiej Klocek throw new Error(`docs currently do not support language: ${lang}`); 208586106d6SBartłomiej Klocek } 209586106d6SBartłomiej Klocek 210586106d6SBartłomiej Klocek html = Prism.highlight(html, grammar, lang as Language); 211cf46431fSAman Mittal if (['properties', 'ruby', 'bash', 'yaml'].includes(lang)) { 212af3546daSEvan Bacon html = this.replaceHashCommentsWithAnnotations(html); 213af3546daSEvan Bacon } else if (['xml', 'html'].includes(lang)) { 214af3546daSEvan Bacon html = this.replaceXmlCommentsWithAnnotations(html); 215af3546daSEvan Bacon } else { 216af3546daSEvan Bacon html = this.replaceSlashCommentsWithAnnotations(html); 217af3546daSEvan Bacon } 218586106d6SBartłomiej Klocek } 219586106d6SBartłomiej Klocek 22095548fffSBartosz Kaszubowski return value?.title ? ( 22195548fffSBartosz Kaszubowski <Snippet> 222a9f9618cSEvan Bacon <SnippetHeader title={value.title} Icon={getIconForFile(value.title)}> 22346dc8e76SBartosz Kaszubowski <CopyAction text={cleanCopyValue(value.value)} /> 22495548fffSBartosz Kaszubowski </SnippetHeader> 2259e1c7407SBartosz Kaszubowski <SnippetContent className="p-0"> 226586106d6SBartłomiej Klocek <pre css={STYLES_CODE_CONTAINER} {...attributes}> 22795548fffSBartosz Kaszubowski <code 22895548fffSBartosz Kaszubowski css={STYLES_CODE_BLOCK} 22995548fffSBartosz Kaszubowski dangerouslySetInnerHTML={{ __html: html.replace(/^@@@.+@@@/g, '') }} 23095548fffSBartosz Kaszubowski /> 23195548fffSBartosz Kaszubowski </pre> 23295548fffSBartosz Kaszubowski </SnippetContent> 23395548fffSBartosz Kaszubowski </Snippet> 23495548fffSBartosz Kaszubowski ) : ( 23595548fffSBartosz Kaszubowski <pre css={[STYLES_CODE_CONTAINER, STYLES_CODE_CONTAINER_BLOCK]} {...attributes}> 236586106d6SBartłomiej Klocek <code css={STYLES_CODE_BLOCK} dangerouslySetInnerHTML={{ __html: html }} /> 237586106d6SBartłomiej Klocek </pre> 238586106d6SBartłomiej Klocek ); 239586106d6SBartłomiej Klocek } 240586106d6SBartłomiej Klocek} 241586106d6SBartłomiej Klocek 242586106d6SBartłomiej Klocekconst remapLanguages: Record<string, string> = { 243586106d6SBartłomiej Klocek 'objective-c': 'objc', 244586106d6SBartłomiej Klocek sh: 'bash', 245586106d6SBartłomiej Klocek rb: 'ruby', 246586106d6SBartłomiej Klocek}; 247586106d6SBartłomiej Klocek 248f8204ef0SBartosz Kaszubowskiconst codeBlockContainerStyle = { 249f8204ef0SBartosz Kaszubowski margin: 0, 250f8204ef0SBartosz Kaszubowski padding: `3px 6px`, 251f8204ef0SBartosz Kaszubowski}; 252f8204ef0SBartosz Kaszubowski 25307ffa84cSBartosz Kaszubowskiconst codeBlockInlineStyle = { 25407ffa84cSBartosz Kaszubowski padding: 4, 25507ffa84cSBartosz Kaszubowski}; 25607ffa84cSBartosz Kaszubowski 257f8204ef0SBartosz Kaszubowskiconst codeBlockInlineContainerStyle = { 258f8204ef0SBartosz Kaszubowski display: 'inline-flex', 25907ffa84cSBartosz Kaszubowski padding: 0, 260f8204ef0SBartosz Kaszubowski}; 261f8204ef0SBartosz Kaszubowski 262f8204ef0SBartosz Kaszubowskitype CodeBlockProps = React.PropsWithChildren<{ inline?: boolean }>; 263f8204ef0SBartosz Kaszubowski 264f8204ef0SBartosz Kaszubowskiexport const CodeBlock = ({ children, inline = false }: CodeBlockProps) => { 265f8204ef0SBartosz Kaszubowski const Element = inline ? 'span' : 'pre'; 266f8204ef0SBartosz Kaszubowski return ( 267f8204ef0SBartosz Kaszubowski <Element 268f8204ef0SBartosz Kaszubowski css={[ 269f8204ef0SBartosz Kaszubowski STYLES_CODE_CONTAINER, 270f8204ef0SBartosz Kaszubowski codeBlockContainerStyle, 271f8204ef0SBartosz Kaszubowski inline && codeBlockInlineContainerStyle, 272f8204ef0SBartosz Kaszubowski ]} 273f8204ef0SBartosz Kaszubowski {...attributes}> 27407ffa84cSBartosz Kaszubowski <CODE css={[STYLES_CODE_BLOCK, inline && codeBlockInlineStyle, { fontSize: '80%' }]}> 27507ffa84cSBartosz Kaszubowski {children} 27607ffa84cSBartosz Kaszubowski </CODE> 277f8204ef0SBartosz Kaszubowski </Element> 278f8204ef0SBartosz Kaszubowski ); 279f8204ef0SBartosz Kaszubowski}; 280a9f9618cSEvan Bacon 281a9f9618cSEvan Baconfunction getIconForFile(filename: string) { 282a9f9618cSEvan Bacon if (/_layout\.[jt]sx?$/.test(filename)) { 283a9f9618cSEvan Bacon return LayoutAlt01Icon; 284a9f9618cSEvan Bacon } 285*46f023faSEvan Bacon if (/\+api\.[jt]sx?$/.test(filename)) { 286*46f023faSEvan Bacon return Server03Icon; 287*46f023faSEvan Bacon } 288a9f9618cSEvan Bacon return FileCode01Icon; 289a9f9618cSEvan Bacon} 290