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