Move stripCompatibilityFeatures to parser code
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
63924bcc50
commit
e472f87aaa
8 changed files with 37 additions and 40 deletions
|
@ -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 = (
|
||||
|
|
|
@ -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>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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'] }),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue