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