自定义节点 DSL 构建器(TypeScript)

Available in Start planBetav0.20.0

Beta 功能

构建器与 JSON DSL 一样,属于同一个 customNodeDsl 功能范畴。两者都以 dslVersion: "1.0" 发布,并生成完全相同的线格式输出。如果你依赖此功能,请固定精确的包版本。

DSL 构建器是编写 custom-nodes DSLTypeScript 友好方式。它是一个小巧、流式、不可变的 API,会编译为线格式所期望的 相同 JSON:每次链式调用都会返回一个新的构建器,而任意链上的 .toJSON() 都会生成 DSL 编译器可接受的字面量载荷。

你将获得:

  • 每个元素属性都有自动补全。 paragraph({ … }) 会提示 stylealignmentspacingbordershading 等,这正是 DSL 编译器验证的字段集合。不再需要从文档里猜测属性名。
  • 编译期包含安全。 paragraph().children(childrenBlock()) 会报 TypeScript 错误:段落只接受内联子元素。table().rows(paragraph()) 也会报错:Table.rows 需要 TableRow 构建器。错误在编辑时就会触发,而不是运行时。
  • 必填属性检查。 externalHyperlink({}) 会报 TypeScript 错误:线格式要求 link 属性必填,而构建器类型会强制这一点。
  • 线格式兼容性。 输出与手写 JSON 完全一致。同一份载荷可用于编辑器、Node.js 服务器,或作为 Conversion REST API 中的 customNodeDsl 字段。

安装与导入

该构建器通过子路径导入发布,位于 DOCX 导出流水线其余部分所在的同一包中:

npm i @tiptap-pro/extension-export-docx@^0.18.0
// 主入口:运行时导出器和可配置扩展。
import { ExportDocx, exportDocx } from '@tiptap-pro/extension-export-docx'

// 构建器子路径:可进行 tree-shaking。那些从不使用 DSL 的客户
// 不会为此付出代价。
import {
  docxNode,
  paragraph,
  textRun,
  externalHyperlink,
  table,
  tableRow,
  tableCell,
  pageBreak,
  childrenInline,
  childrenBlock,
  childrenTableRow,
  childrenTableCell,
  iff,
  switch_,
  fragment,
  textOp,
  ref,
  template,
  op,
  unit,
  switchValue,
  marks,
} from '@tiptap-pro/extension-export-docx/dsl'

该子路径别名可在 Vite、Next.js、Webpack 5+、esbuild、Rollup,以及所有现代打包工具 中使用。较旧的工具链可能需要一个显式支持 exports 字段的解析器。


何时使用构建器

你是…使用…
使用 TypeScript 编写 DOCX 规则,并需要自动补全和重构支持。构建器。
将 DSL 作为 JSON 正文发送到 POST /v2/convert/export/docxJSON DSL:直接从 JSON 文件传递 customNodeDsl
在编辑器扩展中,希望使用完整的 JavaScript(闭包、IO 等)。基于函数的 custom-nodes API
从配置服务动态加载规则。JSON DSL(反序列化配置并直接传递)。

构建器会生成 JSON,因此你可以将两者结合使用:在设计时构建,将 .toJSON() 持久化到磁盘,再把 JSON 发送到服务器。或者在运行时读取 JSON 并直接传给 exportDocx。两个方向都没有锁定。


快速开始

一个 12 行的 hintbox 规则,渲染为带样式的 DOCX 段落:

import { ExportDocx } from '@tiptap-pro/extension-export-docx'
import { childrenInline, docxNode, paragraph } from '@tiptap-pro/extension-export-docx/dsl'

ExportDocx.configure({
  customNodeDsl: {
    dslVersion: '1.0',
    nodes: [
      docxNode('hintbox')
        .block()
        .emit(
          paragraph({ style: 'Hintbox' }) //
            .children(childrenInline({ marks: 'default' })),
        )
        .toJSON(),
    ],
  },
})

这与 JSON DSL 快速开始示例 使用的是相同的载荷。传输的字节完全一致,只是编写层不同。


顶层构建器:docxNode

