refactor(Markdown): remove rehype-sanitize and implement custom element filtering
- Removed rehype-sanitize dependency and its related configuration. - Introduced ALLOWED_ELEMENTS and DISALLOWED_ELEMENTS for custom HTML element filtering. - Updated rehypePlugins logic to conditionally apply plugins based on message content. - Added encodeHTML utility function for HTML entity encoding.
This commit is contained in:
parent
c576aa5cb4
commit
55a9447a7b
@ -190,7 +190,6 @@
|
|||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^7.0.0",
|
"rehype-mathjax": "^7.0.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
|
||||||
"remark-cjk-friendly": "^1.1.0",
|
"remark-cjk-friendly": "^1.1.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
|||||||
@ -8,16 +8,14 @@ import type { Message } from '@renderer/types'
|
|||||||
import { parseJSON } from '@renderer/utils'
|
import { parseJSON } from '@renderer/utils'
|
||||||
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
|
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
|
||||||
import { findCitationInChildren } from '@renderer/utils/markdown'
|
import { findCitationInChildren } from '@renderer/utils/markdown'
|
||||||
import { sanitizeSchema } from '@renderer/utils/markdown'
|
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { type FC, useMemo } from 'react'
|
import { type FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactMarkdown, { type Components } from 'react-markdown'
|
import ReactMarkdown, { type Components } from 'react-markdown'
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
// @ts-ignore next-line
|
// @ts-ignore rehype-mathjax is not typed
|
||||||
import rehypeMathjax from 'rehype-mathjax'
|
import rehypeMathjax from 'rehype-mathjax'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
import rehypeSanitize from 'rehype-sanitize'
|
|
||||||
import remarkCjkFriendly from 'remark-cjk-friendly'
|
import remarkCjkFriendly from 'remark-cjk-friendly'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
@ -26,6 +24,10 @@ import CodeBlock from './CodeBlock'
|
|||||||
import ImagePreview from './ImagePreview'
|
import ImagePreview from './ImagePreview'
|
||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
|
|
||||||
|
const ALLOWED_ELEMENTS =
|
||||||
|
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan|sub|sup)/i
|
||||||
|
const DISALLOWED_ELEMENTS = ['iframe']
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
}
|
}
|
||||||
@ -43,9 +45,12 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
return removeSvgEmptyLines(escapeBrackets(content))
|
return removeSvgEmptyLines(escapeBrackets(content))
|
||||||
}, [message, t])
|
}, [message, t])
|
||||||
|
|
||||||
|
const rehypeMath = useMemo(() => (mathEngine === 'KaTeX' ? rehypeKatex : rehypeMathjax), [mathEngine])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
return [rehypeRaw, [rehypeSanitize, sanitizeSchema], mathEngine === 'KaTeX' ? rehypeKatex : rehypeMathjax]
|
const hasElements = ALLOWED_ELEMENTS.test(messageContent)
|
||||||
}, [mathEngine])
|
return hasElements ? [rehypeRaw, rehypeMath] : [rehypeMath]
|
||||||
|
}, [messageContent, rehypeMath])
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
const baseComponents = {
|
const baseComponents = {
|
||||||
@ -71,6 +76,7 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
className="markdown"
|
className="markdown"
|
||||||
components={components}
|
components={components}
|
||||||
|
disallowedElements={DISALLOWED_ELEMENTS}
|
||||||
remarkRehypeOptions={{
|
remarkRehypeOptions={{
|
||||||
footnoteLabel: t('common.footnotes'),
|
footnoteLabel: t('common.footnotes'),
|
||||||
footnoteLabelTagName: 'h4',
|
footnoteLabelTagName: 'h4',
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import {
|
import { convertMathFormula, findCitationInChildren, removeTrailingDoubleSpaces } from '../markdown'
|
||||||
convertMathFormula,
|
|
||||||
findCitationInChildren,
|
|
||||||
MARKDOWN_ALLOWED_TAGS,
|
|
||||||
removeTrailingDoubleSpaces,
|
|
||||||
sanitizeSchema
|
|
||||||
} from '../markdown'
|
|
||||||
|
|
||||||
describe('markdown', () => {
|
describe('markdown', () => {
|
||||||
describe('findCitationInChildren', () => {
|
describe('findCitationInChildren', () => {
|
||||||
@ -72,27 +66,6 @@ describe('markdown', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('markdown configuration constants', () => {
|
|
||||||
it('MARKDOWN_ALLOWED_TAGS contains expected tags', () => {
|
|
||||||
expect(MARKDOWN_ALLOWED_TAGS).toContain('p')
|
|
||||||
expect(MARKDOWN_ALLOWED_TAGS).toContain('div')
|
|
||||||
expect(MARKDOWN_ALLOWED_TAGS).toContain('code')
|
|
||||||
expect(MARKDOWN_ALLOWED_TAGS).toContain('svg')
|
|
||||||
expect(MARKDOWN_ALLOWED_TAGS.length).toBeGreaterThan(10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sanitizeSchema contains proper configuration', () => {
|
|
||||||
expect(sanitizeSchema.tagNames).toBe(MARKDOWN_ALLOWED_TAGS)
|
|
||||||
expect(sanitizeSchema.attributes).toHaveProperty('*')
|
|
||||||
expect(sanitizeSchema.attributes).toHaveProperty('svg')
|
|
||||||
expect(sanitizeSchema.attributes).toHaveProperty('a')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sanitizeSchema matches snapshot', () => {
|
|
||||||
expect(sanitizeSchema).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('convertMathFormula', () => {
|
describe('convertMathFormula', () => {
|
||||||
it('should convert LaTeX block delimiters to $$$$', () => {
|
it('should convert LaTeX block delimiters to $$$$', () => {
|
||||||
// 验证将 LaTeX 块分隔符转换为 $$$$
|
// 验证将 LaTeX 块分隔符转换为 $$$$
|
||||||
|
|||||||
@ -1,67 +1,3 @@
|
|||||||
export const MARKDOWN_ALLOWED_TAGS = [
|
|
||||||
'style',
|
|
||||||
'p',
|
|
||||||
'div',
|
|
||||||
'span',
|
|
||||||
'b',
|
|
||||||
'i',
|
|
||||||
'strong',
|
|
||||||
'em',
|
|
||||||
'ul',
|
|
||||||
'ol',
|
|
||||||
'li',
|
|
||||||
'table',
|
|
||||||
'tr',
|
|
||||||
'td',
|
|
||||||
'th',
|
|
||||||
'thead',
|
|
||||||
'tbody',
|
|
||||||
'h1',
|
|
||||||
'h2',
|
|
||||||
'h3',
|
|
||||||
'h4',
|
|
||||||
'h5',
|
|
||||||
'h6',
|
|
||||||
'blockquote',
|
|
||||||
'pre',
|
|
||||||
'code',
|
|
||||||
'br',
|
|
||||||
'hr',
|
|
||||||
'svg',
|
|
||||||
'path',
|
|
||||||
'circle',
|
|
||||||
'rect',
|
|
||||||
'line',
|
|
||||||
'polyline',
|
|
||||||
'polygon',
|
|
||||||
'text',
|
|
||||||
'g',
|
|
||||||
'defs',
|
|
||||||
'title',
|
|
||||||
'desc',
|
|
||||||
'tspan',
|
|
||||||
'sub',
|
|
||||||
'sup'
|
|
||||||
]
|
|
||||||
|
|
||||||
// rehype-sanitize配置
|
|
||||||
export const sanitizeSchema = {
|
|
||||||
tagNames: MARKDOWN_ALLOWED_TAGS,
|
|
||||||
attributes: {
|
|
||||||
'*': ['className', 'style', 'id', 'title'],
|
|
||||||
svg: ['viewBox', 'width', 'height', 'xmlns', 'fill', 'stroke'],
|
|
||||||
path: ['d', 'fill', 'stroke', 'strokeWidth', 'strokeLinecap', 'strokeLinejoin'],
|
|
||||||
circle: ['cx', 'cy', 'r', 'fill', 'stroke'],
|
|
||||||
rect: ['x', 'y', 'width', 'height', 'fill', 'stroke'],
|
|
||||||
line: ['x1', 'y1', 'x2', 'y2', 'stroke'],
|
|
||||||
polyline: ['points', 'fill', 'stroke'],
|
|
||||||
polygon: ['points', 'fill', 'stroke'],
|
|
||||||
text: ['x', 'y', 'fill', 'textAnchor', 'dominantBaseline'],
|
|
||||||
g: ['transform', 'fill', 'stroke'],
|
|
||||||
a: ['href', 'target', 'rel']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更彻底的查找方法,递归搜索所有子元素
|
// 更彻底的查找方法,递归搜索所有子元素
|
||||||
export const findCitationInChildren = (children) => {
|
export const findCitationInChildren = (children) => {
|
||||||
if (!children) return null
|
if (!children) return null
|
||||||
@ -107,3 +43,21 @@ export function removeTrailingDoubleSpaces(markdown: string): string {
|
|||||||
// 使用正则表达式匹配末尾的两个空格,并替换为空字符串
|
// 使用正则表达式匹配末尾的两个空格,并替换为空字符串
|
||||||
return markdown.replace(/ {2}$/gm, '')
|
return markdown.replace(/ {2}$/gm, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML实体编码辅助函数
|
||||||
|
* @param str 输入字符串
|
||||||
|
* @returns string 编码后的字符串
|
||||||
|
*/
|
||||||
|
export const encodeHTML = (str: string) => {
|
||||||
|
return str.replace(/[&<>"']/g, (match) => {
|
||||||
|
const entities: { [key: string]: string } = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
}
|
||||||
|
return entities[match]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
22
yarn.lock
22
yarn.lock
@ -4338,7 +4338,6 @@ __metadata:
|
|||||||
rehype-katex: "npm:^7.0.1"
|
rehype-katex: "npm:^7.0.1"
|
||||||
rehype-mathjax: "npm:^7.0.0"
|
rehype-mathjax: "npm:^7.0.0"
|
||||||
rehype-raw: "npm:^7.0.0"
|
rehype-raw: "npm:^7.0.0"
|
||||||
rehype-sanitize: "npm:^6.0.0"
|
|
||||||
remark-cjk-friendly: "npm:^1.1.0"
|
remark-cjk-friendly: "npm:^1.1.0"
|
||||||
remark-gfm: "npm:^4.0.0"
|
remark-gfm: "npm:^4.0.0"
|
||||||
remark-math: "npm:^6.0.0"
|
remark-math: "npm:^6.0.0"
|
||||||
@ -9104,17 +9103,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"hast-util-sanitize@npm:^5.0.0":
|
|
||||||
version: 5.0.2
|
|
||||||
resolution: "hast-util-sanitize@npm:5.0.2"
|
|
||||||
dependencies:
|
|
||||||
"@types/hast": "npm:^3.0.0"
|
|
||||||
"@ungap/structured-clone": "npm:^1.0.0"
|
|
||||||
unist-util-position: "npm:^5.0.0"
|
|
||||||
checksum: 10c0/20951652078a8c21341c1c9a84f90015b2ba01cc41fa16772f122c65cda26a7adb0501fdeba5c8e37e40e2632447e8fe455d0dd2dc27d39663baacca76f2ecb6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"hast-util-to-html@npm:^9.0.5":
|
"hast-util-to-html@npm:^9.0.5":
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
resolution: "hast-util-to-html@npm:9.0.5"
|
resolution: "hast-util-to-html@npm:9.0.5"
|
||||||
@ -14735,16 +14723,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rehype-sanitize@npm:^6.0.0":
|
|
||||||
version: 6.0.0
|
|
||||||
resolution: "rehype-sanitize@npm:6.0.0"
|
|
||||||
dependencies:
|
|
||||||
"@types/hast": "npm:^3.0.0"
|
|
||||||
hast-util-sanitize: "npm:^5.0.0"
|
|
||||||
checksum: 10c0/43d6c056e63c994cf56e5ee0e157052d2030dc5ac160845ee494af9a26e5906bf5ec5af56c7d90c99f9c4dc0091e45a48a168618135fb6c64a76481ad3c449e9
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"remark-cjk-friendly@npm:^1.1.0":
|
"remark-cjk-friendly@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "remark-cjk-friendly@npm:1.1.0"
|
resolution: "remark-cjk-friendly@npm:1.1.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user