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