为 DOCX 导出自定义有序列表编号

@tiptap-pro/extension-export-docx 接受一个可选的 编号格式定义 注册表(多级标记样式、标记文本、对齐、缩进和字体),并通过外层 <ol> 上的一个属性按列表进行选择。具有匹配 id 的列表会按对应定义导出;没有对应定义的列表则按普通 1. 2. 3. 导出。

此功能为可选启用。未提供 numberingFormats 的使用者不会看到任何行为变化。

提供了什么

导出用途
OrderedListNumbering@tiptap-pro/extension-convert-kitTiptap 扩展,为 orderedList 添加 numberingFormat 属性,并提供 setOrderedListNumberingFormat(id)toggleOrderedListWithFormat(format?) 命令,还会通过 editor.storage.orderedListNumbering 跟踪光标选区处的活动格式。由 ConvertKit 注册(通过 orderedListNumbering: true,或带有 defaultFormat / formats 的选项对象启用)。在粘贴、JSON、协作同步和程序化编辑中强制仅最外层编号。
generateNumberingFormatCss(formats, options?)@tiptap-pro/extension-convert-kit纯函数、无依赖,返回用于编辑器预览的 CSS 文本:相同的注册表,相同的视觉效果。
NumberingFormatDefinition, NumberingLevelDefinition, NumberingMarkerFont@tiptap-pro/extension-convert-kit你的注册表遵循的数据结构。结构上与 ExportDocxnumberingFormats 配置兼容。
ExportDocx.configure({ numberingFormats })@tiptap-pro/extension-export-docx将注册表传递给导出器,使 .docx 包含匹配的定义。
LevelFormat, IRunOptions, PositiveUniversalMeasure@tiptap-pro/extension-export-docxdocx 重新导出,因此你无需再添加第二个依赖项。

面向最终用户的选择器 UI 并不属于这些包;这属于应用层关注点,取决于你的组件库。

快速开始

import { Editor } from '@tiptap/core'
import { ConvertKit, type NumberingFormatDefinition } from '@tiptap-pro/extension-convert-kit'
import { ExportDocx, LevelFormat } from '@tiptap-pro/extension-export-docx'

