1import { css } from '@emotion/react'; 2import { theme } from '@expo/styleguide'; 3import React from 'react'; 4import ReactMarkdown from 'react-markdown'; 5 6import { Code, InlineCode } from '~/components/base/code'; 7import { H4 } from '~/components/base/headings'; 8import Link from '~/components/base/link'; 9import { LI, UL } from '~/components/base/list'; 10import { B, P, Quote } from '~/components/base/paragraph'; 11import { 12 CommentData, 13 MethodParamData, 14 MethodSignatureData, 15 PropData, 16 TypeDefinitionData, 17 TypePropertyDataFlags, 18} from '~/components/plugins/api/APIDataTypes'; 19 20export enum TypeDocKind { 21 Enum = 4, 22 Variable = 32, 23 Function = 64, 24 Class = 128, 25 Interface = 256, 26 Property = 1024, 27 TypeAlias = 4194304, 28} 29 30export type MDRenderers = React.ComponentProps<typeof ReactMarkdown>['renderers']; 31 32export const mdRenderers: MDRenderers = { 33 blockquote: ({ children }) => ( 34 <Quote> 35 {React.Children.map(children, child => 36 child.type.name === 'paragraph' ? child.props.children : child 37 )} 38 </Quote> 39 ), 40 code: ({ value, language }) => <Code className={`language-${language}`}>{value}</Code>, 41 heading: ({ children }) => <H4>{children}</H4>, 42 inlineCode: ({ value }) => <InlineCode>{value}</InlineCode>, 43 list: ({ children }) => <UL>{children}</UL>, 44 listItem: ({ children }) => <LI>{children}</LI>, 45 link: ({ href, children }) => <Link href={href}>{children}</Link>, 46 paragraph: ({ children }) => (children ? <P>{children}</P> : null), 47 strong: ({ children }) => <B>{children}</B>, 48 text: ({ value }) => (value ? <span>{value}</span> : null), 49}; 50 51export const mdInlineRenderers: MDRenderers = { 52 ...mdRenderers, 53 paragraph: ({ children }) => (children ? <span>{children}</span> : null), 54}; 55 56const nonLinkableTypes = [ 57 'ColorValue', 58 'E', 59 'EventSubscription', 60 'File', 61 'FileList', 62 'Manifest', 63 'NativeSyntheticEvent', 64 'Omit', 65 'Pick', 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 Promise: 83 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', 84 View: '../../react-native/view', 85 ViewProps: '../../react-native/view#props', 86 ViewStyle: '../../react-native/view-style-props/', 87}; 88 89const renderWithLink = (name: string, type?: string) => 90 nonLinkableTypes.includes(name) ? ( 91 name + (type === 'array' ? '[]' : '') 92 ) : ( 93 <Link href={hardcodedTypeLinks[name] || `#${name.toLowerCase()}`} key={`type-link-${name}`}> 94 {name} 95 {type === 'array' && '[]'} 96 </Link> 97 ); 98 99const renderUnion = (types: TypeDefinitionData[]) => 100 types.map(resolveTypeName).map((valueToRender, index) => ( 101 <span key={`union-type-${index}`}> 102 {valueToRender} 103 {index + 1 !== types.length && ' | '} 104 </span> 105 )); 106 107export const resolveTypeName = ({ 108 elements, 109 elementType, 110 name, 111 type, 112 types, 113 typeArguments, 114 declaration, 115 value, 116 queryType, 117}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => { 118 if (name) { 119 if (type === 'reference') { 120 if (typeArguments) { 121 if (name === 'Record' || name === 'React.ComponentProps') { 122 return ( 123 <> 124 {name}< 125 {typeArguments.map((type, index) => ( 126 <span key={`record-type-${index}`}> 127 {resolveTypeName(type)} 128 {index !== typeArguments.length - 1 ? ', ' : null} 129 </span> 130 ))} 131 > 132 </> 133 ); 134 } else { 135 return ( 136 <> 137 {renderWithLink(name)} 138 < 139 {typeArguments.map((type, index) => ( 140 <span key={`${name}-nested-type-${index}`}> 141 {resolveTypeName(type)} 142 {index !== typeArguments.length - 1 ? ', ' : null} 143 </span> 144 ))} 145 > 146 </> 147 ); 148 } 149 } else { 150 return renderWithLink(name); 151 } 152 } else { 153 return name; 154 } 155 } else if (elementType?.name) { 156 if (elementType.type === 'reference') { 157 return renderWithLink(elementType.name, type); 158 } else if (type === 'array') { 159 return elementType.name + '[]'; 160 } 161 return elementType.name + type; 162 } else if (elementType?.declaration) { 163 if (type === 'array') { 164 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 165 if (parameters && paramType) { 166 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 167 } 168 } 169 return elementType.name + type; 170 } else if (type === 'union' && types?.length) { 171 return renderUnion(types); 172 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 173 const unionTypes = elementType?.types || []; 174 return ( 175 <> 176 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 177 </> 178 ); 179 } else if (declaration?.signatures) { 180 const baseSignature = declaration.signatures[0]; 181 if (baseSignature?.parameters?.length) { 182 return ( 183 <> 184 ( 185 {baseSignature.parameters?.map((param, index) => ( 186 <span key={`param-${index}-${param.name}`}> 187 {param.name}: {resolveTypeName(param.type)} 188 {index + 1 !== baseSignature.parameters?.length && ', '} 189 </span> 190 ))} 191 ) {'=>'} {resolveTypeName(baseSignature.type)} 192 </> 193 ); 194 } else { 195 return ( 196 <> 197 {'() =>'} {resolveTypeName(baseSignature.type)} 198 </> 199 ); 200 } 201 } else if (type === 'reflection' && declaration?.children) { 202 return ( 203 <> 204 {'{ '} 205 {declaration?.children.map((child: PropData, i) => ( 206 <span key={`reflection-${name}-${i}`}> 207 {child.name + ': ' + resolveTypeName(child.type)} 208 {i + 1 !== declaration?.children?.length ? ', ' : null} 209 </span> 210 ))} 211 {' }'} 212 </> 213 ); 214 } else if (type === 'tuple' && elements) { 215 return ( 216 <> 217 [ 218 {elements.map((elem, i) => ( 219 <span key={`tuple-${name}-${i}`}> 220 {resolveTypeName(elem)} 221 {i + 1 !== elements.length ? ', ' : null} 222 </span> 223 ))} 224 ] 225 </> 226 ); 227 } else if (type === 'query' && queryType) { 228 return queryType.name; 229 } else if (type === 'literal' && typeof value === 'boolean') { 230 return `${value}`; 231 } else if (type === 'literal' && value) { 232 return `'${value}'`; 233 } else if (value === null) { 234 return 'null'; 235 } 236 return 'undefined'; 237}; 238 239export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 240 241export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => ( 242 <LI key={`param-${name}`}> 243 <B> 244 {parseParamName(name)} 245 {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>) 246 </B> 247 <CommentTextBlock comment={comment} renderers={mdInlineRenderers} withDash /> 248 </LI> 249); 250 251export const listParams = (parameters: MethodParamData[]) => 252 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 253 254export const renderTypeOrSignatureType = ( 255 type?: TypeDefinitionData, 256 signatures?: MethodSignatureData[], 257 includeParamType: boolean = false 258) => { 259 if (type) { 260 return <InlineCode>{resolveTypeName(type)}</InlineCode>; 261 } else if (signatures && signatures.length) { 262 return signatures.map(({ name, type, parameters }) => ( 263 <InlineCode key={`signature-type-${name}`}> 264 ( 265 {parameters && includeParamType 266 ? parameters.map(param => ( 267 <> 268 {param.name} 269 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 270 </> 271 )) 272 : listParams(parameters)} 273 ) => {resolveTypeName(type)} 274 </InlineCode> 275 )); 276 } 277 return undefined; 278}; 279 280export const renderFlags = (flags?: TypePropertyDataFlags) => 281 flags?.isOptional ? ( 282 <> 283 <br /> 284 <span css={STYLES_OPTIONAL}>(optional)</span> 285 </> 286 ) : undefined; 287 288export type CommentTextBlockProps = { 289 comment?: CommentData; 290 renderers?: MDRenderers; 291 withDash?: boolean; 292 beforeContent?: JSX.Element; 293}; 294 295export const parseCommentContent = (content?: string): string => 296 content && content.length ? content.replace(/*/g, '*') : ''; 297 298export const getCommentOrSignatureComment = ( 299 comment?: CommentData, 300 signatures?: MethodSignatureData[] 301) => comment || (signatures && signatures[0]?.comment); 302 303export const CommentTextBlock: React.FC<CommentTextBlockProps> = ({ 304 comment, 305 renderers = mdRenderers, 306 withDash, 307 beforeContent, 308}) => { 309 const shortText = comment?.shortText?.trim().length ? ( 310 <ReactMarkdown renderers={renderers}>{parseCommentContent(comment.shortText)}</ReactMarkdown> 311 ) : null; 312 const text = comment?.text?.trim().length ? ( 313 <ReactMarkdown renderers={renderers}>{parseCommentContent(comment.text)}</ReactMarkdown> 314 ) : null; 315 316 const example = comment?.tags?.filter(tag => tag.tag === 'example')[0]; 317 const exampleText = example ? ( 318 <ReactMarkdown renderers={renderers}>{`__Example:__ ${example.text}`}</ReactMarkdown> 319 ) : null; 320 321 const deprecation = comment?.tags?.filter(tag => tag.tag === 'deprecated')[0]; 322 const deprecationNote = deprecation ? ( 323 <Quote key="deprecation-note"> 324 {deprecation.text.trim().length ? ( 325 <ReactMarkdown renderers={mdInlineRenderers}>{deprecation.text}</ReactMarkdown> 326 ) : ( 327 <B>Deprecated</B> 328 )} 329 </Quote> 330 ) : null; 331 332 return ( 333 <> 334 {deprecationNote} 335 {beforeContent} 336 {withDash && (shortText || text) ? ' - ' : null} 337 {shortText} 338 {text} 339 {exampleText} 340 </> 341 ); 342}; 343 344export const STYLES_OPTIONAL = css` 345 color: ${theme.text.secondary}; 346 font-size: 90%; 347 padding-top: 22px; 348`; 349 350export const STYLES_SECONDARY = css` 351 color: ${theme.text.secondary}; 352 font-size: 90%; 353 font-weight: 600; 354`; 355