Move stripCompatibilityFeatures to parser code

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-03 12:39:36 +02:00
parent 63924bcc50
commit e472f87aaa
8 changed files with 37 additions and 40 deletions

View file

@ -13,21 +13,39 @@ const nodesToText = (nodes: Array<DOMNode>): string =>
interface IParsedContent {
html: string;
mentions?: Array<Mention>;
/** Whether it's a status which has a quote. */
hasQuote?: boolean;
}
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions }) => {
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote }) => {
return useMemo(() => {
if (html.length === 0) {
return null;
}
const selectors: Array<string> = [];
// Explicit mentions
if (mentions) selectors.push('recipients-inline');
// Quote posting
if (hasQuote) selectors.push('quote-inline');
const options: HTMLReactParserOptions = {
replace(domNode) {
if (domNode instanceof Element && ['script', 'iframe'].includes(domNode.name)) {
return null;
if (!(domNode instanceof Element)) {
return;
}
if (domNode instanceof Element && domNode.name === 'a') {
if (['script', 'iframe'].includes(domNode.name)) {
return <></>;
}
if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) {
return <></>;
}
if (domNode.name === 'a') {
const classes = domNode.attribs.class?.split(' ');
const fallback = (

View file

@ -176,7 +176,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} />
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
);
}
@ -204,7 +204,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} />
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
);
}

View file

@ -12,7 +12,7 @@ import type { Account, Status } from 'pl-fe/normalizers';
interface IReplyIndicator {
className?: string;
status?: Pick<Status, | 'contentHtml' | 'created_at' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text'> & { account: Pick<Account, 'id'> };
status?: Pick<Status, | 'contentHtml' | 'created_at' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'> & { account: Pick<Account, 'id'> };
onCancel?: () => void;
hideActions: boolean;
}
@ -52,7 +52,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
size='sm'
direction={getTextDirection(status.search_index)}
>
<ParsedContent html={status.contentHtml} mentions={status.mentions} />
<ParsedContent html={status.contentHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>
{status.media_attachments.length > 0 && (

View file

@ -3,6 +3,7 @@ import { FormattedDate, FormattedMessage } from 'react-intl';
import { fetchHistory } from 'pl-fe/actions/history';
import AttachmentThumbs from 'pl-fe/components/attachment-thumbs';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import { HStack, Modal, Spinner, Stack, Text } from 'pl-fe/components/ui';
import { useAppDispatch, useAppSelector } from 'pl-fe/hooks';
@ -18,6 +19,8 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
const loading = useAppSelector(state => state.history.getIn([statusId, 'loading']));
const versions = useAppSelector(state => state.history.get(statusId)?.items);
const status = useAppSelector(state => state.statuses.get(statusId));
const onClickClose = () => {
onClose('COMPARE_HISTORY');
};
@ -34,7 +37,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
body = (
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
{versions?.map((version) => {
const content = { __html: version.contentHtml };
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
const spoilerContent = { __html: version.spoilerHtml };
const poll = typeof version.poll !== 'string' && version.poll;
@ -48,7 +51,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
</>
)}
<div className='status__content' dangerouslySetInnerHTML={content} />
<div className='status__content'>
{content}
</div>
{poll && (
<div className='poll'>

View file

@ -5,7 +5,6 @@ import escapeTextContentForBrowser from 'escape-html';
import DOMPurify from 'isomorphic-dompurify';
import emojify from 'pl-fe/features/emoji';
import { stripCompatibilityFeatures } from 'pl-fe/utils/html';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import { normalizePollEdit } from './poll';
@ -20,7 +19,7 @@ const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => {
return {
...statusEdit,
poll,
contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(statusEdit.content, emojiMap)), { ADD_ATTR: ['target'] }),
contentHtml: DOMPurify.sanitize(emojify(statusEdit.content, emojiMap), { ADD_ATTR: ['target'] }),
spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap), { ADD_ATTR: ['target'] }),
};
};

View file

@ -8,7 +8,7 @@ import DOMPurify from 'isomorphic-dompurify';
import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
import emojify from 'pl-fe/features/emoji';
import { stripCompatibilityFeatures, unescapeHTML } from 'pl-fe/utils/html';
import { unescapeHTML } from 'pl-fe/utils/html';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import { normalizeAccount } from './account';
@ -62,7 +62,7 @@ const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoi
return unescapeHTML(fields.join('\n\n')) || '';
};
const calculateContent = (text: string, emojiMap: any, hasQuote?: boolean) => DOMPurify.sanitize(stripCompatibilityFeatures(emojify(text, emojiMap), hasQuote), { USE_PROFILES: { html: true } });
const calculateContent = (text: string, emojiMap: any, hasQuote?: boolean) => DOMPurify.sanitize(emojify(text, emojiMap), { USE_PROFILES: { html: true } });
const calculateSpoiler = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } });
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {

View file

@ -1,12 +1,11 @@
import emojify from 'pl-fe/features/emoji';
import { stripCompatibilityFeatures } from 'pl-fe/utils/html';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import type { Status, Translation as BaseTranslation } from 'pl-api';
const normalizeTranslation = (translation: BaseTranslation, status: Pick<Status, 'emojis'>) => {
const emojiMap = makeEmojiMap(status.emojis);
const content = stripCompatibilityFeatures(emojify(translation.content, emojiMap));
const content = emojify(translation.content, emojiMap);
return {
...translation,

View file

@ -6,29 +6,6 @@ const unescapeHTML = (html: string = ''): string => {
return wrapper.textContent || '';
};
/** Remove compatibility markup for features pl-fe supports. */
const stripCompatibilityFeatures = (html: string, hasQuote = true): string => {
const node = document.createElement('div');
node.innerHTML = html;
const selectors = [
// Explicit mentions
'.recipients-inline',
];
// Quote posting
if (hasQuote) selectors.push('.quote-inline');
// Remove all instances of all selectors
selectors.forEach(selector => {
node.querySelectorAll(selector).forEach(elem => {
elem.remove();
});
});
return node.innerHTML;
};
/** Convert HTML to plaintext. */
// https://stackoverflow.com/a/822486
const stripHTML = (html: string) => {
@ -39,6 +16,5 @@ const stripHTML = (html: string) => {
export {
unescapeHTML,
stripCompatibilityFeatures,
stripHTML,
};