docxNode(type) 会为一种 ProseMirror 节点类型开始一条规则。该链条必须且只能以 emit(...)emitNothing()drop() 中的恰好一个结束。在选择策略之前调用 toJSON() 会抛出错误。

docxNode('hintbox') // ┐
  .block() //          │  可选:显式声明 nodeKind
  .emit(paragraph()) //│  要渲染为 PM 节点位置中的内容
  .toJSON() //         ┘  字面量 CustomNodeRule JSON
方法作用
.block()将规则标记为块级(用于校验器的包含性检查)。
.inline()将规则标记为行内级。
.auto()让校验器根据 emit 的形状推断类型(默认值)。
.emit(node)将匹配到的 PM 节点渲染为 node。接受单个 render-node 构建器、字面量 RenderNode,或数组。
.emitNothing()emit: null:对该 PM 节点不输出任何内容,但仍会遍历其后代。
.drop()render: null:丢弃匹配到的 PM 节点及其所有后代
.toJSON()将该链条具体化为线上的 CustomNodeRule JSON。

每个方法都会返回一个新的构建器,因此可以重复使用部分链条,而不会出现别名带来的意外:

const base = docxNode('mention').inline()
const withColor = base.emit(textRun({ color: '4472C4' }))
const dropped = base.drop()
// `base` 保持不变。

元素工厂

v1 目录中的每个元素都有一个对应工厂。每个工厂都会返回一个可链式调用的构建器,其 .toJSON() 会生成线格式的 ElementNode

paragraph(props?)

接受行内子元素的块级元素。

paragraph({
  style: '提示框',
  alignment: 'center',
  spacing: { before: 240, after: 240, line: 240, lineRule: 'auto' },
  border: {
    top: { style: 'single', size: 4, color: 'B8D8FF', space: 5 },
    bottom: { style: 'single', size: 4, color: 'B8D8FF', space: 5 },
    left: { style: 'single', size: 4, color: 'B8D8FF', space: 5 },
    right: { style: 'single', size: 4, color: 'B8D8FF', space: 5 },
  },
  shading: { type: 'clear', fill: 'E6F3FF' },
}).children(childrenInline({ marks: 'default' }))
方法用途
.children(spec)设置行内子元素(ChildrenInlineBuilder、单个渲染节点构建器,或数组)。块级子元素会在编译时失败。
.inheritOverrides(value)当为 false 时,跳过该段落的全局 paragraphOverrides 组合。默认值为 true
.toJSON()线格式 ElementNode

规范映射: Paragraph element prop schema

bordershading 属性允许自定义节点在 DSL 元素上携带装饰性外观(如边框、提示框、hintbox),而无需单独命名的 styleOverrides 样式。另见下面的 将装饰内联表达

textRun(props?)

行内叶子节点,没有 children。使用 applyMarks 来继承规则的 PM 节点标记。

textRun({
  text: template('@{node.attrs.label}'),
  color: '4472C4',
  size: 24, // 半磅
  highlight: 'cyan',
  style: 'Hyperlink', // 往返一致性标记
}).applyMarks('node')
方法用途
.applyMarks(policy)'node'(继承规则的 PM 节点标记)或一个 marks('node')... 构建器,用于精细控制。这里有意拒绝 'default''none'(见 标记策略)。
.inheritOverrides(value)当为 false 时,跳过该运行的 textRunOverrides。默认值为 true
.toJSON()线格式 ElementNode

规范映射: TextRun element prop schema

externalHyperlink(props)

包裹一个或多个 TextRun 子元素的行内元素。link 属性在类型层面是必需的,因此 externalHyperlink({}) 会导致 TypeScript 错误。

externalHyperlink({ link: ref('node.attrs.href') }).children([
  textRun({ text: ref('node.attrs.label'), style: 'Hyperlink' }) //
    .applyMarks('node'),
])

链接值在运行时也会进行验证,必须以 httphttpsmailtotel 开头(长度上限为 2048 字符)。其他协议会在编译时以 DOCX_DSL_INVALID_PROP 报错拒绝。

