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 PropData, 15 TypeDefinitionData, 16} from '~/components/plugins/api/APIDataTypes'; 17 18export enum TypeDocKind { 19 Enum = 4, 20 Variable = 32, 21 Function = 64, 22 Class = 128, 23 Interface = 256, 24 Property = 1024, 25 TypeAlias = 4194304, 26} 27 28export type MDRenderers = React.ComponentProps<typeof ReactMarkdown>['renderers']; 29 30export const mdRenderers: MDRenderers = { 31 blockquote: ({ children }) => ( 32 <Quote> 33 {React.Children.map(children, child => 34 child.type.name === 'paragraph' ? child.props.children : child 35 )} 36 </Quote> 37 ), 38 code: ({ value, language }) => <Code className={`language-${language}`}>{value}</Code>, 39 heading: ({ children }) => <H4>{children}</H4>, 40 inlineCode: ({ value }) => <InlineCode>{value}</InlineCode>, 41 list: ({ children }) => <UL>{children}</UL>, 42 listItem: ({ children }) => <LI>{children}</LI>, 43 link: ({ href, children }) => <Link href={href}>{children}</Link>, 44 paragraph: ({ children }) => (children ? <P>{children}</P> : null), 45 strong: ({ children }) => <B>{children}</B>, 46 text: ({ value }) => (value ? <span>{value}</span> : null), 47}; 48 49export const mdInlineRenderers: MDRenderers = { 50 ...mdRenderers, 51 paragraph: ({ children }) => (children ? <span>{children}</span> : null), 52}; 53 54const nonLinkableTypes = [ 55 'ColorValue', 56 'E', 57 'EventSubscription', 58 'Manifest', 59 'NativeSyntheticEvent', 60 'Omit', 61 'Pick', 62 'React.FC', 63 'ServiceActionResult', 64 'StyleProp', 65 'T', 66 'TaskOptions', 67 'Uint8Array', 68 'RequestPermissionMethod', 69 'GetPermissionMethod', 70]; 71 72const hardcodedTypeLinks: Record<string, string> = { 73 Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date', 74 Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error', 75 Promise: 76 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', 77 View: '../../react-native/view', 78 ViewProps: '../../react-native/view#props', 79 ViewStyle: '../../react-native/view-style-props/', 80}; 81 82const renderWithLink = (name: string, type?: string) => 83 nonLinkableTypes.includes(name) ? ( 84 name + (type === 'array' ? '[]' : '') 85 ) : ( 86 <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}> 87 {name} 88 {type === 'array' && '[]'} 89 </Link> 90 ); 91 92export const resolveTypeName = ({ 93 elements, 94 elementType, 95 name, 96 type, 97 types, 98 typeArguments, 99 declaration, 100 value, 101 queryType, 102}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => { 103 if (name) { 104 if (type === 'reference') { 105 if (typeArguments) { 106 if (name === 'Record' || name === 'React.ComponentProps') { 107 return ( 108 <> 109 {name}< 110 {typeArguments.map((type, index) => ( 111 <span key={`record-type-${index}`}> 112 {resolveTypeName(type)} 113 {index !== typeArguments.length - 1 ? ', ' : null} 114 </span> 115 ))} 116 > 117 </> 118 ); 119 } else { 120 return ( 121 <> 122 {renderWithLink(name)} 123 < 124 {typeArguments.map((type, index) => ( 125 <span key={`${name}-nested-type-${index}`}> 126 {resolveTypeName(type)} 127 {index !== typeArguments.length - 1 ? ', ' : null} 128 </span> 129 ))} 130 > 131 </> 132 ); 133 } 134 } else { 135 return renderWithLink(name); 136 } 137 } else { 138 return name; 139 } 140 } else if (elementType?.name) { 141 if (elementType.type === 'reference') { 142 return renderWithLink(elementType.name, type); 143 } else if (type === 'array') { 144 return elementType.name + '[]'; 145 } 146 return elementType.name + type; 147 } else if (type === 'union' && types?.length) { 148 return types.map(resolveTypeName).map((valueToRender, index) => ( 149 <span key={`union-type-${index}`}> 150 {valueToRender} 151 {index + 1 !== types.length && ' | '} 152 </span> 153 )); 154 } else if (declaration?.signatures) { 155 const baseSignature = declaration.signatures[0]; 156 if (baseSignature?.parameters?.length) { 157 return ( 158 <> 159 ( 160 {baseSignature.parameters?.map((param, index) => ( 161 <span key={`param-${index}-${param.name}`}> 162 {param.name}: {resolveTypeName(param.type)} 163 {index + 1 !== baseSignature.parameters?.length && ', '} 164 </span> 165 ))} 166 ) {'=>'} {resolveTypeName(baseSignature.type)} 167 </> 168 ); 169 } else { 170 return ( 171 <> 172 {'() =>'} {resolveTypeName(baseSignature.type)} 173 </> 174 ); 175 } 176 } else if (type === 'reflection' && declaration?.children) { 177 return ( 178 <> 179 {'{ '} 180 {declaration?.children.map((child: PropData, i) => ( 181 <span key={`reflection-${name}-${i}`}> 182 {child.name + ': ' + resolveTypeName(child.type)} 183 {i + 1 !== declaration?.children?.length ? ', ' : null} 184 </span> 185 ))} 186 {' }'} 187 </> 188 ); 189 } else if (type === 'tuple' && elements) { 190 return ( 191 <> 192 [ 193 {elements.map((elem, i) => ( 194 <span key={`tuple-${name}-${i}`}> 195 {resolveTypeName(elem)} 196 {i + 1 !== elements.length ? ', ' : null} 197 </span> 198 ))} 199 ] 200 </> 201 ); 202 } else if (type === 'query' && queryType) { 203 return queryType.name; 204 } else if (type === 'literal' && typeof value === 'boolean') { 205 return `${value}`; 206 } else if (type === 'literal' && value) { 207 return `'${value}'`; 208 } else if (value === null) { 209 return 'null'; 210 } 211 return 'undefined'; 212}; 213 214export const renderParam = ({ comment, name, type }: MethodParamData): JSX.Element => ( 215 <LI key={`param-${name}`}> 216 <B> 217 {name} (<InlineCode>{resolveTypeName(type)}</InlineCode>) 218 </B> 219 <CommentTextBlock comment={comment} renderers={mdInlineRenderers} withDash /> 220 </LI> 221); 222 223export type CommentTextBlockProps = { 224 comment?: CommentData; 225 renderers?: MDRenderers; 226 withDash?: boolean; 227 beforeContent?: JSX.Element; 228}; 229 230export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({ 231 comment, 232 renderers = mdRenderers, 233 withDash, 234 beforeContent, 235}) => { 236 const shortText = comment?.shortText?.trim().length ? ( 237 <ReactMarkdown renderers={renderers}>{comment.shortText}</ReactMarkdown> 238 ) : null; 239 const text = comment?.text?.trim().length ? ( 240 <ReactMarkdown renderers={renderers}>{comment.text}</ReactMarkdown> 241 ) : null; 242 243 const example = comment?.tags?.filter(tag => tag.tag === 'example')[0]; 244 const exampleText = example ? ( 245 <ReactMarkdown renderers={renderers}>{`__Example:__ ${example.text}`}</ReactMarkdown> 246 ) : null; 247 248 const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0]; 249 const deprecationNote = deprecation ? ( 250 <Quote key="deprecation-note"> 251 {deprecation.text.trim().length ? ( 252 <ReactMarkdown renderers={mdInlineRenderers}>{deprecation.text}</ReactMarkdown> 253 ) : ( 254 <B>Deprecated</B> 255 )} 256 </Quote> 257 ) : null; 258 259 return ( 260 <> 261 {deprecationNote} 262 {beforeContent} 263 {withDash && (shortText || text) ? ' - ' : null} 264 {shortText} 265 {text} 266 {exampleText} 267 </> 268 ); 269}; 270 271export const STYLES_OPTIONAL = css` 272 color: ${theme.text.secondary}; 273 font-size: 90%; 274 padding-top: 22px; 275`; 276 277export const STYLES_SECONDARY = css` 278 color: ${theme.text.secondary}; 279 font-size: 90%; 280 font-weight: 600; 281`; 282