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