import React, { useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkBreaks from "remark-breaks"; import rehypeRaw from "rehype-raw"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { Copy, Check } from "lucide-react"; import { useTheme } from "../context/ThemeContext"; // Sanitize schema that allows collapsible sections (details/summary) const sanitizeSchema = { ...defaultSchema, tagNames: [...(defaultSchema.tagNames || []), "details", "summary"], attributes: { ...defaultSchema.attributes, details: ["open"], // Allow the 'open' attribute for expanded by default }, }; // Copy button component for code blocks function CodeCopyButton({ code }: { code: string }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( ); } // Cursor Dark Theme colors for syntax highlighting const cursorDarkTheme: { [key: string]: React.CSSProperties } = { 'code[class*="language-"]': { color: "#d4d4d4", background: "#1e1e1e", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, }, 'pre[class*="language-"]': { color: "#d4d4d4", background: "#1e1e1e", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, padding: "1.5em", margin: "1.5em 0", overflow: "auto" as const, borderRadius: "8px", }, comment: { color: "#6a9955", fontStyle: "italic" }, prolog: { color: "#6a9955" }, doctype: { color: "#6a9955" }, cdata: { color: "#6a9955" }, punctuation: { color: "#d4d4d4" }, property: { color: "#9cdcfe" }, tag: { color: "#569cd6" }, boolean: { color: "#569cd6" }, number: { color: "#b5cea8" }, constant: { color: "#4fc1ff" }, symbol: { color: "#4fc1ff" }, deleted: { color: "#f44747" }, selector: { color: "#d7ba7d" }, "attr-name": { color: "#92c5f6" }, string: { color: "#ce9178" }, char: { color: "#ce9178" }, builtin: { color: "#569cd6" }, inserted: { color: "#6a9955" }, operator: { color: "#d4d4d4" }, entity: { color: "#dcdcaa" }, url: { color: "#9cdcfe", textDecoration: "underline" }, variable: { color: "#9cdcfe" }, atrule: { color: "#569cd6" }, "attr-value": { color: "#ce9178" }, function: { color: "#dcdcaa" }, "function-variable": { color: "#dcdcaa" }, keyword: { color: "#569cd6" }, regex: { color: "#d16969" }, important: { color: "#569cd6", fontWeight: "bold" }, bold: { fontWeight: "bold" }, italic: { fontStyle: "italic" }, namespace: { opacity: 0.7 }, "class-name": { color: "#4ec9b0" }, parameter: { color: "#9cdcfe" }, decorator: { color: "#dcdcaa" }, }; // Cursor Light Theme colors for syntax highlighting const cursorLightTheme: { [key: string]: React.CSSProperties } = { 'code[class*="language-"]': { color: "#171717", background: "#f5f5f5", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, }, 'pre[class*="language-"]': { color: "#171717", background: "#f5f5f5", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, padding: "1.5em", margin: "1.5em 0", overflow: "auto" as const, borderRadius: "8px", }, comment: { color: "#6a737d", fontStyle: "italic" }, prolog: { color: "#6a737d" }, doctype: { color: "#6a737d" }, cdata: { color: "#6a737d" }, punctuation: { color: "#24292e" }, property: { color: "#005cc5" }, tag: { color: "#22863a" }, boolean: { color: "#005cc5" }, number: { color: "#005cc5" }, constant: { color: "#005cc5" }, symbol: { color: "#e36209" }, deleted: { color: "#b31d28", background: "#ffeef0" }, selector: { color: "#22863a" }, "attr-name": { color: "#6f42c1" }, string: { color: "#032f62" }, char: { color: "#032f62" }, builtin: { color: "#005cc5" }, inserted: { color: "#22863a", background: "#f0fff4" }, operator: { color: "#d73a49" }, entity: { color: "#6f42c1" }, url: { color: "#005cc5", textDecoration: "underline" }, variable: { color: "#e36209" }, atrule: { color: "#005cc5" }, "attr-value": { color: "#032f62" }, function: { color: "#6f42c1" }, "function-variable": { color: "#6f42c1" }, keyword: { color: "#d73a49" }, regex: { color: "#032f62" }, important: { color: "#d73a49", fontWeight: "bold" }, bold: { fontWeight: "bold" }, italic: { fontStyle: "italic" }, namespace: { opacity: 0.7 }, "class-name": { color: "#6f42c1" }, parameter: { color: "#24292e" }, decorator: { color: "#6f42c1" }, }; // Tan Theme colors for syntax highlighting const cursorTanTheme: { [key: string]: React.CSSProperties } = { 'code[class*="language-"]': { color: "#1a1a1a", background: "#f0ece4", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, }, 'pre[class*="language-"]': { color: "#1a1a1a", background: "#f0ece4", fontFamily: "SF Mono, Monaco, Cascadia Code, Roboto Mono, Consolas, Courier New, monospace", fontSize: "14px", textAlign: "left" as const, whiteSpace: "pre" as const, wordSpacing: "normal", wordBreak: "normal" as const, wordWrap: "normal" as const, lineHeight: "1.6", tabSize: 4, hyphens: "none" as const, padding: "1.5em", margin: "1.5em 0", overflow: "auto" as const, borderRadius: "8px", }, comment: { color: "#7a7a7a", fontStyle: "italic" }, prolog: { color: "#7a7a7a" }, doctype: { color: "#7a7a7a" }, cdata: { color: "#7a7a7a" }, punctuation: { color: "#1a1a1a" }, property: { color: "#8b7355" }, tag: { color: "#8b5a2b" }, boolean: { color: "#8b5a2b" }, number: { color: "#8b5a2b" }, constant: { color: "#8b5a2b" }, symbol: { color: "#a67c52" }, deleted: { color: "#b31d28" }, selector: { color: "#6b8e23" }, "attr-name": { color: "#8b7355" }, string: { color: "#6b8e23" }, char: { color: "#6b8e23" }, builtin: { color: "#8b5a2b" }, inserted: { color: "#6b8e23" }, operator: { color: "#a67c52" }, entity: { color: "#8b7355" }, url: { color: "#8b7355", textDecoration: "underline" }, variable: { color: "#a67c52" }, atrule: { color: "#8b5a2b" }, "attr-value": { color: "#6b8e23" }, function: { color: "#8b7355" }, "function-variable": { color: "#8b7355" }, keyword: { color: "#8b5a2b" }, regex: { color: "#6b8e23" }, important: { color: "#8b5a2b", fontWeight: "bold" }, bold: { fontWeight: "bold" }, italic: { fontStyle: "italic" }, namespace: { opacity: 0.7 }, "class-name": { color: "#8b7355" }, parameter: { color: "#1a1a1a" }, decorator: { color: "#8b7355" }, }; interface BlogPostProps { content: string; } // Generate slug from heading text for anchor links function generateSlug(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") .trim(); } // Extract text content from React children function getTextContent(children: React.ReactNode): string { if (typeof children === "string") return children; if (Array.isArray(children)) { return children.map(getTextContent).join(""); } if (children && typeof children === "object" && "props" in children) { return getTextContent((children as React.ReactElement).props.children); } return ""; } export default function BlogPost({ content }: BlogPostProps) { const { theme } = useTheme(); const getCodeTheme = () => { switch (theme) { case "dark": return cursorDarkTheme; case "light": return cursorLightTheme; case "tan": return cursorTanTheme; default: return cursorDarkTheme; } }; return (
{children} ); } const codeString = String(children).replace(/\n$/, ""); return (
{match && {match[1]}} {codeString}
); }, img({ src, alt }) { return ( {alt {alt && {alt}} ); }, a({ href, children }) { const isExternal = href?.startsWith("http"); return ( {children} ); }, blockquote({ children }) { return (
{children}
); }, h1({ children }) { const id = generateSlug(getTextContent(children)); return (

{children}

); }, h2({ children }) { const id = generateSlug(getTextContent(children)); return (

{children}

); }, h3({ children }) { const id = generateSlug(getTextContent(children)); return (

{children}

); }, h4({ children }) { const id = generateSlug(getTextContent(children)); return (

{children}

); }, h5({ children }) { const id = generateSlug(getTextContent(children)); return (
{children}
); }, h6({ children }) { const id = generateSlug(getTextContent(children)); return (
{children}
); }, ul({ children }) { return ; }, ol({ children }) { return
    {children}
; }, li({ children }) { return
  • {children}
  • ; }, hr() { return
    ; }, // Table components for GitHub-style tables table({ children }) { return (
    {children}
    ); }, thead({ children }) { return {children}; }, tbody({ children }) { return {children}; }, tr({ children }) { return {children}; }, th({ children }) { return {children}; }, td({ children }) { return {children}; }, }} > {content}
    ); }