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