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