xref: /expo/docs/components/base/code.tsx (revision e270611f)
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, '&quot;');
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