table(props?) / tableRow(props?) / tableCell(props?)

table() 通过 .rows(...) 接受 TableRow 构建器(或等价的 .children([row, row, ...]))。tableRow() 通过 .children([cell, cell, ...]) 接受 TableCell 构建器。tableCell() 接受块级内容(段落、嵌套表格)或 childrenBlock()

table({
  width: { size: 100, type: 'pct' },
  borders: {
    top: { style: 'single', size: 4, color: 'B8D8FF' },
    bottom: { style: 'single', size: 4, color: 'B8D8FF' },
    left: { style: 'single', size: 4, color: 'B8D8FF' },
    right: { style: 'single', size: 4, color: 'B8D8FF' },
  },
}).rows(
  tableRow().children([
    tableCell({
      shading: { type: 'clear', fill: 'FFF1CC' },
      verticalAlign: 'top',
    }).children([paragraph().children(childrenInline({ marks: 'default' }))]),
  ]),
)

线格式会在内部将 Table 的子元素映射到 docx 的 rows 构造参数,因此你不需要自己编写 rows: ...

pageBreak()

叶子工厂:发出一个包含 docx 分页符运行的块级段落。若要在段落内部插入行内分页符,请改用 textRun({ break: 1 })

docxNode('explicitPageBreak').block().emit(pageBreak()).toJSON()

子项辅助器

在元素的 .children(...) 中,你可以传入以下任一项:

  • 一个 显式的渲染节点构建器数组[paragraph(), table(), ...])。
  • 一个 childrenX() 委托,让运行时遍历 PM 节点的 content 数组,并通过标准转换器分发每个子节点。
辅助器放置位置传输格式
childrenInline(opts?)paragraph().children(...), externalHyperlink().children(...){ "$children": { "as": "inline", "marks"?: ... } }
childrenBlock(opts?)tableCell().children(...){ "$children": { "as": "block", "wrapInlineInParagraph"?: bool } }
childrenTableRow()table().children(...).rows(...) 的同义形式){ "$children": { "as": "table-row" } }
childrenTableCell()tableRow().children(...){ "$children": { "as": "table-cell" } }
// 内联标记策略:对每个文本子节点运行标准标记流水线。
paragraph({ style: 'Body' }).children(childrenInline({ marks: 'default' }))

// 带内联换行的块委托:将松散的内联子节点缓冲到默认段落中,
// 这样表格单元格始终只包含块内容。
tableCell().children(childrenBlock({ wrapInlineInParagraph: true }))

childrenInline() 接受一个 marks 选项,该选项可传入一个 MarkPolicy'default''none''node')或一个 marks(...) 构建器链(见 标记策略)。


渲染树原语

除了元素之外,还有四种渲染树形状可用于组合动态输出。

iff({ test, then, else? })

结构化条件判断。else 默认值为 null(不输出任何内容)。

iff({
  test: ref('node.attrs.featured'),
  then: paragraph({ style: 'Featured' }).children(childrenInline({ marks: 'default' })),
  else: paragraph().children(childrenInline({ marks: 'default' })),
})

test 使用 v1 的真值规则强制转换为布尔值:falsenullundefined0"" 为假值,其他值都为真值。

switch_({ on, cases, default? })

多路条件判断。查找时会与 cases 的键进行精确匹配。on 在运行时必须解析为字符串。

switch_({
  on: ref('node.attrs.variant'),
  cases: {
    warning: paragraph({ style: 'CalloutWarning' }),
    info: paragraph({ style: 'CalloutInfo' }),
  },
  default: paragraph({ style: 'Callout' }),
})

末尾的下划线是因为 switch 在 JavaScript 中是保留字。还有一个用于选择属性值的 value-form switchValue(...)(见下方的 值表达式)。

fragment(...children)

输出多个同级渲染节点。传给元素 .children([...]) 的裸数组也能完成同样的工作;fragment 适用于“分片”本身就是整个输出的情况。

docxNode('keyValue')
  .block()
  .emit(
    fragment(
      paragraph({ style: 'Key' }).children([textRun({ text: ref('node.attrs.key') })]),
      paragraph({ style: 'Value' }).children(childrenInline({ marks: 'default' })),
    ),
  )
  .toJSON()

