
Md2word
- 6 installs
- 196 repo stars
- Updated April 5, 2026
- cat-xierluo/suitagent
Converts Markdown documents into professionally formatted Word documents that follow Chinese typesetting standards, with multiple preset formats.
About
Converts Markdown into Word documents formatted to Chinese typesetting standards with preset format options. Developers use it to produce formal documents, papers, and reports needing standardized layout from markdown.
- Full Markdown syntax with auto professional formatting
- Multiple preset format templates
Md2word by the numbers
- 6 all-time installs (skills.sh)
- Ranked #520 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cat-xierluo/suitagent --skill md2wordAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 196 |
| Last updated | April 5, 2026 |
| Repository | cat-xierluo/suitagent ↗ |
What it does
Converts Markdown documents into professionally formatted Word documents that follow Chinese typesetting standards, with multiple preset formats.
Files
Markdown转Word文档Skill
概述
将 Markdown 文档转换为符合中文排版标准的 Word 文档。支持完整的 Markdown 语法,自动应用专业格式设置。
依赖要求
Python 依赖
pip install python-docx Pillow beautifulsoup4 PyYAML可选依赖
npm install -g @mermaid-js/mermaid-cli快速开始
主转换脚本:scripts/md2word.py
# 基本转换
python scripts/md2word.py input.md output.docx
# 使用预设格式
python scripts/md2word.py input.md --preset=academic
# 使用自定义配置
python scripts/md2word.py input.md --config=my-config.yaml配置系统
内置预设
预设信息从 YAML 文件动态读取,运行以下命令查看完整列表:
python scripts/config.py --list常用预设:
- legal — 法律文书格式(默认)
- service-plan — 法律服务方案(含分层配色)
- minimal — 极简格式
- academic — 学术论文格式
- report — 工作报告格式
完整配置见assets/presets/*.yaml,设计说明见assets/theme-notes/
自定义配置
复制配置模板并修改:
cp assets/config-template.yaml my-config.yamlWord 模板文件
将 .docx 模板放入 assets/templates/ 目录,或使用 --template 指定。
Word 模板 vs 配置文件:
- Word 模板:控制视觉元素(页眉、页脚、Logo)
- 配置文件:控制格式参数(字号、行距、页边距)
参考文档
- 配置参考: references/config-reference.md
- 样式映射: references/style-mappings.md
- 使用示例: references/examples.md
错误处理
- 文件编码:自动检测 UTF-8 和 GBK
- 模板找不到:使用默认格式创建新文档
- Mermaid 失败:降级为文本描述
- 图片过大:自动压缩和调整尺寸
目录结构
md2word/
├── SKILL.md # 本文档
├── CHANGELOG.md # 版本记录
├── references/ # 参考文档
│ ├── config-reference.md
│ ├── style-mappings.md
│ └── examples.md
├── scripts/ # 转换脚本
│ ├── md2word.py # 主脚本
│ ├── config.py # 配置模块(含 --list 查看预设)
│ ├── extract_template_config.py # 从 Word 模板提取配置
│ ├── formatter.py # 文本格式化模块
│ ├── table_handler.py # 表格处理模块
│ └── chart_handler.py # 图表渲染模块
└── assets/ # 资源文件
├── presets/ # YAML 预设配置
├── theme-notes/ # 预设设计说明文档
├── templates/ # Word 模板文件
└── config-template.yaml# md2word 配置模板
#
# 使用方法:
# 1. 复制此文件到你的工作目录
# 2. 重命名为 my-config.yaml 或任意名称
# 3. 修改下面的配置参数
# 4. 使用:python md2word.py input.md --config=my-config.yaml
#
# 提示:
# - 所有数值单位:页面尺寸为 cm,字体大小为 pt,间距为 pt(1pt ≈ 0.376mm)
# - 对齐方式:left(左对齐)、center(居中)、right(右对齐)、justify(两端对齐)
# - 颜色格式:十六进制,如 "#000000" 为黑色
name: "自定义格式"
description: "用户自定义格式配置"
# ==================== 页面设置 ====================
page:
width: 21.0 # 页面宽度 (cm) - A4 标准为 21.0
height: 29.7 # 页面高度 (cm) - A4 标准为 29.7
margin_top: 2.54 # 上边距 (cm)
margin_bottom: 2.54 # 下边距 (cm)
margin_left: 3.18 # 左边距 (cm)
margin_right: 3.18 # 右边距 (cm)
# ==================== 字体设置 ====================
fonts:
default:
name: "仿宋_GB2312" # 中文字体名称
ascii: "Times New Roman" # 英文字体名称
size: 12 # 字号 (pt) - 常用:小四=12, 五号=10.5, 四号=14, 三号=16, 二号=18, 小三=15
color: "#000000" # 字体颜色 (十六进制)
# ==================== 标题设置 ====================
titles:
level1: # 一级标题 (# 标题)
size: 15 # 字号 (pt)
bold: true # 是否加粗
align: "center" # 对齐方式
space_before: 6 # 段前间距 (pt)
space_after: 6 # 段后间距 (pt)
indent: 0 # 首行缩进 (pt)
level2: # 二级标题 (## 标题)
size: 12
bold: true
align: "justify"
indent: 24
level3: # 三级标题 (### 标题)
size: 12
bold: false
align: "justify"
indent: 24
level4: # 四级标题 (#### 标题)
size: 12
bold: false
align: "justify"
indent: 24
# ==================== 段落设置 ====================
paragraph:
line_spacing: 1.5 # 行距倍数(1.0=单倍,1.5=1.5倍,2.0=双倍)
first_line_indent: 24 # 首行缩进 (pt) - 24pt ≈ 2个字符
align: "justify" # 对齐方式
# ==================== 页码设置 ====================
page_number:
enabled: true # 是否启用页码
format: "1/x" # 页码格式("1", "x", "1/x" 等)
font: "Times New Roman" # 页码字体
size: 10.5 # 页码字号 (pt)
position: "center" # 页码位置 (left/center/right)
# ==================== 引号设置 ====================
quotes:
convert_to_chinese: true # 是否自动转换英文引号为中文引号
# ==================== 表格设置 ====================
table:
border_enabled: true # 是否启用边框
border_color: "#000000" # 边框颜色
border_width: 4 # 边框宽度 (1/8 pt)
line_spacing: 1.2 # 表格内行距
row_height_cm: 0.8 # 行高 (cm)
alignment: "center" # 表格对齐方式 (left/center/right)
cell_margin:
top: 30 # 单元格上边距 (dxa, 1dxa ≈ 0.0176mm)
bottom: 30 # 单元格下边距
left: 60 # 单元格左边距
right: 60 # 单元格右边距
vertical_align: "center" # 垂直对齐 (top/center/bottom)
# 表格标题行(第一行)格式
header:
font: "Times New Roman" # 标题行字体
size: 10.5 # 标题行字号 (pt) - 五号
bold: true # 是否加粗
color: "#000000" # 标题行颜色
# 表格正文格式
body:
font: "仿宋_GB2312" # 正文字体
size: 10.5 # 正文字号 (pt)
color: "#000000" # 正文字色
# ==================== 代码块设置 ====================
code_block:
# 语言标签格式(代码块开头的 [language] 标签)
label:
font: "Times New Roman"
size: 10 # 标签字号 (pt)
color: "#808080" # 灰色 (128,128,128)
# 代码内容格式
content:
font: "Times New Roman"
size: 10 # 代码字号 (pt)
color: "#333333" # 深灰色 (51,51,51)
left_indent: 24 # 左缩进 (pt)
line_spacing: 1.2 # 行距
# ==================== 行内代码设置 ====================
inline_code:
font: "Times New Roman"
size: 10 # 字号 (pt)
color: "#333333" # 深灰色 (51,51,51)
# ==================== 引用块设置 ====================
quote:
background_color: "#EAEAEA" # 背景填充色 (浅灰色)
left_indent_inches: 0.2 # 左侧缩进 (英寸)
font_size: 9 # 字号 (pt) - 小五号
line_spacing: 1.5 # 行距
# ==================== 数学公式设置 ====================
math:
font: "Times New Roman"
size: 11 # 字号 (pt)
italic: true # 是否斜体
color: "#00008B" # 深蓝色 (0,0,139)
# ==================== 图片设置 ====================
image:
display_ratio: 0.92 # 相对于页面可用宽度的显示比例
max_width_cm: 14.2 # 最大显示宽度 (cm)
target_dpi: 260 # 目标DPI(用于图像压缩)
show_caption: true # 是否显示图片标题
# ==================== 分割线设置 ====================
horizontal_rule:
character: "─" # 分割线字符
repeat_count: 55 # 重复次数
font: "Times New Roman"
size: 12 # 字号 (pt)
color: "#808080" # 灰色 (128,128,128)
alignment: "center" # 对齐方式
# ==================== 列表设置 ====================
lists:
bullet:
marker: "•" # 无序列表标记
indent: 24 # 缩进 (pt)
numbered:
indent: 24 # 缩进 (pt)
preserve_format: true # 保持原有编号格式(1. 2. 3.)
task:
unchecked: "☐" # 未完成任务标记
checked: "☑" # 已完成任务标记
name: "学术论文格式"
description: "标准学术论文格式,宋体,小四号,双倍行距"
# 页面设置
page:
width: 21.0
height: 29.7
margin_top: 2.54
margin_bottom: 2.54
margin_left: 3.0
margin_right: 2.5
# 字体设置
fonts:
default:
name: "宋体"
ascii: "Times New Roman"
size: 12 # pt (小四)
color: "#000000"
# 标题设置
titles:
level1:
size: 18 # pt (二号)
bold: true
align: "center"
space_before: 12
space_after: 12
indent: 0
level2:
size: 14 # pt (四号)
bold: true
align: "left"
indent: 0
level3:
size: 12 # pt (小四)
bold: true
align: "left"
indent: 0
level4:
size: 12 # pt (小四)
bold: false
align: "left"
indent: 0
# 段落设置
paragraph:
line_spacing: 2.0 # 双倍行距
first_line_indent: 24
align: "justify"
# 页码设置
page_number:
enabled: true
format: "1/x"
font: "Times New Roman"
size: 10.5
position: "center"
# 引号设置
quotes:
convert_to_chinese: true
# 表格设置
table:
border_enabled: true
border_color: "#000000"
border_width: 4
line_spacing: 1.2
row_height_cm: 0.8
alignment: "center"
cell_margin:
top: 30
bottom: 30
left: 60
right: 60
vertical_align: "center"
# 表格标题行(第一行)格式
header:
font: "Times New Roman"
size: 10.5
bold: true
color: "#000000"
# 表格正文格式
body:
font: "宋体"
size: 10.5
color: "#000000"
# 代码块设置
code_block:
# 语言标签格式(代码块开头的 [language] 标签)
label:
font: "Times New Roman"
size: 10
color: "#808080"
# 代码内容格式
content:
font: "Times New Roman"
size: 10
color: "#333333"
left_indent: 24
line_spacing: 1.2
# 行内代码设置
inline_code:
font: "Times New Roman"
size: 10
color: "#333333"
# 引用块设置
quote:
background_color: "#EAEAEA"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5
# 数学公式设置
math:
font: "Times New Roman"
size: 11
italic: true
color: "#00008B"
# 图片设置
image:
display_ratio: 0.92
max_width_cm: 14.2
target_dpi: 260
show_caption: true
# 分割线设置
horizontal_rule:
character: "─"
repeat_count: 55
font: "Times New Roman"
size: 12
color: "#808080"
alignment: "center"
# 列表设置
lists:
bullet:
marker: "•"
indent: 24
numbered:
indent: 24
preserve_format: true
task:
unchecked: "☐"
checked: "☑"
name: "法律文书格式"
description: "标准法律文书格式,仿宋_GB2312,小四号,1.5倍行距"
# 页面设置
page:
width: 21.0 # cm (A4)
height: 29.7 # cm (A4)
margin_top: 2.54
margin_bottom: 2.54
margin_left: 3.18
margin_right: 3.18
# 字体设置
fonts:
default:
name: "仿宋"
name_alt: "仿宋_GB2312"
ascii: "Times New Roman"
size: 12 # pt (小四)
color: "#000000"
# 标题设置
titles:
level1:
size: 15 # pt (小三)
bold: true
align: "center"
space_before: 6
space_after: 6
indent: 0
level2:
size: 12 # pt (小四)
bold: true
align: "justify"
indent: 24
space_before: 9 # 0.5行距 (12pt × 1.5 × 0.5)
space_after: 9 # 0.5行距
level3:
size: 12 # pt (小四)
bold: true
align: "justify"
indent: 24
space_before: 9 # 0.5行距
space_after: 9 # 0.5行距
level4:
size: 12 # pt (小四)
bold: true
align: "justify"
indent: 24
# 段落设置
paragraph:
line_spacing: 1.5
first_line_indent: 24 # 2个字符
align: "justify"
# 页码设置
page_number:
enabled: true
format: "1/x"
font: "Times New Roman"
size: 10.5 # pt (五号)
position: "center"
# 引号设置
quotes:
convert_to_chinese: true
# 表格设置
table:
border_enabled: true
border_color: "#000000"
border_width: 4
line_spacing: 1.2
row_height_cm: 0.8
alignment: "center"
cell_margin:
top: 30
bottom: 30
left: 60
right: 60
vertical_align: "center"
# 表格标题行(第一行)格式
header:
font: "Times New Roman"
size: 10.5
bold: true
color: "#000000"
# 表格正文格式
body:
font: "仿宋"
font_alt: "仿宋_GB2312"
size: 10.5
color: "#000000"
# 代码块设置
code_block:
# 语言标签格式(代码块开头的 [language] 标签)
label:
font: "Times New Roman"
size: 10
color: "#808080"
# 代码内容格式
content:
font: "Times New Roman"
size: 10
color: "#333333"
left_indent: 24
line_spacing: 1.2
# 行内代码设置
inline_code:
font: "Times New Roman"
size: 10
color: "#333333"
# 引用块设置
quote:
background_color: "#EAEAEA"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5
# 数学公式设置
math:
font: "Times New Roman"
size: 11
italic: true
color: "#00008B"
# 图片设置
image:
display_ratio: 0.92
max_width_cm: 14.2
target_dpi: 260
show_caption: true
# 分割线设置
horizontal_rule:
character: "─"
repeat_count: 55
font: "Times New Roman"
size: 12
color: "#808080"
alignment: "center"
# 列表设置
lists:
bullet:
marker: "•"
indent: 24
numbered:
indent: 24
preserve_format: true
task:
unchecked: "☐"
checked: "☑"
name: "简单文档格式"
description: "简单文档格式,无特殊要求,适合一般用途"
# 页面设置
page:
width: 21.0
height: 29.7
margin_top: 2.54
margin_bottom: 2.54
margin_left: 3.18
margin_right: 3.18
# 字体设置
fonts:
default:
name: "仿宋_GB2312"
ascii: "Times New Roman"
size: 12
color: "#000000"
# 标题设置
titles:
level1:
size: 15
bold: true
align: "center"
space_before: 6
space_after: 6
indent: 0
level2:
size: 12
bold: true
align: "left"
indent: 0
level3:
size: 12
bold: false
align: "left"
indent: 0
level4:
size: 12
bold: false
align: "left"
indent: 0
# 段落设置
paragraph:
line_spacing: 1.5
first_line_indent: 24
align: "justify"
# 页码设置
page_number:
enabled: true
format: "1/x"
font: "Times New Roman"
size: 10.5
position: "center"
# 引号设置
quotes:
convert_to_chinese: true
# 表格设置
table:
border_enabled: true
border_color: "#000000"
border_width: 4
line_spacing: 1.2
row_height_cm: 0.8
alignment: "center"
cell_margin:
top: 30
bottom: 30
left: 60
right: 60
vertical_align: "center"
# 表格标题行(第一行)格式
header:
font: "Times New Roman"
size: 10.5
bold: true
color: "#000000"
# 表格正文格式
body:
font: "仿宋_GB2312"
size: 10.5
color: "#000000"
# 代码块设置
code_block:
# 语言标签格式(代码块开头的 [language] 标签)
label:
font: "Times New Roman"
size: 10
color: "#808080"
# 代码内容格式
content:
font: "Times New Roman"
size: 10
color: "#333333"
left_indent: 24
line_spacing: 1.2
# 行内代码设置
inline_code:
font: "Times New Roman"
size: 10
color: "#333333"
# 引用块设置
quote:
background_color: "#EAEAEA"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5
# 数学公式设置
math:
font: "Times New Roman"
size: 11
italic: true
color: "#00008B"
# 图片设置
image:
display_ratio: 0.92
max_width_cm: 14.2
target_dpi: 260
show_caption: true
# 分割线设置
horizontal_rule:
character: "─"
repeat_count: 55
font: "Times New Roman"
size: 12
color: "#808080"
alignment: "center"
# 列表设置
lists:
bullet:
marker: "•"
indent: 24
numbered:
indent: 24
preserve_format: true
task:
unchecked: "☐"
checked: "☑"
name: "工作报告格式"
description: "工作报告格式,黑体标题,仿宋正文,1.5倍行距"
# 页面设置
page:
width: 21.0
height: 29.7
margin_top: 2.54
margin_bottom: 2.54
margin_left: 3.0
margin_right: 3.0
# 字体设置
fonts:
default:
name: "仿宋_GB2312"
ascii: "Times New Roman"
size: 12 # pt (小四)
color: "#000000"
# 标题设置
titles:
level1:
size: 16 # pt (三号)
bold: true
align: "center"
space_before: 12
space_after: 6
indent: 0
level2:
size: 14 # pt (四号)
bold: true
align: "left"
space_before: 6
space_after: 3
indent: 0
level3:
size: 12 # pt (小四)
bold: true
align: "left"
indent: 0
level4:
size: 12 # pt (小四)
bold: false
align: "left"
indent: 0
# 段落设置
paragraph:
line_spacing: 1.5
first_line_indent: 24
align: "justify"
# 页码设置
page_number:
enabled: true
format: "1/x"
font: "Times New Roman"
size: 10.5
position: "center"
# 引号设置
quotes:
convert_to_chinese: true
# 表格设置
table:
border_enabled: true
border_color: "#000000"
border_width: 4
line_spacing: 1.2
row_height_cm: 0.8
alignment: "center"
cell_margin:
top: 30
bottom: 30
left: 60
right: 60
vertical_align: "center"
# 表格标题行(第一行)格式
header:
font: "Times New Roman"
size: 10.5
bold: true
color: "#000000"
# 表格正文格式
body:
font: "仿宋_GB2312"
size: 10.5
color: "#000000"
# 代码块设置
code_block:
# 语言标签格式(代码块开头的 [language] 标签)
label:
font: "Times New Roman"
size: 10
color: "#808080"
# 代码内容格式
content:
font: "Times New Roman"
size: 10
color: "#333333"
left_indent: 24
line_spacing: 1.2
# 行内代码设置
inline_code:
font: "Times New Roman"
size: 10
color: "#333333"
# 引用块设置
quote:
background_color: "#EAEAEA"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5
# 数学公式设置
math:
font: "Times New Roman"
size: 11
italic: true
color: "#00008B"
# 图片设置
image:
display_ratio: 0.92
max_width_cm: 14.2
target_dpi: 260
show_caption: true
# 分割线设置
horizontal_rule:
character: "─"
repeat_count: 55
font: "Times New Roman"
size: 12
color: "#808080"
alignment: "center"
# 列表设置
lists:
bullet:
marker: "•"
indent: 24
numbered:
indent: 24
preserve_format: true
task:
unchecked: "☐"
checked: "☑"
name: "法律服务方案格式"
description: "法律服务方案专用格式,含分层配色方案、表格交替行、高亮方案标注"
# 配色方案 - 仅3色
colors:
primary: "#1E3A5F" # 深蓝 - 表头、一级标题
accent: "#927F76" # 律所棕 - 高亮(方案名、价格)
alt: "#F5F0ED" # 浅棕 - 交替行背景
# 页面设置
page:
width: 21.0
height: 29.7
margin_top: 2.54
margin_bottom: 2.54
margin_left: 3.18
margin_right: 3.18
# 字体设置
fonts:
default:
name: "仿宋"
name_alt: "仿宋_GB2312"
ascii: "Times New Roman"
size: 12
heading_sans:
name: "微软雅黑"
name_alt: "Microsoft YaHei"
ascii: "Arial"
# 标题设置 - 按层级区分
titles:
level1:
size: 16 # 小二
bold: true
align: "center"
color: "#1E3A5F"
font: "微软雅黑"
space_before: 16
space_after: 12
level2:
size: 14 # 小三
bold: true
align: "left"
color: "#1E3A5F"
font: "微软雅黑"
space_before: 14
space_after: 10
indent: 24
level3:
size: 12 # 小四
bold: true
align: "left"
color: "#1E3A5F"
font: "仿宋"
space_before: 12
space_after: 8
indent: 24
level4:
size: 12
bold: true
align: "left"
font: "仿宋"
space_before: 8
space_after: 4
indent: 24
# 段落设置
paragraph:
line_spacing: 1.5
first_line_indent: 24
align: "justify"
# 页码设置
page_number:
enabled: true
format: "1/x"
font: "Times New Roman"
size: 10.5
position: "center"
# 引号设置
quotes:
convert_to_chinese: true
# 表格设置
table:
border_enabled: true
border_color: "#CCCCCC"
border_width: 4
line_spacing: 1.5
row_height_cm: 0.8
alignment: "center"
cell_margin:
top: 40
bottom: 40
left: 60
right: 60
vertical_align: "center"
header:
font: "微软雅黑"
size: 12
bold: true
color: "#FFFFFF"
background_color: "#1E3A5F"
row_even:
background_color: "#FFFFFF"
row_odd:
background_color: "#F5F0ED"
row_highlight:
background_color: "#F5F0ED"
body:
font: "仿宋"
size: 12
# 高亮文本
highlight:
plan_label:
font: "微软雅黑"
size: 12
bold: true
color: "#927F76"
price:
font: "仿宋"
size: 12
bold: true
color: "#927F76"
# 代码块
code_block:
label:
font: "Times New Roman"
size: 10
content:
font: "Times New Roman"
size: 10
left_indent: 24
line_spacing: 1.2
inline_code:
font: "Times New Roman"
size: 10
# 引用块
quote:
background_color: "#F5F0ED"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5
# 图片
image:
display_ratio: 0.92
max_width_cm: 14.2
target_dpi: 260
show_caption: true
# 分割线
horizontal_rule:
character: "─"
repeat_count: 55
font: "Times New Roman"
size: 12
color: "#CCCCCC"
alignment: "center"
# 列表
lists:
bullet:
marker: "•"
indent: 24
numbered:
indent: 24
preserve_format: true
task:
unchecked: "☐"
checked: "☑"
学术论文主题样式配置
专为学术论文设计的格式,符合国内高校学位论文规范。
主题概述
- 名称: 学术论文主题
- 适用场景: 学位论文、学术期刊文章、研究报告
- 风格: 规范、严谨、传统
- 预设文件:
academic.yaml
---
样式特点
标题
- 字体: 宋体
- 颜色: 黑色(无彩色装饰)
- H1: 18pt 二号,居中
- H2: 14pt 四号
- H3/H4: 12pt 小四
正文
- 字体: 宋体
- 字号: 12pt (小四)
- 行距: 2.0 倍(双倍行距,符合学位论文规范)
- 首行缩进: 2字符
摘要与关键词
- 摘要单独成段
- 关键词以分号分隔
---
页面设置
- 纸张: A4
- 页边距: 上下 2.54cm,左右 3.0/2.5cm
- 页眉: 无特殊要求
- 页脚: 页码居中
---
使用方法
python scripts/md2word.py input.md --preset=academic -o output.docx或从 Word 模板提取:
python scripts/extract_template_config.py \
--template assets/templates/academic.docx \
--output assets/presets/academic.yaml---
Markdown 写作示例
# 论文标题
## 摘要
本文研究了......
**关键词**: 学术论文;格式规范;Markdown
## 一、引言
### 1.1 研究背景
根据已有研究......
## 二、理论框架
### 2.1 核心概念
定义如下......
## 三、结论
1. 第一点结论......
2. 第二点结论......
3. 第三点结论......
---
## 与其他主题的区分
| 特征 | 学术论文 | 法律文书 | 工作报告 |
|------|----------|----------|----------|
| 行距 | 2.0 倍 | 1.5 倍 | 1.5 倍 |
| 标题字体 | 宋体 | 微软雅黑 | 黑体 |
| 标题颜色 | 黑色 | 深蓝黑 | 黑色 |
| 首行缩进 | 有 | 有 | 有 |
律师事务所主题样式配置
专为法律文书设计的专业主题,庄重、严谨。
主题概述
- 名称: 律师事务所主题
- 适用场景: 法律意见书、合同、诉状、备忘录
- 风格: 庄重、专业、传统
- 模板文件:
law-firm.docx
---
样式特点
标题
- 字体: 思源宋体 / 宋体
- 颜色: 深蓝黑 (#1A1A2E)
- H1: 16pt 小四 / H2: 14pt 五号 / H3: 12pt 小五
正文
- 字体: 宋体
- 字号: 12pt (小四)
- 行距: 1.5 (固定值 22pt)
- 首行缩进: 2字符
特殊元素
- 章节编号: 自动编号 (一、二、三...)
- 条款编号: 自动编号 (1.1、1.2、1.3...)
- 引用法条: 特殊样式
---
页面设置
- 纸张: A4
- 页边距: 上下 2.54cm,左右 3.17cm
- 页眉: 律所名称 / 文档标题
- 页脚: 页码 / 保密声明
---
使用方法
python scripts/extract_template_config.py \
--template assets/templates/law-firm.docx \
--output assets/presets/law-firm.yaml \
--profile law-firm---
推荐字体安装
macOS
brew install --cask font-source-han-serifWindows
从思源字体官网下载:
- https://github.com/adobe-fonts/source-han-serif
---
待修改配置
当前为默认模板副本,需手动调整:
1. 打开 law-firm.docx 2. 修改标题字体为思源宋体 3. 调整正文字号为 12pt 4. 设置行距为 1.5 倍 5. 添加首行缩进 6. 调整页边距
---
法律文书写作建议
Markdown 结构示例
# 法律意见书
**案号**: (2025) 京律意字第 001 号
**委托人**: 甲公司
**关于**: 乙公司合同纠纷事宜
## 一、事实概述
根据委托人提供的材料...
## 二、法律分析
### 2.1 合同效力
根据《民法典》第 143 条...
### 2.2 违约责任
依据合同约定...
## 三、法律建议
1. 建议先行协商...
2. 必要时提起诉讼...
**律师**: 张三
**日期**: 2025 年 1 月 21 日极简主题样式配置
极简风格,注重内容本身,去除装饰性元素。
主题概述
- 名称: 极简主题
- 适用场景: 个人笔记、草稿、快速文档
- 风格: 简洁、无装饰
- 模板文件:
minimal.docx
---
样式特点
标题
- 黑色单色,无彩色装饰
- 字号递减:16pt → 14pt → 12pt
- 微软雅黑字体
正文
- 宋体 11pt
- 1.0 倍行距(紧凑)
- 最小段落间距
代码
- 等宽字体,无背景色
- 保持原汁原味
---
使用方法
python scripts/extract_template_config.py \
--template assets/templates/minimal.docx \
--output assets/presets/minimal.yaml \
--profile minimal---
待修改配置
当前为默认模板副本,需手动调整:
1. 打开 minimal.docx 2. 修改标题颜色为黑色 (#000000) 3. 调整正文行距为 1.0 4. 减少段落间距
工作报告主题样式配置
专为政府机关、企事业单位工作报告设计的格式。
主题概述
- 名称: 工作报告主题
- 适用场景: 年度工作报告、项目汇报、工作总结
- 风格: 正式、清晰、层次分明
- 预设文件:
report.yaml
---
样式特点
标题
- 字体: 黑体(标题)/ 仿宋(正文)
- 颜色: 黑色
- H1: 16pt 三号,居中
- H2: 14pt 四号
- H3: 12pt 小四
正文
- 字体: 仿宋_GB2312
- 字号: 12pt (小四)
- 行距: 1.5 倍
- 首行缩进: 2字符
结构特点
- 章节层次清晰
- 段落间距适中
- 适合阅读和打印
---
页面设置
- 纸张: A4
- 页边距: 上下 2.54cm,左右 3.0cm
- 页眉: 可添加单位名称
- 页脚: 页码居中
---
使用方法
python scripts/md2word.py input.md --preset=report -o output.docx或从 Word 模板提取:
python scripts/extract_template_config.py \
--template assets/templates/report.docx \
--output assets/presets/report.yaml---
Markdown 写作示例
# 2025年度工作报告
## 一、工作总体情况
### 1.1 主要业绩
本年度完成......
### 1.2 重点项目
重点推进以下工作......
## 二、存在问题和改进措施
### 2.1 主要问题
当前存在以下问题......
### 2.2 改进措施
针对上述问题,拟采取以下措施......
## 三、下年度工作计划
1. 继续深化......
2. 加快推进......
3. 着力完善......
**报告人**: 张三
**日期**: 2025 年 12 月 31 日---
与法律文书主题的区分
| 特征 | 工作报告 | 法律文书 |
|---|---|---|
| 标题字体 | 黑体 | 微软雅黑 |
| 正文字体 | 仿宋_GB2312 | 仿宋 |
| 标题字号 | H1=16pt | H1=15pt |
| 行距 | 1.5 倍 | 1.5 倍 |
| 适用场景 | 政府/企业报告 | 法律意见书/合同 |
法律服务方案主题样式配置
专为法律服务方案设计的专业格式,带有律所品牌配色。
主题概述
- 名称: 法律服务方案主题
- 适用场景: 法律服务方案、诉讼方案、咨询服务方案
- 风格: 专业、高端、可视化
- 预设文件:
service-plan.yaml
---
样式特点
配色方案
- 主色: #1E3A5F(深蓝)— 表头、一级标题
- 强调色: #927F76(律所棕)— 方案名称、价格高亮
- 交替色: #F5F0ED(浅棕)— 表格交替行背景
标题
- 字体: 微软雅黑(标题)/ 仿宋(正文)
- H1: 16pt 小二,居中,深蓝色
- H2: 14pt 小三,左对齐,深蓝色
- H3: 12pt 小四,左对齐,深蓝色
正文
- 字体: 仿宋
- 字号: 12pt (小四)
- 行距: 1.5 倍
- 首行缩进: 2字符
表格
- 表头: 微软雅黑 12pt 加粗,白色文字,深蓝背景
- 交替行: 白/浅棕背景交替
- 高亮行: 浅棕背景(用于标注重点方案)
---
页面设置
- 纸张: A4
- 页边距: 上下 2.54cm,左右 3.18cm
- 页眉: 可添加律所名称
- 页脚: 页码居中
---
使用方法
python scripts/md2word.py input.md --preset=service-plan -o output.docx---
Markdown 写作示例
# 法律服务方案
## 一、案件概况
**客户**: 某某公司
**对方**: 某某集团
## 二、服务方案
| 方案 | 服务内容 | 费用 |
|------|----------|------|
| **基础方案** | 法律咨询、文书审查 | ¥30,000 |
| **标准方案** | 基础方案 + 诉讼代理 | ¥80,000 |
| **高端方案** | 全方位法律服务 | ¥150,000 |
## 三、增值服务
- ✅ 优先预约
- ✅ 全年不限次咨询
- ✅ 专属法律顾问
## 四、律所优势
1. 专业团队......
2. 丰富经验......
3. 高效响应......---
配色使用指南
深蓝色 (#1E3A5F)
用于:
- 文档主标题
- 表格表头背景
- 一级章节标题
律所棕 (#927F76)
用于:
- 方案名称高亮
- 价格数字
- 重点标注文本
浅棕色 (#F5F0ED)
用于:
- 表格奇数行背景
- 引用块背景
- 高亮段落背景
技术文档主题样式配置
专为技术文档设计的主题,适合 API 文档、开发指南等。
主题概述
- 名称: 技术文档主题
- 适用场景: API 文档、技术规范、开发指南
- 风格: 现代、清晰、易读
- 模板文件:
tech-doc.docx
---
样式特点
标题
- 字体: Source Sans Pro 或 微软雅黑
- 颜色: 技术蓝 (#2196F3)
- H1: 18pt / H2: 16pt / H3: 14pt
正文
- 字体: Source Sans Pro / 宋体
- 字号: 10.5pt
- 行距: 1.3
代码块
- 字体: Fira Code 或 Source Code Pro
- 背景: 深色 (#282C34) 或浅色 (#F5F5F5)
- 语法高亮支持
行内代码
- 字体: 等宽
- 背景: 浅灰 (#E8E8E8)
- 圆角边框
---
使用方法
python scripts/extract_template_config.py \
--template assets/templates/tech-doc.docx \
--output assets/presets/tech-doc.yaml \
--profile tech-doc---
推荐字体安装
macOS
brew install --cask font-source-sans-pro
brew install --cask font-source-code-pro
brew install --cask font-fira-codeWindows
从以下网站下载安装:
- https://fonts.google.com/ (Source Sans Pro, Source Code Pro)
- https://github.com/tonsky/FiraCode
---
待修改配置
当前为默认模板副本,需手动调整:
1. 打开 tech-doc.docx 2. 修改标题颜色为 #2196F3 3. 调整代码块背景色 4. 设置代码字体为 Fira Code
更新日志
本文件记录 md2word 技能的所有重要变更。
[0.4.1] - 2026-02-11
修复
- 导入错误修复: 修复模块化重构后导致的
ImportError: cannot import name 'get_config' from 'config'
- 将
get_config()和set_config()函数从md2word.py移至config.py - 这些函数被所有子模块(formatter.py, table_handler.py, chart_handler.py)依赖,应属于配置管理模块
- 修复了 v0.4.0 重构时引入的循环导入问题
[0.4.0] - 2026-02-10
重构
- 脚本模块化拆分: 将 1955 行的单文件脚本拆分为 4 个模块
md2word.py: 主入口 + 核心转换流程(800 行,减少 59%)formatter.py: 文本/段落格式化模块(388 行)table_handler.py: 表格处理模块(532 行)chart_handler.py: 图表渲染模块(248 行)- 便于扩展新的图表类型支持
- 依赖清理: 移除冗余导入
- 移除未使用的
sys,requests,base64,io等模块 - 移除未使用的
WD_TAB_ALIGNMENT等 docx 枚举 BeautifulSoup移至 table_handler.py
[0.3.0] - 2026-02-10
变更
- Skill 结构重构: 按照 Skill 开发指南最佳实践重构
- 新增
references/目录,实现渐进式披露 - 新增
references/config-reference.md:配置架构快速参考 - 新增
references/examples.md:使用示例和常见场景 - 精简 SKILL.md(从 ~350 行减至 ~90 行)
- 简化
scripts/md2word.py头部注释 - 移除
scripts/requirements.txt(依赖在 SKILL.md 中说明)
- 描述更新: SKILL.md frontmatter description 更新为更通用的表述
- 去除"法律文书"的限定性描述
- 改为"符合中文排版标准的专业格式"
- 强调适用于正式文档、论文、报告等多种场景
改进
- 配置参考文档指向
assets/presets/*.yaml避免重复 - 参考文档与 SKILL.md 通过链接实现渐进式披露
- 文档结构更清晰,便于维护和扩展
- 移除 references 文档中的目录,保持简洁
[0.2.1] - 2026-02-10
修复
- 引号转换修复: 修复英文引号转中文引号的左右配对问题
- 将"上下文感知"逻辑改为更可靠的"交替状态机"方法
- 修复了连续引号都变成闭引号的bug
- 修复了部分引号未被正确转换的问题
- 使用Unicode转义序列避免Python语法警告
变更
- 文档中文化: SKILL.md 和 CHANGELOG.md 完全中文化
- frontmatter 的 name 和 description 改为中文
- 版本记录标题翻译(Added → 新增,Changed → 变更等)
[0.2.0] - 2026-01-29
新增
- 配置系统增强: 添加完整的配置选项到 YAML 模板和预设文件
- 代码块格式配置: 语言标签、内容字体、缩进、行距
- 行内代码格式配置: 字体、字号、颜色
- 引用块格式配置: 背景色、缩进、字号
- 数学公式格式配置: 字体、字号、斜体、颜色
- 图片设置配置: 显示比例、最大宽度、目标DPI
- 分割线设置配置: 字符、重复次数、字体、颜色
- 列表设置配置: 无序列表、有序列表、任务列表标记
- 表格增强配置: 行高、单元格边距、垂直对齐、标题/正文格式
变更
- md2word.py: 重构所有格式化函数使用配置读取
add_horizontal_line(): 使用horizontal_rule配置add_code_block(): 使用code_block配置add_quote(): 使用quote配置add_bullet_list(),add_task_list(): 使用lists配置set_run_format_with_styles(): 使用inline_code和math配置set_table_run_format(),set_table_cell_format(): 使用table配置create_word_table(),create_word_table_from_html(): 使用table配置insert_image_to_word(): 使用image配置- 新增
hex_to_rgb(): 十六进制颜色转换函数
- 所有预设文件: 同步新增配置选项
legal.yaml: 法律文书格式预设(与原始脚本完全一致)academic.yaml: 学术论文格式预设report.yaml: 工作报告格式预设simple.yaml: 简单文档格式预设
- config-template.yaml: 更新配置模板,包含所有新配置选项
[0.1.0] - 2026-01-29
新增
- 初始版本: md2word 技能 - Markdown转Word配置化工具
- YAML 配置系统支持
- 4 种内置预设格式 (legal/academic/report/simple)
- 自定义配置文件支持
- Word 模板文件支持 (.docx)
- 命令行参数:
--preset,--config,--list-presets,--template
功能特性
- 完整的 Markdown 到 Word 转换
- 页面格式设置 (A4, 页边距)
- 字体和字号配置
- 标题格式配置 (4 级标题)
- 段落格式配置 (行距、首行缩进、对齐)
- 页码自动生成 (支持 1/x 格式)
- 引号自动转换 (英文 → 中文)
- 表格转换支持 (Markdown 和 HTML 表格)
- 图片插入和优化
- Mermaid 图表本地渲染
- 格式支持: 加粗、斜体、<u>下划线</u>、~~删除线~~
- 代码块和行内代码支持
- 数学公式支持 ($LaTeX$)
- 列表支持 (无序、有序、任务列表)
- 引用块支持
目录结构
md2word/
├── assets/
│ ├── presets/ # YAML 格式预设
│ ├── templates/ # Word .docx 模板文件
│ └── config-template.yaml
├── scripts/
│ ├── md2word.py # 主转换脚本
│ └── config.py # 配置管理模块
└── SKILL.md # 技能文档配置架构参考
本文档提供 md2word 配置系统的快速参考。
配置概述
md2word 使用 YAML 格式的配置文件来控制 Word 文档的格式化输出。配置采用层级结构,支持以下顶级分组:
| 分组 | 说明 |
|---|---|
page | 页面尺寸和页边距 |
fonts | 默认字体设置 |
titles | 标题格式(4 级) |
paragraph | 段落格式 |
page_number | 页码设置 |
quotes | 引号转换 |
table | 表格格式 |
code_block | 代码块格式 |
inline_code | 行内代码格式 |
quote | 引用块格式 |
math | 数学公式格式 |
image | 图片设置 |
horizontal_rule | 分割线设置 |
lists | 列表设置 |
---
配置分组
页面设置 (page)
page:
width: 21.0 # 纸张宽度 (cm)
height: 29.7 # 纸张高度 (cm)
margin_top: 2.54 # 上边距 (cm)
margin_bottom: 2.54 # 下边距 (cm)
margin_left: 3.18 # 左边距 (cm)
margin_right: 3.18 # 右边距 (cm)字体设置 (fonts)
fonts:
default:
name: "仿宋_GB2312" # 中文字体名称
ascii: "Times New Roman" # 英文字体名称
size: 12 # 字号 (pt)
color: "#000000" # 字体颜色 (十六进制)标题格式 (titles)
titles:
level1:
size: 15 # 字号 (pt)
bold: true # 是否加粗
align: "center" # 对齐方式 (left/center/right/justify)
space_before: 6 # 段前间距 (pt)
space_after: 6 # 段后间距 (pt)
indent: 0 # 首行缩进 (pt)
level2: # 二级标题配置
level3: # 三级标题配置
level4: # 四级标题配置段落格式 (paragraph)
paragraph:
line_spacing: 1.5 # 行距倍数
first_line_indent: 24 # 首行缩进 (pt)
align: "justify" # 对齐方式页码设置 (page_number)
page_number:
enabled: true # 是否启用页码
format: "1/x" # 格式 ("1", "x", "1/x")
font: "Times New Roman" # 字体
size: 10.5 # 字号 (pt)
position: "center" # 位置 (left/center/right)表格格式 (table)
table:
border_enabled: true # 是否显示边框
border_color: "#000000" # 边框颜色
border_width: 4 # 边框宽度
line_spacing: 1.2 # 行距
row_height_cm: 0.8 # 行高 (cm)
alignment: "center" # 表格对齐
cell_margin: # 单元格边距
top: 30
bottom: 30
left: 60
right: 60
vertical_align: "center" # 垂直对齐 (top/center/bottom)
header: # 标题行格式
font: "Times New Roman"
size: 10.5
bold: true
color: "#000000"
body: # 正文格式
font: "仿宋_GB2312"
size: 10.5
color: "#000000"代码块格式 (code_block)
code_block:
label: # 语言标签格式
font: "Times New Roman"
size: 10
color: "#808080"
content: # 代码内容格式
font: "Times New Roman"
size: 10
color: "#333333"
left_indent: 24
line_spacing: 1.2行内代码格式 (inline_code)
inline_code:
font: "Times New Roman"
size: 10
color: "#333333"引用块格式 (quote)
quote:
background_color: "#EAEAEA"
left_indent_inches: 0.2
font_size: 9
line_spacing: 1.5数学公式格式 (math)
math:
font: "Times New Roman"
size: 11
italic: true
color: "#00008B"图片设置 (image)
image:
display_ratio: 0.92 # 相对于页面可用宽度的比例
max_width_cm: 14.2 # 最大显示宽度 (cm)
target_dpi: 260 # 目标 DPI
show_caption: true # 是否显示标题分割线设置 (horizontal_rule)
horizontal_rule:
character: "─" # 分割线字符
repeat_count: 55 # 重复次数
font: "Times New Roman"
size: 12
color: "#808080"
alignment: "center"列表设置 (lists)
lists:
bullet: # 无序列表
marker: "•" # 标记符号
indent: 24
numbered: # 有序列表
indent: 24
preserve_format: true
task: # 任务列表
unchecked: "☐"
checked: "☑"引号设置 (quotes)
quotes:
convert_to_chinese: true # 是否自动转换英文引号为中文引号---
自定义配置
方法一:修改配置模板
1. 复制配置模板:
cp assets/config-template.yaml my-config.yaml2. 编辑配置文件,修改需要的参数
3. 使用自定义配置:
python scripts/md2word.py input.md --config=my-config.yaml方法二:基于预设修改
1. 复制预设文件:
cp assets/presets/legal.yaml my-config.yaml2. 在复制的文件基础上修改
3. 使用自定义配置
---
预设列表
运行以下命令查看所有预设详情(从 YAML 动态读取):
python scripts/config.py --list完整配置文件位于 assets/presets/ 目录,设计说明位于 assets/theme-notes/。
使用示例
本文档提供 md2word 技能的使用示例和常见场景。
基本转换
最简单的用法
# 转换当前目录下的 Markdown 文件
python scripts/md2word.py document.md输出:document_完整版.docx
指定输出文件名
python scripts/md2word.py input.md output.docx---
使用预设格式
Legal 预设(正式文档格式)
python scripts/md2word.py contract.md --preset=legal适用场景:合同、正式函件、法律文书
格式特点:
- 字体:仿宋_GB2312
- 字号:小四 (12pt)
- 行距:1.5 倍
- 页边距:上下 2.54cm,左右 3.18cm
Academic 预设(学术论文格式)
python scripts/md2word.py thesis.md --preset=academic适用场景:学术论文、研究报告
格式特点:
- 字体:宋体
- 字号:小四 (12pt)
- 行距:2.0 倍(双倍行距)
Report 预设(工作报告格式)
python scripts/md2word.py report.md --preset=report适用场景:工作报告、总结报告
格式特点:
- 标题:黑体
- 正文:仿宋_GB2312
- 行距:1.5 倍
Minimal 预设(极简文档格式)
python scripts/md2word.py notes.md --preset=minimal适用场景:内部文档、笔记草稿
格式特点:基础格式,无特殊要求
---
自定义配置
创建自定义配置
1. 复制配置模板:
cp assets/config-template.yaml my-config.yaml2. 编辑配置文件:
name: "我的自定义格式"
description: "自定义的文档格式"
page:
width: 21.0
height: 29.7
margin_top: 2.5
margin_bottom: 2.5
margin_left: 3.0
margin_right: 3.0
fonts:
default:
name: "宋体"
ascii: "Times New Roman"
size: 12
color: "#000000"3. 使用自定义配置:
python scripts/md2word.py document.md --config=my-config.yaml基于预设修改
# 复制预设作为起点
cp assets/presets/legal.yaml my-format.yaml
# 编辑后使用
python scripts/md2word.py document.md --config=my-format.yaml---
使用 Word 模板
自动发现模板
将 .docx 模板文件放入 assets/templates/ 目录,程序会自动查找。
文件名包含"模板"或"template"的文件优先使用。
手动指定模板
python scripts/md2word.py document.md --template=my-template.docx模板用途
Word 模板用于:
- 预设页眉、页脚(如公司 Logo)
- 预设文档结构
- 保留特定的样式设置
注意:模板文件控制视觉元素,配置文件控制格式参数
---
常见场景
场景 1:合同文档转换
# 使用 legal 预设
python scripts/md2word.py contract.md --preset=legal
# 如果有公司模板
python scripts/md2word.py contract.md --preset=legal --template=company-template.docx场景 2:学术论文转换
# 使用 academic 预设
python scripts/md2word.py paper.md --preset=academic output-paper.docx场景 3:批量转换当前目录所有 Markdown 文件
# 不指定输入文件,自动处理所有 .md 文件
python scripts/md2word.py --preset=legal场景 4:使用完全自定义的格式
# 先创建自定义配置
cat > my-custom.yaml << 'EOF'
name: "自定义格式"
fonts:
default:
name: "微软雅黑"
size: 11
paragraph:
line_spacing: 1.3
EOF
# 使用自定义配置转换
python scripts/md2word.py document.md --config=my-custom.yaml场景 5:查看可用预设
python scripts/md2word.py --list-presets输出示例:
可用的预设配置:
- academic: 学术论文格式 - 宋体,小四号,双倍行距
- legal: 正式文档格式 - 仿宋_GB2312,小四号,1.5倍行距
- report: 工作报告格式 - 黑体标题,仿宋正文,1.5倍行距
- minimal: 极简文档格式 - 无特殊格式要求---
输入文件格式示例
基本文档结构
# 文档标题
## 第一条 条款内容
这是正文段落,首行会自动缩进两个字符。
### 子条款
1. 有序列表项
2. 另一项
**重点内容**可以加粗显示。
| 项目 | 说明 |
|:-----|:-----|
| 项目A | 说明A |
| 项目B | 说明B |
> 这是引用块内容
def example(): print("代码块")
支持的 Markdown 元素
- 标题:
#到###### - 列表:无序 (
*,-,+)、有序 (1.)、任务 (- [ ]) - 表格:Markdown 表格语法
- 代码块:三反引号包裹
- 引用块:
>开头 - 文本格式:
**加粗**、*斜体*、<u>下划线</u>、~~删除线~~ - 数学公式:
$公式$ - 分割线:
---、***、___ - 图片:
 - Mermaid 图表:``
mermaid ...``
---
高级功能
Mermaid 图表
输入:
graph TD A[开始] --> B{判断} B -->|是| C[执行A] B -->|否| D[执行B]
程序会自动将 Mermaid 图表渲染为图片插入 Word 文档。
表格格式保留
输入:
| **加粗** | *斜体* | 普通 |
|----------|--------|------|
| <u>下划线</u> | ~~删除线~~ | `代码` |表格中的所有格式都会在 Word 输出中保留。
---
常见问题
Q: 如何修改字号?
A: 编辑自定义配置文件,修改 fonts.default.size 参数。
Q: 如何调整行距?
A: 编辑自定义配置文件,修改 paragraph.line_spacing 参数。
Q: 如何不自动添加页码?
A: 在配置文件中设置 page_number.enabled: false。
Q: 英文引号会自动转换吗?
A: 是的,默认会自动将英文引号转换为中文引号。可在配置中设置 quotes.convert_to_chinese: false 禁用。
Markdown 到 Word 样式映射
本文档定义了 Markdown 元素如何映射到 Word 文档样式。
默认映射
转换器使用以下默认映射:
| Markdown 元素 | Word 样式 | 说明 |
|---|---|---|
# 标题 1 | Heading 1 | 16 pt,加粗 |
## 标题 2 | Heading 2 | 14 pt,加粗 |
### 标题 3 | Heading 3 | 13 pt,加粗 |
#### 标题 4 | Heading 4 | 12 pt,加粗 |
##### 标题 5 | Heading 5 | 11 pt,加粗,斜体 |
###### 标题 6 | Heading 6 | 11 pt,斜体 |
| 普通文本 | Normal | 12 pt,1.5 倍行距,首行缩进 |
> 引用 | Block Quote | 缩进,灰色背景 |
代码块 | Code Block | 等宽字体,灰色背景 |
` 行内代码 ` | Code Char | 等宽字体,品红色 |
*斜体* | Emphasis | 斜体 |
**粗体** | Strong | 加粗 |
***粗斜体*** | Intense Emphasis | 加粗,强调色 |
- 项目 | List Bullet | 项目符号列表 |
1. 项目 | List Number | 编号列表 |
样式自定义
通过配置文件
创建 YAML 配置文件来自定义样式:
fonts:
default:
name: "仿宋_GB2312"
ascii: "Times New Roman"
size: 12
titles:
level1:
size: 16
bold: true
color: "#1A1A2E"通过 Word 模板
将 .docx 模板放入 assets/templates/ 目录,转换时会应用模板中的样式定义。
在 Word 中创建自定义样式
方法:通过 Word 界面
1. 在 Word 中打开模板文件 2. 转到 开始 → 样式 窗格(启动器图标) 3. 右键点击样式 → 修改... 4. 调整格式设置 5. 点击 确定 6. 保存模板文件
常见自定义
修改标题颜色
在 Word 中修改 Heading 1 样式:
- 右键点击 Heading 1 → 修改...
- 格式 → 字体...
- 设置字体颜色
- 点击 确定
调整代码块背景
在 Word 中修改 Code Block 样式:
- 右键点击 Code Block → 修改...
- 格式 → 边框...
- 底纹 选项卡
- 选择填充颜色
- 点击 确定
自定义列表缩进
在 Word 中修改列表样式:
- 右键点击 List Bullet → 修改...
- 格式 → 编号...
- 调整缩进
- 点击 确定
映射故障排除
样式未应用
症状: 文本未使用预期样式
解决方案: 1. 验证配置文件中的样式名称完全匹配 2. 检查预设 YAML 是否存在且格式正确 3. 确保 Word 模板中的样式名称与配置一致
TOC 未生成
症状: 目录缺失
解决方案: 1. 确认 Markdown 中使用了 # 标题语法 2. 验证 Heading 1-3 样式存在 3. 在 Word 中:引用 → 目录 → 更新
中文字体错误
症状: 中文文本使用回退字体
解决方案: 1. 安装思源宋体 (Source Han Serif CN) 2. 在样式中修改为优先使用系统字体:
- macOS: 宋体-简 (Songti SC)
- Windows: 宋体 (SimSun)
测试映射
修改配置后,测试转换:
# 使用预设转换
python scripts/md2word.py input.md --preset=legal -o test.docx
# 使用自定义配置转换
python scripts/md2word.py input.md --config=my-config.yaml -o test.docx
# 打开并验证样式
open test.docx # macOS参考样式名称
Word 文档中定义的所有样式(节选):
- Heading 1, Heading 2, Heading 3, Heading 4, Heading 5, Heading 6
- Normal, Body Text, Body Text 2, Body Text 3
- Block Quote
- Code Block, Code Char
- Emphasis, Strong, Intense Emphasis
- List Bullet, List Number, List Paragraph
- Footer, Header
- Book Title
- Intense Quote
查看完整列表,在 Word 中打开模板文件: 开发工具 选项卡 → 样式 检查器(或 开始 → 样式窗格)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
图表处理模块
处理 Mermaid 等图表的渲染与转换
"""
import os
import re
import subprocess
import shutil
import time
from PIL import Image
# 导入配置模块
from config import get_config
# 导入图片处理函数(延迟导入避免循环)
# from md2word import insert_image_to_word
def preprocess_mermaid_code(mermaid_code: str) -> str:
"""预处理Mermaid源码,避免Mermaid v11 对标签内Markdown解析导致的错误"""
s = mermaid_code
# 反引号替换,避免 codespan 被解析
s = s.replace("`", "'")
# 1) 针对节点标签内部:有序列表 1. -> 1:
def _repl_number_dot(m: re.Match) -> str:
brace = m.group('brace')
quote = m.group('quote') or ''
num = m.group('num')
return f"{brace}{quote}{num}: "
s = re.sub(r"(?m)(?P<brace>[\[\({\>])(?P<quote>\"?\s*)(?P<num>\d+)\.\s", _repl_number_dot, s)
# 2) 针对节点标签内部:无序列表 - / * -> •
def _repl_bullet(m: re.Match) -> str:
brace = m.group('brace')
quote = m.group('quote') or ''
return f"{brace}{quote}• "
s = re.sub(r"(?m)(?P<brace>[\[\({\>])(?P<quote>\"?\s*)[-*]\s", _repl_bullet, s)
# 3) 兜底:整行以列表开头的情况(极少出现在Mermaid内,但保留以防万一)
s = re.sub(r"(?m)^(\s*)-\s+", r"\1• ", s)
s = re.sub(r"(?m)^(\s*)\*\s+", r"\1• ", s)
s = re.sub(r"(?m)^(\s*)(\d+)\.\s+", r"\1\2: ", s)
return s
def try_local_mermaid_render(insert_image_func, get_image_path_func, mermaid_code, md_file_path):
"""尝试使用本地mermaid-cli渲染图表
Args:
insert_image_func: 插入图片到Word的函数
get_image_path_func: 获取图片输出路径的函数
mermaid_code: Mermaid源码
md_file_path: Markdown文件路径
"""
# 为Mermaid文件和输出图片准备路径
timestamp = str(int(time.time() * 1000))
mmd_filename = f"mermaid-src-{timestamp}.mmd"
png_filename = f"mermaid-chart-{timestamp}.png"
# 获取保存图片的最终路径
output_png_path = get_image_path_func(md_file_path, png_filename)
if not output_png_path:
print("⚠️ 无法获取图片输出路径,跳过本地渲染。")
return False
# 临时文件放在脚本所在目录,避免 cwd 不一致导致路径问题
script_dir = os.path.dirname(os.path.abspath(__file__))
temp_mmd_path = os.path.join(script_dir, mmd_filename)
try:
print("🖥️ 尝试本地Mermaid渲染...")
# 创建临时的.mmd文件
with open(temp_mmd_path, 'w', encoding='utf-8') as f:
f.write(mermaid_code)
# 检查 mmdc 命令:优先环境变量 MMDCCMD,其次脚本同目录 node_modules,再其次系统 PATH
mmdc_env = os.environ.get('MMDCCMD', '').strip()
mmdc_path = mmdc_env if mmdc_env else os.path.join(script_dir, "node_modules", ".bin", "mmdc")
if not os.path.exists(mmdc_path):
mmdc_path = shutil.which("mmdc") or ""
if not mmdc_path:
print("⚠️ 本地 mmdc 命令未找到(已跳过本地渲染)")
return False
# 使用mmdc命令生成高分辨率PNG图片
abs_in = os.path.abspath(temp_mmd_path)
abs_out = os.path.abspath(output_png_path)
cfg = os.path.join(script_dir, "mermaid-config.json")
cmd = [mmdc_path, "-i", abs_in, "-o", abs_out, "-t", "neutral", "-w", "2200", "-H", "1500", "--scale", "2.0"]
if os.path.exists(cfg):
cmd.extend(["-c", cfg])
print(f"🔧 执行命令: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
print(f"⚠️ mmdc 命令执行失败: {result.stderr}")
return False
# 检查生成的PNG文件是否存在
if not os.path.exists(output_png_path):
print("⚠️ PNG文件未生成")
return False
# 加载图片并插入Word
image = Image.open(output_png_path)
insert_image_func(image)
print(f"✅ 本地Mermaid图表渲染成功!图片已保存至: {os.path.relpath(output_png_path)}")
return True
except subprocess.TimeoutExpired:
print("⚠️ mmdc命令执行超时")
return False
except Exception as e:
print(f"⚠️ 本地渲染失败: {e}")
return False
finally:
# 无论成功与否,都清理临时的mmd文件
if os.path.exists(temp_mmd_path):
try:
os.unlink(temp_mmd_path)
except:
pass
def create_simple_diagram_text(add_paragraph_func, set_format_func, mermaid_code):
"""创建简化的流程图文本描述"""
p = add_paragraph_func()
run = p.add_run("【流程图】")
run.bold = True
# 解析节点和连接关系
lines = mermaid_code.split('\n')
connections = []
for line in lines:
line = line.strip()
if '-->' in line or '->' in line:
parts = line.split('-->' if '-->' in line else '->')
if len(parts) == 2:
from_node = parts[0].strip()
to_node = parts[1].strip()
connections.append(f"{from_node} → {to_node}")
# 添加解析结果
if connections:
p.add_run("\n主要流程:")
for conn in connections[:8]: # 最多显示8个连接
p.add_run(f"\n• {conn}")
set_format_func(p)
def create_simple_pie_text(add_paragraph_func, set_format_func, mermaid_code):
"""创建简化的饼图文本描述"""
p = add_paragraph_func()
run = p.add_run("【数据分析】")
run.bold = True
# 解析饼图数据
lines = mermaid_code.split('\n')
for line in lines:
if ':' in line and '"' in line:
# 解析数据项
match = re.search(r'"([^"]+)"\s*:\s*(\d+(?:\.\d+)?)', line)
if match:
label, value = match.groups()
p.add_run(f"\n• {label}: {value}")
set_format_func(p)
def create_simple_gantt_text(add_paragraph_func, set_format_func, mermaid_code):
"""创建简化的甘特图文本描述"""
p = add_paragraph_func()
run = p.add_run("【时间安排】")
run.bold = True
# 解析甘特图任务
lines = mermaid_code.split('\n')
current_section = ""
for line in lines:
line = line.strip()
if line.startswith('section '):
current_section = line.replace('section ', '')
p.add_run(f"\n\n{current_section}:")
elif ':' in line and not line.startswith('title'):
# 解析任务
task = line.split(':')[0].strip()
p.add_run(f"\n• {task}")
set_format_func(p)
def create_fallback_text(add_paragraph_func, set_format_func, mermaid_code):
"""创建后备文本方案"""
# 解析图表类型并创建简化版本
if 'graph' in mermaid_code.lower():
create_simple_diagram_text(add_paragraph_func, set_format_func, mermaid_code)
elif 'pie' in mermaid_code.lower():
create_simple_pie_text(add_paragraph_func, set_format_func, mermaid_code)
elif 'gantt' in mermaid_code.lower():
create_simple_gantt_text(add_paragraph_func, set_format_func, mermaid_code)
else:
# 默认处理
p = add_paragraph_func()
run = p.add_run("【图表内容】")
run.bold = True
p.add_run("\n" + mermaid_code)
set_format_func(p)
def create_mermaid_chart(doc, insert_image_func, get_image_path_func, add_paragraph_func, set_format_func, mermaid_code, md_file_path):
"""将Mermaid图表转换为图片并插入Word文档(本地渲染优先)
Args:
doc: Word文档对象
insert_image_func: 插入图片的函数
get_image_path_func: 获取图片路径的函数
add_paragraph_func: 添加段落的函数
set_format_func: 设置段落格式的函数
mermaid_code: Mermaid源码
md_file_path: Markdown文件路径
"""
# 预处理,规避 Mermaid 11 对列表/反引号的 Markdown 解析造成的报错
mermaid_code = preprocess_mermaid_code(mermaid_code)
# 首先尝试本地渲染
local_success = try_local_mermaid_render(insert_image_func, get_image_path_func, mermaid_code, md_file_path)
if local_success:
return
# 仅使用本地渲染:失败则改为文本,不再尝试在线服务
print("⚠️ 本地渲染失败,已禁用在线服务,使用文本替代")
create_fallback_text(add_paragraph_func, set_format_func, mermaid_code)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
md2word 配置管理模块
支持 YAML 格式的配置文件加载和预设管理
"""
import os
import yaml
from typing import Dict, Any, Optional, TYPE_CHECKING, Callable
if TYPE_CHECKING:
from typing import Callable
class Config:
"""配置数据结构"""
def __init__(self, config_dict: Dict[str, Any]):
"""初始化配置"""
self._config = config_dict
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值,支持点分隔的路径(如 'page.width')"""
keys = key.split('.')
value = self._config
for k in keys:
if isinstance(value, dict):
value = value.get(k)
if value is None:
return default
else:
return default
return value if value is not None else default
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return self._config.copy()
@property
def name(self) -> str:
"""配置名称"""
return self.get('name', '未命名配置')
@property
def description(self) -> str:
"""配置描述"""
return self.get('description', '')
# ============================================================================
# 全局配置管理
# ============================================================================
_current_config: Config = None
def get_config() -> Config:
"""获取当前配置"""
global _current_config
if _current_config is None:
_current_config = get_default_preset()
return _current_config
def set_config(config: Config):
"""设置当前配置"""
global _current_config
_current_config = config
def load_config(path: str) -> Optional[Config]:
"""
从 YAML 文件加载配置
Args:
path: 配置文件路径
Returns:
Config 对象,加载失败返回 None
"""
if not os.path.exists(path):
print(f"⚠️ 配置文件不存在: {path}")
return None
try:
with open(path, 'r', encoding='utf-8') as f:
config_dict = yaml.safe_load(f)
return Config(config_dict)
except Exception as e:
print(f"⚠️ 加载配置文件失败: {e}")
return None
def get_preset(name: str) -> Optional[Config]:
"""
获取内置预设配置
Args:
name: 预设名称(legal, minimal, academic, report, service-plan)
Returns:
Config 对象,预设不存在返回 None
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
skill_dir = os.path.dirname(script_dir) # 上级目录是 skill 根目录
preset_path = os.path.join(skill_dir, 'assets', 'presets', f'{name}.yaml')
if not os.path.exists(preset_path):
print(f"⚠️ 预设不存在: {name}")
return None
return load_config(preset_path)
def get_default_preset() -> Config:
"""
获取默认预设(legal)
Returns:
Config 对象
"""
config = get_preset('legal')
if config is None:
# 如果 legal 预设不存在,返回硬编码的默认配置
return get_fallback_config()
return config
def get_fallback_config() -> Config:
"""
获取硬编码的默认配置(兜底方案)
Returns:
Config 对象
"""
return Config({
'name': '默认配置',
'description': '硬编码的默认法律文书格式',
'page': {
'width': 21.0,
'height': 29.7,
'margin_top': 2.54,
'margin_bottom': 2.54,
'margin_left': 3.18,
'margin_right': 3.18,
},
'fonts': {
'default': {
'name': '仿宋_GB2312',
'ascii': 'Times New Roman',
'size': 12,
'color': '#000000',
}
},
'titles': {
'level1': {
'size': 15,
'bold': True,
'align': 'center',
'space_before': 6,
'space_after': 6,
'indent': 0,
},
'level2': {
'size': 12,
'bold': True,
'align': 'justify',
'indent': 24,
},
'level3': {
'size': 12,
'bold': False,
'align': 'justify',
'indent': 24,
},
'level4': {
'size': 12,
'bold': False,
'align': 'justify',
'indent': 24,
},
},
'paragraph': {
'line_spacing': 1.5,
'first_line_indent': 24,
'align': 'justify',
},
'page_number': {
'enabled': True,
'format': '1/x',
'font': 'Times New Roman',
'size': 10.5,
'position': 'center',
},
'quotes': {
'convert_to_chinese': True,
},
'table': {
'border_enabled': True,
'border_color': '#000000',
'border_width': 4,
'line_spacing': 1.2,
},
'code_block': {
'label': {
'font': 'Times New Roman',
'size': 10,
'color': '#808080',
},
'content': {
'font': 'Times New Roman',
'size': 10,
'color': '#333333',
'left_indent': 24,
'line_spacing': 1.2,
},
},
'inline_code': {
'font': 'Times New Roman',
'size': 10,
'color': '#333333',
},
'quote': {
'background_color': '#EAEAEA',
'left_indent_inches': 0.2,
'font_size': 9,
'line_spacing': 1.5,
},
'math': {
'font': 'Times New Roman',
'size': 11,
'italic': True,
'color': '#00008B',
},
'image': {
'display_ratio': 0.92,
'max_width_cm': 14.2,
'target_dpi': 260,
'show_caption': True,
},
'horizontal_rule': {
'character': '─',
'repeat_count': 55,
'font': 'Times New Roman',
'size': 12,
'color': '#808080',
'alignment': 'center',
},
'lists': {
'bullet': {
'marker': '•',
'indent': 24,
},
'numbered': {
'indent': 24,
'preserve_format': True,
},
'task': {
'unchecked': '☐',
'checked': '☑',
},
},
})
def merge_configs(base: Config, override: Optional[Config]) -> Config:
"""
合并两个配置,override 中的值会覆盖 base 中的值
Args:
base: 基础配置
override: 覆盖配置(可选)
Returns:
合并后的 Config 对象
"""
if override is None:
return base
def deep_merge(base_dict: Dict, override_dict: Dict) -> Dict:
"""深度合并字典"""
result = base_dict.copy()
for key, value in override_dict.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
merged = deep_merge(base.to_dict(), override.to_dict())
return Config(merged)
def list_presets() -> list:
"""
列出所有可用的预设名称
Returns:
预设名称列表,如 ['academic', 'legal', 'minimal', 'report', 'service-plan']
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
skill_dir = os.path.dirname(script_dir)
presets_dir = os.path.join(skill_dir, 'assets', 'presets')
if not os.path.exists(presets_dir):
return []
presets = []
for file in os.listdir(presets_dir):
if file.endswith('.yaml'):
presets.append(file[:-5])
return sorted(presets)
def list_presets_info() -> list:
"""
列出所有可用预设的详细信息(从 YAML 文件动态读取)
Returns:
预设信息列表,如 [{'id': 'legal', 'name': '...', 'description': '...'}, ...]
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
skill_dir = os.path.dirname(script_dir)
presets_dir = os.path.join(skill_dir, 'assets', 'presets')
if not os.path.exists(presets_dir):
return []
result = []
for file in sorted(os.listdir(presets_dir)):
if not file.endswith('.yaml'):
continue
preset_id = file[:-5]
path = os.path.join(presets_dir, file)
try:
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
result.append({
'id': preset_id,
'name': data.get('name', preset_id) if data else preset_id,
'description': data.get('description', '') if data else '',
})
except Exception:
result.append({'id': preset_id, 'name': preset_id, 'description': ''})
return result
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='md2word 配置管理工具')
parser.add_argument('--list', action='store_true', help='列出所有可用预设')
args = parser.parse_args()
if args.list:
presets = list_presets_info()
print(f"\n可用预设 ({len(presets)} 个):\n")
for p in presets:
print(f" {p['id']:15s} {p['name']}")
if p['description']:
print(f" {' ' * 17}{p['description']}")
print()
else:
parser.print_help()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从 Word 模板提取 md2word(python-docx 引擎)可用的 YAML 配置。"""
from __future__ import annotations
import argparse
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
BASE_CONFIG: Dict[str, Any] = {
"name": "模板提取配置",
"description": "从 DOCX 模板提取的配置",
"page": {
"width": 21.0,
"height": 29.7,
"margin_top": 2.54,
"margin_bottom": 2.54,
"margin_left": 3.18,
"margin_right": 3.18,
},
"fonts": {
"default": {
"name": "仿宋_GB2312",
"ascii": "Times New Roman",
"size": 12,
"color": "#000000",
}
},
"titles": {
"level1": {
"size": 15,
"bold": True,
"align": "center",
"space_before": 6,
"space_after": 6,
"indent": 0,
},
"level2": {
"size": 12,
"bold": True,
"align": "justify",
"indent": 24,
},
"level3": {
"size": 12,
"bold": False,
"align": "justify",
"indent": 24,
},
"level4": {
"size": 12,
"bold": False,
"align": "justify",
"indent": 24,
},
},
"paragraph": {
"line_spacing": 1.5,
"first_line_indent": 24,
"align": "justify",
},
"page_number": {
"enabled": True,
"format": "1/x",
"font": "Times New Roman",
"size": 10.5,
"position": "center",
},
"quotes": {
"convert_to_chinese": True,
},
"table": {
"border_enabled": True,
"border_color": "#000000",
"border_width": 4,
"line_spacing": 1.2,
"row_height_cm": 0.8,
"alignment": "center",
"cell_margin": {
"top": 30,
"bottom": 30,
"left": 60,
"right": 60,
},
"vertical_align": "center",
"header": {
"font": "Times New Roman",
"size": 10.5,
"bold": True,
"color": "#000000",
},
"body": {
"font": "仿宋_GB2312",
"size": 10.5,
"color": "#000000",
},
},
"code_block": {
"label": {
"font": "Times New Roman",
"size": 10,
"color": "#808080",
},
"content": {
"font": "Times New Roman",
"size": 10,
"color": "#333333",
"left_indent": 24,
"line_spacing": 1.2,
},
},
"inline_code": {
"font": "Times New Roman",
"size": 10,
"color": "#333333",
},
"quote": {
"background_color": "#EAEAEA",
"left_indent_inches": 0.2,
"font_size": 9,
"line_spacing": 1.5,
},
"math": {
"font": "Times New Roman",
"size": 11,
"italic": True,
"color": "#00008B",
},
"image": {
"display_ratio": 0.92,
"max_width_cm": 14.2,
"target_dpi": 260,
"show_caption": True,
},
"horizontal_rule": {
"character": "─",
"repeat_count": 55,
"font": "Times New Roman",
"size": 12,
"color": "#808080",
"alignment": "center",
},
"lists": {
"bullet": {
"marker": "•",
"indent": 24,
},
"numbered": {
"indent": 24,
"preserve_format": True,
},
"task": {
"unchecked": "☐",
"checked": "☑",
},
},
}
PROFILE_OVERRIDES: Dict[str, Dict[str, Any]] = {
"law-firm": {
"name": "律所主题(模板同步)",
"description": "基于 law-firm 主题文档说明的配置覆盖",
"fonts": {
"default": {
"name": "宋体",
"ascii": "Times New Roman",
"size": 12,
}
},
"titles": {
"level1": {"size": 16, "color": "#1A1A2E", "align": "center"},
"level2": {"size": 14, "color": "#1A1A2E"},
"level3": {"size": 12, "color": "#1A1A2E"},
},
"paragraph": {
"line_spacing": 1.5,
"first_line_indent": 24,
},
},
"tech-doc": {
"name": "技术文档主题(模板同步)",
"description": "基于 tech-doc 主题文档说明的配置覆盖",
"fonts": {
"default": {
"name": "微软雅黑",
"ascii": "Source Sans Pro",
"size": 10.5,
}
},
"titles": {
"level1": {"size": 18, "color": "#2196F3"},
"level2": {"size": 16, "color": "#2196F3"},
"level3": {"size": 14, "color": "#2196F3"},
},
"paragraph": {
"line_spacing": 1.3,
"first_line_indent": 0,
},
"code_block": {
"content": {
"font": "Fira Code",
}
},
"inline_code": {
"font": "Fira Code",
},
},
"minimal": {
"name": "极简主题(模板同步)",
"description": "基于 minimal 主题文档说明的配置覆盖",
"fonts": {
"default": {
"name": "宋体",
"ascii": "Times New Roman",
"size": 11,
}
},
"titles": {
"level1": {"size": 16, "color": "#000000"},
"level2": {"size": 14, "color": "#000000"},
"level3": {"size": 12, "color": "#000000"},
},
"paragraph": {
"line_spacing": 1.0,
"first_line_indent": 0,
},
},
}
CJK_FONT_HINTS = {
"simsun",
"kaiti",
"fangsong",
"songti",
"simhei",
"microsoft yahei",
"microsoft jhenghei",
"pingfang",
"heiti",
"source han",
"noto serif cjk",
"noto sans cjk",
}
def _cm(value: Optional[int]) -> Optional[float]:
if value is None:
return None
return round(float(value) / 360000, 2)
def _pt(value: Any) -> Optional[float]:
if value is None:
return None
pt_val = getattr(value, "pt", None)
if pt_val is None:
return None
return round(float(pt_val), 2)
def _alignment_to_str(alignment: Any) -> Optional[str]:
if alignment is None:
return None
mapping = {
WD_PARAGRAPH_ALIGNMENT.LEFT: "left",
WD_PARAGRAPH_ALIGNMENT.CENTER: "center",
WD_PARAGRAPH_ALIGNMENT.RIGHT: "right",
WD_PARAGRAPH_ALIGNMENT.JUSTIFY: "justify",
}
return mapping.get(alignment)
def _to_hex(color_rgb: Any) -> Optional[str]:
if color_rgb is None:
return None
return f"#{str(color_rgb)}"
def _style(document: Document, name: str):
for style in document.styles:
if style.name == name:
return style
return None
def _default_ascii(font_name: Optional[str]) -> str:
if not font_name:
return "Times New Roman"
lower_name = font_name.lower()
if any(ord(ch) > 127 for ch in font_name):
return "Times New Roman"
if any(hint in lower_name for hint in CJK_FONT_HINTS):
return "Times New Roman"
return font_name
def deep_update(target: Dict[str, Any], source: Dict[str, Any]) -> Dict[str, Any]:
for key, value in source.items():
if key in target and isinstance(target[key], dict) and isinstance(value, dict):
deep_update(target[key], value)
else:
target[key] = value
return target
def extract_template_config(template_path: Path) -> Dict[str, Any]:
config = deepcopy(BASE_CONFIG)
document = Document(template_path)
section = document.sections[0]
page = config["page"]
page["width"] = _cm(section.page_width) or page["width"]
page["height"] = _cm(section.page_height) or page["height"]
page["margin_top"] = _cm(section.top_margin) or page["margin_top"]
page["margin_bottom"] = _cm(section.bottom_margin) or page["margin_bottom"]
page["margin_left"] = _cm(section.left_margin) or page["margin_left"]
page["margin_right"] = _cm(section.right_margin) or page["margin_right"]
normal = _style(document, "Normal")
if normal is not None:
normal_font = normal.font
normal_para = normal.paragraph_format
font_name = normal_font.name or config["fonts"]["default"]["name"]
config["fonts"]["default"]["name"] = font_name
config["fonts"]["default"]["ascii"] = _default_ascii(normal_font.name)
config["fonts"]["default"]["size"] = _pt(normal_font.size) or config["fonts"]["default"]["size"]
config["fonts"]["default"]["color"] = _to_hex(normal_font.color.rgb) or config["fonts"]["default"]["color"]
config["paragraph"]["line_spacing"] = (
float(normal_para.line_spacing)
if isinstance(normal_para.line_spacing, (int, float))
else config["paragraph"]["line_spacing"]
)
config["paragraph"]["first_line_indent"] = (
_pt(normal_para.first_line_indent) or config["paragraph"]["first_line_indent"]
)
config["paragraph"]["align"] = _alignment_to_str(normal_para.alignment) or config["paragraph"]["align"]
config["table"]["body"]["font"] = font_name
config["table"]["body"]["size"] = config["fonts"]["default"]["size"]
for level, style_name in enumerate(["Heading 1", "Heading 2", "Heading 3", "Heading 4"], start=1):
style = _style(document, style_name)
if style is None:
continue
key = f"level{level}"
info = config["titles"][key]
style_font = style.font
style_para = style.paragraph_format
size = _pt(style_font.size)
if size:
info["size"] = size
if style_font.bold is not None:
info["bold"] = bool(style_font.bold)
color = _to_hex(style_font.color.rgb)
if color:
info["color"] = color
align = _alignment_to_str(style_para.alignment)
if align:
info["align"] = align
space_before = _pt(style_para.space_before)
if space_before is not None:
info["space_before"] = space_before
space_after = _pt(style_para.space_after)
if space_after is not None:
info["space_after"] = space_after
indent = _pt(style_para.first_line_indent)
if indent is not None:
info["indent"] = indent
code_style = _style(document, "Code Block")
if code_style is not None:
code_font = code_style.font
code_para = code_style.paragraph_format
config["code_block"]["content"]["font"] = code_font.name or config["code_block"]["content"]["font"]
config["code_block"]["content"]["size"] = _pt(code_font.size) or config["code_block"]["content"]["size"]
config["code_block"]["content"]["color"] = _to_hex(code_font.color.rgb) or config["code_block"]["content"]["color"]
config["code_block"]["content"]["line_spacing"] = (
float(code_para.line_spacing)
if isinstance(code_para.line_spacing, (int, float))
else config["code_block"]["content"]["line_spacing"]
)
quote_style = _style(document, "Block Quote")
if quote_style is not None:
quote_font = quote_style.font
quote_para = quote_style.paragraph_format
config["quote"]["font_size"] = _pt(quote_font.size) or config["quote"]["font_size"]
config["quote"]["line_spacing"] = (
float(quote_para.line_spacing)
if isinstance(quote_para.line_spacing, (int, float))
else config["quote"]["line_spacing"]
)
left_indent_pt = _pt(quote_para.left_indent)
if left_indent_pt is not None:
config["quote"]["left_indent_inches"] = round(left_indent_pt / 72.0, 3)
return config
def main() -> int:
parser = argparse.ArgumentParser(description="从 docx 模板提取 md2word YAML 配置")
parser.add_argument("--template", default=None, help="模板文件路径(.docx),默认使用 assets/templates/ 目录下第一个 .docx 文件")
parser.add_argument("--output", required=True, help="输出 YAML 路径")
parser.add_argument("--name", default=None, help="配置名称")
parser.add_argument("--description", default=None, help="配置描述")
parser.add_argument(
"--profile",
default=None,
choices=["law-firm", "tech-doc", "minimal"],
help="在提取结果上应用主题说明覆盖",
)
args = parser.parse_args()
# 相对路径相对于 skill 根目录(脚本上级目录)解析
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
templates_dir = skill_dir / "assets" / "templates"
if args.template:
template_path = (skill_dir / args.template).resolve()
else:
# 自动查找 templates 目录下第一个 .docx
docx_files = sorted(templates_dir.glob("*.docx"))
if not docx_files:
raise FileNotFoundError(
f"未找到模板文件,请将 .docx 模板放入 {templates_dir},或使用 --template 指定路径"
)
template_path = docx_files[0]
print(f"自动选择模板: {template_path.name}")
output_path = (skill_dir / args.output).resolve()
if not template_path.exists():
raise FileNotFoundError(f"模板不存在: {template_path}")
config = extract_template_config(template_path)
if args.name:
config["name"] = args.name
if args.description:
config["description"] = args.description
if args.profile:
deep_update(config, PROFILE_OVERRIDES[args.profile])
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as f:
yaml.safe_dump(config, f, allow_unicode=True, sort_keys=False)
print(f"已生成配置: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文本格式化模块
处理文本格式解析、段落格式设置、字体样式应用
"""
import re
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml.ns import qn
# 导入配置模块
from config import Config, get_config
def convert_quotes_to_chinese(text):
"""将英文引号转换为中文引号(交替状态机版)
规则:
- 将直双引号 " 转为中文开/闭引号 " "(交替状态:开→闭→开→闭...)
- 将直单引号 ' 转为中文开/闭引号 ' ',但保留英文缩写/所有格中的撇号(如 don't, John's)
- 避免转换代码片段中的引号(由反引号 ` 包裹)
"""
if not text:
return text
original_text = text
# 若无需要处理的引号,直接返回
if ('"' not in text) and ("'" not in text):
return text
result = []
i = 0
in_code = False # 是否处于 `code` 片段中
# 交替状态机:0=等待开引号,1=等待闭引号
double_quote_state = 0
single_quote_state = 0
while i < len(text):
ch = text[i]
# 处理反引号包裹的代码片段,保持原样
if ch == '`':
# 统计连续反引号的数量(支持 ``` 块 及 ` 行内`)
j = i + 1
while j < len(text) and text[j] == '`':
j += 1
backtick_count = j - i
result.append('`' * backtick_count)
in_code = not in_code # 简化处理:遇到成组反引号时翻转状态
i = j
continue
if in_code:
# 代码片段内不做引号更换
result.append(ch)
i += 1
continue
if ch == '"':
# 使用交替状态机:第一个是开引号,第二个是闭引号,以此类推
if double_quote_state == 0:
result.append('\u201c') # 中文开双引号 "
double_quote_state = 1 # 下一个是闭引号
else:
result.append('\u201d') # 中文闭双引号 "
double_quote_state = 0 # 重置,下一个是开引号
i += 1
continue
if ch == "'":
# 保留英文缩写/所有格中的撇号:字母-撇号-字母
prev_c = text[i - 1] if i > 0 else ''
next_c = text[i + 1] if i + 1 < len(text) else ''
if prev_c.isalpha() and next_c.isalpha():
result.append("'")
i += 1
continue
# 使用交替状态机
if single_quote_state == 0:
result.append('\u2018') # 中文开单引号 '
single_quote_state = 1
else:
result.append('\u2019') # 中文闭单引号 '
single_quote_state = 0
i += 1
continue
# 其它字符保持
result.append(ch)
i += 1
text = ''.join(result)
if text != original_text:
print(f"✅ 引号转换: {original_text} → {text}")
return text
def parse_text_formatting(paragraph, text, title_level=0, is_quote=False):
"""解析文本格式(支持加粗、斜体、下划线,转换引号为中文)"""
# 转换英文引号为中文引号
text = convert_quotes_to_chinese(text)
# 先处理<br>标签为段内换行
segments = re.split(r'<br\s*/?>', text, flags=re.IGNORECASE)
# 使用正则表达式解析所有格式标记
format_patterns = [
(r'\*\*\*(.*?)\*\*\*', {'bold': True, 'italic': True}),
(r'___(.*?)___', {'bold': True, 'italic': True}),
(r'\*\*(.*?)\*\*', {'bold': True}),
(r'__(.*?)__', {'bold': True}),
(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', {'italic': True}),
(r'(?<!_)_([^_\n]+?)_(?!_)', {'italic': True}),
(r'<u>(.*?)</u>', {'underline': True}),
(r'~~(.*?)~~', {'strikethrough': True}),
(r'`([^`\n]+)`', {'code': True}),
(r'\$([^$\n]+?)\$', {'math': True}), # LaTeX数学公式支持
]
for idx, segment in enumerate(segments):
text_parts = parse_formatted_text(segment, format_patterns)
for part_text, formats in text_parts:
if part_text: # 只有非空文本才创建run
run = paragraph.add_run(part_text)
set_run_format_with_styles(run, formats, title_level=title_level, is_quote=is_quote)
if idx < len(segments) - 1:
paragraph.add_run().add_break()
def parse_formatted_text(text, format_patterns):
"""解析带格式的文本,返回(文本, 格式)的列表"""
if not text:
return []
parts = []
current_pos = 0
# 查找所有格式标记的位置
all_matches = []
for pattern, format_dict in format_patterns:
for match in re.finditer(pattern, text):
all_matches.append({
'start': match.start(),
'end': match.end(),
'text': match.group(1),
'format': format_dict,
'full_match': match.group(0)
})
# 按开始位置排序
all_matches.sort(key=lambda x: x['start'])
# 处理重叠的匹配(选择最长的匹配)
filtered_matches = []
for match in all_matches:
# 检查是否与已有匹配重叠
overlap = False
for existing in filtered_matches:
if (match['start'] < existing['end'] and match['end'] > existing['start']):
# 有重叠,选择更长的匹配
if len(match['full_match']) > len(existing['full_match']):
filtered_matches.remove(existing)
filtered_matches.append(match)
overlap = True
break
if not overlap:
filtered_matches.append(match)
# 重新按位置排序
filtered_matches.sort(key=lambda x: x['start'])
# 构建文本部分列表
for match in filtered_matches:
# 添加前面的普通文本
if current_pos < match['start']:
normal_text = text[current_pos:match['start']]
if normal_text:
parts.append((normal_text, {}))
# 添加格式化文本
parts.append((match['text'], match['format']))
current_pos = match['end']
# 添加剩余的普通文本
if current_pos < len(text):
remaining_text = text[current_pos:]
if remaining_text:
parts.append((remaining_text, {}))
# 如果没有找到任何格式,返回整个文本作为普通文本
if not parts:
parts.append((text, {}))
return parts
def set_run_format(run, title_level=0):
"""设置文本运行格式(基础版本,用于标题)"""
config = get_config()
font_config = config.get('fonts.default', {})
font = run.font
font.name = font_config.get('ascii', 'Times New Roman')
font.color.rgb = RGBColor(0, 0, 0)
font.bold = False
font.italic = False
font.underline = False
# 获取中文字体名称
east_asia_font = font_config.get('name', '仿宋_GB2312')
# 设置字体映射
run._element.rPr.rFonts.set(qn('w:ascii'), font_config.get('ascii', 'Times New Roman'))
run._element.rPr.rFonts.set(qn('w:hAnsi'), font_config.get('ascii', 'Times New Roman'))
run._element.rPr.rFonts.set(qn('w:eastAsia'), east_asia_font)
run._element.rPr.rFonts.set(qn('w:cs'), font_config.get('ascii', 'Times New Roman'))
# 根据标题级别设置字号、加粗、字体和颜色
if title_level >= 1:
title_config = config.get(f'titles.level{title_level}', config.get('titles.level1', {}))
title_font = title_config.get('font')
title_font_alt = title_config.get('font_alt')
title_color = title_config.get('color')
# 应用标题字体(如果配置了)
if title_font:
font.name = title_font
run._element.rPr.rFonts.set(qn('w:eastAsia'), title_font)
if title_font_alt:
run._element.rPr.rFonts.set(qn('w:ascii'), title_font_alt)
run._element.rPr.rFonts.set(qn('w:hAnsi'), title_font_alt)
# 应用标题颜色(如果配置了)
if title_color:
font.color.rgb = hex_to_rgb(title_color)
# 应用字号和加粗
font.size = Pt(title_config.get('size', 15))
font.bold = title_config.get('bold', True)
else:
font.size = Pt(font_config.get('size', 12))
font.bold = False
def set_run_format_with_styles(run, formats, title_level=0, is_quote=False):
"""设置文本运行格式(支持多种样式)"""
config = get_config()
font_config = config.get('fonts.default', {})
font = run.font
font.name = font_config.get('ascii', 'Times New Roman')
font.color.rgb = RGBColor(0, 0, 0)
# 获取中文字体名称
east_asia_font = font_config.get('name', '仿宋_GB2312')
# 设置字体映射
run._element.rPr.rFonts.set(qn('w:ascii'), font_config.get('ascii', 'Times New Roman'))
run._element.rPr.rFonts.set(qn('w:hAnsi'), font_config.get('ascii', 'Times New Roman'))
run._element.rPr.rFonts.set(qn('w:eastAsia'), east_asia_font)
run._element.rPr.rFonts.set(qn('w:cs'), font_config.get('ascii', 'Times New Roman'))
# 设置基础格式
if title_level >= 1:
title_config = config.get(f'titles.level{title_level}', config.get('titles.level1', {}))
title_font = title_config.get('font')
title_font_alt = title_config.get('font_alt')
title_color = title_config.get('color')
# 应用标题字体(如果配置了)
if title_font:
font.name = title_font
run._element.rPr.rFonts.set(qn('w:eastAsia'), title_font)
if title_font_alt:
run._element.rPr.rFonts.set(qn('w:ascii'), title_font_alt)
run._element.rPr.rFonts.set(qn('w:hAnsi'), title_font_alt)
# 应用标题颜色(如果配置了)
if title_color:
font.color.rgb = hex_to_rgb(title_color)
font.size = Pt(title_config.get('size', 15))
font.bold = title_config.get('bold', True)
elif is_quote:
# 引用使用较小字号
font.size = Pt(9)
font.bold = False
else:
font.size = Pt(font_config.get('size', 12))
font.bold = False
# 应用Markdown格式
if formats.get('code', False):
code_config = config.get('inline_code', {})
font.name = code_config.get('font', 'Times New Roman')
font.size = Pt(code_config.get('size', 10))
font.color.rgb = hex_to_rgb(code_config.get('color', '#333333'))
run._element.rPr.rFonts.set(qn('w:ascii'), font.name)
run._element.rPr.rFonts.set(qn('w:hAnsi'), font.name)
run._element.rPr.rFonts.set(qn('w:eastAsia'), font.name)
elif formats.get('math', False):
math_config = config.get('math', {})
font.name = math_config.get('font', 'Times New Roman')
font.size = Pt(math_config.get('size', 11))
font.italic = math_config.get('italic', True)
font.color.rgb = hex_to_rgb(math_config.get('color', '#00008B'))
run._element.rPr.rFonts.set(qn('w:ascii'), font.name)
run._element.rPr.rFonts.set(qn('w:hAnsi'), font.name)
run._element.rPr.rFonts.set(qn('w:eastAsia'), font.name)
else:
if formats.get('bold', False):
font.bold = True
if formats.get('italic', False):
font.italic = True
if formats.get('underline', False):
font.underline = True
if formats.get('strikethrough', False):
font.strike = True
def set_paragraph_format(paragraph, title_level=0, is_quote=False):
"""设置段落格式"""
config = get_config()
paragraph_config = config.get('paragraph', {})
# 设置段落格式
paragraph_format = paragraph.paragraph_format
paragraph_format.line_spacing = paragraph_config.get('line_spacing', 1.5)
if title_level == 1:
# 一级标题配置
title_config = config.get('titles.level1', {})
align_str = title_config.get('align', 'center')
paragraph_format.alignment = parse_alignment(align_str)
paragraph_format.space_before = Pt(title_config.get('space_before', 6))
paragraph_format.space_after = Pt(title_config.get('space_after', 6))
paragraph_format.first_line_indent = Pt(title_config.get('indent', 0))
elif title_level == 2:
# 二级标题配置
title_config = config.get('titles.level2', {})
align_str = title_config.get('align', 'justify')
paragraph_format.alignment = parse_alignment(align_str)
paragraph_format.space_before = Pt(0)
paragraph_format.space_after = Pt(0)
paragraph_format.first_line_indent = Pt(title_config.get('indent', 24))
elif title_level == 3:
# 三级标题配置
title_config = config.get('titles.level3', {})
align_str = title_config.get('align', 'justify')
paragraph_format.alignment = parse_alignment(align_str)
paragraph_format.space_before = Pt(0)
paragraph_format.space_after = Pt(0)
paragraph_format.first_line_indent = Pt(title_config.get('indent', 24))
elif title_level == 4:
# 四级标题配置
title_config = config.get('titles.level4', {})
align_str = title_config.get('align', 'justify')
paragraph_format.alignment = parse_alignment(align_str)
paragraph_format.space_before = Pt(0)
paragraph_format.space_after = Pt(0)
paragraph_format.first_line_indent = Pt(title_config.get('indent', 24))
elif is_quote:
# 引用:两端对齐,无首行缩进
paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
paragraph_format.space_before = Pt(0)
paragraph_format.space_after = Pt(0)
paragraph_format.first_line_indent = Pt(0)
else:
# 正文段落配置
align_str = paragraph_config.get('align', 'justify')
paragraph_format.alignment = parse_alignment(align_str)
paragraph_format.space_before = Pt(0)
paragraph_format.space_after = Pt(0)
paragraph_format.first_line_indent = Pt(paragraph_config.get('first_line_indent', 24))
# 确保所有runs都有正确的格式
for run in paragraph.runs:
if not hasattr(run.font, 'name') or not run.font.name:
set_run_format(run, title_level)
def parse_alignment(align_str: str):
"""将字符串对齐方式转换为 WD_PARAGRAPH_ALIGNMENT 常量"""
align_str = align_str.lower()
if align_str == 'left':
return WD_PARAGRAPH_ALIGNMENT.LEFT
elif align_str == 'center':
return WD_PARAGRAPH_ALIGNMENT.CENTER
elif align_str == 'right':
return WD_PARAGRAPH_ALIGNMENT.RIGHT
else: # justify
return WD_PARAGRAPH_ALIGNMENT.JUSTIFY
def hex_to_rgb(hex_color: str):
"""将十六进制颜色转换为 RGBColor"""
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return RGBColor(r, g, b)
return RGBColor(0, 0, 0) # 默认黑色
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
表格处理模块
处理 Markdown 表格和 HTML 表格的解析与转换
"""
import re
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
from docx.oxml import parse_xml
from docx.oxml.shared import OxmlElement
from bs4 import BeautifulSoup
# 导入配置模块
from config import Config, get_config
def set_cell_background_color(cell, color_hex):
"""设置单元格背景色"""
if not color_hex:
return
color = color_hex.lstrip('#')
shading_elm = OxmlElement('w:shd')
shading_elm.set(qn('w:val'), 'clear')
shading_elm.set(qn('w:color'), 'auto')
shading_elm.set(qn('w:fill'), color)
cell._tc.get_or_add_tcPr().append(shading_elm)
def is_separator_line(line):
"""判断是否是表格分隔行。分隔行必须包含'-',且只能包含'|', '-', ':', ' '等符号。"""
line = line.strip()
if not line or '-' not in line:
return False
return all(c in '|-: ' for c in line)
def is_table_row(line):
"""判断是否是表格行"""
if not line or not line.strip():
return False
line = line.strip()
# 检查是否是分隔行
if is_separator_line(line):
return True
# 检查是否是数据行(包含 |)
# 这里的逻辑保持宽松,依赖于主循环中对其他块级元素的优先判断
if '|' in line:
return True
return False
def create_word_table(doc, table_lines):
"""从Markdown表格行创建Word表格"""
if len(table_lines) < 2:
return
# 解析表格数据
rows_data = []
header_row = None
for i, line in enumerate(table_lines):
# 跳过分隔行(包含横线的行)
if is_separator_line(line):
continue
# 解析单元格
cells = parse_table_row(line)
if cells:
if header_row is None:
header_row = cells
else:
rows_data.append(cells)
if not header_row:
return
# 确定列数
max_cols = len(header_row)
for row in rows_data:
max_cols = max(max_cols, len(row))
# 创建Word表格
total_rows = 1 + len(rows_data) # 标题行 + 数据行
table = doc.add_table(rows=total_rows, cols=max_cols)
# 获取表格配置
config = get_config()
table_config = config.get('table', {})
border_enabled = table_config.get('border_enabled', True)
border_color = table_config.get('border_color', '#000000')
border_width = table_config.get('border_width', 4)
row_height_cm = table_config.get('row_height_cm', 0.8)
alignment_str = table_config.get('alignment', 'center')
line_spacing = table_config.get('line_spacing', 1.2)
cell_margin = table_config.get('cell_margin', {})
vertical_align_str = table_config.get('vertical_align', 'center')
# 设置表格对齐方式
alignment_map = {
'left': WD_TABLE_ALIGNMENT.LEFT,
'center': WD_TABLE_ALIGNMENT.CENTER,
'right': WD_TABLE_ALIGNMENT.RIGHT
}
table.alignment = alignment_map.get(alignment_str.lower(), WD_TABLE_ALIGNMENT.CENTER)
# 设置垂直对齐
vertical_align_map = {
'top': WD_ALIGN_VERTICAL.TOP,
'center': WD_ALIGN_VERTICAL.CENTER,
'bottom': WD_ALIGN_VERTICAL.BOTTOM
}
vertical_align = vertical_align_map.get(vertical_align_str.lower(), WD_ALIGN_VERTICAL.CENTER)
# 统一设置边框和内边距、行高等
if border_enabled:
try:
tbl = table._tbl
color = border_color.lstrip('#')
borders_xml = f'''
<w:tblBorders xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:top w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:left w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:bottom w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:right w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:insideH w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:insideV w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
</w:tblBorders>
'''
tbl.tblPr.append(parse_xml(borders_xml))
except Exception:
pass
try:
tbl = table._tbl
top = cell_margin.get('top', 30)
bottom = cell_margin.get('bottom', 30)
left = cell_margin.get('left', 60)
right = cell_margin.get('right', 60)
cell_margins_xml = f'''
<w:tblCellMar xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:top w:w="{top}" w:type="dxa"/>
<w:left w:w="{left}" w:type="dxa"/>
<w:bottom w:w="{bottom}" w:type="dxa"/>
<w:right w:w="{right}" w:type="dxa"/>
</w:tblCellMar>
'''
tbl.tblPr.append(parse_xml(cell_margins_xml))
except Exception:
pass
# 行高与段落行距统一
try:
for row in table.rows:
row.height = Cm(row_height_cm)
for cell in row.cells:
cell.vertical_alignment = vertical_align
for paragraph in cell.paragraphs:
pf = paragraph.paragraph_format
pf.line_spacing = line_spacing
pf.space_before = Pt(2)
pf.space_after = Pt(2)
except Exception:
pass
# 获取表头背景色配置
header_bg_color = config.get('table.header', {}).get('background_color')
# 填充标题行
header_cells = table.rows[0].cells
for j, cell_text in enumerate(header_row):
if j < len(header_cells):
cell = header_cells[j]
# 处理表格单元格中的格式
if contains_markdown_formatting(cell_text.strip()):
parse_table_cell_formatting(cell, cell_text.strip(), is_header=True)
else:
# 导入 convert_quotes_to_chinese 避免循环导入
from formatter import convert_quotes_to_chinese
cell.text = convert_quotes_to_chinese(cell_text.strip())
set_table_cell_format(cell, is_header=True)
# 应用表头背景色
if header_bg_color:
set_cell_background_color(cell, header_bg_color)
# 获取交替行颜色配置
row_even_color = config.get('table.row_even', {}).get('background_color')
row_odd_color = config.get('table.row_odd', {}).get('background_color')
# 填充数据行
for i, row_data in enumerate(rows_data):
if i + 1 < len(table.rows):
row_cells = table.rows[i + 1].cells
# 确定当前行颜色(奇偶交替)
row_bg_color = row_odd_color if i % 2 == 0 else row_even_color
for j, cell_text in enumerate(row_data):
if j < len(row_cells):
cell = row_cells[j]
# 处理表格单元格中的格式
if contains_markdown_formatting(cell_text.strip()):
parse_table_cell_formatting(cell, cell_text.strip(), is_header=False)
else:
# 导入 convert_quotes_to_chinese 避免循环导入
from formatter import convert_quotes_to_chinese
cell.text = convert_quotes_to_chinese(cell_text.strip())
set_table_cell_format(cell, is_header=False)
# 应用交替行背景色
if row_bg_color:
set_cell_background_color(cell, row_bg_color)
# 调整列宽
adjust_table_column_width(table)
def parse_table_row(line):
"""解析表格行,提取单元格内容"""
if not line or not line.strip():
return []
line = line.strip()
# 移除开头和结尾的 |
if line.startswith('|'):
line = line[1:]
if line.endswith('|'):
line = line[:-1]
# 分割单元格
cells = [cell.strip() for cell in line.split('|')]
# 过滤掉空单元格(但保留有意义的空单元格)
return cells
def contains_markdown_formatting(text):
"""检查文本是否包含Markdown格式标记"""
format_patterns = [
r'\*\*\*.*?\*\*\*', # 加粗斜体
r'\*\*.*?\*\*', # 加粗
r'\*.*?\*', # 斜体
r'___.*?___', # 加粗斜体
r'__.*?__', # 加粗
r'_.*?_', # 斜体
r'<u>.*?</u>', # 下划线
r'~~.*?~~', # 删除线
r'`.*?`', # 行内代码
r'<br\s*/?>', # 换行标签
r'\$.*?\$', # LaTeX数学公式
]
for pattern in format_patterns:
if re.search(pattern, text):
return True
return False
def parse_table_cell_formatting(cell, text, is_header=False):
"""解析表格单元格中的格式化文本"""
# 清空单元格
cell.text = ""
# 导入 convert_quotes_to_chinese 和 parse_formatted_text 避免循环导入
from formatter import convert_quotes_to_chinese, parse_formatted_text
# 转换引号
text = convert_quotes_to_chinese(text)
# 支持<br>换行:拆分后逐段处理
parts_by_br = re.split(r'<br\s*/?>', text, flags=re.IGNORECASE)
# 解析格式
format_patterns = [
(r'\*\*\*(.*?)\*\*\*', {'bold': True, 'italic': True}),
(r'___(.*?)___', {'bold': True, 'italic': True}),
(r'\*\*(.*?)\*\*', {'bold': True}),
(r'__(.*?)__', {'bold': True}),
(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', {'italic': True}),
(r'(?<!_)_([^_\n]+?)_(?!_)', {'italic': True}),
(r'<u>(.*?)</u>', {'underline': True}),
(r'~~(.*?)~~', {'strikethrough': True}),
(r'`([^`\n]+)`', {'code': True}),
(r'\$([^$\n]+?)\$', {'math': True}), # LaTeX数学公式支持
]
for idx, segment in enumerate(parts_by_br):
if idx > 0:
cell.paragraphs[0].add_run().add_break()
text_parts = parse_formatted_text(segment, format_patterns)
for part_text, formats in text_parts:
if part_text: # 只有非空文本才创建run
run = cell.paragraphs[0].add_run(part_text)
set_table_run_format(run, formats, is_header)
def set_table_run_format(run, formats, is_header=False):
"""设置表格单元格run格式"""
config = get_config()
if is_header:
header_config = config.get('table.header', {})
font_name = header_config.get('font', 'Times New Roman')
font_size = header_config.get('size', 10.5)
color_hex = header_config.get('color', '#000000')
bold = header_config.get('bold', True)
else:
body_config = config.get('table.body', {})
font_name = body_config.get('font', '仿宋_GB2312')
font_size = body_config.get('size', 10.5)
color_hex = body_config.get('color', '#000000')
bold = False
font = run.font
font.name = 'Times New Roman' # 默认英文字体
font.size = Pt(font_size)
font.color.rgb = hex_to_rgb(color_hex)
font.bold = bold if is_header else False
# 设置字体映射:英文和数字用Times New Roman,中文用配置的字体
run._element.rPr.rFonts.set(qn('w:ascii'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:hAnsi'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
run._element.rPr.rFonts.set(qn('w:cs'), 'Times New Roman')
# 应用Markdown格式
if formats.get('bold', False):
font.bold = True
if formats.get('italic', False):
font.italic = True
if formats.get('underline', False):
font.underline = True
if formats.get('strikethrough', False):
font.strike = True
if formats.get('code', False):
# 表格中代码使用Times New Roman,稍小字号
code_config = config.get('inline_code', {})
font.name = code_config.get('font', 'Times New Roman')
font.size = Pt(9)
font.color.rgb = hex_to_rgb(code_config.get('color', '#333333'))
run._element.rPr.rFonts.set(qn('w:ascii'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:hAnsi'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Times New Roman')
return
if formats.get('math', False):
# 表格中数学公式使用Times New Roman,斜体,深蓝色
math_config = config.get('math', {})
font.name = math_config.get('font', 'Times New Roman')
font.size = Pt(math_config.get('size', 10))
font.italic = math_config.get('italic', True)
font.color.rgb = hex_to_rgb(math_config.get('color', '#00008B'))
run._element.rPr.rFonts.set(qn('w:ascii'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:hAnsi'), 'Times New Roman')
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Times New Roman')
return
def set_table_cell_format(cell, is_header=False):
"""设置表格单元格格式"""
config = get_config()
table_config = config.get('table', {})
line_spacing = table_config.get('line_spacing', 1.2)
if is_header:
header_config = config.get('table.header', {})
font_name = header_config.get('font', 'Times New Roman')
font_size = header_config.get('size', 10.5)
color_hex = header_config.get('color', '#000000')
bold = header_config.get('bold', True)
else:
body_config = config.get('table.body', {})
font_name = body_config.get('font', '仿宋_GB2312')
font_size = body_config.get('size', 10.5)
color_hex = body_config.get('color', '#000000')
bold = False
# 设置段落格式
for paragraph in cell.paragraphs:
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 居中对齐
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
paragraph.paragraph_format.line_spacing = line_spacing
# 设置文字格式
for run in paragraph.runs:
font = run.font
font.name = font_name
font.size = Pt(font_size)
font.color.rgb = hex_to_rgb(color_hex)
font.bold = bold if is_header else False
# 设置中文字体
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
def adjust_table_column_width(table):
"""调整表格列宽"""
try:
# 获取表格总宽度(页面宽度减去页边距)
available_width = Cm(21.0 - 3.18 * 2) # A4宽度减去左右页边距
# 平均分配列宽
col_count = len(table.columns)
if col_count > 0:
col_width = int(available_width / col_count) # 转换为整数
for column in table.columns:
column.width = col_width
except Exception as e:
print(f"⚠️ 表格列宽调整失败: {e}")
def parse_html_table(html_content):
"""解析HTML表格内容,返回表格数据"""
try:
soup = BeautifulSoup(html_content, 'html.parser')
table = soup.find('table')
if not table:
return None
rows_data = []
for tr in table.find_all('tr'):
row_cells = []
for cell in tr.find_all(['td', 'th']):
# 获取单元格文本内容,保留基本格式
cell_text = cell.get_text(strip=True)
row_cells.append(cell_text)
if row_cells: # 只添加非空行
rows_data.append(row_cells)
return rows_data
except Exception as e:
print(f"⚠️ HTML表格解析失败: {e}")
return None
def create_word_table_from_html(doc, html_content):
"""从HTML表格创建Word表格"""
rows_data = parse_html_table(html_content)
if not rows_data or len(rows_data) < 1:
print("⚠️ HTML表格数据为空或格式不正确")
return
# 导入 convert_quotes_to_chinese 避免循环导入
from formatter import convert_quotes_to_chinese
# 获取表格配置
config = get_config()
table_config = config.get('table', {})
border_enabled = table_config.get('border_enabled', True)
border_color = table_config.get('border_color', '#000000')
border_width = table_config.get('border_width', 4)
row_height_cm = table_config.get('row_height_cm', 0.8)
line_spacing = table_config.get('line_spacing', 1.2)
cell_margin = table_config.get('cell_margin', {})
vertical_align_str = table_config.get('vertical_align', 'center')
# 创建Word表格
table = doc.add_table(rows=len(rows_data), cols=len(rows_data[0]))
# 设置表格对齐方式
alignment_str = table_config.get('alignment', 'center')
alignment_map = {
'left': WD_TABLE_ALIGNMENT.LEFT,
'center': WD_TABLE_ALIGNMENT.CENTER,
'right': WD_TABLE_ALIGNMENT.RIGHT
}
table.alignment = alignment_map.get(alignment_str.lower(), WD_TABLE_ALIGNMENT.CENTER)
# 设置垂直对齐
vertical_align_map = {
'top': WD_ALIGN_VERTICAL.TOP,
'center': WD_ALIGN_VERTICAL.CENTER,
'bottom': WD_ALIGN_VERTICAL.BOTTOM
}
vertical_align = vertical_align_map.get(vertical_align_str.lower(), WD_ALIGN_VERTICAL.CENTER)
# 设置表格边框和单元格边距
if border_enabled:
try:
tbl = table._tbl
color = border_color.lstrip('#')
borders_xml = f'''
<w:tblBorders xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:top w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:left w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:bottom w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:right w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:insideH w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
<w:insideV w:val="single" w:sz="{border_width}" w:space="0" w:color="{color}"/>
</w:tblBorders>
'''
tbl.tblPr.append(parse_xml(borders_xml))
except Exception:
pass
try:
tbl = table._tbl
top = cell_margin.get('top', 30)
bottom = cell_margin.get('bottom', 30)
left = cell_margin.get('left', 60)
right = cell_margin.get('right', 60)
cell_margins_xml = f'''
<w:tblCellMar xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:top w:w="{top}" w:type="dxa"/>
<w:left w:w="{left}" w:type="dxa"/>
<w:bottom w:w="{bottom}" w:type="dxa"/>
<w:right w:w="{right}" w:type="dxa"/>
</w:tblCellMar>
'''
tbl.tblPr.append(parse_xml(cell_margins_xml))
except Exception:
pass
# 设置行高和单元格对齐
try:
for row in table.rows:
row.height = Cm(row_height_cm)
for cell in row.cells:
cell.vertical_alignment = vertical_align
for paragraph in cell.paragraphs:
pf = paragraph.paragraph_format
pf.line_spacing = line_spacing
pf.space_before = Pt(2)
pf.space_after = Pt(2)
except Exception:
pass
# 填充表格数据
for i, row_data in enumerate(rows_data):
if i < len(table.rows):
row_cells = table.rows[i].cells
for j, cell_text in enumerate(row_cells):
if j < len(row_cells):
cell = row_cells[j]
cell.text = convert_quotes_to_chinese(cell_text.strip())
# 第一行作为标题行处理
set_table_cell_format(cell, is_header=(i == 0))
# 调整列宽
adjust_table_column_width(table)
print(f"✅ 处理HTML表格: {len(rows_data)} 行")
def hex_to_rgb(hex_color: str):
"""将十六进制颜色转换为 RGBColor"""
from docx.shared import RGBColor
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return RGBColor(r, g, b)
return RGBColor(0, 0, 0) # 默认黑色