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

Available in Start planBetav0.18.0

Beta 功能

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

DSL 构建器是以 TypeScript 友好 的方式编写 custom-nodes DSL 的方法。它是一个小巧、流式、不可变的 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-shake。那些从不接触 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。可接受单个渲染节点构建器、字面量 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: 'Hintbox',
  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 节点 marks。

textRun({
  text: template('@{node.attrs.label}'),
  color: '4472C4',
  size: 24, // 半磅
  highlight: 'cyan',
  style: 'Hyperlink', // 往返一致性标记
}).applyMarks('node')
方法用途
.applyMarks(policy)'node'(继承该规则的 PM 节点 marks)或一个 marks('node')... 构建器,用于更细粒度控制。这里明确拒绝使用 'default''none' — 见 Mark policies
.inheritOverrides(value)当为 false 时,跳过该 run 的 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 分页符 run 的块级段落。若要在段落内部插入行内分页符,请改用 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 中是保留字。另有一种用于选择属性值的 值形式 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')... 构建器——这里的传输格式会拒绝 '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 块。

命名样式标签仍然有作用:往返一致性。无论 styles.xml 中是否声明了 Hintbox,DOCX 写入器都会输出 <w:pStyle w:val="Hintbox"/>。导入端会读取该 pStyle,以重建匹配的自定义节点。

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()

这与 cssStyles.extractFromDocument 自然搭配——让 CSS 处理标题、正文字体、列表和其他通用选择器;让 DSL 处理任何绑定到自定义节点类型的内容。

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

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


示例详解

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

Hintbox — 带内联装饰的块级段落

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()

Mention — 带模板文本和继承标记的内联 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 })

Callout — 单元格表格,带属性驱动的底纹和切换样式

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()

Code block — 抑制冲突子级标记的段落

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() 和手写对象是深度相等的——这一点已经通过包自身的对等性测试验证。请选择最适合你编写环境的方式;传输格式是相同的。


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 参考页 中描述的所有运行时约束都同样适用:

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

构建器额外增加了一项运行时检查:将 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(...) 等)。


相关内容

  • 自定义节点 DSL(JSON 参考) — 对传输格式的完整规范性描述。构建器只是其语法糖;有关运行时语义、错误代码和资源限制,请查阅参考文档。
  • 基于函数的自定义节点 — 仅用于浏览器内自定义的 JavaScript 回调变体。
  • CSS 转 DOCX — 与构建器自然配合,用于通用样式与自定义样式的分离。
  • 样式覆盖 — 手动命名样式声明。对于希望暴露给 Word 样式选择器的字符样式 / 段落样式仍然有用;但不再只是为了给自定义节点加装饰所必需。
  • DOCX REST API — 相同的 customNodeDsl 负载,通过 HTTP 发送。