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