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