StatusContent: render links with html-react-parser

environments/review-html-parse-ic2vy5/deployments/4174
Alex Gleason 2023-10-13 22:07:57 -05:00
rodzic a37fa8bb85
commit 70dc4caeb9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
3 zmienionych plików z 119 dodań i 72 usunięć

Wyświetl plik

@ -110,6 +110,7 @@
"escape-html": "^1.0.3",
"exifr": "^7.1.3",
"graphemesplit": "^2.4.4",
"html-react-parser": "^4.2.2",
"http-link-header": "^1.0.2",
"immer": "^10.0.0",
"immutable": "^4.2.1",

Wyświetl plik

@ -1,18 +1,20 @@
import clsx from 'clsx';
import parse, { Element, type HTMLReactParserOptions, domToReact, Text } from 'html-react-parser';
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import Icon from 'soapbox/components/icon';
import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content';
import { getTextDirection } from '../utils/rtl';
import Link from './link';
import Markup from './markup';
import Mention from './mention';
import Poll from './polls/poll';
import type { Sizes } from 'soapbox/components/ui/text/text';
import type { Status, Mention } from 'soapbox/types/entities';
import type { Status } from 'soapbox/types/entities';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
const BIG_EMOJI_LIMIT = 10;
@ -45,66 +47,11 @@ const StatusContent: React.FC<IStatusContent> = ({
translatable,
textSize = 'md',
}) => {
const history = useHistory();
const [collapsed, setCollapsed] = useState(false);
const [onlyEmoji, setOnlyEmoji] = useState(false);
const node = useRef<HTMLDivElement>(null);
const onMentionClick = (mention: Mention, e: MouseEvent) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/@${mention.acct}`);
}
};
const onHashtagClick = (hashtag: string, e: MouseEvent) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/tags/${hashtag}`);
}
};
/** For regular links, just stop propogation */
const onLinkClick = (e: MouseEvent) => {
e.stopPropagation();
};
const updateStatusLinks = () => {
if (!node.current) return;
const links = node.current.querySelectorAll('a');
links.forEach(link => {
// Skip already processed
if (link.classList.contains('status-link')) return;
// Add attributes
link.classList.add('status-link');
link.setAttribute('rel', 'nofollow noopener');
link.setAttribute('target', '_blank');
const mention = status.mentions.find(mention => link.href === `${mention.url}`);
// Add event listeners on mentions and hashtags
if (mention) {
link.addEventListener('click', onMentionClick.bind(link, mention), false);
link.setAttribute('title', mention.acct);
link.setAttribute('dir', 'ltr');
} else if (link.textContent?.charAt(0) === '#' || (link.previousSibling?.textContent?.charAt(link.previousSibling.textContent.length - 1) === '#')) {
link.addEventListener('click', onHashtagClick.bind(link, link.text), false);
} else {
link.setAttribute('title', link.href);
link.addEventListener('click', onLinkClick.bind(link), false);
}
});
};
const maybeSetCollapsed = (): void => {
if (!node.current) return;
@ -127,7 +74,6 @@ const StatusContent: React.FC<IStatusContent> = ({
useLayoutEffect(() => {
maybeSetCollapsed();
maybeSetOnlyEmoji();
updateStatusLinks();
});
const parsedHtml = useMemo((): string => {
@ -142,7 +88,53 @@ const StatusContent: React.FC<IStatusContent> = ({
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
const content = { __html: parsedHtml };
const options: HTMLReactParserOptions = {
replace(domNode) {
if (domNode instanceof Element && ['script', 'iframe'].includes(domNode.name)) {
return null;
}
if (domNode instanceof Element && domNode.name === 'a') {
const classes = domNode.attribs.class?.split(' ');
if (classes?.includes('mention')) {
const mention = status.mentions.find(({ url }) => domNode.attribs.href === url);
if (mention) {
return <Mention mention={mention} />;
}
}
if (classes?.includes('hashtag')) {
const child = domNode.children[0];
const hashtag = child instanceof Text ? child.data.replace(/^#/, '') : '';
if (hashtag) {
return (
<Link to={`/tags/${hashtag}`}>
{domToReact(domNode.children, options)}
</Link>
);
}
}
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<a
{...domNode.attribs}
onClick={(e) => e.stopPropagation()}
rel='nofollow noopener'
target='_blank'
title={domNode.attribs.href}
>
{domToReact(domNode.children, options)}
</a>
);
}
},
};
const content = parse(parsedHtml, options);
const direction = getTextDirection(status.search_index);
const className = clsx(baseClassName, {
'cursor-pointer': onClick,
@ -159,10 +151,11 @@ const StatusContent: React.FC<IStatusContent> = ({
key='content'
className={className}
direction={direction}
dangerouslySetInnerHTML={content}
lang={status.language || undefined}
size={textSize}
/>,
>
{content}
</Markup>,
];
if (collapsed) {
@ -185,10 +178,11 @@ const StatusContent: React.FC<IStatusContent> = ({
'leading-normal big-emoji': onlyEmoji,
})}
direction={direction}
dangerouslySetInnerHTML={content}
lang={status.language || undefined}
size={textSize}
/>,
>
{content}
</Markup>,
];
if (status.poll && typeof status.poll === 'string') {

Wyświetl plik

@ -4027,6 +4027,13 @@ domexception@^4.0.0:
dependencies:
webidl-conversions "^7.0.0"
domhandler@5.0.3, domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
@ -4034,13 +4041,6 @@ domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
@ -4050,7 +4050,7 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
domutils@^3.0.1:
domutils@^3.0.1, domutils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
@ -4148,7 +4148,7 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@^4.2.0, entities@^4.4.0:
entities@^4.2.0, entities@^4.4.0, entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@ -5079,6 +5079,14 @@ hosted-git-info@^4.0.1:
dependencies:
lru-cache "^6.0.0"
html-dom-parser@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-4.0.0.tgz#dc382fbbc9306f8c9b5aae4e3f2822e113a48709"
integrity sha512-TUa3wIwi80f5NF8CVWzkopBVqVAtlawUzJoLwVLHns0XSJGynss4jiY0mTWpiDOsuyw+afP+ujjMgRh9CoZcXw==
dependencies:
domhandler "5.0.3"
htmlparser2 "9.0.0"
html-encoding-sniffer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
@ -5099,11 +5107,31 @@ html-minifier-terser@^6.1.0:
relateurl "^0.2.7"
terser "^5.10.0"
html-react-parser@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-4.2.2.tgz#91f1cc2138bc069d65cbd8b9d97b1e71ed423300"
integrity sha512-lh0wEGISnFZEAmvQqK4xc0duFMUh/m9YYyAhFursWxdtNv+hCZge0kj1y4wep6qPB5Zm33L+2/P6TcGWAJJbjA==
dependencies:
domhandler "5.0.3"
html-dom-parser "4.0.0"
react-property "2.0.0"
style-to-js "1.1.4"
html-tags@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
htmlparser2@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.0.0.tgz#e431142b7eeb1d91672742dea48af8ac7140cddb"
integrity sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.1.0"
entities "^4.5.0"
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
@ -5232,6 +5260,11 @@ ini@^1.3.5:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inline-style-parser@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
internal-slot@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986"
@ -7275,6 +7308,11 @@ react-popper@^2.2.5, react-popper@^2.3.0:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-property@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
integrity sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==
react-redux@^8.0.0:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
@ -8103,6 +8141,20 @@ style-search@^0.1.0:
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
style-to-js@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.4.tgz#5fa07a181ec3ca354d699edf442549e0ac61ed32"
integrity sha512-zEeU3vy9xL/hdLBFmzqjhm+2vJ1Y35V0ctDeB2sddsvN1856OdMZUCOOfKUn3nOjjEKr6uLhOnY4CrX6gLDRrA==
dependencies:
style-to-object "0.4.2"
style-to-object@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.2.tgz#a8247057111dea8bd3b8a1a66d2d0c9cf9218a54"
integrity sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==
dependencies:
inline-style-parser "0.1.1"
stylehacks@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738"