import React from 'react'; import type { ReactNode, ReactElement } from 'react'; import { Text, View } from 'react-native'; import getUniqueID from './util/getUniqueID'; import type { ASTNode, RenderRules, RenderFunction, MarkdownStyles, AstRendererOptions } from '../types'; export function rootRenderRule(children: ReactNode[], styles: MarkdownStyles): ReactElement { return ( {children} ); } const DEFAULT_ALLOWED_IMAGE_HANDLERS = [ 'data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://', ]; const DEFAULT_IMAGE_HANDLER = 'http://'; export default class AstRenderer { private _renderRules: RenderRules; private _style: MarkdownStyles; private _onLinkPress?: (url: string) => boolean | void; private _debugPrintTree: boolean; private _maxTopLevelChildren: number | null; private _topLevelMaxExceededItem: ReactNode; private _allowedImageHandlers: string[]; private _defaultImageHandler: string | null; constructor(renderRules: RenderRules, style?: MarkdownStyles, options?: AstRendererOptions) { this._renderRules = renderRules; this._style = style || {}; this._onLinkPress = options?.onLinkPress; this._debugPrintTree = options?.debugPrintTree ?? false; this._maxTopLevelChildren = options?.maxTopLevelChildren ?? null; this._topLevelMaxExceededItem = options?.topLevelMaxExceededItem ?? ( ... ); this._allowedImageHandlers = options?.allowedImageHandlers ?? DEFAULT_ALLOWED_IMAGE_HANDLERS; this._defaultImageHandler = options?.defaultImageHandler ?? DEFAULT_IMAGE_HANDLER; } getRenderFunction = (type: string): RenderFunction => { const renderFunction = this._renderRules[type]; if (!renderFunction) { const fallback = this._renderRules['unknown']; if (!fallback) { throw new Error( `${type} renderRule not defined and no "unknown" fallback rule exists. Add a rule via ` ); } return fallback; } return renderFunction; }; renderNode = (node: ASTNode, parentNodes: ASTNode[]): ReactNode => { if (this._debugPrintTree) { const prefix = '-'.repeat(parentNodes.length); console.log(`${prefix}${node.type}`); } const renderFunction = this.getRenderFunction(node.type); const parents = [...parentNodes]; parents.unshift(node); if (node.type === 'text') { return renderFunction(node, [], parentNodes, this._style); } const children = node.children.map((value) => { return this.renderNode(value, parents); }); if (node.type === 'link' || node.type === 'blocklink') { return renderFunction(node, children, parentNodes, this._style, this._onLinkPress); } if (node.type === 'image') { return renderFunction( node, children, parentNodes, this._style, this._allowedImageHandlers, this._defaultImageHandler ); } return renderFunction(node, children, parentNodes, this._style); }; render = (nodes: ASTNode[]): ReactElement => { let children = nodes.map((value) => this.renderNode(value, [])); if (this._maxTopLevelChildren != null && children.length > this._maxTopLevelChildren) { children = children.slice(0, this._maxTopLevelChildren); children.push(this._topLevelMaxExceededItem); } return rootRenderRule(children, this._style); }; }