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