1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import React from 'react';
4import ReactMarkdown from 'react-markdown';
5
6import { Code, InlineCode } from '~/components/base/code';
7import { H4 } from '~/components/base/headings';
8import Link from '~/components/base/link';
9import { LI, UL } from '~/components/base/list';
10import { B, P, Quote } from '~/components/base/paragraph';
11import {
12  CommentData,
13  MethodParamData,
14  PropData,
15  TypeDefinitionData,
16} from '~/components/plugins/api/APIDataTypes';
17
18export enum TypeDocKind {
19  Enum = 4,
20  Variable = 32,
21  Function = 64,
22  Class = 128,
23  Interface = 256,
24  Property = 1024,
25  TypeAlias = 4194304,
26}
27
28export type MDRenderers = React.ComponentProps<typeof ReactMarkdown>['renderers'];
29
30export const mdRenderers: MDRenderers = {
31  blockquote: ({ children }) => (
32    <Quote>
33      {React.Children.map(children, child =>
34        child.type.name === 'paragraph' ? child.props.children : child
35      )}
36    </Quote>
37  ),
38  code: ({ value, language }) => <Code className={`language-${language}`}>{value}</Code>,
39  heading: ({ children }) => <H4>{children}</H4>,
40  inlineCode: ({ value }) => <InlineCode>{value}</InlineCode>,
41  list: ({ children }) => <UL>{children}</UL>,
42  listItem: ({ children }) => <LI>{children}</LI>,
43  link: ({ href, children }) => <Link href={href}>{children}</Link>,
44  paragraph: ({ children }) => (children ? <P>{children}</P> : null),
45  strong: ({ children }) => <B>{children}</B>,
46  text: ({ value }) => (value ? <span>{value}</span> : null),
47};
48
49export const mdInlineRenderers: MDRenderers = {
50  ...mdRenderers,
51  paragraph: ({ children }) => (children ? <span>{children}</span> : null),
52};
53
54const nonLinkableTypes = [
55  'ColorValue',
56  'NativeSyntheticEvent',
57  'Omit',
58  'Pick',
59  'React.FC',
60  'ServiceActionResult',
61  'StyleProp',
62  'T',
63  'TaskOptions',
64  'Uint8Array',
65];
66
67const hardcodedTypeLinks: Record<string, string> = {
68  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
69  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
70  Promise:
71    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
72  View: '../../react-native/view',
73  ViewProps: '../../react-native/view#props',
74  ViewStyle: '../../react-native/view-style-props/',
75};
76
77const renderWithLink = (name: string, type?: string) =>
78  nonLinkableTypes.includes(name) ? (
79    name + (type === 'array' ? '[]' : '')
80  ) : (
81    <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}>
82      {name}
83      {type === 'array' && '[]'}
84    </Link>
85  );
86
87export const resolveTypeName = ({
88  elements,
89  elementType,
90  name,
91  type,
92  types,
93  typeArguments,
94  declaration,
95  value,
96  queryType,
97}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
98  if (name) {
99    if (type === 'reference') {
100      if (typeArguments) {
101        if (name === 'Record' || name === 'React.ComponentProps') {
102          return (
103            <>
104              {name}&lt;
105              {typeArguments.map((type, index) => (
106                <span key={`record-type-${index}`}>
107                  {resolveTypeName(type)}
108                  {index !== typeArguments.length - 1 ? ', ' : null}
109                </span>
110              ))}
111              &gt;
112            </>
113          );
114        } else {
115          return (
116            <>
117              {renderWithLink(name)}
118              &lt;
119              {typeArguments.map((type, index) => (
120                <span key={`${name}-nested-type-${index}`}>
121                  {resolveTypeName(type)}
122                  {index !== typeArguments.length - 1 ? ', ' : null}
123                </span>
124              ))}
125              &gt;
126            </>
127          );
128        }
129      } else {
130        return renderWithLink(name);
131      }
132    } else {
133      return name;
134    }
135  } else if (elementType?.name) {
136    if (elementType.type === 'reference') {
137      return renderWithLink(elementType.name, type);
138    } else if (type === 'array') {
139      return elementType.name + '[]';
140    }
141    return elementType.name + type;
142  } else if (type === 'union' && types?.length) {
143    return types.map(resolveTypeName).map((valueToRender, index) => (
144      <span key={`union-type-${index}`}>
145        {valueToRender}
146        {index + 1 !== types.length && ' | '}
147      </span>
148    ));
149  } else if (declaration?.signatures) {
150    const baseSignature = declaration.signatures[0];
151    if (baseSignature?.parameters?.length) {
152      return (
153        <>
154          (
155          {baseSignature.parameters?.map((param, index) => (
156            <span key={`param-${index}-${param.name}`}>
157              {param.name}: {resolveTypeName(param.type)}
158              {index + 1 !== baseSignature.parameters?.length && ', '}
159            </span>
160          ))}
161          ) {'=>'} {resolveTypeName(baseSignature.type)}
162        </>
163      );
164    } else {
165      return (
166        <>
167          {'() =>'} {resolveTypeName(baseSignature.type)}
168        </>
169      );
170    }
171  } else if (type === 'reflection' && declaration?.children) {
172    return (
173      <>
174        {'{ '}
175        {declaration?.children.map((child: PropData, i) => (
176          <span key={`reflection-${name}-${i}`}>
177            {child.name + ': ' + resolveTypeName(child.type)}
178            {i + 1 !== declaration?.children?.length ? ', ' : null}
179          </span>
180        ))}
181        {' }'}
182      </>
183    );
184  } else if (type === 'tuple' && elements) {
185    return (
186      <>
187        [
188        {elements.map((elem, i) => (
189          <span key={`tuple-${name}-${i}`}>
190            {resolveTypeName(elem)}
191            {i + 1 !== elements.length ? ', ' : null}
192          </span>
193        ))}
194        ]
195      </>
196    );
197  } else if (type === 'query' && queryType) {
198    return queryType.name;
199  } else if (type === 'literal' && typeof value === 'boolean') {
200    return `${value}`;
201  } else if (type === 'literal' && value) {
202    return `'${value}'`;
203  } else if (value === null) {
204    return 'null';
205  }
206  return 'undefined';
207};
208
209export const renderParam = ({ comment, name, type }: MethodParamData): JSX.Element => (
210  <LI key={`param-${name}`}>
211    <B>
212      {name} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
213    </B>
214    <CommentTextBlock comment={comment} renderers={mdInlineRenderers} withDash />
215  </LI>
216);
217
218export type CommentTextBlockProps = {
219  comment?: CommentData;
220  renderers?: MDRenderers;
221  withDash?: boolean;
222  beforeContent?: JSX.Element;
223};
224
225export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({
226  comment,
227  renderers = mdRenderers,
228  withDash,
229  beforeContent,
230}) => {
231  const shortText = comment?.shortText?.trim().length ? (
232    <ReactMarkdown renderers={renderers}>{comment.shortText}</ReactMarkdown>
233  ) : null;
234  const text = comment?.text?.trim().length ? (
235    <ReactMarkdown renderers={renderers}>{comment.text}</ReactMarkdown>
236  ) : null;
237
238  const example = comment?.tags?.filter(tag => tag.tag === 'example')[0];
239  const exampleText = example ? (
240    <ReactMarkdown renderers={renderers}>{`__Example:__ ${example.text}`}</ReactMarkdown>
241  ) : null;
242
243  const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0];
244  const deprecationNote = deprecation ? (
245    <Quote key="deprecation-note">
246      {deprecation.text.trim().length ? (
247        <ReactMarkdown renderers={renderers}>{deprecation.text}</ReactMarkdown>
248      ) : (
249        <B>Deprecated</B>
250      )}
251    </Quote>
252  ) : null;
253
254  return (
255    <>
256      {deprecationNote}
257      {beforeContent}
258      {withDash && (shortText || text) ? ' - ' : null}
259      {shortText}
260      {text}
261      {exampleText}
262    </>
263  );
264};
265
266export const STYLES_OPTIONAL = css`
267  color: ${theme.text.secondary};
268  font-size: 90%;
269  padding-top: 22px;
270`;
271
272export const STYLES_SECONDARY = css`
273  color: ${theme.text.secondary};
274  font-size: 90%;
275  font-weight: 600;
276`;
277