策略语言

策略是定义读取和编辑权限的 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:

  1. 选择声明了该权限的匹配规则。
  2. 如果没有匹配规则,则允许。
  3. 将省略的优先级视为 0,然后只保留 priority 最高的规则。
  4. 在该优先级下,只要任一规则拒绝就拒绝,否则允许。因此,两条未设置优先级的规则发生冲突时会拒绝。

列表顺序和选择器的具体程度不影响权限。只有所有受影响的操作都被允许时,事务才会被允许。

匹配规则结果
内容在 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。它会生成符合此策略语言的普通规则。