xref: /expo/docs/components/base/code.tsx (revision 46f023fa)
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, '&quot;');
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">&lt;!-- @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">&lt;!-- @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">&lt;!-- @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