From 7f43e45049d0e39150719a259c744914814cbb52 Mon Sep 17 00:00:00 2001 From: Jeff fu Date: Tue, 30 Dec 2025 14:56:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AF=BC=E5=87=BA=E4=B8=BA?= =?UTF-8?q?=20Word=20=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=AF=AD=E6=B3=95=E9=AB=98=E4=BA=AE=E5=92=8C?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=9D=97=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E=E5=92=8C=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/actions/export_to_docx/README.md | 38 +++-- plugins/actions/export_to_docx/README_CN.md | 40 +++-- .../actions/export_to_docx/export_to_word.py | 152 ++++++++++++++++-- plugins/actions/export_to_docx/导出为Word.py | 151 +++++++++++++++-- 4 files changed, 328 insertions(+), 53 deletions(-) diff --git a/plugins/actions/export_to_docx/README.md b/plugins/actions/export_to_docx/README.md index 1fd0228..8fd72ce 100644 --- a/plugins/actions/export_to_docx/README.md +++ b/plugins/actions/export_to_docx/README.md @@ -1,30 +1,33 @@ # Export to Word -Export current conversation from Markdown to Word (.docx) with proper Chinese and English encoding and smarter filenames. +Export current conversation from Markdown to Word (.docx) with **syntax highlighting**, **blockquote support**, and smarter filenames. ## Features - **One-Click Export**: Adds an "Export to Word" action button to the chat. - **Markdown Conversion**: Converts Markdown syntax to Word formatting (headings, bold, italic, code, tables, lists). +- **Syntax Highlighting**: Code blocks are highlighted with Pygments (supports 500+ languages). +- **Blockquote Support**: Markdown blockquotes are rendered with left border and gray styling. - **Multi-language Support**: Properly handles both Chinese and English text without garbled characters. - **Smarter Filenames**: Prefers chat title (from body or chat_id lookup) → first Markdown h1/h2 → user + date. ## Supported Markdown Syntax -| Syntax | Word Result | -| :---------------------------------- | :----------------------------- | -| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 | -| `**bold**` or `__bold__` | Bold text | -| `*italic*` or `_italic_` | Italic text | -| `***bold italic***` | Bold + Italic | -| `` `inline code` `` | Monospace with gray background | -| ` ``` code block ``` ` | Code block with indentation | -| `[link](url)` | Blue underlined link text | -| `~~strikethrough~~` | Strikethrough text | -| `- item` or `* item` | Bullet list | -| `1. item` | Numbered list | -| Markdown tables | Table with grid | -| `---` or `***` | Horizontal rule | +| Syntax | Word Result | +| :---------------------------------- | :-------------------------------- | +| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 | +| `**bold**` or `__bold__` | Bold text | +| `*italic*` or `_italic_` | Italic text | +| `***bold italic***` | Bold + Italic | +| `` `inline code` `` | Monospace with gray background | +| ` ``` code block ``` ` | **Syntax highlighted** code block | +| `> blockquote` | Left-bordered gray italic text | +| `[link](url)` | Blue underlined link text | +| `~~strikethrough~~` | Strikethrough text | +| `- item` or `* item` | Bullet list | +| `1. item` | Numbered list | +| Markdown tables | Table with grid | +| `---` or `***` | Horizontal rule | ## Usage @@ -41,7 +44,10 @@ Export current conversation from Markdown to Word (.docx) with proper Chinese an ### Requirements -- python-docx==1.1.2 (already declared in the plugin docstring; ensure installed in your environment). +- `python-docx==1.1.2` - Word document generation +- `Pygments>=2.15.0` - Syntax highlighting (optional but recommended) + +Both are declared in the plugin docstring; ensure they are installed in your environment. ## Font Configuration diff --git a/plugins/actions/export_to_docx/README_CN.md b/plugins/actions/export_to_docx/README_CN.md index af70e4c..9965341 100644 --- a/plugins/actions/export_to_docx/README_CN.md +++ b/plugins/actions/export_to_docx/README_CN.md @@ -1,30 +1,33 @@ # 导出为 Word -将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码,且具备更智能的文件命名。 +将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持**代码语法高亮**、**引用块样式**和更智能的文件命名。 ## 功能特点 -- **一键导出**:在聊天界面添加"导出为 Word"动作按钮。 +- **一键导出**:在聊天界面添加“导出为 Word”动作按钮。 - **Markdown 转换**:将 Markdown 语法转换为 Word 格式(标题、粗体、斜体、代码、表格、列表)。 +- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。 +- **引用块支持**:Markdown 引用块会渲染为带左侧边框的灰色斜体样式。 - **多语言支持**:正确处理中文和英文文本,无乱码问题。 - **更智能的文件名**:优先使用对话标题(来自请求体或基于 chat_id 查询),其次 Markdown 一级/二级标题,最后用户+日期。 ## 支持的 Markdown 语法 -| 语法 | Word 效果 | -| :-------------------------- | :------------------ | -| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 | -| `**粗体**` 或 `__粗体__` | 粗体文本 | -| `*斜体*` 或 `_斜体_` | 斜体文本 | -| `***粗斜体***` | 粗体 + 斜体 | -| `` `行内代码` `` | 等宽字体 + 灰色背景 | -| ` ``` 代码块 ``` ` | 带缩进的代码块 | -| `[链接](url)` | 蓝色下划线链接文本 | -| `~~删除线~~` | 删除线文本 | -| `- 项目` 或 `* 项目` | 无序列表 | -| `1. 项目` | 有序列表 | -| Markdown 表格 | 带边框表格 | -| `---` 或 `***` | 水平分割线 | +| 语法 | Word 效果 | +| :-------------------------- | :----------------------- | +| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 | +| `**粗体**` 或 `__粗体__` | 粗体文本 | +| `*斜体*` 或 `_斜体_` | 斜体文本 | +| `***粗斜体***` | 粗体 + 斜体 | +| `` `行内代码` `` | 等宽字体 + 灰色背景 | +| ` ``` 代码块 ``` ` | **语法高亮**的代码块 | +| `> 引用文本` | 带左侧边框的灰色斜体文本 | +| `[链接](url)` | 蓝色下划线链接文本 | +| `~~删除线~~` | 删除线文本 | +| `- 项目` 或 `* 项目` | 无序列表 | +| `1. 项目` | 有序列表 | +| Markdown 表格 | 带边框表格 | +| `---` 或 `***` | 水平分割线 | ## 使用方法 @@ -40,7 +43,10 @@ ### 依赖 -- python-docx==1.1.2(已在插件文档字符串中声明,请确保环境已安装)。 +- `python-docx==1.1.2` - Word 文档生成 +- `Pygments>=2.15.0` - 语法高亮(可选但建议安装) + +两者已在插件文档字符串中声明,请确保环境已安装。 ## 字体配置 diff --git a/plugins/actions/export_to_docx/export_to_word.py b/plugins/actions/export_to_docx/export_to_word.py index e3dfb79..7f49d2a 100644 --- a/plugins/actions/export_to_docx/export_to_word.py +++ b/plugins/actions/export_to_docx/export_to_word.py @@ -5,8 +5,8 @@ author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui version: 0.1.0 icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K -requirements: python-docx==1.1.2 -description: Export current conversation from Markdown to Word (.docx) file with proper Chinese and English encoding. +requirements: python-docx==1.1.2, Pygments>=2.15.0 +description: Export current conversation from Markdown to Word (.docx) file with syntax highlighting and blockquote support. """ import os @@ -26,6 +26,16 @@ from docx.oxml.ns import qn from docx.oxml import OxmlElement from open_webui.models.chats import Chats +# Pygments for syntax highlighting +try: + from pygments import lex + from pygments.lexers import get_lexer_by_name, TextLexer + from pygments.token import Token + + PYGMENTS_AVAILABLE = True +except ImportError: + PYGMENTS_AVAILABLE = False + logging.basicConfig( level=logging.INFO, @@ -375,6 +385,24 @@ class Action: i += 1 continue + # Handle blockquotes + if line.strip().startswith(">"): + # Process pending list first + if in_list and list_items: + self.add_list_to_doc(doc, list_items, list_type) + list_items = [] + in_list = False + + # Collect consecutive quote lines + blockquote_lines = [] + while i < len(lines) and lines[i].strip().startswith(">"): + # Remove leading > and optional space + quote_line = re.sub(r"^>\s?", "", lines[i]) + blockquote_lines.append(quote_line) + i += 1 + self.add_blockquote(doc, "\n".join(blockquote_lines)) + continue + # Handle horizontal rules if re.match(r"^[-*_]{3,}$", line.strip()): # Process pending list first @@ -552,23 +580,93 @@ class Action: paragraph.add_run(text[pos:]) def add_code_block(self, doc: Document, code: str, language: str = ""): - """Add code block""" + """Add code block with syntax highlighting""" + # Token color mapping (based on common IDE themes) + TOKEN_COLORS = { + Token.Keyword: RGBColor(0, 0, 255), # Blue - keywords + Token.Keyword.Constant: RGBColor(0, 0, 255), + Token.Keyword.Declaration: RGBColor(0, 0, 255), + Token.Keyword.Namespace: RGBColor(0, 0, 255), + Token.Keyword.Type: RGBColor(0, 0, 255), + Token.Name.Function: RGBColor(136, 18, 128), # Purple - function names + Token.Name.Class: RGBColor(38, 127, 153), # Cyan - class names + Token.Name.Decorator: RGBColor(255, 128, 0), # Orange - decorators + Token.Name.Builtin: RGBColor(0, 112, 32), # Green - builtins + Token.String: RGBColor(163, 21, 21), # Red - strings + Token.String.Doc: RGBColor(128, 128, 128), # Gray - docstrings + Token.Comment: RGBColor(128, 128, 128), # Gray - comments + Token.Comment.Single: RGBColor(128, 128, 128), + Token.Comment.Multiline: RGBColor(128, 128, 128), + Token.Number: RGBColor(9, 134, 88), # Green - numbers + Token.Number.Integer: RGBColor(9, 134, 88), + Token.Number.Float: RGBColor(9, 134, 88), + Token.Operator: RGBColor(104, 118, 135), # Gray-blue - operators + Token.Punctuation: RGBColor(64, 64, 64), # Dark gray - punctuation + } + + def get_token_color(token_type): + """Recursively find token color""" + while token_type: + if token_type in TOKEN_COLORS: + return TOKEN_COLORS[token_type] + token_type = token_type.parent + return None + + # Add language label if available + if language: + lang_para = doc.add_paragraph() + lang_para.paragraph_format.space_before = Pt(6) + lang_para.paragraph_format.space_after = Pt(0) + lang_para.paragraph_format.left_indent = Cm(0.5) + lang_run = lang_para.add_run(language.upper()) + lang_run.font.name = "Consolas" + lang_run.font.size = Pt(8) + lang_run.font.color.rgb = RGBColor(100, 100, 100) + lang_run.font.bold = True + + # Add code block paragraph paragraph = doc.add_paragraph() paragraph.paragraph_format.left_indent = Cm(0.5) - paragraph.paragraph_format.space_before = Pt(6) + paragraph.paragraph_format.space_before = Pt(3) if language else Pt(6) paragraph.paragraph_format.space_after = Pt(6) - # Set code block font - run = paragraph.add_run(code) - run.font.name = "Consolas" - run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") - run.font.size = Pt(10) - # Add light gray background shading = OxmlElement("w:shd") shading.set(qn("w:fill"), "F5F5F5") paragraph._element.pPr.append(shading) + # Try to use Pygments for syntax highlighting + if PYGMENTS_AVAILABLE and language: + try: + lexer = get_lexer_by_name(language, stripall=False) + except Exception: + lexer = TextLexer() + + tokens = list(lex(code, lexer)) + + for token_type, token_value in tokens: + if not token_value: + continue + run = paragraph.add_run(token_value) + run.font.name = "Consolas" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") + run.font.size = Pt(10) + + # Apply color + color = get_token_color(token_type) + if color: + run.font.color.rgb = color + + # Bold keywords + if token_type in Token.Keyword: + run.font.bold = True + else: + # No syntax highlighting, plain text display + run = paragraph.add_run(code) + run.font.name = "Consolas" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") + run.font.size = Pt(10) + def add_table(self, doc: Document, table_lines: List[str]): """Add table""" if len(table_lines) < 2: @@ -661,3 +759,37 @@ class Action: bottom.set(qn("w:color"), "auto") pBdr.append(bottom) pPr.append(pBdr) + + def add_blockquote(self, doc: Document, text: str): + """Add blockquote with left border and gray background""" + for line in text.split("\n"): + paragraph = doc.add_paragraph() + paragraph.paragraph_format.left_indent = Cm(1.0) + paragraph.paragraph_format.space_before = Pt(3) + paragraph.paragraph_format.space_after = Pt(3) + + # Add left border + pPr = paragraph._element.get_or_add_pPr() + pBdr = OxmlElement("w:pBdr") + left = OxmlElement("w:left") + left.set(qn("w:val"), "single") + left.set(qn("w:sz"), "24") # Border thickness + left.set(qn("w:space"), "4") # Space between border and text + left.set(qn("w:color"), "CCCCCC") # Gray border + pBdr.append(left) + pPr.append(pBdr) + + # Add light gray background + shading = OxmlElement("w:shd") + shading.set(qn("w:fill"), "F9F9F9") + pPr.append(shading) + + # Add formatted text + self.add_formatted_text(paragraph, line) + + # Set font to italic gray + for run in paragraph.runs: + run.font.name = "Times New Roman" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "KaiTi") + run.font.color.rgb = RGBColor(85, 85, 85) # Dark gray text + run.italic = True diff --git a/plugins/actions/export_to_docx/导出为Word.py b/plugins/actions/export_to_docx/导出为Word.py index 5f76fc8..ab73a1c 100644 --- a/plugins/actions/export_to_docx/导出为Word.py +++ b/plugins/actions/export_to_docx/导出为Word.py @@ -5,8 +5,8 @@ author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui version: 0.1.0 icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K -requirements: python-docx==1.1.2 -description: 将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码。 +requirements: python-docx==1.1.2, Pygments>=2.15.0 +description: 将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持代码语法高亮和引用块。 """ import os @@ -26,6 +26,16 @@ from docx.oxml.ns import qn from docx.oxml import OxmlElement from open_webui.models.chats import Chats +# Pygments for syntax highlighting +try: + from pygments import lex + from pygments.lexers import get_lexer_by_name, TextLexer + from pygments.token import Token + + PYGMENTS_AVAILABLE = True +except ImportError: + PYGMENTS_AVAILABLE = False + logging.basicConfig( level=logging.INFO, @@ -370,6 +380,24 @@ class Action: i += 1 continue + # 处理引用块 + if line.strip().startswith(">"): + # 先处理之前积累的列表 + if in_list and list_items: + self.add_list_to_doc(doc, list_items, list_type) + list_items = [] + in_list = False + + # 收集连续的引用行 + blockquote_lines = [] + while i < len(lines) and lines[i].strip().startswith(">"): + # 移除开头的 > 和可能的空格 + quote_line = re.sub(r"^>\s?", "", lines[i]) + blockquote_lines.append(quote_line) + i += 1 + self.add_blockquote(doc, "\n".join(blockquote_lines)) + continue + # 处理水平分割线 if re.match(r"^[-*_]{3,}$", line.strip()): # 先处理之前积累的列表 @@ -551,24 +579,93 @@ class Action: paragraph.add_run(text[pos:]) def add_code_block(self, doc: Document, code: str, language: str = ""): - """添加代码块""" + """添加代码块,支持语法高亮""" + # 语法高亮颜色映射 (基于常见的 IDE 配色) + TOKEN_COLORS = { + Token.Keyword: RGBColor(0, 0, 255), # 蓝色 - 关键字 + Token.Keyword.Constant: RGBColor(0, 0, 255), + Token.Keyword.Declaration: RGBColor(0, 0, 255), + Token.Keyword.Namespace: RGBColor(0, 0, 255), + Token.Keyword.Type: RGBColor(0, 0, 255), + Token.Name.Function: RGBColor(136, 18, 128), # 紫色 - 函数名 + Token.Name.Class: RGBColor(38, 127, 153), # 青色 - 类名 + Token.Name.Decorator: RGBColor(255, 128, 0), # 橙色 - 装饰器 + Token.Name.Builtin: RGBColor(0, 112, 32), # 绿色 - 内置函数 + Token.String: RGBColor(163, 21, 21), # 红色 - 字符串 + Token.String.Doc: RGBColor(128, 128, 128), # 灰色 - 文档字符串 + Token.Comment: RGBColor(128, 128, 128), # 灰色 - 注释 + Token.Comment.Single: RGBColor(128, 128, 128), + Token.Comment.Multiline: RGBColor(128, 128, 128), + Token.Number: RGBColor(9, 134, 88), # 绿色 - 数字 + Token.Number.Integer: RGBColor(9, 134, 88), + Token.Number.Float: RGBColor(9, 134, 88), + Token.Operator: RGBColor(104, 118, 135), # 灰蓝色 - 运算符 + Token.Punctuation: RGBColor(64, 64, 64), # 深灰 - 标点 + } + + def get_token_color(token_type): + """递归查找 token 颜色""" + while token_type: + if token_type in TOKEN_COLORS: + return TOKEN_COLORS[token_type] + token_type = token_type.parent + return None + + # 添加语言标签(如果有) + if language: + lang_para = doc.add_paragraph() + lang_para.paragraph_format.space_before = Pt(6) + lang_para.paragraph_format.space_after = Pt(0) + lang_para.paragraph_format.left_indent = Cm(0.5) + lang_run = lang_para.add_run(language.upper()) + lang_run.font.name = "Consolas" + lang_run.font.size = Pt(8) + lang_run.font.color.rgb = RGBColor(100, 100, 100) + lang_run.font.bold = True + # 添加代码块段落 paragraph = doc.add_paragraph() paragraph.paragraph_format.left_indent = Cm(0.5) - paragraph.paragraph_format.space_before = Pt(6) + paragraph.paragraph_format.space_before = Pt(3) if language else Pt(6) paragraph.paragraph_format.space_after = Pt(6) - # 设置代码块背景 - run = paragraph.add_run(code) - run.font.name = "Consolas" - run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") - run.font.size = Pt(10) - # 添加浅灰色背景 shading = OxmlElement("w:shd") shading.set(qn("w:fill"), "F5F5F5") paragraph._element.pPr.append(shading) + # 尝试使用 Pygments 进行语法高亮 + if PYGMENTS_AVAILABLE and language: + try: + lexer = get_lexer_by_name(language, stripall=False) + except Exception: + lexer = TextLexer() + + tokens = list(lex(code, lexer)) + + for token_type, token_value in tokens: + if not token_value: + continue + run = paragraph.add_run(token_value) + run.font.name = "Consolas" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") + run.font.size = Pt(10) + + # 应用颜色 + color = get_token_color(token_type) + if color: + run.font.color.rgb = color + + # 关键字加粗 + if token_type in Token.Keyword: + run.font.bold = True + else: + # 无语法高亮,纯文本显示 + run = paragraph.add_run(code) + run.font.name = "Consolas" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei") + run.font.size = Pt(10) + def add_table(self, doc: Document, table_lines: List[str]): """添加表格""" if len(table_lines) < 2: @@ -661,3 +758,37 @@ class Action: bottom.set(qn("w:color"), "auto") pBdr.append(bottom) pPr.append(pBdr) + + def add_blockquote(self, doc: Document, text: str): + """添加引用块,带有左侧边框和灰色背景""" + for line in text.split("\n"): + paragraph = doc.add_paragraph() + paragraph.paragraph_format.left_indent = Cm(1.0) + paragraph.paragraph_format.space_before = Pt(3) + paragraph.paragraph_format.space_after = Pt(3) + + # 添加左侧边框 + pPr = paragraph._element.get_or_add_pPr() + pBdr = OxmlElement("w:pBdr") + left = OxmlElement("w:left") + left.set(qn("w:val"), "single") + left.set(qn("w:sz"), "24") # 边框粗细 + left.set(qn("w:space"), "4") # 边框与文字间距 + left.set(qn("w:color"), "CCCCCC") # 灰色边框 + pBdr.append(left) + pPr.append(pBdr) + + # 添加浅灰色背景 + shading = OxmlElement("w:shd") + shading.set(qn("w:fill"), "F9F9F9") + pPr.append(shading) + + # 添加格式化文本 + self.add_formatted_text(paragraph, line) + + # 设置字体为斜体灰色 + for run in paragraph.runs: + run.font.name = "Times New Roman" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "楷体") + run.font.color.rgb = RGBColor(85, 85, 85) # 深灰色文字 + run.italic = True