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