Merge pull request #295 from answerdev/feat/1.0.8/ui

fix: use htmlRender control rich-text style and delet dompurify html-…
This commit is contained in:
dashuai 2023-03-29 15:40:58 +08:00 committed by GitHub
commit 8b4e0c928e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 98 additions and 179 deletions

View File

@ -22,9 +22,7 @@
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"diff": "^5.1.0",
"dompurify": "^2.4.3",
"emoji-regex": "^10.2.1",
"html-react-parser": "^3.0.8",
"i18next": "^21.9.0",
"katex": "^0.16.2",
"lodash": "^4.17.21",

View File

@ -29,7 +29,6 @@ specifiers:
customize-cra: ^1.0.0
dayjs: ^1.11.5
diff: ^5.1.0
dompurify: ^2.4.3
emoji-regex: ^10.2.1
eslint: ^8.0.1
eslint-config-airbnb: ^19.0.4
@ -43,7 +42,6 @@ specifiers:
eslint-plugin-promise: ^6.0.0
eslint-plugin-react: ^7.30.1
eslint-plugin-react-hooks: ^4.6.0
html-react-parser: ^3.0.8
husky: ^8.0.1
i18next: ^21.9.0
katex: ^0.16.2
@ -83,9 +81,7 @@ dependencies:
copy-to-clipboard: 3.3.2
dayjs: 1.11.5
diff: 5.1.0
dompurify: registry.npmjs.org/dompurify/2.4.3
emoji-regex: 10.2.1
html-react-parser: registry.npmjs.org/html-react-parser/3.0.8_react@18.2.0
i18next: 21.9.2
katex: 0.16.2
lodash: 4.17.21
@ -11303,16 +11299,6 @@ packages:
domhandler: registry.npmjs.org/domhandler/4.3.1
entities: registry.npmjs.org/entities/2.2.0
registry.npmjs.org/dom-serializer/2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz}
name: dom-serializer
version: 2.0.0
dependencies:
domelementtype: registry.npmjs.org/domelementtype/2.3.0
domhandler: registry.npmjs.org/domhandler/5.0.3
entities: registry.npmjs.org/entities/4.4.0
dev: false
registry.npmjs.org/domelementtype/1.3.1:
resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz}
name: domelementtype
@ -11331,27 +11317,12 @@ packages:
dependencies:
domelementtype: registry.npmjs.org/domelementtype/2.3.0
registry.npmjs.org/domhandler/5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz}
name: domhandler
version: 5.0.3
engines: {node: '>= 4'}
dependencies:
domelementtype: registry.npmjs.org/domelementtype/2.3.0
dev: false
registry.npmjs.org/dompurify/2.4.0:
resolution: {integrity: sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz}
name: dompurify
version: 2.4.0
dev: false
registry.npmjs.org/dompurify/2.4.3:
resolution: {integrity: sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz}
name: dompurify
version: 2.4.3
dev: false
registry.npmjs.org/domutils/1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz}
name: domutils
@ -11369,52 +11340,11 @@ packages:
domelementtype: registry.npmjs.org/domelementtype/2.3.0
domhandler: registry.npmjs.org/domhandler/4.3.1
registry.npmjs.org/domutils/3.0.1:
resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz}
name: domutils
version: 3.0.1
dependencies:
dom-serializer: registry.npmjs.org/dom-serializer/2.0.0
domelementtype: registry.npmjs.org/domelementtype/2.3.0
domhandler: registry.npmjs.org/domhandler/5.0.3
dev: false
registry.npmjs.org/entities/2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/entities/-/entities-2.2.0.tgz}
name: entities
version: 2.2.0
registry.npmjs.org/entities/4.4.0:
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/entities/-/entities-4.4.0.tgz}
name: entities
version: 4.4.0
engines: {node: '>=0.12'}
dev: false
registry.npmjs.org/html-dom-parser/3.1.3:
resolution: {integrity: sha512-fI0yyNlIeSboxU+jnrA4v8qj4+M8SI9/q6AKYdwCY2qki22UtKCDTxvagHniECu7sa5/o2zFRdLleA67035lsA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-3.1.3.tgz}
name: html-dom-parser
version: 3.1.3
dependencies:
domhandler: registry.npmjs.org/domhandler/5.0.3
htmlparser2: registry.npmjs.org/htmlparser2/8.0.1
dev: false
registry.npmjs.org/html-react-parser/3.0.8_react@18.2.0:
resolution: {integrity: sha512-eIxPq/3Ja3+nZkx6X/BNpFy0lNuW+v3V4nzABzuL8KkRVdYY/2KyXO42epKonsEVVRSBxm2zsxWcOkT6fYL+iQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.8.tgz}
id: registry.npmjs.org/html-react-parser/3.0.8
name: html-react-parser
version: 3.0.8
peerDependencies:
react: 0.14 || 15 || 16 || 17 || 18
dependencies:
domhandler: registry.npmjs.org/domhandler/5.0.3
html-dom-parser: registry.npmjs.org/html-dom-parser/3.1.3
react: 18.2.0
react-property: registry.npmjs.org/react-property/2.0.0
style-to-js: registry.npmjs.org/style-to-js/1.1.3
dev: false
registry.npmjs.org/htmlparser2/6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz}
name: htmlparser2
@ -11424,42 +11354,3 @@ packages:
domhandler: registry.npmjs.org/domhandler/4.3.1
domutils: registry.npmjs.org/domutils/2.8.0
entities: registry.npmjs.org/entities/2.2.0
registry.npmjs.org/htmlparser2/8.0.1:
resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz}
name: htmlparser2
version: 8.0.1
dependencies:
domelementtype: registry.npmjs.org/domelementtype/2.3.0
domhandler: registry.npmjs.org/domhandler/5.0.3
domutils: registry.npmjs.org/domutils/3.0.1
entities: registry.npmjs.org/entities/4.4.0
dev: false
registry.npmjs.org/inline-style-parser/0.1.1:
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz}
name: inline-style-parser
version: 0.1.1
dev: false
registry.npmjs.org/react-property/2.0.0:
resolution: {integrity: sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz}
name: react-property
version: 2.0.0
dev: false
registry.npmjs.org/style-to-js/1.1.3:
resolution: {integrity: sha512-zKI5gN/zb7LS/Vm0eUwjmjrXWw8IMtyA8aPBJZdYiQTXj4+wQ3IucOLIOnF7zCHxvW8UhIGh/uZh/t9zEHXNTQ==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz}
name: style-to-js
version: 1.1.3
dependencies:
style-to-object: registry.npmjs.org/style-to-object/0.4.1
dev: false
registry.npmjs.org/style-to-object/0.4.1:
resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==, registry: https://registry.yarnpkg.com/, tarball: https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz}
name: style-to-object
version: 0.4.1
dependencies:
inline-style-parser: registry.npmjs.org/inline-style-parser/0.1.1
dev: false

