docx 库 Line Numbers 行号功能全指南:在 Word 文档中为法律文书与代码清单添加可引用行号
docx 库 Line Numbers 行号功能全指南:在 Word 文档中为法律文书与代码清单添加可引用行号
行号(Line Numbers)是法律合同、代码清单、剧本与审阅文稿中非常实用的功能,它让文档中的每一行都拥有唯一可引用的编号。本文基于 docx 库的官方文档与源码实现,完整讲解如何在 JS/TS 中以声明式 API 为文档的 section 配置行号、控制重启时机与计数间隔、抑制特定段落编号,并结合底层 OOXML 结构说明其工作原理。
行号功能简介
docx 库允许你在 Word 文档的任意 section(节)上开启行号显示。行号通常出现在页面左侧的文本边距内,适用于:
- 法律合同与条款文档:便于双方引用"第 N 行"进行协商与修订;
- 代码清单与脚本:配合审阅场景精确定位代码行;
- 剧本、歌词与翻译对照:逐行引用内容;
- 任何需要精确定位文本行的正式文档。
需要说明的是,行号是 section 级别的页面布局属性,理解行号的前提是先掌握 section(节)的机制,可先阅读 sections 使用指南 了解 section 的创建与分隔方式。
基础用法:为 section 添加行号
行号配置位于 Document 的 sections[].properties.lineNumbers 字段中。最小化配置只需提供 countBy 与 restart 两个属性:
import { Document, LineNumberRestartFormat, Paragraph } from "docx";
const doc = new Document({
sections: [
{
properties: {
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.CONTINUOUS,
},
},
children: [new Paragraph("Line 1"), new Paragraph("Line 2"), new Paragraph("Line 3")],
},
],
});
此时文档中每个段落行都会显示连续递增的行号(1、2、3……)。所有行号配置均为可选属性,即使只传入 lineNumbers: {} 也能生成合法的行号元素(详见下文源码分析中的空配置测试)。
行号配置项详解
lineNumbers 接受以下四个可选属性:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
countBy |
number |
可选 | 每隔 N 行显示一次行号(如 5 表示只显示 5、10、15……) |
start |
number |
可选 | 行号起始值(默认 1) |
restart |
LineNumberRestartFormat |
可选 | 行号何时重新开始计数(默认 NEW_PAGE) |
distance |
number | 字符串度量 |
可选 | 行号与文本边缘之间的距离(单位 twips) |
从源码类型定义看,这四个属性对应 line-number.ts 中的 ILineNumberAttributes 接口:
countBy:虽然每一行都有与之关联的行号,但只有行号是该值的整数倍时才显示出来(如countBy: 5时仅显示第 5、10、15 行……);start:每次按restart规则重置行号时使用的起始值;restart:指定本 section 的行号在何时重置为start指定的值;distance:行号与文本边距之间的距离,使用 twips(缇)度量,也可传入如"10mm"这样的字符串度量。
关于 twips 单位
OOXML 中长度默认使用 twips(1 twip = 1/20 磅 = 1/1440 英寸)。distance: 720 表示 0.5 英寸,是 Word 行号与正文之间常见的间隔。源码中 distance 通过 values.ts 中的 twipsMeasureValue 处理,因此既可以传数字(twips 值),也可以传字符串度量(如 "5mm"、"0.5in" 等),两种形式在 line-number.spec.ts 中都有测试用例验证。
重启选项:控制行号何时重新计数
LineNumberRestartFormat 枚举提供三种重启策略,对应 OOXML 标准中的 ST_LineNumberRestart 简单类型:
import { LineNumberRestartFormat } from "docx";
// 连续计数:行号在整个文档中持续递增
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.CONTINUOUS,
}
// 每页重启:每页从起始值重新开始
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.NEW_PAGE,
}
// 每节重启:每个 section 重新开始
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.NEW_SECTION,
}
三种枚举值在 line-number.ts 中定义为 as const 对象:
| 枚举值 | 底层字符串 | 行为 |
|---|---|---|
NEW_PAGE |
"newPage" |
每当显示新页面时,行号重置为起始值(OOXML 默认值) |
NEW_SECTION |
"newSection" |
每当本 section 开始(即新的节分隔符之后)时重置 |
CONTINUOUS |
"continuous" |
从上一 section 结束时的行号继续递增 |
需要注意,根据源码注释与 OOXML 规范:行号在内部是逐行递增的,即使该行不显示编号也会参与计数,直到到达 restart 指定的重置点。
计数间隔:按 N 行显示一次
countBy 控制行号显示的稀疏程度。例如仅显示第 5、10、15……行的编号:
// Show every 5th line number (5, 10, 15...)
lineNumbers: {
countBy: 5,
restart: LineNumberRestartFormat.CONTINUOUS,
}
在 OOXML 中这对应 w:lnNumType 元素的 w:countBy 属性,例如:
<w:lnNumType w:countBy="5"/>
起始数字:自定义第一个行号
start 用于设置每次重置时的起始编号,例如从 100 开始编号:
lineNumbers: {
countBy: 1,
start: 100, // First line is 100
restart: LineNumberRestartFormat.CONTINUOUS,
}
根据源码注释,start 的默认值为 1(对应 XSD 中 w:start 的 default="1"),而 restart 的默认值为 newPage。也就是说,如果你只配置 countBy 而不指定 restart,行号将在每页顶部重新从 1 开始计数。
抑制行号:让指定段落不显示编号
对于标题、题名、页眉性质的段落等不应参与编号的内容,可以使用段落级属性 suppressLineNumbers 关闭该段的行号:
new Paragraph({
text: "This paragraph will not have a line number",
suppressLineNumbers: true,
});
这在 paragraph/properties.ts 中定义为段落属性 suppressLineNumbers?: boolean,当值为 true 时会在该段落的属性中输出 w:suppressLineNumbers 元素(见 properties.ts)。对应的单元测试在 paragraph.spec.ts 中验证了生成的 XML 结构:
<w:p>
<w:pPr>
<w:suppressLineNumbers/>
</w:pPr>
</w:p>
抑制功能非常适合用于文档标题、章节标题、签名区等不应被编号的内容。
完整示例:带行号的法律合同
将以上知识点组合起来,构建一份典型的使用场景——行号 + 标题抑制的法律合同文档:
import { Document, HeadingLevel, LineNumberRestartFormat, Packer, Paragraph } from "docx";
const doc = new Document({
sections: [
{
properties: {
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.NEW_PAGE,
},
},
children: [
new Paragraph({
text: "AGREEMENT",
heading: HeadingLevel.HEADING_1,
suppressLineNumbers: true, // Don't number the title
}),
new Paragraph("This Agreement is entered into as of the date last signed below."),
new Paragraph("WHEREAS, Party A desires to engage Party B for services;"),
new Paragraph("WHEREAS, Party B has the expertise to provide such services;"),
new Paragraph({
text: "TERMS AND CONDITIONS",
heading: HeadingLevel.HEADING_2,
suppressLineNumbers: true, // Don't number section headers
}),
new Paragraph("1. Party B shall provide consulting services as requested by Party A."),
new Paragraph("2. Payment shall be made within 30 days of invoice receipt."),
new Paragraph("3. This agreement may be terminated by either party with 30 days notice."),
],
},
],
});
这里采用 NEW_PAGE 重启策略,让每页从 1 开始编号(符合法律文书的常见惯例),同时抑制文档标题与章节标题的行号,只对正文行编号。通过 Packer.toBuffer(doc) 或 Packer.toBlob(doc)(浏览器环境)即可导出为 .docx 文件,具体打包方式可参考 packers 使用指南。
多 section 混合配置:按需启用行号
行号是 section 属性,因此可以在同一文档中对不同 section 采用完全不同的配置。例如,前言部分不带行号,正文部分开启连续行号:
import { Document, LineNumberRestartFormat, Paragraph, SectionType } from "docx";
const doc = new Document({
sections: [
// First section - no line numbers
{
children: [new Paragraph("Introduction without line numbers...")],
},
// Second section - numbered lines
{
properties: {
type: SectionType.NEXT_PAGE,
lineNumbers: {
countBy: 1,
restart: LineNumberRestartFormat.CONTINUOUS,
},
},
children: [new Paragraph("This section has line numbers...")],
},
],
});
第二个 section 使用 SectionType.NEXT_PAGE 从新页开始,并开启连续行号。这种"无行号的前置页 + 有行号的正文"结构在报告、论文、合同附件等场景中非常常见。
底层实现:行号如何写入 OOXML
理解行号背后的 OOXML 结构有助于排查问题与深入定制。行号最终落在 section properties(w:sectPr)内部的 w:lnNumType 元素上,其 XSD 定义为 CT_LineNumber:
<xsd:complexType name="CT_LineNumber">
<xsd:attribute name="countBy" type="ST_DecimalNumber" use="optional"/>
<xsd:attribute name="start" type="ST_DecimalNumber" use="optional" default="1"/>
<xsd:attribute name="distance" type="s:ST_TwipsMeasure" use="optional"/>
<xsd:attribute name="restart" type="ST_LineNumberRestart" use="optional" default="newPage"/>
</xsd:complexType>
在 docx 库中,这个元素由 line-number.ts 中的 createLineNumberType 工厂函数创建,其实现逻辑为:
countBy、start通过decimalNumber转为整数输出为w:countBy、w:start;restart直接输出为w:restart属性;distance通过twipsMeasureValue处理(支持数字 twips 或字符串度量)输出为w:distance;- 未提供的属性在输出时会被省略(
undefined时不写入)。
生成的典型 XML 形如:
<w:sectPr>
<w:lnNumType w:countBy="1" w:restart="continuous"/>
</w:sectPr>
SectionProperties(section-properties.ts)在解析配置时,只要 lineNumbers 存在,就会调用 createLineNumberType(lineNumbers) 将行号元素挂载到 w:sectPr 中。对应的单元测试 line-number.spec.ts 验证了三种情况:完整配置、字符串度量("10mm")的 distance、以及空配置 {}——即使不传任何属性,生成的 w:lnNumType 元素依然合法。
可运行的官方 Demo
仓库中提供了两个可直接运行的行号示例:
- demo/40-line-numbers.ts:基础行号演示,展示标题加连续行号的长段落文档;
- demo/70-line-numbers-suppression.ts:行号抑制演示,在段落流中间插入一个
suppressLineNumbers: true的段落,观察该段不显示编号而前后段落继续计数。
运行方式(Node 环境):
# 在仓库根目录安装依赖后
npx ts-node demo/40-line-numbers.ts
两个 demo 均使用 Packer.toBuffer(doc).then((buffer) => fs.writeFileSync("My Document.docx", buffer)) 将结果导出为 Word 文件,可直接用 Word/WPS 打开查看行号效果。
小结
行号功能在 docx 库中是一个典型的 section 属性 + 段落属性组合:
- 在
sections[].properties.lineNumbers中开启并配置计数间隔(countBy)、起始值(start)、重启策略(restart)与距离(distance); - 用
LineNumberRestartFormat的CONTINUOUS/NEW_PAGE/NEW_SECTION控制行号何时重新计数; - 用段落级
suppressLineNumbers: true排除标题等不需要编号的内容; - 通过多个 section 可以灵活地实现"部分章节带行号、部分不带"的复杂版式。
掌握了这套配置,你就能用几行声明式代码生成符合法律文书、代码审阅等场景要求的带行号 Word 文档。