npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

docx-edit

v0.1.0

Published

A JS library that parses DOCX into a virtual component tree and writes paragraph-level text changes back to split OOXML runs.

Readme

docx-edit

一个基于 JavaScript 的 .docx 解析与修改库。相比于传统的 .docx 解析库,本项目的优势在于:对于word动态修改更加友好的支持,支持全文高精度匹配和替换等操作。

当前版本已经实现:

  • 文档虚拟树 diff / patch
  • 段落与 run 的样式级建模
  • 样式新增、修改、清空
  • 组件之间的样式迁移
  • 旧控制器 API 与新虚拟树 API 并存

所有写操作最终都会统一收敛到虚拟树 patch,再同步回底层 OOXML。

特性

  • 解析正文、页眉、页脚、批注、脚注、尾注
  • 识别 paragraphruntexttabletable-rowtable-cellhyperlinktext-box
  • 支持段落跨多个 w:t 的整段文本读取和回写
  • 支持真正的虚拟树 diff / patch
  • 支持段落样式和 run 样式的建模、修改和迁移
  • 兼容旧控制器 API,旧写接口内部自动转为虚拟树 patch
  • 保存修改后的 .docx

安装

npm install docx-edit

本库当前使用 CommonJS 导出,对应 Node.js 环境建议为 >=18

快速开始

const { loadDocx } = require("docx-edit");

async function main() {
  const doc = await loadDocx("./sample.docx");

  doc.replaceAll("旧词", "新词");
  await doc.saveAs("./sample.modified.docx");
}

main();

虚拟树模型

文档会被解析成一棵虚拟树,典型结构如下:

document
  body
    paragraph
      run
        text
    table
      table-row
        table-cell
          paragraph
  header
    paragraph
  footer
    paragraph
  comments
    comment
      paragraph

目前支持的节点类型:

  • document
  • body
  • header
  • footer
  • footnotes
  • endnotes
  • comments
  • paragraph
  • run
  • text
  • table
  • table-row
  • table-cell
  • hyperlink
  • tab
  • break
  • text-box
  • comment
  • footnote
  • endnote

内部写入流程

无论你调用的是旧控制器接口,还是直接使用 doc.patch(nextTree),内部流程都是一致的:

  1. 从当前文档生成一棵新的虚拟树副本
  2. 在副本上修改目标节点
  3. 调用 doc.patch(nextTree)
  4. patch 引擎执行 INSERT / REMOVE / REPLACE / MOVE / PROPS/TEXT_UPDATE
  5. 将结果同步回底层 OOXML
  6. 从 XML 重新建树并重建索引

对于段落文本修改,仍然保留当前 ParagraphTextModel 的策略:

  • 尽量保留原有 w:r / w:t
  • 尽量保留 tab / break
  • 只将新的文本重新分配回原有文本节点

样式模型

当前已经支持两层样式建模:

  • paragraph.props.style 对应 w:pPr
  • run.props.style 对应 w:rPr

已支持的段落样式字段

{
  styleId: "BodyText",
  alignment: "center",
  keepNext: true,
  keepLines: true,
  pageBreakBefore: false,
  spacing: {
    before: "120",
    after: "240",
    line: "360",
    lineRule: "auto",
  },
  indent: {
    left: "240",
    right: "120",
    firstLine: "240",
    hanging: "240",
  },
}

已支持的 run 样式字段

{
  styleId: "Emphasis",
  bold: true,
  italic: true,
  underline: "single",
  color: "FF0000",
  highlight: "yellow",
  fontSize: "28",
  fontFamily: {
    ascii: "Calibri",
    hAnsi: "Calibri",
    eastAsia: "宋体",
    cs: "Arial",
  },
}

导出 API

入口定义在 src/index.js

const {
  loadDocx,
  VirtualWordDocument,
  VNode,
  createVNode,
  cloneVNode,
  DocumentPartController,
  ParagraphController,
  RunController,
  TableController,
  TableRowController,
  TableCellController,
  TextBoxController,
  StructuredEntryController,
} = require("docx-edit");

文档 API

loadDocx(input)