View File

@ -8,7 +8,6 @@ import {
} from 'react';
import { markdownToHtml } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from './utils';
@ -51,9 +50,9 @@ const Index = ({ value }, ref) => {
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
{htmlToReact(html)}
</div>
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
};

View File

@ -82,9 +82,11 @@ export function htmlRender(el: HTMLElement | null) {
if (!el) return;
// Replace all br tags with newlines
// Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors.
el.querySelectorAll('br').forEach((br) => {
br.parentNode?.insertBefore(document.createTextNode('\n'), br);
br.parentNode?.removeChild(br);
el.querySelectorAll('p').forEach((p) => {
if (p.innerHTML.startsWith('$$') && p.innerHTML.endsWith('$$')) {
const str = p.innerHTML.replace(/<br>/g, '\n');
p.innerHTML = str;
}
});
import('mermaid').then(({ default: mermaid }) => {
@ -106,6 +108,7 @@ export function htmlRender(el: HTMLElement | null) {
render(el, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$$<br>', right: '<br>$$', display: true },
{
left: '\\begin{equation}',
right: '\\end{equation}',
@ -121,8 +124,21 @@ export function htmlRender(el: HTMLElement | null) {
},
);
// remove change table style to htmlToReact function
/**
* @description: You modify the DOM with other scripts after React has rendered the DOM. This way, on the next render cycle (re-render), React cannot find the DOM node it rendered before, because it has been modified or removed by other scripts.
*/
// change table style
el.querySelectorAll('table').forEach((table) => {
if (
(table.parentNode as HTMLDivElement)?.classList.contains(
'table-responsive',
)
) {
return;
}
table.classList.add('table', 'table-bordered');
const div = document.createElement('div');
div.className = 'table-responsive';
table.parentNode?.replaceChild(div, table);
div.appendChild(table);
});
}

View File

@ -1,9 +1,9 @@
import { FC } from 'react';
import { FC, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { useLegalPrivacy } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
@ -13,6 +13,15 @@ const Index: FC = () => {
const { data: privacy } = useLegalPrivacy();
const contentText = privacy?.privacy_policy_original_text;
let matchUrl: URL | undefined;
useEffect(() => {
const fmt = document.querySelector('.fmt') as HTMLElement;
if (!fmt) {
return;
}
htmlRender(fmt);
}, [privacy?.privacy_policy_parsed_text]);
try {
if (contentText) {
matchUrl = new URL(contentText);
@ -26,9 +35,12 @@ const Index: FC = () => {
return (
<>
<h3 className="mb-4">{t('privacy')}</h3>
<div className="fmt">
{htmlToReact(privacy?.privacy_policy_parsed_text || '')}
</div>
<div
className="fmt"
dangerouslySetInnerHTML={{
__html: privacy?.privacy_policy_parsed_text || '',
}}
/>
</>
);
};

View File

@ -1,9 +1,9 @@
import { FC } from 'react';
import { FC, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { useLegalTos } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
@ -13,6 +13,15 @@ const Index: FC = () => {
const { data: tos } = useLegalTos();
const contentText = tos?.terms_of_service_original_text;
let matchUrl: URL | undefined;
useEffect(() => {
const fmt = document.querySelector('.fmt') as HTMLElement;
if (!fmt) {
return;
}
htmlRender(fmt);
}, [tos?.terms_of_service_parsed_text]);
try {
if (contentText) {
matchUrl = new URL(contentText);
@ -23,12 +32,16 @@ const Index: FC = () => {
window.location.replace(matchUrl.toString());
return null;
}
return (
<div>
<h3 className="mb-4">{t('tos')}</h3>
<div className="fmt">
{htmlToReact(tos?.terms_of_service_parsed_text || '')}
</div>
<div
className="fmt"
dangerouslySetInnerHTML={{
__html: tos?.terms_of_service_parsed_text || '',
}}
/>
</div>
);
};

View File

@ -12,7 +12,7 @@ import {
FormatTime,
htmlRender,
} from '@/components';
import { scrollToElementTop, bgFadeOut, htmlToReact } from '@/utils';
import { scrollToElementTop, bgFadeOut } from '@/utils';
import { AnswerItem } from '@/common/interface';
import { acceptanceAnswer } from '@/services';
@ -84,9 +84,10 @@ const Index: FC<Props> = ({
</Badge>
</div>
)}
<article className="fmt text-break text-wrap">
{htmlToReact(data?.html)}
</article>
<article
className="fmt text-break text-wrap"
dangerouslySetInnerHTML={{ __html: data?.html }}
/>
<div className="d-flex align-items-center mt-4">
<Actions
source="answer"

View File

@ -12,7 +12,7 @@ import {
FormatTime,
htmlRender,
} from '@/components';
import { formatCount, guard, htmlToReact } from '@/utils';
import { formatCount, guard } from '@/utils';
import { following } from '@/services';
import { pathFactory } from '@/router/pathFactory';
@ -106,9 +106,11 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
return <Tag className="m-1" key={item.slug_name} data={item} />;
})}
</div>
<article ref={ref} className="fmt text-break text-wrap mt-4">
{htmlToReact(data?.html)}
</article>
<article
ref={ref}
className="fmt text-break text-wrap mt-4"
dangerouslySetInnerHTML={{ __html: data?.html }}
/>
<Actions
className="mt-4"

View File

@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import classNames from 'classnames';
import { handleFormError, scrollToDocTop, htmlToReact } from '@/utils';
import { handleFormError, scrollToDocTop } from '@/utils';
import { usePageTags, usePromptWithUnload } from '@/hooks';
import { pathFactory } from '@/router/pathFactory';
import { Editor, EditorRef, Icon } from '@/components';
import { Editor, EditorRef, Icon, htmlRender } from '@/components';
import type * as Type from '@/common/interface';
import {
useQueryAnswerInfo,
@ -73,6 +73,13 @@ const Index = () => {
const questionContentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!questionContentRef?.current) {
return;
}
htmlRender(questionContentRef.current);
}, [questionContentRef]);
usePromptWithUnload({
when: contentChanged,
});
@ -195,9 +202,9 @@ const Index = () => {
<div className="question-content-wrap">
<div
ref={questionContentRef}
className="content position-absolute top-0 w-100">
{htmlToReact(data?.question.html)}
</div>
className="content position-absolute top-0 w-100"
dangerouslySetInnerHTML={{ __html: data?.question.html }}
/>
<div
className="resize-bottom"
style={{ maxHeight: questionContentRef?.current?.scrollHeight }}

View File

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { usePageTags } from '@/hooks';
import { Tag, TagSelector, FormatTime, Modal } from '@/components';
import { Tag, TagSelector, FormatTime, Modal, htmlRender } from '@/components';
import {
useTagInfo,
useQuerySynonymsTags,
@ -16,7 +16,6 @@ import {
} from '@/services';
import { pathFactory } from '@/router/pathFactory';
import { loggedUserInfoStore, toastStore } from '@/stores';
import { htmlToReact } from '@/utils';
const TagIntroduction = () => {
const userInfo = loggedUserInfoStore((state) => state.user);
@ -45,6 +44,15 @@ const TagIntroduction = () => {
});
}
}, [locationState]);
useEffect(() => {
const fmt = document.querySelector('.content.fmt') as HTMLElement;
if (!fmt) {
return;
}
htmlRender(fmt);
}, [tagInfo?.parsed_text]);
if (!tagInfo) {
return null;
}
@ -145,9 +153,10 @@ const TagIntroduction = () => {
/>
</div>
<div className="content text-break">
{htmlToReact(tagInfo?.parsed_text)}
</div>
<div
className="content text-break fmt"
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
/>
<div className="mt-4">
{tagInfo?.member_actions.map((action, index) => {
return (

View File

@ -1,6 +1,4 @@
import i18next from 'i18next';
import parse from 'html-react-parser';
import * as DOMPurify from 'dompurify';
const Diff = require('diff');
@ -235,32 +233,6 @@ function diffText(newText: string, oldText?: string): string {
return result.join('');
}
function htmlToReact(html: string) {
const cleanedHtml = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
const ele = document.createElement('div');
ele.innerHTML = cleanedHtml;
ele.querySelectorAll('table').forEach((table) => {
if (
(!table || (table.parentNode as HTMLDivElement))?.classList.contains(
'table-responsive',
)
) {
return;
}
table.classList.add('table', 'table-bordered');
const div = document.createElement('div');
div.className = 'table-responsive';
table.parentNode?.replaceChild(div, table);
div.appendChild(table);
});
return parse(ele.innerHTML);
}
export {
thousandthDivision,
formatCount,
@ -276,5 +248,4 @@ export {
labelStyle,
handleFormError,
diffText,
htmlToReact,
};