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