import { EyeOutlined } from '@ant-design/icons'; import { sanitizeUrl } from '@braintree/sanitize-url'; import { Checkbox, Image, Typography } from 'antd'; import { unescape } from 'lodash'; import { TokensList, marked } from 'marked'; import React, { Fragment, useCallback, useEffect } from 'react'; import HighlightCode from '../highlight-code'; import './index.less'; const { Text, Link, Paragraph } = Typography; interface MarkdownViewerProps { content: string; height?: string; theme?: 'light' | 'dark'; generateImgLink?: (src: string) => string; } const MarkdownViewer: React.FC = ({ content, generateImgLink, height = 'auto', theme = 'light' }) => { const renderer = new marked.Renderer(); const tokens = marked.lexer(content); const reDefineTypes = [ 'code', 'link', 'hr', 'heading', 'paragraph', 'codespan', 'strong', 'text', 'image', 'em', 'list', 'list_item', 'br', 'html', 'escape', 'del', 'blockquote', 'checkbox' // 'bibtex' ]; const isValidURL = useCallback((url: string) => { const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/; return pattern.test(url); }, []); const generateImgSrc = useCallback( (url: string | null) => { const src = sanitizeUrl(url || ''); if (!src) { return ''; } if (generateImgLink) { return isValidURL(src) ? src : generateImgLink(src); } return src; }, [generateImgLink] ); const renderItem = useCallback( (token: any, render: any) => { if (!reDefineTypes.includes(token.type)) { return ( ); } let htmlstr: any = null; let child: any = null; if (token.tokens?.length) { child = render?.(token.tokens as TokensList, render); } const text = child ? child : unescape(token.text); if (token.type === 'escape') { htmlstr = text; } if (token.type === 'html') { htmlstr =
; } if (token.type === 'list') { htmlstr = token.order ? (
    {render?.(token.items, render)}
) : ( ); } if (token.type === 'list_item') { htmlstr =
  • {text}
  • ; } if (token.type === 'del') { htmlstr = {text}; } if (token.type === 'blockquote') { htmlstr =
    {text}
    ; } if (token.type === 'checkbox') { htmlstr = ; } if (token.type === 'br') { htmlstr =
    ; } if (token.type === 'em') { htmlstr = {text}; } if (token.type === 'image') { let href = generateImgSrc(token.href); htmlstr = ( {token.text} }} /> ); } if (token.type === 'text') { htmlstr = text; } if (token.type === 'codespan') { htmlstr = {text}; } if (token.type === 'strong') { htmlstr = {text}; } if (token.type === 'heading') { htmlstr = {text}; } if (token.type === 'paragraph') { htmlstr = {text}; } if (token.type === 'code') { htmlstr = ; } if (token.type === 'link') { htmlstr = ( {text} ); } if (token.type === 'hr') { htmlstr =
    ; } return htmlstr; }, [generateImgSrc] ); const renderTokens = (tokens: TokensList): any => { return tokens?.map((token: any, index: number) => { return {renderItem(token, renderTokens)}; }); }; useEffect(() => { if (!content) { return; } const imgs = document.querySelectorAll('.markdown-viewer img'); const links = document.querySelectorAll('.markdown-viewer a'); links.forEach((link) => { // set target blank for all links link.setAttribute('target', '_blank'); }); imgs.forEach((img) => { const src = img.getAttribute('src'); img.setAttribute('src', generateImgSrc(src)); }); }, [content, generateImgSrc]); return ( <>
    {renderTokens(tokens)}
    ); }; export default React.memo(MarkdownViewer);