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