1import { css } from '@emotion/react';
2import { borderRadius, shadows, spacing, theme, typography } 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, OL } 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';
20import { APISectionPlatformTags } from '~/components/plugins/api/APISectionPlatformTags';
21import * as Constants from '~/constants/theme';
22import { Cell, HeaderCell, Row, Table, TableHead } from '~/ui/components/Table';
23import { tableWrapperStyle } from '~/ui/components/Table/Table';
24import { A } from '~/ui/components/Text';
25
26const isDev = process.env.NODE_ENV === 'development';
27
28export enum TypeDocKind {
29  LegacyEnum = 4,
30  Enum = 8,
31  Variable = 32,
32  Function = 64,
33  Class = 128,
34  Interface = 256,
35  Property = 1024,
36  Method = 2048,
37  Parameter = 32768,
38  TypeAlias = 4194304,
39}
40
41export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components'];
42
43const getInvalidLinkMessage = (href: string) =>
44  `Using "../" when linking other packages in doc comments produce a broken link! Please use "./" instead. Problematic link:\n\t${href}`;
45
46export const mdComponents: MDComponents = {
47  blockquote: ({ children }) => (
48    <Quote>
49      {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */}
50      {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))}
51    </Quote>
52  ),
53  code: ({ children, className }) =>
54    className ? <Code className={className}>{children}</Code> : <InlineCode>{children}</InlineCode>,
55  h1: ({ children }) => <H4>{children}</H4>,
56  ul: ({ children }) => <UL>{children}</UL>,
57  ol: ({ children }) => <OL>{children}</OL>,
58  li: ({ children }) => <LI>{children}</LI>,
59  a: ({ href, children }) => {
60    if (
61      href?.startsWith('../') &&
62      !href?.startsWith('../..') &&
63      !href?.startsWith('../react-native')
64    ) {
65      if (isDev) {
66        throw new Error(getInvalidLinkMessage(href));
67      } else {
68        console.warn(getInvalidLinkMessage(href));
69      }
70    }
71    return <Link href={href}>{children}</Link>;
72  },
73  p: ({ children }) => (children ? <P>{children}</P> : null),
74  strong: ({ children }) => <B>{children}</B>,
75  span: ({ children }) => (children ? <span>{children}</span> : null),
76  table: ({ children }) => <Table>{children}</Table>,
77  thead: ({ children }) => <TableHead>{children}</TableHead>,
78  tr: ({ children }) => <Row>{children}</Row>,
79  th: ({ children }) => <HeaderCell>{children}</HeaderCell>,
80  td: ({ children }) => <Cell>{children}</Cell>,
81};
82
83export const mdInlineComponents: MDComponents = {
84  ...mdComponents,
85  p: ({ children }) => (children ? <span>{children}</span> : null),
86};
87
88const nonLinkableTypes = [
89  'ColorValue',
90  'Component',
91  'ComponentClass',
92  'E',
93  'EventSubscription',
94  'File',
95  'FileList',
96  'NativeSyntheticEvent',
97  'ParsedQs',
98  'ServiceActionResult',
99  'T',
100  'TaskOptions',
101  'Uint8Array',
102  // React & React Native
103  'React.FC',
104  'ForwardRefExoticComponent',
105  'StyleProp',
106  // Cross-package permissions management
107  'RequestPermissionMethod',
108  'GetPermissionMethod',
109  'Options',
110  'PermissionHookBehavior',
111];
112
113/**
114 * List of type names that should not be visible in the docs.
115 */
116const omittableTypes = [
117  // Internal React type that adds `ref` prop to the component
118  'RefAttributes',
119];
120
121/**
122 * Map of internal names/type names that should be replaced with something more developer-friendly.
123 */
124const replaceableTypes: Partial<Record<string, string>> = {
125  ForwardRefExoticComponent: 'Component',
126};
127
128const hardcodedTypeLinks: Record<string, string> = {
129  AVPlaybackSource: '/versions/latest/sdk/av/#avplaybacksource',
130  AVPlaybackStatus: '/versions/latest/sdk/av/#avplaybackstatus',
131  AVPlaybackStatusToSet: '/versions/latest/sdk/av/#avplaybackstatustoset',
132  Blob: 'https://developer.mozilla.org/en-US/docs/Web/API/Blob',
133  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
134  Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component',
135  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
136  ExpoConfig:
137    'https://github.com/expo/expo/blob/main/packages/%40expo/config-types/src/ExpoConfig.ts',
138  MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent',
139  Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys',
140  Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys',
141  Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype',
142  Promise:
143    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
144  View: '/versions/latest/react-native/view',
145  ViewProps: '/versions/latest/react-native/view#props',
146  ViewStyle: '/versions/latest/react-native/view-style-props',
147  WebGL2RenderingContext: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext',
148  WebGLFramebuffer: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGLFramebuffer',
149};
150
151const renderWithLink = (name: string, type?: string) => {
152  const replacedName = replaceableTypes[name] ?? name;
153
154  return nonLinkableTypes.includes(replacedName) ? (
155    replacedName + (type === 'array' ? '[]' : '')
156  ) : (
157    <Link
158      href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`}
159      key={`type-link-${replacedName}`}>
160      {replacedName}
161      {type === 'array' && '[]'}
162    </Link>
163  );
164};
165
166const renderUnion = (types: TypeDefinitionData[]) =>
167  types
168    .map(type => resolveTypeName(type))
169    .map((valueToRender, index) => (
170      <span key={`union-type-${index}`}>
171        {valueToRender}
172        {index + 1 !== types.length && ' | '}
173      </span>
174    ));
175
176export const resolveTypeName = (
177  typeDefinition: TypeDefinitionData
178): string | JSX.Element | (string | JSX.Element)[] => {
179  if (!typeDefinition) {
180    return 'undefined';
181  }
182
183  const {
184    elements,
185    elementType,
186    name,
187    type,
188    types,
189    typeArguments,
190    declaration,
191    value,
192    queryType,
193    operator,
194    objectType,
195    indexType,
196  } = typeDefinition;
197
198  try {
199    if (name) {
200      if (type === 'reference') {
201        if (typeArguments) {
202          if (name === 'Record' || name === 'React.ComponentProps') {
203            return (
204              <>
205                {name}&lt;
206                {typeArguments.map((type, index) => (
207                  <span key={`record-type-${index}`}>
208                    {resolveTypeName(type)}
209                    {index !== typeArguments.length - 1 ? ', ' : null}
210                  </span>
211                ))}
212                &gt;
213              </>
214            );
215          } else {
216            return (
217              <>
218                {renderWithLink(name)}
219                &lt;
220                {typeArguments.map((type, index) => (
221                  <span key={`${name}-nested-type-${index}`}>
222                    {resolveTypeName(type)}
223                    {index !== typeArguments.length - 1 ? ', ' : null}
224                  </span>
225                ))}
226                &gt;
227              </>
228            );
229          }
230        } else {
231          return renderWithLink(name);
232        }
233      } else {
234        return name;
235      }
236    } else if (elementType?.name) {
237      if (elementType.type === 'reference') {
238        return renderWithLink(elementType.name, type);
239      } else if (type === 'array') {
240        return elementType.name + '[]';
241      }
242      return elementType.name + type;
243    } else if (elementType?.declaration) {
244      if (type === 'array') {
245        const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
246        if (parameters && paramType) {
247          return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
248        }
249      }
250      return elementType.name + type;
251    } else if (type === 'union' && types?.length) {
252      return renderUnion(types);
253    } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
254      const unionTypes = elementType?.types || [];
255      return (
256        <>
257          ({renderUnion(unionTypes)}){type === 'array' && '[]'}
258        </>
259      );
260    } else if (declaration?.signatures) {
261      const baseSignature = declaration.signatures[0];
262      if (baseSignature?.parameters?.length) {
263        return (
264          <>
265            (
266            {baseSignature.parameters?.map((param, index) => (
267              <span key={`param-${index}-${param.name}`}>
268                {param.name}: {resolveTypeName(param.type)}
269                {index + 1 !== baseSignature.parameters?.length && ', '}
270              </span>
271            ))}
272            ) {'=>'} {resolveTypeName(baseSignature.type)}
273          </>
274        );
275      } else {
276        return (
277          <>
278            {'() =>'} {resolveTypeName(baseSignature.type)}
279          </>
280        );
281      }
282    } else if (type === 'reflection' && declaration?.children) {
283      return (
284        <>
285          {'{ '}
286          {declaration?.children.map((child: PropData, i) => (
287            <span key={`reflection-${name}-${i}`}>
288              {child.name + ': '}
289              {resolveTypeName(child.type)}
290              {i + 1 !== declaration?.children?.length ? ', ' : null}
291            </span>
292          ))}
293          {' }'}
294        </>
295      );
296    } else if (type === 'tuple' && elements) {
297      return (
298        <>
299          [
300          {elements.map((elem, i) => (
301            <span key={`tuple-${name}-${i}`}>
302              {resolveTypeName(elem)}
303              {i + 1 !== elements.length ? ', ' : null}
304            </span>
305          ))}
306          ]
307        </>
308      );
309    } else if (type === 'query' && queryType) {
310      return queryType.name;
311    } else if (type === 'literal' && typeof value === 'boolean') {
312      return `${value}`;
313    } else if (type === 'literal' && value) {
314      return `'${value}'`;
315    } else if (type === 'intersection' && types) {
316      return types
317        .filter(({ name }) => !omittableTypes.includes(name ?? ''))
318        .map((value, index, array) => (
319          <span key={`intersection-${name}-${index}`}>
320            {resolveTypeName(value)}
321            {index + 1 !== array.length && ' & '}
322          </span>
323        ));
324    } else if (type === 'indexedAccess') {
325      return `${objectType?.name}['${indexType?.value}']`;
326    } else if (type === 'typeOperator') {
327      return operator || 'undefined';
328    } else if (value === null) {
329      return 'null';
330    }
331    return 'undefined';
332  } catch (e) {
333    console.warn('Type resolve has failed!', e);
334    return 'undefined';
335  }
336};
337
338export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
339
340export const renderParamRow = ({
341  comment,
342  name,
343  type,
344  flags,
345  defaultValue,
346}: MethodParamData): JSX.Element => {
347  const initValue = parseCommentContent(defaultValue || getTagData('default', comment)?.text);
348  return (
349    <Row key={`param-${name}`}>
350      <Cell>
351        <B>{parseParamName(name)}</B>
352        {renderFlags(flags, initValue)}
353      </Cell>
354      <Cell>
355        <InlineCode>{resolveTypeName(type)}</InlineCode>
356      </Cell>
357      <Cell>
358        <CommentTextBlock
359          comment={comment}
360          components={mdInlineComponents}
361          afterContent={renderDefaultValue(initValue)}
362          emptyCommentFallback="-"
363        />
364      </Cell>
365    </Row>
366  );
367};
368
369export const renderTableHeadRow = () => (
370  <TableHead>
371    <Row>
372      <HeaderCell>Name</HeaderCell>
373      <HeaderCell>Type</HeaderCell>
374      <HeaderCell>Description</HeaderCell>
375    </Row>
376  </TableHead>
377);
378
379export const renderParams = (parameters: MethodParamData[]) => (
380  <Table>
381    {renderTableHeadRow()}
382    <tbody>{parameters?.map(renderParamRow)}</tbody>
383  </Table>
384);
385
386export const listParams = (parameters: MethodParamData[]) =>
387  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
388
389export const renderDefaultValue = (defaultValue?: string) =>
390  defaultValue && defaultValue !== '...' ? (
391    <div css={defaultValueContainerStyle}>
392      <B>Default:</B> <InlineCode>{defaultValue}</InlineCode>
393    </div>
394  ) : undefined;
395
396export const renderTypeOrSignatureType = (
397  type?: TypeDefinitionData,
398  signatures?: MethodSignatureData[]
399) => {
400  if (signatures && signatures.length) {
401    return (
402      <InlineCode key={`signature-type-${signatures[0].name}`}>
403        (
404        {signatures?.map(({ parameters }) =>
405          parameters?.map(param => (
406            <span key={`signature-param-${param.name}`}>
407              {param.name}
408              {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
409            </span>
410          ))
411        )}
412        ) =&gt;{' '}
413        {type ? (
414          <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>
415        ) : (
416          'void'
417        )}
418      </InlineCode>
419    );
420  } else if (type) {
421    return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>;
422  }
423  return undefined;
424};
425
426export const renderFlags = (flags?: TypePropertyDataFlags, defaultValue?: string) =>
427  (flags?.isOptional || defaultValue) && (
428    <>
429      <br />
430      <span css={STYLES_OPTIONAL}>(optional)</span>
431    </>
432  );
433
434export const renderIndexSignature = (kind: TypeDocKind) =>
435  kind === TypeDocKind.Parameter && (
436    <>
437      <br />
438      <A
439        css={STYLES_OPTIONAL}
440        href="https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures"
441        openInNewTab
442        isStyled>
443        (index signature)
444      </A>
445    </>
446  );
447
448export type CommentTextBlockProps = {
449  comment?: CommentData;
450  components?: MDComponents;
451  withDash?: boolean;
452  beforeContent?: JSX.Element;
453  afterContent?: JSX.Element;
454  includePlatforms?: boolean;
455  emptyCommentFallback?: string;
456};
457
458export const parseCommentContent = (content?: string): string =>
459  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
460
461export const getCommentOrSignatureComment = (
462  comment?: CommentData,
463  signatures?: MethodSignatureData[]
464) => comment || (signatures && signatures[0]?.comment);
465
466export const getTagData = (tagName: string, comment?: CommentData) =>
467  getAllTagData(tagName, comment)?.[0];
468
469export const getAllTagData = (tagName: string, comment?: CommentData) =>
470  comment?.tags?.filter(tag => tag.tag === tagName);
471
472export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
473
474export const CommentTextBlock = ({
475  comment,
476  components = mdComponents,
477  withDash,
478  beforeContent,
479  afterContent,
480  includePlatforms = true,
481  emptyCommentFallback,
482}: CommentTextBlockProps) => {
483  const shortText = comment?.shortText?.trim().length ? (
484    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
485      {parseCommentContent(comment.shortText)}
486    </ReactMarkdown>
487  ) : null;
488  const text = comment?.text?.trim().length ? (
489    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
490      {parseCommentContent(comment.text)}
491    </ReactMarkdown>
492  ) : null;
493
494  if (emptyCommentFallback && (!comment || (!shortText && !text))) {
495    return <>{emptyCommentFallback}</>;
496  }
497
498  const examples = getAllTagData('example', comment);
499  const exampleText = examples?.map((example, index) => (
500    <React.Fragment key={'example-' + index}>
501      {components !== mdComponents ? (
502        <div css={STYLES_EXAMPLE_IN_TABLE}>
503          <B>Example</B>
504        </div>
505      ) : (
506        <div css={STYLES_NESTED_SECTION_HEADER}>
507          <H4>Example</H4>
508        </div>
509      )}
510      <ReactMarkdown components={components}>{example.text}</ReactMarkdown>
511    </React.Fragment>
512  ));
513
514  const see = getTagData('see', comment);
515  const seeText = see ? (
516    <Quote>
517      <B>See: </B>
518      <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown>
519    </Quote>
520  ) : null;
521
522  const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0;
523
524  return (
525    <>
526      {!withDash && includePlatforms && hasPlatforms && (
527        <APISectionPlatformTags comment={comment} prefix="Only for:" />
528      )}
529      {beforeContent}
530      {withDash && (shortText || text) && ' - '}
531      {withDash && includePlatforms && <APISectionPlatformTags comment={comment} />}
532      {shortText}
533      {text}
534      {afterContent}
535      {seeText}
536      {exampleText}
537    </>
538  );
539};
540
541export const getComponentName = (name?: string, children: PropData[] = []) => {
542  if (name && name !== 'default') return name;
543  const ctor = children.filter((child: PropData) => child.name === 'constructor')[0];
544  return ctor?.signatures?.[0]?.type?.name ?? 'default';
545};
546
547export const STYLES_APIBOX = css({
548  borderRadius: borderRadius.medium,
549  borderWidth: 1,
550  borderStyle: 'solid',
551  borderColor: theme.border.default,
552  padding: `${spacing[1]}px ${spacing[5]}px`,
553  boxShadow: shadows.micro,
554  marginBottom: spacing[6],
555  overflowX: 'hidden',
556
557  h3: {
558    marginTop: spacing[4],
559  },
560
561  th: {
562    color: theme.text.secondary,
563    padding: `${spacing[3]}px ${spacing[4]}px`,
564  },
565
566  [`.css-${tableWrapperStyle.name}`]: {
567    boxShadow: 'none',
568  },
569
570  [`@media screen and (max-width: ${Constants.breakpoints.mobile})`]: {
571    padding: `0 ${spacing[4]}px`,
572  },
573});
574
575export const STYLES_APIBOX_NESTED = css({
576  boxShadow: 'none',
577});
578
579export const STYLES_NESTED_SECTION_HEADER = css({
580  display: 'flex',
581  borderTop: `1px solid ${theme.border.default}`,
582  borderBottom: `1px solid ${theme.border.default}`,
583  margin: `${spacing[6]}px -${spacing[5]}px ${spacing[4]}px`,
584  padding: `${spacing[2.5]}px ${spacing[5]}px`,
585  backgroundColor: theme.background.secondary,
586
587  h4: {
588    ...typography.fontSizes[16],
589    fontFamily: typography.fontFaces.medium,
590    marginBottom: 0,
591    color: theme.text.secondary,
592  },
593});
594
595export const STYLES_NOT_EXPOSED_HEADER = css({
596  marginTop: spacing[5],
597  marginBottom: spacing[2],
598  display: 'inline-block',
599});
600
601export const STYLES_OPTIONAL = css({
602  color: theme.text.secondary,
603  fontSize: '90%',
604  paddingTop: 22,
605});
606
607export const STYLES_SECONDARY = css({
608  color: theme.text.secondary,
609  fontSize: '90%',
610  fontWeight: 600,
611});
612
613const defaultValueContainerStyle = css({
614  marginTop: spacing[2],
615});
616
617const STYLES_EXAMPLE_IN_TABLE = css({
618  margin: `${spacing[2]}px 0`,
619});
620