1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import React from 'react';
4import ReactMarkdown from 'react-markdown';
5import remarkGfm from 'remark-gfm';
6
7import { Code, InlineCode } from '~/components/base/code';
8import { H4 } from '~/components/base/headings';
9import Link from '~/components/base/link';
10import { LI, UL } from '~/components/base/list';
11import { B, P, Quote } from '~/components/base/paragraph';
12import {
13  CommentData,
14  MethodParamData,
15  MethodSignatureData,
16  PropData,
17  TypeDefinitionData,
18  TypePropertyDataFlags,
19} from '~/components/plugins/api/APIDataTypes';
20
21export enum TypeDocKind {
22  Enum = 4,
23  Variable = 32,
24  Function = 64,
25  Class = 128,
26  Interface = 256,
27  Property = 1024,
28  TypeAlias = 4194304,
29}
30
31export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components'];
32
33export const mdComponents: MDComponents = {
34  blockquote: ({ children }) => (
35    <Quote>
36      {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */}
37      {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))}
38    </Quote>
39  ),
40  code: ({ children, node }) =>
41    Array.isArray(node.properties?.className) ? (
42      <Code className={node.properties?.className[0].toString() || 'language-unknown'}>
43        {children}
44      </Code>
45    ) : (
46      <InlineCode>{children}</InlineCode>
47    ),
48  h1: ({ children }) => <H4>{children}</H4>,
49  ul: ({ children }) => <UL>{children}</UL>,
50  li: ({ children }) => <LI>{children}</LI>,
51  a: ({ href, children }) => <Link href={href}>{children}</Link>,
52  p: ({ children }) => (children ? <P>{children}</P> : null),
53  strong: ({ children }) => <B>{children}</B>,
54  span: ({ children }) => (children ? <span>{children}</span> : null),
55};
56
57export const mdInlineComponents: MDComponents = {
58  ...mdComponents,
59  p: ({ children }) => (children ? <span>{children}</span> : null),
60};
61
62const nonLinkableTypes = [
63  'ColorValue',
64  'E',
65  'EventSubscription',
66  'File',
67  'FileList',
68  'Manifest',
69  'NativeSyntheticEvent',
70  'Omit',
71  'Pick',
72  'React.FC',
73  'ServiceActionResult',
74  'StyleProp',
75  'T',
76  'TaskOptions',
77  'Uint8Array',
78  // Cross-package permissions management
79  'RequestPermissionMethod',
80  'GetPermissionMethod',
81  'Options',
82  'PermissionHookBehavior',
83];
84
85const hardcodedTypeLinks: Record<string, string> = {
86  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
87  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
88  Promise:
89    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
90  View: '../../react-native/view',
91  ViewProps: '../../react-native/view#props',
92  ViewStyle: '../../react-native/view-style-props/',
93};
94
95const renderWithLink = (name: string, type?: string) =>
96  nonLinkableTypes.includes(name) ? (
97    name + (type === 'array' ? '[]' : '')
98  ) : (
99    <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}>
100      {name}
101      {type === 'array' && '[]'}
102    </Link>
103  );
104
105const renderUnion = (types: TypeDefinitionData[]) =>
106  types.map(resolveTypeName).map((valueToRender, index) => (
107    <span key={`union-type-${index}`}>
108      {valueToRender}
109      {index + 1 !== types.length && ' | '}
110    </span>
111  ));
112
113export const resolveTypeName = ({
114  elements,
115  elementType,
116  name,
117  type,
118  types,
119  typeArguments,
120  declaration,
121  value,
122  queryType,
123}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
124  if (name) {
125    if (type === 'reference') {
126      if (typeArguments) {
127        if (name === 'Record' || name === 'React.ComponentProps') {
128          return (
129            <>
130              {name}&lt;
131              {typeArguments.map((type, index) => (
132                <span key={`record-type-${index}`}>
133                  {resolveTypeName(type)}
134                  {index !== typeArguments.length - 1 ? ', ' : null}
135                </span>
136              ))}
137              &gt;
138            </>
139          );
140        } else {
141          return (
142            <>
143              {renderWithLink(name)}
144              &lt;
145              {typeArguments.map((type, index) => (
146                <span key={`${name}-nested-type-${index}`}>
147                  {resolveTypeName(type)}
148                  {index !== typeArguments.length - 1 ? ', ' : null}
149                </span>
150              ))}
151              &gt;
152            </>
153          );
154        }
155      } else {
156        return renderWithLink(name);
157      }
158    } else {
159      return name;
160    }
161  } else if (elementType?.name) {
162    if (elementType.type === 'reference') {
163      return renderWithLink(elementType.name, type);
164    } else if (type === 'array') {
165      return elementType.name + '[]';
166    }
167    return elementType.name + type;
168  } else if (elementType?.declaration) {
169    if (type === 'array') {
170      const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
171      if (parameters && paramType) {
172        return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
173      }
174    }
175    return elementType.name + type;
176  } else if (type === 'union' && types?.length) {
177    return renderUnion(types);
178  } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
179    const unionTypes = elementType?.types || [];
180    return (
181      <>
182        ({renderUnion(unionTypes)}){type === 'array' && '[]'}
183      </>
184    );
185  } else if (declaration?.signatures) {
186    const baseSignature = declaration.signatures[0];
187    if (baseSignature?.parameters?.length) {
188      return (
189        <>
190          (
191          {baseSignature.parameters?.map((param, index) => (
192            <span key={`param-${index}-${param.name}`}>
193              {param.name}: {resolveTypeName(param.type)}
194              {index + 1 !== baseSignature.parameters?.length && ', '}
195            </span>
196          ))}
197          ) {'=>'} {resolveTypeName(baseSignature.type)}
198        </>
199      );
200    } else {
201      return (
202        <>
203          {'() =>'} {resolveTypeName(baseSignature.type)}
204        </>
205      );
206    }
207  } else if (type === 'reflection' && declaration?.children) {
208    return (
209      <>
210        {'{ '}
211        {declaration?.children.map((child: PropData, i) => (
212          <span key={`reflection-${name}-${i}`}>
213            {child.name + ': ' + resolveTypeName(child.type)}
214            {i + 1 !== declaration?.children?.length ? ', ' : null}
215          </span>
216        ))}
217        {' }'}
218      </>
219    );
220  } else if (type === 'tuple' && elements) {
221    return (
222      <>
223        [
224        {elements.map((elem, i) => (
225          <span key={`tuple-${name}-${i}`}>
226            {resolveTypeName(elem)}
227            {i + 1 !== elements.length ? ', ' : null}
228          </span>
229        ))}
230        ]
231      </>
232    );
233  } else if (type === 'query' && queryType) {
234    return queryType.name;
235  } else if (type === 'literal' && typeof value === 'boolean') {
236    return `${value}`;
237  } else if (type === 'literal' && value) {
238    return `'${value}'`;
239  } else if (value === null) {
240    return 'null';
241  }
242  return 'undefined';
243};
244
245export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
246
247export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
248  <LI key={`param-${name}`}>
249    <B>
250      {parseParamName(name)}
251      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
252    </B>
253    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
254  </LI>
255);
256
257export const listParams = (parameters: MethodParamData[]) =>
258  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
259
260export const renderTypeOrSignatureType = (
261  type?: TypeDefinitionData,
262  signatures?: MethodSignatureData[],
263  includeParamType: boolean = false
264) => {
265  if (type) {
266    return <InlineCode>{resolveTypeName(type)}</InlineCode>;
267  } else if (signatures && signatures.length) {
268    return signatures.map(({ name, type, parameters }) => (
269      <InlineCode key={`signature-type-${name}`}>
270        (
271        {parameters && includeParamType
272          ? parameters.map(param => (
273              <>
274                {param.name}
275                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
276              </>
277            ))
278          : listParams(parameters)}
279        ) =&gt; {resolveTypeName(type)}
280      </InlineCode>
281    ));
282  }
283  return undefined;
284};
285
286export const renderFlags = (flags?: TypePropertyDataFlags) =>
287  flags?.isOptional ? (
288    <>
289      <br />
290      <span css={STYLES_OPTIONAL}>(optional)</span>
291    </>
292  ) : undefined;
293
294export type CommentTextBlockProps = {
295  comment?: CommentData;
296  components?: MDComponents;
297  withDash?: boolean;
298  beforeContent?: JSX.Element;
299};
300
301export const parseCommentContent = (content?: string): string =>
302  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
303
304export const getCommentOrSignatureComment = (
305  comment?: CommentData,
306  signatures?: MethodSignatureData[]
307) => comment || (signatures && signatures[0]?.comment);
308
309export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({
310  comment,
311  components = mdComponents,
312  withDash,
313  beforeContent,
314}) => {
315  const shortText = comment?.shortText?.trim().length ? (
316    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
317      {parseCommentContent(comment.shortText)}
318    </ReactMarkdown>
319  ) : null;
320  const text = comment?.text?.trim().length ? (
321    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
322      {parseCommentContent(comment.text)}
323    </ReactMarkdown>
324  ) : null;
325
326  const example = comment?.tags?.filter(tag => tag.tag === 'example')[0];
327  const exampleText = example ? (
328    <>
329      <br />
330      <br />
331      <ReactMarkdown components={components}>{`__Example:__ ${example.text}`}</ReactMarkdown>
332    </>
333  ) : null;
334
335  const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0];
336  const deprecationNote = deprecation ? (
337    <Quote key="deprecation-note">
338      {deprecation.text.trim().length ? (
339        <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown>
340      ) : (
341        <B>Deprecated</B>
342      )}
343    </Quote>
344  ) : null;
345
346  return (
347    <>
348      {deprecationNote}
349      {beforeContent}
350      {withDash && (shortText || text) ? ' - ' : null}
351      {shortText}
352      {text}
353      {exampleText}
354    </>
355  );
356};
357
358export const STYLES_OPTIONAL = css`
359  color: ${theme.text.secondary};
360  font-size: 90%;
361  padding-top: 22px;
362`;
363
364export const STYLES_SECONDARY = css`
365  color: ${theme.text.secondary};
366  font-size: 90%;
367  font-weight: 600;
368`;
369