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  'ParsedQs',
68  'ServiceActionResult',
69  'T',
70  'TaskOptions',
71  'Uint8Array',
72  // React & React Native
73  'React.FC',
74  'ForwardRefExoticComponent',
75  'StyleProp',
76  // Cross-package permissions management
77  'RequestPermissionMethod',
78  'GetPermissionMethod',
79  'Options',
80  'PermissionHookBehavior',
81];
82
83/**
84 * List of type names that should not be visible in the docs.
85 */
86const omittableTypes = [
87  // Internal React type that adds `ref` prop to the component
88  'RefAttributes',
89];
90
91/**
92 * Map of internal names/type names that should be replaced with something more developer-friendly.
93 */
94const replaceableTypes: Partial<Record<string, string>> = {
95  ForwardRefExoticComponent: 'Component',
96};
97
98const hardcodedTypeLinks: Record<string, string> = {
99  AVPlaybackSource: '../av/#playback-api',
100  AVPlaybackStatus: '../av/#playback-status',
101  AVPlaybackStatusToSet: '../av/#default-initial--avplaybackstatustoset',
102  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
103  Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component',
104  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
105  ExpoConfig: 'https://github.com/expo/expo-cli/blob/main/packages/config-types/src/ExpoConfig.ts',
106  MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent',
107  Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys',
108  Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys',
109  Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype',
110  Promise:
111    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
112  View: '../../react-native/view',
113  ViewProps: '../../react-native/view#props',
114  ViewStyle: '../../react-native/view-style-props/',
115};
116
117const renderWithLink = (name: string, type?: string) => {
118  const replacedName = replaceableTypes[name] ?? name;
119
120  return nonLinkableTypes.includes(replacedName) ? (
121    replacedName + (type === 'array' ? '[]' : '')
122  ) : (
123    <Link
124      href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`}
125      key={`type-link-${replacedName}`}>
126      {replacedName}
127      {type === 'array' && '[]'}
128    </Link>
129  );
130};
131
132const renderUnion = (types: TypeDefinitionData[]) =>
133  types.map(resolveTypeName).map((valueToRender, index) => (
134    <span key={`union-type-${index}`}>
135      {valueToRender}
136      {index + 1 !== types.length && ' | '}
137    </span>
138  ));
139
140export const resolveTypeName = ({
141  elements,
142  elementType,
143  name,
144  type,
145  types,
146  typeArguments,
147  declaration,
148  value,
149  queryType,
150  operator,
151  objectType,
152  indexType,
153}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
154  try {
155    if (name) {
156      if (type === 'reference') {
157        if (typeArguments) {
158          if (name === 'Record' || name === 'React.ComponentProps') {
159            return (
160              <>
161                {name}&lt;
162                {typeArguments.map((type, index) => (
163                  <span key={`record-type-${index}`}>
164                    {resolveTypeName(type)}
165                    {index !== typeArguments.length - 1 ? ', ' : null}
166                  </span>
167                ))}
168                &gt;
169              </>
170            );
171          } else {
172            return (
173              <>
174                {renderWithLink(name)}
175                &lt;
176                {typeArguments.map((type, index) => (
177                  <span key={`${name}-nested-type-${index}`}>
178                    {resolveTypeName(type)}
179                    {index !== typeArguments.length - 1 ? ', ' : null}
180                  </span>
181                ))}
182                &gt;
183              </>
184            );
185          }
186        } else {
187          return renderWithLink(name);
188        }
189      } else {
190        return name;
191      }
192    } else if (elementType?.name) {
193      if (elementType.type === 'reference') {
194        return renderWithLink(elementType.name, type);
195      } else if (type === 'array') {
196        return elementType.name + '[]';
197      }
198      return elementType.name + type;
199    } else if (elementType?.declaration) {
200      if (type === 'array') {
201        const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
202        if (parameters && paramType) {
203          return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
204        }
205      }
206      return elementType.name + type;
207    } else if (type === 'union' && types?.length) {
208      return renderUnion(types);
209    } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
210      const unionTypes = elementType?.types || [];
211      return (
212        <>
213          ({renderUnion(unionTypes)}){type === 'array' && '[]'}
214        </>
215      );
216    } else if (declaration?.signatures) {
217      const baseSignature = declaration.signatures[0];
218      if (baseSignature?.parameters?.length) {
219        return (
220          <>
221            (
222            {baseSignature.parameters?.map((param, index) => (
223              <span key={`param-${index}-${param.name}`}>
224                {param.name}: {resolveTypeName(param.type)}
225                {index + 1 !== baseSignature.parameters?.length && ', '}
226              </span>
227            ))}
228            ) {'=>'} {resolveTypeName(baseSignature.type)}
229          </>
230        );
231      } else {
232        return (
233          <>
234            {'() =>'} {resolveTypeName(baseSignature.type)}
235          </>
236        );
237      }
238    } else if (type === 'reflection' && declaration?.children) {
239      return (
240        <>
241          {'{ '}
242          {declaration?.children.map((child: PropData, i) => (
243            <span key={`reflection-${name}-${i}`}>
244              {child.name + ': ' + resolveTypeName(child.type)}
245              {i + 1 !== declaration?.children?.length ? ', ' : null}
246            </span>
247          ))}
248          {' }'}
249        </>
250      );
251    } else if (type === 'tuple' && elements) {
252      return (
253        <>
254          [
255          {elements.map((elem, i) => (
256            <span key={`tuple-${name}-${i}`}>
257              {resolveTypeName(elem)}
258              {i + 1 !== elements.length ? ', ' : null}
259            </span>
260          ))}
261          ]
262        </>
263      );
264    } else if (type === 'query' && queryType) {
265      return queryType.name;
266    } else if (type === 'literal' && typeof value === 'boolean') {
267      return `${value}`;
268    } else if (type === 'literal' && value) {
269      return `'${value}'`;
270    } else if (type === 'intersection' && types) {
271      return types
272        .filter(({ name }) => !omittableTypes.includes(name ?? ''))
273        .map((value, index, array) => (
274          <span key={`intersection-${name}-${index}`}>
275            {resolveTypeName(value)}
276            {index + 1 !== array.length && ' & '}
277          </span>
278        ));
279    } else if (type === 'indexedAccess') {
280      return `${objectType?.name}['${indexType?.value}']`;
281    } else if (type === 'typeOperator') {
282      return operator || 'undefined';
283    } else if (value === null) {
284      return 'null';
285    }
286    return 'undefined';
287  } catch (e) {
288    console.warn('Type resolve has failed!', e);
289    return 'undefined';
290  }
291};
292
293export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
294
295export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
296  <LI key={`param-${name}`}>
297    <B>
298      {parseParamName(name)}
299      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
300    </B>
301    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
302  </LI>
303);
304
305export const listParams = (parameters: MethodParamData[]) =>
306  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
307
308export const renderTypeOrSignatureType = (
309  type?: TypeDefinitionData,
310  signatures?: MethodSignatureData[],
311  includeParamType: boolean = false
312) => {
313  if (type) {
314    return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>;
315  } else if (signatures && signatures.length) {
316    return signatures.map(({ name, type, parameters }) => (
317      <InlineCode key={`signature-type-${name}`}>
318        (
319        {parameters && includeParamType
320          ? parameters.map(param => (
321              <span key={`signature-param-${param.name}`}>
322                {param.name}
323                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
324              </span>
325            ))
326          : listParams(parameters)}
327        ) =&gt; {resolveTypeName(type)}
328      </InlineCode>
329    ));
330  }
331  return undefined;
332};
333
334export const renderFlags = (flags?: TypePropertyDataFlags) =>
335  flags?.isOptional ? (
336    <>
337      <br />
338      <span css={STYLES_OPTIONAL}>(optional)</span>
339    </>
340  ) : undefined;
341
342export type CommentTextBlockProps = {
343  comment?: CommentData;
344  components?: MDComponents;
345  withDash?: boolean;
346  beforeContent?: JSX.Element;
347  includePlatforms?: boolean;
348};
349
350export const parseCommentContent = (content?: string): string =>
351  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
352
353export const getCommentOrSignatureComment = (
354  comment?: CommentData,
355  signatures?: MethodSignatureData[]
356) => comment || (signatures && signatures[0]?.comment);
357
358export const getTagData = (tagName: string, comment?: CommentData) =>
359  getAllTagData(tagName, comment)?.[0];
360
361export const getAllTagData = (tagName: string, comment?: CommentData) =>
362  comment?.tags?.filter(tag => tag.tag === tagName);
363
364const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
365
366const formatPlatformName = (name: string) => {
367  const cleanName = name.toLowerCase().replace('\n', '');
368  return cleanName.includes('ios')
369    ? cleanName.replace('ios', 'iOS')
370    : cleanName.includes('expo')
371    ? cleanName.replace('expo', 'Expo Go')
372    : capitalize(name);
373};
374
375export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => {
376  const platforms = getAllTagData('platform', comment);
377  return platforms?.length ? (
378    <>
379      {platforms.map(platform => (
380        <div key={platform.text} css={STYLES_PLATFORM}>
381          {formatPlatformName(platform.text)} Only
382        </div>
383      ))}
384      {breakLine && <br />}
385    </>
386  ) : null;
387};
388
389export const CommentTextBlock = ({
390  comment,
391  components = mdComponents,
392  withDash,
393  beforeContent,
394  includePlatforms = true,
395}: CommentTextBlockProps) => {
396  const shortText = comment?.shortText?.trim().length ? (
397    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
398      {parseCommentContent(comment.shortText)}
399    </ReactMarkdown>
400  ) : null;
401  const text = comment?.text?.trim().length ? (
402    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
403      {parseCommentContent(comment.text)}
404    </ReactMarkdown>
405  ) : null;
406
407  const examples = getAllTagData('example', comment);
408  const exampleText = examples?.map((example, index) => (
409    <React.Fragment key={'Example-' + index}>
410      <H4>Example</H4>
411      <ReactMarkdown components={components}>{example.text}</ReactMarkdown>
412    </React.Fragment>
413  ));
414
415  const deprecation = getTagData('deprecated', comment);
416  const deprecationNote = deprecation ? (
417    <Quote key="deprecation-note">
418      {deprecation.text.trim().length ? (
419        <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown>
420      ) : (
421        <B>Deprecated</B>
422      )}
423    </Quote>
424  ) : null;
425
426  const see = getTagData('see', comment);
427  const seeText = see ? (
428    <Quote>
429      <B>See: </B>
430      <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown>
431    </Quote>
432  ) : null;
433
434  return (
435    <>
436      {deprecationNote}
437      {beforeContent}
438      {withDash && (shortText || text) && ' - '}
439      {includePlatforms && getPlatformTags(comment, !withDash)}
440      {shortText}
441      {text}
442      {seeText}
443      {exampleText}
444    </>
445  );
446};
447
448export const getComponentName = (name?: string, children: PropData[] = []) => {
449  if (name && name !== 'default') return name;
450  const ctor = children.filter((child: PropData) => child.name === 'constructor')[0];
451  return ctor?.signatures?.[0]?.type?.name ?? 'default';
452};
453
454export const STYLES_OPTIONAL = css`
455  color: ${theme.text.secondary};
456  font-size: 90%;
457  padding-top: 22px;
458`;
459
460export const STYLES_SECONDARY = css`
461  color: ${theme.text.secondary};
462  font-size: 90%;
463  font-weight: 600;
464`;
465
466export const STYLES_PLATFORM = css`
467  display: inline-block;
468  background-color: ${theme.background.tertiary};
469  color: ${theme.text.default};
470  font-size: 90%;
471  font-weight: 700;
472  padding: 6px 12px;
473  margin-bottom: 8px;
474  margin-right: 8px;
475  border-radius: 4px;
476`;
477