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  TypeAlias = 4194304,
30}
31
32export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components'];
33
34export const mdComponents: MDComponents = {
35  blockquote: ({ children }) => (
36    <Quote>
37      {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */}
38      {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))}
39    </Quote>
40  ),
41  code: ({ children, className }) =>
42    className ? <Code className={className}>{children}</Code> : <InlineCode>{children}</InlineCode>,
43  h1: ({ children }) => <H4>{children}</H4>,
44  ul: ({ children }) => <UL>{children}</UL>,
45  li: ({ children }) => <LI>{children}</LI>,
46  a: ({ href, children }) => <Link href={href}>{children}</Link>,
47  p: ({ children }) => (children ? <P>{children}</P> : null),
48  strong: ({ children }) => <B>{children}</B>,
49  span: ({ children }) => (children ? <span>{children}</span> : null),
50};
51
52export const mdInlineComponents: MDComponents = {
53  ...mdComponents,
54  p: ({ children }) => (children ? <span>{children}</span> : null),
55};
56
57const nonLinkableTypes = [
58  'ColorValue',
59  'Component',
60  'E',
61  'EventSubscription',
62  'File',
63  'FileList',
64  'Manifest',
65  'NativeSyntheticEvent',
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  Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys',
83  Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys',
84  Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype',
85  Promise:
86    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
87  View: '../../react-native/view',
88  ViewProps: '../../react-native/view#props',
89  ViewStyle: '../../react-native/view-style-props/',
90};
91
92const renderWithLink = (name: string, type?: string) =>
93  nonLinkableTypes.includes(name) ? (
94    name + (type === 'array' ? '[]' : '')
95  ) : (
96    <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}>
97      {name}
98      {type === 'array' && '[]'}
99    </Link>
100  );
101
102const renderUnion = (types: TypeDefinitionData[]) =>
103  types.map(resolveTypeName).map((valueToRender, index) => (
104    <span key={`union-type-${index}`}>
105      {valueToRender}
106      {index + 1 !== types.length && ' | '}
107    </span>
108  ));
109
110export const resolveTypeName = ({
111  elements,
112  elementType,
113  name,
114  type,
115  types,
116  typeArguments,
117  declaration,
118  value,
119  queryType,
120  checkType,
121  extendsType,
122}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
123  if (name) {
124    if (type === 'reference') {
125      if (typeArguments) {
126        if (name === 'Record' || name === 'React.ComponentProps') {
127          return (
128            <>
129              {name}&lt;
130              {typeArguments.map((type, index) => (
131                <span key={`record-type-${index}`}>
132                  {resolveTypeName(type)}
133                  {index !== typeArguments.length - 1 ? ', ' : null}
134                </span>
135              ))}
136              &gt;
137            </>
138          );
139        } else {
140          return (
141            <>
142              {renderWithLink(name)}
143              &lt;
144              {typeArguments.map((type, index) => (
145                <span key={`${name}-nested-type-${index}`}>
146                  {resolveTypeName(type)}
147                  {index !== typeArguments.length - 1 ? ', ' : null}
148                </span>
149              ))}
150              &gt;
151            </>
152          );
153        }
154      } else {
155        return renderWithLink(name);
156      }
157    } else {
158      return name;
159    }
160  } else if (elementType?.name) {
161    if (elementType.type === 'reference') {
162      return renderWithLink(elementType.name, type);
163    } else if (type === 'array') {
164      return elementType.name + '[]';
165    }
166    return elementType.name + type;
167  } else if (elementType?.declaration) {
168    if (type === 'array') {
169      const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
170      if (parameters && paramType) {
171        return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
172      }
173    }
174    return elementType.name + type;
175  } else if (type === 'union' && types?.length) {
176    return renderUnion(types);
177  } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
178    const unionTypes = elementType?.types || [];
179    return (
180      <>
181        ({renderUnion(unionTypes)}){type === 'array' && '[]'}
182      </>
183    );
184  } else if (declaration?.signatures) {
185    const baseSignature = declaration.signatures[0];
186    if (baseSignature?.parameters?.length) {
187      return (
188        <>
189          (
190          {baseSignature.parameters?.map((param, index) => (
191            <span key={`param-${index}-${param.name}`}>
192              {param.name}: {resolveTypeName(param.type)}
193              {index + 1 !== baseSignature.parameters?.length && ', '}
194            </span>
195          ))}
196          ) {'=>'} {resolveTypeName(baseSignature.type)}
197        </>
198      );
199    } else {
200      return (
201        <>
202          {'() =>'} {resolveTypeName(baseSignature.type)}
203        </>
204      );
205    }
206  } else if (type === 'reflection' && declaration?.children) {
207    return (
208      <>
209        {'{ '}
210        {declaration?.children.map((child: PropData, i) => (
211          <span key={`reflection-${name}-${i}`}>
212            {child.name + ': ' + resolveTypeName(child.type)}
213            {i + 1 !== declaration?.children?.length ? ', ' : null}
214          </span>
215        ))}
216        {' }'}
217      </>
218    );
219  } else if (type === 'tuple' && elements) {
220    return (
221      <>
222        [
223        {elements.map((elem, i) => (
224          <span key={`tuple-${name}-${i}`}>
225            {resolveTypeName(elem)}
226            {i + 1 !== elements.length ? ', ' : null}
227          </span>
228        ))}
229        ]
230      </>
231    );
232  } else if (type === 'query' && queryType) {
233    return queryType.name;
234  } else if (type === 'literal' && typeof value === 'boolean') {
235    return `${value}`;
236  } else if (type === 'literal' && value) {
237    return `'${value}'`;
238  } else if (value === null) {
239    return 'null';
240  }
241  return 'undefined';
242};
243
244export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
245
246export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
247  <LI key={`param-${name}`}>
248    <B>
249      {parseParamName(name)}
250      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
251    </B>
252    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
253  </LI>
254);
255
256export const listParams = (parameters: MethodParamData[]) =>
257  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
258
259export const renderTypeOrSignatureType = (
260  type?: TypeDefinitionData,
261  signatures?: MethodSignatureData[],
262  includeParamType: boolean = false
263) => {
264  if (type) {
265    return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>;
266  } else if (signatures && signatures.length) {
267    return signatures.map(({ name, type, parameters }) => (
268      <InlineCode key={`signature-type-${name}`}>
269        (
270        {parameters && includeParamType
271          ? parameters.map(param => (
272              <span key={`signature-param-${param.name}`}>
273                {param.name}
274                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
275              </span>
276            ))
277          : listParams(parameters)}
278        ) =&gt; {resolveTypeName(type)}
279      </InlineCode>
280    ));
281  }
282  return undefined;
283};
284
285export const renderFlags = (flags?: TypePropertyDataFlags) =>
286  flags?.isOptional ? (
287    <>
288      <br />
289      <span css={STYLES_OPTIONAL}>(optional)</span>
290    </>
291  ) : undefined;
292
293export type CommentTextBlockProps = {
294  comment?: CommentData;
295  components?: MDComponents;
296  withDash?: boolean;
297  beforeContent?: JSX.Element;
298};
299
300export const parseCommentContent = (content?: string): string =>
301  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
302
303export const getCommentOrSignatureComment = (
304  comment?: CommentData,
305  signatures?: MethodSignatureData[]
306) => comment || (signatures && signatures[0]?.comment);
307
308export const getTagData = (tagName: string, comment?: CommentData) =>
309  comment?.tags?.filter(tag => tag.tag === tagName)[0];
310
311export const CommentTextBlock = ({
312  comment,
313  components = mdComponents,
314  withDash,
315  beforeContent,
316}: CommentTextBlockProps) => {
317  const shortText = comment?.shortText?.trim().length ? (
318    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
319      {parseCommentContent(comment.shortText)}
320    </ReactMarkdown>
321  ) : null;
322  const text = comment?.text?.trim().length ? (
323    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
324      {parseCommentContent(comment.text)}
325    </ReactMarkdown>
326  ) : null;
327
328  const example = getTagData('example', comment);
329  const exampleText = example ? (
330    <>
331      <H4>Example</H4>
332      <ReactMarkdown components={components}>{example.text}</ReactMarkdown>
333    </>
334  ) : null;
335
336  const deprecation = getTagData('deprecated', comment);
337  const deprecationNote = deprecation ? (
338    <Quote key="deprecation-note">
339      {deprecation.text.trim().length ? (
340        <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown>
341      ) : (
342        <B>Deprecated</B>
343      )}
344    </Quote>
345  ) : null;
346
347  const see = getTagData('see', comment);
348  const seeText = see ? (
349    <Quote>
350      <B>See: </B>
351      <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown>
352    </Quote>
353  ) : null;
354
355  return (
356    <>
357      {deprecationNote}
358      {beforeContent}
359      {withDash && (shortText || text) && ' - '}
360      {shortText}
361      {text}
362      {seeText}
363      {exampleText}
364    </>
365  );
366};
367
368export const STYLES_OPTIONAL = css`
369  color: ${theme.text.secondary};
370  font-size: 90%;
371  padding-top: 22px;
372`;
373
374export const STYLES_SECONDARY = css`
375  color: ${theme.text.secondary};
376  font-size: 90%;
377  font-weight: 600;
378`;
379