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