From a17d62f3ec274570b644db53c99055124eb842a0 Mon Sep 17 00:00:00 2001 From: suyao Date: Fri, 12 Sep 2025 07:53:26 +0800 Subject: [PATCH] feat: enhance PDF export functionality with syntax highlighting and localized messages --- src/main/constant.ts | 4 +- src/renderer/src/i18n/locales/en-us.json | 6 ++ src/renderer/src/i18n/locales/zh-cn.json | 6 ++ src/renderer/src/i18n/locales/zh-tw.json | 6 ++ .../src/pages/notes/utils/exportUtils.ts | 70 ++++++++++++++++++- 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/main/constant.ts b/src/main/constant.ts index d1a9e9b67c..a2107caa5c 100644 --- a/src/main/constant.ts +++ b/src/main/constant.ts @@ -29,8 +29,8 @@ export const PRINT_HTML_TEMPLATE = ` {{richtextCss}} - -
+ +
{{content}}
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 9fbe04c4a2..ecc34508aa 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1389,6 +1389,9 @@ "no_api_key": "Notion ApiKey or Notion DatabaseID is not configured", "no_content": "There is nothing to export to Notion." }, + "pdf": { + "export": "Failed to export to PDF" + }, "siyuan": { "export": "Failed to export to Siyuan Note, please check connection status and configuration according to documentation", "no_config": "Siyuan Note API address or token is not configured" @@ -1491,6 +1494,9 @@ "notion": { "export": "Successfully exported to Notion" }, + "pdf": { + "export": "Successfully exported to PDF" + }, "siyuan": { "export": "Successfully exported to Siyuan Note" }, diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8ec7bcf361..8f0ca9bd0e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1390,6 +1390,9 @@ "no_api_key": "未配置 Notion API Key 或 Notion Database ID", "no_content": "无可导出到 Notion 的内容" }, + "pdf": { + "export": "导出 PDF 失败" + }, "siyuan": { "export": "导出思源笔记失败,请检查连接状态并对照文档检查配置", "no_config": "未配置思源笔记 API 地址或令牌" @@ -1492,6 +1495,9 @@ "notion": { "export": "成功导出到 Notion" }, + "pdf": { + "export": "成功导出为 PDF" + }, "siyuan": { "export": "导出到思源笔记成功" }, diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 2a7db994b8..7f2d7e6be8 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1389,6 +1389,9 @@ "no_api_key": "未設定 Notion API Key 或 Notion Database ID", "no_content": "沒有可匯出至 Notion 的內容" }, + "pdf": { + "export": "導出 PDF 失敗" + }, "siyuan": { "export": "導出思源筆記失敗,請檢查連接狀態並對照文檔檢查配置", "no_config": "未配置思源筆記 API 地址或令牌" @@ -1491,6 +1494,9 @@ "notion": { "export": "成功匯出到 Notion" }, + "pdf": { + "export": "成功導出為 PDF" + }, "siyuan": { "export": "導出到思源筆記成功" }, diff --git a/src/renderer/src/pages/notes/utils/exportUtils.ts b/src/renderer/src/pages/notes/utils/exportUtils.ts index 3bdf3972fa..fbce660856 100644 --- a/src/renderer/src/pages/notes/utils/exportUtils.ts +++ b/src/renderer/src/pages/notes/utils/exportUtils.ts @@ -1,5 +1,7 @@ import { loggerService } from '@logger' import { RichEditorRef } from '@renderer/components/RichEditor/types' +import i18n from '@renderer/i18n' +import { getHighlighter } from '@renderer/utils/shiki' import { RefObject } from 'react' const logger = loggerService.withContext('exportUtils') @@ -10,21 +12,85 @@ export interface ExportContext { fileName: string } +/** + * 添加语法高亮 + * @param html 原始 HTML + * @returns 添加语法高亮后的 HTML + */ +const addSyntaxHighlighting = async (html: string): Promise => { + try { + const highlighter = await getHighlighter() + const parser = new DOMParser() + const doc = parser.parseFromString(html, 'text/html') + + const codeBlocks = doc.querySelectorAll('pre code') + + for (const codeElement of codeBlocks) { + const preElement = codeElement.parentElement as HTMLPreElement + const codeText = codeElement.textContent || '' + + if (!codeText.trim()) continue + + let languageMatch = preElement.className.match(/language-(\w+)/) + if (!languageMatch) { + languageMatch = codeElement.className.match(/language-(\w+)/) + } + const language = languageMatch ? languageMatch[1] : 'text' + + // Skip highlighting for plain text + if (language === 'text' || language === 'plain' || language === 'plaintext') { + continue + } + + try { + const loadedLanguages = highlighter.getLoadedLanguages() + + if (loadedLanguages.includes(language)) { + const highlightedHtml = highlighter.codeToHtml(codeText, { + lang: language, + theme: 'one-light' + }) + + const tempDoc = parser.parseFromString(highlightedHtml, 'text/html') + const highlightedCode = tempDoc.querySelector('code') + + if (highlightedCode) { + codeElement.innerHTML = highlightedCode.innerHTML + } + } + } catch (error) { + logger.warn(`Failed to highlight ${language} code block:`, error as Error) + } + } + + return doc.documentElement.outerHTML + } catch (error) { + logger.warn('Failed to add syntax highlighting, using original HTML:', error as Error) + return html + } +} + export const handleExportPDF = async (context: ExportContext) => { if (!context.editorRef?.current || !context.currentContent?.trim()) { return } try { - // Use Tiptap's getHTML API to get HTML content - const htmlContent = context.editorRef.current.getHtml() + let htmlContent = context.editorRef.current.getHtml() + + htmlContent = await addSyntaxHighlighting(htmlContent) + const filename = context.fileName ? `${context.fileName}.pdf` : 'note.pdf' const result = await window.api.export.toPDF(htmlContent, filename) if (result.success) { logger.info('PDF exported successfully to:', result.filePath) + window.toast.success(i18n.t('message.success.pdf.export')) } else { logger.error('PDF export failed:', result.message) + if (!result.message.includes('canceled')) { + window.toast.error(i18n.t('message.error.pdf.export', { message: result.message })) + } } } catch (error: any) { logger.error('PDF export error:', error)