加载 .docx 文件。

  • input: string | Buffer
  • 返回:Promise<VirtualWordDocument>
const doc = await loadDocx("./sample.docx");

doc.toComponentTree()

返回当前文档虚拟树的副本。你可以在这棵树上修改,再传给 doc.patch()

const tree = doc.toComponentTree();
console.log(tree.type); // document

doc.patch(nextTree)

对完整虚拟树执行 patch,并把结果同步到底层 XML。

  • 根节点类型必须为 document
  • 支持文本更新、结构新增、删除、替换、重排
  • 支持段落样式和 run 样式修改
  • 返回 patch 结果,包含执行的操作列表
const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");
body.children[0].props.text = "新的第一段";

const result = doc.patch(tree);
console.log(result.operations);

doc.toBuffer()

返回修改后的 .docx 二进制内容。

const buffer = await doc.toBuffer();

doc.saveAs(outputPath)

保存文档到指定路径。

await doc.saveAs("./sample.modified.docx");

文档级查询接口

doc.getParts();
doc.getBody();
doc.getHeaders();
doc.getFooters();
doc.getParagraphs();
doc.getParagraph(0);
doc.getTables();
doc.getTextBoxes();
doc.getFootnotes();
doc.getEndnotes();
doc.getComments();

doc.replaceAll(searchValue, replacement, options?)

全文替换段落文本。

  • searchValue: string | RegExp
  • replacement: string | Function
  • options.partTypes?: string[]
doc.replaceAll("活动", "主题活动");
doc.replaceAll(/2025/g, "2026");
doc.replaceAll("页眉", "新页眉", { partTypes: ["header"] });

控制器 API

旧控制器 API 仍然保留,但内部已经迁移到虚拟树 patch。

DocumentPartController

常见来源:

const body = doc.getBody();
const header = doc.getHeaders()[0];
const footer = doc.getFooters()[0];

可用方法:

body.toComponentTree();
body.getParagraphs();
body.getParagraph(0);
body.getTables();
body.getTable(0);
body.getTextBoxes();
body.replaceAll("旧词", "新词");

对于 comments / footnotes / endnotes part,还可以:

const commentsPart = doc.getParts().find((part) => part.type === "comments");
commentsPart.getEntries();
commentsPart.getEntries({ includeSpecial: true });

ParagraphController

const paragraph = doc.getBody().getParagraph(0);

paragraph.getText();
paragraph.setText("新的段落内容");
paragraph.replace("旧词", "新词");
paragraph.replaceAll("青年", "青年学生");
paragraph.getStyle();
paragraph.setStyle({ alignment: "center" });
paragraph.patchStyle({ spacing: { after: "240" } });
paragraph.getRuns();
paragraph.getRun(0);

RunController

const run = doc.getBody().getParagraph(0).getRun(0);

run.getText();
run.getStyle();
run.setStyle({
  bold: true,
  color: "FF0000",
  fontSize: "28",
});
run.patchStyle({
  italic: true,
  underline: "single",
});

样式迁移

const paragraphA = doc.getBody().getParagraph(0);
const paragraphB = doc.getBody().getParagraph(1);

paragraphB.copyStyleFrom(paragraphA);

const runA = paragraphA.getRun(0);
const runB = paragraphB.getRun(0);
runB.copyStyleFrom(runA);

TableController

const table = doc.getTables()[0];

table.getRows();
table.getRow(0);
table.getCell(1, 2);

table.fill(
  [
    ["活动名称", "日期", "负责人", "备注"],
    ["分享会", "2026-03-24", "张三", "已确认"],
  ],
  { startRow: 0 },
);

TableRowController

const row = doc.getTables()[0].getRow(0);

row.getCells();
row.getCell(0);

TableCellController

const cell = doc.getTables()[0].getCell(1, 0);

cell.getParagraphs();
cell.getParagraph(0);
cell.getText();
cell.setText("新的单元格内容");

TextBoxController

const textBox = doc.getTextBoxes()[0];

textBox.getParagraphs();
textBox.getText();

StructuredEntryController

用于 comment / footnote / endnote

