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