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  Enum = 4,
23  Variable = 32,
24  Function = 64,
25  Class = 128,
26  Interface = 256,
27  Property = 1024,
28  TypeAlias = 4194304,
29}
30
31export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components'];
32
33export const mdComponents: MDComponents = {
34  blockquote: ({ children }) => (
35    <Quote>
36      {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */}
37      {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))}
38    </Quote>
39  ),
40  code: ({ children, node }) =>
41    Array.isArray(node.properties?.className) ? (
42      <Code className={node.properties?.className[0].toString() || 'language-unknown'}>
43        {children}
44      </Code>
45    ) : (
46      <InlineCode>{children}</InlineCode>
47    ),
48  h1: ({ children }) => <H4>{children}</H4>,
49  ul: ({ children }) => <UL>{children}</UL>,
50  li: ({ children }) => <LI>{children}</LI>,
51  a: ({ href, children }) => <Link href={href}>{children}</Link>,
52  p: ({ children }) => (children ? <P>{children}</P> : null),
53  strong: ({ children }) => <B>{children}</B>,
54  span: ({ children }) => (children ? <span>{children}</span> : null),
55};
56
57export const mdInlineComponents: MDComponents = {
58  ...mdComponents,
59  p: ({ children }) => (children ? <span>{children}</span> : null),
60};
61
62const nonLinkableTypes = [
63  'ColorValue',
64  'E',
65  'EventSubscription',
66  'File',
67  'FileList',
68  'Manifest',
69  'NativeSyntheticEvent',
70  'React.FC',
71  'ServiceActionResult',
72  'StyleProp',
73  'T',
74  'TaskOptions',
75  'Uint8Array',
76  // Cross-package permissions management
77  'RequestPermissionMethod',
78  'GetPermissionMethod',
79  'Options',
80  'PermissionHookBehavior',
81];
82
83const hardcodedTypeLinks: Record<string, string> = {
84  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
85  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
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  if (name) {
126    if (type === 'reference') {
127      if (typeArguments) {
128        if (name === 'Record' || name === 'React.ComponentProps') {
129          return (
130            <>
131              {name}&lt;
132              {typeArguments.map((type, index) => (
133                <span key={`record-type-${index}`}>
134                  {resolveTypeName(type)}
135                  {index !== typeArguments.length - 1 ? ', ' : null}
136                </span>
137              ))}
138              &gt;
139            </>
140          );
141        } else {
142          return (
143            <>
144              {renderWithLink(name)}
145              &lt;
146              {typeArguments.map((type, index) => (
147                <span key={`${name}-nested-type-${index}`}>
148                  {resolveTypeName(type)}
149                  {index !== typeArguments.length - 1 ? ', ' : null}
150                </span>
151              ))}
152              &gt;
153            </>
154          );
155        }
156      } else {
157        return renderWithLink(name);
158      }
159    } else {
160      return name;
161    }
162  } else if (elementType?.name) {
163    if (elementType.type === 'reference') {
164      return renderWithLink(elementType.name, type);
165    } else if (type === 'array') {
166      return elementType.name + '[]';
167    }
168    return elementType.name + type;
169  } else if (elementType?.declaration) {
170    if (type === 'array') {
171      const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
172      if (parameters && paramType) {
173        return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
174      }
175    }
176    return elementType.name + type;
177  } else if (type === 'union' && types?.length) {
178    return renderUnion(types);
179  } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
180    const unionTypes = elementType?.types || [];
181    return (
182      <>
183        ({renderUnion(unionTypes)}){type === 'array' && '[]'}
184      </>
185    );
186  } else if (declaration?.signatures) {
187    const baseSignature = declaration.signatures[0];
188    if (baseSignature?.parameters?.length) {
189      return (
190        <>
191          (
192          {baseSignature.parameters?.map((param, index) => (
193            <span key={`param-${index}-${param.name}`}>
194              {param.name}: {resolveTypeName(param.type)}
195              {index + 1 !== baseSignature.parameters?.length && ', '}
196            </span>
197          ))}
198          ) {'=>'} {resolveTypeName(baseSignature.type)}
199        </>
200      );
201    } else {
202      return (
203        <>
204          {'() =>'} {resolveTypeName(baseSignature.type)}
205        </>
206      );
207    }
208  } else if (type === 'reflection' && declaration?.children) {
209    return (
210      <>
211        {'{ '}
212        {declaration?.children.map((child: PropData, i) => (
213          <span key={`reflection-${name}-${i}`}>
214            {child.name + ': ' + resolveTypeName(child.type)}
215            {i + 1 !== declaration?.children?.length ? ', ' : null}
216          </span>
217        ))}
218        {' }'}
219      </>
220    );
221  } else if (type === 'tuple' && elements) {
222    return (
223      <>
224        [
225        {elements.map((elem, i) => (
226          <span key={`tuple-${name}-${i}`}>
227            {resolveTypeName(elem)}
228            {i + 1 !== elements.length ? ', ' : null}
229          </span>
230        ))}
231        ]
232      </>
233    );
234  } else if (type === 'query' && queryType) {
235    return queryType.name;
236  } else if (type === 'literal' && typeof value === 'boolean') {
237    return `${value}`;
238  } else if (type === 'literal' && value) {
239    return `'${value}'`;
240  } else if (value === null) {
241    return 'null';
242  }
243  return 'undefined';
244};
245
246export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
247
248export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
249  <LI key={`param-${name}`}>
250    <B>
251      {parseParamName(name)}
252      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
253    </B>
254    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
255  </LI>
256);
257
258export const listParams = (parameters: MethodParamData[]) =>
259  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
260
261export const renderTypeOrSignatureType = (
262  type?: TypeDefinitionData,
263  signatures?: MethodSignatureData[],
264  includeParamType: boolean = false
265) => {
266  if (type) {
267    return <InlineCode>{resolveTypeName(type)}</InlineCode>;
268  } else if (signatures && signatures.length) {
269    return signatures.map(({ name, type, parameters }) => (
270      <InlineCode key={`signature-type-${name}`}>
271        (
272        {parameters && includeParamType
273          ? parameters.map(param => (
274              <>
275                {param.name}
276                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
277              </>
278            ))
279          : listParams(parameters)}
280        ) =&gt; {resolveTypeName(type)}
281      </InlineCode>
282    ));
283  }
284  return undefined;
285};
286
287export const renderFlags = (flags?: TypePropertyDataFlags) =>
288  flags?.isOptional ? (
289    <>
290      <br />
291      <span css={STYLES_OPTIONAL}>(optional)</span>
292    </>
293  ) : undefined;
294
295export type CommentTextBlockProps = {
296  comment?: CommentData;
297  components?: MDComponents;
298  withDash?: boolean;
299  beforeContent?: JSX.Element;
300};
301
302export const parseCommentContent = (content?: string): string =>
303  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
304
305export const getCommentOrSignatureComment = (
306  comment?: CommentData,
307  signatures?: MethodSignatureData[]
308) => comment || (signatures && signatures[0]?.comment);
309
310export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({
311  comment,
312  components = mdComponents,
313  withDash,
314  beforeContent,
315}) => {
316  const shortText = comment?.shortText?.trim().length ? (
317    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
318      {parseCommentContent(comment.shortText)}
319    </ReactMarkdown>
320  ) : null;
321  const text = comment?.text?.trim().length ? (
322    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
323      {parseCommentContent(comment.text)}
324    </ReactMarkdown>
325  ) : null;
326
327  const example = comment?.tags?.filter(tag => tag.tag === 'example')[0];
328  const exampleText = example ? (
329    <>
330      <br />
331      <br />
332      <ReactMarkdown components={components}>{`__Example:__ ${example.text}`}</ReactMarkdown>
333    </>
334  ) : null;
335
336  const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0];
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  return (
348    <>
349      {deprecationNote}
350      {beforeContent}
351      {withDash && (shortText || text) ? ' - ' : null}
352      {shortText}
353      {text}
354      {exampleText}
355    </>
356  );
357};
358
359export const STYLES_OPTIONAL = css`
360  color: ${theme.text.secondary};
361  font-size: 90%;
362  padding-top: 22px;
363`;
364
365export const STYLES_SECONDARY = css`
366  color: ${theme.text.secondary};
367  font-size: 90%;
368  font-weight: 600;
369`;
370