mcpField
D
docx-editor-cn

docx-editor-cn

github.com/gostyan文档处理46253 次安装15 周前更新
代码仓库

关于 docx-editor-cn

面向 DOCX 文档的创建、编辑与分析技能,提供通过 JavaScript 生成文档和通过 XML 修改现有文件的做法。内容还涵盖中文学术论文的页面、标题、表格、图注及公式格式规范。

主要功能

  1. 01读取与分析 DOCX 内容
  2. 02创建并验证新文档
  3. 03解包、编辑和重打包 XML
  4. 04插入三线表与公式
  5. 05中文学术排版规范
  6. 06处理修订和格式转换

适用场景

  1. 01生成符合规范的中文论文
  2. 02在现有文档中插入表格或公式
  3. 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.txt
package-lock.json
package.json
README.md
scripts/
__init__.py
accept_changes.py
comment.py
convert_paper.js
formula.py
mathml-to-docx.js
new_doc.js
scripts/office/
pack.py
scripts/office/helpers/
__init__.py
merge_runs.py
simplify_redlines.py
scripts/office/schemas/ecma/fouth-edition/
opc-contentTypes.xsd
opc-coreProperties.xsd
opc-digSig.xsd
opc-relationships.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/
dml-chart.xsd
dml-chartDrawing.xsd
dml-diagram.xsd
dml-lockedCanvas.xsd
dml-main.xsd
dml-picture.xsd
dml-spreadsheetDrawing.xsd
dml-wordprocessingDrawing.xsd
pml.xsd
shared-additionalCharacteristics.xsd
shared-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,90616,8389,070
US Letter12,24015,8409,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(1418 DXA)。
  • 字体:中文 = 宋体,英文/数学/代码 = Cambria Math(默认);英文文本也可使用 Times New Roman。
  • 字号:12pt(小四)→ docx-js 中为 size: 24(半磅)。
  • 段落:首行缩进 2 个字符(firstLine: 480 DXA),默认行距为单倍(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 实体(&#x201C; 等),以确保其在编辑后仍能保留。使用 --merge-runs false 可跳过合并 runs。

步骤 2:编辑 XML

编辑 unpacked/word/ 中的文件。有关模式,请参阅下方的 XML 参考。

对于修订和评论,作者请使用“Claude”,除非用户明确要求使用其他名称。

直接使用 Edit 工具进行字符串替换。不要编写 Python 脚本。 脚本会引入不必要的复杂性。Edit 工具会清楚显示被替换的内容。

重要:新增内容请使用智能引号。 添加包含撇号或引号的文本时,请使用 XML 实体生成智能引号:


<w:t>Here&#x2019;s a quote: &#x201C;Hello&#x201D;</w:t>
实体字符
&#x2018;‘(左单引号)
&#x2019;’(右单引号/撇号)
&#x201C;“(左双引号)
&#x201D;”(右双引号)

添加评论: 使用 comment.py 处理多个 XML 文件中的样板内容(文本必须预先转义为 XML):

python scripts/comment.py unpacked/ 0 "Comment text with &amp; and &#x2019;"
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 编码:解包步骤会将 " / " 转换为 &#x201C; / &#x201D; 实体。在 document.xml 中搜索包含中文引号的标题文字时,应搜索实体形式(&#x201C;),而不是原始 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>
图片
  1. 将图片文件添加到 word/media/
  2. 在 word/_rels/document.xml.rels 中添加关系:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
  1. 在 [Content_Types].xml 中添加内容类型:
<Default Extension="png" ContentType="image/png"/>
  1. 在 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
新建转换的工作流
  1. 将 new_doc.js 模板复制到你的项目中
  2. 安装依赖:npm install docx temml fast-xml-parser
  3. 编辑 CONTENT 部分,设置文档结构
  4. 使用辅助函数:
    • h1/h2/h3(text) - 自动编号的标题
    • body(text) - 正文段落(自动识别数学公式和引用)
    • formula(latex, number) - 块级公式
    • threeLineTable(headers, rows, colWidths) - 三线表
    • tableCaption/figCaption(label) - 标题
    • pageBreak() - 分页符(Issue 10)
    • ref(text) - 参考文献条目
  5. 运行:node new_doc.js

评论

支持 Markdown