关于 docx-editor-cn
面向 DOCX 文档的创建、编辑与分析技能,提供通过 JavaScript 生成文档和通过 XML 修改现有文件的做法。内容还涵盖中文学术论文的页面、标题、表格、图注及公式格式规范。
主要功能
- 01读取与分析 DOCX 内容
- 02创建并验证新文档
- 03解包、编辑和重打包 XML
- 04插入三线表与公式
- 05中文学术排版规范
- 06处理修订和格式转换
适用场景
- 01生成符合规范的中文论文
- 02在现有文档中插入表格或公式
- 03提取修订内容并整理文档
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gostyan/ docx-skill-4-cn-paper --skill docx-editor-cn安装后在 Claude Code 中输入/docx-editor-cn确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.txtpackage-lock.jsonpackage.jsonREADME.mdscripts/__init__.pyaccept_changes.pycomment.pyconvert_paper.jsformula.pymathml-to-docx.jsnew_doc.jsscripts/office/pack.pyscripts/office/helpers/__init__.pymerge_runs.pysimplify_redlines.pyscripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsdopc-coreProperties.xsdopc-digSig.xsdopc-relationships.xsdscripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsddml-chartDrawing.xsddml-diagram.xsddml-lockedCanvas.xsddml-main.xsddml-picture.xsddml-spreadsheetDrawing.xsddml-wordprocessingDrawing.xsdpml.xsdshared-additionalCharacteristics.xsdshared-bibliography.xsd详细介绍
DOCX 创建、编辑和分析
概述
.docx 文件是一个包含 XML 文件的 ZIP 归档。
快速参考
| 任务 | 方法 |
|---|---|
| 读取/分析内容 | 使用 pandoc,或解包以查看原始 XML |
| 创建新文档 | node scripts/new_doc.js(先编辑 CONTENT 部分) |
| 编辑现有文档 | 解包 → 编辑 XML → 重新打包——参见下方“编辑现有文档” |
| 插入三线表(编辑 XML) | python scripts/table.py unpacked/ "1-1" "标题" --headers … --rows … |
| 插入块级公式(编辑 XML) | python scripts/formula.py unpacked/ "LaTeX" 1 --anchor "锚文本" |
将 .doc 转换为 .docx
必须先转换旧版 .doc 文件才能进行编辑:
python scripts/office/soffice.py --headless --convert-to docx document.doc
读取内容
# Text extraction with tracked changes
pandoc --track-changes=all document.docx -o output.md
# Raw XML access
python scripts/office/unpack.py document.docx unpacked/
转换为图像
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page
接受修订
要生成接受所有修订的干净文档(需要 LibreOffice):
python scripts/accept_changes.py input.docx output.docx
创建新文档
使用 JavaScript 生成 .docx 文件,然后进行验证。安装:npm install -g docx
设置
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
VerticalAlign, PageNumber, PageBreak } = require('docx');
const doc = new Document({ sections: [{ children: [/* content */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
验证
创建文件后,请进行验证。如果验证失败,请解包、修复 XML,然后重新打包。
python scripts/office/validate.py doc.docx
页面大小
// Chinese academic papers use A4 with 2.5cm margins on all sides
sections: [{
properties: {
page: {
size: {
width: 11906, // A4 width in DXA
height: 16838 // A4 height in DXA
},
margin: { top: 1418, right: 1418, bottom: 1418, left: 1418 } // 2.5cm = 1418 DXA
}
},
children: [/* content */]
}]
常用页面大小(DXA 单位,1440 DXA = 1 英寸,567 DXA = 1 厘米):
| 纸张 | 宽度 | 高度 | 内容宽度(边距 2.5 厘米) |
|---|---|---|---|
| A4(中国标准) | 11,906 | 16,838 | 9,070 |
| US Letter | 12,240 | 15,840 | 9,404 |
横向: docx-js 会在内部交换宽度和高度,因此请传入纵向尺寸,并由它处理交换:
size: {
width: 12240, // Pass SHORT edge as width
height: 15840, // Pass LONG edge as height
orientation: PageOrientation.LANDSCAPE // docx-js swaps them in the XML
},
// Content width = 15840 - left margin - right margin (uses the long edge)
样式(学术论文格式)
重要:生成学术论文、课程设计或数学文档时,必须严格遵守以下中文学术格式规范。
1. 全局页面与正文规范
- 页面:A4(11906 × 16838 DXA),页边距均为 2.5cm(
1418DXA)。 - 字体:中文 = 宋体,英文/数学/代码 = Cambria Math(默认);英文文本也可使用 Times New Roman。
- 字号:12pt(小四)→ docx-js 中为
size: 24(半磅)。 - 段落:首行缩进 2 个字符(
firstLine: 480DXA),默认行距为单倍(line: 240, lineRule: LineRuleType.AUTO);可选:固定 20pt(line: 400, lineRule: LineRuleType.EXACT)或 1.5 倍(line: 360, lineRule: LineRuleType.AUTO)。段前/段后间距为 0pt。
2. 标题规范
- 标题 1(一级标题):黑体,16pt(三号,
size: 32),加粗,居中,1.2 倍行距(line: 288)。自动编号:阿拉伯数字1、2、3(显示在标题文本中或手动添加“一、二、”)。参见下文“标题与参考文献自动编号”。 - 标题 2(二级标题):黑体,14pt(四号,
size: 28),加粗,左对齐,1.5 倍行距(line: 360)。自动编号:1.1、1.2、2.1(随章递增的阿拉伯数字层级编号)。 - 标题 3(三级标题):黑体,12pt(小四,
size: 24),加粗,左对齐,1.1 倍行距(line: 264)。自动编号:1.1.1、2.4.1。
⚠️ 标题编号规则:所有三级标题必须使用单一的全阿拉伯数字多级编号配置。这是确保跨章编号正确递增(1.1、2.1、2.4 等),同时让 H2/H3 编号保持纯阿拉伯数字的唯一方法。如果 H1 级别使用 CHINESE_COUNTING,H2 将显示为 二.4,而不是 2.4——这是已知的 OOXML 渲染问题。
3. 图表与公式规范
- 图标题:宋体,11pt(
size: 22),居中,加粗。间距:单倍行距,段前 0.5 行(before: 120),段后 3pt(after: 60)。位置:图下方。格式:“图 章-图序”,例如“图 1-1”。 - 表标题:宋体,11pt(
size: 22),居中,无缩进,加粗。间距:单倍行距,段前 0.5 行(before: 120),段后 3pt(after: 60)。位置:表上方。格式:“表 章-表序”,例如“表 1-1”。表格必须采用三线表样式(顶线和底线加粗,表头下方为细线,不设其他边框)。 - 独立公式:使用公式表格布局——无边框的 3 列表格:[1cm 间隔列 | 居中公式 | 右对齐公式编号,宽 1cm]。使用
scripts/formula.py生成 XML。行内公式:以纯文本形式书写,并设置为斜体。
创建新文档——使用模板脚本:
# Copy the template, edit the CONTENT section, then run:
node scripts/new_doc.js
# Outputs output.docx with all styles, numbering, and page setup pre-configured.
# Change OUTPUT_PATH inside the script if needed.
scripts/new_doc.js 提供了可直接使用的辅助函数:h1/h2/h3(text)、body(text)、tableCaption/figCaption(label)、threeLineTable(headers, rows, colWidths)、ref(text)、blank()。文件顶部的 STYLES / NUMBERING / 页面常量是下文所有规范的权威实现;具体 docx-js 值请参阅该文件。
列表(绝不使用 Unicode 项目符号)
// ❌ WRONG - never manually insert bullet characters
new Paragraph({ children: [new TextRun("• Item")] }) // BAD
new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD
// ✅ CORRECT - use numbering config with LevelFormat.BULLET
const doc = new Document({
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
children: [
new Paragraph({ numbering: { reference: "bullets", level: 0 },
children: [new TextRun("Bullet item")] }),
new Paragraph({ numbering: { reference: "numbers", level: 0 },
children: [new TextRun("Numbered item")] }),
]
}]
});
// ⚠️ Each reference creates INDEPENDENT numbering
// Same reference = continues (1,2,3 then 4,5,6)
// Different reference = restarts (1,2,3 then 1,2,3)
表格(三线表)
中文学术论文必须使用三线表:顶线/底线加粗(1.5pt),表头下方为细线(0.75pt),不设其他边框。标题置于表格上方。
新建文档——使用 scripts/new_doc.js 中的 threeLineTable() 辅助函数:
// Inside the CONTENT array:
tableCaption('符号说明'),
threeLineTable(
['符号', '说明'], // headers
[['S', '状态空间'], ['A', '动作空间']], // data rows
[1800, 7270] // DXA column widths; must sum to 9070
),
编辑现有文档(XML)——使用 scripts/table.py:
# Insert after paragraph containing anchor text:
python scripts/table.py unpacked/ "1-1" "符号说明" \\
--headers "符号,说明" \\
--rows '[["S","状态空间"],["A","动作空间"]]' \\
--anchor "以下是符号说明"
# Custom column widths (must sum to 9070):
python scripts/table.py unpacked/ "2-1" "参数" \\
--headers "参数,取值,说明" \\
--rows '[["lr","0.001","学习率"]]' \\
--widths "1800,1500,5770"
# Print XML snippet only (no file modification):
python scripts/table.py --caption "示例" \\
--headers "列1,列2" --rows '[["a","b"]]'
关键规则:
- 内容宽度 = 9070 DXA(A4 纸、2.5cm 页边距);
columnWidths的总和必须恰好为 9070 - 始终使用
WidthType.DXA——绝不使用WidthType.PERCENTAGE - 表格宽度 =
columnWidths的总和;每个单元格也必须设置相同的width
图片
// CRITICAL: type parameter is REQUIRED
new Paragraph({
children: [new ImageRun({
type: "png", // Required: png, jpg, jpeg, gif, bmp, svg
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Title", description: "Desc", name: "Name" } // All three required
})]
})
分页符
// CRITICAL: PageBreak must be inside a Paragraph
new Paragraph({ children: [new PageBreak()] })
// Or use pageBreakBefore
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
目录
// The overridden Heading1/2/3 styles (defined in paragraphStyles above) include outlineLevel,
// which TableOfContents uses to build the TOC. Using heading: HeadingLevel.HEADING_X on
// each paragraph applies that overridden style automatically — giving both TOC support AND
// the custom SimHei/size formatting. Do NOT skip paragraphStyles and rely on built-in styles.
new TableOfContents("目录", { hyperlink: true, headingStyleRange: "1-3" })
// Apply to heading paragraphs — heading property applies the overridden style:
new Paragraph({
heading: HeadingLevel.HEADING_1, // → w:pStyle "Heading1" (SimHei 16pt, outlineLevel:0)
children: [new TextRun("一、引言")]
})
new Paragraph({
heading: HeadingLevel.HEADING_2, // → w:pStyle "Heading2" (SimHei 14pt, outlineLevel:1)
children: [new TextRun("1.1 研究背景")]
})
页眉/页脚
sections: [{
properties: {
page: { margin: { top: 1418, right: 1418, bottom: 1418, left: 1418 } } // 2.5cm = 1418 DXA
},
headers: {
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
},
footers: {
default: new Footer({ children: [new Paragraph({
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
})] })
},
children: [/* content */]
}]
标题自动编号(按章节重置 Word 编号)
H1 使用直接写入文本的中文数字(一、二、三)。H2/H3 使用 Word 自动编号,并通过按章节划分的编号引用(sections_c1、sections_c2……)实现。每个章节边界都会自动重置 H2/H3 计数器——在章节内添加或删除节时,编号会自动全部更新。
关键——编号格式规则:
多级配置中的 H1 绝不能使用 LevelFormat.CHINESE_COUNTING——H2 文本中的 %1 会展开为“二”,从而得到 二.4。每个级别都应使用 LevelFormat.DECIMAL。
编号引用由 buildNumberingConfig(chapterCount) 动态生成。每个章节都有自己的 sections_c{N} 引用,包含 2 个级别(H2、H3)。这样可确保 H2/H3 按章节重置,同时由 Word 处理所有编号。
// H1 — Chinese numerals in text (no Word numbering)
h1Chinese('一、引言') // → "一、引言" (Heading1 style)
// H2 — Word auto-numbered, chapter.section format (1.1, 2.4)
h2('研究背景') // → "1.1 研究背景" (Word: sections_c1, level 0)
h2('文献综述') // → "1.2 文献综述"
// H3 — Word auto-numbered, chapter.section.subsection format (1.1.1)
h3('研究现状') // → "1.1.1 研究现状" (Word: sections_c1, level 1)
h3('存在不足') // → "1.1.2 存在不足"
// Next chapter — H2/H3 auto-reset with new chapter prefix
h1Chinese('二、方法') // _chapter increments, sections_c2 used
h2('算法设计') // → "2.1 算法设计" (reset + "2." prefix)
h3('数据结构') // → "2.1.1 数据结构"
_chapter 计数器由 h1Chinese() 递增,并由 h2()、h3()、figCaption() 和 tableCaption() 用于选择正确的编号引用/SEQ 标识符。
图注自动编号(SEQ 字段)
图注使用 Word SEQ 字段进行按章节自动编号。章节编号是一个字面前缀,序列计数器则按章节区分(例如,figure_c3、table_c1)。
// Figure captions — auto: "图 章-序 描述"
figCaption('系统架构图') // → "图 1-1 系统架构图" (SEQ figure_c1)
figCaption('数据流图') // → "图 1-2 数据流图"
// Table captions — auto: "表 章-序 描述"
tableCaption('符号说明') // → "表 1-1 符号说明" (SEQ table_c1)
只传入描述文本。切勿手动添加“图 3-1”或“表 1-1”前缀——SEQ 字段会自动处理编号。
Markdown 编号剥离
转换 Markdown 时,使用这些辅助函数从输入文本中剥离手动编号:
stripH1Number('一、引言') // → "引言"
stripH2Number('1.1 研究背景') // → "研究背景"
stripCaptionNumber('图 3-1 系统') // → "系统"
块级公式布局
块级公式使用无边框的 3 列表格,以便将公式编号右对齐,同时将公式居中。使用 scripts/formula.py 自动生成并插入公式块。
# Generate a formula block and insert after paragraph containing anchor text:
python scripts/formula.py unpacked/ "Q_n(x,a) = r + \\gamma \\max_{a'} Q_{n-1}" 1 --anchor "由此可得"
# Output XML snippet only (no insertion):
python scripts/formula.py --latex "E=mc^2" --number 2
布局(A4,内容宽度为 9070 DXA):
┌──────────┬──────────────────────────┬──────────┐
│ 1cm gap │ formula (centered) │ (n) │
│ 567 DXA │ 7936 DXA │ 567 DXA │
└──────────┴──────────────────────────┴──────────┘
no border throughout; single line spacing
如果更倾向于直接编辑 XML,基本结构如下:
<w:tbl>
<w:tblPr>
<w:tblW w:w="9070" w:type="dxa"/>
<w:tblBorders>
<w:top w:val="none"/><w:left w:val="none"/>
<w:bottom w:val="none"/><w:right w:val="none"/>
<w:insideH w:val="none"/><w:insideV w:val="none"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="567"/><w:gridCol w:w="7936"/><w:gridCol w:w="567"/>
</w:tblGrid>
<w:tr>
<w:tc><w:tcPr><w:tcW w:w="567" w:type="dxa"/></w:tcPr>
<w:p/></w:tc>
<w:tc><w:tcPr><w:tcW w:w="7936" w:type="dxa"/></w:tcPr>
<w:p><w:pPr><w:jc w:val="center"/>
<w:spacing w:line="240" w:lineRule="auto"/></w:pPr>
<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">
</m:oMath>
</w:p></w:tc>
<w:tc><w:tcPr><w:tcW w:w="567" w:type="dxa"/></w:tcPr>
<w:p><w:pPr><w:jc w:val="right"/>
<w:spacing w:line="240" w:lineRule="auto"/></w:pPr>
<w:r><w:t>(%NUMBER%)</w:t></w:r>
</w:p></w:tc>
</w:tr>
</w:tbl>
参考文献(GB/T 7714-2015)
使用上文定义的 "references" 编号引用。按照 GB/T 7714-2015 的条目类型手动格式化文本。
// Reference list style (add to paragraphStyles)
{ id: "Reference", name: "Reference", basedOn: "Normal",
run: { font: { ascii: "Cambria Math", eastAsia: "SimSun" }, size: 24 },
paragraph: { spacing: { line: 240, lineRule: LineRuleType.AUTO },
indent: { left: 480, hanging: 480 } } }
// Usage — one Paragraph per entry:
new Paragraph({
style: "Reference",
numbering: { reference: "references", level: 0 },
children: [new TextRun("Author. "),
new TextRun({ text: "Title", italics: true }),
new TextRun("[J]. Journal, Year, Vol(Issue): Pages.")]
})
GB/T 7714-2015 常见格式模板(填写相应字段):
| 类型 | 标识 | 格式 |
|---|---|---|
| 期刊论文 | [J] | 作者. 题名[J]. 刊名, 年, 卷(期): 起止页. |
| 专著 | [M] | 作者. 书名[M]. 出版地: 出版者, 出版年: 起止页. |
| 学位论文 | [D] | 作者. 题名[D]. 保存地: 保存单位, 年份. |
| 会议论文 | [C] | 作者. 题名[C]//会议名. 出版地: 出版者, 年: 起止页. |
| 网络资源 | [EB/OL] | 作者. 题名[EB/OL]. (发布日期)[引用日期]. URL. |
页脚(页码)
页码在页脚中居中对齐。使用 A4 纸张和 2.5 cm 页边距。
const { Footer, Paragraph, TextRun, PageNumber, AlignmentType } = require('docx');
sections: [{
properties: {
page: {
size: { width: 11906, height: 16838 }, // A4
margin: { top: 1418, right: 1418, bottom: 1418, left: 1418 } // 2.5cm
}
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ children: [PageNumber.CURRENT] })]
})
]
})
},
children: [/* content */]
}]
// For "第 X 页 共 Y 页" style:
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun("第 "),
new TextRun({ children: [PageNumber.CURRENT] }),
new TextRun(" 页 共 "),
new TextRun({ children: [PageNumber.TOTAL_PAGES] }),
new TextRun(" 页")
]
})
docx-js 的关键规则
- 明确设置页面大小 - 中文学术文档始终使用 A4(11906 × 16838 DXA),页边距为 2.5cm(1418 DXA)
- 横向:传入纵向尺寸 - docx-js 会在内部交换宽度和高度;将短边作为
width、长边作为height传入,并设置orientation: PageOrientation.LANDSCAPE - 绝不使用
\n- 使用单独的 Paragraph 元素 - 绝不使用 Unicode 项目符号 - 使用
LevelFormat.BULLET和编号配置 - PageBreak 必须位于 Paragraph 中 - 单独使用会生成无效 XML
- ImageRun 必须指定
type- 始终指定 png/jpg 等类型 - 标题编号:按章节进行 Word 自动编号 — H1 在文本中使用中文数字(一、二、三),通过
h1Chinese()实现。H2/H3 使用 Word 自动编号,通过buildNumberingConfig(chapterCount)生成的每章sections_c{N}引用实现。在多级配置中绝不使用LevelFormat.CHINESE_COUNTING。传入不带手动编号前缀的标题文本 — 所有编号均由 Word 处理。 - 图/表标题:不手动编号 — 使用
figCaption('描述'),不要加“图 3-1 ”前缀。SEQ 字段会自动生成“图 章-序 描述”。tableCaption()同理。使用stripCaptionNumber()清理 Markdown 输入。 - 表格必须为三线表 - 使用较粗的顶端/底端边框(size:12,1.5pt)和较细的表头底边框(size:6,0.75pt);将所有其他边框设为
BorderStyle.NONE;表头单元格不设填充颜色 - 表格标题:不缩进首行 - 在标题段落上使用
indent: { firstLine: 0 },覆盖 Normal 样式的firstLine - 块级公式使用 3 列表格 - [567 DXA 间隔列 | 7936 DXA 公式居中 | 567 DXA 编号右对齐];使用
scripts/formula.py生成;最终 docx 中绝不使用纯$$LaTeX - 始终使用 DXA 设置表格
width- 绝不使用WidthType.PERCENTAGE(会导致 Google Docs 中显示异常) - 表格需要双重宽度设置 -
columnWidths数组和单元格width都必须设置,且二者匹配 - 表格宽度 =
columnWidths之和 - 对于 DXA,确保二者之和完全相等 - 始终添加单元格边距 - 使用
margins: { top: 80, bottom: 80, left: 120, right: 120 },以提供易读的内边距 - 使用
ShadingType.CLEAR- 绝不对表格底纹使用 SOLID - 目录:使用
heading: HeadingLevel.HEADING_X- 应用经过覆盖的 Heading 样式(包含outlineLevel),同时实现目录识别和自定义格式 - 覆盖内置样式 - 使用确切的 ID:“Heading1”、“Heading2”等;始终使用
font: { ascii, eastAsia, hAnsi }对象形式 —font: "SimHei"字符串简写只会设置 ASCII 字体槽,中文字符仍会回退到 Normal 字体 - 包含
outlineLevel- 目录必需(H1 为 0,H2 为 1,依此类推)
编辑现有文档
按顺序完成以下 3 个步骤。
步骤 1:解包
python scripts/office/unpack.py document.docx unpacked/
提取 XML、美化格式、合并相邻的 runs,并将智能引号转换为 XML 实体(“ 等),以确保其在编辑后仍能保留。使用 --merge-runs false 可跳过合并 runs。
步骤 2:编辑 XML
编辑 unpacked/word/ 中的文件。有关模式,请参阅下方的 XML 参考。
对于修订和评论,作者请使用“Claude”,除非用户明确要求使用其他名称。
直接使用 Edit 工具进行字符串替换。不要编写 Python 脚本。 脚本会引入不必要的复杂性。Edit 工具会清楚显示被替换的内容。
重要:新增内容请使用智能引号。 添加包含撇号或引号的文本时,请使用 XML 实体生成智能引号:
<w:t>Here’s a quote: “Hello”</w:t>
| 实体 | 字符 |
|---|---|
‘ | ‘(左单引号) |
’ | ’(右单引号/撇号) |
“ | “(左双引号) |
” | ”(右双引号) |
添加评论: 使用 comment.py 处理多个 XML 文件中的样板内容(文本必须预先转义为 XML):
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # custom author name
然后在 document.xml 中添加标记(请参阅 XML 参考中的评论)。
步骤 3:打包
python scripts/office/pack.py unpacked/ output.docx --original document.docx
进行验证并自动修复、精简 XML,然后创建 DOCX。使用 --validate false 可跳过验证。
自动修复会处理:
durableId>= 0x7FFFFFFF(重新生成有效 ID)<w:t>中包含空白字符但缺少xml:space="preserve"的情况
自动修复不会处理:
- 格式错误的 XML、无效的元素嵌套、缺失的关系、违反架构规范的情况
常见问题
- 替换整个
<w:r>元素:添加修订时,将整个<w:r>...</w:r>块替换为同级的<w:del>...<w:ins>...。不要将修订标签插入某个 run 内部。 - 保留
<w:rPr>格式:将原始 run 的<w:rPr>块复制到修订 run 中,以保留粗体、字号等格式。 - 绝不要重新生成
styles.xml:编辑现有文档时,所有模板样式都位于word/styles.xml中。只编辑document.xml内容;除非收到明确要求,否则不要覆盖或重新创建styles.xml,否则会抹掉用户的模板。 - 保留
<w:pStyle>引用:插入新段落时,从相邻的同类型段落复制<w:pStyle w:val="..."/>。省略<w:pStyle>会静默回退到文档默认样式,导致模板中的所有标题和正文格式丢失。 - 解包后,弯引号会被 XML 编码:解包步骤会将
"/"转换为“/”实体。在document.xml中搜索包含中文引号的标题文字时,应搜索实体形式(“),而不是原始 Unicode 字符。
XML 参考
架构规范
<w:pPr>中的元素顺序:<w:pStyle>、<w:numPr>、<w:spacing>、<w:ind>、<w:jc>,最后是<w:rPr>- 空白字符:在包含前导或尾随空格的
<w:t>中添加xml:space="preserve" - RSID:必须是 8 位十六进制数(例如
00AB1234)
修订
插入:
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>
删除:
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
在 <w:del> 内部:使用 <w:delText> 代替 <w:t>,使用 <w:delInstrText> 代替 <w:instrText>。
最小化编辑——只标记发生变化的内容:
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>
删除整个段落或列表项——删除段落中的所有内容时,还要将段落标记为删除,使其与下一段合并。在 <w:pPr><w:rPr> 内添加 <w:del/>:
<w:p>
<w:pPr>
<w:numPr>...</w:numPr>
<w:rPr>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
</w:del>
</w:p>
如果 <w:pPr><w:rPr> 中没有 <w:del/>,接受修订后会留下一个空段落或列表项。
拒绝其他作者的插入——将删除嵌套在对方的插入中:
<w:ins w:author="Jane" w:id="5">
<w:del w:author="Claude" w:id="10">
<w:r><w:delText>their inserted text</w:delText></w:r>
</w:del>
</w:ins>
恢复其他作者的删除——在其删除内容之后添加插入(不要修改对方的删除):
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
<w:r><w:t>deleted text</w:t></w:r>
</w:ins>
批注
运行 comment.py(参见步骤 2)后,在 document.xml 中添加标记。回复时,使用 --parent 标志,并将标记嵌套在父批注的标记内。
重要:<w:commentRangeStart> 和 <w:commentRangeEnd> 是 <w:r> 的同级元素,绝不能置于 <w:r> 内部。
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted</w:delText></w:r>
</w:del>
<w:r><w:t> more text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:commentRangeStart w:id="0"/>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>text</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
图片
- 将图片文件添加到
word/media/ - 在
word/_rels/document.xml.rels中添加关系:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
- 在
[Content_Types].xml中添加内容类型:
<Default Extension="png" ContentType="image/png"/>
- 在 document.xml 中引用:
<w:drawing>
<wp:inline>
<wp:extent cx="914400" cy="914400"/>
<a:graphic>
<a:graphicData uri=".../picture">
<pic:pic>
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
依赖项
- pandoc:文本提取及 LaTeX→OMML 公式转换(
scripts/formula.py要求 pandoc ≥ 2.0) - docx:
npm install docx(新建文档) - temml:
npm install temml(将 LaTeX 转换为 MathML,供 Word 原生数学公式使用) - fast-xml-parser:
npm install fast-xml-parser(用于 docx 转换的 MathML 解析) - LibreOffice:PDF 转换(通过
scripts/office/soffice.py自动配置,以适用于沙盒环境) - Poppler:用于图片的
pdftoppm
Markdown 转 Word(中文学术论文)
本节详细介绍如何将包含 LaTeX 公式、表格和引文的 Markdown 论文转换为格式规范的 Word 文档,并遵循中文学术标准。
快速开始
# Install dependencies
npm install docx temml fast-xml-parser
# Run conversion
node scripts/new_doc.js
关键问题与解决方案
在生产环境中发现并解决了以下 8 个问题。所有解决方案均已在 scripts/new_doc.js 和 scripts/mathml-to-docx.js 中实现。
问题 1:三线表的中间边框可见
问题:三线表正文行的边框显示为可见,而不是隐藏。
解决方案:将所有正文行边框设为 NONE,仅保留:
- 表头上边框:
THICK(1.5pt) - 表头下边框:
THIN(0.75pt) - 最后一行下边框:
THICK(1.5pt)
const THICK = { style: BorderStyle.SINGLE, size: 12, color: '000000' };
const THIN = { style: BorderStyle.SINGLE, size: 6, color: '000000' };
const NONE = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' };
// Header row
cellOf(h, colWidths[i], { top: THICK, bottom: THIN, left: NONE, right: NONE }, true)
// Body rows - ALL borders NONE except last row bottom
cellOf(cell, colWidths[i], {
top: NONE,
bottom: isLastRow ? THICK : NONE,
left: NONE,
right: NONE,
})
问题 2:公式表边框可见
问题:块级公式表(三列布局)显示了可见边框。
解决方案:将所有边框(包括 insideHorizontal 和 insideVertical)设为 NONE:
return new Table({
width: { size: CONTENT_W, type: WidthType.DXA },
columnWidths: [567, 7936, 567],
borders: {
top: NONE,
bottom: NONE,
left: NONE,
right: NONE,
insideHorizontal: NONE, // CRITICAL: Must include these
insideVertical: NONE, // CRITICAL: Must include these
},
rows: [new TableRow({ children: [leftCell, formulaCell, numberCell] })],
});
问题 3:标题编号——Word 自动编号并按章节重置
问题:最初,标题编号通过手动 JS 计数器写入段落文本。向 Word 中添加或删除内容时,编号不会更新。
解决方案:H2/H3 使用 Word 原生自动编号,并按章节引用编号配置。H1 保留中文数字文本(一、二、三)。buildNumberingConfig(chapterCount) 函数会为每个章节生成一个包含 2 个级别的 sections_c{N} 编号引用。到达章节边界时,_chapter 会递增,H2/H3 则切换到新的编号引用,从而实现自动重置。
let _chapter = 0;
function h1Chinese(text) { // Chinese numeral in text
_chapter++;
return new Paragraph({
heading: HeadingLevel.HEADING_1,
indent: { firstLine: 0 },
children: [new TextRun(text)],
});
}
function h2(text) { // Word auto-numbered (level 0 in per-chapter ref)
return new Paragraph({
heading: HeadingLevel.HEADING_2,
numbering: { reference: `sections_c${_chapter}`, level: 0 },
indent: { firstLine: 0 },
children: [new TextRun(text)],
});
}
function h3(text) { // Word auto-numbered (level 1 in per-chapter ref)
return new Paragraph({
heading: HeadingLevel.HEADING_3,
numbering: { reference: `sections_c${_chapter}`, level: 1 },
indent: { firstLine: 0 },
children: [new TextRun(text)],
});
}
问题 4:标题中的英文和数字使用了错误字体(SimSun,而非 Cambria Math)
问题:标题、图注和表格标题中的英文文本与数字显示为 SimSun(宋体),而非 Cambria Math。
解决方案:使用包含 ascii、eastAsia 和 hAnsi 属性的混合字体配置:
// Style definition for headings
{
id: 'Heading1', name: 'Heading 1', basedOn: 'Normal',
run: {
font: {
ascii: 'Cambria Math', // English letters, numbers
eastAsia: 'SimHei', // Chinese characters (黑体)
hAnsi: 'Cambria Math', // Western European characters
},
size: 32, bold: true,
},
// ... paragraph settings
}
// For captions (SimSun body font for Chinese)
font: { ascii: 'Cambria Math', eastAsia: 'SimSun', hAnsi: 'Cambria Math' }
问题 5:块级公式未使用 Word 公式编辑器
问题:LaTeX 公式被渲染为纯文本或图像,而非 Word 原生公式。
解决方案:使用 temml(LaTeX→MathML)+ mathml-to-docx.js(MathML→OMML)流水线:
const temml = require('temml');
const { mathmlToDocxChildren } = require('./mathml-to-docx');
function latexToMath(latex) {
const mathml = temml.renderToString(latex, { displayMode: true, throwOnError: false });
const children = mathmlToDocxChildren(mathml);
if (children && children.length) {
return new Math({ children });
}
// Fallback
return new Math({ children: [new MathRun(latex)] });
}
// Usage in formula table
const mathObj = latexToMath('Q_n(x, a) = r + \\gamma V_{n-1}(y)');
问题 6:主标题中的英文使用了错误字体
问题:文档标题中的英文文本显示为 SimSun,而非 Cambria Math。
解决方案:对标题段落应用相同的混合字体配置:
new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({
text: '论文标题 Paper Title',
bold: true,
size: 36,
font: { ascii: 'Cambria Math', eastAsia: 'SimHei', hAnsi: 'Cambria Math' },
})],
})
问题 7:行内数学表达式检测过于激进
问题:行内数学表达式正则表达式匹配了普通数字(如“1992”)和英文单词(如“Agent”“Watkins”),并错误地将它们转换为公式对象。
解决方案:使用严格的正则表达式,仅匹配实际数学内容:
// Detection function - returns true only for real math content
function containsMath(text) {
// Greek letters
if (/[αβγδεζηθικλμνξπρστυφχψωΓΔΘΛΞΠΣΦΨΩ]/.test(text)) return true;
// Unicode subscript/superscript characters
if (/[₀₁₂₃₄₅₆₇₈₉ₙₓᵢₜₛ⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]/.test(text)) return true;
// Math operators and special symbols
if (/[∞∑∏∫≤≥≠≈→←↔∈∉⊂⊃∀∃∧∨×÷±∓·…⋯′″⟨⟩]/.test(text)) return true;
// Starred symbols like π*, Q*
if (/[A-Z]\*/.test(text)) return true;
// Explicit $...$ LaTeX
if (/\$[^$]+\$/.test(text)) return true;
return false;
}
// Parsing regex - strict matching for inline formulas
const mathPattern = /\$([^$]+)\$|([A-Z][₀₁₂₃₄₅₆₇₈₉ₙₓᵢₜₛ⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]+\*?\s*\([^)]+\))|([A-Z]\s*\([^)]*[αβγδεζηθικλμνξπρστυφχψωΓΔΘΛΞΠΣΦΨΩ₀₁₂₃₄₅₆₇₈₉ₙₓᵢₜₛ][^)]*\))|([αβγδεζηθικλμνξπρστυφχψωΓΔΘΛΞΠΣΦΨΩ][₀₁₂₃₄₅₆₇₈₉ₙₓᵢₜₛ⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]*\*?)|([A-Za-z][₀₁₂₃₄₅₆₇₈₉ₙₓᵢₜₛ⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]+\*?)|([A-Z]\*)/g;
// What it matches:
// - $...$ explicit LaTeX
// - Qₙ(x,a) - function with subscripts
// - V(αₙ) - function with Greek params
// - αₙ - Greek with subscript
// - xₙ - variable with subscript
// - Q* - starred variable
// What it does NOT match:
// - Plain numbers: 1992, 500
// - English words: Agent, Watkins, Dayan
// - Plain parentheses: (1992), (optional)
问题 8:引用未设为上标
问题:参考文献引用(如 [1]、[2])显示为普通文本,而非上标。
解决方案:检测引用模式并应用 superScript: true:
function containsCitation(text) {
return /\[\d+\]/.test(text);
}
// In parseInlineContentWithCitations():
const combinedPattern = /(\[\d+\])|...; // Citation first in alternation
if (match[1]) {
// Citation [n] - convert to superscript
children.push(new TextRun({
text: match[1],
superScript: true,
}));
}
问题 10:摘要后和参考文献前分页
问题:摘要页应在关键词之后结束,参考文献应从新的一页开始。
解决方案:添加 pageBreak() 辅助函数:
// ISSUE 10 FIX: Page break helper
function pageBreak() {
return new Paragraph({ children: [new PageBreak()] });
}
// Usage in CONTENT:
// After keywords (end of abstract)
body('关键词:强化学习;Q-learning'),
pageBreak(), // <- Start new page after abstract
// Before references
pageBreak(), // <- Start new page for references
new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [new TextRun('参考文献')],
}),
重要:PageBreak 必须包装在 Paragraph 中,不能单独使用。
问题 11:公式表单元格垂直对齐
问题:在三列无边框公式表中,公式和公式编号 (n) 未垂直居中,导致对齐不一致。
解决方案:在全部三个 TableCell 定义中添加 verticalAlign: VerticalAlign.CENTER:
// First, add VerticalAlign to imports:
const {
// ... other imports
VerticalAlign, // <-- Add this
} = require('docx');
// Then apply to each cell in the formula() function:
const leftCell = new TableCell({
width: { size: 567, type: WidthType.DXA },
borders: noBorders,
shading: { fill: 'FFFFFF', type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER, // <-- Add this
children: [new Paragraph({ indent: { firstLine: 0 }, children: [] })],
});
const formulaCell = new TableCell({
width: { size: 7936, type: WidthType.DXA },
borders: noBorders,
shading: { fill: 'FFFFFF', type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER, // <-- Add this
children: [new Paragraph({
alignment: AlignmentType.CENTER,
indent: { firstLine: 0 },
children: [mathObj],
})],
});
const numberCell = new TableCell({
width: { size: 567, type: WidthType.DXA },
borders: noBorders,
shading: { fill: 'FFFFFF', type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER, // <-- Add this
children: [new Paragraph({
alignment: AlignmentType.RIGHT,
indent: { firstLine: 0 },
children: [new TextRun(`(${number})`)],
})],
});
结果:公式和公式编号现在会在同一基线上水平对齐。
文件结构
scripts/
├── new_doc.js # Main template with Issues 1-8, 10-11 fixes (~820 lines)
├── mathml-to-docx.js # MathML→docx Math converter (~250 lines)
├── formula.py # Block formula XML insertion (legacy)
├── table.py # Table XML insertion (legacy)
└── office/
├── unpack.py # DOCX→XML extraction
├── pack.py # XML→DOCX assembly
└── validate.py # Document validation
Unicode 到 LaTeX 的映射
脚本包含全面的 Unicode 数学符号转换:
const UNICODE_TO_LATEX = {
// Greek letters
'α': '\\alpha', 'β': '\\beta', 'γ': '\\gamma', // ... etc
// Subscripts
'₀': '_0', '₁': '_1', '₂': '_2', // ... etc
// Superscripts
'²': '^2', '³': '^3', // ... etc
// Special symbols
'∞': '\\infty', '∑': '\\sum', '∫': '\\int', // ... etc
};
MathML 到 DOCX 的转换
scripts/mathml-to-docx.js 会将 MathML(来自 temml)转换为 docx Math 组件:
- 分数:
<mfrac>→MathFraction - 下标:
<msub>→MathSubScript - 上标:
<msup>→MathSuperScript - 组合上下标:
<msubsup>→MathSubSuperScript - 根式:
<msqrt>、<mroot>→MathRadical - 求和:带有 ∑ 的
<munderover>→MathSum - 积分:带有 ∫ 的
<munderover>→MathIntegral - 矩阵:
<mtable>→MathMatrix
新建转换的工作流
- 将
new_doc.js模板复制到你的项目中 - 安装依赖:
npm install docx temml fast-xml-parser - 编辑 CONTENT 部分,设置文档结构
- 使用辅助函数:
h1/h2/h3(text)- 自动编号的标题body(text)- 正文段落(自动识别数学公式和引用)formula(latex, number)- 块级公式threeLineTable(headers, rows, colWidths)- 三线表tableCaption/figCaption(label)- 标题pageBreak()- 分页符(Issue 10)ref(text)- 参考文献条目
- 运行:
node new_doc.js