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