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