PageSourceSearch

https://md2pdf.cc/js/app.js?v=1784603343

js md2pdf.cc collected 2026-09-25 21:47:13 UTC 301,731 bytes, 4,855 lines download raw bytes

1/**
2 * i18n TRANSLATIONS
3 */
4
5/**
6 * GA4 TRACKING HELPER
7 * Safely sends events to Google Analytics 4
8 */
9function trackEvent(eventName, params = {}) {
10    if (typeof gtag === 'function') {
11        const enrichedParams = {
12            'attributed_lp': sessionStorage.getItem('original_lp') || 'direct',
13            'current_path': window.location.pathname,
14            ...params
15        };
16        gtag('event', eventName, enrichedParams);
17    } else {
18        console.debug(`[GA4] Event: ${eventName}`, params);
19    }
20}
21
22const langMap = {
23    'en': {
24        seoTitle: "MD2PDF - Free Markdown to PDF Converter Online | md2pdf.cc",
25        seoDescription: "Export Mermaid.js diagrams (Flowcharts, Sequence, Gantt) to professional PDF documents instantly. 100% Client-side and private.",
26        h1Title: "Free Mermaid to PDF Converter",
27        h2Title: "Convert Mermaid Diagrams to Professional PDF",
28        p1: "Seamlessly transform Mermaid.js diagrams into professional PDF documents. Supports flowcharts, sequence diagrams, and more.",
29        p2: "MD2PDF (md2pdf.cc) is optimized for technical documentation, providing the best way to export Mermaid charts via Markdown.",
30        li1: "Privacy First: All processing occurs in the browser.",
31        li2: "Support for Flowcharts, Sequence Diagrams, Gantt Charts.",
32        li3: "Instant high-quality SVG to PDF rendering.",
33        mermaidTitle: "Mermaid Diagram Rendering",
34        aiExportTitle: "AI Chat to PDF Export",
35        newFile: "New File", newFolder: "New Folder", rename: "Rename", download: "Download MD", delete: "Delete",
36        syncDrive: "Cloud Sync (2-way)", exportPdf: "Export PDF", feedback: "Feedback", editorPh: "# Start your prompt journey...",
37        confirmDelete: "Delete this item?", welcomeTitle: "My Reports", welcomeFile: "Welcome.md",
38        welcomeContent: `# MD2PDF.cc\n## **The Professional Engine for the AI Era**\n<span style="color: #7c3aed; font-weight: bold;">Privacy First • 100% Client-Side</span> • Publication Quality PDF\n\n---\n\n## ✨ Key Features\n\n### 1. Math & Science ($LaTeX$)\nPerfect for DeepSeek, GPT-4o, or Claude outputs:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Diagrams (Mermaid)\nRender flowcharts and diagrams instantly:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Style| D[Beautiful Layout]\n\`\`\`\n\n### 3. Professional Features\n- [x] **Mermaid.js** support\n- [x] **GitHub Callouts**\n- [x] **Academic Footnotes**\n- [ ] **Custom CSS** (Coming soon)\n\n#### GitHub-Style Callouts\n> [!NOTE]\n> This is a **NOTE** for neutral but important information.\n\n> [!TIP]\n> This is a **TIP** to help users do things better or faster.\n\n> [!WARNING]\n> This is a **WARNING** for potentially dangerous or critical actions.\n\n> [!DANGER]\n> This is a **DANGER** block for critical errors or data loss risks.\n\n#### Academic Footnotes\nMarkdown is powerful[^1], but MD2PDF makes it elegant[^2].\n\n[^1]: Standard Markdown supports basic text formatting.\n[^2]: MD2PDF adds professional PDF output and advanced syntax.\n\n---\n\n## 🆕 Modern Note-Taking Support (Obsidian & Notion)\nCompatible with your favorite note-taking syntax:\n\n- **Highlights**: You can ==highlight important text== easily.\n- **WikiLinks**: Support for [[Internal Links]] styling.\n\n---\n\n## 🚀 Tech-Native Formatting\n\`\`\`javascript\n// Clean and professional code blocks\nfunction helloWorld() {\n  console.log("Hello from MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API for Developers\nAutomate your workflow:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 Click **"Export PDF"** to see the magic.`,
39        aiPrompt: "AI Assistant", guideTitle: "AI Assistant Integration", guideSub: "Instant Professional PDF", howItWorks: "How it works",
40        guideDesc: "MD2PDF enables AI agents to generate professional reports. Use the system prompt below so the AI provides direct URL commands for the address bar to generate documents instantly.",
41        copyPrompt: "Copy System Prompt", benefit1: "Pro Layout", benefit2: "Privacy First",
42        legalTitle: "Security & Terms", dataSecurity: "Data Security", termsOfService: "Terms of Service", privacyPolicy: "Privacy Policy",
43        dataSecurityDesc: "MD2PDF is a client-side application. Your Markdown content and files are stored only in your browser's local storage. We do not have servers that store your private documents, ensuring maximum privacy and security.",
44        termsDesc: "Use of md2pdf.cc is governed by Quanta Flow Inc. (Wyoming). We provide no warranties for data persistence in browser storage.",
45        privacyDesc: "Privacy-first: Documents are processed locally. We use minimal tracking for analytics and future advertising purposes.",
46        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
47        terms: "Terms", privacy: "Privacy", security: "Security",
48        themeLight: "Light", themeDark: "Dark", themeSepia: "Sepia", themeSolarized: "Solarized", themeOcean: "Ocean",
49        viewEditor: "Full Editor", viewSplit: "Split View", viewPreview: "Full Preview",
50        modalCancel: "Cancel", modalConfirm: "Confirm", newItemName: "Enter name",
51        renameTitle: "Rename", newFileTitle: "New File", newFolderTitle: "New Folder",
52        exporting: "Exporting...", exportSuccess: "Export Successful!", exportFail: "
52Export failed. Content might be too large.",
53        pdfStyle: "PDF Style", styleStandard: "Standard (GitHub)", styleAcademic: "Academic (Elegant)", styleModern: "Modern (Clean)", styleDefault: "System Default",
54        styleDefaultSimple: "Default", styleStandardSimple: "Standard", styleAcademicSimple: "Academic", styleModernSimple: "Modern",
55        styleUpdated: "Default PDF Style Updated",
56        syncStart: "Starting Bidirectional Sync...", syncSuccess: "All files synced to Google Drive!", syncFail: "Sync failed: ", syncReauth: "Authorization required. Please tap Sync again to connect Google Drive.",
57        installMsg: "Install MD2PDF for a better experience", installBtn: "Install Now", installSuccess: "App installed successfully!",
58        chromeInstallMsg: "Install MD2PDF as a Desktop App for quick access", chromeInstallBtn: "Install App",
59        iosInstallMsg: "Tap the share button then 'Add to Home Screen' to install as an App",
60        copy: "Copy", copyJson: "Copy JSON", copied: "Copied to clipboard",
61        language: "Language", theme: "Theme", copyContent: "Copy Content", copyEditor: "Copy Editor", copyPreview: "Copy Preview",
62        sidebarSubTitle: "Automated Documentation",
63        paste: "Paste from Clipboard",
64        recommended: "Recommended",
65        pdfSettings: "PDF Settings", pdfSettingsTitle: "PDF Customization", pdfPageSetup: "Page Setup", pdfCover: "Cover Page",
66        docTitle: "Document Title", docSubtitle: "Subtitle / Author", pdfTOC: "Table of Contents", pdfHeaderFooter: "Header & Footer",
67        pdfHeader: "Header", pdfFooter: "Footer", pdfMacros: "Macros: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Save & Close",
68        pdfLeft: "Left", pdfCenter: "Center", pdfRight: "Right",
69        pdfBranding: "The Professional AI Artifacts to PDF Engine",
70        exportSettings: "Export Settings",
71        coverShowHeader: "Show header/footer on cover",
72        disableBrowserHF: "💡 Tips: Disable browser H/F",
73        printDialogTip: "💡 In the print dialog, please disable browser's default headers/footers to use custom settings",
74        printDialogNote: "Note: Please turn off 'Headers and footers' in the print dialog",
75        headerLogoLeft: "Header Logo (Left)",
76        headerLogoRight: "Header Logo (Right)",
77        watermarkText: "Watermark Text",
78        uploadLogo: "Upload",
79        logoUrl: "Logo URL",
80        logoAndWatermark: "Logo & Watermark",
81        clear: "Clear",
82        watermark: "Watermark",
83        feedbackBtnTitle: "Share your feedback",
84        feedbackModalTitle: "Share Your Feedback 💬",
85        feedbackModalDesc: "Help us improve MD2PDF — your feedback matters!",
86        feedbackRatingLabel: "How's your experience?",
87        feedbackMsgLabel: "Your feedback",
88        feedbackMsgPh: "Tell us what you think — features, bugs, improvements...",
89        feedbackEmailPh: "[email protected]",
90        feedbackSubmitBtn: "Send Feedback",
91        feedbackSending: "Sending...",
92        feedbackSuccessTitle: "Thank you! 🎉",
93        feedbackSuccessMsg: "Your feedback has been received. We'll carefully review it and use it to make MD2PDF even better.",
94        feedbackErrorMsg: "Failed to send. Please check your connection and try again.",
95        feedbackNewsletterTitle: "Subscribe to our newsletter",
96        feedbackNewsletterDesc: "Markdown tips, MD2PDF updates & productivity tricks — English only, low frequency.",
97        feedbackEmailRequired: "Please enter a valid email address.",
98        exportBrandingTitle: "Remove PDF branding",
99        exportBrandingFree: "Free exports include the MD2PDF branding page.",
100        exportBrandingActive: "Branding-free export is active.",
101        exportViewPlans: "View plans",
102        account: "Account",
103        exportOneTime: "One-time payment, all devices",
104        exportMonthly: "Cancel anytime; active while subscribed",
105        exportFullPricing: "View full pricing",
106        licenseActivated: "MD2PDF access activated on this device."
107    },
108    'zh': {
109        seoTitle: "MD2PDF - 免费 Markdown 转 PDF 工具 (在线、隐私) | md2pdf.cc",
110        seoDescription: "一键将 Mermaid.js 流程图、序列图、甘特图转换为专业 PDF。纯前端处理,隐私安全,无需登录。",
111        h1Title: "免费 Mermaid 转 PDF 工具",
112        h2Title: "将 Mermaid 图表转换为专业 PDF",
113        p1: "无缝将 Mermaid.js 图表转换为专业 PDF 文档。支持流程图、序列图、甘特图等多种类型。",
114        p2: "MD2PDF (md2pdf.cc) 专为技术文档优化,是导出 Mermaid 图表的最佳 Markdown 方案。",
115        li1: "隐私优先:所有处理均在浏览器中完成,绝不上传服务器。",
116        li2: "完善支持流程图、时序图、甘特图、类图等。",
117        li3: "即时生成高质量 SVG 到 PDF 的完美转换。",
118        mermaidTitle: "Mermaid 流程图支持",
119        aiExportTitle: "AI 聊天内容导出",
120        newFile: "新建文件", newFolder: "新建文件夹", rename: "重命名", download: "下载源码", delete: "删除",
121        syncDrive: "云端双向同步", exportPdf: "导出 PDF", feedback: "反馈", editorPh: "# 开始你的提示词之旅...",
122        confirmDelete: "确认删除?", welcomeTitle: "我的文档", welcomeFile: "欢迎使用 MD2PDF",
123        welcomeContent: `# MD2PDF.cc\n## **为 AI 时代而生的专业排版引擎**\n<span style="color: #7c3aed; font-weight: bold;">隐私全掌控 · 100% 本地渲染</span>
123 · 出版级 A4 转换\n\n---\n\n## ✨ 核心功能\n\n### 1. 数学与科学 ($LaTeX$)\n完美支持 DeepSeek, GPT-4o 或 Claude 渲染:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. 图表渲染 (Mermaid)\n瞬间将代码转为流程图:\n\`\`\`mermaid\ngraph LR\n    A[AI 聊天] --> B{MD2PDF}\n    B -->|解析| C[PDF 报告]\n    B -->|样式| D[精美布局]\n\`\`\`\n\n### 3. 专业排版功能\n- [x] **Mermaid.js** 渲染\n- [x] **GitHub 呼号 (Callouts)**\n- [x] **学术级角标 (Footnotes)**\n- [ ] **自定义 CSS** (即将推出)\n\n#### GitHub 呼号支持\n> [!NOTE]\n> 这是一个 **NOTE** (备注),用于突出显示中性但重要的信息。\n\n> [!TIP]\n> 这是一个 **TIP** (提示),帮助用户更高效地完成任务。\n\n> [!WARNING]\n> 这是一个 **WARNING** (警告),提醒用户注意潜在的风险。\n\n> [!DANGER]\n> 这是一个 **DANGER** (危险),用于警示严重错误或风险。\n\n#### 学术级角标支持\nMarkdown 非常强大[^1],但 MD2PDF 让它更优雅[^2]。\n\n[^1]: 标准 Markdown 支持基础文本格式。\n[^2]: MD2PDF 增加了专业 PDF 输出和高级语法支持。\n\n---\n\n## 🆕 现代笔记支持 (Obsidian & Notion)\n兼容你最喜欢的笔记语法:\n\n- **文本高亮**:你可以像在 Obsidian 中一样 ==高亮重点内容==。\n- **双向链接 (WikiLinks)**:支持 [[内部链接]] 样式的语法。\n\n---\n\n## 🚀 技术原生排版\n\`\`\`javascript\n// 简洁专业的代码块显示\nfunction welcome() {\n  console.log("欢迎使用 MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ 开发者 API\n自动化你的工作流:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 点击 **“导出 PDF”** 查看效果。`,
124        aiPrompt: "AI 助手", guideTitle: "AI 助手集成指南", guideSub: "极速生成专业 PDF", howItWorks: "核心能力",
125        guideDesc: "MD2PDF 支持 AI 智能体生成专业报告。请使用下方的系统提示词,让 AI 为你提供直接的地址栏命令,复制到浏览器即可瞬间生成 PDF。",
126        copyPrompt: "复制 AI 系统提示词", benefit1: "专业排版", seoBenefit: "专为 AI 生成内容优化,确保 PDF 转换质量达到出版级标准。",
127        benefit2: "隐私安全", privacyBenefit: "所有文档处理均在客户端完成。通过链接传递的数据仅用于即时渲染,绝不上传至任何服务器。",
128        legalTitle: "安全与条款", dataSecurity: "数据安全", termsOfService: "使用协议", privacyPolicy: "隐私政策",
129        dataSecurityDesc: "MD2PDF 是一个客户端应用。您的 Markdown 内容和文件仅存储在浏览器的本地存储(Local Storage)中。我们不设存储您私有文档的服务器,确保最高的隐私和安全性。",
130        termsDesc: "本服务由 Quanta Flow Inc. (美国怀俄明州) 提供。用户须同意相关协议,我们不对浏览器本地存储的数据丢失承担责任。",
131        privacyDesc: "隐私优先:所有文档均在本地处理。我们仅收集少量匿名数据用于分析,未来可能包含经过合规处理的广告展示。",
132        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
133        terms: "服务条款", privacy: "隐私政策", security: "安全说明",
134        themeLight: "明亮", themeDark: "暗黑", themeSepia: "护眼", themeSolarized: "经典", themeOcean: "深蓝",
135        viewEditor: "全屏编辑", viewSplit: "分屏模式", viewPreview: "全屏预览",
136        modalCancel: "取消", modalConfirm: "确认", newItemName: "请输入名称",
137        renameTitle: "重命名", newFileTitle: "新建文件", newFolderTitle: "新建文件夹",
138        exporting: "正在导出...", exportSuccess: "导出成功!", exportFail: "导出失败,内容可能过大。",
139        pdfStyle: "导出的风格", styleStandard: "标准 (GitHub 风格)", styleAcademic: "学术 (典雅宋体)", styleModern: "现代 (极简无衬线)", styleDefault: "默认导出的风格",
140        styleDefaultSimple: "默认", styleStandardSimple: "标准", styleAcademicSimple: "学术", styleModernSimple: "现代",
141        styleUpdated: "默认导出风格已更新",
142        syncStart: "开始双向同步 (Google Drive)...", syncSuccess: "所有文件已同步到 Google Drive!", syncFail: "同步失败: ", syncReauth: "需要重新授权,请再次点击「云端同步」按钮以连接 Google Drive。",
143        installMsg: "安装 MD2PDF 到本地,随时随地开启创作", installBtn: "立
143即安装", installSuccess: "应用安装成功!您现在可以从应用列表直接启动。",
144        chromeInstallMsg: "检测到 Chrome 浏览器,建议安装为 App 以获得最佳体验", chromeInstallBtn: "安装为 App",
145        iosInstallMsg: "点击下方分享按钮,选择“添加到主屏幕”,即可像 App 一样使用",
146        copy: "复制内容", copyJson: "复制 JSON", copied: "已复制到剪贴板",
147        language: "选择语言", theme: "主题选择", copyContent: "复制内容", copyEditor: "复制编辑区", copyPreview: "复制预览区",
148        sidebarSubTitle: "自动化文档排版",
149        paste: "从剪贴板粘贴",
150        recommended: "为您推荐",
151        pdfSettings: "PDF 设置", pdfSettingsTitle: "PDF 自定义", pdfPageSetup: "页面设置", pdfCover: "封面页",
152        docTitle: "文档标题", docSubtitle: "副标题 / 作者", pdfTOC: "目录页 (TOC)", pdfHeaderFooter: "页眉与页脚",
153        pdfHeader: "页眉", pdfFooter: "页脚", pdfMacros: "支持变量: {date}, {title}, {pagenum}, {totalpages}", modalSave: "保存设置",
154        pdfLeft: "左侧", pdfCenter: "中间", pdfRight: "右侧",
155        pdfBranding: "专业级 AI 助手 PDF 排版引擎",
156        exportSettings: "导出设置",
157        coverShowHeader: "封面显示页眉/页脚",
158        disableBrowserHF: "💡 提示:请关闭浏览器默认页眉页脚",
159        printDialogTip: "💡 在打印对话框中,请关闭浏览器默认的页眉页脚选项,以使用自定义设置",
160        printDialogNote: "提示:请在打印对话框中关闭「页眉和页脚」选项",
161        headerLogoLeft: "页眉左侧 Logo",
162        headerLogoRight: "页眉右侧 Logo",
163        watermarkText: "文字水印",
164        uploadLogo: "上传图片",
165        logoUrl: "图片链接",
166        logoAndWatermark: "Logo 与水印",
167        clear: "清除",
168        watermark: "水印文字",
169        feedbackBtnTitle: "分享你的反馈",
170        feedbackModalTitle: "分享你的反馈 💬",
171        feedbackModalDesc: "帮助我们改善 MD2PDF — 你的每条反馈都很重要!",
172        feedbackRatingLabel: "你的使用体验如何?",
173        feedbackMsgLabel: "你的反馈",
174        feedbackMsgPh: "说说你的想法——功能、问题、建议都可以...",
175        feedbackEmailPh: "[email protected]",
176        feedbackSubmitBtn: "提交反馈",
177        feedbackSending: "提交中...",
178        feedbackSuccessTitle: "感谢你!🎉",
179        feedbackSuccessMsg: "你的反馈已收到,我们会认真阅读每条意见,并用它不断改善 MD2PDF。",
180        feedbackErrorMsg: "提交失败,请检查网络后重试。",
181        feedbackNewsletterTitle: "订阅我们的邮件列表",
182        feedbackNewsletterDesc: "Markdown 技巧、MD2PDF 产品更新与效率工具——仅英文,低频发送。",
183        feedbackEmailRequired: "请输入有效的电子邮箱地址。",
184        exportBrandingTitle: "移除 PDF 品牌信息",
185        exportBrandingFree: "免费导出会包含 MD2PDF 品牌页。",
186        exportBrandingActive: "已启用无品牌信息导出。",
187        exportViewPlans: "查看方案",
188        account: "账户",
189        exportOneTime: "一次性付款,所有设备可用",
190        exportMonthly: "可随时取消;订阅有效期内可用",
191        exportFullPricing: "查看完整价格方案",
192        licenseActivated: "MD2PDF 权益已在此设备上激活。"
193    },
194    'tw': {
195        seoTitle: "MD2PDF - 免費 Markdown 轉 PDF 工具 (線上、隱私) | md2pdf.cc",
196        seoDescription: "一鍵將 Mermaid.js 流程圖、序列圖、甘特圖轉換為專業 PDF。純前端處理,隱私安全,無需登錄。",
197        h1Title: "免費 Mermaid 轉 PDF 工具",
198        h2Title: "將 Mermaid 圖表轉換為專業 PDF",
199        p1: "無縫將 Mermaid.js 圖表轉換為專業 PDF 文檔。支援流程圖、序列圖、甘特圖等多種類型。",
200        p2: "MD2PDF (md2pdf.cc) 專為技術文檔優化,是匯出 Mermaid 圖表的最佳 Markdown 方案。",
201        li1: "隱私優先:所有處理均在瀏覽器中完成,絕不上傳伺服器。",
202        li2: "完善支援流程圖、時序圖、甘特圖、類圖等。",
203        li3: "即時生成高品質 SVG 到 PDF 的完美轉換。",
204        mermaidTitle: "Mermaid 流程圖支援",
205        aiExportTitle: "AI 聊天內容匯出",
206        newFile: "新建檔案", newFolder: "新建資料夾", rename: "重新命名", download: "下載源碼", delete: "刪除",
207        syncDrive: "雲端雙向同步", exportPdf: "匯出 PDF", feedback: "回饋", editorPh: "# 開始您的提示詞之旅...",
208        confirmDelete: "確認刪除?", welcomeTitle: "我的文件", welcomeFile: "歡迎使用 MD2PDF",
209        welcomeContent: `# MD2PDF.cc\n## **為 AI 時代而生的專業排版引擎**\n<span style="color: #7c3aed; font-weight: bold;">隱私全掌控 · 100% 本地渲染</span>
209 · 出版級 A4 轉換\n\n---\n\n## ✨ 核心功能\n\n### 1. 數學與科學 ($LaTeX$)\n完美支持 DeepSeek, GPT-4o 或 Claude 渲染:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. 圖表渲染 (Mermaid)\n瞬間將代碼轉為流程圖:\n\`\`\`mermaid\ngraph LR\n    A[AI 聊天] --> B{MD2PDF}\n    B -->|解析| C[PDF 報告]\n    B -->|樣式| D[精美佈局]\n\`\`\`\n\n### 3. 專業排版功能\n- [x] **Mermaid.js** 渲染\n- [x] **GitHub 呼號 (Callouts)**\n- [x] **學術級角標 (Footnotes)**\n- [ ] **自定義 CSS** (即將推出)\n\n#### GitHub 呼號支持\n> [!NOTE]\n> 這是一個 **NOTE** (備註),用於突出顯示中性但重要的資訊。\n\n> [!TIP]\n> 這是一個 **TIP** (提示),幫助用戶更高效地完成任務。\n\n> [!WARNING]\n> 這是一個 **WARNING** (警告),提醒用戶注意潛在的風險。\n\n> [!DANGER]\n> 這是一個 **DANGER** (危險),用於警示嚴重錯誤或風險。\n\n#### 學術級角標支持\nMarkdown 非常強大[^1],但 MD2PDF 讓它更優雅[^2]。\n\n[^1]: 標準 Markdown 支持基礎文本格式。\n[^2]: MD2PDF 增加了專業 PDF 輸出和高級語法支持。\n\n---\n\n## 🆕 現代筆記支持 (Obsidian & Notion)\n相容您最喜歡的筆記語法:\n\n- **文本高亮**:您可以像在 Obsidian 中一樣 ==高亮重點內容==。\n- **雙向連結 (WikiLinks)**:支持 [[內部連結]] 樣式的語法。\n\n---\n\n## 🚀 技術原生排版\n\`\`\`javascript\n// 簡潔專業的代碼塊顯示\nfunction welcome() {\n  console.log("歡迎使用 MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ 開發者 API\n自動化您的工作流程:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 點擊 **「匯出 PDF」** 查看效果。`,
210        aiPrompt: "AI 助手", guideTitle: "AI 助手一鍵匯出指南", guideSub: "極速生成專業 PDF", howItWorks: "核心能力",
211        copyPrompt: "複製 AI 系統提示詞", benefit1: "排版優化", benefit2: "隱私安全",
212        legalTitle: "安全與條款", dataSecurity: "數據安全", termsOfService: "使用協議", privacyPolicy: "隱私政策",
213        dataSecurityDesc: "MD2PDF 是一个客户端应用。您的 Markdown 內容和檔案僅儲存在瀏覽器的本地儲存(Local Storage)中。我們不設儲存您私有文檔的服務器,確保最高的隱私和安全性。",
214        termsDesc: "本服務由 Quanta Flow Inc. (美國懷俄明州) 提供。用戶須同意相關協議,我們不對瀏覽器本地儲存的數據丟失承擔責任。",
215        privacyDesc: "隱私優先:所有文檔均在本地處理。我們僅收集少量匿名數據用於分析,未來可能包含經過合規處理的廣告展示。",
216        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
217        terms: "服務條款", privacy: "隱私政策", security: "安全說明",
218        themeLight: "明亮", themeDark: "暗黑", themeSepia: "護眼", themeSolarized: "經典", themeOcean: "深藍",
219        viewEditor: "全螢幕編輯", viewSplit: "分屏模式", viewPreview: "全螢幕預覽",
220        modalCancel: "確認", modalConfirm: "確認", newItemName: "請輸入名稱",
221        renameTitle: "重新命名", newFileTitle: "新建檔案", newFolderTitle: "新建資料夾",
222        pdfStyle: "導出的風格", styleStandard: "標準 (GitHub 風格)", styleAcademic: "學術 (典雅宋體)", styleModern: "現代 (極簡無襯線)", styleDefault: "默認導出的風格",
223        styleDefaultSimple: "默認", styleStandardSimple: "標準", styleAcademicSimple: "學術", styleModernSimple: "現代",
224        styleUpdated: "默認 PDF 風格已更新",
225        syncStart: "開始雙向同步 (Google Drive)...", syncSuccess: "所有檔案已同步到 Google Drive!", syncFail: "同步失敗: ", syncReauth: "需要重新授權,請再次點擊「雲端同步」按鈕以連接 Google Drive。",
226        installMsg: "安裝 MD2PDF 到本地,隨時隨地開啟創作", installBtn: "立
226即安裝", installSuccess: "應用安裝成功!您現在可以从应用列表直接启动。",
227        iosInstallMsg: "點擊下方分享按鈕,選擇「加入主畫面」,即可像 App 一样使用",
228        copy: "複製內容", copyJson: "複製 JSON", copied: "已複製到剪贴板",
229        language: "選擇語言", theme: "主題選擇", copyContent: "複製內容", copyEditor: "複製編輯區", copyPreview: "複製預覽區",
230        sidebarSubTitle: "自動化文檔排版",
231        recommended: "為您推薦",
232        pdfSettings: "PDF 設定", pdfSettingsTitle: "PDF 自定義", pdfPageSetup: "頁面設置", pdfCover: "封面頁",
233        docTitle: "文件標題", docSubtitle: "副標題 / 作者", pdfTOC: "目錄頁 (TOC)", pdfHeaderFooter: "頁首與頁尾",
234        pdfHeader: "頁首", pdfFooter: "頁尾", pdfMacros: "支援變數: {date}, {title}, {pagenum}, {totalpages}", modalSave: "儲存設置",
235        pdfLeft: "左側", pdfCenter: "中間", pdfRight: "右側",
236        pdfBranding: "專業級 AI 助手 PDF 排版引擎",
237        exportSettings: "匯出設定",
238        coverShowHeader: "封面顯示頁首/頁尾",
239        disableBrowserHF: "💡 提示:請關閉瀏覽器預設頁首頁尾",
240        printDialogTip: "💡 在打印對話框中,請關閉瀏覽器預設的頁首頁尾選項,以使用自訂設置",
241        printDialogNote: "提示:請在打印對話框中關閉「頁首和頁尾」選項",
242        headerLogoLeft: "頁眉左側 Logo",
243        headerLogoRight: "頁眉右側 Logo",
244        watermarkText: "文字浮水印",
245        uploadLogo: "上傳圖片",
246        logoUrl: "圖片連結",
247        logoAndWatermark: "Logo 與浮水印",
248        clear: "清除",
249        watermark: "浮水印文字",
250        feedbackBtnTitle: "分享你的回饋",
251        feedbackModalTitle: "分享你的回饋 💬",
252        feedbackModalDesc: "幫助我們改善 MD2PDF — 你的每條回饋都很重要!",
253        feedbackRatingLabel: "你的使用體驗如何?",
254        feedbackMsgLabel: "你的回饋",
255        feedbackMsgPh: "說說你的想法——功能、問題、建議都可以...",
256        feedbackEmailPh: "[email protected]",
257        feedbackSubmitBtn: "提交回饋",
258        feedbackSending: "提交中...",
259        feedbackSuccessTitle: "感謝你!🎉",
260        feedbackSuccessMsg: "你的回饋已收到,我們會認真閱讀每條意見,並持續改善 MD2PDF。",
261        feedbackErrorMsg: "提交失敗,請檢查網路後重試。",
262        feedbackNewsletterTitle: "訂閱我們的電子報",
263        feedbackNewsletterDesc: "Markdown 技巧、MD2PDF 產品更新與效率工具——僅英文,低頻發送。",
264        feedbackEmailRequired: "請輸入有效的電子郵件地址。"
265    },
266    'es': {
267        seoTitle: "MD2PDF - Convertidor Gratis de Markdown a PDF | md2pdf.cc",
268        seoDescription: "Convierte chats de IA en PDFs profesionales al instante. Soporta fórmulas KaTeX, diagramas Mermaid y sintaxis Obsidian. 100% del lado del cliente, sin registro.",
269        h1Title: "Convertidor Profesional de ChatGPT y DeepSeek a PDF",
270        mermaidTitle: "Renderizado de Diagramas Mermaid",
271        aiExportTitle: "Exportación de Chat de IA a PDF",
272        newFile: "Nuevo archivo", newFolder: "Nueva carpeta", rename: "Renombrar", download: "Descargar MD", delete: "Eliminar",
273        syncDrive: "Sincronización (2-vías)", exportPdf: "Exportar PDF", feedback: "Comentarios", editorPh: "# Comienza tu viaje...",
274        confirmDelete: "¿Eliminar este elemento?", welcomeTitle: "Mis informes", welcomeFile: "Bienvenido.md",
275        welcomeContent: `# MD2PDF.cc\n## **El motor profesional para la era de la IA**\n<span style="color: #7c3aed; font-weight: bold;">Privacidad primero • 100% del lado del cliente</span> • PDF de calidad de publicación\n\n---\n\n## ✨ Características principales\n\n### 1. Matemáticas y Ciencias ($LaTeX$)\nPerfectos para DeepSeek, GPT-4o o Claude:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Diagramas (Mermaid)\nRenderiza diagramas de flujo al instante:\n\\\`\\\`\\\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parsear| C[Informe PDF]\n    B -->|Estilo| D[Hermoso diseño]\n\\\`\\\`\\\`\n\n### 3. Funciones Profesionales\n- [x] Soporte **Mermaid.js**\n- [x] **GitHub Callouts**\n- [x] **Notas al pie académicas**\n- [ ] **CSS personalizado** (Próximamente)\n\n#### Callouts estilo GitHub\n> [!NOTE]\n> Esta es una **NOTA** para información neutral pero importante.\n\n> [!TIP]\n> Esta es una **SUGERENCIA** para ayudar a los usuarios a hacer las cosas mejor.\n\n> [!WARNING]\n> Esta es una **ADVERTENCIA** para acciones potencialmente peligrosas.\n\n> [!DANGER]\n> Este es un bloque de **PELIGRO** para errores críticos o riesgos.\n\n#### Notas al pie académicas\nMarkdown es potente[^1], pero MD2PDF lo hace elegante[^2].\n\n[^1]: El Markdown estándar admite formato de texto básico.\n[^2]: MD2PDF añade salida PDF profesional y sintaxis avanzada.\n\n---\n\n## 🆕 Soporte para notas modernas (Obsidian & Notion)\nCompatible con tu sintaxis favorita para tomar notas:\n\n- **Resaltado**: Puedes ==resaltar texto importante== fácilmente.\n- **WikiLinks**: Soporte para el estilo de [[Enlaces internos]].\n\n---\n\n## 🚀 Formato Nativo Técnico\n\\\`\\\`\\\`javascript\n// Bloques de código limpios y profesionales\nfunction holaMundo() {\n  console.log(\\"¡Hola de
275sde MD2PDF!\\");\n}\n\\\`\\\`\\\`\n\n---\n\n## 🛠️ API para desarrolladores\nAutomatiza tu flujo de trabajo:\n\\\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\\\`\n\n👇 Haz clic en **\\"Exportar PDF\\"** para ver la magia.`,
276        aiPrompt: "Asistente AI", guideTitle: "Guía de exportación AI", guideSub: "PDF profesional instantáneo", howItWorks: "Características",
277        copyPrompt: "Copiar Prompt", benefit1: "Diseño Pro", benefit2: "Privacidad",
278        legalTitle: "Seguridad y Términos", dataSecurity: "Seguridad de Datos", termsOfService: "Términos de Uso", privacyPolicy: "Política de Privacidad",
279        dataSecurityDesc: "MD2PDF es una aplicación del lado del cliente. Su contenido de Markdown y archivos se almacenan únicamente en el almacenamiento local de su navegador. No mantenemos servidores que almacenen sus documentos privados, garantizando la máxima privacidad y seguridad.",
280        termsDesc: "El uso de md2pdf.cc está regido por Quanta Flow Inc. (Wyoming). No ofrecemos garantías por la persistencia de datos en el almacenamiento del navegador.",
281        privacyDesc: "Privacidad primero: los documentos se procesan localmente. Usamos seguimiento mínimo para análisis y futuros fines publicitarios.",
282        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
283        terms: "Términos", privacy: "Privacidad", security: "Seguridad",
284        themeLight: "Claro", themeDark: "Oscuro", themeSepia: "Sepia", themeSolarized: "Solarized", themeOcean: "Océano",
285        viewEditor: "Editor Completo", viewSplit: "Vista Dividida", viewPreview: "Vista Previa",
286        modalCancel: "Cancelar", modalConfirm: "Confirmar", newItemName: "Ingrese nombre",
287        renameTitle: "Renombrar", newFileTitle: "Nuevo Archivo", newFolderTitle: "Nueva Carpeta",
288        exporting: "Exportando...", exportSuccess: "¡Exportación exitosa!", exportFail: "Error de exportación. El contenido puede ser demasiado grande.",
289        pdfStyle: "Estilo PDF", styleStandard: "Estándar (GitHub)", styleAcademic: "Académico (Elegante)", styleModern: "Moderno (Limpio)", styleDefault: "Predeterminado del sistema",
290        styleDefaultSimple: "Predeterm.", styleStandardSimple: "Estándar", styleAcademicSimple: "Académico", styleModernSimple: "Moderno",
291        styleUpdated: "Estilo PDF actualizado",
292        syncStart: "Iniciando sincronización bidireccional...", syncSuccess: "¡Todos los archivos sincronizados!", syncFail: "Sincronización fallida: ", syncReauth: "Se requiere autorización. Toca Sincronizar nuevamente para conectar Google Drive.",
293        installMsg: "Instalar MD2PDF en el escritorio", installBtn: "Instalar", installSuccess: "¡Aplicación instalada con éxito!",
294        iosInstallMsg: "Toca el botón compartir y luego 'Añadir a la pantalla de inicio' para instalarlo",
295        copy: "Copiar", copyJson: "Copiar JSON", copied: "Copiado al portapapeles",
296        language: "Idioma", theme: "Tema", copyContent: "Copiar Contenido", copyEditor: "Copiar Editor", copyPreview: "Copiar Vista Previa",
297        sidebarSubTitle: "Documentación Automatizada",
298        paste: "Pegar desde el portapapeles",
299        recommended: "Recomendado",
300        pdfSettings: "Ajustes PDF", pdfSettingsTitle: "Personalización PDF", pdfPageSetup: "Configuración de Página", pdfCover: "Portada",
301        docTitle: "Título del Documento", docSubtitle: "Subtítulo / Autor", pdfTOC: "Índice (TOC)", pdfHeaderFooter: "Encabezado y Pie de página",
302        pdfHeader: "Encabezado", pdfFooter: "Pie de página", pdfMacros: "Macros: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Guardar y Cerrar",
303        pdfLeft: "Izquierda", pdfCenter: "Centro", pdfRight: "Derecha",
304        pdfBranding: "El motor profesional de PDF para artefactos de IA",
305        exportSettings: "Configuración de exportación",
306        coverShowHeader: "Mostrar encabezado/pie en portada",
307        disableBrowserHF: "💡 Consejos: Desactivar H/P",
308        printDialogTip: "💡 En el diálogo de impresión, desactive los encabezados/pies predeterminados del navegador para usar configuraciones personalizadas",
309        printDialogNote: "Nota: Desactive 'Encabezados y pies de página' en el diálogo de impresión",
310        headerLogoLeft: "Logo de encabezado (izq.)",
311        headerLogoRight: "Logo de encabezado (der.)",
312        watermarkText: "Marca de agua",
313        uploadLogo: "Subir",
314        logoUrl: "URL del logo",
315        logoAndWatermark: "Logo y Marca de Agua",
316        clear: "Eliminar",
317        watermark: "Marca de agua",
318        feedbackBtnTitle: "Compartir opinión",
319        feedbackModalTitle: "Compartir tu opinión 💬",
320        feedbackModalDesc: "Ayúdanos a mejorar MD2PDF — ¡tu opinión importa!",
321        feedbackRatingLabel: "¿Cómo es tu experiencia?",
322        feedbackMsgLabel: "Tu opinión",
323        feedbackMsgPh: "Cuéntanos qué piensas — funciones, errores, mejoras...",
324        feedbackEmailPh: "[email protected]",
325        feedbackSubmitBtn: "Enviar opinión",
326        feedbackSending: "Enviando...",
327        feedbackSuccessTitle: "¡Gracias! 🎉",
328        feedbackSuccessMsg: "Tu opinión ha sido recibida. La leeremos con cuidado y la usaremos para mejorar MD2PDF.",
329        feedbackErrorMsg: "Error al enviar. Comprueba tu conexión e inténtalo de nuevo.",
330        feedbackNewsletterTitle: "Suscríbete a nuestro boletín",
331        feedbackNewsletterDesc: "Trucos de Markdown, actualizaciones de MD2PDF y consejos de productividad — solo en inglés, baja frecuencia.",
332        feedbackEmailRequired: "Por favor, introduce una dirección de correo válida."
333    },
334    'de': {
335        seoTitle: "MD2PDF - Kostenloser Markdown zu PDF Konverter | md2pdf.cc",
336        seoDescription: "Verwandeln Sie KI-Chats sofort in professionelle PDFs. Unterstützt KaTeX-Formeln, Mermaid-Diagramme und Obsidian-Syntax. 100% Client-seitig, kein Login.",
337        h1Title: "Professioneller ChatGPT & DeepSeek zu PDF Konverter",
338        mermaidTitle: "Mermaid Diagramm Rendering",
339        aiExportTitle: "AI Chat zu PDF Export",
340        newFile: "Neue Datei", newFolder: "Neuer Ordner", rename: "Umbenennen", download: "MD herunterladen", delete: "Löschen",
341        syncDrive: "Cloud-Synchronisierung (2-Wege)", exportPdf: "PDF exportieren", feedback: "Feedback", editorPh: "# Starten Sie Ihre Reise...",
342        confirmDelete: "Dieses Element löschen?", welcomeTitle: "Meine Berichte", welcomeFile: "Willkommen.md",
343        welcomeContent: `# MD2PDF.cc\n## **Die professionelle Engine für die KI-Ära**\n<span style="color: #7c3aed; font-weight: bold;">Datenschutz zuerst • 100% Client-seitig</span> • PDF in Publikationsqualität\n\n---\n\n## ✨ Hauptmerkmale\n\n### 1. Mathematik & Wissenschaft ($LaTeX$)\nPerfekt für DeepSeek, GPT-4o oder Claude-Ausgaben:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Diagramme (Mermaid)\nFlussdiagramme sofort rendern:\n\`\`\`mermaid\ngraph LR\n    A[KI-Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF-Bericht]\n    B -->|Stil| D[Schönes Layout]\n\`\`\`\n\n### 3. Professionelle Funktionen\n- [x] **Mermaid.js** Unterstützung\n- [x] **GitHub-Callouts**\n- [x] **Akademische Fußnoten**\n- [ ] **Benutzerdefiniertes CSS** (Demnächst)\n\n#### GitHub-Style Callouts\n> [!NOTE]\n> Dies ist eine **NOTIZ** für neutrale, aber wichtige Informationen.\n\n> [!TIP]\n> Dies ist ein **TIPP**, um Benutzern zu helfen, Dinge besser oder schneller zu machen.\n\n> [!WARNING]\n> Dies ist eine **WARNUNG** für potenziell gefährliche oder kritische Aktionen.\n\n> [!DANGER]\n> Dies ist ein **GEFAHR**-Block für kritische Fehler oder Datenverlustrisiken.\n\n#### Akademische Fußnoten\nMarkdown ist mächtig[^1], aber MD2PDF macht es elegant[^2].\n\n[^1]: Standard-Markdown unterstützt grundlegende Textformatierung.\n[^2]: MD2PDF bietet professionelle PDF-Ausgabe und erweiterte Syntax.\n\n---\n\n## 🆕 Unterstützung für moderne Notizen (Obsidian & Notion)\nKompatibel mit Ihrer bevorzugten Notiz-Syntax:\n\n- **Highlights**: Sie können ==wichtigen Text markieren==.\n- **WikiLinks**: Unterstützung für [[Interne Links]].\n\n---\n\n## 🚀 Technologie-原生 Formatierung\n\`\`\`javascript\n// Saubere und professionelle Codeblöcke\nfunction helloWorld() {\n  console.log("Hallo von MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API für Entwickler\nAutomatisieren Sie Ihren Workflow:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}
343\`\n\n👇 Klicken Sie auf **"PDF exportieren"**, um die Magie zu sehen.`,
344        aiPrompt: "KI-Assistent", guideTitle: "KI-Assistent Integration", guideSub: "Sofortiges professionelles PDF", howItWorks: "Funktionsweise",
345        guideDesc: "MD2PDF ermöglicht es KI-Agenten, professionelle Berichte zu erstellen. Verwenden Sie den folgenden System-Prompt, damit die KI direkte URL-Befehle für die Adressleiste bereitstellt, um Dokumente sofort zu generieren.",
346        copyPrompt: "System-Prompt kopieren", benefit1: "Pro Layout", benefit2: "Datenschutz zuerst",
347        legalTitle: "Sicherheit & Bedingungen", dataSecurity: "Datensicherheit", termsOfService: "Nutzungsbedingungen", privacyPolicy: "Datenschutzerklärung",
348        dataSecurityDesc: "MD2PDF ist eine clientseitige Anwendung. Ihre Markdown-Inhalte und Dateien werden nur im lokalen Speicher Ihres Browsers gespeichert. Wir haben keine Server, die Ihre privaten Dokumente speichern, was maximale Privatsphäre und Sicherheit gewährleistet.",
349        termsDesc: "Die Nutzung von md2pdf.cc unterliegt Quanta Flow Inc. (Wyoming). Wir geben keine Garantien für die Datenausdauer im Browserspeicher.",
350        privacyDesc: "Datenschutz zuerst: Dokumente werden lokal verarbeitet. Wir verwenden minimales Tracking für Analysen und zukünftige Werbezwecke.",
351        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
352        terms: "Bedingungen", privacy: "Datenschutz", security: "Sicherheit",
353        themeLight: "Hell", themeDark: "Dunkel", themeSepia: "Sepia", themeSolarized: "Solarized", themeOcean: "Ozean",
354        viewEditor: "Vollbild Editor", viewSplit: "Geteilte Ansicht", viewPreview: "Vollbild Vorschau",
355        modalCancel: "Abbrechen", modalConfirm: "Bestätigen", newItemName: "Namen eingeben",
356        renameTitle: "Umbenennen", newFileTitle: "Neue Datei", newFolderTitle: "Neuer Ordner",
357        exporting: "Exportieren...", exportSuccess: "Export erfolgreich!", exportFail: "Export fehlgeschlagen. Der Inhalt ist möglicherweise zu groß.",
358        pdfStyle: "PDF-Stil", styleStandard: "Standard (GitHub)", styleAcademic: "Akademisch (Elegant)", styleModern: "Modern (Clean)", styleDefault: "Systemstandard",
359        styleDefaultSimple: "Standard", styleStandardSimple: "Klassisch", styleAcademicSimple: "Akademisch", styleModernSimple: "Modern",
360        styleUpdated: "Standard-PDF-Stil aktualisiert",
361        syncStart: "Synchronisierung startet...", syncSuccess: "Dateien mit Google Drive synchronisiert!", syncFail: "Synchronisierung fehlgeschlagen: ", syncReauth: "Autorisierung erforderlich. Tippen Sie erneut auf Synchronisieren, um Google Drive zu verbinden.",
362        installMsg: "Installieren Sie MD2PDF für eine bessere Erfahrung", installBtn: "Jetzt installieren", installSuccess: "App erfolgreich installiert!",
363        chromeInstallMsg: "MD2PDF als Desktop-App installieren", chromeInstallBtn: "App installieren",
364        iosInstallMsg: "Tippen Sie auf Teilen und dann auf 'Zum Home-Bildschirm'",
365        copy: "Kopieren", copyJson: "JSON kopieren", copied: "In Zwischenablage kopiert",
366        language: "Sprache", theme: "Thema", copyContent: "Inhalt kopieren", copyEditor: "Editor kopieren", copyPreview: "Vorschau kopieren",
367        sidebarSubTitle: "Automatisierte Dokumentation",
368        paste: "Aus Zwischenablage einfügen",
369        recommended: "Empfohlen",
370        pdfSettings: "PDF-Einstellungen", pdfSettingsTitle: "PDF-Anpassung", pdfPageSetup: "Seiteneinrichtung", pdfCover: "Titelseite",
371        docTitle: "Dokumenttitel", docSubtitle: "Untertitel / Autor", pdfTOC: "Inhaltsverzeichnis", pdfHeaderFooter: "Kopf- & Fußzeile",
372        pdfHeader: "Kopfzeile", pdfFooter: "Fußzeile", pdfMacros: "Makros: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Speichern & Schließen",
373        pdfLeft: "Links", pdfCenter: "Mitte", pdfRight: "Rechts",
374        pdfBranding: "Die professionelle KI-Artefakt-zu-PDF-Engine",
375        exportSettings: "Export-Einstellungen",
376        coverShowHeader: "Kopf/Fuß auf Cover zeigen",
377        disableBrowserHF: "💡 Tipp: Browser K/F deaktivieren",
378        printDialogTip: "💡 Deaktivieren Sie im Druckdialog die Standard-Kopf-/Fußzeilen des Browsers",
379        printDialogNote: "Hinweis: Bitte schalten Sie 'Kopf- und Fußzeilen' im Druckdialog aus",
380        headerLogoLeft: "Kopfzeilen-Logo (Links)",
381        headerLogoRight: "Kopfzeilen-Logo (Rechts)",
382        watermarkText: "Wasserzeichen-Text",
383        uploadLogo: "Hochladen",
384        logoUrl: "Logo-URL",
385        logoAndWatermark: "Logo & Wasserzeichen",
386        clear: "Löschen",
387        watermark: "Wasserzeichen",
388        feedbackBtnTitle: "Feedback teilen",
389        feedbackModalTitle: "Feedback teilen 💬",
390        feedbackModalDesc: "Helfen Sie uns, MD2PDF zu verbessern — Ihr Feedback ist wichtig!",
391        feedbackRatingLabel: "Wie ist Ihre Erfahrung?",
392        feedbackMsgLabel: "Ihr Feedback",
393        feedbackMsgPh: "Teilen Sie Ihre Gedanken mit — Funktionen, Fehler, Verbesserungen...",
394        feedbackEmailPh: "[email protected]",
395        feedbackSubmitBtn: "Feedback senden",
396        feedbackSending: "Wird gesendet...",
397        feedbackSuccessTitle: "Vielen Dank! 🎉",
398        feedbackSuccessMsg: "Ihr Feedback wurde empfangen. Wir lesen es sorgfältig und nutzen es zur Verbesserung von MD2PDF.",
399        feedbackErrorMsg: "Senden fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
400        feedbackNewsletterTitle: "Unseren Newsletter abonnieren",
401        feedbackNewsletterDesc: "Markdown-Tipps, MD2PDF-Updates & Produktivitätstricks — nur auf Englisch, niedrige Frequenz.",
402        feedbackEmailRequired: "Bitte geben Sie eine gültige E-Mail-Adresse ein."
403    },
404    'ru': {
405        seoTitle: "MD2PDF - Бесплатный конвертер Markdown в PDF | md2pdf.cc",
406        seoDescription: "Мгновенно превращайте чаты ИИ в профессиональные PDF. Поддержка формул KaTeX, диаграмм Mermaid и синтаксиса Obsidian. 100% на стороне клиента, без регистрации.",
407        h1Title: "Профессиональный конвертер ChatGPT и DeepSeek в PDF",
408        mermaidTitle: "Отрисовка диаграмм Mermaid",
409        aiExportTitle: "Экспорт ИИ чатов в PDF",
410        newFile: "Новый файл", newFolder: "Новая папка", rename: "Переименовать", download: "Скачать MD", delete: "Удалить",
411        syncDrive: "Облачная синхронизация", exportPdf: "Экспорт в PDF", feedback: "Отзыв", editorPh: "# Начните свой путь...",
412        confirmDelete: "Удалить этот элемент?", welcomeTitle: "Мои отчеты", welcomeFile: "Добро-пожаловать.md",
413        welcomeContent: `# MD2PDF.cc\n## **Профессиональный движок для эры ИИ**\n<span style="color: #7c3aed; font-weight: bold;">Приватность прежде всего • 100% на стороне клиента</sp
413an> • PDF высокого качества\n\n---\n\n## ✨ Ключевые особенности\n\n### 1. Математика и наука ($LaTeX$)\nИдеально для DeepSeek, GPT-4o или Claude:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Диаграммы (Mermaid)\nМгновенная отрисовка блок-схем:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Style| D[Beautiful Layout]\n\`\`\`\n\n### 3. Профессиональные функции\n- [x] Поддержка **Mermaid.js**\n- [x] **GitHub Callouts**\n- [x] **Академические сноски**\n- [ ] **Пользовательский CSS** (Скоро)\n\n#### Callouts в стиле GitHub\n> [!NOTE]\n> Это **ПРИМЕЧАНИЕ** для нейтральной, но важной информации.\n\n> [!TIP]\n> Это **СОВЕТ**, который поможет пользователям делать вещи лучше или быстрее.\n\n> [!WARNING]\n> Это **ПРЕДУПРЕЖДЕНИЕ** о потенциально опасных действиях.\n\n> [!DANGER]\n> Это блок **ОПАСНОСТЬ** для критических ошибок или рисков потери данных.\n\n#### Академические сноски\nMarkdown — это мощно[^1], но MD2PDF делает его элегантным[^2].\n\n[^1]: Стандартный Markdown поддерживает базовое форматирование текста.\n[^2]: MD2PDF добавляет профессиональный вывод PDF и расширенный синтаксис.\n\n---\n\n## 🆕 Современные заметки (Obsidian & Notion)\nСовместимо с вашим любимым синтаксисом:\n\n- **Выделение**: Вы можете легко ==выделять важный текст==.\n- **WikiLinks**: Поддержка [[Внутренних ссылок]].\n\n---\n\n## 🚀 Технологичный формат\n\`\`\`javascript\n// Чистые и профессиональные блоки кода\nfunction helloWorld() {\n  console.log("Привет от MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API для разработчиков\nАвтоматизируйте свой рабочий процесс:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 Нажмите **"Экспорт в PDF"**, чтобы увидеть магию.`,
414        aiPrompt: "ИИ-ассистент", guideTitle: "Интеграция ИИ-ассистента", guideSub: "Мгновенный профессиональный PDF", howItWorks: "Как это работает",
415        guideDesc: "MD2PDF позволяет ИИ-агентам создавать профессиональные отчеты. Используйте системный промпт ниже, чтобы ИИ предоставлял прямые URL-команды для адресной строки для мгновенной генерации документов.",
416        copyPrompt: "Копировать системный промпт", benefit1: "Про-макет", benefit2: "Приватность",
417        legalTitle: "Безопасность и условия", dataSecurity: "Безопасность данных", termsOfService: "Условия использования", privacyPolicy: "Политика конфиденциальности",
418        dataSecurityDesc: "MD2PDF — это клиентское приложение. Ваш контент Markdown и файлы хранятся только в локальном хранилище вашего браузера. У нас нет серверов, хранящих ваши документы, что обеспечивает полную безопасность.",
419        termsDesc: "Использование md2pdf.cc регулируется Quanta Flow Inc. (Вайоминг). Мы не даем гарантий сохранности данных в браузере.",
420        privacyDesc: "Приватность прежде всего: документы обрабатываются локально. Мы используем минимальное отслеживание для аналитики.",
421        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
422        terms: "Условия", privacy: "Приватность", security: "Безопасность",
423        themeLight: "Светлая", themeDark: "Темная", themeSepia: "Сепия", themeSolarized: "Solarized", themeOcean: "Океан",
424        viewEditor: "Редактор", viewSplit: "Разделенный вид", viewPreview: "Предпросмотр",
425        modalCancel: "Отмена", modalConfirm: "Подтвердить", newItemName: "Введите имя",
426        renameTitle: "Переименовать", newFileTitle: "Новый файл", newFolderTitle: "Новая папка",
427        exporting: "Экспорт...", exportSuccess: "Экспорт выполнен успешно!", exportFail: "Ошибка экспорта. Возможно, контент слишком велик.",
428        pdfStyle: "Стиль PDF", styleStandard: "Стандарт (GitHub)", styleAcademic: "Академический (Elegant)", styleModern: "Модерн (Clean)", styleDefault: "Системный по умолчанию",
429        styleDefaultSimple: "По умолч.", styleStandardSimple: "Стандарт", styleAcademicSimple: "Акад.", styleModernSimple: "Модерн",
430        styleUpdated: "Стиль PDF по умолчанию обновлен",
431        syncStart: "Синхронизация...", syncSuccess: "Все файлы синхронизированы с Google Drive!", syncFail: "Ошибка синхронизации: ", syncReauth: "Требуется авторизация. Нажмите «Синхронизировать» ещё раз для подключения Google Drive.",
432        installMsg: "Установите MD2PDF для лучшей работы", installBtn: "Установить", installSuccess: "Приложение успешно установлено!",
433        chromeInstallMsg: "Установить MD2PDF как десктопное приложение", chromeInstallBtn: "Установить",
434        iosInstallMsg: "Нажмите кнопку 'Поделиться', затем 'На экран «Домой»'",
435        copy: "Копировать", copyJson: "Копировать JSON", copied: "Скопировано в буфер обмена",
436        language: "Язык", theme: "Тема", copyContent: "Копировать контент", copyEditor: "Копировать редактор", copyPreview: "Копировать превью",
437        sidebarSubTitle: "Автоматизация документации",
438        paste: "Вставить из буфера",
439        recommended: "Рекомендуется",
440        pdfSettings: "Настройки PDF", pdfSettingsTitle: "Кастомизация PDF", pdfPageSetup: "Параметры страницы", pdfCover: "Титульный лист",
441        docTitle: "Заголовок документа", docSubtitle: "Подзаголовок / Автор", pdfTOC: "Оглавление", pdfHeaderFooter: "КолонтитуÐ
441»Ñ‹",
442        pdfHeader: "Верхний колонтитул", pdfFooter: "Нижний колонтитул", pdfMacros: "Макросы: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Сохранить и закрыть",
443        pdfLeft: "Слева", pdfCenter: "Центр", pdfRight: "Справа",
444        pdfBranding: "Профессиональный движок преобразования ИИ-артефактов в PDF",
445        exportSettings: "Настройки экспорта",
446        coverShowHeader: "Показывать колонтитулы на обложке",
447        disableBrowserHF: "💡 Совет: Отключите стандартные колонтитулы",
448        printDialogTip: "💡 В диалоге печати отключите стандартные колонтитулы браузера",
449        printDialogNote: "Примечание: Отключите «Верхние и нижние колонтитулы» в настройках печати",
450        headerLogoLeft: "Логотип сверху (Слева)",
451        headerLogoRight: "Логотип сверху (Справа)",
452        watermarkText: "Текст водяного знака",
453        uploadLogo: "Загрузить",
454        logoUrl: "URL логотипа",
455        logoAndWatermark: "Логотип и водяной знак",
456        clear: "Очистить",
457        watermark: "Водяной знак",
458        feedbackBtnTitle: "Поделиться отзывом",
459        feedbackModalTitle: "Поделиться отзывом 💬",
460        feedbackModalDesc: "Помогите нам улучшить MD2PDF — ваш отзыв важен!",
461        feedbackRatingLabel: "Как вам работа с приложением?",
462        feedbackMsgLabel: "Ваш отзыв",
463        feedbackMsgPh: "Поделитесь мыслями — функции, ошибки, предложения...",
464        feedbackEmailPh: "ваш@email.com",
465        feedbackSubmitBtn: "Отправить отзыв",
466        feedbackSending: "Отправка...",
467        feedbackSuccessTitle: "Спасибо! 🎉",
468        feedbackSuccessMsg: "Ваш отзыв получен. Мы внимательно его прочитаем и используем для улучшения MD2PDF.",
469        feedbackErrorMsg: "Не удалось отправить. Проверьте подключение и попробуйте снова.",
470        feedbackNewsletterTitle: "Подписаться на рассылку",
471        feedbackNewsletterDesc: "Советы по Markdown, обновления MD2PDF и лайфхаки — только на английском, редко.",
472        feedbackEmailRequired: "Пожалуйста, введите корректный адрес электронной почты."
473    },
474    'ja': {
475        seoTitle: "MD2PDF - 無料 Markdown PDF 変換ツール | md2pdf.cc",
476        seoDescription: "AIチャットを瞬時に本格的なPDFへ。KaTeX数式、Mermaidダイアグラム、Obsidian構文に対応。100%クライアントサイド処理でログイン不要。",
477        h1Title: "プロフェッショナルな ChatGPT & DeepSeek PDF 変換ツール",
478        mermaidTitle: "Mermaid ダイアグラムのレンダリング",
479        aiExportTitle: "AI チャットの PDF 書き出し",
480        newFile: "新規ファイル", newFolder: "新規フォルダ", rename: "名前の変更", download: "MDをダウンロード", delete: "削除",
481        syncDrive: "クラウド同期", exportPdf: "PDFを書き出す", feedback: "フィードバック", editorPh: "# プロンプトの旅を始めましょう...",
482        confirmDelete: "このアイテムを削除しますか?", welcomeTitle: "マイレポート", welcomeFile: "ようこそ.md",
483        welcomeContent: `# MD2PDF.cc\n## **AI時代のためのプロフェッショナルエンジン**\n<span style="color: #7c3aed; font-weight: bold;">プライバシー第一 • 100% クライアント側</span>
483 • 出版品質のPDF\n\n---\n\n## ✨ 主な機能\n\n### 1. 数学と科学 ($LaTeX$)\nDeepSeek、GPT-4o、Claudeの出力に最適:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. ダイアグラム (Mermaid)\nフローチャートを即座にレンダリング:\n\`\`\`mermaid\ngraph LR\n    A[AIチャット] --> B{MD2PDF}\n    B -->|解析| C[PDFレポート]\n    B -->|スタイル| D[美しいレイアウト]\n\`\`\`\n\n### 3. プロフェッショナル機能\n- [x] **Mermaid.js** サポート\n- [x] **GitHub コールアウト**\n- [x] **学術的な脚注**\n- [ ] **カスタム CSS** (間もなく登場)\n\n#### GitHubスタイル コールアウト\n> [!NOTE]\n> これは **NOTE** (備考) です。重要ですが中立的な情報に使用します。\n\n> [!TIP]\n> これは **TIP** (ヒント) です。作業をより良く、または速く行うのに役立ちます。\n\n> [!WARNING]\n> 这是 **WARNING** (警告) です。危険な操作や重要なアクションについて警告します。\n\n> [!DANGER]\n> これは **DANGER** (危険) です。致命的なエラーやデータ消失のリスクを示します。\n\n#### 学術的な脚注\nMarkdownは強力[^1]ですが、MD2PDFはそれをエレガント[^2]にします。\n\n[^1]: 標準のMarkdownは基本的なテキスト装飾をサポートします。\n[^2]: MD2PDFはプロフェッショナルなPDF出力と高度な構文を追加します。\n\n---\n\n## 🆕 モダンなノート管理のサポート (Obsidian & Notion)\nお好みのノート構文と互換性があります:\n\n- **ハイライト**: ==重要なテキストをハイライト== できます。\n- **WikiLinks**: [[内部リンク]] スタイルのサポート。\n\n---\n\n## 🚀 技術系向けフォーマット\n\`\`\`javascript\n// 清潔でプロフェッショナルなコードブロック\nfunction helloWorld() {\n  console.log("Hello from MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ 開発者向け API\nワークフローを自動化:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 **「PDFを書き出す」** をクリックして魔法を体験してください。`,
484        aiPrompt: "AIアシスタント", guideTitle: "AIアシスタントの統合", guideSub: "高品質なPDFを即座に生成", howItWorks: "機能について",
485        guideDesc: "MD2PDFを使用すると、AIエージェントがプロフェッショナルなレポートを作成できます。以下のシステムプロンプトを使用して、AIがアドレスバー用の直接URLコマンドを提供し、ドキュメントを即座に生成できるようにします。",
486        copyPrompt: "システムプロンプトをコピー", benefit1: "プロ仕様のレイアウト", benefit2: "プライバシー重視",
487        legalTitle: "セキュリティと規約", dataSecurity: "データセキュリティ", termsOfService: "利用規約", privacyPolicy: "プライバシーポリシー",
488        dataSecurityDesc: "MD2PDFはクライアント側アプリケーションです。Markdownコンテンツとファイルはブラウザのローカルストレージにのみ保存されます。プライベートなドキュメントを保存するサーバーは一切なく、最大限のプライバシーとセキュリティを確保します。",
489        termsDesc: "md2pdf.ccの利用は、Quanta Flow Inc. (Wyoming) によって管理されます。ブラウザストレージのデータ永続性については保証いたしません。",
490        privacyDesc: "プライバシー第一:ドキュメントはローカルで処理されます。分析および将来の広告目的で最小限のトラッキングを使用します。",
491        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
492        terms: "利用規約", privacy: "プライバシー", security: "セキュリティ",
493        themeLight: "ライト", themeDark: "ダーク", themeSepia: "セピア", themeSolarized: "Solarized", themeOcean: "オーシャン",
494        viewEditor: "全画面エディタ", viewSplit: "分割表示", viewPreview: "全画面プレビュー",
495        modalCancel: "キャンセル", modalConfirm: "確認", newItemName: "名前を入力",
496        renameTitle: "名前の変更", newFileTitle: "新規ファイル", newFolderTitle: "新規フォルダ",
497        exporting: "書き出し中...", exportSuccess: "書き出し成功!", exportFail: "書き出しに失敗しました。内容が大きすぎる可能性があります。",
498        pdfStyle: "PDFスタイル", styleStandard: "標準 (GitHub)", styleAcademic: "アカデミック (Elegant)", styleModern: "モダン (Clean)", styleDefault: "システムデフォルト",
499        styleDefaultSimple: "デフォルト", styleStandardSimple: "標準", styleAcademicSimple: "学術", styleModernSimple: "モダン",
500        styleUpdated: "デフォルトのPDFスタイルが更新されました",
501        syncStart: "同期を開始しています...", syncSuccess: "すべてのファイルが Google Drive に同期されました!", syncFail: "同期失敗: ", syncReauth: "再認証が必要です。「クラウド同期」をもう一度タップして Google Drive に接続してください。",
502        installMsg: "より良い体験のためにMD2PDFをインストール", installBtn: "今すぐインストール", installSuccess: "アプリが正常にイãƒ
502³ã‚¹ãƒˆãƒ¼ãƒ«ã•れました!",
503        chromeInstallMsg: "MD2PDFをデスクトップアプリとしてインストール", chromeInstallBtn: "アプリをインストール",
504        iosInstallMsg: "共有ボタンをタップして「ホーム画面に追加」を選択",
505        copy: "コピー", copyJson: "JSONをコピー", copied: "クリップボードにコピーしました",
506        language: "言語選択", theme: "テーマ選択", copyContent: "内容をコピー", copyEditor: "エディタをコピー", copyPreview: "プレビューをコピー",
507        sidebarSubTitle: "自動化されたドキュメント作成",
508        paste: "クリップボードから貼り付け",
509        recommended: "おすすめ",
510        pdfSettings: "PDF設定", pdfSettingsTitle: "PDFのカスタマイズ", pdfPageSetup: "ページ設定", pdfCover: "表紙",
511        docTitle: "ドキュメントタイトル", docSubtitle: "サブタイトル / 著者", pdfTOC: "目次", pdfHeaderFooter: "ヘッダーとフッター",
512        pdfHeader: "ヘッダー", pdfFooter: "フッター", pdfMacros: "マクロ: {date}, {title}, {pagenum}, {totalpages}", modalSave: "保存して閉じる",
513        pdfLeft: "左寄せ", pdfCenter: "中央寄せ", pdfRight: "右寄せ",
514        pdfBranding: "プロフェッショナルなAIアーティファクトPDF変換エンジン",
515        exportSettings: "書き出し設定",
516        coverShowHeader: "表紙にヘッダー/フッターを表示",
517        disableBrowserHF: "💡 ヒント:ブラウザのH/Fを無効にする",
518        printDialogTip: "💡 印刷ダイアログでブラウザのデフォルトのヘッダー/フッターを無効にしてください",
519        printDialogNote: "注意:印刷ダイアログで「ヘッダーとフッター」をオフにしてください",
520        headerLogoLeft: "ヘッダーロゴ (左)",
521        headerLogoRight: "ヘッダーロゴ (右)",
522        watermarkText: "透かしテキスト",
523        uploadLogo: "アップロード",
524        logoUrl: "ロゴURL",
525        logoAndWatermark: "ロゴと透かし",
526        clear: "クリア",
527        watermark: "透かし",
528        feedbackBtnTitle: "フィードバックを送る",
529        feedbackModalTitle: "フィードバックを送る 💬",
530        feedbackModalDesc: "MD2PDFの改善にご協力ください — あなたのご意見は大切です!",
531        feedbackRatingLabel: "ご利用体験はいかがですか?",
532        feedbackMsgLabel: "フィードバック",
533        feedbackMsgPh: "機能、バグ、改善案など何でもお気軽にどうぞ...",
534        feedbackEmailPh: "[email protected]",
535        feedbackSubmitBtn: "送信する",
536        feedbackSending: "送信中...",
537        feedbackSuccessTitle: "ありがとうございます!🎉",
538        feedbackSuccessMsg: "フィードバックを受け取りました。内容を丁寧に確認し、MD2PDFの改善に活かします。",
539        feedbackErrorMsg: "送信に失敗しました。接続を確認してもう一度お試しください。",
540        feedbackNewsletterTitle: "ニュースレターを購読する",
541        feedbackNewsletterDesc: "Markdownのヒント、MD2PDFのアップデート&生産性テクニック — 英語のみ、低頻度配信。",
542        feedbackEmailRequired: "有効なメールアドレスを入力してください。"
543    },
544    'ko': {
545        seoTitle: "MD2PDF - 무료 Markdown PDF 변환기 | md2pdf.cc",
546        seoDescription: "AI 채팅을 즉시 전문적인 PDF로 변환하세요. KaTeX 수학 공식, Mermaid 다이어그램, Obsidian 문법 지원. 100% 클라이언트 측 처리, 로그인 불필요.",
547        h1Title: "전문적인 ChatGPT 및 DeepSeek PDF 변환기",
548        mermaidTitle: "Mermaid 다이어그램 렌더링",
549        aiExportTitle: "AI 채팅 PDF 내보내기",
550        newFile: "새 파일", newFolder: "새 폴더", rename: "이름 바꾸기", download: "MD 다운로드", delete: "삭제",
551        syncDrive: "클라우드 동기화", exportPdf: "PDF 내보내기", feedback: "피드백", editorPh: "# 프롬프트 여정을 시작하세요...",
552        confirmDelete: "이 항목을 삭제하시겠습니까?", welcomeTitle: "내 보고서", welcomeFile: "환영합니다.md",
553        welcomeContent: `# MD2PDF.cc\n## **AI 시대를 위한 전문 엔진**\n<span style="color: #7c3aed; font-weight: bold;">개인정보 보호 우선 • 100% 클라이언트 측</span> • 출판 품질의 PDF\n\n---\n\n## ✨ 주요 특징\n\n### 1. 수학 및 과학 ($LaTeX$)\nDeepSeek, GPT-4o 또는 Claude 출력에 완벽 대응:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. 다이어그램 (Mermaid)\n플로우차트를 즉시 렌더링:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Style| D[Beautiful Layout]\n\`\`\`\n\n### 3. 전문 기능\n- [x] **Mermaid.js** 지원\n- [x] **GitHub Callouts**\n- [x] **학술적 각주**\n- [ ] **커스텀 CSS** (곧 출시)\n\n#### GitHub 스타일 콜아웃\n> [!NOTE]\n> 중립적이지만 중요한 정보를 위한 **참고** 표시입니다.\n\n> [!TIP]\n> 작업을 더 잘하거나 빠르게 하는 데 도움이 되는 **팁**입니다.\n\n> [!WARNING]\n> 잠재적으로 위험하거나 중요한 작업에 대한 **경고**입니다.\n\n> [!DANGER]\n> 치명적인 오류나 데이터 손실 위험을 알리는 **위험** 블록입니다.\n\n#### 학술적 각주\nM
553arkdown은 강력[^1]하지만, MD2PDF는 이를 우아[^2]하게 만듭니다.\n\n[^1]: 표준 Markdown은 기본 텍스트 서식을 지원합니다.\n[^2]: MD2PDF는 전문적인 PDF 출력과 고급 구문을 추가합니다.\n\n---\n\n## 🆕 현대적인 노트 작성 지원 (Obsidian & Notion)\n선호하는 노트 구문과 호환됩니다:\n\n- **하이라이트**: ==중요한 텍스트를 고유하게== 표시할 수 있습니다.\n- **WikiLinks**: [[내부 링크]] 스타일 지원.\n\n---\n\n## 🚀 기술 친화적 포맷팅\n\`\`\`javascript\n// 깔끔하고 전문적인 코드 블록\nfunction helloWorld() {\n  console.log("Hello from MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ 개발자용 API\n워크플로우 자동화:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 **"PDF 내보내기"**를 클릭하여 마법을 확인하세요.`,
554        aiPrompt: "AI 어시스턴트", guideTitle: "AI 어시스턴트 통합", guideSub: "즉각적인 전문 PDF 생성", howItWorks: "작동 방식",
555        guideDesc: "MD2PDF는 AI 에이전트가 전문적인 보고서를 생성할 수 있게 해줍니다. 아래의 시스템 프롬프트를 사용하여 AI가 주소창에 직접 입력할 수 있는 URL 명령을 제공하도록 함으로써 즉시 문서를 생성하세요.",
556        copyPrompt: "시스템 프롬프트 복사", benefit1: "프로 레이아웃", benefit2: "개인정보 보호",
557        legalTitle: "보안 및 약관", dataSecurity: "데이터 보안", termsOfService: "서비스 약관", privacyPolicy: "개인정보 처리방침",
558        dataSecurityDesc: "MD2PDF는 클라이언트 측 애플리케이션입니다. 귀하의 Markdown 콘텐츠와 파일은 브라우저의 로컬 저장소에만 저장됩니다. 저희는 귀하의 개인 문서를 저장하는 서버를 운영하지 않아 최고 수준의 개인정보 보호와 보안을 보장합니다.",
559        termsDesc: "md2pdf.cc 이용은 Quanta Flow Inc. (Wyoming)의 관할을 받습니다. 브라우저 저장소의 데이터 보존에 대해서는 보증하지 않습니다.",
560        privacyDesc: "개인정보 보호 우선: 문서는 로컬에서 처리됩니다. 분석 및 향후 광고 목적으로 최소한의 트래킹을 사용합니다.",
561        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
562        terms: "약관", privacy: "개인정보", security: "보안",
563        themeLight: "라이트", themeDark: "다크", themeSepia: "세피아", themeSolarized: "솔라라이즈드", themeOcean: "오션",
564        viewEditor: "전체 에디터", viewSplit: "분할 뷰", viewPreview: "전체 미리보기",
565        modalCancel: "취소", modalConfirm: "확인", newItemName: "이름 입력",
566        renameTitle: "이름 바꾸기", newFileTitle: "새 파일", newFolderTitle: "새 폴더",
567        exporting: "내보내는 중...", exportSuccess: "내보내기 성공!", exportFail: "내보내기 실패. 콘텐츠가 너무 클 수 있습니다.",
568        pdfStyle: "PDF 스타일", styleStandard: "표준 (GitHub)", styleAcademic: "학술 (Elegant)", styleModern: "모던 (Clean)", styleDefault: "시스템 기본값",
569        styleDefaultSimple: "기본", styleStandardSimple: "표준", styleAcademicSimple: "학술", styleModernSimple: "모던",
570        styleUpdated: "기본 PDF 스타일 업데이트됨",
571        syncStart: "동기화 시작 중...", syncSuccess: "모든 파일이 Google Drive에 동기화되었습니다!", syncFail: "동기화 실패: ", syncReauth: "재인증이 필요합니다. 「클라우드 동기화」를 다시 탭하여 Google Drive에 연결하세요.",
572        installMsg: "더 나은 환경을 위해 MD2PDF 설치", installBtn: "지금 설치", installSuccess: "앱이 성공적으로 설치되었습니다!",
573        chromeInstallMsg: "MD2PDF를 데스크톱 앱으로 설치", chromeInstallBtn: "앱 설치",
574        iosInstallMsg: "공유 버튼을 누른 후 '홈 화면에 추가'를 선택하세요",
575        copy: "복사", copyJson: "JSON 복사", copied: "클립보드에 복사됨",
576        language: "언어 선택", theme: "테마 선택", copyContent: "콘텐츠 복사", copyEditor: "에디터 복사", copyPreview: "미리보기 복사",
577        sidebarSubTitle: "자동화된 문서 작업",
578        paste: "클립보드에서 붙여넣기",
579        recommended: "추천",
580        pdfSettings: "PDF 설정", pdfSettingsTitle: "PDF 사용자 정의", pdfPageSetup: "페이지 설정", pdfCover: "표지",
581        docTitle: "문서 제목", docSubtitle: "부제목 / 저자", pdfTOC: "목차", pdfHeaderFooter: "머리말 및 꼬리말",
582        pdfHeader: "머리말", pdfFooter: "꼬리말", pdfMacros: "매크로: {date}, {title}, {pagenum}, {totalpages}", modalSave: "저장 및 닫기",
583        pdfLeft: "왼쪽", pdfCenter: "가운데", pdfRight: "오른쪽",
584        pdfBranding: "전문적인 AI 아티팩트 PDF 변환 엔진",
585        exportSettings: "내보내기 설정",
586        coverShowHeader: "표지에 머리말/꼬리말 표시",
587        disableBrowserHF: "💡 팁: 브라우저 기본 머리말/꼬리말 비활성화",
588        printDialogTip: "💡 인쇄 대화상자에서 브라우저의 기본 머리말/꼬리말을 비활성화하세요",
589        printDialogNote: "참고: 인쇄 대화상자에서 '머리말 및 꼬리말' 옵션을 꺼주세요",
590        headerLogoLeft: "머리말 로고 (왼쪽)",
591        headerLogoRight: "머리말 로고 (오른쪽)",
592        watermarkText: "워터마크 텍스트",
593        uploadLogo: "업로드",
594        logoUrl: "로고 URL",
595        logoAndWatermark: "로고 및 워터마크",
596        clear: "지우기",
597        watermark: "워터마크",
598        feedbackBtnTitle: "피드백 공유",
599        feedbackModalTitle: "피드백 공유 💬",
600        feedbackModalDesc: "MD2PDF 개선에 도움을 주세요 — 귀하의 의견이 중요합니다!",
601        feedbackRatingLabel: "사용 경험은 어떠셨나요?",
602        feedbackMsgLabel: "피드백",
603        feedbackMsgPh: "생각을 자유롭게 공유해 주세요 — 기능, 버그, 개선 사항 등...",
604        feedbackEmailPh: "[email protected]",
605        feedbackSubmitBtn: "피드백 보내기",
606        feedbackSending: "전송 중...",
607        feedbackSuccessTitle: "감사합니다! 🎉",
608        feedbackSuccessMsg: "피드백이 접수되었습니다. 내용을 꼼꼼히 검토하고 MD2PDF 개선에 활용하겠습니다.",
609        feedbackErrorMsg: "전송 실패. 연결을 확인하고 다시 시도해 주세요.",
610        feedbackNewsletterTitle: "뉴스레터 구독하기",
611        feedbackNewsletterDesc: "Markdown 팁, MD2PDF 업데이트 & 생산성 트릭 — 영어 전용, 저빈도 발송.",
612        feedbackEmailRequired: "유효한 이메일 주소를 입력해 주세요."
613    },
614    'bg': {
615        seoTitle: "MD2PDF - Безплатен Markdown към PDF конвертор | md2pdf.cc",
616        seoDescription: "Незабавно превърнете AI чатовете в професионални PDF файлове. Поддържа KaTeX формули, Mermaid диаграми и Obsidian синтаксис. 100% клиентска част, без вход.",
617        h1Title: "Професионален ChatGPT и DeepSeek конвертор към PDF",
618        mermaidTitle: "Mermaid диаграми",
619        aiExportTitle: "Експортиране на AI чат",
620        newFile: "Нов файл", newFolder: "Нова папка", rename: "Преименуване", download: "Изтегляне на MD", delete: "Изтриване",
621        syncDrive: "Облачна синхронизация", exportPdf: "Експортиране в PDF", feedback: "Обратна връзка", editorPh: "# Започнете вашето пътуване...",
622        confirmDelete: "Изтриване на този елемент?", welcomeTitle: "Моите отчети", welcomeFile: "Добре-дошли.md",
623        welcomeContent: `# MD2PDF.cc\n## **Професионален двигател за ерата на ИИ**\n<span style="color: #7c3aed; font-weight: bold;">Поверителност на първо място • 100% клиентски базиран</span> • PDF с качество за публикуване\n\n---\n\n## ✨ Ключови характеристики\n\n### 1. Математика и наука ($LaTeX$)\nПерфектно за DeepSeek, GPT-4o или Claude:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Диаграми (Mermaid)\nНезабавно изобразяване на блок-схеми:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Style| D[Beautiful Layout]\n\`\`\`\n\n### 3. Професионални функции\n- [x] **Mermaid.js** поддръжка\n- [x] **GitHub Callouts**\n- [x] **Академични бележки**\n- [ ] **Персонализиран CSS** (Очаквайте скоро)\n\n#### GitHub-Style Callouts\n> [!NOTE]\n>
623 Това е **БЕЛЕЖКА** за неутрална, но важна информация.\n\n> [!TIP]\n> Това е **СЪВЕТ**, който помага на потребителите да правят нещата по-добре.\n\n> [!WARNING]\n> Това е **ПРЕДУПРЕЖДЕНИЕ** за потенциално опасни действия.\n\n> [!DANGER]\n> Това е блок за **ОПАСНОСТ** при критични грешки или риск от загуба на данни.\n\n#### Академични бележки\nMarkdown е мощен[^1], но MD2PDF го прави елегантен[^2].\n\n[^1]: Стандартният Markdown поддържа основно текстово форматиране.\n[^2]: MD2PDF добавя професионален PDF изход и разширен синтаксис.\n\n---\n\n## 🆕 Модерна поддръжка за бележки (Obsidian & Notion)\nСъвместим с любимия ви синтаксис за бележки:\n\n- **Акценти**: Можете лесно да ==акцентирате върху важен текст==.\n- **WikiLinks**: Поддръжка за [[Вътрешни връзки]].\n\n---\n\n## 🚀 Технологично форматиране\n\`\`\`javascript\n// Чисти и професионални кодови блокове\nfunction helloWorld() {\n  console.log("Здравей от MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API за разработчици\nАвтоматизирайте работния си процес:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 Кликнете върху **"Експортиране в PDF"**, за да видите магията.`,
624        aiPrompt: "ИИ Асистент", guideTitle: "Интеграция на ИИ асистент", guideSub: "Незабавен професионален PDF", howItWorks: "Как работи",
625        guideDesc: "MD2PDF позволява на ИИ агентите да генерират професионални отчети. Използвайте системния промпт по-долу, така че ИИ да предоставя директни URL команди.",
626        copyPrompt: "Копиране на системния промпт", benefit1: "Професионален изглед", benefit2: "Поверителност първо",
627        legalTitle: "Сигурност и условия", dataSecurity: "Сигурност на данните", termsOfService: "Условия за ползване", privacyPolicy: "Политика за поверителност",
628        dataSecurityDesc: "MD2PDF е клиентско приложение. Вашето съдържание и файлове се съхраняват само в локалното хранилище на вашия браузър. Ние нямаме сървъри, които да съхраняват вашите документи.",
629        termsDesc: "Използването на md2pdf.cc се урежда от Quanta Flow Inc. (Wyoming). Не даваме гаранции за съхранението на данни в браузъра.",
630        privacyDesc: "Поверителност на първо място: Документите се обработват локално. Използваме минимално проследяване за аналитични цели.",
631        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
632        terms: "Условия", privacy: "Поверителност", security: "Сигурност",
633        themeLight: "Светла", themeDark: "Тъмна", themeSepia: "Сепия", themeSolarized: "Solarized", themeOcean: "Океан",
634        viewEditor: "Пълен редактор", viewSplit: "Разделен изглед", viewPreview: "Преглед",
635        modalCancel: "Отказ", modalConfirm: "Потвърждаване", newItemName: "Въведете име",
636        renameTitle: "Преименуване", newFileTitle: "Нов файл", newFolderTitle: "Нова папка",
637        exporting: "Експортиране...", exportSuccess: "Експортирането е успешно!", exportFail: "Грешка при експортиране. Съдържанието може да е твърде голямо.",
638        pdfStyle: "Стил на PDF", styleStandard: "Стандартен (GitHub)", styleAcademic: "Академичен (Elegant)", styleModern: "Модерен (Clean)", styleDefault: "Системно по подразбиране",
639        styleDefaultSimple: "По подр.", styleStandardSimple: "Стандарт", styleAcademicSimple: "Академичен", styleModernSimple: "Модерен",
640        styleUpdated: "Стилът по подразбиране е актуализиран",
641        syncStart: "Стартиране на синхронизация...", syncSuccess: "Всички файлове са синхронизирани с Google Drive!", syncFail: "Синхронизация
641та се провали: ", syncReauth: "Необходима е повторна оторизация. Докоснете отново \u201EСинхронизация\u201C, за да свържете Google Drive.",
642        installMsg: "Инсталирайте MD2PDF за по-добро изживяване", installBtn: "Инсталирай сега", installSuccess: "Приложението е инсталирано успешно!",
643        chromeInstallMsg: "Инсталирайте MD2PDF като десктоп приложение", chromeInstallBtn: "Инсталирай приложението",
644        iosInstallMsg: "Докоснете бутона за споделяне и след това 'Към началния екран'",
645        copy: "Копирай", copyJson: "Копирай JSON", copied: "Копирано в клипборда",
646        language: "Език", theme: "Тема", copyContent: "Копирай съдържанието", copyEditor: "Копирай редактора", copyPreview: "Копирай прегледа",
647        sidebarSubTitle: "Автоматизирана документация",
648        paste: "Постави от клипборда",
649        recommended: "Препоръчително",
650        pdfSettings: "PDF настройки", pdfSettingsTitle: "Персонализация на PDF", pdfPageSetup: "Настройка на страницата", pdfCover: "Заглавна страница",
651        docTitle: "Заглавие на документа", docSubtitle: "Подзаглавие / Автор", pdfTOC: "Съдържание", pdfHeaderFooter: "Горна и долна част",
652        pdfHeader: "Горен колонтитул", pdfFooter: "Долен колонтитул", pdfMacros: "Макроси: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Запази и затвори",
653        pdfLeft: "Ляво", pdfCenter: "Център", pdfRight: "Дясно",
654        pdfBranding: "Професионален механизъм за ИИ артефакти към PDF",
655        exportSettings: "Настройки за експортиране",
656        coverShowHeader: "Покажи горна/долна част на корицата",
657        disableBrowserHF: "💡 Съвет: Изключете браузърния H/F",
658        printDialogTip: "💡 В диалога за печат, моля изключете стандартните заглавия на браузъра",
659        printDialogNote: "Забележка: Моля, изключете 'Header and footers' в настройките за печат",
660        headerLogoLeft: "Лого в горната част (Ляво)",
661        headerLogoRight: "Лого в горната част (Дясно)",
662        watermarkText: "Текст за воден знак",
663        uploadLogo: "Качи",
664        logoUrl: "URL на лого",
665        logoAndWatermark: "Лого и воден знак",
666        clear: "Изчисти",
667        watermark: "Воден знак",
668        feedbackBtnTitle: "Споделете вашето мнение",
669        feedbackModalTitle: "Споделете вашето мнение 💬",
670        feedbackModalDesc: "Помогнете ни да подобрим MD2PDF — вашето мнение е важно!",
671        feedbackRatingLabel: "Как е вашето изживяване?",
672        feedbackMsgLabel: "Вашето мнение",
673        feedbackMsgPh: "Споделете мислите си — функции, грешки, предложения...",
674        feedbackEmailPh: "[email protected]",
675        feedbackSubmitBtn: "Изпрати мнение",
676        feedbackSending: "Изпращане...",
677        feedbackSuccessTitle: "Благодаря! 🎉",
678        feedbackSuccessMsg: "Вашето мнение е получено. Ще го прочетем внимателно и ще го използваме за подобряване на MD2PDF.",
679        feedbackErrorMsg: "Неуспешно изпращане. Проверете връзката и опитайте отново.",
680        feedbackNewsletterTitle: "Абонирайте се за бюлетина",
681        feedbackNewsletterDesc: "Съвети за Markdown, актуализации на MD2PDF и трикове за продуктивност — само на английски, рядко.",
682        feedbackEmailRequired: "Моля, въведете валиден имейл адрес."
683    },
684    'ar': {
685        seoTitle: "MD2PDF - محول مجاني من Markdown إلى PDF | md2pdf.cc",
686        seoDescription: "حول محادثات الذكاء الاصطناعي إلى ملفات PDF احترافية فوراً. يدعم صيغ KaTeX، ورسوم Mermaid، وتنسيق Obsidian. يعمل كلياً على جهازك، بدون تسجيل دخول.",
687        h1Title: "محول ChatGPT و DeepSeek إلى PDF احترافي",
688        mermaidTitle: "دعم رسوم Mermaid",
689        aiExportTitle: "تصدير محادثات الذكاء الاصطناعي",
690        newFile: "ملف جديد", newFolder: "مجلد جديد", rename: "إعادة تسمية", download: "تحميل MD", delete: "حذف",
691        syncDrive: "مزامنة سحابية (اتجاهين)", exportPdf: "تصدير PDF", feedback: "تعليقات", editorPh: "# ابدأ رحلتك...",
692        confirmDelete: "حذف هذا العنصر؟", welcomeTitle: "تقاريري", welcomeFile: "ترحيب.md",
693        welcomeContent: `# MD2PDF.cc\n## **المحرك الاحترافي لعصر الذكاء الاصطناعي**\n<span style="color: #7c3aed; font-weight: bold;">الخصوصية أولاً • 100% من جانب العميل</span> • PDF بجودة نشر\n\n---\n\n## ✨ الميزات الرئيسية\n\n### 1. الرياضيات والعلوم ($LaTeX$)\nمثالي لمخرجات DeepSeek أو GPT-4o أو Claude:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. الرسوم البيانية (Mermaid)\nعرض المخططات الانسيابية على الفور:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Style| D[Beautiful Layout]\n\`\`\`\n\n### 3. ميزات احترافية\n- [x] دعم **Mermaid.js**\n- [x] **تنبيهات GitHub**\n- [x] **هوامش أكاديمية**\n- [ ] **CSS مخصص** (قريباً)\n\n#### تنبيهات بنمط GitHub\n> [!NOTE]\n>
693 هذه **ملاحظة** للمعلومات المحايدة ولكن المهمة.\n\n> [!TIP]\n> هذه **نصيحة** لمساعدة المستخدمين على القيام بالأشياء بشكل أفضل أو أسرع.\n\n> [!WARNING]\n> هذا **تحذير** للإجراءات الخطيرة أو الحرجة المحتملة.\n\n> [!DANGER]\n> هذه كتلة **خطر** للأخطاء الحرجة أو مخاطر فقدان البيانات.\n\n#### هوامش أكاديمية\nMarkdown قوي[^1]، لكن MD2PDF يجعله أنيقاً[^2].\n\n[^1]: يدعم Markdown القياسي تنسيق النص الأساسي.\n[^2]: يضيف MD2PDF إنتاج PDF احترافي وصيغة متقدمة.\n\n---\n\n## 🆕 دعم الملاحظات الحديثة (Obsidian & Notion)\nمتوافق مع صيغة الملاحظات المفضلة لديك:\n\n- **التظليل**: يمكنك ==تظليل النص المهم== بسهولة.\n- **WikiLinks**: دعم نمط [[الروابط الداخلية]].\n\n---\n\n## 🚀 تنسيق تقني أصيل\n\`\`\`javascript\n// كتل برمجية نظيفة واحترافية\nfunction helloWorld() {\n  console.log("مرحباً من MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API للمطورين\nأتمتة سير عملك:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 انقر على **"تصدير PDF"** لمشاهدة السحر.`,
694        aiPrompt: "مساعد الذكاء الاصطناعي", guideTitle: "تكامل مساعد الذكاء الاصطناعي", guideSub: "PDF احترافي فوري", howItWorks: "كيف يعمل",
695        guideDesc: "يمكّن MD2PDF وكلاء الذكاء الاصطناعي من إنشاء تقارير احترافية. استخدم موجه النظام أدناه حتى يوفر الذكاء الاصطناعي أوامر URL مباشرة لشريط العنوان لإنشاء المستندات على الفور.",
696        copyPrompt: "نسخ موجه النظام", benefit1: "تنسيق احترافي", benefit2: "الخصوصية أولاً",
697        legalTitle: "الأمان والشروط", dataSecurity: "أمن البيانات", termsOfService: "شروط الخدمة", privacyPolicy: "سياسة الخصوصية",
698        dataSecurityDesc: "MD2PDF هو تطبيق من جانب العميل. يتم تخزين محتوى Markdown وملفاتك فقط في التخزين المحلي لمتصفحك. لا نمتلك خوادم تخزن مستنداتك الخاصة، مما يضمن أقصى قدر من الخصوصية والأمان.",
699        termsDesc: "يخضع استخدام md2pdf.cc لشركة Quanta Flow Inc. (وايومنغ). لا نقدم ضمانات لاستمرار البيانات في تخزين المتصفح.",
700        privacyDesc: "الخصوصية أولاً: تتم معالجة المستندات محليًا. نستخدم تتبعاً حده الأدنى للتحليلات والأغراض الإعلانية المستقبلية.",
701        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
702        terms: "الشروط", privacy: "الخصوصية", security: "الأمان",
703        themeLight: "فاتح", themeDark: "داكن", themeSepia: "سيبيا", themeSolarized: "سولارايزد", themeOcean: "محيط",
704        viewEditor: "محرر كامل", viewSplit: "عرض منقسم", viewPreview: "معاينة كاملة",
705        modalCancel: "إلغاء", modalConfirm: "تأكيد", newItemName: "أدخل الاسم",
706        renameTitle: "إعادة تسمية", newFileTitle: "ملف جديد", newFolderTitle: "مجلد جديد",
707        exporting: "جاري التصدير...", exportSuccess: "تم التصدير بنجاح!", exportFail: "فشل التصدير. قد يكون المحتوى كبيراً جداً.",
708        pdfStyle: "نمط PDF", styleStandard: "قياسي (GitHub)", styleAcademic: "أكاديمي (Elegant)", styleModern: "حديث (Clean)", styleDefault: "افتراضي النظام",
709        styleDefaultSimple: "افتراضي", styleStandardSimple: "قياسي", styleAcademicSimple: "أكاديمي", styleModernSimple: "عصري",
710        styleUpdated: "تم تحديث نمط PDF الافتراضي",
711        syncStart: "بدء المزامنة...", syncSuccess: "تم مزامنة جميع الملفات مع Google Drive!", syncFail: "فشلت المزامنة: ", syncReauth: "مطلوب إعادة التفويض. اضغط على «مزامنة» مرة أخرى للاتصال بـ Google Drive.",
712        installMsg: "ثبت MD2PDF لتجربة أفضل", installBtn: "ثبت الآن", installSuccess: "تم تثبيت التطبيق بنجاح!",
713        chromeInstallMsg: "ثبت MD2PDF كتطبيق سطح مكتب", chromeInstallBtn: "تثبيت التطبيق",
714        iosInstallMsg: "اضغط على زر المشاركة ثم 'إضافة إلى الشاشة الرئيسية'",
715        copy: "نسخ", copyJson: "نسخ JSON", copied: "تم النسخ إلى الحافظة",
716        language: "اللغة", theme: "السمة", copyContent: "نسخ المحتوى", copyEditor: "نسخ المحرر", copyPreview: "نسخ المعاينة",
717        sidebarSubTitle: "التوثيق الآلي",
718        paste: "لصق من الحافظة",
719        recommended: "موصى به",
720        pdfSettings: "إعدادات PDF", pdfSettingsTitle: "تخصيص PDF", pdfPageSetup: "إعداد الصفحة", pdfCover: "صفحة الغلاف",
721        docTitle: "عنوان المستند", docSubtitle: "العنوان الفرعي / المؤلف", pdfTOC: "جدول المحتويات", pdfHeaderFooter: "الرأس والتذييل",
722        pdfHeader: "الرأس", pdfFooter: "التذييل", pdfMacros: "الماكرو: {date}, {title}, {pagenum}, {totalpages}", modalSave: "حفظ وإغلاق",
723        pdfLeft: "يسار", pdfCenter: "وسط", pdfRight: "يمين",
724        pdfBranding: "محرك احترافي لتحويل مخرجات الذكاء الاصطناعي إلى PDF",
725        exportSettings: "إعدادات التصدير",
726        coverShowHeader: "إظهار الرأس/التذييل على الغلاف",
727        disableBrowserHF: "💡 نصيحة: تعطيل رأس/تذييل المتصفح",
728        printDialogTip: "💡 في حوار الطباعة، يرجى تعطيل رؤوس وتذييلات المتصفح الافتراضية",
729        printDialogNote: "ملاحظة: يرجى إيقاف تشغيل 'الرؤوس والتذييلات' في حوار الطباعة",
730        headerLogoLeft: "شعار الرأس (يسار)",
731        headerLogoRight: "شعار الرأس (يمين)",
732        watermarkText: "نص العلامة المائية",
733        uploadLogo: "رفع",
734        logoUrl: "رابط الشعار",
735        logoAndWatermark: "العلامة المائية والشعار",
736        clear: "حذف",
737        watermark: "علامة مائية",
738        feedbackBtnTitle: "شارك ملاحظاتك",
739        feedbackModalTitle: "شارك ملاحظاتك 💬",
740        feedbackModalDesc: "ساعدنا في تحسين MD2PDF — ملاحظاتك مهمة!",
741        feedbackRatingLabel: "كيف هي تجربتك؟",
742        feedbackMsgLabel: "ملاحظاتك",
743        feedbackMsgPh: "شارك أفكارك — الميزات والأخطاء والتحسينات...",
744        feedbackEmailPh: "[email protected]",
745        feedbackSubmitBtn: "إرسال الملاحظات",
746        feedbackSending: "جاري الإرسال...",
747        feedbackSuccessTitle: "شكراً لك! 🎉",
748        feedbackSuccessMsg: "تم استلام ملاحظاتك. سنقرأها بعناية ونستخدمها لتحسين MD2PDF.",
749        feedbackErrorMsg: "فشل الإرسال. تحقق من اتصالك وحاول مرة أخرى.",
750        feedbackNewsletterTitle: "اشترك في نشرتنا البريدية",
751        feedbackNewsletterDesc: "نصائح Markdown، تحديثات MD2PDF وحيل الإنتاجية — بالإنجليزية فقط، وبشكل غير متكرر.",
752        feedbackEmailRequired: "يرجى إدخال عنوان بريد إلكتروني صحيح."
753    },
754    'it': {
755        seoTitle: "MD2PDF - Convertitore Gratuito da Markdown a PDF | md2pdf.cc",
756        seoDescription: "Trasforma istantaneamente le chat AI in PDF professionali. Supporta formule KaTeX, diagrammi Mermaid e sintassi Obsidian. 100% lato client, nessun login.",
757        h1Title: "Convertitore Professionale da ChatGPT e DeepSeek a PDF",
758        mermaidTitle: "Rendering Diagrammi Mermaid",
759        aiExportTitle: "Esportazione Chat AI in PDF",
760        newFile: "Nuovo file", newFolder: "Nuova cartella", rename: "Rinomina", download: "Scarica MD", delete: "Elimina",
761        syncDrive: "Sincronizzazione Cloud (2-vie)", exportPdf: "Esporta PDF", feedback: "Feedback", editorPh: "# Inizia il tuo viaggio...",
762        confirmDelete: "Eliminare questo elemento?", welcomeTitle: "I miei rapporti", welcomeFile: "Benvenuto.md",
763        welcomeContent: `# MD2PDF.cc\n## **Il motore professionale per l'era dell'IA**\n<span style="color: #7c3aed; font-weight: bold;">Privacy prima di tutto • 100% lato client</span> • PDF di qualità editoriale\n\n---\n\n## ✨ Caratteristiche principali\n\n### 1. Matematica e Scienza ($LaTeX$)\nPerfetto per output DeepSeek, GPT-4o o Claude:\n$$f(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2\\pi i \\xi x}\\,d\\xi$$\n\n### 2. Diagrammi (Mermaid)\nRenderizza diagrammi di flusso istantaneamente:\n\`\`\`mermaid\ngraph LR\n    A[AI Chat] --> B{MD2PDF}\n    B -->|Parse| C[PDF Report]\n    B -->|Stile| D[Layout bellissimo]\n\`\`\`\n\n### 3. Funzioni professionali\n- [x] Supporto **Mermaid.js**\n- [x] **GitHub Callouts**\n- [x] **Note a piè di pagina accademiche**\n- [ ] **CSS personalizzato** (Presto disponibile)\n\n#### Callout in stile GitHub\n> [!NOTE]\n> Questa è una **NOTA** per informazioni neutre ma importanti.\n\n> [!TIP]\n> Questo è un **SUGGERIMENTO** per aiutare gli utenti a fare le cose meglio o più velocemente.\n\n> [!WARNING]\n> Questo è un **AVVERTIMENTO** per azioni potenzialmente pericolose.\n\n> [!DANGER]\n> Questo è un blocco di **PERICOLO** per errori critici o rischi di perdita di dati.\n\n#### Note a piè di pagina accademiche\nMarkdown è potente[^1], maar MD2PDF lo rende elegante[^2].\n\n[^1]: Markdown standard supporta la formattazione di base del testo.\n[^2]: MD2PDF aggiunge un output PDF professionale e una sintassi avanzata.\n\n---\n\n## 🆕 Supporto per note moderne (Obsidian & Notion)\nCompatibile con la tua sintassi preferita per prendere appunti:\n\n- **Evidenziazioni**: Puoi ==evidenziare il testo importante== facilmente.\n- **WikiLinks**: Supporto per lo stile [[Link interni]].\n\n---\n\n## 🚀 Formattazione nativa tecnologica\n\`\`\`javascript\n// Blocchi di codice puliti e professionali\nfunction helloWorld() {\n  console.log("Ciao da MD2PDF!");\n}\n\`\`\`\n\n---\n\n## 🛠️ API per sviluppatori\nAutomatizza il tuo flusso di lavoro:\n\`https://md2pdf.cc/?content={URL_ENCODED_MARKDOWN}\`\n\n👇 Clicca su **"Esporta PDF"** per vedere la magia.`,
764        aiPrompt: "Assistente IA", guideTitle: "Integrazione Assistente IA", guideSub: "PDF professionale istantaneo", howItWorks: "Come funziona",
765        guideDesc: "MD2PDF consente agli agenti IA di generare report professionali. Usa il prompt di sistema qui sotto in modo che l'IA fornisca comandi URL diretti per la barra degli indirizzi per generare documenti istantaneamente.",
766        copyPrompt: "Copia Prompt di sistema", benefit1: "Layout Pro", benefit2: "Privacy First",
767        legalTitle: "Sicurezza e Termini", dataSecurity: "Sicurezza dei dati", termsOfService: "Termini di servizio", privacyPolicy: "Informativa sulla privacy",
768        dataSecurityDesc: "MD2PDF è un'applicazione lato client. I tuoi contenuti Markdown e i file sono memorizzati solo nel database locale del tuo browser. Non abbiamo server che memorizzano i tuoi documenti privati, garantendo la massima privacy e sicurezza.",
769        termsDesc: "L'uso di md2pdf.cc è regolato da Quanta Flow Inc. (Wyoming). Non forniamo garanzie per la persistenza dei dati nella memoria del browser.",
770        privacyDesc: "Privacy-first: i documenti vengono elaborati localmente. Utilizziamo un tracciamento minimo per scopi analitici e pubblicitari futuri.",
771        quantaBranding: " ©2026 <a href=\"https://quantaflow.cc\" target=\"_blank\" class=\"hover:text-primary transition-colors\">Quanta Flow Inc.</a>",
772        terms: "Termini", privacy: "Privacy", security: "Sicurezza",
773        themeLight: "Luce", themeDark: "Buio", themeSepia: "Seppia", themeSolarized: "Solarized", themeOcean: "Oceano",
774        viewEditor: "Editor completo", viewSplit: "Vista divisa", viewPreview: "Anteprima completa",
775        modalCancel: "Annulla", modalConfirm: "Conferma", newItemName: "Inserisci nome",
776        renameTitle: "Rinomina", newFileTitle: "Nuovo file", newFolderTitle: "Nuova cartella",
777        exporting: "Esportazione...", exportSuccess: "Esportazione completata!", exportFail: "Esportazione fallita. Il contenuto potrebbe essere troppo grande.",
778        pdfStyle: "Stile PDF", styleStandard: "Standard (GitHub)", styleAcademic: "Accademico (Elegant)", styleModern: "Moderno (Clean)", styleDefault: "Predefinito di sistema",
779        styleDefaultSimple: "Predef.", styleStandardSimple: "Standard", styleAcademicSimple: "Accademico", styleModernSimple: "Moderno",
780        styleUpdated: "Stile PDF predefinito aggiornato",
781        syncStart: "Avvio sincronizzazione...", syncSuccess: "Tutti i file sincronizzati su Google Drive!", syncFail: "Sincronizzazione fallita: ", syncReauth: "Autorizzazione richiesta. Tocca di nuovo «Sincronizza» per connettere Google Drive.",
782        installMsg: "Installa MD2PDF per un'esperienza migliore", installBtn: "Installa ora", installSuccess: "App installata con successo!",
783        chromeInstallMsg: "Installa MD2PDF come app desktop", chromeInstallBtn: "Installa app",
784        iosInstallMsg: "Tocca il pulsante di condivisione e poi 'Aggiungi alla schermata Home'",
785        copy: "Copia", copyJson: "Copia JSON", copied: "Copiato negli appunti",
786        language: "Lingua", theme: "Tema", copyContent: "Copia contenuto", copyEditor: "Copia editor", copyPreview: "Copia anteprima",
787        sidebarSubTitle: "Documentazione Automatizzata",
788        paste: "Incolla dagli appunti",
789        recommended: "Consigliato",
790        pdfSettings: "Impostazioni PDF", pdfSettingsTitle: "Personalizzazione PDF", pdfPageSetup: "Impostazione pagina", pdfCover: "Copertina",
791        docTitle: "Titolo del documento", docSubtitle: "Sottotitolo / Autore", pdfTOC: "Indice", pdfHeaderFooter: "Intestazione e Piè di pagina",
792        pdfHeader: "Intestazione", pdfFooter: "Piè di pagina", pdfMacros: "Macro: {date}, {title}, {pagenum}, {totalpages}", modalSave: "Salva e chiudi",
793        pdfLeft: "Sinistra", pdfCenter: "Centro", pdfRight: "Destra",
794        pdfBranding: "Il motore professionale di PDF per gli artefatti dell'IA",
795        exportSettings: "Impostazioni di esportazione",
796        coverShowHeader: "Mostra intestazione/piè di pagina sulla copertina",
797        disableBrowserHF: "💡 Suggerimento: Disabilita H/P del browser",
798        printDialogTip: "💡 Nella finestra di dialogo di stampa, disabilitare le intestazioni/piè di pagina predefiniti del browser",
799        printDialogNote: "Nota: Disattivare 'Intestazioni e piè di pagina' nella finestra di stampa",
800        headerLogoLeft: "Logo intestazione (Sinistra)",
801        headerLogoRight: "Logo intestazione (Destra)",
802        watermarkText: "Testo filigrana",
803        uploadLogo: "Carica",
804        logoUrl: "URL del logo",
805        logoAndWatermark: "Logo e filigrana",
806        clear: "Elimina",
807        watermark: "Marca d'acqua",
808        feedbackBtnTitle: "Condividi il tuo feedback",
809        feedbackModalTitle: "Condividi il tuo feedback 💬",
810        feedbackModalDesc: "Aiutaci a migliorare MD2PDF — il tuo feedback è importante!",
811        feedbackRatingLabel: "Com'è la tua esperienza?",
812        feedbackMsgLabel: "Il tuo feedback",
813        feedbackMsgPh: "Condividi i tuoi pensieri — funzionalità, bug, miglioramenti...",
814        feedbackEmailPh: "[email protected]",
815        feedbackSubmitBtn: "Invia Feedback",
816        feedbackSending: "Invio in corso...",
817        feedbackSuccessTitle: "Grazie! 🎉",
818        feedbackSuccessMsg: "Il tuo feedback è stato ricevuto. Lo leggeremo attentamente e lo useremo per migliorare MD2PDF.",
819        feedbackErrorMsg: "Invio non riuscito. Controlla la connessione e riprova.",
820        feedbackNewsletterTitle: "Iscriviti alla nostra newsletter",
821        feedbackNewsletterDesc: "Consigli Markdown, aggiornamenti MD2PDF & trucchi di produttività — solo in inglese, bassa frequenza.",
822        feedbackEmailRequired: "Inserisci un indirizzo email valido."
823    }
824};
825
826// Billing/export copy is shared by the editor and standalone billing pages.
827// Keep these keys in every supported locale so paid-export UI never falls back to English.
828const billingLocaleText = {
829    tw: { exportBrandingTitle:'移除 PDF 品牌資訊', exportBrandingFree:'免費匯出會包含 MD2PDF 品牌頁。', exportBrandingActive:'已啟用無品牌資訊匯出。', exportViewPlans:'查看方案', account:'帳戶', exportOneTime:'一次性付款,所有裝置可用', exportMonthly:'可隨時取消;訂閱有效期間可用', exportFullPricing:'查看完整價格方案', licenseActivated:'MD2PDF 權益已在此裝置上啟用。' },
830    es: { exportBrandingTitle:'Eliminar marca del PDF', exportBrandingFree:'Las exportaciones gratuitas incluyen la página de marca de MD2PDF.', exportBrandingActive:'La exp
830ortación sin marca está activa.', exportViewPlans:'Ver planes', account:'Cuenta', exportOneTime:'Pago único, todos los dispositivos', exportMonthly:'Cancela cuando quieras; activo mientras estés suscrito', exportFullPricing:'Ver precios completos', licenseActivated:'El acceso de MD2PDF se activó en este dispositivo.' },
831    de: { exportBrandingTitle:'PDF-Branding entfernen', exportBrandingFree:'Kostenlose Exporte enthalten die MD2PDF-Markenseite.', exportBrandingActive:'Brandingfreier Export ist aktiv.', exportViewPlans:'Tarife ansehen', account:'Konto', exportOneTime:'Einmalzahlung, alle Geräte', exportMonthly:'Jederzeit kündbar; aktiv während des Abos', exportFullPricing:'Alle Preise ansehen', licenseActivated:'Der MD2PDF-Zugriff wurde auf diesem Gerät aktiviert.' },
832    ru: { exportBrandingTitle:'Удалить брендинг PDF', exportBrandingFree:'Бесплатный экспорт включает страницу брендинга MD2PDF.', exportBrandingActive:'Экспорт без брендинга активен.', exportViewPlans:'Посмотреть тарифы', account:'Аккаунт', exportOneTime:'Один платёж, все устройства', exportMonthly:'Отмена в любое время; доступен при подписке', exportFullPricing:'Все цены', licenseActivated:'Доступ MD2PDF активирован на этом устройстве.' },
833    ja: { exportBrandingTitle:'PDFのブランド情報を削除', exportBrandingFree:'無料出力にはMD2PDFのブランドページが含まれます。', exportBrandingActive:'ブランドなし出力が有効です。', exportViewPlans:'料金を見る', account:'アカウント', exportOneTime:'一回払い、すべての端末', exportMonthly:'いつでも解約可能。購読中は有効', exportFullPricing:'料金プランを見る', licenseActivated:'この端末でMD2PDFのアクセスが有効になりました。' },
834    ko: { exportBrandingTitle:'PDF 브랜드 제거', exportBrandingFree:'무료 내보내기에는 MD2PDF 브랜드 페이지가 포함됩니다.', exportBrandingActive:'브랜드 없는 내보내기가 활성화되었습니다.', exportViewPlans:'요금제 보기', account:'계정', exportOneTime:'일회 결제, 모든 기기', exportMonthly:'언제든 취소 가능; 구독 중 활성', exportFullPricing:'전체 요금제 보기', licenseActivated:'이 기기에서 MD2PDF 이용 권한이 활성화되었습니다.' },
835    bg: { exportBrandingTitle:'Премахване на PDF брандинга', exportBrandingFree:'Безплатният експорт включва брандирана страница на MD2PDF.', exportBrandingActive:'Експортът без брандинг е активен.', exportViewPlans:'Виж плановете', account:'Акаунт', exportOneTime:'Еднократно плащане, всички устройства', exportMonthly:'Отказ по всяко време; активен при абонамент', exportFullPricing:'Виж всички цени', licenseActivated:'Достъпът до MD2PDF е активиран на това устройство.' },
836    ar: { exportBrandingTitle:'إزالة علامة PDF التجارية', exportBrandingFree:'يتضمن التصدير المجاني صفحة علامة MD2PDF.', exportBrandingActive:'التصدير دون علامة مفعّل.', exportViewPlans:'عرض الخطط', account:'الحساب', exportOneTime:'دفعة واحدة، جميع الأجهزة', exportMonthly:'إلغاء في أي وقت؛ فعال أثناء الاشتراك', exportFullPricing:'عرض الأسعار الكاملة', licenseActivated:'تم تفعيل الوصول إلى MD2PDF على هذا الجهاز.' },
837    it: { exportBrandingTitle:'Rimuovi branding PDF', exportBrandingFree:'Le esportazioni gratuite includono la pagina brand MD2PDF.', exportBrandingActive:'L’esportazione senza branding è attiva.', exportViewPlans:'Vedi piani', account:'Account', exportOneTime:'Pagamento unico, tutti i dispositivi', exportMonthly:'Annulla quando vuoi; attivo durante l’abbonamento', exportFullPricing:'Vedi prezzi completi', licenseActivated:'L’accesso a MD2PDF è stato attivato su questo dispositivo.' }
838};
839Object.entries(billingLocaleText).forEach(([locale, strings]) => Object.assign(langMap[locale], strings));
840
841const getLang = () => {
842    const params = new URLSearchParams(window.location.search);
843    const urlLang = params.get('lang');
844    if (urlLang && langMap[urlLang]) {
845        localStorage.setItem('MD2PDF_LANG', urlLang);
846        return urlLang;
847    }
848    const saved = localStorage.getItem('MD2PDF_LANG');
849    if (saved) return saved;
850    const l = navigator.language || navigator.userLanguage;
851    if (l.startsWith('zh-TW') || l.startsWith('zh-HK')) return 'tw';
852    if (l.startsWith('zh')) return 'zh';
853    if (l.startsWith('es')) return 'es';
854    if (l.startsWith('de')) return 'de';
855    if (l.startsWith('ru')) return 'ru';
856    if (l.startsWith('ja')) return 'ja';
857    if (l.startsWith('ko')) return 'ko';
858    if (l.startsWith('bg')) return 'bg';
859    if (l.startsWith('ar')) return 'ar';
860    if (l.startsWith('it')) return 'it';
861    return 'en';
862};
863
864/**
865 * Updates the canonical tag based on the current URL and language.
866 * Ensures that tracking parameters (UTM, etc.) are stripped.
867 */
868const updateCanonicalTag = () => {
869    let canonical = document.querySelector('link[rel="canonical"]');
870    if (!canonical) {
871        canonical = document.createElement('link');
872        canonical.setAttribute('rel', 'canonical');
873        document.head.appendChild(canonical);
874    }
875
876    try {
877        const url = new URL(window.location.href);
878        const lang = url.searchParams.get('lang');
879        // Ensure pathname ends with a slash if it's a directory
880        let pathname = url.pathname;
881        if (!pathname.endsWith('/') && !pathname.split('/').pop().includes('.')) {
882            pathname += '/';
883        }
884        const cleanUrl = url.origin + pathname + (lang ? `?lang=${lang}` : '');
885        canonical.setAttribute('href', cleanUrl);
886    } catch (e) {
887        console.error('Failed to update canonical tag:', e);
888    }
889};
890
891const translateUI = () => {
892    try {
893        const lang = getLang();
894        const dict = langMap[lang] || langMap['en'];
895
896        updateCanonicalTag();
897
898        // 设置页面方向 (RTL 支持)
899        if (lang === 'ar') {
900            document.documentElement.setAttribute('dir', 'rtl');
901            document.body.classList.add('rtl-mode');
902        } else {
903            document.documentElement.setAttribute('dir', 'ltr');
904            document.body.classList.remove('rtl-mode');
905        }
906
907        document.querySelectorAll('[data-i18n]').forEach(el => {
908            const key = el.getAttribute('data-i18n');
909            if (dict[key]) el.innerHTML = dict[key];
910        });
911        document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
912            const key = el.getAttribute('data-i18n-placeholder');
913            if (dict[key]) el.placeholder = dict[key];
914        });
915        document.querySelectorAll('[data-i18n-title]').forEach(el => {
916            const key = el.getAttribute('data-i18n-title');
917            if (dict[key]) el.title = dict[key];
918        });
919
920        // Dynamic Meta Tag Update
921        // Only update title for main page (not landing pages with specific paths)
922        if (dict.seoTitle && window.location.pathname === '/') {
923            document.title = dict.seoTitle;
924            const ogTitle = document.querySelector('meta[property="og:title"]');
925            if (ogTitle) ogTitle.setAttribute('content', dict.seoTitle);
926        }
927        if (dict.seoDescription) {
928            const metaDesc = document.querySelector('meta[name="description"]');
929            if (metaDesc) metaDesc.setAttribute('content', dict.seoDescription);
930            const ogDesc = document.querySelector('meta[property="og:description"]');
931            if (ogDesc) ogDesc.setAttribute('content', dict.seoDescription);
932
933            // Structured Data update
934            const schemaScript = document.querySelector('script[type="application/ld+json"]');
935            if (schemaScript) {
936                try {
937                    const schemaData = JSON.parse(schemaScript.innerHTML);
938                    schemaData.description = dict.seoDescription;
939                    schemaScript.innerHTML = JSON.stringify(schemaData, null, 2);
940                } catch (e) {
941                    console.error("Schema.org update error:", e);
942                }
943            }
944        }
945
946        uiActions.updatePromptTemplate();
947    } catch (err) {
948        console.error("UI Translation Error:", err);
949    }
950};
951
952/**
953 * UI ACTIONS
954 */
955const uiActions = {
956    toggleLangMenu(e, btn) {
957        e.stopPropagation();
958        if (window.innerWidth <= 768) {
959            const currentLang = getLang();
960            const dict = langMap[currentLang] || langMap['en'];
961            const items = Array.from(document.querySelectorAll('#langMenu .context-item')).map(item => {
962                const isSelected = item.getAttribute('onclick')?.includes(`'${currentLang}'`);
963                return `<div class="p-4 border-b border-border active:bg-accent flex items-center justify-between text-base font-medium ${isSelected ? 'text-primary' : ''}" onclick="uiActions.closeDrawer(); ${item.getAttribute('onclick')}">
964                    <div class="flex items-center gap-3">${item.innerHTML}</div>
965                    ${isSelected ? '<i class="fas fa-check-circle"></i>' : ''}
966                </div>`;
967            }).join('');
968            this.showDrawer(dict.language || 'Language', `<div>${items}</div>`, null, false);
969            return;
970        }
971        const menu = document.getElementById('langMenu');
972        const isHidden = menu.classList.contains('hidden');
973        document.querySelectorAll('.context-menu').forEach(m => {
974            m.classList.add('hidden');
975            m.style.display = 'none';
976        });
977        if (isHidden) {
978            menu.classList.remove('hidden');
979            menu.style.display = 'flex';
980            const rect = btn.getBoundingClientRect();
981            menu.style.top = `${rect.bottom + 8}px`;
982            menu.style.left = `${rect.left}px`;
983
984            const close = () => { menu.classList.add('hidden'); menu.style.display = 'none'; document.removeEventListener('click', close); };
985            setTimeout(() => document.addEventListener('click', close), 10);
986        }
987    },
988    openModal(id) {
989        const m = document.getElementById(id);
990        if (m) {
991            m.classList.remove('hidden');
992            m.style.setProperty('display', 'flex', 'important');
993        }
994    },
995    closeModal(id) {
996        const m = document.getElementById(id);
997        if (m) {
998            m.classList.add('hidden');
999            m.style.setProperty('display', 'none', 'important');
1000        }
1001    },
1002    handleLogoUpload(input, targetId) {
1003        if (input.files && input.files[0]) {
1004            const reader = new FileReader();
1005            reader.onload = (e) => {
1006                document.getElementById(targetId).value = e.target.result;
1007                this.updateLogoPreview(targetId);
1008            };
1009            reader.readAsDataURL(input.files[0]);
1010        }
1011    },
1012    updateLogoPreview(id) {
1013        const input = document.getElementById(id);
1014        if (!input) return;
1015
1016        const val = input.value;
1017        const container = document.getElementById(id + '-preview-container');
1018        const previewImg = document.getElementById(id + '-preview');
1019
1020        // 获取对应的文本输入框 ID (default-logo-left -> default-header-left, export-logo-right -> export-header-right)
1021        let textFieldId = '';
1022        if (id.includes('logo-left')) textFieldId = id.replace('logo-left', 'header-left');
1023        else if (id.includes('logo-right')) textFieldId = id.replace('logo-right', 'header-right');
1024
1025        const textField = textFieldId ? document.getElementById(textFieldId) : null;
1026        const hasLogo = val && val.trim().length > 0;
1027
1028        if (container && previewImg) {
1029            if (hasLogo && (val.startsWith('data:image') || val.startsWith('http'))) {
1030                previewImg.src = val;
1031                container.classList.remove('hidden');
1032                if (val.startsWith('data:image')) {
1033                    input.classList.add('hidden');
1034                } else {
1035                    input.classList.remove('hidden');
1036                }
1037            } else {
1038                container.classList.add('hidden');
1039                input.classList.remove('hidden');
1040            }
1041        }
1042
1043        // 无论是否能预览,只要有值在Logo框里,就禁用文本框
1044        if (textField) {
1045            if (hasLogo) {
1046                textField.disabled = true;
1047                textField.style.opacity = '0.5';
1048                textField.style.cursor = 'not-allowed';
1049                textField.title = 'Logo enabled - Header text for this position is disabled';
1050            } else {
1051                textField.disabled = false;
1052                textField.style.opacity = '1';
1053                textField.style.cursor = 'text';
1054                textField.title = '';
1055            }
1056        }
1057    },
1058    clearLogo(id) {
1059        document.getElementById(id).value = '';
1060        this.updateLogoPreview(id);
1061    },
1062    openPdfSettings() {
1063        const defaults = JSON.parse(localStorage.getItem('MD2PDF_DEFAULT_HEADER_FOOTER') || '{}');
1064        const ids = ['default-subtitle', 'default-header-left', 'default-header-center', 'default-header-right', 'default-footer-left', 'default-footer-center', 'default-footer-right', 'default-logo-left', 'default-logo-right', 'default-watermark'];
1065        ids.forEach(id => {
1066            const el = document.getElementById(id);
1067            if (!el) return;
1068            el.value = defaults[id] || '';
1069        });
1070
1071        ['default-logo-left', 'default-logo-right'].forEach(id => this.updateLogoPreview(id));
1072
1073        // 加载封面页眉页脚设置
1074        const coverShowHeaderEl = document.getElementById('default-cover-show-header');
1075        if (coverShowHeaderEl) {
1076            coverShowHeaderEl.checked = defaults['default-cover-show-header'] || false;
1077        }
1078
1079        this.openModal('pdfSettingsModal');
1080    },
1081    saveDefaultHeaderFooter() {
1082        const defaults = {};
1083        const ids = ['default-subtitle', 'default-header-left', 'default-header-center', 'default-header-right', 'default-footer-left', 'default-footer-center', 'default-footer-right', 'default-logo-left', 'default-logo-right', 'default-watermark'];
1084        ids.forEach(id => {
1085            const el = document.getElementById(id);
1086            if (!el) return;
1087            defaults[id] = el.value;
1088        });
1089
1090        // 保存封面页眉页脚设置
1091        const coverShowHeaderEl = document.getElementById('default-cover-show-header');
1092        if (coverShowHeaderEl) {
1093            defaults['default-cover-show-header'] = coverShowHeaderEl.checked;
1094        }
1095
1096        localStorage.setItem('MD2PDF_DEFAULT_HEADER_FOOTER', JSON.stringify(defaults));
1097        this.closeModal('pdfSettingsModal');
1098        const lang = getLang();
1099        const dict = langMap[lang] || langMap['en'];
1100        this.showToast(dict.styleUpdated || "Default settings saved", "success");
1101    },
1102    openExportSettings() {
1103        // Get current file
1104        const file = db.nodes.find(n => n.id === db.activeId);
1105        if (!file) return;
1106
1107        // Load file's export settings or defaults
1108        const exportSettings = file.exportSettings || {};
1109        const defaults = JSON.parse(localStorage.getItem('MD2PDF_DEFAULT_HEADER_FOOTER') || '{}');
1110
1111        // Set style (radio buttons)
1112        const style = exportSettings.style || db.pdfStyle || 'default';
1113        document.querySelectorAll('input[name="export-style"]').forEach(radio => {
1114            radio.checked = (radio.value === style);
1115        });
1116
1117        // Set cover page
1118        const useCover = exportSettings['export-use-cover'] || false;
1119        const useCoverEl = document.getElementById('export-use-cover');
1120        const coverOptionsEl = document.getElementById('export-cover-options');
1121        const titleEl = document.getElementById('export-title');
1122        const subtitleEl = document.getElementById('export-subtitle');
1123        if (useCoverEl) useCoverEl.checked = useCover;
1124        if (coverOptionsEl) coverOptionsEl.classList.toggle('hidden', !useCover);
1125        if (titleEl) titleEl.value = exportSettings['export-title'] || file.name || '';
1126        if (subtitleEl) subtitleEl.value = exportSettings['export-subtitle'] !== undefined ? exportSettings['export-subtitle'] : (defaults['default-subtitle'] || '');
1127        // document.getElementById('export-cover-show-header').checked = exportSettings['export-cover-show-header'] !== undefined ? exportSettings['export-cover-show-header'] : (defaults['default-cover-show-header'] || false);
1128
1129        // Set TOC
1130        const useTocEl = document.getElementById('export-use-toc');
1131        if (useTocEl) useTocEl.checked = exportSettings['export-use-toc'] || false;
1132
1133        // Set header/footer (use file settings if exist, otherwise use defaults)
1134        ['header-left', 'header-center', 'header-right', 'footer-left', 'footer-center', 'footer-right', 'logo-left', 'logo-right', 'watermark'].forEach(key => {
1135            const el = document.getElementById('export-' + key);
1136            if (el) {
1137                el.value = exportSettings['export-' + key] !== undefined ? exportSettings['export-' + key] : (defaults['default-' + key] || '');
1138            }
1139        });
1140
1141        ['export-logo-left', 'export-logo-right'].forEach(id => this.updateLogoPreview(id));
1142
1143        this.openModal('exportSettingsModal');
1144    },
1145    saveExportSettings() {
1146        const file = db.nodes.find(n => n.id === db.activeId);
1147        if (!file) return;
1148
1149        // Get selected style
1150        const selectedStyle = document.querySelector('input[name="export-style"]:checked');
1151        const style = selectedStyle ? selectedStyle.value : 'default';
1152
1153        // Update global default style
1154        db.pdfStyle = style;
1155
1156        // Collect all export settings
1157        const exportSettings = {
1158            style: style,
1159            'export-use-cover': document.getElementById('export-use-cover').checked,
1160            'export-title': document.getElementById('export-title').value,
1161            'export-subtitle': document.getElementById('export-subtitle').value,
1162            // 'export-cover-show-header': document.getElementById('export-cover-show-header').checked,
1163            'export-use-toc': document.getElementById('export-use-toc').checked,
1164            'export-header-left': document.getElementById('export-header-left').value,
1165            'export-header-center': document.getElementById('export-header-center').value,
1166            'export-header-right': document.getElementById('export-header-right').value,
1167            'export-footer-left': document.getElementById('export-footer-left').value,
1168            'export-footer-center': document.getElementById('export-footer-center').value,
1169            'export-footer-right': document.getElementById('export-footer-right').value,
1170            'export-logo-left': document.getElementById('export-logo-left').value,
1171            'export-logo-right': document.getElementById('export-logo-right').value,
1172            'export-watermark': document.getElementById('export-watermark').value
1173        };
1174
1175        // Save to file's exportSettings
1176        file.exportSettings = exportSettings;
1177        file.updatedAt = Date.now();
1178        save();
1179
1180        this.closeModal('exportSettingsModal');
1181    },
1182    savePdfSettings() {
1183        // This function is deprecated, keeping for backward compatibility
1184        this.saveDefaultHeaderFooter();
1185    },
1186    toggleThemeMenu(e, btn) {
1187        e.stopPropagation();
1188        if (window.innerWidth <= 768) {
1189            const currentTheme = document.body.getAttribute('data-theme') || 'light';
1190            const lang = getLang();
1191            const dict = langMap[lang] || langMap['en'];
1192            const items = Array.from(document.querySelectorAll('#themeMenu .context-item')).map(item => {
1193                const isSelected = item.getAttribute('onclick')?.includes(`'${currentTheme}'`);
1194                return `<div class="p-4 border-b border-border active:bg-accent flex items-center justify-between text-base font-medium ${isSelected ? 'text-primary' : ''}" onclick="uiActions.closeDrawer(); ${item.getAttribute('onclick')}">
1195                    <div class="flex items-center gap-3">${item.innerHTML}</div>
1196                    ${isSelected ? '<i class="fas fa-check-circle"></i>' : ''}
1197                </div>`;
1198            }).join('');
1199            this.showDrawer(dict.theme || 'Theme / 主题', `<div>${items}</div>`, null, false);
1200            return;
1201        }
1202        const menu = document.getElementById('themeMenu');
1203        const isHidden = menu.classList.contains('hidden');
1204        document.querySelectorAll('.context-menu').forEach(m => {
1205            m.classList.add('hidden');
1206            m.style.display = 'none';
1207        });
1208        if (isHidden) {
1209            menu.classList.remove('hidden');
1210            menu.style.display = 'flex';
1211            const rect = btn.getBoundingClientRect();
1212            menu.style.top = `${rect.bottom + 8}px`;
1213            menu.style.left = `${rect.left}px`;
1214
1215            const close = () => { menu.classList.add('hidden'); menu.style.display = 'none'; document.removeEventListener('click', close); };
1216            setTimeout(() => document.addEventListener('click', close), 10);
1217        }
1218    },
1219    togglePdfStyleMenu(e, btn) {
1220        // This function is deprecated - kept for backward compatibility
1221        // Now we use per-file export settings instead
1222        e.stopPropagation();
1223        this.openExportSettings();
1224    },
1225    toggleCopyMenu(e, btn) {
1226        e.stopPropagation();
1227        if (window.innerWidth <= 768) {
1228            const lang = getLang();
1229            const dict = langMap[lang] || langMap['en'];
1230            const items = [
1231                { label: dict.copyEditor || 'Copy Editor', icon: 'fa-file-alt', action: 'uiActions.copyEditor()' },
1232                { label: dict.copyPreview || 'Copy Preview', icon: 'fa-eye', action: 'uiActions.copyPreview()' },
1233                { label: dict.paste || 'Paste from Clipboard', icon: 'fa-paste', action: 'uiActions.pasteFromClipboard()' }
1234            ].map(item =>
1235                `<div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); ${item.action}; uiActions.closeDrawer();">
1236                    <i class="fas ${item.icon} text-primary"></i> ${item.label}
1237                </div>`
1238            ).join('');
1239            this.showDrawer(dict.copyContent || 'Copy Content', `<div>${items}</div>`, null, false);
1240            return;
1241        }
1242        const menu = document.getElementById('copyMenu');
1243        const isHidden = menu.classList.contains('hidden');
1244        document.querySelectorAll('.context-menu').forEach(m => {
1245            m.classList.add('hidden');
1246            m.style.display = 'none';
1247        });
1248        if (isHidden) {
1249            menu.classList.remove('hidden');
1250            menu.style.display = 'flex';
1251            const rect = btn.getBoundingClientRect();
1252            menu.style.top = `${rect.bottom + 8}px`;
1253            menu.style.left = `${rect.left}px`; // Align to left of button
1254
1255            const close = () => { menu.classList.add('hidden'); menu.style.display = 'none'; document.removeEventListener('click', close); };
1256            setTimeout(() => document.addEventListener('click', close), 10);
1257        }
1258    },
1259    setLang(lang) {
1260        try {
1261            const oldLang = getLang();
1262
1263            // 如果目标语言和当前语言一致,直接返回
1264            if (oldLang === lang) {
1265                const menu = document.getElementById('langMenu');
1266                if (menu) {
1267                    menu.classList.add('hidden');
1268                    menu.style.display = 'none';
1269                }
1270                return;
1271            }
1272
1273            // 在更新 localStorage 之前,先获取旧的字典
1274            const oldDict = langMap[oldLang] || langMap['en'];
1275            const newDict = langMap[lang] || langMap['en'];
1276
1277            // 更新语言设置
1278            localStorage.setItem('MD2PDF_LANG', lang);
1279            translateUI();
1280
1281            // 同步更新默认生成的欢迎文档内容
1282            db.nodes.forEach(node => {
1283                // 检查是否是任何一种语言定义的欢迎文件
1284                let isWelcomeFile = false;
1285                let isWelcomeFolder = false;
1286                for (const l in langMap) {
1287                    if (node.name === langMap[l].welcomeFile) isWelcomeFile = true;
1288                    if (node.name === langMap[l].welcomeTitle) isWelcomeFolder = true;
1289                }
1290
1291                if (isWelcomeFolder) {
1292                    node.name = newDict.welcomeTitle;
1293                } else if (isWelcomeFile) {
1294                    node.name = newDict.welcomeFile;
1295                    // 由于用户可能微调过内容,我们采用“模糊匹配”或直接覆盖(因为这是欢迎页)
1296                    // 这里我们保持逻辑:如果内容没怎么变,就更新它
1297                    node.content = newDict.welcomeContent;
1298                }
1299            });
1300
1301            save(); // 确保更改被保存
1302            renderTree();
1303
1304            // 如果当前打开的文件正是欢迎文档,则刷新编辑器内容
1305            if (db.activeId) {
1306                const activeNode = db.nodes.find(n => n.id === db.activeId);
1307                if (activeNode) {
1308                    document.getElementById('editor').value = activeNode.content || '';
1309                    document.getElementById('filenameDisplay').value = activeNode.name;
1310                }
1311            }
1312
1313            updatePreview();
1314
1315            // 更新面包屑(如果当前选中的是默认文件,名称可能变了)
1316            if (db.activeId) updateBreadcrumb(db.activeId);
1317
1318            const menu = document.getElementById('langMenu');
1319            if (menu) {
1320                menu.classList.add('hidden');
1321                menu.style.display = 'none';
1322            }
1323        } catch (err) {
1324            console.error("SetLang Error:", err);
1325        }
1326    },
1327    setTheme(theme) {
1328        // GA4 Tracking
1329        trackEvent('md_theme_change', { theme_name: theme });
1330
1331        document.body.setAttribute('data-theme', theme);
1332        localStorage.setItem('MD2PDF_THEME', theme);
1333        const menu = document.getElementById('themeMenu');
1334        menu.classList.add('hidden');
1335        menu.style.display = 'none';
1336    },
1337    setPdfStyle(style) {
1338        // This function is deprecated - kept for backward compatibility
1339        // Now we use per-file export settings instead
1340        const file = db.nodes.find(n => n.id === db.activeId);
1341        if (!file) return;
1342
1343        if (!file.exportSettings) file.exportSettings = {};
1344        file.exportSettings.style = style;
1345        file.updatedAt = Date.now();
1346        save();
1347
1348        const lang = getLang();
1349        const dict = langMap[lang] || langMap['en'];
1350        uiActions.showToast(dict.styleUpdated || "Style Updated", "success");
1351    },
1352    updatePdfStyleUI() {
1353        // This function is deprecated - kept for backward compatibility
1354        // UI updates are now handled in openExportSettings
1355    },
1356    setView(mode) {
1357        const ws = document.getElementById('workspace');
1358        ws.className = `workspace-container relative mode-${mode}`;
1359
1360        // Update UI state
1361        ['editor', 'split', 'preview'].forEach(m => {
1362            const btn = document.getElementById(`view-${m}`);
1363
1364            if (btn) {
1365                btn.classList.remove('bg-background', 'shadow-sm');
1366                if (m === mode) btn.classList.add('bg-background', 'shadow-sm');
1367            }
1368        });
1369
1370        // Update Mobile Single Toggle Icon
1371        const mobileToggleIconHeader = document.getElementById('mobile-toggle-icon-header');
1372        if (mobileToggleIconHeader) {
1373            mobileToggleIconHeader.className = mode === 'editor' ? 'fas fa-eye text-xs' : 'fas fa-pen text-xs';
1374        }
1375
1376        if (mode !== 'editor') updatePreview();
1377    },
1378    toggleMobileView() {
1379        const ws = document.getElementById('workspace');
1380        const isEditor = ws.classList.contains('mode-editor');
1381        this.setView(isEditor ? 'preview' : 'editor');
1382    },
1383    toggleSidebar() {
1384        const sidebar = document.getElementById('sidebar');
1385        const overlay = document.getElementById('sidebar-overlay');
1386        if (window.innerWidth <= 768) {
1387            const isOpen = sidebar.classList.toggle('open');
1388            if (overlay) {
1389                if (isOpen) overlay.classList.remove('hidden');
1390                else overlay.classList.add('hidden');
1391            }
1392        } else {
1393            sidebar.classList.toggle('closed');
1394        }
1395        this.updateLogoVisibility();
1396        setTimeout(() => window.dispatchEvent(new Event('resize')), 300);
1397    },
1398    updateLogoVisibility() {
1399        const sidebar = document.getElementById('sidebar');
1400        const logoCollapsed = document.getElementById('logo-collapsed');
1401        if (!logoCollapsed) return;
1402
1403        if (window.innerWidth <= 768) {
1404            logoCollapsed.classList.remove('hidden');
1405            logoCollapsed.classList.add('flex');
1406        } else {
1407            if (sidebar.classList.contains('closed')) {
1408                logoCollapsed.classList.remove('hidden');
1409                logoCollapsed.classList.add('flex');
1410            } else {
1411                logoCollapsed.classList.add('hidden');
1412                logoCollapsed.classList.remove('flex');
1413            }
1414        }
1415    },
1416    toggleGuide() {
1417        const m = document.getElementById('guideModal');
1418        const isHidden = m.classList.contains('hidden');
1419        m.classList.toggle('hidden');
1420        m.style.display = isHidden ? 'flex' : 'none';
1421        if (isHidden) {
1422            this.switchGuideTab('link');
1423            this.updatePromptTemplate();
1424        }
1425    },
1426    switchGuideTab(tab) {
1427        const linkBtn = document.getElementById('tabLink');
1428        const actionBtn = document.getElementById('tabAction');
1429        const linkTab = document.getElementById('guideTabLink');
1430        const actionTab = document.getElementById('guideTabAction');
1431
1432        if (tab === 'link') {
1433            linkBtn.className = "px-3 py-1.5 text-[11px] font-bold border-b-2 border-primary text-foreground";
1434            actionBtn.className = "px-3 py-1.5 text-[11px] font-medium text-muted-foreground hover:text-foreground";
1435            linkTab.classList.remove('hidden');
1436            actionTab.classList.add('hidden');
1437        } else {
1438            actionBtn.className = "px-3 py-1.5 text-[11px] font-bold border-b-2 border-primary text-foreground";
1439            linkBtn.className = "px-3 py-1.5 text-[11px] font-medium text-muted-foreground hover:text-foreground";
1440            actionTab.classList.remove('hidden');
1441            linkTab.classList.add('hidden');
1442        }
1443    },
1444    updatePromptTemplate() {
1445        const lang = getLang();
1446        const isZH = lang === 'zh' || lang === 'tw';
1447
1448        const promptText = isZH ?
1449            `你现在是一名专业的文档编写专家。请务必保持回答使用的语言(如果是中文请保持中文)。
1450生成报告后,请根据内容长度选择合适的 URL 格式,放在代码块中提示我复制打开:
1451
1452要求:
14531. 链接格式选择:
1454- **方案 A (推荐)**:若 Markdown 长度较短(约 1500 字符内),请使用 Base64 编码:
1455  https://md2pdf.cc/?title=[标题]&b64=[Base64内容]&mode=export
1456  *(注意:Base64 需先将字符串转为 UTF-8 字节流再进行编码)*
1457
1458- **方案 B (长文本)**:若内容较长,请使用两次 URL 编码:
1459  https://md2pdf.cc/?title=[标题]&content=[二次编码内容]&mode=export
1460  *(注意:必须对全文进行 **两次** 完整的 URL 编码,以保护 #, &, ? 等符号)*
1461
14622. 共同要求:
1463- [标题] 统一使用标准 URL 编码。
1464- 链接必须包含 '&mode=export'。
1465- 若内容超过 4000 字符,请告知链接可能失效,建议手动复制内容到 md2pdf.cc。` :
1466            `You are a professional documentation expert. You MUST maintain the language used in the response.
1467After generating the report, provide a plain text link in a code block based on content length:
1468
1469Requirements:
14701. URL Format Selection:
1471- **Option A (Highly Recommended)**: If content is short (<1500 chars), use Base64:
1472  https://md2pdf.cc/?title=[Title]&b64=[Base64Content]&mode=export
1473  *(Note: Convert string to UTF-8 bytes before Base64 encoding)*
1474
1475- **Option B (Long Content)**: If content is long, use Double URL Encoding:
1476  https://md2pdf.cc/?title=[Title]&content=[DoubleEncodedContent]&mode=export
1477  *(Note: Perform **Double URL Encoding** on the whole body to protect #, &, and ?)*
1478
14792. General Rules:
1480- [Title] MUST be Standard URL encoded.
1481- Link MUST include '&mode=export'.
1482- If content exceeds 4000 chars, warn the user and suggest manual copy-paste.`;
1483
1484        const openApiJson = {
1485            "openapi": "3.1.0",
1486            "info": { "title": "MD2PDF Export", "version": "1.0.0" },
1487            "servers": [{ "url": "https://md2pdf.cc" }],
1488            "paths": {
1489                "/": {
1490                    "get": {
1491                        "operationId": "exportPdf",
1492                        "summary": "Convert text to professional PDF",
1493                        "parameters": [
1494                            { "name": "title", "in": "query", "required": true, "schema": { "type": "string" } },
1495                            { "name": "content", "in": "query", "required": true, "schema": { "type": "string" } },
1496                            { "name": "mode", "in": "query", "schema": { "type": "string", "default": "export" } }
1497                        ]
1498                    }
1499                }
1500            }
1501        };
1502
1503        const pt = document.getElementById('promptTemplate');
1504        const at = document.getElementById('actionTemplate');
1505        if (pt) pt.innerText = promptText;
1506        if (at) at.innerText = JSON.stringify(openApiJson, null, 2);
1507    },
1508    copyPrompt(type) {
1509        const id = type === 'action' ? 'actionTemplate' : 'promptTemplate';
1510        const el = document.getElementById(id);
1511        if (!el) return;
1512        const text = el.innerText;
1513        navigator.clipboard.writeText(text).then(() => {
1514            const lang = getLang();
1515            const dict = langMap[lang] || langMap['en'];
1516            this.showToast(dict.copied || "Copied to clipboard", "success");
1517        });
1518    },
1519    showPrompt(title, defaultValue, callback) {
1520        if (window.innerWidth <= 768) {
1521            this.showDrawer(title, `<input id="drawerInput" type="text" class="w-full h-12 px-4 rounded-xl border border-input bg-muted/30 text-base outline-none focus:ring-2 focus:ring-primary/50" value="${defaultValue}">`, (db) => {
1522                const val = db.querySelector('#drawerInput').value.trim();
1523                callback(val);
1524            });
1525            setTimeout(() => document.getElementById('drawerInput').focus(), 300);
1526            return;
1527        }
1528        const input = document.getElementById('promptInput');
1529        const confirmBtn = document.getElementById('promptConfirmBtn');
1530
1531        document.getElementById('promptTitle').innerText = title;
1532        input.value = defaultValue;
1533        this.openModal('promptModal');
1534        input.focus();
1535        input.select();
1536
1537        const onConfirm = () => {
1538            const val = input.value.trim();
1539            this.closeModal('promptModal');
1540            callback(val);
1541        };
1542
1543        confirmBtn.onclick = onConfirm;
1544        input.onkeydown = (e) => {
1545            if (e.key === 'Enter') onConfirm();
1546            if (e.key === 'Escape') this.closeModal('promptModal');
1547        };
1548    },
1549    showConfirm(title, message, callback) {
1550        if (window.innerWidth <= 768) {
1551            this.showDrawer(title, `<p class="text-sm text-muted-foreground">${message}</p>`, () => {
1552                callback();
1553            });
1554            return;
1555        }
1556        const confirmBtn = document.getElementById('confirmConfirmBtn');
1557        document.getElementById('confirmTitle').innerText = title;
1558        document.getElementById('confirmMessage').innerText = message;
1559
1560        this.openModal('confirmModal');
1561
1562        confirmBtn.onclick = () => {
1563            this.closeModal('confirmModal');
1564            callback();
1565        };
1566    },
1567    showDrawer(title, content, onConfirm, showFooter = true) {
1568        const d = document.getElementById('drawer');
1569        if (this._drawerTimeout) {
1570            clearTimeout(this._drawerTimeout);
1571            this._drawerTimeout = null;
1572        }
1573
1574        const dt = document.getElementById('drawerTitle');
1575        const db = document.getElementById('drawerBody');
1576        const df = d.querySelector('.drawer-footer');
1577        const dcb = document.getElementById('drawerConfirmBtn');
1578
1579        dt.innerText = title;
1580        db.innerHTML = content;
1581        df.style.display = showFooter ? 'flex' : 'none';
1582        d.classList.remove('hidden');
1583        d.style.display = 'flex';
1584        d.classList.remove('closing');
1585
1586        // Translate dynamic content
1587        translateUI();
1588
1589        dcb.onclick = () => {
1590            const result = onConfirm(db);
1591            if (result !== false) this.closeDrawer();
1592        };
1593    },
1594    showNodeActions(id) {
1595        const node = db.nodes.find(n => n.id === id);
1596        if (!node) return;
1597        const lang = getLang();
1598        const dict = langMap[lang] || langMap['en'];
1599
1600        nodeActions.currentId = id;
1601
1602        let items = '';
1603        if (node.type === 'folder') {
1604            items = `
1605                <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); nodeActions.create('file')">
1606                    <i class="fa-solid fa-file-circle-plus w-6 text-center"></i> ${dict.newFile}
1607                </div>
1608                <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); nodeActions.create('folder')">
1609                    <i class="fa-solid fa-folder-plus w-6 text-center"></i> ${dict.newFolder}
1610                </div>
1611            `;
1612        } else {
1613            items = `
1614                <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); nodeActions.download(); uiActions.closeDrawer();">
1615                    <i class="fa-solid fa-download w-6 text-center"></i> ${dict.download}
1616                </div>
1617                <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); if(db.activeId !== '${id}') openFile('${id}'); exportHandler.performPdfExport(); uiActions.closeDrawer();">
1618                    <i class="fa-solid fa-file-pdf w-6 text-center text-primary"></i> ${dict.exportPdf}
1619                </div>
1620            `;
1621        }
1622        items += `
1623            <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium" onclick="event.stopPropagation(); nodeActions.rename()">
1624                <i class="fa-solid fa-pen-to-square w-6 text-center"></i> ${dict.rename}
1625            </div>
1626            <div class="p-4 border-b border-border active:bg-accent flex items-center gap-3 text-base font-medium text-destructive" onclick="event.stopPropagation(); nodeActions.delete()">
1627                <i class="fa-solid fa-trash w-6 text-center"></i> ${dict.delete}
1628            </div>
1629        `;
1630
1631        this.showDrawer(node.name, `<div>${items}</div>`, null, false);
1632    },
1633    closeDrawer() {
1634        const d = document.getElementById('drawer');
1635        if (!d) return;
1636        d.classList.add('closing');
1637        if (this._drawerTimeout) clearTimeout(this._drawerTimeout);
1638        this._drawerTimeout = setTimeout(() => {
1639            d.classList.add('hidden');
1640            d.style.display = 'none';
1641            d.classList.remove('closing');
1642            this._drawerTimeout = null;
1643        }, 300);
1644    },
1645    showAlert(title, message) {
1646        if (window.innerWidth <= 768) {
1647            this.showDrawer(title, `<p class="text-sm text-muted-foreground">${message}</p>`, () => { });
1648            return;
1649        }
1650        document.getElementById('alertTitle').innerText = title;
1651        document.getElementById('alertMessage').innerText = message;
1652        this.openModal('alertModal');
1653    },
1654    showToast(message, type = 'info') {
1655        const container = document.getElementById('toastContainer');
1656        if (!container) return;
1657
1658        const toast = document.createElement('div');
1659        const bgColor = type === 'error' ? 'bg-destructive' : (type === 'success' ? 'bg-green-600' : 'bg-primary');
1660        const textColor = 'text-white';
1661
1662        toast.className = `toast-item ${bgColor} ${textColor} px-4 py-3 rounded-lg flex items-center shadow-2xl space-x-3 pointer-events-auto transition-all`;
1663
1664        const icon = type === 'error' ? 'fa-circle-exclamation' : (type === 'success' ? 'fa-circle-check' : 'fa-info-circle');
1665        toast.innerHTML = `
1666            <i class="fas ${icon} text-lg shrink-0"></i>
1667            <span class="text-sm font-medium leading-tight">${message}</span>
1668        `;
1669
1670        container.appendChild(toast);
1671
1672        setTimeout(() => {
1673            toast.classList.add('exiting');
1674            setTimeout(() => toast.remove(), 200);
1675        }, 3000);
1676    },
1677    showInstallToast(message, btnText, onInstall) {
1678        const container = document.getElementById('toastContainer');
1679        if (!container) return;
1680
1681        const toast = document.createElement('div');
1682        toast.className = `toast-item bg-primary text-white px-4 py-3 rounded-xl flex items-center justify-between shadow-2xl space-x-4 pointer-events-auto transition-all border border-white/10`;
1683
1684        toast.innerHTML = `
1685            <div class="flex items-center space-x-3">
1686                <i class="fas fa-download text-lg shrink-0"></i>
1687                <span class="text-sm font-bold leading-tight">${message}</span>
1688            </div>
1689            <div class="flex items-center gap-2">
1690                <button class="bg-white text-primary px-3 py-1.5 rounded-lg text-xs font-black hover:bg-white/90 transition-colors uppercase tracking-wider">
1691                    ${btnText}
1692                </button>
1693                <button class="close-btn text-white/60 hover:text-white transition-colors p-1">
1694                    <i class="fas fa-times"></i>
1695                </button>
1696            </div>
1697        `;
1698
1699        const btn = toast.querySelector('button:not(.close-btn)');
1700        btn.onclick = () => {
1701            onInstall();
1702            toast.remove();
1703        };
1704
1705        const closeBtn = toast.querySelector('.close-btn');
1706        closeBtn.onclick = () => {
1707            localStorage.setItem('MD2PDF_INSTALL_DISMISSED', 'true');
1708            toast.remove();
1709        };
1710
1711        container.appendChild(toast);
1712    },
1713    async copyToClipboard(text) {
1714        try {
1715            if (navigator.clipboard && window.isSecureContext) {
1716                await navigator.clipboard.writeText(text);
1717            } else {
1718                // Fallback for older browsers or non-secure contexts
1719                const textArea = document.createElement('textarea');
1720                textArea.value = text;
1721                textArea.style.position = 'fixed';
1722                textArea.style.left = '-999999px';
1723                textArea.style.top = '-999999px';
1724                document.body.appendChild(textArea);
1725                textArea.focus();
1726                textArea.select();
1727                document.execCommand('copy');
1728                document.body.removeChild(textArea);
1729            }
1730            const lang = getLang();
1731            const dict = langMap[lang] || langMap['en'];
1732            this.showToast(dict.copied || "Copied to clipboard", "success");
1733        } catch (err) {
1734            console.error('Failed to copy text: ', err);
1735            this.showToast("Failed to copy", "error");
1736        }
1737    },
1738    copyEditor() {
1739        const editor = document.getElementById('editor');
1740        if (editor) {
1741            this.copyToClipboard(editor.value);
1742        }
1743    },
1744    copyPreview() {
1745        const preview = document.getElementById('preview-content');
1746        if (preview) {
1747            // Get text content from the preview, removing HTML tags
1748            const text = preview.textContent || preview.innerText || '';
1749            this.copyToClipboard(text);
1750        }
1751    },
1752    async pasteFromClipboard() {
1753        try {
1754            const text = await navigator.clipboard.readText();
1755            const editor = document.getElementById('editor');
1756            if (editor && text) {
1757                // GA4 Tracking
1758                trackEvent('md_clipboard_paste');
1759
1760                // Insert at cursor
1761                const start = editor.selectionStart;
1762                const end = editor.selectionEnd;
1763                const val = editor.value;
1764                editor.value = val.substring(0, start) + text + val.substring(end);
1765                editor.focus();
1766                editor.setSelectionRange(start + text.length, start + text.length);
1767                updatePreview();
1768                save();
1769            }
1770        } catch (err) {
1771            console.error('Failed to read clipboard: ', err);
1772            // Non-blocking fallback
1773        }
1774    }
1775};
1776
1777/**
1778 * CONFIG & GLOBAL STATE
1779 */
1780const CONFIG = {
1781    CLIENT_ID: '161561844059-n4uhfihna0k9vih6qdb888n587fuc1fr.apps.googleusercontent.com',
1782    STORAGE_KEY: 'MD2PDF_STORAGE_V1',
1783    WELCOME_VERSION: '1773326925' // Unix timestamp or unique version
1784};
1785
1786let db = {
1787    nodes: [],
1788    deletedNodes: [], // Track deleted nodes for sync: { id, remoteId, type }
1789    activeId: null,
1790    expanded: new Set(),
1791    pdfStyle: 'default'
1792};
1793
1794/**
1795 * INITIALIZATION & CORE FUNCTIONS
1796 */
1797function initDefaultDocs() {
1798    const lang = getLang();
1799    const dict = langMap[lang] || langMap['en'];
1800    const folderId = createId();
1801    const fileId = createId();
1802    const now = Date.now();
1803    db.nodes = [
1804        { id: folderId, type: 'folder', name: dict.welcomeTitle, parentId: null, order: 0, updatedAt: now },
1805        { id: fileId, type: 'file', name: dict.welcomeFile, parentId: folderId, content: dict.welcomeContent, order: 0, updatedAt: now }
1806    ];
1807    db.activeId = fileId;
1808    db.expanded = new Set([folderId]);
1809    save();
1810}
1811
1812function checkWelcomeUpdate() {
1813    const lang = getLang();
1814    const dict = langMap[lang] || langMap['en'];
1815    const savedVersion = localStorage.getItem('MD2PDF_WELCOME_VERSION');
1816
1817    // If version mismatched, update it.
1818    if (savedVersion !== CONFIG.WELCOME_VERSION) {
1819        let updated = false;
1820        db.nodes.forEach(node => {
1821            let isWelcomeFile = false;
1822            for (const l in langMap) {
1823                if (node.name === langMap[l].welcomeFile) {
1824                    isWelcomeFile = true;
1825                    break;
1826                }
1827            }
1828            if (isWelcomeFile) {
1829                node.content = dict.welcomeContent;
1830                updated = true;
1831            }
1832        });
1833        if (updated) {
1834            save();
1835        }
1836    }
1837    localStorage.setItem('MD2PDF_WELCOME_VERSION', CONFIG.WELCOME_VERSION);
1838}
1839
1840function normalizeOrders(parentId) {
1841    const siblings = db.nodes.filter(n => n.parentId === parentId)
1842        .sort((a, b) => (a.order || 0) - (b.order || 0) || (a.type === 'folder' ? -1 : 1));
1843    siblings.forEach((n, i) => n.order = i);
1844}
1845
1846function renderTree() {
1847    const container = document.getElementById('treeContainer');
1848    if (!container) return;
1849    container.innerHTML = '';
1850
1851    const renderNodes = (parentId, depth) => {
1852        const children = db.nodes.filter(n => n.parentId === parentId)
1853            .sort((a, b) => (a.order || 0) - (b.order || 0) || (a.type === 'folder' ? -1 : 1));
1854
1855        children.forEach(node => {
1856            const el = document.createElement('div');
1857            el.className = `tree-node ${node.id === db.activeId ? 'active' : ''}`;
1858            el.style.paddingLeft = `${depth * 16 + 12}px`;
1859            el.draggable = true;
1860
1861            const isMobile = window.innerWidth <= 768;
1862
1863            el.innerHTML = `
1864                <i class="fa-solid ${node.type === 'folder' ? (db.expanded.has(node.id) ? 'fa-folder-open' : 'fa-folder') : 'fa-file-lines'} mr-2 opacity-60 text-[12px]"></i>
1865                <span class="truncate text-xs flex-1">${node.name}</span>
1866                <div class="tree-actions">
1867                    ${isMobile ? `
1868                        <button class="w-7 h-7 flex items-center justify-center rounded-md hover:bg-accent/50 active:bg-accent transition-colors" onclick="event.stopPropagation(); uiActions.showNodeActions('${node.id}')">
1869                            <i class="fa-solid fa-ellipsis-vertical text-[12px] opacity-70"></i>
1870                        </button>
1871                    ` : `
1872                        ${node.type === 'folder' ? `
1873                            <button class="action-btn" onclick="event.stopPropagation(); nodeActions.currentId='${node.id}'; nodeActions.create('file')" title="${langMap[getLang()].newFile}"><i class="fa-solid fa-file-circle-plus text-[10px]"></i></button>
1874                            <button class="action-btn" onclick="event.stopPropagation(); nodeActions.currentId='${node.id}'; nodeActions.create('folder')" title="${langMap[getLang()].newFolder}"><i class="fa-solid fa-folder-plus text-[10px]"></i></button>
1875                        ` : `
1876                            <button class="action-btn text-primary" onclick="event.stopPropagation(); nodeActions.currentId='${node.id}'; nodeActions.download()" title="${langMap[getLang()].download}"><i class="fa-solid fa-download text-[10px]"></i></button>
1877                            <button class="action-btn text-primary" onclick="event.stopPropagation(); if(db.activeId !== '${node.id}') openFile('${node.id}'); exportHandler.performPdfExport()" title="${langMap[getLang()].exportPdf}"><i class="fa-solid fa-file-pdf text-[10px]"></i></button>
1878                        `}
1879                        <button class="action-btn" onclick="event.stopPropagation(); nodeActions.currentId='${node.id}'; nodeActions.rename()" title="${langMap[getLang()].rename}"><i class="fa-solid fa-pen-to-square text-[10px]"></i></button>
1880                        <button class="action-btn text-destructive" onclick="event.stopPropagation(); nodeActions.currentId='${node.id}'; nodeActions.delete()" title="${langMap[getLang()].delete}"><i class="fa-solid fa-trash text-[10px]"></i></button>
1881                    `}
1882                </div>
1883            `;
1884
1885            el.onclick = (e) => {
1886                e.stopPropagation();
1887                if (node.type === 'folder') {
1888                    if (db.expanded.has(node.id)) db.expanded.delete(node.id);
1889                    else db.expanded.add(node.id);
1890                    // On mobile, also make it active to show selected state in drawer if needed
1891                    if (isMobile) db.activeId = node.id;
1892                    renderTree();
1893                } else {
1894                    openFile(node.id);
1895                }
1896            };
1897
1898            // Drag and Drop
1899            el.ondragstart = (e) => {
1900                e.dataTransfer.setData('text/plain', node.id);
1901                e.dataTransfer.effectAllowed = 'move';
1902            };
1903
1904            el.ondragover = (e) => {
1905                e.preventDefault();
1906                const rect = el.getBoundingClientRect();
1907                const relY = e.clientY - rect.top;
1908
1909                el.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom');
1910
1911                if (node.type === 'folder') {
1912                    if (relY < rect.height * 0.2) el.classList.add('drag-over-top');
1913                    else if (relY > rect.height * 0.8) el.classList.add('drag-over-bottom');
1914                    else el.classList.add('drag-over');
1915                } else {
1916                    if (relY < rect.height * 0.5) el.classList.add('drag-over-top');
1917                    else el.classList.add('drag-over-bottom');
1918                }
1919            };
1920
1921            el.ondragleave = () => {
1922                el.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom');
1923            };
1924
1925            el.ondrop = (e) => {
1926                e.preventDefault();
1927                el.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom');
1928                const draggedId = e.dataTransfer.getData('text/plain');
1929                if (draggedId === node.id) return;
1930
1931                const rect = el.getBoundingClientRect();
1932                const relY = e.clientY - rect.top;
1933                const draggedNode = db.nodes.find(n => n.id === draggedId);
1934                if (!draggedNode) return;
1935
1936                let targetParentId = node.parentId;
1937                let newOrder = 0;
1938
1939                if (node.type === 'folder' && relY >= rect.height * 0.2 && relY <= rect.height * 0.8) {
1940                    // Drop INTO folder
1941                    targetParentId = node.id;
1942                    const siblings = db.nodes.filter(n => n.parentId === targetParentId);
1943                    newOrder = siblings.length > 0 ? Math.max(...siblings.map(s => s.order || 0)) + 1 : 0;
1944                } else {
1945                    // Drop BEFORE or AFTER
1946                    if (relY < (node.type === 'folder' ? rect.height * 0.2 : rect.height * 0.5)) {
1947                        newOrder = (node.order || 0) - 0.5;
1948                    } else {
1949                        newOrder = (node.order || 0) + 0.5;
1950                    }
1951                }
1952
1953                if (isDescendant(draggedId, targetParentId)) {
1954                    uiActions.showToast("Cannot move folder into itself", "error");
1955                    return;
1956                }
1957
1958                draggedNode.parentId = targetParentId;
1959                draggedNode.order = newOrder;
1960
1961                normalizeOrders(targetParentId);
1962
1963                if (targetParentId) db.expanded.add(targetParentId);
1964                save();
1965                renderTree();
1966                updateBreadcrumb(db.activeId);
1967            };
1968
1969            container.appendChild(el);
1970            if (node.type === 'folder' && db.expanded.has(node.id)) {
1971                renderNodes(node.id, depth + 1);
1972            }
1973        });
1974    };
1975
1976    renderNodes(null, 0);
1977
1978    // Allow dropping on the root container
1979    container.ondragover = (e) => e.preventDefault();
1980    container.ondrop = (e) => {
1981        if (e.target === container) {
1982            const draggedId = e.dataTransfer.getData('text/plain');
1983            const draggedNode = db.nodes.find(n => n.id === draggedId);
1984            if (draggedNode && draggedNode.parentId !== null) {
1985                draggedNode.parentId = null;
1986                draggedNode.order = db.nodes.filter(n => n.parentId === null).length;
1987                normalizeOrders(null);
1988                save();
1989                renderTree();
1990                updateBreadcrumb(db.activeId);
1991            }
1992        }
1993    };
1994}
1995
1996function isDescendant(parentIdx, childId) {
1997    if (!childId) return false;
1998    let curr = db.nodes.find(n => n.id === childId);
1999    while (curr) {
2000        if (curr.parentId === parentIdx) return true;
2001        curr = db.nodes.find(n => n.id === curr.parentId);
2002    }
2003    return false;
2004}
2005
2006function openFile(id) {
2007    db.activeId = id;
2008    const file = db.nodes.find(n => n.id === id);
2009    if (!file) return;
2010    const editor = document.getElementById('editor');
2011    editor.value = file.content || '';
2012    document.getElementById('filenameDisplay').value = file.name;
2013    updateBreadcrumb(id);
2014    renderTree();
2015    save();
2016    updatePreview();
2017
2018    if (window.innerWidth <= 768) {
2019        const sidebar = document.getElementById('sidebar');
2020        const overlay = document.getElementById('sidebar-overlay');
2021        if (sidebar) sidebar.classList.remove('open');
2022        if (overlay) overlay.classList.add('hidden');
2023    }
2024}
2025
2026const nodeActions = {
2027    currentId: null,
2028    create(type) {
2029        const lang = getLang();
2030        const dict = langMap[lang] || langMap['en'];
2031        const title = type === 'file' ? dict.newFileTitle : dict.newFolderTitle;
2032        const defaultName = type === 'file' ? 'NewNote.md' : (dict.newFolder || 'New Folder');
2033
2034        uiActions.showPrompt(title, defaultName, (name) => {
2035            if (!name) return;
2036            const parent = db.nodes.find(n => n.id === this.currentId);
2037            const parentId = parent ? (parent.type === 'folder' ? parent.id : parent.parentId) : null;
2038            const siblings = db.nodes.filter(n => n.parentId === parentId);
2039
2040            const newNode = {
2041                id: createId(),
2042                type,
2043                name,
2044                parentId,
2045                content: type === 'file' ? '' : undefined,
2046                order: siblings.length,
2047                updatedAt: Date.now()
2048            };
2049
2050            db.nodes.push(newNode);
2051            if (parentId) db.expanded.add(parentId);
2052            if (type === 'file') openFile(newNode.id);
2053            renderTree();
2054            save();
2055        });
2056    },
2057    delete() {
2058        const lang = getLang();
2059        const dict = langMap[lang] || langMap['en'];
2060
2061        uiActions.showConfirm(dict.delete || "Delete", langMap[lang].confirmDelete, () => {
2062            const idsToDelete = new Set();
2063            const nodesToDelete = [];
2064            const collectIds = (id) => {
2065                const node = db.nodes.find(n => n.id === id);
2066                if (node) {
2067                    idsToDelete.add(id);
2068                    nodesToDelete.push(node);
2069                }
2070                db.nodes.filter(n => n.parentId === id).forEach(child => collectIds(child.id));
2071            };
2072            collectIds(this.currentId);
2073
2074            // Add to deletedNodes tracking if they have remoteId
2075            nodesToDelete.forEach(node => {
2076                if (node.remoteId) {
2077                    db.deletedNodes.push({
2078                        id: node.id,
2079                        remoteId: node.remoteId,
2080                        type: node.type
2081                    });
2082                }
2083            });
2084
2085            db.nodes = db.nodes.filter(n => !idsToDelete.has(n.id));
2086            if (idsToDelete.has(db.activeId)) {
2087                db.activeId = null;
2088                const editor = document.getElementById('editor');
2089                if (editor) editor.value = '';
2090                const filenameDisplay = document.getElementById('filenameDisplay');
2091                if (filenameDisplay) filenameDisplay.value = '';
2092                updatePreview();
2093            }
2094            renderTree();
2095            save();
2096        });
2097    },
2098    rename() {
2099        const node = db.nodes.find(n => n.id === this.currentId);
2100        const lang = getLang();
2101        const dict = langMap[lang] || langMap['en'];
2102
2103        uiActions.showPrompt(dict.renameTitle, node.name, (newName) => {
2104            if (newName && newName !== node.name) {
2105                node.name = newName;
2106                node.updatedAt = Date.now();
2107                renderTree();
2108                save();
2109                if (node.id === db.activeId) {
2110                    document.getElementById('filenameDisplay').value = node.name;
2111                }
2112            }
2113        });
2114    },
2115    download() {
2116        const node = db.nodes.find(n => n.id === this.currentId);
2117        if (node.type !== 'file') return;
2118        const blob = new Blob([node.content], { type: 'text/markdown' });
2119        const url = URL.createObjectURL(blob);
2120        const a = document.createElement('a');
2121        a.href = url; a.download = node.name; a.click();
2122    }
2123};
2124
2125/**
2126 * URL PARAMS (GROWTH ENGINE)
2127 */
2128function handleUrlParams() {
2129    const params = new URLSearchParams(window.location.search);
2130    const titleParam = params.get('title');
2131    const contentParam = params.get('content');
2132    const b64 = params.get('b64');
2133    const mode = params.get('mode');
2134
2135    // 鲁棒的 URL 解码逻辑:针对 Double Encoding 设计
2136    const robustDecode = (str) => {
2137        if (!str) return "";
2138        let current = str;
2139
2140        // 尝试最多解码 3 次,处理可能存在的 Double/Triple Encoding
2141        for (let i = 0; i < 3; i++) {
2142            if (!current.includes('%')) break;
2143            try {
2144                // 标准解码
2145                const next = decodeURIComponent(current);
2146                if (next === current) break;
2147                current = next;
2148            } catch (e) {
2149                // 如果标准解码因非法字符(AI 幻觉)报错,尝试字节修复
2150                try {
2151                    const bytes = [];
2152                    for (let j = 0; j < current.length; j++) {
2153                        if (current[j] === '%' && /^[0-9A-Fa-f]{2}$/.test(current.substring(j + 1, j + 3))) {
2154                            bytes.push(parseInt(current.substring(j + 1, j + 3), 16));
2155                            j += 2;
2156                        } else {
2157                            // 跳过 AI 误加的非法百分号或是保留原文
2158                            if (current[j] === '%' && j + 1 < current.length && current.charCodeAt(j + 1) > 127) continue;
2159                            const charBytes = new TextEncoder().encode(current[j]);
2160                            charBytes.forEach(b => bytes.push(b));
2161                        }
2162                    }
2163                    current = new TextDecoder('utf-8', { fatal: false }).decode(new Uint8Array(bytes));
2164                } catch (err) {
2165                    break;
2166                }
2167            }
2168        }
2169        return current;
2170    };
2171
2172    if (titleParam || contentParam || b64) {
2173        try {
2174            let title = robustDecode(titleParam) || 'Untitled';
2175            let content = '';
2176
2177            if (b64) {
2178                // b64 保持原样
2179                try {
2180                    const binString = window.atob(b64.replace(/ /g, '+'));
2181                    const bytes = new Uint8Array(binString.length);
2182                    for (let i = 0; i < binString.length; i++) bytes[i] = binString.charCodeAt(i);
2183                    content = new TextDecoder().decode(bytes);
2184                } catch (e) {
2185                    try {
2186                        content = decodeURIComponent(escape(window.atob(b64.replace(/ /g, '+'))));
2187                    } catch (e2) {
2188                        content = "Error decoding Base64 content.";
2189                    }
2190                }
2191            } else {
2192                content = robustDecode(contentParam || '');
2193            }
2194
2195            if (!content && !b64) return;
2196
2197            const newNode = {
2198                id: createId(),
2199                type: 'file',
2200                name: title.endsWith('.md') ? title : title + ".md",
2201                parentId: null,
2202                content: content,
2203                updatedAt: Date.now()
2204            };
2205            db.nodes.push(newNode);
2206            openFile(newNode.id);
2207
2208            if (mode === 'export') {
2209                const lang = getLang();
2210                const dict = langMap[lang] || langMap['en'];
2211                uiActions.showToast(dict.exporting || "Exporting PDF...", "info");
2212
2213                setTimeout(() => {
2214                    exportHandler.performPdfExport();
2215                }, 1200);
2216            }
2217
2218            window.history.replaceState({}, document.title, "/");
2219            save(); // 确保持久化导入的内容
2220            return true;
2221        } catch (err) {
2222            console.error("URL Params Parse Error:", err);
2223            return false;
2224        }
2225    }
2226    return false;
2227}
2228
2229/**
2230 * LICENSE / BILLING STATE
2231 * The server is the source of truth. localStorage is only a short offline cache.
2232 */
2233const licenseManager = {
2234    cacheKey: 'MD2PDF_LICENSE_CACHE_V1',
2235    cacheTtlMs: 5 * 60 * 1000,
2236    offlineGraceMs: 7 * 24 * 60 * 60 * 1000,
2237    state: null,
2238    refreshPromise: null,
2239
2240    copy() {
2241        const localized = langMap[getLang()] || langMap.en;
2242        return getLang() === 'zh'
2243            ? { upgrade: '移除 PDF 品牌信息', statusFree: '免费版:最后一页包含 MD2PDF 品牌信息', statusPro: '品牌信息已移除', lifetime: '永久版 · $19.9', monthly: '月度版 · $1.9/月', oneTime: '一次性付款,所有设备可用', monthlyDesc: '可随时取消;订阅有效期内可用', fullPricing: '查看完整价格方案', restore: '恢复购买', manage: '管理订阅', account: '账户', close: '关闭', email: '购买时使用的邮箱', linkSent: '请检查邮箱并点击激活链接。', checkoutError: '暂时无法打开支付页面,请稍后重试。', linkError: '激活链接发送失败,请稍后重试。', title: '移除 PDF 品牌信息', desc: '免费导出仍可使用。付费后只会影响新的 PDF 导出。', licenseActivated: 'MD2PDF 权益已在此设备上激活。' }
2244            : { upgrade: 'Remove PDF branding', statusFree: 'Free: MD2PDF branding is included on the last page', statusPro: 'PDF branding is removed', lifetime: 'Lifetime · $19.9', monthly: 'Monthly · $1.9/mo', oneTime: 'One-time payment, all devices', monthlyDesc: 'Cancel anytime; active while subscribed', fullPricing: 'View full pricing', restore: 'Restore purchase', manage: 'Manage subscription', account: 'Account', close: 'Close', email: 'Email used for purchase', linkSent: 'Check your email for the activation link.', checkoutError: 'Unable to open checkout. Please try again.', linkError: 'Unable to send the activation link.', title: 'Remove PDF branding', desc: 'The free export remains available. Payment affects new PDF exports only.', licenseActivated: localized.licenseActivated || 'MD2PDF access activated on this device.' };
2245    },
2246
2247    readCache() { try { return JSON.parse(localStorage.getItem(this.cacheKey) || 'null'); } catch { return null; } },
2248    writeCache(state) { try { localStorage.setItem(this.cacheKey, JSON.stringify(state)); } catch { /* private browsing */ } },
2249
2250    async api(path, options = {}) {
2251        const controller = new AbortController();
2252        const timeout = setTimeout(() => controller.abort(), options.timeout || 6000);
2253        try {
2254            const response = await fetch(path, { credentials: 'include', ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } });
2255            const data = await response.json().catch(() => ({}));
2256            if (!response.ok) throw new Error(data.error || `Request failed (${response.status})`);
2257            return data;
2258        } finally { clearTimeout(timeout); }
2259    },
2260
2261    async refresh(force = false) {
2262        const now = Date.now();
2263        if (!force && this.state?.checkedAt && now - new Date(this.state.checkedAt).getTime() < this.cacheTtlMs) return this.state;
2264        if (this.refreshPromise) return this.refreshPromise;
2265        this.refreshPromise = this.api('/api/license/status')
2266            .then(data => { this.state = data; this.writeCache(data); this.updateUi(); return data; })
2267            .catch(error => {
2268                const cached = this.state || this.readCache();
2269                if (cached?.canRemoveBranding && cached.checkedAt && now - new Date(cached.checkedAt).getTime() <= this.offlineGraceMs) {
2270                    this.state = { ...cached, offline: true }; this.updateUi(); return this.state;
2271                }
2272                console.debug('[License] status unavailable:', error.message);
2273                this.state = { authenticated: false, canRemoveBranding: false, entitlement: 'branding_required', checkedAt: new Date().toISOString() }; this.updateUi(); return this.state;
2274            })
2275            .finally(() => { this.refreshPromise = null; });
2276        return this.refreshPromise;
2277    },
2278
2279    async canRemoveBranding() { return (await this.refresh())?.canRemoveBranding === true; },
2280
2281    async startCheckout(plan) {
2282        try {
2283            const result = await this.api('/api/billing/create-checkout', { method: 'POST', body: JSON.stringify({ plan }) });
2284            if (result.url) window.location.href = result.url;
2285        } catch { uiActions.showToast(this.copy().checkoutError, 'error'); }
2286    },
2287
2288    async requestMagicLink() {
2289        const input = document.getElementById('licenseEmailInput');
2290        const message = document.getElementById('licenseModalMessage');
2291        const email = input?.value.trim();
2292        if (!email) return;
2293        try {
2294            await this.api('/api/auth/request-link', { method: 'POST', body: JSON.stringify({ email }) });
2295            if (message) { message.textContent = this.copy().linkSent; message.className = 'mt-2 text-xs text-green-600'; }
2296        } catch {
2297            if (message) { message.textContent = this.copy().linkError; message.className = 'mt-2 text-xs text-red-600'; }
2298        }
2299    },
2300
2301    async openPortal() {
2302        try {
2303            const result = await this.api('/api/billing/portal', { method: 'POST', body: '{}' });
2304            if (result.url) window.location.href = result.url;
2305        } catch { uiActions.showToast(this.copy().checkoutError, 'error'); }
2306    },
2307
2308    closeModal() { document.getElementById('licenseUpgradeModal')?.remove(); },
2309
2310    openUpgrade() {
2311        this.closeModal();
2312        const c = this.copy();
2313        const modal = document.createElement('div');
2314        modal.id = 'licenseUpgradeModal';
2315        modal.className = 'fixed inset-0 z-[10000] flex items-start sm:items-center justify-center overflow-y-auto bg-black/40 p-3 sm:p-4';
2316        modal.innerHTML = `<div class="my-3 sm:my-6 w-full max-w-md rounded-2xl border border-border bg-card p-4 sm:p-5 shadow-2xl" style="max-height:calc(100dvh - 1.5rem);overflow-y:auto;">
2317            <div class="flex items-start justify-between gap-3"><div><h2 class="text-lg font-bold">${c.title}</h2><p class="mt-1 text-xs text-muted-foreground">${c.desc}</p></div><button type="button" class="h-7 w-7 rounded-md hover:bg-accent" onclick="licenseManager.closeModal()">×</button></div>
2318            <div class="mt-5 grid gap-3"><button type="button" class="rounded-xl border-2 border-primary bg-primary/5 p-4 text-left hover:bg-primary/10" onclick="licenseManager.startCheckout('lifetime')"><span class="block text-sm font-bold">${c.lifetime}</span><span class="mt-1 block text-xs text-muted-foreground">${c.oneTime}</span></button><button type="button" class="rounded-xl border border-border p-4 text-left hover:bg-accent" onclick="licenseManager.startCheckout('monthly')"><span class="block text-sm font-bold">${c.monthly}</span><span class="mt-1 block text-xs text-muted-foreground">${c.monthlyDesc}</span></button></div>
2319            <div class="mt-5 border-t border-border pt-4"><label class="block text-xs font-semibold mb-1.5">${c.email}</label><div class="flex flex-col gap-2 sm:flex-row"><input id="licenseEmailInput" type="email" class="min-w-0 flex-1 h-10 rounded-lg border border-input bg-background px-3 text-xs" placeholder="[email protected]"><button type="button" class="h-10 w-full shrink-0 rounded-lg bg-secondary px-3 text-xs font-bold sm:w-auto" onclick="licenseManager.requestMagicLink()">${c.restore}</button></div><p id="licenseModalMessage" class="mt-2 text-xs text-muted-foreground"></p></div>
2320            <div class="mt-4 flex items-center justify-between gap-3"><a href="/pricing/" class="text-xs text-primary hover:underline">${c.fullPricing}</a><button type="button" class="text-xs text-muted-foreground hover:text-foreground" onclick="licenseManager.closeModal()">${c.close}</button></div>
2321        </div>`;
2322        modal.addEventListener('click', event => { if (event.target === modal) this.closeModal(); });
2323        document.body.appendChild(modal);
2324    },
2325
2326    updateUi() {
2327        const statusEl = document.getElementById('licenseStatusText');
2328        const exportButton = document.getElementById('exportBillingButton');
2329        const accountLink = document.getElementById('exportBillingAccountLink');
2330        const exportStatus = document.getElementById('exportBillingStatus');
2331        const exportTitle = document.getElementById('exportBillingTitle');
2332        const c = this.copy();
2333        if (statusEl) statusEl.textContent = this.state?.canRemoveBranding ? c.statusPro : c.statusFree;
2334        if (exportTitle) exportTitle.textContent = this.state?.canRemoveBranding ? c.statusPro : c.title;
2335        if (exportStatus) exportStatus.textContent = this.state?.canRemoveBranding ? c.statusPro : c.statusFree;
2336        if (exportButton) {
2337            if (this.state?.canRemoveBranding) {
2338                if (this.state?.canManageBilling) {
2339                    exportButton.hidden = false;
2340                    exportButton.textContent = c.manage;
2341                    exportButton.onclick = () => this.openPortal();
2342                } else {
2343                    // The static Account link beside this button already provides account access.
2344                    exportButton.hidden = true;
2345                }
2346            } else {
2347                exportButton.hidden = false;
2348                exportButton.textContent = c.upgrade;
2349                exportButton.onclick = () => this.openUpgrade();
2350            }
2351        }
2352        if (accountLink) accountLink.hidden = false;
2353    },
2354
2355    mount() {
2356        this.updateUi();
2357        this.refresh();
2358        const params = new URLSearchParams(window.location.search);
2359        if (params.get('billing') === 'success') {
2360            setTimeout(() => { uiActions.showToast('Payment received. Enter your payment email to activate this device.', 'success'); this.openUpgrade(); }, 300);
2361        } else if (params.get('license') === 'activated') {
2362            this.refresh(true);
2363            uiActions.showToast(this.copy().licenseActivated, 'success');
2364        }
2365    }
2366};
2367
2368window.licenseManager = licenseManager;
2369
2370/**
2371 * PDF EXPORT (THE CORE)
2372 */
2373const exportHandler = {
2374    async performPdfExport() {
2375        if (this.isExporting) return;
2376        const file = db.nodes.find(n => n.id === db.activeId);
2377        if (!file) return;
2378
2379        // GA4 Tracking
2380        trackEvent('md_export_pdf', {
2381            theme_used: document.body.getAttribute('data-theme') || 'light',
2382            content_length: file.content ? file.content.length : 0
2383        });
2384
2385        // 强行更新一次预览,确保获取的是最新内容,尤其是移动端处于编辑模式时
2386        updatePreview();
2387
2388        const previewEl = document.getElementById('preview-content');
2389        if (!previewEl) return;
2390
2391        const lang = getLang();
2392        const dict = langMap[lang] || langMap['en'];
2393        const canRemoveBranding = await licenseManager.canRemoveBranding();
2394
2395        this.isExporting = true;
2396        document.body.classList.add('is-exporting');
2397
2398        const exportBtn = document.querySelector('[onclick*="performPdfExport"]');
2399        const originalBtnHtml = exportBtn ? exportBtn.innerHTML : '';
2400        if (exportBtn) {
2401            exportBtn.style.opacity = '0.5';
2402            exportBtn.innerHTML = `<i class="fas fa-spinner fa-spin sm:mr-2"></i><span class="hidden sm:inline">${dict.exporting}</span>`;
2403        }
2404
2405        // 1. 获取文件的导出设置(如果没有则使用默认值)
2406        const exportSettings = file.exportSettings || {};
2407        const style = exportSettings.style || db.pdfStyle || 'default';
2408        let styleCss = '';
2409
2410        if (style === 'academic') {
2411            styleCss = `
2412                .pdf-main-container {
2413                    padding: 0 !important;
2414                    font-family: "STSong", "SimSun", "Garamond", "Georgia", serif !important;
2415                    line-height: 1.8 !important;
2416                    text-align: justify;
2417                }
2418                .markdown-body { font-family: inherit !important; font-size: 15px !important; color: #111 !important; }
2419                .markdown-body h1, .markdown-body h2 { border-bottom: none !important; text-align: center; margin-top: 1.5em !important; font-weight: 700; }
2420                .markdown-body blockquote { border-left: 3px solid #ccc !important; font-style: italic; color: #444 !important; background: transparent !important; }
2421                .markdown-body code { background: #f5f5f5 !important; }
2422            `;
2423        } else if (style === 'modern') {
2424            styleCss = `
2425                .pdf-main-container {
2426                    padding: 0 !important;
2427                    font-family: "Inter", -apple-system, system-ui, sans-serif !important;
2428                    line-height: 1.6 !important;
2429                    color: #1a1a1a !important;
2430                }
2431                .markdown-body { font-family: inherit !important; font-size: 16px !important; }
2432                .markdown-body h1 { color: #000; font-weight: 800; letter-spacing: -0.02em; border: none !important; font-size: 2.25em !important; }
2433                .markdown-body h2 { border: none !important; font-weight: 700; margin-top: 2em !important; }
2434                .markdown-body pre { background: #f8f9fa !important; border: 1px solid #e9ecef !important; border-radius: 12px !important; padding: 20px !important; }
2435                .markdown-body code { color: #7c3aed !important; background: rgba(124, 58, 237, 0.05) !important; border-radius: 4px; }
2436                .markdown-body blockquote { border: none !important; padding: 20px !important; background: #f1f5f9 !important; border-radius: 12px !important; color: #475569 !important; }
2437            `;
2438        } else if (style === 'standard') {
2439            styleCss = `
2440                .pdf-main-container {
2441                    padding: 0 !important;
2442                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif !important;
2443                    line-height: 1.6 !important;
2444                }
2445                .markdown-body { font-family: inherit !important; font-size: 16px !important; }
2446                .markdown-body h1 { border-bottom: 1px solid #eaecef 
2446!important; padding-bottom: 0.3em !important; font-weight: 600; }
2447                .markdown-body h2 { border-bottom: 1px solid #eaecef !important; padding-bottom: 0.3em !important; font-weight: 600; }
2448                .markdown-body pre { background-color: #f6f8fa !important; border-radius: 6px !important; padding: 16px !important; }
2449            `;
2450        } else {
2451            // "Default" - Exactly match the preview's look
2452            styleCss = `
2453                .pdf-main-container {
2454                    padding: 0 !important;
2455                    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Amiri", "Tahoma", "Arial", sans-serif !important;
2456                    line-height: 1.75 !important;
2457                }
2458                .markdown-body { font-family: inherit !important; font-size: 16px !important; color: #24292e !important; }
2459                .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
2460                    font-family: inherit !important;
2461                    font-weight: 600 !important;
2462                    line-height: 1.25 !important;
2463                    margin-top: 1.5em !important;
2464                    margin-bottom: 1rem !important;
2465                }
2466                .markdown-body h1 { font-size: 2em !important; padding-bottom: 0.3em !important;
2466 border-bottom: 1px solid #eaecef !important; }
2467                .markdown-body h2 { font-size: 1.5em !important; padding-bottom: 0.3em !important; border-bottom: 1px solid #eaecef !important; }
2468                .markdown-body pre {
2469                    background-color: #f6f8fa !important;
2470                    border-radius: 8px !important;
2471                    padding: 16px !important;
2472                    border: 1px solid #e1e4e8 !important;
2473                }
2474                .markdown-body blockquote {
2475                    padding: 0 1em !important;
2476                    color: #6a737d !important;
2477                    border-left: 0.25em solid #dfe2e5 !important;
2478                    background: transparent !important;
2479                }
2480            `;
2481        }
2482
2483        // 2. 获取 PDF 设置并准备注入内容
2484        // 优先使用文件级别的导出设置,如果没有则使用默认的页眉页脚设置
2485        const dateStr = new Date().toLocaleDateString(lang === 'zh' ? 'zh-CN' : (lang === 'tw' ? 'zh-TW' : 'en-US'), {
2486            year: 'numeric',
2487            month: 'long',
2488            day: 'numeric'
2489        });
2490
2491        const replaceMacros = (str) => {
2492            if (!str) return '';
2493            return str
2494                .replace(/{date}/g, dateStr)
2495                .replace(/{title}/g, file.name || 'document')
2496                // 确保页码元素有内容,以便计数器在某些浏览器模式下生效
2497                .replace(/{pagenum}/g, '<span class="page-number">&nbsp;</span>')
2498                .replace(/{totalpages}/g, '<span class="page-count"></span>');
2499        };
2500
2501        const hLeft = replaceMacros(exportSettings['export-header-left']);
2502        const hCenter = replaceMacros(exportSettings['export-header-center']);
2503        const hRight = replaceMacros(exportSettings['export-header-right']);
2504        const fLeft = replaceMacros(exportSettings['export-footer-left']);
2505        const fCenter = replaceMacros(exportSettings['export-footer-center']);
2506        const fRight = replaceMacros(exportSettings['export-footer-right']);
2507
2508        const logoLeft = exportSettings['export-logo-left'] || '';
2509        const logoRight = exportSettings['export-logo-right'] || '';
2510        const watermarkText = exportSettings['export-watermark'] || '';
2511
2512        // 检查是否真的设置了页眉或页脚内容 (包括文字和 Logo)
2513        const hasHeader = !!(exportSettings['export-header-left'] || exportSettings['export-header-center'] || exportSettings['export-header-right'] || logoLeft || logoRight);
2514        const hasFooter = !!(exportSettings['export-footer-left'] || exportSettings['export-footer-center'] || exportSettings['export-footer-right']);
2515
2516        const container = document.createElement('div');
2517        container.id = 'pdf-export-wrapper';
2518        if (lang === 'ar') {
2519            container.setAttribute('dir', 'rtl');
2520            container.classList.add('rtl-mode');
2521        }
2522        Object.assign(container.style, {
2523            // 定位到视口原点,确保 iOS Safari 不会因元素超出渲染范围而跳过布局计算。
2524            // 不使用 zIndex:'-1'(内联样式优先级高于 @media print 规则,会导致打印时
2525            // 容器被渲染在页面白色背景之后,输出空白 PDF)。
2526            // clip-path:inset(100%) 在屏幕上完全剪裁元素使其不可见,且不影响布局测量,
2527            // @media print 中将被 clip-path:none 覆盖。
2528            position: 'fixed',
2529            top: '0',
2530            left: '0',
2531            width: '210mm',
2532            pointerEvents: 'none',
2533            background: 'white',
2534            clipPath: 'inset(100%)',
2535            visibility: 'hidden'
2536        });
2537
2538        // 3. 克隆内容并处理封面页与目录
2539        const clone = previewEl.cloneNode(true);
2540        clone.querySelectorAll('.copy-btn, button, .action-btn').forEach(el => el.remove());
2541
2542        let coverHtml = '';
2543        // 移除 export-cover-show-header 设置,强制封面使用正常逻辑,不再使用遮罩层
2544        if (exportSettings['export-use-cover']) {
2545            coverHtml = `
2546                <div class="pdf-cover-page" style="height: 980px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; background: transparent; position: relative;">
2547                    <h1 style="font-size: 3.5rem; margin-bottom: 1rem; border: none !important;">${exportSettings['export-title'] || file.name}</h1>
2548                    <h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0; border: none !important;">${exportSettings['export-subtitle'] || ''}</h2>
2549                </div>
2550            `;
2551        }
2552
2553        let tocHtml = '';
2554        if (exportSettings['export-use-toc']) {
2555            const headings = Array.from(clone.querySelectorAll('h1, h2, h3'));
2556            if (headings.length > 0) {
2557                const tocItems = headings.map((h, i) => {
2558                    const id = `toc-anchor-${i}`;
2559                    h.setAttribute('id', id);
2560                    const level = parseInt(h.tagName[1]);
2561                    const indentSide = lang === 'ar' ? 'margin-right' : 'margin-left';
2562                    const paddingSide = lang === 'ar' ? 'padding-left' : 'padding-right';
2563                    return `<a href="#${id}" style="text-decoration: none; color: inherit; display: block; ${indentSide}: ${(level - 1) * 20}px; margin-bottom: 12px; font-size: 14px;">
2564                        <div style="display: flex; justify-content: space-between; border-bottom: 1px dotted #e2e8f0; padding-bottom: 4px;">
2565                            <span style="background: white; ${paddingSide}: 8px;">${h.innerText}</span>
2566                        </div>
2567                    </a>`;
2568                }).join('');
2569
2570                const tocMap = {
2571                    'en': 'Table of Contents',
2572                    'zh': '目录',
2573                    'tw': '目錄',
2574                    'es': 'Índice',
2575                    'de': 'Inhaltsverzeichnis',
2576                    'ja': '目次',
2577                    'ko': '목차',
2578                    'ru': 'Содержание',
2579                    'bg': 'Съдържание',
2580                    'it': 'Indice',
2581                    'ar': 'جدول المحتويات'
2582                };
2583                const tocTitle = tocMap[lang] || tocMap['en'];
2584
2585                tocHtml = `
2586                    <div class="pdf-toc-page" style="padding: 40px 0; min-height: 980px; background: transparent; position: relative; direction: ${lang === 'ar' ? 'rtl' : 'ltr'};">
2587                        <h1 style="text-align: center; margin-bottom: 3rem; border: none !important; font-size: 2rem;">${tocTitle}</h1>
2588                        <div style="max-width: 600px; margin: 0 auto;">${tocItems}</div>
2589                    </div>
2590                `;
2591            }
2592        }
2593
2594        const styleTag = document.createElement('style');
2595        styleTag.textContent = `
2596            #pdf-export-wrapper .pdf-main-container {
2597                width: 210mm !important;
2598                background-color: #ffffff !important;
2599                color: #000000 !important;
2600                position: relative !important;
2601                box-sizing: border-box !important;
2602                font-size: 16px !important;
2603                padding: 40px !important;
2604                -webkit-font-smoothing: antialiased;
2605            }
2606            ${styleCss}
2607
2608            @media print {
2609                html, body {
2610                    height: auto !important;
2611                    overflow: visible !important;
2612                    margin: 0 !important;
2613                    padding: 0 !important;
2614                }
2615
2616                /* 显式初始化页码计数,避免浏览器默认值为0 */
2617                body {
2618                    counter-reset: page 0;
2619                }
2620
2621                @page {
2622                    margin: 0; /* 强制为0,由应用内部控制页边距,解决分页溢出问题 */
2623                    size: A4;
2624                    counter-increment: page;
2625                }
2626
2627                /* 非 JS 分页模式下的容器边距补回 */
2628                #pdf-export-wrapper:not(.js-paginate) .pdf-main-container {
2629                    margin: 0 auto !important;
2630                    padding: ${hasHeader ? '23mm' : '15mm'} 15mm ${hasFooter ? '23mm' : '15mm'} 15mm !important;
2631                    width: 210mm !important;
2632                    box-sizing: border-box !important;
2633                }
2634
2635                /* 自定义页眉页脚样式 - 这些是应用内设置的,不受浏览器打印对话框影响 */
2636                .pdf-header {
2637                    position: fixed;
2638                    top: 6mm;
2639                    left: 0;
2640                    right: 0;
2641                    height: 12mm;
2642                    display: ${hasHeader ? 'flex' : 'none'} !important;
2643                    justify-content: space-between;
2644                    align-items: center;
2645                    font-size: 10px;
2646                    color: #334155;
2647                    padding: 0 15mm;
2648                    border-bottom: 0.5px solid #f1f5f9;
2649                    z-index: 99999 !important;
2650                    background: white !important;
2651                    -webkit-print-color-adjust: exact !important;
2652                    print-color-adjust: exact !important;
2653                    overflow: hidden;
2654                    box-sizing: border-box !important;
2655                }
2656                .pdf-footer {
2657                    position: fixed;
2658                    bottom: 6mm;
2659                    left: 0;
2660                    right: 0;
2661                    height: 12mm;
2662                    display: ${hasFooter ? 'flex' : 'none'} !important;
2663                    justify-content: space-between;
2664                    align-items: center;
2665                    font-size: 10px;
2666                    color: #334155;
2667                    padding: 0 15mm;
2668                    border-top: 0.5px solid #f1f5f9;
2669                    z-index: 99999 !important;
2670                    background: white !important;
2671                    -webkit-print-color-adjust: exact !important;
2672                    print-color-adjust: exact !important;
2673                }
2674
2675                /* JS 分页时,使用每页绝对定位的页眉/页脚 */
2676                #pdf-export-wrapper.js-paginate .pdf-header,
2677                #pdf-export-wrapper.js-paginate .pdf-footer {
2678                    display: none !important;
2679                }
2680                #pdf-export-wrapper.js-paginate {
2681                    --footer-offset: 0;
2682                }
2683                #pdf-export-wrapper.js-paginate .pdf-page-header {
2684                    position: absolute;
2685                    left: 10mm;
2686                    right: 10mm;
2687                    height: 12mm;
2688                    display: ${hasHeader ? 'flex' : 'none'} !important;
2689                    justify-content: space-between;
2690                    align-items: center;
2691                    font-size: 10px;
2692                    color: #334155;
2693                    padding: 0;
2694                    background: white !important;
2695                    -webkit-print-color-adjust: exact !important;
2696                    print-color-adjust: exact !important;
2697                    box-sizing: border-box !important;
2698                    z-index: 99998 !important;
2699                    top: 6mm;
2700                    border-bottom: 0.5px solid #f1f5f9;
2701                }
2702                #pdf-export-wrapper.js-paginate .pdf-page-footer {
2703                    position: absolute;
2704                    left: 10mm;
2705                    right: 10mm;
2706                    height: 12mm;
2707                    display: ${hasFooter ? 'flex' : 'none'} !important;
2708                    justify-content: space-between;
2709                    align-items: center;
2710                    font-size: 10px;
2711                    color: #334155;
2712                    padding: 0;
2713                    background: white !important;
2714                    -webkit-print-color-adjust: exact !important;
2715                    print-color-adjust: exact !important;
2716                    box-sizing: border-box !important;
2717                    z-index: 99998 !important;
2718                    bottom: 6mm;
2719                    border-top: 0.5px solid #f1f5f9;
2720                    transform: translateY(var(--footer-offset));
2721                }
2722                #pdf-export-wrapper.js-paginate .pdf-page-inner {
2723                    z-index: 1 !important;
2724                    top: ${hasHeader ? '23mm' : '15mm'} !important;
2725                    bottom: ${hasFooter ? '23mm' : '15mm'} !important;
2726                }
2727
2728                .pdf-header-logo {
2729                    max-height: 11mm !important;
2730                    max-width: 80mm !important;
2731                    width: auto !important;
2732                    height: auto !important;
2733                    object-fit: contain !important;
2734                    display: block !important;
2735                    flex-shrink: 0 !important;
2736                    margin: 0;
2737                    padding: 0;
2738                }
2739
2740                .pdf-watermark {
2741                    position: fixed;
2742                    top: 50%;
2743                    left: 50%;
2744                    transform: translate(-50%, -50%) rotate(-30deg);
2745                    font-size: 60pt;
2746                    color: rgba(150, 150, 150, 0.1);
2747                    z-index: 10 !important;
2748                    pointer-events: none;
2749                    white-space: nowrap;
2750                    font-weight: bold;
2751                    text-transform: uppercase;
2752                    display: flex !important;
2753                    align-items: center;
2754                    justify-content: center;
2755                    width: 100%;
2756                    height: 100%;
2757                    -webkit-print-color-adjust: exact !important;
2758                    print-color-adjust: exact !important;
2759                }
2760
2761                /* 页码显示:使用CSS内置的page counter,它会自动跟踪当前打印页码 */
2762                .page-number::after {
2763                    content: counter(page);
2764                }
2765
2766                /* 移除冗余的 pdf-main-container 强制边距,由上面的特定规则处理 */
2767                .pdf-cover-page {
2768                    margin: 0 !important;
2769                    padding: 0 !important;
2770                    page-break-after: always;
2771                }
2772                .pdf-toc-page {
2773                    margin: 0 !important;
2774                    padding: 0 !important;
2775                    page-break-before: always;
2776                }
2777            }
2778
2779            #pdf-export-wrapper .pdf-footer-branding {
2780                margin-top: 50px !important;
2781                padding-top: 20px !important;
2782                border-top: 1px solid #f1f5f9 !important;
2783                text-align: center !important;
2784                font-size: 12px !important;
2785                color: #94a3b8 !important;
2786                page-break-inside: avoid;
2787            }
2788
2789            #pdf-export-wrapper.js-paginate .pdf-main-container {
2790                padding: 0 !important;
2791                margin-top: 0 !important;
2792                margin-bottom: 0 !important;
2793            }
2794            #pdf-export-wrapper.js-paginate .pdf-pages {
2795                position: relative;
2796                width: 210mm;
2797                margin: 0 auto;
2798            }
2799            #pdf-export-wrapper.js-paginate .pdf-page-sheet {
2800                position: relative;
2801                width: 210mm;
2802                height: 296.8mm; /* Reduced slightly to prevent extra blank pages in some browsers */
2803                page-break-after: always;
2804                break-after: page;
2805                background: white;
2806                box-sizing: border-box;
2807                overflow: hidden;
2808            }
2809            #pdf-export-wrapper.js-paginate .pdf-page-sheet:last-child {
2810                page-break-after: avoid !important;
2811                break-after: avoid !important;
2812            }
2813            #pdf-export-wrapper.js-paginate .pdf-page-inner {
2814                position: absolute;
2815                top: ${hasHeader ? '23mm' : '15mm'};
2816                left: 10mm;
2817                right: 10mm;
2818                bottom: ${hasFooter ? '23mm' : '15mm'};
2819                padding: 0;
2820                box-sizing: border-box;
2821                z-index: 1;
2822            }
2823            #pdf-export-wrapper.js-paginate .pdf-page-content {
2824                position: relative;
2825                width: 100%;
2826                height: 100%;
2827                overflow: hidden;
2828                box-sizing: border-box;
2829            }
2830            #pdf-export-wrapper.js-paginate .pdf-page-watermark {
2831                position: absolute;
2832                inset: 0;
2833                transform: rotate(-30deg);
2834                font-size: 60pt;
2835                color: rgba(150, 150, 150, 0.1);
2836                z-index: 5 !important;
2837                pointer-events: none;
2838                white-space: nowrap;
2839                font-weight: bold;
2840                text-transform: uppercase;
2841                display: flex !important;
2842                align-items: center;
2843                justify-content: center;
2844                -webkit-print-color-adjust: exact !important;
2845                print-color-adjust: exact !important;
2846            }
2847            #pdf-export-wrapper.js-paginate .pdf-cover-page,
2848            #pdf-export-wrapper.js-paginate .pdf-toc-page {
2849                page-break-before: auto !important;
2850                page-break-after: auto !important;
2851                break-before: auto !important;
2852                break-after: auto !important;
2853            }
2854
2855            /* 内容包装,确保不被截断 */
2856            #pdf-export-wrapper .pdf-content {
2857                position: relative;
2858                z-index: 2;
2859                overflow: visible !important; /* 关键:确保打印时不截断 */
2860            }
2861
2862            /* 修正 markdown-body 的潜在截断问题 */
2863            .markdown-body {
2864                overflow: visible !important;
2865                background: transparent !important;
2866            }
2867
2868            /* 分页优化 */
2869            #pdf-export-wrapper .markdown-body p { orphans: 3; widows: 3; }
2870            #pdf-export-wrapper .markdown-body h1, #pdf-export-wrapper .markdown-body h2, #pdf-export-wrapper .markdown-body h3 { break-after: avoid-page; page-break-after: avoid; }
2871
2872            #pdf-export-wrapper h1, #pdf-export-wrapper h2, #pdf-export-wrapper h3, #pdf-export-wrapper h4, #pdf-export-wrapper h5, #pdf-export-wrapper h6 { page-break-after: avoid !important; break-after: avoid !important; }
2873            #pdf-export-wrapper pre, #pdf-export-wrapper blockquote, #pdf-export-wrapper img, #pdf-export-wrapper table, #pdf-export-wrapper .mermaid, #pdf-export-wrapper .admonition, #pdf-export-wrapper .katex-display {
2874                page-break-inside: avoid !important;
2875                break-inside: avoid !important;
2876                margin-bottom: 1.5em; /* 去掉 !important 以允许 JS 动态调整缩放后的间距 */
2877            }
2878            /* 强制 Mermaid 和 KaTeX 正常渲染 */
2879            #pdf-export-wrapper .katex-mathml, #pdf-export-wrapper .katex-error { display: none !important; }
2880            #pdf-export-wrapper .mermaid {
2881                page-break-inside: avoid !important;
2882                break-inside: avoid !important;
2883                margin: 1.5em 0; /* 同上,去掉 !important */
2884                width: 100% !important;
2885                text-align: center !important;
2886                transition: none !important; /* 避免缩放动画 */
2887            }
2888            #pdf-export-wrapper .mermaid svg {
2889                max-width: 100% !important;
2890                max-height: 23cm !important; /* 限制高度以适应单页 A4,防止溢出或因过大导致的分页异常 */
2891                height: auto !important;
2892                width: auto !important; /* 允许根据高度等比缩小 */
2893                display: block !important;
2894                margin: 0 auto !important;
2895                object-fit: contain !important;
2896            }
2897            #pdf-export-wrapper pre, #pdf-export-wrapper code {
2898                white-space: pre-wrap !important;
2899                word-break: break-all !important;
2900            }
2901            #pdf-export-wrapper img {
2902                max-width: 100% !important;
2903                height: auto !important;
2904                object-fit: contain !important;
2905            }
2906
2907            /* RTL Support for PDF */
2908            #pdf-export-wrapper.rtl-mode {
2909                text-align: right !important;
2910                direction: rtl !important;
2911            }
2912            #pdf-export-wrapper.rtl-mode .pdf-pages,
2913            #pdf-export-wrapper.rtl-mode .pdf-page-sheet {
2914                direction: rtl !important;
2915            }
2916            #pdf-export-wrapper.rtl-mode .mermaid,
2917            #pdf-export-wrapper.rtl-mode pre,
2918            #pdf-export-wrapper.rtl-mode code,
2919            #pdf-export-wrapper.rtl-mode .katex-display {
2920                direction: ltr !important;
2921                text-align: left !important;
2922            }
2923            #pdf-export-wrapper.rtl-mode .mermaid svg {
2924                margin: 0 auto !important;
2925            }
2926            #pdf-export-wrapper.rtl-mode ul,
2927            #pdf-export-wrapper.rtl-mode ol {
2928                padding-right: 2em !important;
2929                padding-left: 0 !important;
2930                margin-left: 0 !important;
2931            }
2932            #pdf-export-wrapper.rtl-mode .task-list-item {
2933                list-style-type: none !important;
2934                padding-right: 0 !important;
2935            }
2936            #pdf-export-wrapper.rtl-mode .task-list-item-checkbox {
2937                margin: 0 -1.6em 0.25em 0.5em !important;
2938            }
2939            #pdf-export-wrapper.rtl-mode .markdown-body blockquote {
2940                border-left: none !important;
2941                border-right: 0.25em solid #dfe2e5 !
2941important;
2942                padding: 0 1em 0 0 !important;
2943            }
2944            #pdf-export-wrapper.rtl-mode .pdf-header,
2945            #pdf-export-wrapper.rtl-mode .pdf-footer,
2946            #pdf-export-wrapper.rtl-mode .pdf-page-header,
2947            #pdf-export-wrapper.rtl-mode .pdf-page-footer {
2948                direction: rtl !important;
2949            }
2950            /* 修正 RTL 模式下可能出现的水平溢出 */
2951            #pdf-export-wrapper.rtl-mode .pdf-page-inner {
2952                left: auto !important;
2953                right: 10mm !important;
2954                width: 190mm !important;
2955            }
2956            #pdf-export-wrapper.rtl-mode .pdf-content {
2957                width: 100% !important;
2958            }
2959
2960            * { box-sizing: border-box !important; }
2961        `;
2962
2963        container.appendChild(styleTag);
2964
2965        // 构造页眉页脚 DOM (分离出 main-container 以避免样式干扰)
2966        const headerHtml = `
2967            <div class="pdf-header">
2968                <div style="flex: 1; height: 100%; display: flex; align-items: center; gap: 8px; justify-content: flex-start; overflow: hidden;">
2969                    ${logoLeft ? `<img src="${logoLeft}" class="pdf-header-logo">` : `<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${hLeft}</span>`}
2970                </div>
2971                <div style="flex: 1; height: 100%; display: flex; align-items: center; justify-content: center; overflow: hidden;">
2972                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${hCenter}</span>
2973                </div>
2974                <div style="flex: 1; height: 100%; display: flex; align-items: center; gap: 8px; justify-content: flex-end; overflow: hidden;">
2975                    ${logoRight ? `<img src="${logoRight}" class="pdf-header-logo">` : `<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${hRight}</span>`}
2976                </div>
2977            </div>`;
2978
2979        const footerHtml = `
2980            <div class="pdf-footer">
2981                <div style="flex: 1; display: flex; align-items: center; justify-content: flex-start; overflow: hidden;">
2982                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${fLeft}</span>
2983                </div>
2984                <div style="flex: 1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
2985                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${fCenter}</span>
2986                </div>
2987                <div style="flex: 1; display: flex; align-items: center; justify-content: flex-end; overflow: hidden;">
2988                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${fRight}</span>
2989                </div>
2990            </div>`;
2991
2992        // 插入水印 (Fixed)
2993        if (watermarkText) {
2994            container.insertAdjacentHTML('beforeend', `<div class="pdf-watermark">${watermarkText}</div>`);
2995        }
2996
2997        // 插入页眉 (Fixed)
2998        if (hasHeader) {
2999            container.insertAdjacentHTML('beforeend', headerHtml);
3000        }
3001
3002        // 插入页脚 (Fixed) - 必须放在 content wrapper 之前,以确保在多页文档中的正确渲染层级和位置
3003        if (hasFooter) {
3004            container.insertAdjacentHTML('beforeend', footerHtml);
3005        }
3006
3007        const wrapper = document.createElement('div');
3008        wrapper.className = 'pdf-main-container';
3009        wrapper.innerHTML = `
3010            ${coverHtml}
3011            ${tocHtml}
3012            <div class="pdf-content markdown-body">${clone.innerHTML}</div>
3013            ${canRemoveBranding ? '' : `<div class="pdf-footer-branding">
3014                ${dict.pdfBranding || 'The Professional AI Artifacts to PDF Engine'} - <a href="https://md2pdf.cc" target="_blank" style="color: #7c3aed; text-decoration: none; font-weight: 600; margin-left: 4px;">md2pdf.cc</a>
3015            </div>`}
3016        `;
3017        container.appendChild(wrapper);
3018
3019        // 插入页脚 (Moved above)
3020        // container.insertAdjacentHTML('beforeend', footerHtml);
3021
3022        document.body.appendChild(container);
3023
3024        // 给予浏览器一小段时间进行初步布局计算,防止 Mermaid 渲染时坐标计算出错 (NaN)
3025        await new Promise(resolve => setTimeout(resolve, 100));
3026
3027        // 为导出容器内的 Mermaid 图表进行渲染
3028        if (window.mermaid) {
3029            try {
3030                const mermaidNodes = container.querySelectorAll('.mermaid');
3031                if (mermaidNodes.length > 0) {
3032                    // 对于导出,我们需要确保它立即渲染
3033                    await mermaid.run({
3034                        nodes: mermaidNodes,
3035                    });
3036
3037                    // 智能分页算法:防止 Mermaid 图表造成过大的页面留白
3038                    // 逻辑:如果图表会被推到下一页,且当前页剩余空间 > 1/3,则缩小图表使其留在当前页
3039                    const PAGE_HEIGHT_PX = 980; // A4 约 260mm 的像素值
3040                    mermaidNodes.forEach(node => {
3041                        const rect = node.getBoundingClientRect();
3042                        const wrapperRect = container.getBoundingClientRect();
3043                        const topRelativeToWrapper = rect.top - wrapperRect.top;
3044
3045                        const pageIndex = Math.floor(topRelativeToWrapper / PAGE_HEIGHT_PX);
3046                        const pageBottom = (pageIndex + 1) * PAGE_HEIGHT_PX;
3047                        const remainingSpace = pageBottom - topRelativeToWrapper;
3048                        const elementHeight = rect.height;
3049
3050                        // 如果元素高度大于剩余空间(即会被推到下一页)
3051                        // 且当前剩余空间超过页面高度的 1/3
3052                        if (elementHeight > remainingSpace && remainingSpace > (PAGE_HEIGHT_PX / 3)) {
3053                            // 优化:检查后续元素,如果缩放比例在可接受范围内 (>=0.45),尽量把后续内容也拉上来
3054                            let extraHeightNeeded = 0;
3055                            const nextEl = node.nextElementSibling;
3056                            if (nextEl) {
3057                                // 即使不是标题,如果后续元素较小,也尝试一起拉上来
3058                                extraHeightNeeded = Math.min(nextEl.offsetHeight, 100) + 20;
3059                            }
3060
3061                            // 重新计算缩放比例,尝试留出空间给后续元素
3062                            let scale = (remainingSpace - 40 - extraHeightNeeded) / elementHeight;
3063
3064                            // 如果为了容纳后续内容导致缩放太严重,则优先保证图表本身大小,仅压缩图表到本页末尾
3065                            if (scale < 0.45) {
3066                                scale = (remainingSpace - 30) / elementHeight;
3067                            }
3068
3069                            if (scale > 0.4) { // 缩放比例底线
3070                                node.style.transform = `scale(${scale})`;
3071                                node.style.transformOrigin = 'top center';
3072                                // 关键:修改实际布局高度,让分页引擎感知到缩放后的尺寸
3073                                // 使用稍微大一点的偏移补偿,确保内容紧凑
3074                                node.style.height = `${elementHeight * scale}px`;
3075
3076                                // 强制允许在该元素位置进行分页计算
3077                                node.style.setProperty('break-inside', 'auto', 'important');
3078                                node.style.setProperty('page-break-inside', 'auto', 'important');
3079
3080                                node.style.marginBottom = '5px';
3081                                node.style.marginTop = '5px';
3082                                node.style.overflow = 'visible';
3083                            }
3084                        } else if (elementHeight <= remainingSpace) {
3085                            // 如果本来就能放下,确保恢复默认的 avoid 逻辑
3086                            node.style.transform = '';
3087                            node.style.height = 'auto';
3088                            node.style.setProperty('break-inside', 'avoid', 'important');
3089                            node.style.setProperty('page-break-inside', 'avoid', 'important');
3090                            node.style.marginTop = '';
3091                            node.style.marginBottom = '';
3092                        }
3093                    });
3094                }
3095            } catch (err) {
3096                console.error("Mermaid export render error:", err);
3097            }
3098        }
3099
3100        try {
3101            // 给移动端留出更多的渲染时间
3102            const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
3103            await new Promise(resolve => setTimeout(resolve, isMobile ? 2500 : 1500));
3104
3105            const mmToPx = (mm) => {
3106                const probe = document.createElement('div');
3107                probe.style.height = `${mm}mm`;
3108                probe.style.width = '1mm';
3109                probe.style.position = 'absolute';
3110                probe.style.visibility = 'hidden';
3111                document.body.appendChild(probe);
3112                const px = probe.getBoundingClientRect().height;
3113                probe.remove();
3114                return px;
3115            };
3116
3117            const getPageMetrics = () => {
3118                const pageProbe = document.createElement('div');
3119                pageProbe.style.width = '210mm';
3120                pageProbe.style.height = '297mm';
3121                pageProbe.style.position = 'absolute';
3122                pageProbe.style.visibility = 'hidden';
3123                document.body.appendChild(pageProbe);
3124
3125                const rect = pageProbe.getBoundingClientRect();
3126                pageProbe.remove();
3127
3128                const pageHeight = rect.height || mmToPx(297);
3129                const headerFooterHeight = pageHeight * (12 / 297);
3130                const contentHeight = pageHeight * (270 / 297);
3131
3132                return {
3133                    pageHeight,
3134                    headerFooterHeight,
3135                    contentHeight
3136                };
3137            };
3138
3139            // 计算总页数
3140            // A4 页面高度约为 297mm,减去上下边距 (10mm + 17mm),内容区域高度约为 270mm
3141            // 转换为像素:使用 A4 页面探针获取真实渲染比例
3142            const pageMetrics = getPageMetrics();
3143            const PAGE_MM = {
3144                width: 210,
3145                height: 296.8, /* Changed from 297 for printing safety */
3146                marginTop: 10,
3147                marginBottom: 0,
3148                marginLeft: 10,
3149                marginRight: 10,
3150                headerFooter: 12,
3151                gap: 3
3152            };
3153            const CONTENT_GAP_PX = mmToPx(PAGE_MM.gap);
3154            const MARGIN_TOP_PX = mmToPx(PAGE_MM.marginTop);
3155            const MARGIN_RIGHT_PX = mmToPx(PAGE_MM.marginRight);
3156            const MARGIN_BOTTOM_PX = mmToPx(PAGE_MM.marginBottom);
3157            const MARGIN_LEFT_PX = mmToPx(PAGE_MM.marginLeft);
3158            const PAGE_HEIGHT_PX = pageMetrics.pageHeight;
3159            const HEADER_FOOTER_HEIGHT_PX = pageMetrics.headerFooterHeight;
3160
3161            // 动态计算内容区域高度,如果没有页眉或页脚,则留出更少空间
3162            const pt_px = hasHeader ? mmToPx(23) : mmToPx(15);
3163            const pb_px = hasFooter ? mmToPx(23) : mmToPx(15);
3164
3165            const PAGE_CONTENT_HEIGHT = Math.max(
3166                1,
3167                PAGE_HEIGHT_PX - pt_px - pb_px
3168            );
3169            const containerHeight = container.offsetHeight;
3170            let totalPages = Math.max(1, Math.ceil(containerHeight / PAGE_CONTENT_HEIGHT));
3171
3172            // 替换所有的总页数占位符
3173            const pageCountElements = container.querySelectorAll('.page-count');
3174            pageCountElements.forEach(el => {
3175                el.textContent = totalPages.toString();
3176            });
3177
3178            const pageMacroSource = [
3179                exportSettings['export-header-left'],
3180                exportSettings['export-header-center'],
3181                exportSettings['export-header-right'],
3182                exportSettings['export-footer-left'],
3183                exportSettings['export-footer-center'],
3184                exportSettings['export-footer-right']
3185            ].join(' ');
3186
3187            const hasPageMacros = /\{pagenum\}|\{totalpages\}/.test(pageMacroSource);
3188
3189            // 始终开启 JS 分页模式以确保布局一致性,防止内容与页眉页脚重叠,并启用防截断逻辑
3190            const useJsPaginate = true;
3191
3192            const replacePageMacros = (str, pageNum, pageTotal) => {
3193                if (!str) return '';
3194                return str
3195                    .replace(/{date}/g, dateStr)
3196                    .replace(/{title}/g, file.name || 'document')
3197                    .replace(/{pagenum}/g, String(pageNum))
3198                    .replace(/{totalpages}/g, String(pageTotal));
3199            };
3200
3201            if (useJsPaginate) {
3202                container.classList.add('js-paginate');
3203
3204                const wrapperEl = container.querySelector('.pdf-main-container');
3205                const contentEl = wrapperEl ? wrapperEl.querySelector('.pdf-content') : null;
3206                const brandingEl = wrapperEl ? wrapperEl.querySelector('.pdf-footer-branding') : null;
3207                const coverEl = wrapperEl ? wrapperEl.querySelector('.pdf-cover-page') : null;
3208                const tocEl = wrapperEl ? wrapperEl.querySelector('.pdf-toc-page') : null;
3209                const pagesEl = document.createElement('div');
3210                pagesEl.className = 'pdf-pages';
3211
3212                if (contentEl && wrapperEl) {
3213                    const createNodeFromHtml = (html) => {
3214                        if (!html) return null;
3215                        const wrapper = document.createElement('div');
3216                        wrapper.innerHTML = html;
3217                        return wrapper.firstElementChild || null;
3218                    };
3219
3220                    // 拆分"整体高度已超过单页可用高度"的表格:
3221                    // 全局 CSS 对 table 强制 page-break-inside: avoid !important(防止表格内部随意断行),
3222                    // 但当表格本身(如 7.1 字段规范表)渲染高度就超过整页内容区域时,浏览器原生打印分页
3223                    // 无法在“不可拆分”和“放不下一整页”之间找到解,会导致该表格被直接丢弃、输出空白页。
3224                    // 这里在测量阶段按行拆分为多个更小的表格(每个都复制表头),交由下方逐节点分页逻辑
3225                    // 正常地把每一小段分配到合适的页面。
3226                    const splitOversizedTable = (table, hostEl, maxHeight) => {
3227                        if (!table || table.tagName !== 'TABLE') return [table];
3228                        if (table.offsetHeight <= maxHeight - 4) return [table];
3229
3230                        const thead = table.querySelector(':scope > thead');
3231                        const tbody = table.querySelector(':scope > tbody');
3232                        if (!tbody || tbody.children.length <= 1) return [table];
3233
3234                        const scratch = document.createElement('div');
3235                        scratch.style.position = 'absolute';
3236                        scratch.style.visibility = 'hidden';
3237                        scratch.style.pointerEvents = 'none';
3238                        scratch.style.width = `${table.offsetWidth || table.getBoundingClientRect().width}px`;
3239                        hostEl.appendChild(scratch);
3240
3241                        const makeShell = () => {
3242                            const t = table.cloneNode(false);
3243                            if (thead) t.appendChild(thead.cloneNode(true));
3244                            const tb = document.createElement('tbody');
3245                            t.appendChild(tb);
3246                            return { t, tb };
3247                        };
3248
3249                        const rows = Array.from(tbody.children);
3250                        const chunks = [];
3251                        let shell = makeShell();
3252                        scratch.innerHTML = '';
3253                        scratch.appendChild(shell.t);
3254
3255                        rows.forEach(row => {
3256                            shell.tb.appendChild(row.cloneNode(true));
3257                            if (scratch.offsetHeight > maxHeight - 4 && shell.tb.children.length > 1) {
3258                                const overflowRow = shell.tb.lastElementChild;
3259                                shell.tb.removeChild(overflowRow);
3260                                chunks.push(shell.t);
3261                                shell = makeShell();
3262                                scratch.innerHTML = '';
3263                                scratch.appendChild(shell.t);
3264                                shell.tb.appendChild(overflowRow);
3265                            }
3266                        });
3267                        chunks.push(shell.t);
3268                        scratch.remove();
3269
3270                        // 若拆分未生效(例如单行本身就超过整页高度),回退为原表格,交由后续分页逻辑兜底处理
3271                        return chunks.length > 1 ? chunks : [table];
3272                    };
3273
3274                    const flowNodes = [];
3275                    const coverNode = coverEl || createNodeFromHtml(coverHtml);
3276                    const tocNode = tocEl || createNodeFromHtml(tocHtml);
3277                    if (coverNode) flowNodes.push(coverNode);
3278                    if (tocNode) flowNodes.push(tocNode);
3279                    flowNodes.push(...Array.from(contentEl.childNodes).filter(node => {
3280                        if (node.nodeType === Node.TEXT_NODE) {
3281                            return node.textContent && node.textContent.trim().length > 0;
3282                        }
3283                        return true;
3284                    }).flatMap(node => {
3285                        if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'TABLE') {
3286                            return splitOversizedTable(node, contentEl, PAGE_CONTENT_HEIGHT);
3287                        }
3288                        return [node];
3289                    }));
3290                    if (brandingEl) flowNodes.push(brandingEl);
3291
3292                    contentEl.remove();
3293                    if (brandingEl && brandingEl.parentNode === wrapperEl) brandingEl.remove();
3294                    if (coverEl && coverEl.parentNode === wrapperEl) coverEl.remove();
3295                    if (tocEl && tocEl.parentNode === wrapperEl) tocEl.remove();
3296
3297                    wrapperEl.appendChild(pagesEl);
3298
3299                    const isFullPageNode = (node) => {
3300                        if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
3301                        return node.classList.contains('pdf-cover-page') || node.classList.contains('pdf-toc-page');
3302                    };
3303
3304                    const createPage = (options = {}) => {
3305                        const allowOverflow = options.allowOverflow || false;
3306                        const page = document.createElement('div');
3307                        page.className = 'pdf-page-sheet';
3308                        page.style.width = `${PAGE_MM.width}mm`;
3309                        page.style.height = `${PAGE_MM.height}mm`;
3310
3311                        const inner = document.createElement('div');
3312                        inner.className = 'pdf-page-inner';
3313
3314                        const content = document.createElement('div');
3315                        content.className = 'pdf-page-content markdown-body';
3316                        content.style.position = 'absolute';
3317                        content.style.top = '0';
3318                        content.style.left = '0';
3319                        content.style.right = '0';
3320                        content.style.bottom = '0';
3321                        content.style.width = '100%';
3322                        content.style.height = '100%';
3323                        content.style.padding = '0';
3324                        content.style.margin = '0';
3325                        content.style.boxSizing = 'border-box';
3326                        content.style.overflow = allowOverflow ? 'visible' : 'hidden';
3327                        content.style.overflowX = 'hidden';
3328                        content.style.direction = 'inherit';
3329                        content.style.textAlign = 'inherit';
3330
3331                        inner.appendChild(content);
3332                        page.appendChild(inner);
3333                        pagesEl.appendChild(page);
3334                        return { page, inner, content };
3335                    };
3336
3337                    let current = null;
3338                    flowNodes.forEach(node => {
3339                        if (isFullPageNode(node)) {
3340                            const fullPage = createPage({ allowOverflow: true });
3341                            fullPage.content.appendChild(node);
3342                            current = null;
3343                            return;
3344                        }
3345
3346                        if (!current) current = createPage();
3347
3348                        current.content.appendChild(node);
3349                        // 当 clientHeight 为 0 时(移动端 iOS Safari 可能因渲染优化不计算高度),
3350                        // 使用预先测量好的 PAGE_CONTENT_HEIGHT 作为兜底,避免分页逻辑失效导致缺页。
3351                        const effectiveClientHeight = current.content.clientHeight > 10
3352                            ? current.content.clientHeight
3353                            : PAGE_CONTENT_HEIGHT;
3354                        if (current.content.scrollHeight > effectiveClientHeight + 1) {
3355                            current.content.removeChild(node);
3356                            if (current.content.childNodes.length === 0) {
3357                                current.content.appendChild(node);
3358                            } else {
3359                                current = createPage();
3360                                current.content.appendChild(node);
3361                            }
3362                        }
3363                    });
3364
3365                    totalPages = pagesEl.children.length || totalPages;
3366
3367                    const buildHeaderHtml = (pageNum) => {
3368                        const left = replacePageMacros(exportSettings['export-header-left'], pageNum, totalPages);
3369                        const center = replacePageMacros(exportSettings['export-header-center'], pageNum, totalPages);
3370                        const right = replacePageMacros(exportSettings['export-header-right'], pageNum, totalPages);
3371                        return `
3372                            <div class="pdf-page-header">
3373                                <div style="flex: 1; height: 100%; display: flex; align-items: center; gap: 8px; justify-content: flex-start; overflow: hidden;">
3374                                    ${logoLeft ? `<img src="${logoLeft}" class="pdf-header-logo">` : `<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${left}</span>`}
3375                                </div>
3376                                <div style="flex: 1; height: 100%; display: flex; align-items: center; justify-content: center; overflow: hidden;">
3377                                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${center}</span>
3378                                </div>
3379                                <div style="flex: 1; height: 100%; display: flex; align-items: center; gap: 8px; justify-content: flex-end; overflow: hidden;">
3380                                    ${logoRight ? `<img src="${logoRight}" class="pdf-header-logo">` : `<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${right}</span>`}
3381                                </div>
3382                            </div>`;
3383                    };
3384
3385                    const buildFooterHtml = (pageNum) => {
3386                        const left = replacePageMacros(exportSettings['export-footer-left'], pageNum, totalPages);
3387                        const center = replacePageMacros(exportSettings['export-footer-center'], pageNum, totalPages);
3388                        const right = replacePageMacros(exportSettings['export-footer-right'], pageNum, totalPages);
3389                        return `
3390                            <div class="pdf-page-footer">
3391                                <div style="flex: 1; display: flex; align-items: center; justify-content: flex-start; overflow: hidden;">
3392                                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${left}</span>
3393                                </div>
3394                                <div style="flex: 1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
3395                                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${center}</span>
3396                                </div>
3397                                <div style="flex: 1; display: flex; align-items: center; justify-content: flex-end; overflow: hidden;">
3398                                    <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${right}</span>
3399                                </div>
3400                            </div>`;
3401                    };
3402
3403                    Array.from(pagesEl.children).forEach((page, index) => {
3404                        const pageNum = index + 1;
3405                        const pageInner = page.querySelector('.pdf-page-inner');
3406                        if (watermarkText) {
3407                            const watermark = document.createElement('div');
3408                            watermark.className = 'pdf-page-watermark';
3409                            watermark.textContent = watermarkText;
3410                            page.appendChild(watermark);
3411                        }
3412
3413                        if (hasHeader) {
3414                            const header = document.createElement('div');
3415                            header.innerHTML = buildHeaderHtml(pageNum);
3416                            const headerNode = header.firstElementChild;
3417                            page.appendChild(headerNode);
3418                        }
3419
3420                        if (hasFooter) {
3421                            const footer = document.createElement('div');
3422                            footer.innerHTML = buildFooterHtml(pageNum);
3423                            const footerNode = footer.firstElementChild;
3424                            page.appendChild(footerNode);
3425                        }
3426                    });
3427                }
3428            }
3429
3430            pageCountElements.forEach(el => {
3431                el.textContent = totalPages.toString();
3432            });
3433
3434            const pageMarginCss = '0'; // Always 0, controlled by internal padding
3435
3436            const printStyle = document.createElement('style');
3437            printStyle.id = 'pdf-print-style';
3438
3439            // 动态边距与显示逻辑
3440            const pt_css = hasHeader ? "23mm" : "15mm";
3441            const pb_css = hasFooter ? "23mm" : "15mm";
3442            const displayHeader = hasHeader ? "flex" : "none";
3443            const displayFooter = hasFooter ? "flex" : "none";
3444
3445            printStyle.textContent = `
3446            @media print {
3447                html, body {
3448                    height: auto !important;
3449                    overflow: visible !important;
3450                    margin: 0 !important; /* Reset browser default */
3451                    padding: 0 !important;
3452                }
3453                @page {
3454                    margin: ${pageMarginCss};
3455                    size: A4;
3456                    counter-increment: page;
3457                }
3458
3459                /* Explicitly reset page counter to avoid 0 on first page */
3460                body {
3461                    counter-reset: page 0;
3462                }
3463
3464                /* Hide everything else */
3465                body > *:not(#pdf-export-wrapper) {
3466                    display: none !important;
3467                }
3468
3469                /* Wrapper configuration */
3470                #pdf-export-wrapper {
3471                    position: absolute !important;
3472                    top: 0 !important;
3473                    left: 0 !important;
3474                    right: 0 !important;
3475                    width: 100% !important;
3476                    margin: 0 !important;
3477                    padding: 0 !important;
3478                    display: block !important;
3479                    visibility: visible !important;
3480                    clip-path: none !important;
3481                    overflow: visible !important;
3482                    transform: none !important;
3483                    z-index: 9999 !important;
3484                }
3485
3486                #pdf-export-wrapper.js-paginate {
3487                    width: 210mm !important;
3488                    margin: 0 auto !important;
3489                    --footer-offset: 0;
3490                }
3491
3492                /* Main Content Container */
3493                #pdf-export-wrapper .pdf-main-container {
3494                    width: 100% !important;
3495                    box-shadow: none !important;
3496                    position: relative !important;
3497                    transform: none !important;
3498                    z-index: 10;
3499                    margin-top: 0 !important;
3500                    margin-bottom: 0 !important;
3501                    padding: ${pt_css} 15mm ${pb_css} 15mm !important; /* Visual spacing for non-JS paginate case */
3502                    box-sizing: border-box !important;
3503                }
3504
3505                /* Header & Footer: FIXED POSITION IS KEY */
3506                #pdf-export-wrapper .pdf-header {
3507                    display: ${displayHeader} !important;
3508                    opacity: 1 !important;
3509                    visibility: visible !important;
3510                    position: fixed !important; /* Must be fixed */
3511                    z-index: 200 !important; /* Topmost */
3512                    background-color: white !important;
3513                    color: #334155 !important;
3514                    left: 0 !important;
3515                    right: 0 !important;
3516                    height: 12mm !important;
3517                    padding: 0 15mm !important;
3518                    box-sizing: border-box !important;
3519                    top: 6mm !important;
3520                    border-bottom: 0.5px solid #e2e8f0;
3521                }
3522
3523                #pdf-export-wrapper .pdf-footer {
3524                    display: ${displayFooter} !important;
3525                    opacity: 1 !important;
3526                    visibility: visible !important;
3527                    position: fixed !important; /* Must be fixed */
3528                    z-index: 200 !important; /* Topmost */
3529                    background-color: white !important;
3530                    color: #334155 !important;
3531                    left: 0 !important;
3532                    right: 0 !important;
3533                    height: 12mm !important;
3534                    padding: 0 15mm !important;
3535                    box-sizing: border-box !important;
3536                    bottom: 6mm !important;
3537                    border-top: 0.5px solid #e2e8f0;
3538                }
3539
3540                /* JS 分页时隐藏固定页眉页脚,显示每页定位的页眉页脚 */
3541                #pdf-export-wrapper.js-paginate .pdf-header,
3542                #pdf-export-wrapper.js-paginate .pdf-footer {
3543                    display: none !important;
3544                }
3545                #pdf-export-wrapper.js-paginate .pdf-page-header {
3546                    display: ${displayHeader} !important;
3547                    position: absolute !important;
3548                    left: 10mm !important;
3549                    right: 10mm !important;
3550                    height: 12mm !important;
3551                    padding: 0 !important;
3552                    box-sizing: border-box !important;
3553                    background-color: white !important;
3554                    z-index: 200 !important;
3555                    top: 6mm !important;
3556                    transform: none !important;
3557                    border-bottom: 0.5px solid #e2e8f0;
3558                }
3559                #pdf-export-wrapper.js-paginate .pdf-page-footer {
3560                    display: ${displayFooter} !important;
3561                    position: absolute !important;
3562                    left: 10mm !important;
3563                    right: 10mm !important;
3564                    height: 12mm !important;
3565                    padding: 0 !important;
3566                    box-sizing: border-box !important;
3567                    background-color: white !important;
3568                    z-index: 200 !important;
3569                    bottom: 6mm !important;
3570                    border-top: 0.5px solid #e2e8f0;
3571                    transform: translateY(var(--footer-offset));
3572                }
3573                #pdf-export-wrapper.js-paginate .pdf-page-inner {
3574                    z-index: 1 !important;
3575                    top: ${pt_css} !important;
3576                    bottom: ${pb_css} !important;
3577                }
3578
3579                /* Override margins for JS pagination to prevent blank pages */
3580                #pdf-export-wrapper.js-paginate .pdf-main-container {
3581                    margin-top: 0 !important;
3582                    margin-bottom: 0 !important;
3583                    padding: 0 !important;
3584                }
3585
3586                /* Page Number Counter */
3587                .page-number::after {
3588                    content: counter(page);
3589                }
3590            `;
3591            document.head.appendChild(printStyle);
3592
3593            const originalTitle = document.title;
3594            document.title = file.name || 'document';
3595
3596            // 显示打印提示
3597            const hasCustomHeader = exportSettings['export-header-left'] || exportSettings['export-header-center'] || exportSettings['export-header-right'];
3598            const hasCustomFooter = exportSettings['export-footer-left'] || exportSettings['export-footer-center'] || exportSettings['export-footer-right'];
3599            if (hasCustomHeader || hasCustomFooter) {
3600                // 创建一个临时提示,在打印对话框打开后自动出现
3601                setTimeout(() => {
3602                    const toastContainer = document.getElementById('toast-container');
3603                    if (toastContainer) {
3604                        const toast = document.createElement('div');
3605                        toast.className = 'toast toast-info';
3606                        toast.innerHTML = `<i class="fas fa-info-circle"></i><span>${dict.printDialogNote || 'Please disable browser headers/footers in print dialog'}</span>`;
3607                        toastContainer.appendChild(toast);
3608                        setTimeout(() => toast.classList.add('show'), 10);
3609                        setTimeout(() => {
3610                            toast.classList.remove('show');
3611                            setTimeout(() => toast.remove(), 300);
3612                        }, 5000);
3613                    }
3614                }, 500);
3615            }
3616
3617            window.print();
3618
3619            // 清理逻辑:恢复标题、移除打印样式和容器
3620            let cleanupDone = false;
3621            const cleanupExport = () => {
3622                if (cleanupDone) return;
3623                cleanupDone = true;
3624                window.removeEventListener('afterprint', cleanupExport);
3625                document.removeEventListener('visibilitychange', onVisibilityChange);
3626                document.title = originalTitle;
3627                const ps = document.getElementById('pdf-print-style');
3628                if (ps) ps.remove();
3629                if (container.parentNode) document.body.removeChild(container);
3630                document.body.classList.remove('is-exporting');
3631                if (exportBtn) {
3632                    exportBtn.style.opacity = '1';
3633                    exportBtn.innerHTML = originalBtnHtml;
3634                }
3635                this.isExporting = false;
3636            };
3637
3638            // iOS Safari 的 afterprint 在打印面板弹出时立即触发(而非用户保存后),
3639            // 若此时恢复 document.title,PDF 文件名会变回网页标题。
3640            // 修复:移动端改用 visibilitychange:打印面板弹出时页面变 hidden,
3641            // 用户关闭面板(保存/取消)后页面变 visible,此时 PDF 已落盘,再恢复标题。
3642            const onVisibilityChange = () => {
3643                if (document.visibilityState === 'visible') {
3644                    document.removeEventListener('visibilitychange', onVisibilityChange);
3645                    setTimeout(cleanupExport, 300);
3646                }
3647            };
3648
3649            if (isMobile) {
3650                // 移动端:等页面重新可见再清理(打印面板关闭后)
3651                document.addEventListener('visibilitychange', onVisibilityChange);
3652                // 兜底:30s 后强制清理(visibilitychange 不触发的极端情况)
3653                setTimeout(cleanupExport, 30000);
3654            } else {
3655                // 桌面端:afterprint 可靠地在打印完成后触发
3656                window.addEventListener('afterprint', cleanupExport);
3657                setTimeout(cleanupExport, 3000);
3658            }
3659
3660        } catch (err) {
3661            console.error("PDF Export Fail", err);
3662            uiActions.showToast(dict.exportFail, 'error');
3663            this.isExporting = false;
3664            if (container.parentNode) document.body.removeChild(container);
3665            document.body.classList.remove('is-exporting');
3666        }
3667    },
3668    // 为保持向后兼容,保留 exportPDF 别名
3669    async exportPDF() {
3670        return this.performPdfExport();
3671    }
3672};
3673
3674
3675/**
3676 * FEEDBACK HANDLER
3677 */
3678const feedbackHandler = {
3679    selectedRating: 0,
3680
3681    openModal() {
3682        this.selectedRating = 0;
3683        // Reset form
3684        const msgEl = document.getElementById('feedbackMessage');
3685        const emailEl = document.getElementById('feedbackEmail');
3686        const subscribeEl = document.getElementById('feedbackSubscribe');
3687        const subRow = document.getElementById('feedbackSubscribeRow');
3688        const errEl = document.getElementById('feedbackError');
3689        const formBody = document.getElementById('feedbackFormBody');
3690        const successBody = document.getElementById('feedbackSuccessBody');
3691        const footer = document.getElementById('feedbackFooter');
3692
3693        if (msgEl) msgEl.value = '';
3694        if (emailEl) emailEl.value = '';
3695        if (subscribeEl) subscribeEl.checked = true;
3696        if (subRow) subRow.classList.remove('hidden');
3697        if (errEl) errEl.classList.add('hidden');
3698        if (formBody) formBody.classList.remove('hidden');
3699        if (successBody) successBody.classList.add('hidden');
3700        // Always restore footer to original buttons (submit may have replaced it with just OK)
3701        if (footer) {
3702            const dict = langMap[getLang()] || langMap['en'];
3703            footer.innerHTML = `
3704                <button onclick="feedbackHandler.closeModal()" class="h-8 px-3 rounded-md text-xs font-medium border border-input bg-background hover:bg-accent">${dict.modalCancel || 'Cancel'}</button>
3705                <button onclick="feedbackHandler.submit()" id="feedbackSubmitBtn" class="h-8 px-4 rounded-md text-xs font-bold bg-primary text-primary-foreground hover:opacity-90 active:scale-95 shadow-lg shadow-primary/20 min-w-[100px]">
3706                    <span>${dict.feedbackSubmitBtn || 'Send Feedback'}</span>
3707                </button>`;
3708        }
3709
3710        // Reset emoji rating buttons
3711        document.querySelectorAll('.feedback-emoji-btn').forEach(btn => btn.classList.remove('selected'));
3712
3713        uiActions.openModal('feedbackModal');
3714    },
3715
3716    closeModal() {
3717        uiActions.closeModal('feedbackModal');
3718    },
3719
3720    setRating(value) {
3721        this.selectedRating = value;
3722        document.querySelectorAll('.feedback-emoji-btn').forEach(btn => {
3723            btn.classList.toggle('selected', parseInt(btn.dataset.rating) === value);
3724        });
3725    },
3726
3727    onEmailInput() {
3728        // Email is now required; subscribe row is always visible
3729    },
3730
3731    submit() {
3732        const dict = langMap[getLang()] || langMap['en'];
3733        const msgEl = document.getElementById('feedbackMessage');
3734        const emailEl = document.getElementById('feedbackEmail');
3735        const subscribeEl = document.getElementById('feedbackSubscribe');
3736        const errEl = document.getElementById('feedbackError');
3737        const submitBtn = document.getElementById('feedbackSubmitBtn');
3738        const formBody = document.getElementById('feedbackFormBody');
3739        const successBody = document.getElementById('feedbackSuccessBody');
3740        const footer = document.getElementById('feedbackFooter');
3741
3742        const message = msgEl ? msgEl.value.trim() : '';
3743        if (message.length < 3) {
3744            if (errEl) {
3745                errEl.textContent = dict.feedbackErrorMsg || 'Please enter your feedback.';
3746                errEl.classList.remove('hidden');
3747            }
3748            if (msgEl) msgEl.focus();
3749            return;
3750        }
3751
3752        const emailVal = emailEl ? emailEl.value.trim() : '';
3753        if (!emailVal || !emailVal.includes('@')) {
3754            if (errEl) {
3755                errEl.textContent = dict.feedbackEmailRequired || 'Please enter a valid email address.';
3756                errEl.classList.remove('hidden');
3757            }
3758            if (emailEl) emailEl.focus();
3759            return;
3760        }
3761
3762        if (errEl) errEl.classList.add('hidden');
3763
3764        const payload = {
3765            rating: this.selectedRating || null,
3766            message,
3767            email: emailVal,
3768            subscribe: subscribeEl ? subscribeEl.checked : false,
3769        };
3770
3771        // Optimistic UI: show success immediately without waiting for network
3772        if (formBody) formBody.classList.add('hidden');
3773        if (successBody) successBody.classList.remove('hidden');
3774        if (footer) footer.innerHTML = `<button onclick="feedbackHandler.closeModal()" class="h-8 px-6 rounded-md text-xs font-bold bg-primary text-primary-foreground hover:opacity-90 active:scale-95">OK</button>`;
3775
3776        trackEvent('feedback_submitted', { rating: payload.rating, has_email: !!payload.email, subscribe: payload.subscribe });
3777
3778        // Fire-and-forget — send in background, no need to block UX
3779        fetch('/api/feedback', {
3780            method: 'POST',
3781            headers: { 'Content-Type': 'application/json' },
3782            body: JSON.stringify(payload),
3783        }).catch(err => console.error('Feedback submit error:', err));
3784    },
3785};
3786
3787// Expose to global scope for inline onclick handlers
3788window.feedbackHandler = feedbackHandler;
3789
3790/**
3791 * GOOGLE DRIVE SYNC
3792 */
3793const googleSync = {
3794    client: null,
3795    accessToken: null,
3796    tokenExpiry: 0,
3797    isSyncing: false, // 用于通知 SW 更新逻辑:同步进行中,暂缓页面刷新
3798    init(opt = null) {
3799        // 1. 内存中 token 仍有效,直接执行同步
3800        if (this.accessToken && Date.now() < this.tokenExpiry) {
3801            this.startSync();
3802            return;
3803        }
3804
3805        // 2. 防御性检查:GSI 库尚未加载完成(移动端弱网环境下可能出现)
3806        if (typeof google === 'undefined' || !google.accounts || !google.accounts.oauth2) {
3807            const lang = getLang();
3808            const dict = langMap[lang] || langMap['en'];
3809            uiActions.showToast((dict.syncFail || 'Sync failed: ') + 'Google Sign-In library n
3809ot loaded yet, please try again.', 'error');
3810            return;
3811        }
3812
3813        // 3. 检测是否为移动设备(iOS Safari / Android WebView 等)
3814        //    移动端浏览器对弹窗有严格的"用户手势上下文"限制:
3815        //    只有在用户手势(tap/click)的同步调用栈中才允许打开弹窗,
3816        //    异步回调(如 immediate_failed)中触发弹窗会被直接拦截。
3817        const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
3818
3819        // 4. 初始化 OAuth2 Token Client(只初始化一次,后续复用)
3820        if (!this.client) {
3821            this.client = google.accounts.oauth2.initTokenClient({
3822                client_id: CONFIG.CLIENT_ID,
3823                scope: 'https://www.googleapis.com/auth/drive.file',
3824                callback: (resp) => {
3825                    if (resp.error) {
3826                        if (resp.error === 'immediate_failed') {
3827                            // 静默授权失败(prompt: '')的处理策略因平台而异:
3828                            //
3829                            // ● 桌面端:immediate_failed 回调在极短时间内触发(无网络往返),
3830                            //   手势上下文通常仍有效,可以在此直接调用 select_account 弹窗。
3831                            //
3832                            // ● 移动端(iOS Safari ITP / Android SameSite Cookie 限制):
3833                            //   immediate_failed 回调属于异步调用,手势上下文已失效,
3834                            //   此处调用 requestAccessToken 会被浏览器静默拦截——弹窗不出现、
3835                            //   没有任何错误提示,导致用户反复点击同步却始终没有反应(死循环)。
3836                            //   正确做法:清除 enabled 标记 + 提示用户重新点击,
3837                            //   让下一次 tap 在真正的用户手势上下文中发起 select_account 请求。
3838                            const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
3839                            if (!isMobile) {
3840                                // 桌面端回退:在手势上下文仍有效时弹出账号选择器
3841                                console.warn('[GAuth] 静默授权失败,桌面端回退到完整授权弹窗');
3842                                this.client.requestAccessToken({ prompt: 'select_account' });
3843                            } else {
3844                                // 移动端回退:清除标记,提示用户重新点击同步按钮
3845                                console.warn('[GAuth] 移动端静默授权失败,清除授权状态,等待用户重新操作');
3846                                localStorage.removeItem('MD2PDF_DRIVE_SYNC_ENABLED');
3847                                const lang = getLang();
3848                                const dict = langMap[lang] || langMap['en'];
3849                                uiActions.showToast(dict.syncReauth || 'Authorization required. Please tap Sync again to connect Google Drive.', 'info');
3850                            }
3851                            return;
3852                        }
3853                        // 其他错误(popup_closed_by_user 等):清除同步标记,避免下次仍走静默模式
3854                        console.error('[GAuth] 授权错误:', resp.error);
3855                        localStorage.removeItem('MD2PDF_DRIVE_SYNC_ENABLED');
3856                        const lang = getLang();
3857                        const dict = langMap[lang] || langMap['en'];
3858                        uiActions.showToast((dict.syncFail || 'Sync failed: ') + resp.error, 'error');
3859                        return;
3860                    }
3861                    this.accessToken = resp.access_token;
3862                    this.tokenExpiry = Date.now() + (resp.expires_in || 3600) * 1000;
3863                    localStorage.setItem('MD2PDF_DRIVE_SYNC_ENABLED', 'true');
3864                    this.startSync();
3865                }
3866            });
3867        }
3868
3869        // 5. 决定授权策略
3870        //    ● 移动端:始终直接弹出 select_account(当前调用栈在用户 tap 手势中,弹窗不会被拦截)
3871        //      跳过 prompt: '' 静默授权,避免触发 immediate_failed → 异步回调 → 弹窗被拦截的死循环。
3872        //    ● 桌面端 + 曾经授权:先尝试 prompt: '' 静默刷新,失败时在回调中回退到弹窗。
3873        //    ● 桌面端 + 首次使用:直接弹出 select_account。
3874        if (opt) {
3875            this.client.requestAccessToken(opt);
3876        } else {
3877            const wasEnabled = localStorage.getItem('MD2PDF_DRIVE_SYNC_ENABLED') === 'true';
3878            if (wasEnabled && !isMobileDevice) {
3879                // 桌面端静默刷新,失败时由回调处理
3880                this.client.requestAccessToken({ prompt: '' });
3881            } else {
3882                // 移动端或首次使用:直接在用户手势上下文中弹出授权窗口
3883                this.client.requestAccessToken({ prompt: 'select_account' });
3884            }
3885        }
3886    },
3887
3888    async startSync() {
3889        const lang = getLang();
3890        const dict = langMap[lang] || langMap['en'];
3891        uiActions.showToast(dict.syncStart, "info");
3892
3893        this.isSyncing = true;
3894        try {
3895            // 1. Find or Create root folder for MD2PDF
3896            const vaultId = await this.getOrCreateFolder("MD2PDF_Vault");
3897
3898            // 2. Handle Deletions
3899            if (db.deletedNodes && db.deletedNodes.length > 0) {
3900                const remainingDeletes = [];
3901                for (const delNode of db.deletedNodes) {
3902                    if (delNode.remoteId) {
3903                        const success = await this.deleteRemoteFile(delNode.remoteId);
3904                        if (!success) remainingDeletes.push(delNode);
3905                    }
3906                }
3907                db.deletedNodes = remainingDeletes;
3908                save();
3909            }
3910
3911            // 3. Map remote files
3912            const remoteFiles = await this.getAllRemoteFiles(vaultId);
3913            const remoteMap = new Map();
3914            remoteFiles.forEach(f => {
3915                if (f.id !== vaultId) remoteMap.set(f.id, f);
3916            });
3917
3918            // 4. Local -> Remote Sync (Update & Upload)
3919            const idMap = { 'null': vaultId };
3920            const sortedNodes = [...db.nodes].sort((a, b) => {
3921                if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
3922                return (a.parentId || '').localeCompare(b.parentId || '');
3923            });
3924
3925            const nodesToDeleteLocally = new Set();
3926            for (const node of sortedNodes) {
3927                if (nodesToDeleteLocally.has(node.parentId)) {
3928                    nodesToDeleteLocally.add(node.id);
3929                    continue;
3930                }
3931
3932                const parentGId = idMap[node.parentId || 'null'] || vaultId;
3933                let remoteNode = node.remoteId ? remoteMap.get(node.remoteId) : null;
3934
3935                // Detection: Remote Deletion
3936                if (!remoteNode && node.remoteId) {
3937                    // This node was previously synced but is no longer on Google Drive
3938                    const localTime = node.updatedAt || 0;
3939                    const lastSyncTime = node.remoteModifiedTime || 0;
3940                    if (localTime > lastSyncTime + 2000) {
3941                        // Local has changes since last sync, re-upload to GDrive
3942                        const gId = await this.uploadNewNode(node, parentGId);
3943                        node.remoteId = gId;
3944                        node.remoteModifiedTime = await this.getRemoteModifiedTime(gId);
3945                    } else {
3946                        // No local changes, accept the remote deletion
3947                        nodesToDeleteLocally.add(node.id);
3948                        continue;
3949                    }
3950                }
3951
3952                // If not found by ID (new node), try match by name and parent to avoid duplicates
3953                if (!remoteNode && !node.remoteId) {
3954                    const nodeGName = node.type === 'file' ? (node.name.endsWith('.md') ? node.name : node.name + '.md') : node.name;
3955                    remoteNode = remoteFiles.find(f => f.name === nodeGName && f.parents && f.parents.includes(parentGId));
3956                }
3957
3958                if (remoteNode) {
3959                    const remoteTime = new Date(remoteNode.modifiedTime).getTime();
3960                    const localTime = node.updatedAt || 0;
3961                    const lastSyncTime = node.remoteModifiedTime || 0;
3962
3963                    if (localTime > lastSyncTime && (localTime - remoteTime > 2000)) {
3964                        // 本地更新,且比远端新 2s 以上 → 上传到远端
3965                        await this.updateRemoteNode(node, remoteNode.id);
3966                        node.remoteId = remoteNode.id;
3967                        node.remoteModifiedTime = await this.getRemoteModifiedTime(node.remoteId);
3968                    } else if (remoteTime > lastSyncTime && (remoteTime - localTime > 2000)) {
3969                        // 远端更新,且比本地新 2s 以上 → 下载到本地
3970                        await this.downloadRemoteNode(node, remoteNode);
3971                    } else if (localTime > lastSyncTime && remoteTime > lastSyncTime) {
3972                        // 冲突:两端都在上次同步后修改,取时间戳较新的一方(2s 阈值内无法区分时取本地)
3973                        if (localTime >= remoteTime) {
3974                            await this.updateRemoteNode(node, remoteNode.id);
3975                            node.remoteId = remoteNode.id;
3976                            node.remoteModifiedTime = await this.getRemoteModifiedTime(node.remoteId);
3977                        } else {
3978                            await this.downloadRemoteNode(node, remoteNode);
3979                        }
3980                    } else if (localTime > lastSyncTime && remoteTime <= lastSyncTime) {
3981                        // Bug fix: 本地在上次同步后有修改,但与远端时间差 ≤ 2s(时钟误差或极快修改)
3982                        // 远端未变更,应仍以本地为准上传,避免修改永久丢失
3983                        await this.updateRemoteNode(node, remoteNode.id);
3984                        node.remoteId = remoteNode.id;
3985                        node.remoteModifiedTime = await this.getRemoteModifiedTime(node.remoteId);
3986                    } else {
3987                        // 无需同步,仅更新本地记录的远端元数据
3988                        node.remoteId = remoteNode.id;
3989                        node.remoteModifiedTime = remoteTime;
3990                    }
3991                    remoteMap.delete(remoteNode.id);
3992                } else {
3993                    // Truly new local node -> Upload new
3994                    const gId = await this.uploadNewNode(node, parentGId);
3995                    node.remoteId = gId;
3996                    node.remoteModifiedTime = await this.getRemoteModifiedTime(gId);
3997                }
3998                idMap[node.id] = node.remoteId;
3999            }
4000
4001            if (nodesToDeleteLocally.size > 0) {
4002                db.nodes = db.nodes.filter(n => !nodesToDeleteLocally.has(n.id));
4003            }
4004
4005            // 5. Remote -> Local Sync (Download missing)
4006            let remotesToCreate = Array.from(remoteMap.values());
4007            let attempts = 0;
4008            while (remotesToCreate.length > 0 && attempts < 100) {
4009                const initialLen = remotesToCreate.length;
4010                for (let i = 0; i < remotesToCreate.length; i++) {
4011                    const rNode = remotesToCreate[i];
4012                    const parentGId = rNode.parents ? rNode.parents[0] : null;
4013
4014                    const localParent = db.nodes.find(n => n.remoteId === parentGId);
4015                    if (parentGId === vaultId || localParent) {
4016                        const parentId = localParent ? localParent.id : null;
4017                        const newNode = {
4018                            id: createId(),
4019                            name: rNode.mimeType.includes('folder') ? rNode.name : rNode.name.replace(/\.md$/, ''),
4020                            type: rNode.mimeType.includes('folder') ? 'folder' : 'file',
4021                            parentId: parentId,
4022                            remoteId: rNode.id,
4023                            remoteModifiedTime: new Date(rNode.modifiedTime).getTime(),
4024                            updatedAt: new Date(rNode.modifiedTime).getTime(),
4025                            order: db.nodes.filter(n => n.parentId === parentId).length,
4026                            content: ''
4027                        };
4028                        if (newNode.type === 'file') {
4029                            // exportSettings 嵌入文件内容末尾注释,_parseContent 会剥离并解析
4030                            const raw = await this.downloadFile(rNode.id);
4031                            const { content, exportSettings } = this._parseContent(raw);
4032                            newNode.content = content;
4033                            if (exportSettings) newNode.exportSettings = exp
4033ortSettings;
4034                        }
4035                        db.nodes.push(newNode);
4036                        remotesToCreate.splice(i, 1);
4037                        i--;
4038                    }
4039                }
4040                if (remotesToCreate.length === initialLen) break;
4041                attempts++;
4042            }
4043
4044            save();
4045            renderTree();
4046            uiActions.showToast(dict.syncSuccess, "success");
4047        } catch (err) {
4048            console.error("Sync Error:", err);
4049            // 特殊处理:Drive API 403/401 表示授权已失效,清除 token 并提示重新授权
4050            const isAuthError = err.message && (err.message.includes('403') || err.message.includes('401'));
4051            if (isAuthError) {
4052                this.accessToken = null;
4053                this.tokenExpiry = 0;
4054                localStorage.removeItem('MD2PDF_DRIVE_SYNC_ENABLED');
4055                uiActions.showToast(dict.syncReauth || 'Authorization expired. Please tap Sync again to reconnect Google Drive.', 'error');
4056            } else {
4057                uiActions.showToast(dict.syncFail + err.message, "error");
4058            }
4059        } finally {
4060            this.isSyncing = false;
4061        }
4062    },
4063
4064    async getAllRemoteFiles(vaultId) {
4065        let allFiles = [];
4066        let pageToken = null;
4067        // Bug fix: do NOT swallow errors here — a fetch failure (e.g. expired token) must
4068        // propagate so startSync can catch it and show the user an error, rather than
4069        // silently returning [] and causing all local files to be re-uploaded as duplicates.
4070        do {
4071            const query = "trashed = false";
4072            const resp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=nextPageToken,files(id,name,mimeType,modifiedTime,parents)&pageSize=1000${pageToken ? `&pageToken=${pageToken}` : ''}`, {
4073                headers: { 'Authorization': `Bearer ${this.accessToken}` }
4074            });
4075            if (!resp.ok) throw new Error(`Drive API ${resp.status}: getAllRemoteFiles`);
4076            const data = await resp.json();
4077            allFiles = allFiles.concat(data.files || []);
4078            pageToken = data.nextPageToken;
4079        } while (pageToken);
4080        return allFiles;
4081    },
4082
4083    async getRemoteModifiedTime(id) {
4084        const resp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files/${id}?fields=modifiedTime`, {
4085            headers: { 'Authorization': `Bearer ${this.accessToken}` }
4086        });
4087        if (!resp.ok) throw new Error(`Drive API ${resp.status}: getRemoteModifiedTime`);
4088        const data = await resp.json();
4089        return new Date(data.modifiedTime).getTime();
4090    },
4091
4092    async downloadFile(id) {
4093        const resp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files/${id}?alt=media`, {
4094            headers: { 'Authorization': `Bearer ${this.accessToken}` }
4095        });
4096        if (!resp.ok) throw new Error(`Drive API ${resp.status}: downloadFile`);
4097        return await resp.text();
4098    },
4099
4100    async deleteRemoteFile(id) {
4101        try {
4102            const resp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files/${id}`, {
4103                method: 'DELETE',
4104                headers: { 'Authorization': `Bearer ${this.accessToken}` }
4105            });
4106            if (resp.ok || resp.status === 404) {
4107                return true;
4108            } else {
4109                console.warn(`Failed to delete remote file ${id}: ${resp.status}`);
4110                return false;
4111            }
4112        } catch (e) {
4113            console.warn("Network error deleting remote file", id, e);
4114            return false;
4115        }
4116    },
4117
4118    // 将 exportSettings 序列化为文件末尾的 HTML 注释,避免 Drive appProperties 124字节限制。
4119    // HTML 注释不会被 Markdown 渲染器显示,对阅读体验无影响。
4120    _serializeContent(node) {
4121        const base = node.content || '';
4122        if (node.type !== 'file' || !node.exportSettings || !Object.keys(node.exportSettings).length) {
4123            return base;
4124        }
4125        const tag = `<!-- md2pdf-settings:${JSON.stringify(node.exportSettings)} -->`;
4126        return base + '\n' + tag;
4127    },
4128
4129    // 从下载的文件内容中提取并剥离 exportSettings 注释,返回 { content, exportSettings }
4130    _parseContent(raw) {
4131        const marker = '<!-- md2pdf-settings:';
4132        const idx = raw.lastIndexOf(marker);
4133        if (idx === -1) return { content: raw, exportSettings: null };
4134        const end = raw.indexOf('-->', idx);
4135        if (end === -1) return { content: raw, exportSettings: null };
4136        const json = raw.slice(idx + marker.length, end).trim();
4137        const content = raw.slice(0, idx).replace(/\n$/, ''); // 去掉追加时的换行
4138        try {
4139            return { content, exportSettings: JSON.parse(json) };
4140        } catch (e) {
4141            return { content: raw, exportSettings: null };
4142        }
4143    },
4144
4145    async updateRemoteNode(node, remoteId) {
4146        const name = node.type === 'file' ? (node.name.endsWith('.md') ? node.name : node.name + '.md') : node.name;
4147        const updateMeta = { name: name };
4148
4149        // Bug fix: check response status for metadata PATCH
4150        const metaResp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files/${remoteId}`, {
4151            method: 'PATCH',
4152            headers: {
4153                'Authorization': `Bearer ${this.accessToken}`,
4154                'Content-Type': 'application/json'
4155            },
4156            body: JSON.stringify(updateMeta)
4157        });
4158        if (!metaResp.ok) throw new Error(`Drive API ${metaResp.status}: updateRemoteNode (metadata)`);
4159
4160        if (node.type === 'file') {
4161            // exportSettings 以 HTML 注释形式嵌入文件末尾,规避 appProperties 124字节限制
4162            const mediaResp = await this.fetchWithRetry(`https://www.googleapis.com/upload/drive/v3/files/${remoteId}?uploadType=media`, {
4163                method: 'PATCH',
4164                headers: {
4165                    'Authorization': `Bearer ${this.accessToken}`,
4166                    'Content-Type': 'text/markdown'
4167                },
4168                body: this._serializeContent(node)
4169            });
4170            if (!mediaResp.ok) throw new Error(`Drive API ${mediaResp.status}: updateRemoteNode (content)`);
4171        }
4172    },
4173
4174    async downloadRemoteNode(localNode, remoteNode) {
4175        localNode.name = localNode.type === 'file' ? remoteNode.name.replace(/\.md$/, '') : remoteNode.name;
4176        if (localNode.type === 'file') {
4177            const raw = await this.downloadFile(remoteNode.id);
4178            // exportSettings 以 HTML 注释嵌入文件末尾,解析并剥离后存入 node
4179            const { content, exportSettings } = this._parseContent(raw);
4180            localNode.content = content;
4181            if (exportSettings) localNode.exportSettings = exportSettings;
4182        }
4183        localNode.remoteId = remoteNode.id;
4184        localNode.remoteModifiedTime = new Date(remoteNode.modifiedTime).getTime();
4185        localNode.updatedAt = localNode.remoteModifiedTime;
4186    },
4187
4188    async uploadNewNode(node, parentGId) {
4189        const name = node.type === 'file' ? (node.name.endsWith('.md') ? node.name : node.name + '.md') : node.name;
4190        const mimeType = node.type === 'folder' ? 'application/vnd.google-apps.folder' : 'text/markdown';
4191        const meta = { name: name, mimeType: mimeType, parents: [parentGId] };
4192        // exportSettings 通过文件内容末尾的 HTML 注释传输,不使用 appProperties(有 124字节限制)
4193
4194        if (node.type === 'folder') {
4195            const resp = await this.fetchWithRetry('https://www.googleapis.com/drive/v3/files', {
4196                method: 'POST',
4197                headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
4198                body: JSON.stringify(meta)
4199            });
4200            // Bug fix: check response before reading id; bad response => throw to surface the error
4201            if (!resp.ok) throw new Error(`Drive API ${resp.status}: uploadNewNode (folder)`);
4202            const data = await resp.json();
4203            if (!data.id) throw new Error('Drive API: uploadNewNode folder returned no id');
4204            return data.id;
4205        } else {
4206            const form = new FormData();
4207            form.append('metadata', new Blob([JSON.stringify(meta)], { type: 'application/json' }));
4208            // exportSettings 序列化为内容末尾注释,避免 appProperties 124字节长度限制
4209            form.append('file', new Blob([this._serializeContent(node)], { type: 'text/markdown' }));
4210            const resp = await this.fetchWithRetry('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
4211                method: 'POST',
4212                headers: { 'Authorization': `Bearer ${this.accessToken}` },
4213                body: form
4214            });
4215            // Bug fix: check response before reading id
4216            if (!resp.ok) throw new Error(`Drive API ${resp.status}: uploadNewNode (file)`);
4217            const data = await resp.json();
4218            if (!data.id) throw new Error('Drive API: uploadNewNode file returned no id');
4219            return data.id;
4220        }
4221    },
4222
4223    async fetchWithRetry(url, options = {}, retries = 3) {
4224        let lastErr;
4225        
4226        // Android/Mobile optimization:
4227        // 1. Omit credentials to avoid cookie issues (crucial for mobile ITP/SameSite)
4228        // 2. Set cache to 'no-store' to bypass SW/browser cache issues
4229        // 3. Explicitly set mode to 'cors'
4230        if (url.includes('googleapis.com')) {
4231            if (!options.credentials) options.credentials = 'omit';
4232            if (!options.cache) options.cache = 'no-store';
4233            options.mode = 'cors'; // Ensure CORS mode is explicit
4234            
4235            // Remove referrerPolicy='no-referrer' as it might block valid API calls checking origin
4236            // if (!options.referrerPolicy) options.referrerPolicy = 'no-referrer';
4237        }
4238        
4239        for (let i = 0; i < retries; i++) {
4240            try {
4241                // Check online status first
4242                if (navigator.onLine === false) {
4243                    throw new Error('Network offline');
4244                }
4245                
4246                const response = await fetch(url, options);
4247                
4248                // If 401/403, read error body for diagnostics, then return for caller to handle
4249                if (response.status === 401 || response.status === 403) {
4250                    try {
4251                        const errBody = await response.clone().json();
4252                        const reason = errBody?.error?.errors?.[0]?.reason || errBody?.error?.message || '';
4253                        if (reason) console.warn(`[Drive API] ${response.status} - ${reason}`, url);
4254                    } catch (_) { /* response body may not be JSON */ }
4255                    return response;
4256                }
4257                
4258                // If 5xx, retry
4259                if (response.status >= 500) {
4260                    throw new Error(`Server error: ${response.status}`);
4261                }
4262                
4263                return response;
4264            } catch (err) {
4265                lastErr = err;
4266                if (i < retries - 1) {
4267                    await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
4268                }
4269            }
4270        }
4271        throw lastErr;
4272    },
4273
4274    async getOrCreateFolder(name, parentId = null) {
4275        // Bug fix: when searching for the root vault folder (no parentId), add 'root' in parents
4276        // constraint to prevent matching a same-named folder elsewhere in the user's Drive.
4277        let query = `name = '${name}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`;
4278        if (parentId) {
4279            query += ` and '${parentId}' in parents`;
4280        } else {
4281            query += ` and 'root' in parents`;
4282        }
4283
4284        const resp = await this.fetchWithRetry(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
4285            headers: { 'Authorization': `Bearer ${this.accessToken}` }
4286        });
4287        if (!resp.ok) throw new Error(`Drive API ${resp.status}: getOrCreateFolder`);
4288        const data = await resp.json();
4289
4290        if (data.files && data.files.length > 0) {
4291            return data.files[0].id;
4292        }
4293
4294        const meta = {
4295            name: name,
4296            mimeType: 'application/vnd.google-apps.folder'
4297        };
4298        if (parentId) meta.parents = [parentId];
4299
4300        const createResp = await this.fetchWithRetry('https://www.googleapis.com/drive/v3/files', {
4301            method: 'POST',
4302            headers: {
4303                'Authorization': `Bearer ${this.accessToken}`,
4304                'Content-Type': 'application/json'
4305            },
4306            body: JSON.stringify(meta)
4307        });
4308        if (!createResp.ok) throw new Error(`Drive API ${createResp.status}: getOrCreateFolder (create)`);
4309        const created = await createResp.json();
4310        if (!created.id) throw new Error('Drive API: getOrCreateFolder returned no id');
4311        return created.id;
4312    }
4313};
4314
4315/**
4316 * HELPERS
4317 */
4318function createId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); }
4319
4320function save() {
4321    const toSave = { ...db, expanded: Array.from(db.expanded) };
4322    localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(toSave));
4323}
4324
4325function updateBreadcrumb(id) {
4326    let path = [];
4327    let curr = db.nodes.find(n => n.id === id);
4328    while (curr) {
4329        if (curr.parentId) {
4330            curr = db.nodes.find(n => n.id === curr.parentId);
4331            if (curr) path.unshift(curr.name);
4332        } else break;
4333    }
4334    const el = document.getElementById('breadcrumb');
4335    if (el) el.innerText = path.join(' / ') + (path.length ? ' /' : '');
4336}
4337
4338function updatePreview() {
4339    const editorValue = document.getElementById('editor').value;
4340    const previewContent = document.getElementById('preview-content');
4341    if (previewContent) {
4342        window._footnotes = []; // Reset footnotes collection
4343        let html = marked.parse(editorValue || '');
4344
4345        // Render footnotes at the bottom
4346        if (window._footnotes && window._footnotes.length > 0) {
4347            html += '<div class="footnotes"><hr><section class="footnotes-list"><ol>';
4348            window._footnotes.forEach(fn => {
4349                html += `<li id="fn-${fn.id}">${fn.content} <a href="#fnref-${fn.id}" class="footnote-backref">↩</a></li>`;
4350            });
4351            html += '</ol></section></div>';
4352        }
4353
4354        previewContent.innerHTML = html;
4355
4356        // Render Mermaid Diagrams if present
4357        if (window.mermaid) {
4358            // Use a slight delay to ensure the DOM is ready and avoid measurement errors
4359            setTimeout(() => {
4360                // IMPORTANT: Only render if the preview is actually visible to avoid "NaN" transform errors
4361                const previewPane = document.getElementById('preview-pane');
4362                if (previewPane && previewPane.offsetParent === null) return;
4363
4364                const mermaidNodes = previewContent.querySelectorAll('.mermaid');
4365                if (mermaidNodes.length > 0) {
4366                    mermaid.run({
4367                        nodes: mermaidNodes,
4368                    }).catch(err => {
4369                        // Prevent error loop by logging but not crashing
4370                        console.error("Mermaid Render Error:", err);
4371                    });
4372                }
4373            }, 50);
4374        }
4375    }
4376}
4377
4378function setupEventListeners() {
4379    // GA4 Tracking: Affiliate Links & External Tools
4380    document.addEventListener('click', (e) => {
4381        const link = e.target.closest('a');
4382        if (link && link.target === '_blank' && !link.href.includes(window.location.host)) {
4383            // Track clicks on affiliate links
4384            const isAffiliate = link.href.includes('quantaflow.cc') || link.getAttribute('data-affiliate') === 'true' || link.innerText.includes('AI Power Tools');
4385            if (isAffiliate) {
4386                const linkName = link.getAttribute('data-link-name') || link.innerText.trim() || link.hostname;
4387                trackEvent('md_affiliate_click', { link_name: linkName });
4388            }
4389        }
4390    });
4391
4392    // GA4 Tracking: Landing Page Hero CTA Clicks
4393    document.addEventListener('click', (e) => {
4394        const ctaButton = e.target.closest('button');
4395        if (ctaButton) {
4396            const onclickAttr = ctaButton.getAttribute('onclick') || '';
4397            let ctaType = '';
4398            // Detect primary CTA types
4399            if (onclickAttr.includes('performPdfExport')) ctaType = 'export_pdf';
4400            if (onclickAttr.includes('toggleGuide')) ctaType = 'ai_assistant';
4401            if (onclickAttr.includes('toggleCopyMenu')) ctaType = 'copy_content';
4402
4403            if (ctaType) {
4404                // Determine LP context from pathname for cta_location
4405                const path = window.location.pathname;
4406                let lpId = 'home';
4407
4408                if (path !== '/' && path !== '/index.html') {
4409                    // Extract the primary folder name or filename
4410                    const segments = path.split('/').filter(s => s && s !== 'index.html');
4411                    if (segments.length > 0) {
4412                        lpId = segments[0].replace('-to-pdf', '').replace('-exporter', '').replace('-to-pdf-online', '');
4413                    }
4414                }
4415
4416                trackEvent('md_lp_cta_click', {
4417                    cta_location: `hero_section_${lpId}`,
4418                    cta_type: ctaType
4419                });
4420            }
4421        }
4422    });
4423
4424    const mobileMenuBtn = document.getElementById('mobileMenuBtn');
4425    if (mobileMenuBtn) {
4426        mobileMenuBtn.onclick = () => {
4427            const sidebar = document.getElementById('sidebar');
4428            sidebar.classList.toggle('open');
4429        };
4430    }
4431
4432    // Close sidebar when clicking outside on mobile
4433    document.addEventListener('click', (e) => {
4434        if (window.innerWidth < 768) {
4435            const sidebar = document.getElementById('sidebar');
4436            const mobileMenuBtn = document.getElementById('mobileMenuBtn');
4437            if (sidebar && sidebar.classList.contains('open') &&
4438                !sidebar.contains(e.target) &&
4439                mobileMenuBtn && !mobileMenuBtn.contains(e.target)) {
4440                sidebar.classList.remove('open');
4441            }
4442        }
4443    });
4444
4445    // Close sidebar when clicking a file on mobile
4446    const treeContainer = document.getElementById('treeContainer');
4447    if (treeContainer) {
4448        treeContainer.addEventListener('click', (e) => {
4449            if (window.innerWidth < 768 && e.target.closest('.tree-node')) {
4450                const node = e.target.closest('.tree-node');
4451                // Check if it's a file by verifying no folder icon
4452                if (node.querySelector('.fa-file-lines')) {
4453                    document.getElementById('sidebar').classList.remove('open');
4454                }
4455            }
4456        });
4457    }
4458
4459    // Fix footnote links jumping causing page layout shift
4460    const previewPane = document.getElementById('preview-pane');
4461    if (previewPane) {
4462        previewPane.addEventListener('click', (e) => {
4463            const anchor = e.target.closest('a');
4464            if (anchor && anchor.getAttribute('href')?.startsWith('#fn')) {
4465                const targetId = anchor.getAttribute('href').slice(1);
4466                const targetEl = document.getElementById(targetId);
4467                if (targetEl) {
4468                    e.preventDefault();
4469                    targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
4470                }
4471            }
4472        });
4473    }
4474
4475    document.getElementById('editor').addEventListener('input', (e) => {
4476        const file = db.nodes.find(n => n.id === db.activeId);
4477        if (file) {
4478            file.content = e.target.value;
4479            file.updatedAt = Date.now();
4480            save();
4481        }
4482        updatePreview();
4483    });
4484    document.addEventListener('contextmenu', e => {
4485        if (!e.target.closest('.tree-node')) e.preventDefault();
4486    });
4487}
4488
4489function initResizer() {
4490    const resizer = document.getElementById('resizer');
4491    const editor = document.getElementById('editor');
4492    const ws = document.getElementById('workspace');
4493    if (!resizer) return;
4494    let isDragging = false;
4495
4496    resizer.addEventListener('mousedown', (e) => {
4497        isDragging = true;
4498        resizer.classList.add('dragging');
4499        document.body.style.cursor = 'col-resize';
4500    });
4501
4502    document.addEventListener('mousemove', (e) => {
4503        if (!isDragging) return;
4504        const containerRect = ws.getBoundingClientRect();
4505        const relativeX = e.clientX - containerRect.left;
4506        const percentage = (relativeX / containerRect.width) * 100;
4507
4508        if (percentage > 10 && percentage < 90) {
4509            editor.style.flex = `0 0 ${percentage}%`;
4510        }
4511    });
4512
4513    document.addEventListener('mouseup', () => {
4514        if (isDragging) {
4515            isDragging = false;
4516            resizer.classList.remove('dragging');
4517            document.body.style.cursor = 'default';
4518        }
4519    });
4520}
4521
4522/**
4523 * SYNC SCROLL (EDITOR & PREVIEW)
4524 */
4525function initSyncScroll() {
4526    const editor = document.getElementById('editor');
4527    const previewPane = document.getElementById('preview-pane');
4528    if (!editor || !previewPane) return;
4529
4530    let isSyncingEditor = false;
4531    let isSyncingPreview = false;
4532
4533    editor.onscroll = () => {
4534        if (isSyncingEditor) {
4535            isSyncingEditor = false;
4536            return;
4537        }
4538        const ws = document.getElementById('workspace');
4539        // Only sync in split mode
4540        if (!ws || !ws.classList.contains('mode-split')) return;
4541
4542        isSyncingPreview = true;
4543        const denominator = editor.scrollHeight - editor.clientHeight;
4544        if (denominator <= 0) return;
4545
4546        const percentage = editor.scrollTop / denominator;
4547        previewPane.scrollTop = percentage * (previewPane.scrollHeight - previewPane.clientHeight);
4548    };
4549
4550    previewPane.onscroll = () => {
4551        if (isSyncingPreview) {
4552            isSyncingPreview = false;
4553            return;
4554        }
4555        const ws = document.getElementById('workspace');
4556        // Only sync in split mode
4557        if (!ws || !ws.classList.contains('mode-split')) return;
4558
4559        isSyncingEditor = true;
4560        const denominator = previewPane.scrollHeight - previewPane.clientHeight;
4561        if (denominator <= 0) return;
4562
4563        const percentage = previewPane.scrollTop / denominator;
4564        editor.scrollTop = percentage * (editor.scrollHeight - editor.clientHeight);
4565    };
4566}
4567
4568window.onload = () => {
4569    // Landing Page Detection & Storage
4570    if (!sessionStorage.getItem('original_lp')) {
4571        const params = new URLSearchParams(window.location.search);
4572        const source = params.get('utm_source') || params.get('ref') || 'organic';
4573        const campaign = params.get('utm_campaign') || '';
4574        const lpValue = `${window.location.pathname}${source !== 'organic' ? '?source=' + source : ''}${campaign ? '&campaign=' + campaign : ''}`;
4575        sessionStorage.setItem('original_lp', lpValue);
4576
4577        // Trigger Initial Visit Event
4578        trackEvent('md_lp_view', {
4579            lp_path: window.location.pathname,
4580            entry_source: source,
4581            campaign: campaign
4582        });
4583    }
4584
4585    // GA4 Tracking: Traffic Source & Entry Context
4586    const urlParamsForGA = new URLSearchParams(window.location.search);
4587    if (urlParamsForGA.has('content')) {
4588        trackEvent('md_entry_api', { source: urlParamsForGA.get('source') || 'api' });
4589    }
4590
4591    const referrer = document.referrer;
4592    if (referrer) {
4593        try {
4594            const refUrl = new URL(referrer);
4595            const domain = refUrl.hostname.replace('www.', '');
4596            const referralSources = ['v2ex.com', 'reddit.com', 'producthunt.com'];
4597            if (referralSources.includes(domain)) {
4598                trackEvent('md_referral_source', { source: domain });
4599            }
4600        } catch (e) { /* ignore parse errors */ }
4601    }
4602
4603    // 补丁:解决 html2pdf.js 调用 jsPDF 构造函数时参数过时警告的问题
4604    const fixJsPDF = () => {
4605        const _jsPDF = window.jsPDF || (window.jspdf && window.jspdf.jsPDF);
4606        if (_jsPDF && !_jsPDF.__patched) {
4607            const PatchedjsPDF = function (orientation, unit, format, compressPDF) {
4608                if (arguments.length > 1 && typeof orientation !== 'object') {
4609                    return new _jsPDF({ orientation, unit, format, compressPDF });
4610                }
4611                return new _jsPDF(...arguments);
4612            };
4613            PatchedjsPDF.prototype = _jsPDF.prototype;
4614            PatchedjsPDF.__patched = true;
4615            window.jsPDF = PatchedjsPDF;
4616            if (window.jspdf) window.jspdf.jsPDF = PatchedjsPDF;
4617        }
4618    };
4619    fixJsPDF();
4620
4621    translateUI();
4622    uiActions.updateLogoVisibility();
4623    window.addEventListener('resize', () => {
4624        uiActions.updateLogoVisibility();
4625        renderTree();
4626    });
4627
4628    // Load saved theme
4629    const savedTheme = localStorage.getItem('MD2PDF_THEME') || 'light';
4630    document.body.setAttribute('data-theme', savedTheme);
4631
4632    marked.setOptions({
4633        gfm: true,
4634        breaks: true,
4635        headerIds: false,
4636        mangle: false,
4637        headerPrefix: ''
4638    });
4639
4640    // Custom Extensions for Marked
4641    marked.use({
4642        renderer: {
4643            code(code, lang) {
4644                const text = typeof code === 'object' ? code.text : code;
4645                const language = typeof code === 'object' ? code.lang : lang;
4646                if (language === 'mermaid') {
4647                    return `<div class="mermaid">${text}</div>`;
4648                }
4649                return false;
4650            },
4651            blockquote(quote) {
4652                const isToken = typeof quote !== 'string';
4653                const text = isToken ? (quote.text || '') : quote;
4654                const reg = isToken ? /^\s*\[!(NOTE|TIP|WARNING|DANGER|INFO|IMPORTANT|CAUTION|TODO|HINT|BUG|EXAMPLE|QUOTE)\](?:\s*(.*))?/i : /^<p>\[!(NOTE|TIP|WARNING|DANGER|INFO|IMPORTANT|CAUTION|TODO|HINT|BUG|EXAMPLE|QUOTE)\](?:\s*(.*))?<\/p>/i;
4655
4656                const match = text.match(reg);
4657                if (match) {
4658                    const type = match[1].toLowerCase();
4659                    const customTitle = match[2] ? match[2].trim() : null;
4660                    const icons = {
4661                        note: 'fa-info-circle',
4662                        info: 'fa-circle-info',
4663                        todo: 'fa-check-circle',
4664                        tip: 'fa-lightbulb',
4665                        hint: 'fa-magnifying-glass',
4666                        warning: 'fa-exclamation-triangle',
4667                        caution: 'fa-exclamation-circle',
4668                        important: 'fa-circle-exclamation',
4669                        danger: 'fa-skull-crossbones',
4670                        error: 'fa-circle-xmark',
4671                        bug: 'fa-bug',
4672                        example: 'fa-list-ul',
4673                        quote: 'fa-quote-left'
4674                    };
4675
4676                    const titlesMap = {
4677                        'zh': { note: '备注', tip: '提示', warning: '警告', danger: '危险', info: '信息', important: '重要', caution: '注意', todo: '待办', hint: '启发', bug: '缺陷', example: '例子', quote: '引用' },
4678                        'tw': { note: '備註', tip: '提示', warning: '警告', danger: '危險', info: '信息', important: '重要', caution: '注意', todo: '待辦', hint: '啟發', bug: '缺陷', example: '例子', quote: '引用' },
4679                        'es': { note: 'Nota', tip: 'Consejo', warning: 'Advertencia', danger: 'Peligro', info: 'Información', important: 'Importante', caution: 'Precaución', todo: 'Tarea', hint: 'Pista', bug: 'Error', example: 'Ejemplo', quote: 'Cita' },
4680                        'en': { note: 'Note', tip: 'Tip', warning: 'Warning', danger: 'Danger', info: 'Info', important: 'Important', caution: 'Caution', todo: 'Todo', hint: 'Hint', bug: 'Bug', example: 'Example', quote: 'Quote' }
4681                    };
4682                    const lang = typeof getLang === 'function' ? getLang() : 'en';
4683                    const titles = titlesMap[lang] || titlesMap['en'];
4684                    const displayName = customTitle || titles[type] || type.toUpperCase();
4685
4686                    let content;
4687                    if (isToken) {
4688                        const tokens = JSON.parse(JSON.stringify(quote.tokens || []));
4689                        if (tokens[0] && tokens[0].type === 'paragraph') {
4690                            tokens[0].text = tokens[0].text.replace(reg, '').trim();
4691                            if (tokens[0].tokens && tokens[0].tokens[0] && tokens[0].tokens[0].type === 'text') {
4692                                tokens[0].tokens[0].text = tokens[0].tokens[0].text.replace(/^\[!(NOTE|TIP|WARNING|DANGER|INFO|IMPORTANT|CAUTION|TODO|HINT|BUG|EXAMPLE|QUOTE)\](?:\s*(.*))?/i, '').trim();
4693                            }
4694                        }
4695                        content = this.parser.parse(tokens);
4696                    } else {
4697                        content = quote.replace(reg, '<p>').replace('<p></p>', '');
4698                    }
4699
4700                    const colorMap = {
4701                        'note': 'note', 'info': 'info', 'todo': 'todo',
4702                        'tip': 'tip', 'hint': 'hint',
4703                        'warning': 'warning', 'caution': 'caution', 'important': 'important',
4704                        'danger': 'danger', 'error': 'danger', 'bug': 'danger',
4705                        'example': 'example', 'quote': 'quote'
4706                    };
4707                    const colorType = colorMap[type] || 'note';
4708
4709                    return `<div class="callout callout-${colorType}">
4710                        <p class="callout-title"><i class="fas ${icons[type] || 'fa-info-circle'}"></i> ${displayName}</p>
4711                        ${content}
4712                    </div>`;
4713                }
4714                return false;
4715            }
4716        }
4717    });
4718
4719    // Custom Extensions
4720    marked.use({
4721        extensions: [
4722            {
4723                name: 'highlight',
4724                level: 'inline',
4725                start(src) { return src.match(/==/)?.index; },
4726                tokenizer(src, tokens) {
4727                    const match = src.match(/^==(?=[^\s=])([\s\S]*?[^\s=])==/);
4728                    if (match) {
4729                        return {
4730                            type: 'highlight',
4731                            raw: match[0],
4732                            text: this.lexer.inlineTokens(match[1])
4733                        };
4734                    }
4735                },
4736                renderer(token) {
4737                    return `<mark>${this.parser.parseInline(token.text)}</mark>`;
4738                }
4739            },
4740            {
4741                name: 'wikilink',
4742                level: 'inline',
4743                start(src) { return src.match(/\[\[/)?.index; },
4744                tokenizer(src, tokens) {
4745                    const match = src.match(/^\[\[([^\]\|]+)(\|([^\]]+))?\]\]/);
4746                    if (match) {
4747                        return {
4748                            type: 'wikilink',
4749                            raw: match[0],
4750                            link: match[1].trim(),
4751                            alias: match[3] ? match[3].trim() : match[1].trim()
4752                        };
4753                    }
4754                },
4755                renderer(token) {
4756                    return `<span class="wikilink">${token.alias}</span>`;
4757                }
4758            },
4759            {
4760                name: 'footnote',
4761                level: 'inline',
4762                start(src) { return src.match(/\[\^/)?.index; },
4763                tokenizer(src, tokens) {
4764                    const match = src.match(/^\[\^([^\]]+)\]/);
4765                    if (match) {
4766                        return {
4767                            type: 'footnote',
4768                            raw: match[0],
4769                            id: match[1]
4770                        };
4771                    }
4772                },
4773                renderer(token) {
4774                    return `<sup class="footnote-ref" id="fnref-${token.id}"><a href="#fn-${token.id}">${token.id}</a></sup>`;
4775                }
4776            },
4777            {
4778                name: 'footnoteDef',
4779                level: 'block',
4780                start(src) { return src.match(/^\[\^/)?.index; },
4781                tokenizer(src, tokens) {
4782                    const match = src.match(/^\[\^([^\]]+)\]:\s+(.*)(\n|$)/);
4783                    if (match) {
4784                        return {
4785                            type: 'footnoteDef',
4786                            raw: match[0],
4787                            id: match[1],
4788                            content: match[2]
4789                        };
4790                    }
4791                },
4792                renderer(token) {
4793                    if (!window._footnotes) window._footnotes = [];
4794                    window._footnotes.push(token);
4795                    return '';
4796                }
4797            }
4798        ]
4799    });
4800
4801    if (typeof markedKatex !== 'undefined') {
4802        marked.use(markedKatex({ throwOnError: false }));
4803    }
4804
4805    // Initialize Mermaid
4806    if (window.mermaid) {
4807        mermaid.initialize({
4808            startOnLoad: false,
4809            theme: 'default',
4810            flowchart: { htmlLabels: false, useMaxWidth: true, curve: 'linear' },
4811            sequence: { htmlLabels: false, useMaxWidth: true, boxMargin: 5 },
4812            gantt: { htmlLabels: false, useMaxWidth: true }
4813        });
4814    }
4815
4816    const saved = localStorage.getItem(CONFIG.STORAGE_KEY);
4817    if (saved) {
4818        try {
4819            const data = JSON.parse(saved);
4820            db = { ...db, ...data }; // Merge with defaults
4821            db.expanded = new Set(data.expanded || []);
4822            db.deletedNodes = data.deletedNodes || [];
4823
4824            // Migration: Add updatedAt to old nodes if missing
4825            db.nodes.forEach(node => {
4826                if (!node.updatedAt) node.updatedAt = Date.now();
4827            });
4828
4829            checkWelcomeUpdate();
4830        } catch (e) { initDefaultDocs(); }
4831    } else {
4832        initDefaultDocs();
4833        localStorage.setItem('MD2PDF_WELCOME_VERSION', CONFIG.WELCOME_VERSION);
4834    }
4835
4836    if (!handleUrlParams()) {
4837        if (db.activeId && db.nodes.find(n => n.id === db.activeId)) openFile(db.activeId);
4838        else if (db.nodes.length > 0) {
4839            const firstFile = db.nodes.find(n => n.type === 'file');
4840            if (firstFile) openFile(firstFile.id);
4841        }
4842    }
4843    renderTree();
4844    setupEventListeners();
4845    initResizer();
4846    initSyncScroll();
4847    licenseManager.mount();
4848
4849    if (window.innerWidth <= 1024) {
4850        // 平板和移动端默认进入编辑模式(单栏),避免两栏在小å
4850±å¹•上过窄
4851        uiActions.setView('editor');
4852    } else {
4853        uiActions.setView('split');
4854    }
4855};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.