策略语言
策略是定义读取和编辑权限的 JSON 对象。除非匹配的规则拒绝访问,否则允许所有内容。
ContentProtectionPolicy
属性
version(1):策略格式版本。仅支持1。rules(ProtectionRule[]):权限规则。空数组允许所有读取和编辑。
const policy: ContentProtectionPolicy = {
version: 1,
rules: [
{
selector: { target: 'node', node: { types: ['heading'] } },
permissions: { edit: false },
},
],
}ProtectionRule
一个选择器及其授予或拒绝的权限。
属性
id?(string):可选的诊断标识符。在策略中必须非空且唯一。匿名规则通过其数组索引标识。selector(ProtectionSelector):规则适用的对象。permissions({ read?: boolean; edit?: boolean }):至少指定一项权限。true表示允许,false表示拒绝。省略权限的规则没有效果,会被忽略。priority?(number):整数优先级。省略时为0。数值越高越优先;允许使用负值。在优先级相同的情况下,无论规则顺序如何,拒绝都优先于允许。两条规则都省略priority时同样适用。reason?(string):在诊断信息中返回的纯文本说明。不影响权限,且不能包含隐藏内容。
ProtectionSelector
使用字符串 target 选择一个方面,或使用非空目标数组选择多个方面。节点目标使用 node,标记目标使用 mark。SingleProtectionSelector 是该类型的字符串目标形式。
多个目标
当多个方面共享权限时,可以组合相同匹配节点或标记的方面:
const rule = {
selector: {
target: ['nodeType', 'attribute'],
node: { types: ['heading'] },
names: ['level'],
},
permissions: { edit: false },
} satisfies ProtectionRule这会保护标题结构和级别,同时保持文本可编辑。它的行为类似于权限和优先级相同的独立 nodeType 和 attribute 规则。
- 节点目标(
node、nodeType、nodeContent、children、attribute)必须与node一起使用,标记目标(mark、markType、markContent、markAttribute)必须与mark一起使用。混合两类目标无效。 - 任意选定方面适用时,规则就会参与。所有方面共享其 ID、原因和优先级。匹配多个方面不会提高优先级,也不会在诊断元数据中重复该规则。重复目标不会产生额外效果。
- 只有选择
attribute或markAttribute时才能使用names,并且它只应用于该方面。对于节点属性,省略表示选择所有属性。标记属性必须提供names。空的names数组不选择任何属性,其他目标仍然生效。 - 如果任意目标是
children,规则只支持edit。read的任意值都无效。 - 空目标数组无效。现有的字符串目标和策略版本
1仍受支持。
within 中的祖先条件适用于每个选定方面。它们只用于定位匹配的节点或标记,不会保护祖先。请参阅嵌套祖先匹配。
target: 'node'
node(NodeMatch):要匹配的节点。edit: false:保护节点的存在、类型、属性和全部内容,包括后代节点和标记。read: false:隐藏节点及其子树。
使用它可以锁定整个节点。更改其类型或匹配的属性不会解锁它。更高优先级的规则可以允许其中的特定编辑。
target: 'nodeType'
node(NodeMatch):要匹配的节点。edit: false:阻止插入、移除、重新设置类型、拆分、合并或移动节点。read: false:隐藏节点及其子树。
仅保护编辑时,内容和属性仍可编辑。已隐藏节点完全不可编辑。之前编辑造成的位置偏移不属于移动。
target: 'nodeContent'
node(NodeMatch):受其内容控制的节点。edit: false:阻止更改文本、后代节点、后代属性和标记。read: false:隐藏节点的子内容。
仅保护编辑时,所属节点的类型、属性和存在性不受保护。更改其类型或匹配的属性可能允许后续编辑。使用 node 保护整个节点,或添加 nodeType 和适当的 attribute 规则。删除包含受保护内容的所属节点仍需要删除这些内容的权限。
使用 read: false 时,所属节点也不可编辑:即使内容为空,也不能更改其类型、属性和存在性。这可以防止通过更改所属节点来显示隐藏内容。
嵌套内容规则
内容规则会递归应用,包括嵌套 Slot。当独立规则的优先级高于祖先编辑规则时,它们可以授予或拒绝子树内部的编辑权限。使用 within 或属性匹配嵌套区域。选择器深度不影响优先级,覆盖性的编辑规则也适用于其后代。
这不会覆盖读取限制:已隐藏祖先内部的内容仍然隐藏且无法编辑。请参阅读取限制。
const rules = [
{
selector: { target: 'nodeContent', node: { types: ['blockquote'] } },
permissions: { edit: false },
},
{
selector: {
target: 'nodeContent',
node: { types: ['paragraph'], within: { types: ['blockquote'] } },
},
permissions: { edit: true },
priority: 10,
},
] satisfies ProtectionRule[]这允许编辑受保护 blockquote 内的段落文本和格式。blockquote 的其他内容仍受保护。允许规则的优先级必须高于拒绝规则的默认值 0。
target: 'children'
node(NodeMatch):要匹配的父节点。edit: false:父节点存在时,保留直接非文本子节点的有序序列。read:不支持;请改用nodeContent。
子节点中的文本和格式仍可编辑。使用 nodeType 保护父节点的存在性,而不冻结其内容。该目标支持列固定、单元格可编辑且正文行可删除的表格。
target: 'attribute'
node(NodeMatch):要匹配的节点。names?(string[]):要控制的属性。省略表示所有属性,[]表示没有属性。edit: false:阻止更改选定属性。read: false:隐藏所属节点及其子树。
target: 'mark'
mark(MarkMatch):要匹配的标记。edit: false:保护标记的存在性、类型、属性和标记内容,包括其他格式。read: false:隐藏标记内容。
使用它可以锁定带标记的段落及其元数据。
target: 'markType'
mark(MarkMatch):要匹配的标记。edit: false:阻止添加、移除或用其他类型替换标记。read: false:隐藏标记内容。
文字、标记属性和其他格式仍可编辑。删除带有标记的最后一段内容会移除标记,并需要相应权限。使用 markAttribute 可以在允许更改文字的同时保护元数据。
target: 'markContent'
mark(MarkMatch):标识受保护内容的标记。edit: false:阻止更改标记内容,包括其标记和后代内容。read: false:隐藏标记内容。
移除或更改选定标记也受此规则控制。对于编辑保护,markContent 和 mark 覆盖相同的标记范围。更高优先级的 markType 规则可以允许移除标记;请在单独的事务中编辑新解锁的内容。
target: 'markAttribute'
mark(MarkMatch):要匹配的标记。names(string[]):要控制的属性。必需;[]表示不选择任何属性。edit: false:阻止更改选定的标记属性。read: false:隐藏标记内容。
NodeMatch
匹配文档中的节点,与 DOM 结构或 CSS 无关。
属性
types?(string[]):精确且区分大小写的 schema 名称。省略表示所有节点类型,[]表示没有类型。各项按 OR 组合。attributes?(AttributePredicate[]):按 AND 组合的属性测试。省略或为[]表示不限制属性。within?(NodeMatch):要求匹配的严格祖先。嵌套的within值表示更上层的祖先。
{} 匹配每个节点。使用其 schema 名称(通常为 doc)选择根节点;根节点上的 nodeContent 覆盖整个文档。
嵌套祖先匹配
const rule = {
selector: {
target: ['nodeType', 'attribute'],
node: {
types: ['heading'],
within: {
types: ['blockquote'],
within: {
types: ['blockSlot'],
attributes: [{ name: 'id', operator: 'eq', value: 'legal-notice' }],
},
},
},
names: ['level'],
},
permissions: { edit: false },
} satisfies ProtectionRule当标题位于 legal-notice Slot 内的 blockquote 中时,这会保护标题结构和级别。文本仍可编辑。每个 within 都要求在任意深度存在祖先,不一定是直接父节点,并且这些条件必须组成祖先链。
blockquote 和 Slot 是匹配条件,不是额外的保护目标。within 只接受匹配器属性;在其中放置 target 或 permissions 无效。请使用单独的规则保护祖先本身。
使用 UniqueID 定位单个节点
为相关节点类型配置 UniqueID:
import { UniqueID } from '@tiptap/extension-unique-id'
UniqueID.configure({ types: ['paragraph'] })匹配节点存储的 id 属性。将 'paragraph-id' 替换为文档中的 ID:
const rule = {
selector: {
target: 'nodeContent',
node: {
types: ['paragraph'],
attributes: [{ name: 'id', operator: 'eq', value: 'paragraph-id' }],
},
},
permissions: { edit: false },
} satisfies ProtectionRule这会保护该段落的内容。改用 target: 'node' 可同时保护其类型、存在性和属性,包括 ID。如果 UniqueID 使用自定义 attributeName,请在谓词中使用该名称,而不是 id。
MarkMatch
匹配行内节点上的标记。
属性
types?(string[]):精确且区分大小写的标记名称。省略表示所有标记类型,[]表示没有类型。各项按 OR 组合。attributes?(AttributePredicate[]):按 AND 组合的属性测试。省略或为[]表示不限制属性。within?(NodeMatch):要求存在标记行内节点的匹配祖先。
不同标记类型会独立评估,包括它们重叠的情况。
AttributePredicate
这些谓词专用于内容保护,不实现外部查询标准。operator 从下面的测试中选择一个。属性名称指完整属性,不支持带点的路径。
属性值必须是 JSON:字符串、有限数字、布尔值、null,或包含这些值的数组和普通对象。TypeScript API 将此类型称为 JsonValue。
operator: 'exists'
name(string):键必须存在的属性。值为null仍算作存在。
operator: 'eq'
name(string):要比较的属性。value(JsonValue):要求的值。使用不进行强制转换的结构相等;忽略对象键顺序,保留数组顺序。
operator: 'in'
name(string):要比较的属性。values(JsonValue[]):可接受的值,使用eq比较。[]不匹配任何内容。
schema 默认值会计为属性值。缺少属性与属性值为 null 不同。
规则优先级
对每个受影响的操作分别解析 read 和 edit:
- 选择声明了该权限的匹配规则。
- 如果没有匹配规则,则允许。
- 将省略的优先级视为
0,然后只保留priority最高的规则。 - 在该优先级下,只要任一规则拒绝就拒绝,否则允许。因此,两条未设置优先级的规则发生冲突时会拒绝。
列表顺序和选择器的具体程度不影响权限。只有所有受影响的操作都被允许时,事务才会被允许。
| 匹配规则 | 结果 |
|---|---|
内容在 0 拒绝;内容在 10 允许 | 允许访问允许区域。 |
在 10 同时允许和拒绝 | 拒绝。 |
属性在 20 拒绝;外层内容在 10 允许 | 拒绝属性更改。 |
读取在 50 允许;编辑在 0 拒绝 | 可读但不可编辑。 |
读取限制
- 读取规则会在每个选定目标上按优先级解析。读取被拒绝的目标会隐藏其整个子树。
- 子节点上的独立
read: true规则不会显示该子节点,即使其优先级高于祖先的读取拒绝。 - 要显示子节点,请缩小祖先的拒绝范围,或使用更高优先级、针对祖先本身的读取允许规则。
- 已隐藏节点不可编辑,包括其类型、属性、存在性和内容。无需额外的
edit: false规则。重新设置类型、删除或更改匹配属性都不能显示或移除隐藏内容。 - 已隐藏的标记内容不能更改,也不能移除其选定标记。无论优先级如何,编辑允许都不能覆盖读取拒绝。
- 拒绝某个属性会隐藏其所属节点,不会只对 NodeView 做选择性清理。
有关渲染、剪贴板和导出行为,请参阅已隐藏内容。
验证
将策略直接传给 ContentProtection.configure({ policy }) 或 setContentProtectionPolicy({ policy })。扩展会自动验证策略,并存储不可变副本。
独立工具也直接接受策略 JSON,并根据文档或事务的 schema 验证。使用 validateContentProtectionPolicy 可以在不应用策略的情况下检查它。
无效策略包括:
- 不支持的版本、未知属性、目标或运算符。
- 空目标数组、混合节点/标记目标,或没有属性目标却使用
names。 - 任意目标为
markAttribute时缺少names。 - 空权限、重复或空 ID、非整数优先级。
- 无效 JSON 值,或 schema 中不存在的显式类型/属性。
- 在
children上使用read,包括与其他目标组合时的情况。
如何检查编辑
输入、粘贴内容、运行编辑器命令(包括 setContent)以及使用本地撤销/重做时,都会检查本地更改。Tiptap 协作更新(包括初始同步和协作撤销/重做)会绕过本地权限检查,以便使用不同策略的协作者保持同步。
如果事务的任意部分进行了被禁止的更改,整个事务都会被拒绝。例如,同时覆盖可编辑文本和受保护文本的粘贴操作会使文档保持不变。
对于仅编辑 Slot 的场景,请使用 createSlotFillingPolicy。它会生成符合此策略语言的普通规则。