/* eslint-disable jsx-a11y/alt-text */ import React, { ReactEventHandler, Suspense, lazy } from 'react'; import parse, { Element, Text as DOMText, HTMLReactParserOptions, attributesToProps, domToReact, } from 'html-react-parser'; import { MatrixClient, Room } from 'matrix-js-sdk'; import classNames from 'classnames'; import { Scroll, Text } from 'folds'; import { Opts as LinkifyOpts } from 'linkifyjs'; import Linkify from 'linkify-react'; import { ErrorBoundary } from 'react-error-boundary'; import * as css from '../styles/CustomHtml.css'; import { getMxIdLocalPart, getRoomWithCanonicalAlias } from '../utils/matrix'; import { getMemberDisplayName } from '../utils/room'; import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex'; import { sanitizeText } from '../utils/sanitize'; import { getHexcodeForEmoji, getShortcodeFor } from './emoji'; const ReactPrism = lazy(() => import('./react-prism/ReactPrism')); const EMOJI_REG = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g'); export const LINKIFY_OPTS: LinkifyOpts = { attributes: { target: '_blank', rel: 'noreferrer noopener', }, validate: { url: (value) => /^(https|http|ftp|mailto|magnet)?:/.test(value), }, ignoreTags: ['span'], }; const emojifyParserOptions: HTMLReactParserOptions = { replace: (domNode) => { if (domNode instanceof DOMText) { return {domNode.data}; } return undefined; }, }; export const emojifyAndLinkify = (unsafeText: string, linkify?: boolean) => { const emojifyHtml = sanitizeText(unsafeText).replace( EMOJI_REG, (emoji) => `${emoji}` ); return <>{parse(emojifyHtml, linkify ? emojifyParserOptions : undefined)}; }; export const getReactCustomHtmlParser = ( mx: MatrixClient, room: Room, params: { handleSpoilerClick?: ReactEventHandler; handleMentionClick?: ReactEventHandler; } ): HTMLReactParserOptions => { const opts: HTMLReactParserOptions = { replace: (domNode) => { if (domNode instanceof Element && 'name' in domNode) { const { name, attribs, children, parent } = domNode; const props = attributesToProps(attribs); if (name === 'h1') { return ( {domToReact(children, opts)} ); } if (name === 'h2') { return ( {domToReact(children, opts)} ); } if (name === 'h3') { return ( {domToReact(children, opts)} ); } if (name === 'h4') { return ( {domToReact(children, opts)} ); } if (name === 'h5') { return ( {domToReact(children, opts)} ); } if (name === 'h6') { return ( {domToReact(children, opts)} ); } if (name === 'p') { return ( {domToReact(children, opts)} ); } if (name === 'pre') { return (
{domToReact(children, opts)}
); } if (name === 'blockquote') { return ( {domToReact(children, opts)} ); } if (name === 'ul') { return ( ); } if (name === 'ol') { return (
    {domToReact(children, opts)}
); } if (name === 'code') { if (parent && 'name' in parent && parent.name === 'pre') { const codeReact = domToReact(children, opts); if (typeof codeReact === 'string') { let lang = props.className; if (lang === 'language-rs') lang = 'language-rust'; return ( {codeReact}}> {codeReact}}> {(ref) => ( {codeReact} )} ); } } else { return ( {domToReact(children, opts)} ); } } if (name === 'a') { const mention = decodeURIComponent(props.href).match( /^https?:\/\/matrix.to\/#\/((@|#|!).+:[^?/]+)/ ); if (mention) { // convert mention link to pill const mentionId = mention[1]; const mentionPrefix = mention[2]; if (mentionPrefix === '#' || mentionPrefix === '!') { const mentionRoom = mentionPrefix === '#' ? getRoomWithCanonicalAlias(mx, mentionId) : mx.getRoom(mentionId); return ( {domToReact(children, opts)} ); } if (mentionPrefix === '@') return ( {`@${getMemberDisplayName(room, mentionId) ?? getMxIdLocalPart(mentionId)}`} ); } } if (name === 'span' && 'data-mx-spoiler' in props) { return ( {domToReact(children, opts)} ); } if (name === 'img') { const htmlSrc = mx.mxcUrlToHttp(props.src); if (htmlSrc && props.src.startsWith('mxc://') === false) { return ( {props.alt || props.title || htmlSrc} ); } if (htmlSrc && 'data-mx-emoticon' in props) { return ( ); } if (htmlSrc) return ; } } if (domNode instanceof DOMText) { const linkify = !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'code') && !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'a'); return emojifyAndLinkify(domNode.data, linkify); } return undefined; }, }; return opts; };