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