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