自定义节点 DSL 构建器(TypeScript)
Beta 功能
构建器与 JSON
DSL 一样,属于同一个 customNodeDsl 功能范畴。两者都以 dslVersion: "1.0" 发布,并生成完全相同的线格式输出。如果你依赖此功能,请固定精确的包版本。
DSL 构建器是编写 custom-nodes DSL 的 TypeScript 友好方式。它是一个小巧、流式、不可变的 API,会编译为线格式所期望的 相同 JSON:每次链式调用都会返回一个新的构建器,而任意链上的 .toJSON() 都会生成 DSL 编译器可接受的字面量载荷。
你将获得:
- 每个元素属性都有自动补全。
paragraph({ … })会提示style、alignment、spacing、border、shading等,这正是 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/docx。 | JSON 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。
border 和 shading 属性允许自定义节点在 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'),
])链接值在运行时也会进行验证,必须以 http、https、mailto 或 tel 开头(长度上限为 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 的真值规则强制转换为布尔值:false、null、undefined、0、"" 为假值,其他值都为真值。
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 节点读取一个值。允许的路径:node、node.type、node.attrs、node.attrs.<key>、node.text、node.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() 构建器直接暴露了 border 和 shading 属性,因此装饰可以直接内联在元素上——不再需要 styleOverrides 块。
styleOverrides 的一种常见模式是:先声明一个带边框和底纹的命名 Hintbox 段落样式,然后在 DSL 规则中通过 style: 'Hintbox' 引用它。现在,paragraph() 构建器直接暴露了 border 和 shading 属性,因此装饰可以直接内联在元素上——不再需要 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()自定义链接:包装 TextRun 的内联 ExternalHyperlink
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。 - 资源上限 会在编译期和运行时强制执行,可通过
customNodeDslLimits在exportDocx上调整,也可通过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> 是一个辅助类型,使每个叶子节点都可以接受字面量值(number、string 等)或 ValueExpr(ref(...)、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 发送。