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  'E',
57  'EventSubscription',
58  'Manifest',
59  'NativeSyntheticEvent',
60  'Omit',
61  'Pick',
62  'React.FC',
63  'ServiceActionResult',
64  'StyleProp',
65  'T',
66  'TaskOptions',
67  'Uint8Array',
68  'RequestPermissionMethod',
69  'GetPermissionMethod',
70];
71
72const hardcodedTypeLinks: Record<string, string> = {
73  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
74  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
75  Promise:
76    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
77  View: '../../react-native/view',
78  ViewProps: '../../react-native/view#props',
79  ViewStyle: '../../react-native/view-style-props/',
80};
81
82const renderWithLink = (name: string, type?: string) =>
83  nonLinkableTypes.includes(name) ? (
84    name + (type === 'array' ? '[]' : '')
85  ) : (
86    <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}>
87      {name}
88      {type === 'array' && '[]'}
89    </Link>
90  );
91
92export const resolveTypeName = ({
93  elements,
94  elementType,
95  name,
96  type,
97  types,
98  typeArguments,
99  declaration,
100  value,
101  queryType,
102}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
103  if (name) {
104    if (type === 'reference') {
105      if (typeArguments) {
106        if (name === 'Record' || name === 'React.ComponentProps') {
107          return (
108            <>
109              {name}&lt;
110              {typeArguments.map((type, index) => (
111                <span key={`record-type-${index}`}>
112                  {resolveTypeName(type)}
113                  {index !== typeArguments.length - 1 ? ', ' : null}
114                </span>
115              ))}
116              &gt;
117            </>
118          );
119        } else {
120          return (
121            <>
122              {renderWithLink(name)}
123              &lt;
124              {typeArguments.map((type, index) => (
125                <span key={`${name}-nested-type-${index}`}>
126                  {resolveTypeName(type)}
127                  {index !== typeArguments.length - 1 ? ', ' : null}
128                </span>
129              ))}
130              &gt;
131            </>
132          );
133        }
134      } else {
135        return renderWithLink(name);
136      }
137    } else {
138      return name;
139    }
140  } else if (elementType?.name) {
141    if (elementType.type === 'reference') {
142      return renderWithLink(elementType.name, type);
143    } else if (type === 'array') {
144      return elementType.name + '[]';
145    }
146    return elementType.name + type;
147  } else if (type === 'union' && types?.length) {
148    return types.map(resolveTypeName).map((valueToRender, index) => (
149      <span key={`union-type-${index}`}>
150        {valueToRender}
151        {index + 1 !== types.length && ' | '}
152      </span>
153    ));
154  } else if (declaration?.signatures) {
155    const baseSignature = declaration.signatures[0];
156    if (baseSignature?.parameters?.length) {
157      return (
158        <>
159          (
160          {baseSignature.parameters?.map((param, index) => (
161            <span key={`param-${index}-${param.name}`}>
162              {param.name}: {resolveTypeName(param.type)}
163              {index + 1 !== baseSignature.parameters?.length && ', '}
164            </span>
165          ))}
166          ) {'=>'} {resolveTypeName(baseSignature.type)}
167        </>
168      );
169    } else {
170      return (
171        <>
172          {'() =>'} {resolveTypeName(baseSignature.type)}
173        </>
174      );
175    }
176  } else if (type === 'reflection' && declaration?.children) {
177    return (
178      <>
179        {'{ '}
180        {declaration?.children.map((child: PropData, i) => (
181          <span key={`reflection-${name}-${i}`}>
182            {child.name + ': ' + resolveTypeName(child.type)}
183            {i + 1 !== declaration?.children?.length ? ', ' : null}
184          </span>
185        ))}
186        {' }'}
187      </>
188    );
189  } else if (type === 'tuple' && elements) {
190    return (
191      <>
192        [
193        {elements.map((elem, i) => (
194          <span key={`tuple-${name}-${i}`}>
195            {resolveTypeName(elem)}
196            {i + 1 !== elements.length ? ', ' : null}
197          </span>
198        ))}
199        ]
200      </>
201    );
202  } else if (type === 'query' && queryType) {
203    return queryType.name;
204  } else if (type === 'literal' && typeof value === 'boolean') {
205    return `${value}`;
206  } else if (type === 'literal' && value) {
207    return `'${value}'`;
208  } else if (value === null) {
209    return 'null';
210  }
211  return 'undefined';
212};
213
214export const renderParam = ({ comment, name, type }: MethodParamData): JSX.Element => (
215  <LI key={`param-${name}`}>
216    <B>
217      {name} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
218    </B>
219    <CommentTextBlock comment={comment} renderers={mdInlineRenderers} withDash />
220  </LI>
221);
222
223export type CommentTextBlockProps = {
224  comment?: CommentData;
225  renderers?: MDRenderers;
226  withDash?: boolean;
227  beforeContent?: JSX.Element;
228};
229
230export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({
231  comment,
232  renderers = mdRenderers,
233  withDash,
234  beforeContent,
235}) => {
236  const shortText = comment?.shortText?.trim().length ? (
237    <ReactMarkdown renderers={renderers}>{comment.shortText}</ReactMarkdown>
238  ) : null;
239  const text = comment?.text?.trim().length ? (
240    <ReactMarkdown renderers={renderers}>{comment.text}</ReactMarkdown>
241  ) : null;
242
243  const example = comment?.tags?.filter(tag => tag.tag === 'example')[0];
244  const exampleText = example ? (
245    <ReactMarkdown renderers={renderers}>{`__Example:__ ${example.text}`}</ReactMarkdown>
246  ) : null;
247
248  const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0];
249  const deprecationNote = deprecation ? (
250    <Quote key="deprecation-note">
251      {deprecation.text.trim().length ? (
252        <ReactMarkdown renderers={mdInlineRenderers}>{deprecation.text}</ReactMarkdown>
253      ) : (
254        <B>Deprecated</B>
255      )}
256    </Quote>
257  ) : null;
258
259  return (
260    <>
261      {deprecationNote}
262      {beforeContent}
263      {withDash && (shortText || text) ? ' - ' : null}
264      {shortText}
265      {text}
266      {exampleText}
267    </>
268  );
269};
270
271export const STYLES_OPTIONAL = css`
272  color: ${theme.text.secondary};
273  font-size: 90%;
274  padding-top: 22px;
275`;
276
277export const STYLES_SECONDARY = css`
278  color: ${theme.text.secondary};
279  font-size: 90%;
280  font-weight: 600;
281`;
282