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