textOp(value, opts?)

用于从值表达式方便地发出单个文本运行。与 v1 中的 { element: 'TextRun', props: { text: value } } 等价,并会应用标记策略,但写起来更简短。

textOp(template('@{node.attrs.label}'))
textOp(ref('node.attrs.title'), { default: '(未命名)', marks: 'none' })

值表达式

凡是接受属性值(或 $text$if.test$switch.on$op.args)的地方,你都可以传入字面量 下面这些带类型的表达式之一。

ref(path, opts?)

从源 PM 节点读取一个值。允许的路径:nodenode.typenode.attrsnode.attrs.<key>node.textnode.textContent。其他路径会以 DOCX_DSL_INVALID_REF 报错拒绝。

ref('node.attrs.color')
ref('node.attrs.color', { default: '4472C4', transform: 'hexNoHash' })
ref('node.textContent')

transform 选项可以接受单个 TransformName 或数组。参见 JSON DSL 参考页中的 转换表

template(s)

字符串插值,始终返回字符串。替换内容使用 {path}(路径语法与 ref 相同);{{}} 用于转义字面量花括号。

template('@{node.attrs.label}') //        →  '@alice'
template('size: {node.attrs.size}px') //  →  'size: 24px'
template('{{escaped}} braces') //          →  '{escaped} braces'

解析后的长度上限为 maxTemplateLength(默认 2 000)。超过上限会在运行时以 DOCX_DSL_RESOURCE_LIMIT 拒绝。

op(name, ...args)

类型化计算。操作是封闭列表,且参数个数严格固定(参见 op 参数个数表)。

op('mul', ref('node.attrs.size'), 2)
op('coalesce', ref('node.attrs.color'), ref('node.attrs.fallbackColor'), '000000')
op('and', ref('node.attrs.visible'), op('not', ref('node.attrs.hidden')))

没有 concat;字符串请使用 template(...)。也没有隐式数值转换,所以 op('add', '3', 1) 会被拒绝。

unit(name, value)

连接到一个封闭列表的单位 / 转换辅助器,其底层使用的正是转换器内部的同一组函数。结果类型与该辅助器的签名一致。

unit('pixelsToHalfPoints', 16) //                          →  24(半磅)
unit('pointsToTwips', 12) //                               →  240(twips)
unit('normalizeColor', ref('node.attrs.backgroundColor')) //→  '4F46E5'

完整列表:单位分发表

switchValue({ on, cases, default? })

值形式的 $switch:通过字符串匹配选择一个属性值(而不是渲染节点)。

paragraph({
  style: switchValue({
    on: ref('node.attrs.variant'),
    cases: { warning: 'CalloutWarning', info: 'CalloutInfo' },
    default: 'Callout',
  }),
})

校验器会根据周围上下文来区分值形式与渲染树形式:switchValue 只能用于属性值中,而 switch_ 只能用于 RenderNode 插槽中。


标记策略

marks(mode) 构建器会创建一个可链式调用的 MarkPolicy 对象。适用于你需要比字符串简写更精细控制的场景。

import { childrenInline, marks } from '@tiptap-pro/extension-export-docx/dsl'

childrenInline({
  marks: marks('default') //
    .disable('bold', 'italic')
    .overrides({
      underline: { props: { color: 'EA580C' } },
      highlight: { replace: true, props: { color: 'FFE066' } },
    }),
})
方法用途
marks(mode)开始构建器。mode'default'(对子节点自身的标记应用标准管线)或 'node'(使用规则的 PM 节点标记)。
.disable(...marks)完全跳过这些标记。它们的标准映射不会运行,任何匹配的 overrides 条目也会被忽略。
.overrides({ markType: { props?, replace? } })按标记进行调整。replace: true 会抑制该标记的标准映射;props 会在其基础上合并。
.toJSON()生成传输格式的 MarkPolicyObject。容器辅助器会自动为你调用它。

applyMarks 的适用范围更窄

