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