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