textRun().applyMarks(...)externalHyperlink().applyMarks(...) 仅接受 'node'marks('node')... 构建器。这里的 wire format 会拒绝 'default''none',因为它们只适用于子节点标记。

构建器会在运行时强制执行这一点:将 marks('default') 传给 applyMarks 时会抛出有帮助的错误,而不是生成一个日后会导致 DSL 编译器失败的负载。

textRun().applyMarks('node') //                       ✓
textRun().applyMarks(marks('node').disable('code')) // ✓
textRun().applyMarks(marks('default')) //              ✗ 在构建时抛出

类型系统保证

构建器的 TypeScript 接口会在编辑阶段强制执行运行时本应捕获的每一条包含规则。以下都是编译错误:

// 段落只接受行内子节点。
paragraph().children(childrenBlock())
//                  ^^^^^^^^^^^^^^^^
// Type 'ChildrenBlockBuilder' is not assignable to type 'ParagraphChildSpec'.

// Table.rows 需要 TableRow 构建器。
table().rows(paragraph())
//           ^^^^^^^^^^^
// Type 'ParagraphBuilder' is not assignable to type 'TableRowBuilder'.

// TableRow 的子节点必须是 TableCell 构建器。
tableRow().children([tableRow()])
//                   ^^^^^^^^^^
// Type 'TableRowBuilder' is not assignable to type 'TableCellBuilder'.

// TableCell 的子节点是块级;会拒绝行内子节点委托。
tableCell().children(childrenInline())
//                   ^^^^^^^^^^^^^^^^

// TextRun 是叶子节点,没有子节点。
textRun({ text: 'hi' }).children
//                       ^^^^^^^^
// Property 'children' does not exist on type 'TextRunBuilder'.

// externalHyperlink.link 是必需的。
externalHyperlink({})
//                ^^
// Property 'link' is missing in type '{}' but required in type 'ExternalHyperlinkProps'.

// applyMarks 拒绝 'default' / 'none'。
textRun().applyMarks('default')
//                   ^^^^^^^^^
// Argument of type '"default"' is not assignable to parameter of type 'ApplyMarksPolicy | MarkPolicyBuilder'.

这些规则与运行时的 DOCX_DSL_INVALID_CONTEXT 规则一一对应:约束相同,只是更早暴露。

内联表达装饰

styleOverrides 的一种常见模式是:先声明一个带边框和底纹的命名 Hintbox 段落样式,然后在 DSL 规则中通过 style: 'Hintbox' 引用它。现在,paragraph() 构建器直接暴露了 bordershading 属性,因此装饰可以直接内联在元素上——不再需要 styleOverrides 块。

styleOverrides 的一种常见模式是:先声明一个带边框和底纹的命名 Hintbox 段落样式,然后在 DSL 规则中通过 style: 'Hintbox' 引用它。现在,paragraph() 构建器直接暴露了 bordershading 属性,因此装饰可以直接内联在元素上——不再需要 styleOverrides 块。

The named-style label still has a job: round-trip identity. The DOCX writer emits <w:pStyle w:val="Hintbox"/> whether or not Hintbox is declared in styles.xml. The import side reads that pStyle to reconstruct the matching custom node.

docxNode('hintbox')
  .block()
  .emit(
    paragraph({
      // 往返一致性标记。
      style: 'Hintbox',
      // 装饰现在直接写在这里;不需要 `styleOverrides`。
      spacing: { before: 240, after: 240 },
      border: {
        top: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        bottom: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        left: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        right: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
      },
      // `clear` 底纹让 `fill` 作为背景透出来。
      // `solid` 会以 100% 的覆盖率绘制前景(默认 `auto` → 黑色)
      // 并完全遮住 `fill`。见下方的 OOXML 陷阱。
      shading: { type: 'clear', fill: 'E6F3FF' },
    }).children(childrenInline({ marks: 'default' })),
  )
  .toJSON()

This pairs naturally with cssStyles.extractFromDocument: let CSS handle headings, body fonts, lists, and other generic selectors; let the DSL handle anything tied to a custom node type.

