| import { Theme } from '@/types/app' | import { Theme } from '@/types/app' | ||||
| import useTheme from '@/hooks/use-theme' | import useTheme from '@/hooks/use-theme' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import SVGRenderer from './svg-gallery' | |||||
| // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | ||||
| const capitalizationLanguageNameMap: Record<string, string> = { | const capitalizationLanguageNameMap: Record<string, string> = { | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| // Attention: SVGRenderer has xss vulnerability | |||||
| // else if (language === 'svg' && isSVG) { | |||||
| // return ( | |||||
| // <ErrorBoundary> | |||||
| // <SVGRenderer content={content} /> | |||||
| // </ErrorBoundary> | |||||
| // ) | |||||
| // } | |||||
| else if (language === 'svg' && isSVG) { | |||||
| return ( | |||||
| <ErrorBoundary> | |||||
| <SVGRenderer content={content} /> | |||||
| </ErrorBoundary> | |||||
| ) | |||||
| } | |||||
| else { | else { | ||||
| return ( | return ( | ||||
| <SyntaxHighlighter | <SyntaxHighlighter | ||||
| } | } | ||||
| } | } | ||||
| function escapeSVGTags(htmlString: string): string { | |||||
| return htmlString.replace(/(<svg[\s\S]*?>)([\s\S]*?)(<\/svg>)/gi, (match: string, openTag: string, innerContent: string, closeTag: string): string => { | |||||
| return openTag.replace(/</g, '<').replace(/>/g, '>') | |||||
| + innerContent.replace(/</g, '<').replace(/>/g, '>') | |||||
| + closeTag.replace(/</g, '<').replace(/>/g, '>') | |||||
| }) | |||||
| } | |||||
| export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { | export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { | ||||
| const latexContent = flow([ | const latexContent = flow([ | ||||
| preprocessThinkTag, | preprocessThinkTag, | ||||
| preprocessLaTeX, | preprocessLaTeX, | ||||
| ])(escapeSVGTags(props.content)) | |||||
| ])(props.content) | |||||
| return ( | return ( | ||||
| <div className={cn('markdown-body', '!text-text-primary', props.className)}> | <div className={cn('markdown-body', '!text-text-primary', props.className)}> |
| import { useEffect, useRef, useState } from 'react' | import { useEffect, useRef, useState } from 'react' | ||||
| import { SVG } from '@svgdotjs/svg.js' | import { SVG } from '@svgdotjs/svg.js' | ||||
| import ImagePreview from '@/app/components/base/image-uploader/image-preview' | import ImagePreview from '@/app/components/base/image-uploader/image-preview' | ||||
| import DOMPurify from 'dompurify' | |||||
| export const SVGRenderer = ({ content }: { content: string }) => { | export const SVGRenderer = ({ content }: { content: string }) => { | ||||
| const svgRef = useRef<HTMLDivElement>(null) | const svgRef = useRef<HTMLDivElement>(null) | ||||
| svgRef.current.style.width = `${Math.min(originalWidth, 298)}px` | svgRef.current.style.width = `${Math.min(originalWidth, 298)}px` | ||||
| const rootElement = draw.svg(content) | |||||
| const rootElement = draw.svg(DOMPurify.sanitize(content)) | |||||
| rootElement.click(() => { | rootElement.click(() => { | ||||
| setImagePreview(svgToDataURL(svgElement as Element)) | setImagePreview(svgToDataURL(svgElement as Element)) |
| "crypto-js": "^4.2.0", | "crypto-js": "^4.2.0", | ||||
| "dayjs": "^1.11.13", | "dayjs": "^1.11.13", | ||||
| "decimal.js": "^10.4.3", | "decimal.js": "^10.4.3", | ||||
| "dompurify": "^3.2.4", | |||||
| "echarts": "^5.5.1", | "echarts": "^5.5.1", | ||||
| "echarts-for-react": "^3.0.2", | "echarts-for-react": "^3.0.2", | ||||
| "elkjs": "^0.9.3", | "elkjs": "^0.9.3", | ||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@antfu/eslint-config": "^4.1.1", | "@antfu/eslint-config": "^4.1.1", | ||||
| "@eslint/js": "^9.20.0", | |||||
| "@chromatic-com/storybook": "^3.1.0", | "@chromatic-com/storybook": "^3.1.0", | ||||
| "@eslint-react/eslint-plugin": "^1.15.0", | "@eslint-react/eslint-plugin": "^1.15.0", | ||||
| "@eslint/eslintrc": "^3.1.0", | "@eslint/eslintrc": "^3.1.0", | ||||
| "@eslint/js": "^9.20.0", | |||||
| "@faker-js/faker": "^9.0.3", | "@faker-js/faker": "^9.0.3", | ||||
| "@next/eslint-plugin-next": "^15.2.3", | "@next/eslint-plugin-next": "^15.2.3", | ||||
| "@rgrove/parse-xml": "^4.1.0", | "@rgrove/parse-xml": "^4.1.0", | ||||
| "code-inspector-plugin": "^0.18.1", | "code-inspector-plugin": "^0.18.1", | ||||
| "cross-env": "^7.0.3", | "cross-env": "^7.0.3", | ||||
| "eslint": "^9.20.1", | "eslint": "^9.20.1", | ||||
| "eslint-config-next": "^15.0.0", | |||||
| "eslint-plugin-react-hooks": "^5.1.0", | "eslint-plugin-react-hooks": "^5.1.0", | ||||
| "eslint-plugin-react-refresh": "^0.4.19", | "eslint-plugin-react-refresh": "^0.4.19", | ||||
| "eslint-plugin-storybook": "^0.11.2", | "eslint-plugin-storybook": "^0.11.2", | ||||
| "eslint-plugin-tailwindcss": "^3.18.0", | "eslint-plugin-tailwindcss": "^3.18.0", | ||||
| "eslint-config-next": "^15.0.0", | |||||
| "husky": "^9.1.6", | "husky": "^9.1.6", | ||||
| "jest": "^29.7.0", | "jest": "^29.7.0", | ||||
| "jest-environment-jsdom": "^29.7.0", | "jest-environment-jsdom": "^29.7.0", |
| decimal.js: | decimal.js: | ||||
| specifier: ^10.4.3 | specifier: ^10.4.3 | ||||
| version: 10.4.3 | version: 10.4.3 | ||||
| dompurify: | |||||
| specifier: ^3.2.4 | |||||
| version: 3.2.4 | |||||
| echarts: | echarts: | ||||
| specifier: ^5.5.1 | specifier: ^5.5.1 | ||||
| version: 5.5.1 | version: 5.5.1 | ||||
| resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} | resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} | ||||
| engines: {node: '>= 4'} | engines: {node: '>= 4'} | ||||
| dompurify@3.2.3: | |||||
| resolution: {integrity: sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==} | |||||
| dompurify@3.2.4: | |||||
| resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} | |||||
| domutils@2.8.0: | domutils@2.8.0: | ||||
| resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} | resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} | ||||
| dependencies: | dependencies: | ||||
| domelementtype: 2.3.0 | domelementtype: 2.3.0 | ||||
| dompurify@3.2.3: | |||||
| dompurify@3.2.4: | |||||
| optionalDependencies: | optionalDependencies: | ||||
| '@types/trusted-types': 2.0.7 | '@types/trusted-types': 2.0.7 | ||||
| d3-sankey: 0.12.3 | d3-sankey: 0.12.3 | ||||
| dagre-d3-es: 7.0.11 | dagre-d3-es: 7.0.11 | ||||
| dayjs: 1.11.13 | dayjs: 1.11.13 | ||||
| dompurify: 3.2.3 | |||||
| dompurify: 3.2.4 | |||||
| katex: 0.16.21 | katex: 0.16.21 | ||||
| khroma: 2.1.0 | khroma: 2.1.0 | ||||
| lodash-es: 4.17.21 | lodash-es: 4.17.21 |