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 ExpoConfig: 85 'https://github.com/expo/expo-cli/blob/master/packages/config-types/src/ExpoConfig.ts', 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 try { 126 if (name) { 127 if (type === 'reference') { 128 if (typeArguments) { 129 if (name === 'Record' || name === 'React.ComponentProps') { 130 return ( 131 <> 132 {name}< 133 {typeArguments.map((type, index) => ( 134 <span key={`record-type-${index}`}> 135 {resolveTypeName(type)} 136 {index !== typeArguments.length - 1 ? ', ' : null} 137 </span> 138 ))} 139 > 140 </> 141 ); 142 } else { 143 return ( 144 <> 145 {renderWithLink(name)} 146 < 147 {typeArguments.map((type, index) => ( 148 <span key={`${name}-nested-type-${index}`}> 149 {resolveTypeName(type)} 150 {index !== typeArguments.length - 1 ? ', ' : null} 151 </span> 152 ))} 153 > 154 </> 155 ); 156 } 157 } else { 158 return renderWithLink(name); 159 } 160 } else { 161 return name; 162 } 163 } else if (elementType?.name) { 164 if (elementType.type === 'reference') { 165 return renderWithLink(elementType.name, type); 166 } else if (type === 'array') { 167 return elementType.name + '[]'; 168 } 169 return elementType.name + type; 170 } else if (elementType?.declaration) { 171 if (type === 'array') { 172 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 173 if (parameters && paramType) { 174 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 175 } 176 } 177 return elementType.name + type; 178 } else if (type === 'union' && types?.length) { 179 return renderUnion(types); 180 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 181 const unionTypes = elementType?.types || []; 182 return ( 183 <> 184 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 185 </> 186 ); 187 } else if (declaration?.signatures) { 188 const baseSignature = declaration.signatures[0]; 189 if (baseSignature?.parameters?.length) { 190 return ( 191 <> 192 ( 193 {baseSignature.parameters?.map((param, index) => ( 194 <span key={`param-${index}-${param.name}`}> 195 {param.name}: {resolveTypeName(param.type)} 196 {index + 1 !== baseSignature.parameters?.length && ', '} 197 </span> 198 ))} 199 ) {'=>'} {resolveTypeName(baseSignature.type)} 200 </> 201 ); 202 } else { 203 return ( 204 <> 205 {'() =>'} {resolveTypeName(baseSignature.type)} 206 </> 207 ); 208 } 209 } else if (type === 'reflection' && declaration?.children) { 210 return ( 211 <> 212 {'{ '} 213 {declaration?.children.map((child: PropData, i) => ( 214 <span key={`reflection-${name}-${i}`}> 215 {child.name + ': ' + resolveTypeName(child.type)} 216 {i + 1 !== declaration?.children?.length ? ', ' : null} 217 </span> 218 ))} 219 {' }'} 220 </> 221 ); 222 } else if (type === 'tuple' && elements) { 223 return ( 224 <> 225 [ 226 {elements.map((elem, i) => ( 227 <span key={`tuple-${name}-${i}`}> 228 {resolveTypeName(elem)} 229 {i + 1 !== elements.length ? ', ' : null} 230 </span> 231 ))} 232 ] 233 </> 234 ); 235 } else if (type === 'query' && queryType) { 236 return queryType.name; 237 } else if (type === 'literal' && typeof value === 'boolean') { 238 return `${value}`; 239 } else if (type === 'literal' && value) { 240 return `'${value}'`; 241 } else if (value === null) { 242 return 'null'; 243 } 244 return 'undefined'; 245 } catch (e) { 246 console.warn('Type resolve has failed!', e); 247 return 'undefined'; 248 } 249}; 250 251export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 252 253export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => ( 254 <LI key={`param-${name}`}> 255 <B> 256 {parseParamName(name)} 257 {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>) 258 </B> 259 <CommentTextBlock comment={comment} components={mdInlineComponents} withDash /> 260 </LI> 261); 262 263export const listParams = (parameters: MethodParamData[]) => 264 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 265 266export const renderTypeOrSignatureType = ( 267 type?: TypeDefinitionData, 268 signatures?: MethodSignatureData[], 269 includeParamType: boolean = false 270) => { 271 if (type) { 272 return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>; 273 } else if (signatures && signatures.length) { 274 return signatures.map(({ name, type, parameters }) => ( 275 <InlineCode key={`signature-type-${name}`}> 276 ( 277 {parameters && includeParamType 278 ? parameters.map(param => ( 279 <span key={`signature-param-${param.name}`}> 280 {param.name} 281 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 282 </span> 283 )) 284 : listParams(parameters)} 285 ) => {resolveTypeName(type)} 286 </InlineCode> 287 )); 288 } 289 return undefined; 290}; 291 292export const renderFlags = (flags?: TypePropertyDataFlags) => 293 flags?.isOptional ? ( 294 <> 295 <br /> 296 <span css={STYLES_OPTIONAL}>(optional)</span> 297 </> 298 ) : undefined; 299 300export type CommentTextBlockProps = { 301 comment?: CommentData; 302 components?: MDComponents; 303 withDash?: boolean; 304 beforeContent?: JSX.Element; 305 includePlatforms?: boolean; 306}; 307 308export const parseCommentContent = (content?: string): string => 309 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 310 311export const getCommentOrSignatureComment = ( 312 comment?: CommentData, 313 signatures?: MethodSignatureData[] 314) => comment || (signatures && signatures[0]?.comment); 315 316export const getTagData = (tagName: string, comment?: CommentData) => 317 getAllTagData(tagName, comment)?.[0]; 318 319export const getAllTagData = (tagName: string, comment?: CommentData) => 320 comment?.tags?.filter(tag => tag.tag === tagName); 321 322const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 323 324const formatPlatformName = (name: string) => { 325 const cleanName = name.toLowerCase().replace('\n', ''); 326 return cleanName.includes('ios') 327 ? cleanName.replace('ios', 'iOS') 328 : cleanName.includes('expo') 329 ? cleanName.replace('expo', 'Expo Go') 330 : capitalize(name); 331}; 332 333export const getPlatformTags = (comment?: CommentData) => { 334 const platforms = getAllTagData('platform', comment); 335 return platforms?.length ? ( 336 <> 337 {platforms.map(platform => ( 338 <div key={platform.text} css={STYLES_PLATFORM}> 339 {formatPlatformName(platform.text)} Only 340 </div> 341 ))} 342 <br /> 343 </> 344 ) : null; 345}; 346 347export const CommentTextBlock = ({ 348 comment, 349 components = mdComponents, 350 withDash, 351 beforeContent, 352 includePlatforms = true, 353}: CommentTextBlockProps) => { 354 const shortText = comment?.shortText?.trim().length ? ( 355 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 356 {parseCommentContent(comment.shortText)} 357 </ReactMarkdown> 358 ) : null; 359 const text = comment?.text?.trim().length ? ( 360 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 361 {parseCommentContent(comment.text)} 362 </ReactMarkdown> 363 ) : null; 364 365 const example = getTagData('example', comment); 366 const exampleText = example ? ( 367 <> 368 <H4>Example</H4> 369 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 370 </> 371 ) : null; 372 373 const deprecation = getTagData('deprecated', comment); 374 const deprecationNote = deprecation ? ( 375 <Quote key="deprecation-note"> 376 {deprecation.text.trim().length ? ( 377 <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown> 378 ) : ( 379 <B>Deprecated</B> 380 )} 381 </Quote> 382 ) : null; 383 384 const see = getTagData('see', comment); 385 const seeText = see ? ( 386 <Quote> 387 <B>See: </B> 388 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 389 </Quote> 390 ) : null; 391 392 return ( 393 <> 394 {deprecationNote} 395 {beforeContent} 396 {withDash && (shortText || text) && ' - '} 397 {includePlatforms && getPlatformTags(comment)} 398 {shortText} 399 {text} 400 {seeText} 401 {exampleText} 402 </> 403 ); 404}; 405 406export const STYLES_OPTIONAL = css` 407 color: ${theme.text.secondary}; 408 font-size: 90%; 409 padding-top: 22px; 410`; 411 412export const STYLES_SECONDARY = css` 413 color: ${theme.text.secondary}; 414 font-size: 90%; 415 font-weight: 600; 416`; 417 418export const STYLES_PLATFORM = css` 419 display: inline-block; 420 background-color: ${theme.background.tertiary}; 421 color: ${theme.text.default}; 422 font-size: 90%; 423 font-weight: 700; 424 padding: 6px 12px; 425 margin-bottom: 8px; 426 margin-right: 8px; 427 border-radius: 4px; 428`; 429