OOXML 陷阱:`solid` 底纹会绘制前景

在 OOXML 中,带有 w:val="solid"<w:shd> 会以 100% 覆盖率绘制前景fill 属性是背景(位于前景之后)。当 color 未设置时,前景默认为 auto,而 Word 会将其渲染为黑色, 从而完全遮住 fill。如果要设置段落背景色,请使用 type: 'clear'(空图案,fill 会透出来)。 同样的陷阱也存在于 JSON DSL 中;这是 OOXML 的语义问题,不是构建器的 bug。


示例详解

以下五个示例与 JSON DSL 参考页 中出现的完全相同,这里通过构建器来编写。

提示框:带内联装饰的块级段落

docxNode('hintbox')
  .block()
  .emit(
    paragraph({
      style: 'Hintbox',
      spacing: { before: 240, after: 240 },
      border: {
        top: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        bottom: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        left: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
        right: { style: 'single', size: 1, color: 'B8D8FF', space: 5 },
      },
      shading: { type: 'clear', fill: 'E6F3FF' },
    }).children(childrenInline({ marks: 'default' })),
  )
  .toJSON()

提及:带模板文本和继承标记的内联 TextRun

docxNode('mention')
  .inline()
  .emit(
    textRun({
      text: template('@{node.attrs.label}'),
      color: ref('node.attrs.color', { default: '4472C4', transform: 'hexNoHash' }),
    }).applyMarks('node'),
  )
  .toJSON()

加粗的 mention 会渲染为 new TextRun({ text: '@alice', color: '4472C4', bold: true })

标注框:带属性驱动底纹和切换样式的单元格表格

docxNode('calloutBox')
  .block()
  .emit(
    table({
      width: { size: 100, type: 'pct' },
      borders: {
        top: { style: 'single', size: 4, color: 'B8D8FF' },
        bottom: { style: 'single', size: 4, color: 'B8D8FF' },
        left: { style: 'single', size: 4, color: 'B8D8FF' },
        right: { style: 'single', size: 4, color: 'B8D8FF' },
      },
    }).rows(
      tableRow().children([
        tableCell({
          shading: {
            type: 'clear',
            fill: unit('normalizeColor', ref('node.attrs.backgroundColor', { default: 'E6F3FF' })),
          },
          margins: {
            top: unit('pointsToTwips', 8),
            bottom: unit('pointsToTwips', 8),
            left: unit('pointsToTwips', 10),
            right: unit('pointsToTwips', 10),
          },
        }).children([
          paragraph({
            style: switchValue({
              on: ref('node.attrs.variant'),
              cases: { warning: 'CalloutWarning', info: 'CalloutInfo' },
              default: 'Callout',
            }),
          }).children(childrenInline({ marks: 'default' })),
        ]),
      ]),
    ),
  )
  .toJSON()

代码块:抑制冲突子标记的段落

docxNode('codeBlock')
  .block()
  .emit(
    paragraph({ style: 'Code' }).children(
      childrenInline({ marks: marks('default').disable('bold', 'italic') }),
    ),
  )
  .toJSON()
docxNode('customLink')
  .inline()
  .emit(
    externalHyperlink({ link: ref('node.attrs.href') }).children([
      textRun({ text: ref('node.attrs.label'), style: 'Hyperlink' }) //
        .applyMarks('node'),
    ]),
  )
  .toJSON()

并排对比:构建器 vs 手写 JSON

构建器生成的 JSON 在传输层面是等价的。下面是同一个 hintbox 规则的两种写法:

构建器

docxNode('hintbox')
  .block()
  .emit(
    paragraph({ style: 'Hintbox' }) //
      .children(childrenInline({ marks: 'default' })),
  )
  .toJSON()

JSON

{
  "type": "hintbox",
  "nodeKind": "block",
  "render": {
    "emit": {
      "element": "Paragraph",
      "props": { "style": "Hintbox" },
      "children": { "$children": { "as": "inline", "marks": "default" } },
    },
  },
}

