memos/web/src/components/MemoContent/CodeBlock.tsx
Steven 4668c4714b 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>
2025-12-02 22:45:22 +08:00

111 lines
3.8 KiB
TypeScript

import hljs from "highlight.js";
import { CheckIcon, CopyIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import { userStore } from "@/store";
import { getThemeWithFallback, resolveTheme } from "@/utils/theme";
import { MermaidBlock } from "./MermaidBlock";
import { extractCodeContent, extractLanguage } from "./utils";
interface CodeBlockProps {
children?: React.ReactNode;
className?: string;
}
export const CodeBlock = observer(({ children, className, ...props }: CodeBlockProps) => {
const [copied, setCopied] = useState(false);
const codeElement = children as React.ReactElement;
const codeClassName = codeElement?.props?.className || "";
const codeContent = extractCodeContent(children);
const language = extractLanguage(codeClassName);
// If it's a mermaid block, render with MermaidBlock component
if (language === "mermaid") {
return (
<MermaidBlock className={className} {...props}>
{children}
</MermaidBlock>
);
}
const theme = getThemeWithFallback(userStore.state.userGeneralSetting?.theme);
const resolvedTheme = resolveTheme(theme);
const isDarkTheme = resolvedTheme.includes("dark");
// Dynamically load highlight.js theme based on app theme
useEffect(() => {
const dynamicImportStyle = async () => {
// Remove any existing highlight.js style
const existingStyle = document.querySelector("style[data-hljs-theme]");
if (existingStyle) {
existingStyle.remove();
}
try {
const cssModule = isDarkTheme
? await import("highlight.js/styles/github-dark-dimmed.css?inline")
: await import("highlight.js/styles/github.css?inline");
// Create and inject the style
const style = document.createElement("style");
style.textContent = cssModule.default;
style.setAttribute("data-hljs-theme", isDarkTheme ? "dark" : "light");
document.head.appendChild(style);
} catch (error) {
console.warn("Failed to load highlight.js theme:", error);
}
};
dynamicImportStyle();
}, [resolvedTheme, isDarkTheme]);
// Highlight code using highlight.js
const highlightedCode = useMemo(() => {
try {
const lang = hljs.getLanguage(language);
if (lang) {
return hljs.highlight(codeContent, {
language: language,
}).value;
}
} catch {
// Skip error and use default highlighted code.
}
// Escape any HTML entities when rendering original content.
return Object.assign(document.createElement("span"), {
textContent: codeContent,
}).innerHTML;
}, [language, codeContent]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(codeContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy code:", err);
}
};
return (
<pre className="relative group">
<div className="w-full flex flex-row justify-between items-center">
<span className="text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider select-none">{language}</span>
<button
onClick={handleCopy}
className={cn("p-1.5 rounded-md transition-all", "hover:bg-accent/50", copied ? "text-primary" : "text-muted-foreground")}
aria-label={copied ? "Copied" : "Copy code"}
title={copied ? "Copied!" : "Copy code"}
>
{copied ? <CheckIcon className="w-3.5 h-3.5" /> : <CopyIcon className="w-3.5 h-3.5" />}
</button>
</div>
<div className={className} {...props}>
<code className={`language-${language}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
</div>
</pre>
);
});