const comment = doc.getComments()[0];

comment.getParagraphs();
comment.getText();
comment.replaceAll("原文", "新文");

虚拟树 API 调用说明

1. 修改已有段落文本

const { loadDocx } = require("docx-edit");

const doc = await loadDocx("./sample.docx");
const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

body.children[0].props.text = "这是更新后的第一段";

await doc.patch(tree);
await doc.saveAs("./sample.modified.docx");

2. 插入一个新段落

const { createVNode, loadDocx } = require("docx-edit");

const doc = await loadDocx("./sample.docx");
const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

body.children.splice(
  1,
  0,
  createVNode({
    type: "paragraph",
    props: { text: "这是新插入的段落" },
    children: [],
  }),
);

await doc.patch(tree);
await doc.saveAs("./sample.modified.docx");

3. 删除一个段落

const doc = await loadDocx("./sample.docx");
const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

body.children.splice(0, 1);

await doc.patch(tree);

4. 使用 key 做稳定重排

如果你要频繁重排同层节点,建议设置 key

const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

body.children[0].key = "first";
body.children[1].key = "second";
body.children[2].key = "third";

await doc.patch(tree);

const nextTree = doc.toComponentTree();
const nextBody = nextTree.children.find((node) => node.type === "body");
nextBody.children = [nextBody.children[2], nextBody.children[0], nextBody.children[1]];

await doc.patch(nextTree);

5. 修改段落样式

const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

body.children[0].props.style = {
  styleId: "BodyText",
  alignment: "center",
  spacing: {
    before: "120",
    after: "240",
  },
};

await doc.patch(tree);

6. 修改 run 样式

const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");
const firstRun = body.children[0].children[0];

firstRun.props.style = {
  bold: true,
  italic: true,
  color: "FF0000",
  underline: "single",
};

await doc.patch(tree);

7. 在组件之间迁移样式

const tree = doc.toComponentTree();
const body = tree.children.find((node) => node.type === "body");

const sourceParagraphStyle = body.children[0].props.style;
body.children[1].props.style = sourceParagraphStyle;

const sourceRunStyle = body.children[0].children[0].props.style;
body.children[1].children[0].props.style = sourceRunStyle;

await doc.patch(tree);

8. 修改页眉、页脚、批注、文本框

const tree = doc.toComponentTree();

const header = tree.children.find((node) => node.type === "header");
const footer = tree.children.find((node) => node.type === "footer");
const comments = tree.children.find((node) => node.type === "comments");

header.children[0].props.text = "新的页眉";
footer.children[0].props.text = "新的页脚";
comments.children[0].children[0].props.text = "新的批注内容";

await doc.patch(tree);

createVNode() 说明

createVNode() 用来手动创建新节点。

const node = createVNode({
  type: "paragraph",
  key: "intro",
  props: { text: "介绍段落" },
  children: [],
});

参数说明:

  • type: 节点类型
  • key: 可选,同层稳定重排时推荐提供
  • props: 节点属性
  • children: 子节点数组

注意:

  • 根节点必须是 document
  • patch 时必须保持已有 part 不变,不能随意删除 body/header/footer/comments 这些 part 根
  • 新增节点时,要符合当前支持的父子关系
  • 样式修改建议直接写到 paragraph.props.stylerun.props.style

测试

运行示例脚本:

npm run example

运行测试:

npm test

当前测试覆盖:

  • 段落整段读取和回写
  • tab / break 保留
  • 虚拟树文本 patch
  • 虚拟树结构插入、删除、重排
  • 表格 patch 与 fill() 混用
  • header / footer / comment / text-box 持久化
  • 段落样式和 run 样式解析
  • 样式新增、修改、清空
  • 样式在组件之间迁移
  • 真实样本文档回归

已知边界

  • 当前只覆盖常见文本相关 OOXML 节点,不是完整的 Word OOXML 实现
  • 当前样式建模主要覆盖段落和 run 的常用属性
  • 对未知节点的策略是尽量保留,而不是细粒度理解和编辑
  • doc.patch(nextTree) 期望目标树是由当前树演化而来,不保证支持任意非法结构