// 1. 定义你的注册表:单一事实来源,供下面两侧共用。
const MY_FORMATS: NumberingFormatDefinition[] = [
  {
    id: 'decimal-paren',
    levels: [
      { baseStyle: LevelFormat.DECIMAL, textTemplate: '%1)' },
      { baseStyle: LevelFormat.LOWER_LETTER, textTemplate: '%2)' },
      { baseStyle: LevelFormat.LOWER_ROMAN, textTemplate: '%3)' },
    ],
  },
  {
    id: 'outline',
    levels: [
      { baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.' },
      { baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.%2.' },
      { baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.%2.%3.' },
    ],
  },
]

// 2. 通过 ConvertKit 选择启用。传入 `formats` 会生成编辑器预览 CSS,
//    并自动注入;同时用相同的注册表注册 ExportDocx,以便
//    .docx 包含匹配的定义。
const editor = new Editor({
  extensions: [
    ConvertKit.configure({
      // 默认关闭,因此不使用自定义编号的使用者可以保持干净的
      // orderedList schema。传入一个选项对象(或 `true`)即可启用。
      orderedListNumbering: { formats: MY_FORMATS },
    }),
    ExportDocx.configure({
      numberingFormats: MY_FORMATS,
      onCompleteExport: (blob) => {
        /* 下载 blob */
      },
    }),
  ],
})

// 3. 在当前选区以所选格式开始一个编号列表
//    (或者,当已经在列表中时,使用 setOrderedListNumberingFormat 更改其格式)。
editor.chain().focus().toggleOrderedListWithFormat('outline').run()

更喜欢自己管理样式表?那就省略 formats,改为从 generateNumberingFormatCss 注入 CSS。这两种方式会生成相同的标记。

启用有序列表编号

OrderedListNumbering@tiptap-pro/extension-convert-kit 提供,但默认关闭,以便为不使用自定义编号的使用者保持 orderedList schema 的简洁。通过 ConvertKit 选择启用:

ConvertKit.configure({ orderedListNumbering: true })

不要传 true,而是传入一个选项对象来配置两件事:

ConvertKit.configure({
  orderedListNumbering: {
    // 新建有序列表时开始使用的格式 id。默认为 `null`(普通的 `1. 2. 3.`)。
    defaultFormat: 'outline',
    // 传给 ExportDocx 的相同定义。提供后,
    // 会据此生成并自动注入编辑器预览 CSS,
    // 因此屏幕上的标记会与导出结果一致,无需额外接线。
    formats: MY_FORMATS,
  },
})
OptionDefaultDescription
defaultFormatnull应用于新建有序列表的编号格式 id(numberingFormat 属性的默认值)。仅对最外层列表进行格式化;嵌套列表保持不变。
formatsnull用于生成并注入编辑器预览 CSS 的编号格式定义,与你传给 ExportDocx.configure({ numberingFormats }) 的数组相同。使用相同格式配置的编辑器会共享一份预览样式表,而使用不同格式配置的编辑器则会获得各自独立的样式表,因此每个编辑器始终显示正确的标记;共享样式表只有在最后一个使用它的编辑器被销毁后才会移除。

在这里传入 formats,等同于你自己调用 generateNumberingFormatCss 并注入其结果:按你的设置选择最合适的方式即可。

应用格式

setOrderedListNumberingFormat(id) 命令会在当前选区最外层的有序列表祖先上设置 numberingFormat 属性。传入 null 可清除它(此时列表会作为普通的 1. 2. 3. 导出)。

editor.chain().focus().setOrderedListNumberingFormat('decimal-paren').run()
editor.chain().focus().setOrderedListNumberingFormat(null).run()

当选区不在有序列表中时,该命令返回 false

以某种格式开始新列表

setOrderedListNumberingFormat 只会作用于选区已经位于其中的列表。要从非列表选区开始以所选格式创建编号列表,请使用 toggleOrderedListWithFormat(format?)。它会在一次调用中创建有序列表并应用格式,这正是“选择一种格式来开始编号列表”工具栏按钮的自然操作:

// 从段落开始:创建一个有序列表并设置其格式。
editor.chain().focus().toggleOrderedListWithFormat('outline').run()

// 省略格式可按配置的 `defaultFormat` 开始列表。
editor.chain().focus().toggleOrderedListWithFormat().run()

当选区已经位于有序列表中时,该命令会切换关闭该列表,与 toggleOrderedList 的行为一致。链式调用 toggleOrderedList().setOrderedListNumberingFormat(format) 会得到相同的最终状态;只是单个命令更方便,也会处理关闭切换的情况。

要在工具栏中反映当前激活的格式,请从扩展的 storage 中读取它;当选区和文档发生变化时,它会保持同步:

const activeFormatId = editor.storage.orderedListNumbering.activeNumberingFormat
// 一个格式 id,或者在选区不在有序列表中时为 `null`

NumberingFormatDefinition

interface NumberingFormatDefinition {
  id: string
  levels: NumberingLevelDefinition[]
}
字段类型描述
idstring在你的 numberingFormats[] 中唯一。序列化到 orderedList 的 numberingFormat 属性中。
levelsNumberingLevelDefinition[]每个嵌套深度对应一项。必须非空。当列表嵌套深度超过数组长度时,深度 N 会复用 levels[N % levels.length]

NumberingLevelDefinition

interface NumberingLevelDefinition {
  baseStyle: NumberingBaseStyle
  textTemplate: string
  startAt?: number
  alignment?: 'left' | 'center' | 'right'
  numberIndent?: number | string
  textIndent?: number | string
  markerFont?: NumberingMarkerFont
}

ConvertKit 的类型使用普通字符串字面量,因此该包不会引入 docx。这些字段在结构上与 ExportDocx 更严格(使用 docx 类型)的版本兼容,因此从 @tiptap-pro/extension-export-docx 传入 LevelFormat.DECIMAL 的效果与传入字符串 'decimal' 相同。

字段默认值描述
baseStyle(必填)计数器字形,对应于匹配的 docx LevelFormat 值。拉丁/数字样式('decimal''decimalZero''upperLetter''lowerLetter''upperRoman''lowerRoman''none')以及许多区域样式(例如 'hebrew1''thaiNumbers''hindiNumbers''japaneseCounting''koreanDigital''chineseCounting''ideographDigital')会映射到匹配的 CSS 计数器样式,因此预览会渲染其原生字形。没有对应 CSS 等价形式的样式(例如 'chicago''ordinal''cardinalText'),以及任何无法识别的字符串,都会在仅编辑器预览中回退为 decimal;导出的 .docx 始终会保留你设置的精确 LevelFormat
textTemplate(必填)使用 Word 的 <w:lvlText> 语法的标记文本(见下文)。
startAt1初始计数值。
alignment'left'数字区域内的标记文本对齐方式。
numberIndentWord 的每级默认值从页面边距到标记的距离。Twips 数值或 docx 风格的度量字符串('0.63cm''0.25in''18pt')。
textIndentWord 的每级默认值从页面边距到正文文本的距离。应大于 numberIndent
markerFont仅标记的 run 格式(见下文 NumberingMarkerFont)。可随 DOCX 导出保留,并会在编辑器预览中由 generateNumberingFormatCss 反映。

NumberingMarkerFont

interface NumberingMarkerFont {
  font?: string | { name: string }
  size?: number | string
  bold?: boolean
  italics?: boolean
  color?: string
  underline?: unknown
}
字段描述
font字体族名称,或一个 docx 风格的 { name } 对象。
sizedocx 的半磅值,使用数字表示(例如 28 = 14pt),或 docx 度量字符串,例如 '14pt'
bold设置为 true 可将标记渲染为粗体。
italics设置为 true 可将标记渲染为斜体。
color十六进制颜色值,可带或不带前导 #
underline任何真值都会在预览中应用 text-decoration: underline

generateNumberingFormatCss 所理解的 docx IRunOptions 子集一致。你传递给 ExportDocx 的额外字段会在预览中被忽略,但仍会保留到 .docx 中。

textTemplate 语法

  • %1%9 引用的是 1 基索引的嵌套深度上的计数器。因此,无论 textTemplate 属于哪一层,%1 始终表示“最外层的计数器”。
  • 其他所有字符都会按字面量渲染。
  • 以非数字结尾的孤立 %(例如 "50%")会被保留。

要让每一层显示自己的计数器,请按层级使用递增的 %N。要让某一层包含父级计数器(法律式大纲),请把它们串联起来:在第 1 层使用 '%1.%2.' 时会渲染为 '1.1.'

模板所在层级 N渲染示例
第 0 层的 '%1.'1.
第 1 层的 '%2.'1.(第 1 层的计数器)
第 1 层的 '%1.%2.'1.1.
第 2 层的 '%1.%2.%3.'1.1.1.
第 0 层的 'Article %1'Article 1
第 0 层的 '§ %1 —'§ 1 —
第 2 层的 '(%3)'(1)

架构约定:仅限最外层

numberingFormat 属性只应放在最外层 <ol> 上。一个定义会声明整个多级列表的所有嵌套层级。OrderedListNumbering 会在解析时强制执行这一点:如果粘贴的 HTML 中,嵌套的 <ol> 上带有 data-numbering-format,该属性会被移除。

同一个最外层 <ol> 下的所有嵌套层级共享一个计数器作用域,并且子级会在父级递增时重新开始,就像 Word 的行为一样。

循环规则

levels.length 定义了循环周期。当列表嵌套深度超过 levels.length - 1 时,深度 N 会在 DOCX 导出和编辑器预览 CSS 中都复用 levels[N % levels.length]。提供九个层级可以覆盖 Word 的完整嵌套深度而不循环;如果更深的层级可以自然重复较浅的条目,则可提供更少的层级。

默认值与 Word 标准一致

当省略 numberIndent / textIndent 时,导出器和 CSS 辅助函数都会输出 Word 的标准多级列表默认值:

深度textIndentnumberIndent悬挂缩进
0720 (0.50″)360 (0.25″)360
11140 (0.79″)780 (0.54″)360
21440 (1.00″)1080 (0.75″)360
31740 (1.21″)1380 (0.96″)360
42040 (1.42″)1680 (1.17″)360
52340 (1.63″)1980 (1.38″)360
62640 (1.83″)2280 (1.58″)360
72940 (2.04″)2580 (1.79″)360
83240 (2.25″)2880 (2.00″)360

generateNumberingFormatCss(formats, options?)

返回 CSS 文本,由你决定如何注入(例如 <style> 标签、CSS-in-JS 层、由构建流水线提供的样式表等)。该函数是纯函数且无依赖,可安全地在启动时或注册表发生变化时调用。

generateNumberingFormatCss(formats, {
  scope: '.tiptap.ProseMirror', // 默认
  maxDepth: 9, // 默认
})
选项默认值描述
scope'.tiptap.ProseMirror'为每条生成的规则添加 CSS 选择器前缀作用域。传入空字符串可输出不带作用域的结果(适合注入到 Shadow DOM 或你自己的 CSS 层中)。
maxDepth9要生成规则的最大嵌套深度。较小的值会生成更小的样式表。

生成的 CSS 会将每个列表标记及其正文文本定位到与 Word 渲染的绝对位置一致,包括每一层嵌套深度的阶梯式缩进。如果你需要为主题定制覆盖某些内容(深色模式、RTL、字体缩放),可以把输出包裹在你自己的选择器或作用域中,并依赖级联规则。

解析规则

  • 匹配的 id 会使用相应的定义来渲染该多级列表的每一层(包括所有嵌套的 <ol>)。
  • 未匹配的 id(拼写错误、缺少属性、空的 numberingFormats)会渲染为普通的 1. 2. 3. 编号。
  • 引用相同格式的同级多级列表会独立重新开始,每个都从各自的 startAt 开始。

已知限制

导出器刻意没有建模的 Word 功能:

功能变通方案 / 范围
强制父级计数器引用无论基础样式如何都以阿拉伯数字显示(Word 的“法律编号”覆盖)为相同视觉输出定义一个所有层级都为 DECIMAL 的单独格式。
按层级自定义计数器重启始终使用 Word 的默认行为(子级会在父级递增时重新开始)。
标记文本后缀(标记与正文之间的制表符 / 空格 / 无)始终为 tab(Word 的默认值)。
在独立列表之间延续编号每个列表都会独立从其 startAt 开始。
按条目覆盖标记使用一个单独的列表从不同的计数值开始。
DOCX → 编辑器导入@tiptap-pro/extension-import-docx 单独处理。

另请参阅