
Md2word
- 174 installs
- 543 repo stars
- Updated August 5, 2026
- cat-xierluo/legal-skills
Convert agent-authored Markdown memos, policies, and briefs into Word files counsel and clients can redline, comment on, and circulate.
About
md2word from cat-xierluo/legal-skills converts Markdown legal drafts into Word documents with usable structure for review. It closes the gap between agent-generated memos in repos and the DOCX files lawyers expect for comments, redlines, and client distribution.
- Markdown to DOCX conversion
- Preserves headings and lists
- Supports legal memo handoff
- Enables Word track-changes workflow
- Bridges agent output to Office
Md2word by the numbers
- 174 all-time installs (skills.sh)
- Ranked #271 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cat-xierluo/legal-skills --skill md2wordAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 174 |
|---|---|
| repo stars | ★ 543 |
| Last updated | August 5, 2026 |
| Repository | cat-xierluo/legal-skills ↗ |
What it does
Convert agent-authored Markdown memos, policies, and briefs into Word files counsel and clients can redline, comment on, and circulate.
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: 1.0
max_width_cm: 14.64
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: "#5A4E48" # 深蓝 - 表头、一级标题
accent: "#5A4E48" # 深棕 - 高亮(方案名、价格)
alt: "#F5F0ED" # 浅棕 - 交替行背景
# 封面页
cover_page:
enabled: true
title:
font: "微软雅黑"
size: 22
bold: true
color: "#5A4E48"
alignment: center
space_before_pt: 120
space_after_pt: 36
meta:
font: "仿宋"
size: 12
color: "#333333"
alignment: center
line_spacing: 1.8
# 页面设置
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: "#5A4E48"
font: "微软雅黑"
space_before: 16
space_after: 12
level2:
size: 14 # 小三
bold: true
align: "left"
color: "#5A4E48"
font: "微软雅黑"
space_before: 14
space_after: 10
indent: 24
level3:
size: 12 # 小四
bold: true
align: "left"
color: "#5A4E48"
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: "#5A4E48"
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: "#5A4E48"
price:
font: "仿宋"
size: 12
bold: true
color: "#5A4E48"
# 代码块
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
font_size: 12
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 技能的所有重要变更。
待优化事项
Word 格式微调(持续优化中)
已完成(已并入 [1.0.3]):
- 表格中含格式文本(加粗等)的单元格未居中
- 二级/三级标题段前段后硬编码为 0pt
- 二级标题前自动插入空段落
仍待观察/后续可能调整:
- 四级标题的 space_before/space_after 同样硬编码为 0pt,是否需要读取配置
- 正文段落的段前段后间距(目前为 0pt),实际使用中是否需要微调
- 表格列宽自动分配策略,当前列宽是否合理
- 列表项的行距和缩进,与正文的协调性
[1.0.3] - 2026-06-09
改进
- HTML/CSS 对齐语法扩展:原脚本只支持 CSS
style="text-align: ..."写法,现扩展为同时支持 HTMLalign属性 - 支持
<div align="right">/<div align=right>(无引号) /<div align='right'>三种写法 - 大小写不敏感:
<DIV ALIGN="RIGHT">也能正确识别 - 支持中文引号(
align="right") - 块级标签范围扩展到
span/section/article(原仅div/p) - 重构对齐解析为独立函数
formatter.extract_alignment(style_attr),便于后续任务(HTML 样式扩展)复用 - 模板加载行为变更(默认关闭):
find_template_file()默认返回None(auto=False),解决"默认 docx 带了律所 logo 页眉"问题 - 根因:原
find_template_file()默认会从assets/templates/自动加载第一个.docx模板,模板 header 含律所 logo - 用户显式需要时可用
--template path/to/file.docx或新加的--auto-template开关 - `create_word_document(template_file=None)` 显式不加载模板的行为保持不变(向后兼容)
修复
- 表格中含格式文本(加粗等)的单元格未居中:修复
table_handler.py:274parse_table_cell_formatting()缺少段落对齐设置(沿用 [1.0.2] 之后的格式微调) - 二级/三级标题段前段后硬编码为 0pt:改为读取
formatter.py:358-359, 366-367中titles.levelN配置值(legal 预设为 9pt) - 二级标题前自动插入空段落导致多余空行:移除该逻辑,间距由标题样式的
space_before/space_after控制 - 四级标题段前段后硬编码为 0pt:
formatter.py:374-375改为读取titles.level4配置(与 H1-H3 对齐),未配置时回退 0pt。用户调legal.yaml的titles.level4.space_before/space_after现在生效。
技术优化
- 测试基建:建立
pytest测试体系(pytest.ini+tests/conftest.py+.venv/),含 6 个端到端测试(tests/test_html_alignment.py)、17 个extract_alignment单元测试(tests/test_extract_alignment.py)、5 个模板加载测试(tests/test_template_loading.py)、4 个标题间距测试(tests/test_heading_spacing.py),全部 32 个测试通过 - 解析函数抽离:将 HTML 对齐解析从主流程
md2word.py抽到formatter.extract_alignment,明确职责边界 - 依赖防护:
.venv/包含pytest/python-docx/beautifulsoup4/Pillow/PyYAML,完整可运行环境
文档完善
- TASKS.md 结构化:从简单 bullet 升级为结构化任务卡片(字段:优先级 / 关联文件 / 估计工作量 / 依赖 / 背景 / 验收);原 8 条任务按"已完成/高/中/调研/远期"5 档重排,所有远期任务补全详细说明
[1.0.2] - 2026-04-11
新增
- 外部URL图片支持: 支持从 Markdown 中的外部 URL 图片自动下载并嵌入 Word 文档
- 新增
download_external_image()函数,通过urllib.request下载外链图片 - 支持本地路径图片和 HTTP/HTTPS 外链图片
- 自动居中插入图片,复用现有
_postprocess_image_for_word()和insert_image_to_word()管线 - 图片下载失败时自动降级为文字占位符
[图片: alt文本] - 修复正则表达式以兼容 URL 中含括号的情况(如
no_upscale()?imageUrl=...)
文档完善
- 2026-04-22:按独立仓库 README 新规范重写首页,补充典型场景、预设范围、可执行安装命令、使用示例、边界说明、关键文件入口、Legal Skills 关联项目导流、作者联系入口和微信二维码
[1.0.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)依赖,应属于配置管理模块
- 修复了 v1.0.0 重构时引入的循环导入问题
[1.0.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
将 Markdown 文档转换为符合中文排版习惯的 Word 文档,适合正式报告、法律文书、服务方案、论文和工作材料。
写 Markdown,交付 Word。把标题、表格、图片、代码块和基础版式交给脚本处理,减少手动调格式。
典型场景
用户:请把这份 Markdown 服务方案转成正式 Word,使用法律服务方案风格。
AI:我会调用 md2word,选择 service-plan 预设,生成排版后的 .docx 文件。它能产出什么
.docxWord 文档- 按预设应用的标题、正文、页边距、表格和代码块样式
- 自动嵌入本地图片和外部 URL 图片
- Mermaid 失败时的降级文本占位
- 可复用的自定义 YAML 配置
当前覆盖范围
内置常用预设:
legal:法律文书格式,默认预设service-plan:法律服务方案,含分层配色minimal:极简正式文档academic:学术论文report:工作报告
完整预设以 assets/presets/*.yaml 为准,可运行以下命令查看:
python scripts/config.py --list安装方式
1. 打开本仓库的 GitHub Releases。 2. 下载最新版本的 skill 压缩包。 3. 解压后将 md2word/ 文件夹放入你的 skill 目录。 4. 安装 Python 依赖:
pip install python-docx Pillow beautifulsoup4 PyYAML如需渲染 Mermaid 图表,可选安装:
npm install -g @mermaid-js/mermaid-cli可以怎么用
# 基本转换
python scripts/md2word.py input.md output.docx
# 使用预设
python scripts/md2word.py input.md output.docx --preset legal
# 使用自定义配置
python scripts/md2word.py input.md output.docx --config my-config.yaml也可以直接让 Agent 帮你选择预设:
- “把这份 Markdown 转成正式法律文书 Word”
- “用学术论文格式导出这份论文草稿”
- “把报告转成 Word,外链图片也嵌入进去”
使用边界
这个 skill 适合:
- Markdown 到 Word 的批量或重复转换
- 中文正式文档的基础排版
- 需要预设样式、图片、表格和代码块的文档
这个 skill 不适合:
- 精细到每一页版面都要人工设计的复杂 Word 模板
- 依赖 Word 高级域、复杂目录、批注修订或宏的文档
- 从 PDF、扫描件或图片中抽取内容后再排版;这类任务应先用 OCR 或文档解析工具
关键文件
- SKILL.md:Agent 使用入口
- scripts/md2word.py:主转换脚本
- references/config-reference.md:配置项说明
- references/style-mappings.md:Markdown 到 Word 样式映射
- assets/config-template.yaml:自定义配置模板
许可证
本作品采用 MIT 许可证。
关于作者 / 咨询与交流
杨卫薪律师(微信 ywxlaw)
如需使用交流、企业内部落地、定制开发或商用授权,欢迎添加微信(请注明来意)。
<div align="center"> <img src="https://raw.githubusercontent.com/cat-xierluo/legal-skills/main/wechat-qr.jpg" width="200" alt="微信二维码"/> <p><em>微信:ywxlaw</em></p> </div>
关联项目
本仓库是 Legal Skills 的子项目。如果需要合同、商标、专利、OPC、小微企业合规、文档处理等更多法律类开源 Skill,可以关注主仓库。
相关项目:
- contract-copilot:合同审查、起草和 Word 修订批注
- legal-proposal-generator:法律服务方案生成
- de-ai-polish:中文文章去 AI 腔和自然化润色
配置架构参考
本文档提供 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:
orientation: portrait # 页面方向: portrait(纵向,默认) / landscape(横向)
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': 12,
'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'<strong>(.*?)</strong>', {'bold': True}),
(r'<b>(.*?)</b>', {'bold': True}),
(r'<em>(.*?)</em>', {'italic': True}),
(r'<i>(.*?)</i>', {'italic': True}),
(r'<u>(.*?)</u>', {'underline': True}),
(r'~~(.*?)~~', {'strikethrough': True}),
(r'<s>(.*?)</s>', {'strikethrough': True}),
(r'<del>(.*?)</del>', {'strikethrough': True}),
(r'<strike>(.*?)</strike>', {'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(12)
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(title_config.get('space_before', 9))
paragraph_format.space_after = Pt(title_config.get('space_after', 9))
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(title_config.get('space_before', 9))
paragraph_format.space_after = Pt(title_config.get('space_after', 9))
paragraph_format.first_line_indent = Pt(title_config.get('indent', 24))
elif title_level == 4:
# 四级标题配置(与 H1-H3 对齐读 titles.level4,未配置时回退 0)
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(title_config.get('space_before', 0))
paragraph_format.space_after = Pt(title_config.get('space_after', 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
_ALIGN_VALUES = r'(left|center|right|justify)'
# 支持的引号字符:ASCII 双/单 + 中文双开/闭 + 中文单开/闭
_QUOTE_CHARS = r'["\'“”‘’]'
def extract_alignment(style_attr: str):
"""从 HTML 标签属性串中解析对齐方式。
支持写法(按优先级):
1. CSS ``text-align: <value>`` (如 ``<div style="text-align: right">``)
2. HTML ``align="<value>"`` / ``align=<value>`` / ``align='<value>'`` (ASCII 与中文引号均支持)
大小写不敏感。未命中时返回 ``None``,调用方保持默认行为。
"""
if not style_attr:
return None
# 1. CSS text-align
m = re.search(r'text-align\s*:\s*' + _ALIGN_VALUES, style_attr, re.IGNORECASE)
if m:
return parse_alignment(m.group(1).lower())
# 2. HTML align 属性
# 要求 align 前面不是 word 字符也不是 `-`(避免误匹配 data-align / xalign),
# 且 value 之后必须是引号、空白或字符串末尾(避免误匹配 justifyAll)
m = re.search(
r'(?<![\w-])align\s*=\s*' + _QUOTE_CHARS + '?' + _ALIGN_VALUES + r'(?:' + _QUOTE_CHARS + r'|\s|$)',
style_attr,
re.IGNORECASE,
)
if m:
return parse_alignment(m.group(1).lower())
return None
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) # 默认黑色