builder.toJSON() 和手写对象在深度上是相等的,这一点已由包自身的 parity tests 验证。请选择最适合你创作环境的写法;线上的格式是相同的。


cssStyles 的组合

该构建器与 cssStyles 配合得很好,可以清晰地分离职责:

  • CSS 描述通用选择器样式(标题、段落、列表、引用块),这些样式已经存在于编辑器的样式表中。
  • DSL 描述自定义节点渲染:任何需要 Tiptap 节点类型、且无法用通用 CSS 选择器表达的内容。
ExportDocx.configure({
  cssStyles: {
    extractFromDocument: true, // 从当前样式表中提取 .tiptap 规则
    baseFontSize: 16,
  },
  customNodeDsl: {
    dslVersion: '1.0',
    nodes: [
      docxNode('hintbox')
        .block()
        .emit(
          paragraph({
            style: 'Hintbox', // 往返一致性
            border: { top: { style: 'single', size: 1, color: 'B8D8FF', space: 5 } /* ... */ },
            shading: { type: 'clear', fill: 'E6F3FF' },
          }).children(childrenInline({ marks: 'default' })),
        )
        .toJSON(),
    ],
  },
})

采用这种方式后,你可以完全移除 styleOverrides 块。随附的 ExportDocxCustomNodeDslBuilder 演示正是使用这一模式。


冲突策略和运行时错误

构建器只是一个轻量的编写层,因此 JSON DSL 参考页面 中描述的所有运行时约束都完全适用:

  • 当两个 API 中都出现相同的 type 时,基于函数的 customNodes 会优先生效(每次导出调用中,每个冲突类型只记录一次警告)。
  • DSL 错误会返回带有 结构化信封 的结果,其中包含 code + dslPath +(对于运行时错误)nodePath + nodeType
  • 资源上限 会在编译期和运行时强制执行,可通过 customNodeDslLimitsexportDocx 上调整,也可通过 DOCX_DSL_MAX_* 环境变量在 REST 接口上调整。

构建器额外增加了一项运行时检查:将 marks('default') 构建器传给 applyMarks 时,会抛出清晰的错误,而不是生成一个稍后会以 DOCX_DSL_INVALID_SHAPE 失败的负载。其余所有内容都会直接流向标准 DSL 编译器。


TypeScript 参考

所有构建器、辅助函数以及属性形状接口都从 @tiptap-pro/extension-export-docx/dsl 导出,供直接复用:

import type {
  // 顶层规则
  CustomNodeRuleBuilder,

  // 元素构建器
  ParagraphBuilder,
  TextRunBuilder,
  ExternalHyperlinkBuilder,
  TableBuilder,
  TableRowBuilder,
  TableCellBuilder,

  // 子节点辅助
  ChildrenInlineBuilder,
  ChildrenBlockBuilder,
  ChildrenTableRowBuilder,
  ChildrenTableCellBuilder,

  // 渲染节点抽象
  RenderNodeBuilder,

  // 标记策略链
  MarkPolicyBuilder,

  // 各元素属性接口
  ParagraphProps,
  TextRunProps,
  ExternalHyperlinkProps,
  TableProps,
  TableRowProps,
  TableCellProps,

  // 通用形状
  BorderSide,
  Prop,
} from '@tiptap-pro/extension-export-docx/dsl'

Prop<T> 是一个辅助类型,使每个叶子节点都可以接受字面量值(numberstring 等)或 ValueExprref(...)template(...)op(...) 等)。


相关内容

  • Custom-nodes DSL (JSON reference):wire format 的完整规范性描述。builder 只是其上的一层语法糖;有关运行时语义、错误代码和资源限制,请查阅参考文档。
  • Function-based custom nodes:仅用于浏览器内自定义的 JavaScript 回调变体。
  • CSS to DOCX:与 builder 自然配合,用于通用与自定义的分离。
  • Style overrides:手动命名样式声明。对于希望向 Word 的样式选择器公开的字符/段落样式,它仍然有用;但仅仅用于装饰自定义节点时已不再需要。
  • DOCX REST API:相同的 customNodeDsl 载荷,通过 HTTP 发送。