import React from 'react'; import { Text, TouchableWithoutFeedback, View, Image } from 'react-native'; import type { ReactNode } from 'react'; import openUrl from './util/openUrl'; import hasParents from './util/hasParents'; import applyStyle from './util/applyStyle'; import type { ASTNode, RenderRules, MarkdownStyles } from '../types'; function trimTrailingNewline(content: string): string { if (content.length > 0 && content.charAt(content.length - 1) === '\n') { return content.substring(0, content.length - 1); } return content; } const renderRules: RenderRules = { unknown: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {node.type} ); }, textgroup: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, inline: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return {children}; }, text: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return {node.content}; }, span: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return {children}; }, strong: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, s: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, link: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles, ...args: unknown[]) => { const onLinkPress = args[0] as ((url: string) => boolean | void) | undefined; const handlePress = () => { const url = node.attributes.href; if (onLinkPress) { onLinkPress(url); } else { openUrl(url); } }; return ( {children} ); }, blocklink: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles, ...args: unknown[]) => { const onLinkPress = args[0] as ((url: string) => boolean | void) | undefined; const handlePress = () => { const url = node.attributes.href; if (onLinkPress) { onLinkPress(url); } else { openUrl(url); } }; return ( {children} ); }, em: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, heading1: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {applyStyle(children as any, [styles.heading, styles.heading1], 'Text')} ); }, heading2: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {applyStyle(children as any, [styles.heading, styles.heading2], 'Text')} ); }, heading3: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {applyStyle(children as any, [styles.heading, styles.heading3], 'Text')} ), heading4: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {applyStyle(children as any, [styles.heading, styles.heading4], 'Text')} ), heading5: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {applyStyle(children as any, [styles.heading, styles.heading5], 'Text')} ), heading6: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {applyStyle(children as any, [styles.heading, styles.heading6], 'Text')} ), paragraph: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), hardbreak: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {'\n'} ), blockquote: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), code_inline: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {node.content} ); }, code_block: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {trimTrailingNewline(node.content)} ); }, fence: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {trimTrailingNewline(node.content)} ); }, pre: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), bullet_list: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, ordered_list: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, list_item: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { if (hasParents(parent, 'bullet_list')) { return ( {'\u00B7'} {children} ); } if (hasParents(parent, 'ordered_list')) { const orderedListParent = parent.find((el) => el.type === 'ordered_list'); const startNumber = orderedListParent?.attributes?.start ? parseInt(orderedListParent.attributes.start, 10) : 1; const listItemNumber = startNumber + node.index; return ( {listItemNumber} {node.markup} {children} ); } return ( {children} ); }, table: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), thead: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), tbody: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {children} ), th: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, tr: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, td: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, hr: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ; }, softbreak: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => ( {'\n'} ), image: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles, ...args: unknown[]) => { const allowedImageHandlers = (args[0] as string[] | undefined) ?? [ 'data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://', ]; const defaultImageHandler = args.length > 1 ? (args[1] as string | null) : 'http://'; let { src } = node.attributes; const isAllowed = allowedImageHandlers.some((handler) => src.startsWith(handler)); if (!isAllowed) { if (defaultImageHandler == null) { return null; } src = `${defaultImageHandler}${src}`; } return ( ); }, html_block: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {node.content} ); }, html_inline: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {node.content} ); }, u: (node: ASTNode, children: ReactNode[], parent: ASTNode[], styles: MarkdownStyles) => { return ( {children} ); }, }; export default renderRules;