mirror of
https://github.com/usememos/memos.git
synced 2025-12-18 14:50:13 +08:00
refactor(web): improve MemoContent security and maintainability
Security improvements: - Add rehype-sanitize for XSS protection in markdown content - Remove DOMPurify and deprecated __html code block feature - Extract sanitize schema to constants with comprehensive documentation Maintainability improvements: - Extract SANITIZE_SCHEMA to constants.ts for better organization - Create utils.ts with shared code extraction utilities - Refactor CodeBlock and MermaidBlock to use shared utilities - Rename PreProps to CodeBlockProps for clarity - Reduce code duplication across components Dependency cleanup: - Remove explicit katex dependency (now transitive via rehype-katex) - Remove @matejmazur/react-katex (unused) - Remove dompurify (replaced by rehype-sanitize) - Update vite config to remove katex-vendor chunk Changes: 7 files changed, 84 insertions(+), 100 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d9f8bc80f0
commit
4668c4714b
8 changed files with 109 additions and 100 deletions
|
|
@ -16,7 +16,6 @@
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@github/relative-time-element": "^4.5.0",
|
"@github/relative-time-element": "^4.5.0",
|
||||||
"@matejmazur/react-katex": "^3.1.3",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|
@ -33,11 +32,9 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dompurify": "^3.3.0",
|
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"i18next": "^25.6.3",
|
"i18next": "^25.6.3",
|
||||||
"katex": "^0.16.25",
|
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
|
@ -57,6 +54,7 @@
|
||||||
"react-use": "^17.6.0",
|
"react-use": "^17.6.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
|
|
||||||
41
web/pnpm-lock.yaml
generated
41
web/pnpm-lock.yaml
generated
|
|
@ -26,9 +26,6 @@ importers:
|
||||||
'@github/relative-time-element':
|
'@github/relative-time-element':
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
'@matejmazur/react-katex':
|
|
||||||
specifier: ^3.1.3
|
|
||||||
version: 3.1.3(katex@0.16.25)(react@18.3.1)
|
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
specifier: ^1.3.3
|
||||||
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -77,9 +74,6 @@ importers:
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.19
|
specifier: ^1.11.19
|
||||||
version: 1.11.19
|
version: 1.11.19
|
||||||
dompurify:
|
|
||||||
specifier: ^3.3.0
|
|
||||||
version: 3.3.0
|
|
||||||
fuse.js:
|
fuse.js:
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
|
|
@ -89,9 +83,6 @@ importers:
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^25.6.3
|
specifier: ^25.6.3
|
||||||
version: 25.6.3(typescript@5.9.3)
|
version: 25.6.3(typescript@5.9.3)
|
||||||
katex:
|
|
||||||
specifier: ^0.16.25
|
|
||||||
version: 0.16.25
|
|
||||||
leaflet:
|
leaflet:
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
|
@ -149,6 +140,9 @@ importers:
|
||||||
rehype-raw:
|
rehype-raw:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
|
rehype-sanitize:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
remark-breaks:
|
remark-breaks:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
|
@ -682,13 +676,6 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
'@matejmazur/react-katex@3.1.3':
|
|
||||||
resolution: {integrity: sha512-rBp7mJ9An7ktNoU653BWOYdO4FoR4YNwofHZi+vaytX/nWbIlmHVIF+X8VFOn6c3WYmrLT5FFBjKqCZ1sjR5uQ==}
|
|
||||||
engines: {node: '>=12', yarn: '>=1.1'}
|
|
||||||
peerDependencies:
|
|
||||||
katex: '>=0.9'
|
|
||||||
react: '>=16'
|
|
||||||
|
|
||||||
'@mermaid-js/parser@0.6.3':
|
'@mermaid-js/parser@0.6.3':
|
||||||
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
||||||
|
|
||||||
|
|
@ -1988,6 +1975,9 @@ packages:
|
||||||
hast-util-raw@9.1.0:
|
hast-util-raw@9.1.0:
|
||||||
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.2:
|
||||||
|
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
|
||||||
|
|
||||||
hast-util-to-jsx-runtime@2.3.6:
|
hast-util-to-jsx-runtime@2.3.6:
|
||||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||||
|
|
||||||
|
|
@ -2647,6 +2637,9 @@ packages:
|
||||||
rehype-raw@7.0.0:
|
rehype-raw@7.0.0:
|
||||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
|
||||||
|
|
||||||
remark-breaks@4.0.0:
|
remark-breaks@4.0.0:
|
||||||
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
|
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
|
||||||
|
|
||||||
|
|
@ -3417,11 +3410,6 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
'@matejmazur/react-katex@3.1.3(katex@0.16.25)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
katex: 0.16.25
|
|
||||||
react: 18.3.1
|
|
||||||
|
|
||||||
'@mermaid-js/parser@0.6.3':
|
'@mermaid-js/parser@0.6.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
langium: 3.3.1
|
langium: 3.3.1
|
||||||
|
|
@ -4743,6 +4731,12 @@ snapshots:
|
||||||
web-namespaces: 2.0.1
|
web-namespaces: 2.0.1
|
||||||
zwitch: 2.0.4
|
zwitch: 2.0.4
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@ungap/structured-clone': 1.3.0
|
||||||
|
unist-util-position: 5.0.0
|
||||||
|
|
||||||
hast-util-to-jsx-runtime@2.3.6:
|
hast-util-to-jsx-runtime@2.3.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
@ -5666,6 +5660,11 @@ snapshots:
|
||||||
hast-util-raw: 9.1.0
|
hast-util-raw: 9.1.0
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
hast-util-sanitize: 5.0.2
|
||||||
|
|
||||||
remark-breaks@4.0.0:
|
remark-breaks@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import hljs from "highlight.js";
|
import hljs from "highlight.js";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
@ -7,23 +6,20 @@ import { cn } from "@/lib/utils";
|
||||||
import { userStore } from "@/store";
|
import { userStore } from "@/store";
|
||||||
import { getThemeWithFallback, resolveTheme } from "@/utils/theme";
|
import { getThemeWithFallback, resolveTheme } from "@/utils/theme";
|
||||||
import { MermaidBlock } from "./MermaidBlock";
|
import { MermaidBlock } from "./MermaidBlock";
|
||||||
|
import { extractCodeContent, extractLanguage } from "./utils";
|
||||||
|
|
||||||
interface PreProps {
|
interface CodeBlockProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlock = observer(({ children, className, ...props }: PreProps) => {
|
export const CodeBlock = observer(({ children, className, ...props }: CodeBlockProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// Extract the code element and its props
|
|
||||||
const codeElement = children as React.ReactElement;
|
const codeElement = children as React.ReactElement;
|
||||||
const codeClassName = codeElement?.props?.className || "";
|
const codeClassName = codeElement?.props?.className || "";
|
||||||
const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, "");
|
const codeContent = extractCodeContent(children);
|
||||||
|
const language = extractLanguage(codeClassName);
|
||||||
// Extract language from className (format: language-xxx)
|
|
||||||
const match = /language-(\w+)/.exec(codeClassName);
|
|
||||||
const language = match ? match[1] : "";
|
|
||||||
|
|
||||||
// If it's a mermaid block, render with MermaidBlock component
|
// If it's a mermaid block, render with MermaidBlock component
|
||||||
if (language === "mermaid") {
|
if (language === "mermaid") {
|
||||||
|
|
@ -34,66 +30,6 @@ export const CodeBlock = observer(({ children, className, ...props }: PreProps)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's __html special language, render sanitized HTML
|
|
||||||
if (language === "__html") {
|
|
||||||
const sanitizedHTML = DOMPurify.sanitize(codeContent, {
|
|
||||||
ALLOWED_TAGS: [
|
|
||||||
"div",
|
|
||||||
"span",
|
|
||||||
"p",
|
|
||||||
"br",
|
|
||||||
"strong",
|
|
||||||
"b",
|
|
||||||
"em",
|
|
||||||
"i",
|
|
||||||
"u",
|
|
||||||
"s",
|
|
||||||
"strike",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
"blockquote",
|
|
||||||
"code",
|
|
||||||
"pre",
|
|
||||||
"ul",
|
|
||||||
"ol",
|
|
||||||
"li",
|
|
||||||
"dl",
|
|
||||||
"dt",
|
|
||||||
"dd",
|
|
||||||
"table",
|
|
||||||
"thead",
|
|
||||||
"tbody",
|
|
||||||
"tr",
|
|
||||||
"th",
|
|
||||||
"td",
|
|
||||||
"a",
|
|
||||||
"img",
|
|
||||||
"figure",
|
|
||||||
"figcaption",
|
|
||||||
"hr",
|
|
||||||
"small",
|
|
||||||
"sup",
|
|
||||||
"sub",
|
|
||||||
],
|
|
||||||
ALLOWED_ATTR: "href title alt src width height class id style target rel colspan rowspan".split(" "),
|
|
||||||
FORBID_ATTR: "onerror onload onclick onmouseover onfocus onblur onchange".split(" "),
|
|
||||||
FORBID_TAGS: "script iframe object embed form input button".split(" "),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full overflow-auto my-2!"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: sanitizedHTML,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = getThemeWithFallback(userStore.state.userGeneralSetting?.theme);
|
const theme = getThemeWithFallback(userStore.state.userGeneralSetting?.theme);
|
||||||
const resolvedTheme = resolveTheme(theme);
|
const resolvedTheme = resolveTheme(theme);
|
||||||
const isDarkTheme = resolvedTheme.includes("dark");
|
const isDarkTheme = resolvedTheme.includes("dark");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { userStore } from "@/store";
|
import { userStore } from "@/store";
|
||||||
import { getThemeWithFallback, resolveTheme, setupSystemThemeListener } from "@/utils/theme";
|
import { getThemeWithFallback, resolveTheme, setupSystemThemeListener } from "@/utils/theme";
|
||||||
|
import { extractCodeContent } from "./utils";
|
||||||
|
|
||||||
interface MermaidBlockProps {
|
interface MermaidBlockProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -20,9 +21,7 @@ export const MermaidBlock = observer(({ children, className }: MermaidBlockProps
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
const [systemThemeChange, setSystemThemeChange] = useState(0);
|
const [systemThemeChange, setSystemThemeChange] = useState(0);
|
||||||
|
|
||||||
// Extract Mermaid code content from children
|
const codeContent = extractCodeContent(children);
|
||||||
const codeElement = children as React.ReactElement;
|
|
||||||
const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, "");
|
|
||||||
|
|
||||||
// Get theme preference (reactive via MobX observer)
|
// Get theme preference (reactive via MobX observer)
|
||||||
// Falls back to localStorage or system preference if no user setting
|
// Falls back to localStorage or system preference if no user setting
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,59 @@
|
||||||
|
import { defaultSchema } from "rehype-sanitize";
|
||||||
|
|
||||||
export const MAX_DISPLAY_HEIGHT = 256;
|
export const MAX_DISPLAY_HEIGHT = 256;
|
||||||
|
|
||||||
export const COMPACT_STATES: Record<"ALL" | "SNIPPET", { textKey: string; next: "ALL" | "SNIPPET" }> = {
|
export const COMPACT_STATES: Record<"ALL" | "SNIPPET", { textKey: string; next: "ALL" | "SNIPPET" }> = {
|
||||||
ALL: { textKey: "memo.show-more", next: "SNIPPET" },
|
ALL: { textKey: "memo.show-more", next: "SNIPPET" },
|
||||||
SNIPPET: { textKey: "memo.show-less", next: "ALL" },
|
SNIPPET: { textKey: "memo.show-less", next: "ALL" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitization schema for markdown HTML content.
|
||||||
|
* Extends the default schema to allow:
|
||||||
|
* - KaTeX math rendering elements (MathML tags)
|
||||||
|
* - KaTeX-specific attributes (className, style, aria-*, data-*)
|
||||||
|
* - Safe HTML elements for rich content
|
||||||
|
*
|
||||||
|
* This prevents XSS attacks while preserving math rendering functionality.
|
||||||
|
*/
|
||||||
|
export const SANITIZE_SCHEMA = {
|
||||||
|
...defaultSchema,
|
||||||
|
attributes: {
|
||||||
|
...defaultSchema.attributes,
|
||||||
|
div: [...(defaultSchema.attributes?.div || []), "className"],
|
||||||
|
span: [...(defaultSchema.attributes?.span || []), "className", "style", ["aria*"], ["data*"]],
|
||||||
|
// MathML attributes for KaTeX rendering
|
||||||
|
annotation: ["encoding"],
|
||||||
|
math: ["xmlns"],
|
||||||
|
mi: [],
|
||||||
|
mn: [],
|
||||||
|
mo: [],
|
||||||
|
mrow: [],
|
||||||
|
mspace: [],
|
||||||
|
mstyle: [],
|
||||||
|
msup: [],
|
||||||
|
msub: [],
|
||||||
|
msubsup: [],
|
||||||
|
mfrac: [],
|
||||||
|
mtext: [],
|
||||||
|
semantics: [],
|
||||||
|
},
|
||||||
|
tagNames: [
|
||||||
|
...(defaultSchema.tagNames || []),
|
||||||
|
// MathML elements for KaTeX math rendering
|
||||||
|
"math",
|
||||||
|
"annotation",
|
||||||
|
"semantics",
|
||||||
|
"mi",
|
||||||
|
"mn",
|
||||||
|
"mo",
|
||||||
|
"mrow",
|
||||||
|
"mspace",
|
||||||
|
"mstyle",
|
||||||
|
"msup",
|
||||||
|
"msub",
|
||||||
|
"msubsup",
|
||||||
|
"mfrac",
|
||||||
|
"mtext",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { memo } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
|
import rehypeSanitize from "rehype-sanitize";
|
||||||
import remarkBreaks from "remark-breaks";
|
import remarkBreaks from "remark-breaks";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
|
|
@ -15,12 +16,12 @@ import { remarkTag } from "@/utils/remark-plugins/remark-tag";
|
||||||
import { isSuperUser } from "@/utils/user";
|
import { isSuperUser } from "@/utils/user";
|
||||||
import { CodeBlock } from "./CodeBlock";
|
import { CodeBlock } from "./CodeBlock";
|
||||||
import { createConditionalComponent, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
|
import { createConditionalComponent, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
|
||||||
|
import { SANITIZE_SCHEMA } from "./constants";
|
||||||
import { useCompactLabel, useCompactMode } from "./hooks";
|
import { useCompactLabel, useCompactMode } from "./hooks";
|
||||||
import { MemoContentContext } from "./MemoContentContext";
|
import { MemoContentContext } from "./MemoContentContext";
|
||||||
import { Tag } from "./Tag";
|
import { Tag } from "./Tag";
|
||||||
import { TaskListItem } from "./TaskListItem";
|
import { TaskListItem } from "./TaskListItem";
|
||||||
import type { MemoContentProps } from "./types";
|
import type { MemoContentProps } from "./types";
|
||||||
import "katex/dist/katex.min.css";
|
|
||||||
|
|
||||||
const MemoContent = observer((props: MemoContentProps) => {
|
const MemoContent = observer((props: MemoContentProps) => {
|
||||||
const { className, contentClassName, content, memoName, onClick, onDoubleClick } = props;
|
const { className, contentClassName, content, memoName, onClick, onDoubleClick } = props;
|
||||||
|
|
@ -34,7 +35,6 @@ const MemoContent = observer((props: MemoContentProps) => {
|
||||||
const memo = memoName ? memoStore.getMemoByName(memoName) : null;
|
const memo = memoName ? memoStore.getMemoByName(memoName) : null;
|
||||||
const allowEdit = !props.readonly && memo && (currentUser?.name === memo.creator || isSuperUser(currentUser));
|
const allowEdit = !props.readonly && memo && (currentUser?.name === memo.creator || isSuperUser(currentUser));
|
||||||
|
|
||||||
// Context for custom components
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
memoName,
|
memoName,
|
||||||
readonly: !allowEdit,
|
readonly: !allowEdit,
|
||||||
|
|
@ -60,7 +60,7 @@ const MemoContent = observer((props: MemoContentProps) => {
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, remarkBreaks, remarkMath, remarkTag, remarkPreserveType]}
|
remarkPlugins={[remarkGfm, remarkBreaks, remarkMath, remarkTag, remarkPreserveType]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
rehypePlugins={[rehypeRaw, rehypeKatex, [rehypeSanitize, SANITIZE_SCHEMA]]}
|
||||||
components={{
|
components={{
|
||||||
// Conditionally render custom components based on AST node type
|
// Conditionally render custom components based on AST node type
|
||||||
input: createConditionalComponent(TaskListItem, "input", isTaskListItemNode),
|
input: createConditionalComponent(TaskListItem, "input", isTaskListItemNode),
|
||||||
|
|
|
||||||
25
web/src/components/MemoContent/utils.ts
Normal file
25
web/src/components/MemoContent/utils.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts code content from a react-markdown code element.
|
||||||
|
* Handles the nested structure where code is passed as children.
|
||||||
|
*
|
||||||
|
* @param children - The children prop from react-markdown (typically a code element)
|
||||||
|
* @returns The extracted code content as a string with trailing newline removed
|
||||||
|
*/
|
||||||
|
export const extractCodeContent = (children: React.ReactNode): string => {
|
||||||
|
const codeElement = children as React.ReactElement;
|
||||||
|
return String(codeElement?.props?.children || "").replace(/\n$/, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the language identifier from a code block's className.
|
||||||
|
* react-markdown uses the format "language-xxx" for code blocks.
|
||||||
|
*
|
||||||
|
* @param className - The className string from a code element
|
||||||
|
* @returns The language identifier, or empty string if none found
|
||||||
|
*/
|
||||||
|
export const extractLanguage = (className: string): string => {
|
||||||
|
const match = /language-(\w+)/.exec(className);
|
||||||
|
return match ? match[1] : "";
|
||||||
|
};
|
||||||
|
|
@ -40,7 +40,6 @@ export default defineConfig({
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
"utils-vendor": ["dayjs", "lodash-es"],
|
"utils-vendor": ["dayjs", "lodash-es"],
|
||||||
"katex-vendor": ["katex"],
|
|
||||||
"mermaid-vendor": ["mermaid"],
|
"mermaid-vendor": ["mermaid"],
|
||||||
"leaflet-vendor": ["leaflet", "react-leaflet"],
|
"leaflet-vendor": ["leaflet", "react-leaflet"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue