1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import React from 'react';
4import ReactMarkdown from 'react-markdown';
5
6import { Code, InlineCode } from '~/components/base/code';
7import { H4 } from '~/components/base/headings';
8import Link from '~/components/base/link';
9import { LI, UL } from '~/components/base/list';
10import { B, P, Quote } from '~/components/base/paragraph';
11import {
12  CommentData,
13  MethodParamData,
14  MethodSignatureData,
15  PropData,
16  TypeDefinitionData,
17  TypePropertyDataFlags,
18} from '~/components/plugins/api/APIDataTypes';
19
20export enum TypeDocKind {
21  Enum = 4,
22  Variable = 32,
23  Function = 64,
24  Class = 128,
25  Interface = 256,
26  Property = 1024,
27  TypeAlias = 4194304,
28}
29
30export type MDRenderers = React.ComponentProps<typeof ReactMarkdown>['renderers'];
31
32export const mdRenderers: MDRenderers = {
33  blockquote: ({ children }) => (
34    <Quote>
35      {React.Children.map(children, child =>
36        child.type.name === 'paragraph' ? child.props.children : child
37      )}
38    </Quote>
39  ),
40  code: ({ value, language }) => <Code className={`language-${language}`}>{value}</Code>,
41  heading: ({ children }) => <H4>{children}</H4>,
42  inlineCode: ({ value }) => <InlineCode>{value}</InlineCode>,
43  list: ({ children }) => <UL>{children}</UL>,
44  listItem: ({ children }) => <LI>{children}</LI>,
45  link: ({ href, children }) => <Link href={href}>{children}</Link>,
46  paragraph: ({ children }) => (children ? <P>{children}</P> : null),
47  strong: ({ children }) => <B>{children}</B>,
48  text: ({ value }) => (value ? <span>{value}</span> : null),
49};
50
51export const mdInlineRenderers: MDRenderers = {
52  ...mdRenderers,
53  paragraph: ({ children }) => (children ? <span>{children}</span> : null),
54};
55
56const nonLinkableTypes = [
57  'ColorValue',
58  'E',
59  'EventSubscription',
60  'File',
61  'FileList',
62  'Manifest',
63  'NativeSyntheticEvent',
64  'Omit',
65  'Pick',
66  'React.FC',
67  'ServiceActionResult',
68  'StyleProp',
69  'T',
70  'TaskOptions',
71  'Uint8Array',
72  // Cross-package permissions management
73  'RequestPermissionMethod',
74  'GetPermissionMethod',
75  'Options',
76  'PermissionHookBehavior',
77];
78
79const hardcodedTypeLinks: Record<string, string> = {
80  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
81  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
82  Promise:
83    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
84  View: '../../react-native/view',
85  ViewProps: '../../react-native/view#props',
86  ViewStyle: '../../react-native/view-style-props/',
87};
88
89const renderWithLink = (name: string, type?: string) =>
90  nonLinkableTypes.includes(name) ? (
91    name + (type === 'array' ? '[]' : '')
92  ) : (
93    <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}>
94      {name}
95      {type === 'array' && '[]'}
96    </Link>
97  );
98
99const renderUnion = (types: TypeDefinitionData[]) =>
100  types.map(resolveTypeName).map((valueToRender, index) => (
101    <span key={`union-type-${index}`}>
102      {valueToRender}
103      {index + 1 !== types.length && ' | '}
104    </span>
105  ));
106
107export const resolveTypeName = ({
108  elements,
109  elementType,
110  name,
111  type,
112  types,
113  typeArguments,
114  declaration,
115  value,
116  queryType,
117}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
118  if (name) {
119    if (type === 'reference') {
120      if (typeArguments) {
121        if (name === 'Record' || name === 'React.ComponentProps') {
122          return (
123            <>
124              {name}&lt;
125              {typeArguments.map((type, index) => (
126                <span key={`record-type-${index}`}>
127                  {resolveTypeName(type)}
128                  {index !== typeArguments.length - 1 ? ', ' : null}
129                </span>
130              ))}
131              &gt;
132            </>
133          );
134        } else {
135          return (
136            <>
137              {renderWithLink(name)}
138              &lt;
139              {typeArguments.map((type, index) => (
140                <span key={`${name}-nested-type-${index}`}>
141                  {resolveTypeName(type)}
142                  {index !== typeArguments.length - 1 ? ', ' : null}
143                </span>
144              ))}
145              &gt;
146            </>
147          );
148        }
149      } else {
150        return renderWithLink(name);
151      }
152    } else {
153      return name;
154    }
155  } else if (elementType?.name) {
156    if (elementType.type === 'reference') {
157      return renderWithLink(elementType.name, type);
158    } else if (type === 'array') {
159      return elementType.name + '[]';
160    }
161    return elementType.name + type;
162  } else if (elementType?.declaration) {
163    if (type === 'array') {
164      const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
165      if (parameters && paramType) {
166        return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
167      }
168    }
169    return elementType.name + type;
170  } else if (type === 'union' && types?.length) {
171    return renderUnion(types);
172  } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
173    const unionTypes = elementType?.types || [];
174    return (
175      <>
176        ({renderUnion(unionTypes)}){type === 'array' && '[]'}
177      </>
178    );
179  } else if (declaration?.signatures) {
180    const baseSignature = declaration.signatures[0];
181    if (baseSignature?.parameters?.length) {
182      return (
183        <>
184          (
185          {baseSignature.parameters?.map((param, index) => (
186            <span key={`param-${index}-${param.name}`}>
187              {param.name}: {resolveTypeName(param.type)}
188              {index + 1 !== baseSignature.parameters?.length && ', '}
189            </span>
190          ))}
191          ) {'=>'} {resolveTypeName(baseSignature.type)}
192        </>
193      );
194    } else {
195      return (
196        <>
197          {'() =>'} {resolveTypeName(baseSignature.type)}
198        </>
199      );
200    }
201  } else if (type === 'reflection' && declaration?.children) {
202    return (
203      <>
204        {'{ '}
205        {declaration?.children.map((child: PropData, i) => (
206          <span key={`reflection-${name}-${i}`}>
207            {child.name + ': ' + resolveTypeName(child.type)}
208            {i + 1 !== declaration?.children?.length ? ', ' : null}
209          </span>
210        ))}
211        {' }'}
212      </>
213    );
214  } else if (type === 'tuple' && elements) {
215    return (
216      <>
217        [
218        {elements.map((elem, i) => (
219          <span key={`tuple-${name}-${i}`}>
220            {resolveTypeName(elem)}
221            {i + 1 !== elements.length ? ', ' : null}
222          </span>
223        ))}
224        ]
225      </>
226    );
227  } else if (type === 'query' && queryType) {
228    return queryType.name;
229  } else if (type === 'literal' && typeof value === 'boolean') {
230    return `${value}`;
231  } else if (type === 'literal' && value) {
232    return `'${value}'`;
233  } else if (value === null) {
234    return 'null';
235  }
236  return 'undefined';
237};
238
239export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
240
241export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
242  <LI key={`param-${name}`}>
243    <B>
244      {parseParamName(name)}
245      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
246    </B>
247    <CommentTextBlock comment={comment} renderers={mdInlineRenderers} withDash />
248  </LI>
249);
250
251export const listParams = (parameters: MethodParamData[]) =>
252  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
253
254export const renderTypeOrSignatureType = (
255  type?: TypeDefinitionData,
256  signatures?: MethodSignatureData[],
257  includeParamType: boolean = false
258) => {
259  if (type) {
260    return <InlineCode>{resolveTypeName(type)}</InlineCode>;
261  } else if (signatures && signatures.length) {
262    return signatures.map(({ name, type, parameters }) => (
263      <InlineCode key={`signature-type-${name}`}>
264        (
265        {parameters && includeParamType
266          ? parameters.map(param => (
267              <>
268                {param.name}
269                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
270              </>
271            ))
272          : listParams(parameters)}
273        ) =&gt; {resolveTypeName(type)}
274      </InlineCode>
275    ));
276  }
277  return undefined;
278};
279
280export const renderFlags = (flags?: TypePropertyDataFlags) =>
281  flags?.isOptional ? (
282    <>
283      <br />
284      <span css={STYLES_OPTIONAL}>(optional)</span>
285    </>
286  ) : undefined;
287
288export type CommentTextBlockProps = {
289  comment?: CommentData;
290  renderers?: MDRenderers;
291  withDash?: boolean;
292  beforeContent?: JSX.Element;
293};
294
295export const parseCommentContent = (content?: string): string =>
296  content && content.length ? content.replace(/&ast;/g, '*') : '';
297
298export const getCommentOrSignatureComment = (
299  comment?: CommentData,
300  signatures?: MethodSignatureData[]
301) => comment || (signatures && signatures[0]?.comment);
302
303export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({
304  comment,
305  renderers = mdRenderers,
306  withDash,
307  beforeContent,
308}) => {
309  const shortText = comment?.shortText?.trim().length ? (
310    <ReactMarkdown renderers={renderers}>{parseCommentContent(comment.shortText)}</ReactMarkdown>
311  ) : null;
312  const text = comment?.text?.trim().length ? (
313    <ReactMarkdown renderers={renderers}>{parseCommentContent(comment.text)}</ReactMarkdown>
314  ) : null;
315
316  const example = comment?.tags?.filter(tag => tag.tag === 'example')[0];
317  const exampleText = example ? (
318    <ReactMarkdown renderers={renderers}>{`__Example:__ ${example.text}`}</ReactMarkdown>
319  ) : null;
320
321  const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0];
322  const deprecationNote = deprecation ? (
323    <Quote key="deprecation-note">
324      {deprecation.text.trim().length ? (
325        <ReactMarkdown renderers={mdInlineRenderers}>{deprecation.text}</ReactMarkdown>
326      ) : (
327        <B>Deprecated</B>
328      )}
329    </Quote>
330  ) : null;
331
332  return (
333    <>
334      {deprecationNote}
335      {beforeContent}
336      {withDash && (shortText || text) ? ' - ' : null}
337      {shortText}
338      {text}
339      {exampleText}
340    </>
341  );
342};
343
344export const STYLES_OPTIONAL = css`
345  color: ${theme.text.secondary};
346  font-size: 90%;
347  padding-top: 22px;
348`;
349
350export const STYLES_SECONDARY = css`
351  color: ${theme.text.secondary};
352  font-size: 90%;
353  font-weight: 600;
354`;
355