import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Download, Printer, Trash2 } from 'lucide-react'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; import CopyButton from '@/components/CopyButton'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import { Button } from '@/components/ui/button'; import { useStorageState } from '@/utils/useStorageState'; import type { MarkdownToHtmlPreviewMode } from '@/types/storage'; import { downloadHtmlFile, markdownToHtml, printHtml, SAMPLE_MARKDOWN, wrapHtmlDocument, } from '@/utils/markdownToHtml'; import { cn } from '@/lib/utils'; const isValidPreviewMode = (val: unknown): val is MarkdownToHtmlPreviewMode => typeof val === 'string' && ['split', 'preview', 'html'].includes(val); // 💡 规范回归:保持最纯净的通用选择器集合,内部变量全部交由全局 :root 驱动 const PREVIEW_STYLES = ` .markdown-body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: var(--md-foreground); background-color: transparent; font-size: 14px; } .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; color: var(--md-foreground); } .markdown-body h1 { border-bottom: 1px solid var(--md-border); padding-bottom: 0.3em; font-size: 1.6em; } .markdown-body h2 { border-bottom: 1px solid var(--md-border); padding-bottom: 0.3em; font-size: 1.35em; } .markdown-body p { margin-top: 0; margin-bottom: 16px; } .markdown-body a { color: #3b82f6; text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body code { background-color: var(--md-code-bg); border-radius: 4px; font-size: 85%; padding: 0.2em 0.4em; font-family: Menlo, Consolas, monospace; } .markdown-body pre { background-color: var(--md-pre-bg); border-radius: 8px; font-size: 85%; line-height: 1.45; overflow: auto; padding: 16px; margin: 0 0 16px; border: 1px solid var(--md-border); } .markdown-body pre code { background-color: transparent; border: 0; padding: 0; } .markdown-body blockquote { border-left: 0.25em solid var(--md-quote-line); color: var(--md-muted); margin: 0 0 16px; padding: 0 1em; } .markdown-body table { border-collapse: collapse; width: 100%; margin-bottom: 16px; font-size: 13px; } .markdown-body table th, .markdown-body table td { border: 1px solid var(--md-border); padding: 6px 13px; } .markdown-body table tr:nth-child(2n) { background-color: var(--md-code-bg); } .markdown-body table th { font-weight: 600; background-color: var(--md-code-bg); } `; export default function MarkdownToHtmlPage() { const { t } = useLazyTranslation('markdownToHtml'); const [previewMode, setPreviewMode] = useStorageState( 'markdownToHtml/previewMode', 'split' as MarkdownToHtmlPreviewMode, isValidPreviewMode, ); const [markdown, setMarkdown] = useState(SAMPLE_MARKDOWN); const iframeRef = useRef(null); const result = useMemo(() => markdownToHtml(markdown), [markdown]); const error = result.hasError ? (result.error ?? null) : null; // 💡 自适应大总管:以极高严谨度拼装明暗大闸,用标准换行切断粘连风险 useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const isDarkMode = document.documentElement.classList.contains('dark'); const themeVariables = isDarkMode ? `:root { --md-bg: #090d16; --md-foreground: #e6edf3; --md-border: rgba(255,255,255,0.15); --md-code-bg: rgba(255,255,255,0.12); --md-pre-bg: rgba(255,255,255,0.04); --md-muted: #8b949e; --md-quote-line: rgba(255,255,255,0.25); }` : `:root { --md-bg: #ffffff; --md-foreground: #1f2328; --md-border: rgba(128,128,128,0.2); --md-code-bg: rgba(128,128,128,0.08); --md-pre-bg: rgba(128,128,128,0.03); --md-muted: #4b5563; --md-quote-line: rgba(128,128,128,0.3); }`; // 💡 3. 核心大清洗:将全局基础树(html, body)与派生样式完全独立硬编码,杜绝任何语法踩踏 const baseGlobalStyles = ` html, body { margin: 0; padding: 0; width: 100%; height: 100%; background-color: var(--md-bg); color: var(--md-foreground); } body { padding: 16px; box-sizing: border-box; } `; iframe.srcdoc = ` ${result.html} `; }, [result.html, previewMode]); const handleModeChange = useCallback( (newMode: MarkdownToHtmlPreviewMode) => { setPreviewMode(newMode); }, [setPreviewMode], ); const handleClear = useCallback(() => { setMarkdown(''); }, []); const handlePrint = useCallback(() => { printHtml(result.html, t('pageTitle')); }, [result.html, t]); const handleDownload = useCallback(() => { const doc = wrapHtmlDocument(result.html, t('pageTitle')); downloadHtmlFile(doc, 'markdown-export.html'); }, [result.html, t]); const showInput = previewMode !== 'preview'; const showPreview = previewMode !== 'html'; return (
{/* 工具集成控制中枢 */}
{/* 错误拦截提示框 */} {error && (
{error}
)} {/* 主框架多栏联动排版轴 */}
{/* Markdown 输入翼终端 */} {showInput && (
{t('inputLabel')} {t('charCount', { count: markdown.length })}