{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plugin-rules-docs",
  "title": "Plugin Rules",
  "description": "Configure common editing behaviors.",
  "files": [
    {
      "path": "../../content/docs/(guides)/plugin-rules.mdx",
      "content": "---\ntitle: Plugin Rules\ndescription: Configure common editing behaviors.\n---\n\nPlugin Rules control how editor nodes respond to common user actions. Instead of overriding the editor methods, you can configure these behaviors directly on a plugin's `rules` property.\n\nThis guide shows you how to use `rules.break`, `rules.delete`, `rules.merge`, `rules.normalize`, `rules.selection`\n and `rules.match` to create intuitive editing experiences.\n\n<ComponentPreview name=\"plugin-rules-demo\" />\n\n## Actions\n\nPlugin rules use specific action names to define behavior:\n\n- **`'default'`**: Default Slate behavior.\n- **`'reset'`**: Changes the current block to a default paragraph, keeping content.\n- **`'exit'`**: Exits the current block, inserting a new paragraph after it. See [Exit Break](/docs/exit-break) to learn more about this behavior.\n- **`'lift'`**: Lifts the current block out of the nearest matching ancestor container, changing one structural level at a time.\n- **`'deleteExit'`**: Deletes content then exits the block.\n- **`'lineBreak'`**: Inserts a line break (`\\n`) instead of splitting the block.\n\n### `default`\n\nStandard Slate behavior. For `rules.break`, splits the block. For `rules.delete`, merges with the previous block.\n\n```tsx\n<p>\n  Hello world|\n</p>\n```\n\nAfter pressing `Enter`:\n\n```tsx\n<p>Hello world</p>\n<p>\n  |\n</p>\n```\n\nAfter pressing `Backspace`:\n\n```tsx\n<p>Hello world|</p>\n```\n\n### `reset`\n\nConverts the current block to a default paragraph while preserving content. Custom properties are removed.\n\n```tsx\n<h3 listStyleType=\"disc\">\n  |\n</h3>\n```\n\nAfter pressing `Enter` with `rules: { break: { empty: 'reset' } }`:\n\n```tsx\n<p>\n  |\n</p>\n```\n\n### `exit`\n\nExits the current block structure by inserting a new paragraph after it.\n\n```tsx\n<blockquote>\n  |\n</blockquote>\n```\n\nAfter pressing `Enter` with `rules: { break: { empty: 'exit' } }`:\n\n```tsx\n<blockquote>\n  <text />\n</blockquote>\n<p>\n  |\n</p>\n```\n\n### `lift`\n\nLifts the current block out of the nearest matching ancestor container.\n\n```tsx\n<blockquote>\n  <p>\n    |\n  </p>\n</blockquote>\n```\n\nAfter pressing `Enter` with `rules: { break: { empty: 'lift' } }`:\n\n```tsx\n<p>\n  |\n</p>\n```\n\n### `deleteExit`\n\nDeletes content then exits the block.\n\n```tsx\n<blockquote>\n  line1\n  |\n</blockquote>\n```\n\nAfter pressing `Enter` with `rules: { break: { emptyLineEnd: 'deleteExit' } }`:\n\n```tsx\n<blockquote>line1</blockquote>\n<p>\n  |\n</p>\n```\n\n### `lineBreak`\n\nInserts a soft line break (`\\n`) instead of splitting the block.\n\n```tsx\n<blockquote>\n  Hello|\n</blockquote>\n```\n\nAfter pressing `Enter` with `rules: { break: { default: 'lineBreak' } }`:\n\n```tsx\n<blockquote>\n  Hello\n  |\n</blockquote>\n```\n\n## `rules.break`\n\nControls what happens when users press `Enter` within specific block types.\n\n### Configuration\n\n```tsx\nCalloutPlugin.configure({\n  rules: {\n    break: {\n      // Action when Enter is pressed normally\n      default: 'default' | 'lineBreak' | 'exit' | 'deleteExit',\n      \n      // Action when Enter is pressed in an empty block\n      empty: 'default' | 'reset' | 'exit' | 'lift' | 'deleteExit',\n      \n      // Action when Enter is pressed at end of empty line\n      emptyLineEnd: 'default' | 'exit' | 'deleteExit',\n\n      // If true, the new block after splitting will be reset\n      splitReset: boolean,\n    },\n  },\n});\n```\n\nEach property controls a specific scenario:\n\n- `default`\n  - [`'default'`](#default)\n  - [`'lineBreak'`](#linebreak)\n  - [`'exit'`](#exit)\n  - [`'deleteExit'`](#deleteexit)\n\n- `empty`\n  - [`'default'`](#default)\n  - [`'reset'`](#reset)\n  - [`'exit'`](#exit)\n  - [`'lift'`](#lift)\n  - [`'deleteExit'`](#deleteexit)\n\n- `emptyLineEnd`\n  - [`'default'`](#default)\n  - [`'exit'`](#exit)\n  - [`'deleteExit'`](#deleteexit)\n\n- `splitReset`: If `true`, resets the new block to the default type after a split. This is useful for exiting a formatted block like a heading.\n\n### Examples\n\n**Reset heading on break:**\n\n```tsx\nimport { H1Plugin } from '@platejs/heading/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  H1Plugin.configure({\n    rules: {\n      break: {\n        splitReset: true,\n      },\n    },\n  }),\n];\n```\n\nBefore pressing `Enter`:\n\n```tsx\n<h1>\n  Heading|text\n</h1>\n```\n\nAfter (split and reset):\n\n```tsx\n<h1>\n  Heading\n</h1>\n<p>\n  |text\n</p>\n```\n\n**Callout with line breaks and smart exits:**\n\n```tsx\nimport { CalloutPlugin } from '@platejs/callout/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CalloutPlugin.configure({\n    rules: {\n      break: {\n        default: 'lineBreak',\n        empty: 'reset',\n        emptyLineEnd: 'deleteExit',\n      },\n    },\n  }),\n];\n```\n\nBefore pressing `Enter` in callout:\n```tsx\n<callout>\n  Quote text|\n</callout>\n```\n\nAfter (line break):\n```tsx\n<callout>\n  Quote text\n  |\n</callout>\n```\n\n**Code block with custom empty handling:**\n\n```tsx\nimport { CodeBlockPlugin } from '@platejs/code-block/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CodeBlockPlugin.configure({\n    rules: {\n      delete: { empty: 'reset' },\n      match: ({ editor, rule }) => {\n        return rule === 'delete.empty' && isCodeBlockEmpty(editor);\n      },\n    },\n  }),\n];\n```\n\nBefore pressing `Backspace` in empty code block:\n```tsx\n<code_block>\n  <code_line>\n    |\n  </code_line>\n</code_block>\n```\n\nAfter (reset):\n```tsx\n<p>\n  |\n</p>\n```\n\n## `rules.delete`\n\nControls what happens when users press `Backspace` at specific positions.\n\n### Configuration\n\n```tsx\nHeadingPlugin.configure({\n  rules: {\n    delete: {\n      // Action when Backspace is pressed at block start\n      start: 'default' | 'reset' | 'lift',\n      \n      // Action when Backspace is pressed in empty block\n      empty: 'default' | 'reset',\n    },\n  },\n});\n```\n\nEach property controls a specific scenario:\n\n- `start`\n  - [`'default'`](#default)\n  - [`'reset'`](#reset)\n  - [`'lift'`](#lift)\n\n- `empty`\n  - [`'default'`](#default)\n  - [`'reset'`](#reset)\n\n### Examples\n\n**Reset callouts at start:**\n\n```tsx\nimport { CalloutPlugin } from '@platejs/callout/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CalloutPlugin.configure({\n    rules: {\n      delete: { start: 'reset' },\n    },\n  }),\n];\n```\n\nBefore pressing `Backspace` at start:\n```tsx\n<callout>\n  |Callout content\n</callout>\n```\n\nAfter (reset):\n```tsx\n<p>\n  |Callout content\n</p>\n```\n\n**List items with start reset:**\n\n```tsx\nimport { ListPlugin } from '@platejs/list/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  ListPlugin.configure({\n    rules: {\n      delete: { start: 'reset' },\n      match: ({ rule, node }) => {\n        return rule === 'delete.start' && Boolean(node.listStyleType);\n      },\n    },\n  }),\n];\n```\n\nBefore pressing `Backspace` at start of list item:\n```tsx\n<p listStyleType=\"disc\">\n  |List item content\n</p>\n```\n\nAfter (reset):\n```tsx\n<p>\n  |List item content\n</p>\n```\n\n## `rules.merge`\n\nControls how blocks behave when merging with previous blocks.\n\n### Configuration\n\n```tsx\nParagraphPlugin.configure({\n  rules: {\n    merge: {\n      // Whether to remove empty blocks when merging\n      removeEmpty: boolean,\n    },\n  },\n});\n```\n\n### Examples\n\nOnly paragraph and heading plugins enable removal by default. Most other plugins use `false`:\n\n```tsx\nimport { H1Plugin, ParagraphPlugin } from 'platejs/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  H1Plugin, // rules.merge: { removeEmpty: true } by default\n  ParagraphPlugin, // rules.merge: { removeEmpty: true } by default\n];\n```\n\nBefore pressing `Backspace` at start:\n```tsx\n<p>\n  <text />\n</p>\n<h1>\n  |Heading content\n</h1>\n```\n\nAfter (empty paragraph removed):\n```tsx\n<h1>\n  |Heading content\n</h1>\n```\n\n**Callout with removal disabled:**\n\n```tsx\nimport { CalloutPlugin } from '@platejs/callout/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CalloutPlugin.configure({\n    rules: {\n      merge: { removeEmpty: false }, // Default\n    },\n  }),\n];\n```\n\nBefore pressing `Backspace` at start:\n```tsx\n<p>\n  <text />\n</p>\n<callout>\n  |Callout content\n</callout>\n```\n\nAfter (empty paragraph preserved):\n```tsx\n<p>\n  |Code content\n</p>\n```\n\n**Table cells preserve structure during merge:**\n\n```tsx\nimport { TablePlugin } from '@platejs/table/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  TablePlugin, // Table cells have rules.merge: { removeEmpty: false }\n];\n```\n\nBefore pressing `Delete` at end of paragraph:\n```tsx\n<p>\n  Content|\n</p>\n<table>\n  <tr>\n    <td>\n      <p>Cell data</p>\n    </td>\n    <td>\n      <p>More data</p>\n    </td>\n  </tr>\n</table>\n```\n\nAfter (cell content merged, structure preserved):\n```tsx\n<p>\n  Content|Cell data\n</p>\n<table>\n  <tr>\n    <td>\n      <p>\n        <text />\n      </p>\n    </td>\n    <td>\n      <p>More data</p>\n    </td>\n  </tr>\n</table>\n```\n\n<Callout>\nSlate's default is `true` since the default block (paragraph) is first-class, while Plate plugins are likely used to define other node behaviors that shouldn't automatically remove empty predecessors.\n</Callout>\n\n## `rules.normalize`\n\nControls how nodes are normalized during the normalization process.\n\n### Configuration\n\n```tsx\nLinkPlugin.configure({\n  rules: {\n    normalize: {\n      // Whether to remove nodes with empty text\n      removeEmpty: boolean,\n    },\n  },\n});\n```\n\n### Examples\n\n**Remove empty link nodes:**\n\n```tsx\nimport { LinkPlugin } from '@platejs/link/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  LinkPlugin.configure({\n    rules: {\n      normalize: { removeEmpty: true },\n    },\n  }),\n];\n```\n\nBefore normalization:\n```tsx\n<p>\n  <a href=\"http://google.com\">\n    <text />\n  </a>\n  <cursor />\n</p>\n```\n\nAfter normalization (empty link removed):\n```tsx\n<p>\n  <cursor />\n</p>\n```\n\n## `rules.match`\n\nThe `match` function in plugin rules allows you to override the default behavior of specific plugins based on node properties beyond just type matching. This is particularly useful when you want to extend existing node types with new behaviors.\n\n### Examples\n\n**Code block with custom empty detection:**\n\n```tsx\nimport { CodeBlockPlugin } from '@platejs/code-block/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CodeBlockPlugin.configure({\n    rules: {\n      delete: { empty: 'reset' },\n      match: ({ rule, node }) => {\n        return rule === 'delete.empty' && isCodeBlockEmpty(editor);\n      },\n    },\n  }),\n];\n```\n\nSince the list plugin extends existing blocks that already have their own plugin configuration (e.g. `ParagraphPlugin`), using `rules.match` allows you to override those behaviors.\n\n**List override for paragraphs:**\n\n```tsx\nimport { ListPlugin } from '@platejs/list/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  ListPlugin.configure({\n    rules: {\n      match: ({ editor, rule }) => {\n        return rule === 'delete.empty' && isCodeBlockEmpty(editor);\n      },\n    },\n  }),\n];\n```\n\n## Custom Reset Logic\n\nSome plugins need special reset behavior beyond the standard paragraph conversion. You can override the `resetBlock` transform:\n\n**List plugin reset (outdents instead of converting to paragraph):**\n\n```tsx\nconst ListPlugin = createPlatePlugin({\n  key: 'list',\n  // ... other config\n}).overrideEditor(({ editor, tf: { resetBlock } }) => ({\n  transforms: {\n    resetBlock(options) {\n      if (editor.api.block(options)?.[0]?.listStyleType) {\n        outdentList();\n        return;\n      }\n      \n      return resetBlock(options);\n    },\n  },\n}));\n```\n\n**Code block reset (unwraps instead of converting):**\n\n```tsx\nconst CodeBlockPlugin = createPlatePlugin({\n  key: 'code_block',\n  // ... other config\n}).overrideEditor(({ editor, tf: { resetBlock } }) => ({\n  transforms: {\n    resetBlock(options) {\n      if (editor.api.block({\n        at: options?.at,\n        match: { type: 'code_block' },\n      })) {\n        unwrapCodeBlock();\n        return;\n      }\n      \n      return resetBlock(options);\n    },\n  },\n}));\n```\n\n## Combining Rules\n\nYou can combine different rules for comprehensive block behavior:\n\n```tsx\nimport { H1Plugin } from '@platejs/heading/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  H1Plugin.configure({\n    rules: {\n      break: {\n        empty: 'reset',\n        splitReset: true,\n      },\n      delete: {\n        start: 'reset',\n      },\n    },\n  }),\n];\n```\n\n**Line break behavior (default):**\n```tsx\n<blockquote>\n  Hello|\n</blockquote>\n```\n\nAfter `Enter`:\n```tsx\n<blockquote>\n  Hello\n  |\n</blockquote>\n```\n\n**Empty reset behavior:**\n```tsx\n<blockquote>\n  |\n</blockquote>\n```\n\nAfter `Enter`:\n```tsx\n<p>\n  |\n</p>\n```\n\n**Start reset behavior:**\n```tsx\n<blockquote>\n  |Quote content\n</blockquote>\n```\nAfter `Backspace`:\n```tsx\n<p>\n  |Quote content\n</p>\n```\n\n## Advanced\n\nFor complex scenarios beyond simple rules, you can override editor transforms directly using [`.overrideEditor`](/docs/plugin-methods#overrideeditor). This gives you complete control over transforms like [`resetBlock`](/docs/plugin-methods#extendtransforms) and [`insertExitBreak`](/docs/plugin-methods#extendtransforms):\n\n```tsx\nconst CustomPlugin = createPlatePlugin({\n  key: 'custom',\n  // ... other config\n}).overrideEditor(({ editor, tf: { insertBreak, deleteBackward, resetBlock } }) => ({\n  transforms: {\n    insertBreak() {\n      const block = editor.api.block();\n      \n      if (/* Custom condition */) {\n        // Custom behavior\n        return;\n      }\n      \n      // Default behavior\n      insertBreak();\n    },\n    \n    deleteBackward(unit) {\n      const block = editor.api.block();\n      \n      if (/* Custom condition */) {\n        // Custom behavior\n        return;\n      }\n      \n      deleteBackward(unit);\n    },\n    \n    resetBlock(options) {\n      if (/* Custom condition */) {\n        // Custom behavior\n        return true;\n      }\n      \n      return resetBlock(options);\n    },\n  },\n}));\n```\n\n## `rules.selection`\n\nControls how cursor positioning and text insertion behave at node boundaries, particularly for marks and inline elements.\n\n### Configuration\n\n```tsx\nBoldPlugin.configure({\n  rules: {\n    selection: {\n      // Define selection behavior at boundaries\n      affinity: 'default' | 'directional' | 'outward' | 'hard',\n    },\n  },\n});\n```\n\n### Affinity Options\n\nThe `affinity` property determines how the cursor behaves when positioned at the boundary between different marks or inline elements:\n\n#### `default`\n\nUses Slate's default behavior. For marks, the cursor has outward affinity at the start edge (typing before the mark doesn't apply it) and inward affinity at the end edge (typing after the mark extends it).\n\n**At end of mark (inward affinity):**\n```tsx\n<p>\n  <text bold>Bold text|</text><text>Normal text</text>\n</p>\n```\n\nTyping would extend the bold formatting to new text.\n\n**At start of mark (outward affinity):**\n```tsx\n<p>\n  <text>Normal text|</text><text bold>Bold text</text>\n</p>\n```\n\nTyping would not apply bold formatting to new text.\n\n#### `directional`\n\nSelection affinity is determined by the direction of cursor movement. When the cursor moves to a boundary, it maintains the affinity based on where it came from.\n\n```tsx\nimport { BoldPlugin } from '@platejs/basic-nodes/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  BoldPlugin.configure({\n    rules: {\n      selection: { affinity: 'directional' },\n    },\n  }),\n];\n```\n\n**Movement from right (inward affinity):**\n```tsx\n<p>\n  <text>Normal</text><text bold>B|old text</text>\n</p>\n```\n\nAfter pressing `←`:\n```tsx\n<p>\n  <text>Normal</text><text bold>|Bold text</text>\n</p>\n```\n\nTyping would extend the bold formatting, which is not possible with `default` affinity.\n\n```tsx\nimport { LinkPlugin } from '@platejs/link/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  LinkPlugin.configure({\n    rules: {\n      selection: { affinity: 'directional' },\n    },\n  }),\n];\n```\n\n**Movement from right (outward affinity):**\n```tsx\n<p>\n  Visit <a href=\"https://example.com\">our website</a> |for more information text.\n</p>\n```\n\nAfter pressing `←`:\n```tsx\n<p>\n  Visit <a href=\"https://example.com\">our website</a>| for more information text.\n</p>\n```\n\nCursor movement direction determines whether new text extends the link or creates new text outside it.\n\n#### `outward`\n\nForces outward affinity, automatically clearing marks when typing at their boundaries. This creates a natural \"exit\" behavior from formatted text.\n\n```tsx\nimport { CommentPlugin } from '@platejs/comment/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CommentPlugin.configure({\n    rules: {\n      selection: { affinity: 'outward' },\n    },\n  }),\n];\n```\n\n**At end of marked text:**\n```tsx\n<p>\n  <text comment>Commented text|</text><text>Normal</text>\n</p>\n```\n\nAfter typing:\n```tsx\n<p>\n  <text comment>Commented text</text><text>x|Normal</text>\n</p>\n```\n\nUsers automatically exit comment formatting by typing at the end of commented text.\n\n#### `hard`\n\nCreates a \"hard\" edge that requires two key presses to move across. This provides precise cursor control for elements that need exact positioning.\n\n```tsx\nimport { CodePlugin } from '@platejs/basic-nodes/react';\n\nconst plugins = [\n  // ...otherPlugins,\n  CodePlugin.configure({\n    rules: {\n      selection: { affinity: 'hard' },\n    },\n  }),\n];\n```\n\n**Moving across hard edges:**\n```tsx\n<p>\n  <text>Before</text><text code>code|</text><text>After</text>\n</p>\n```\n\nFirst `→` press changes affinity:\n```tsx\n<p>\n  <text>Before</text><text code>code</text>|<text>After</text>\n</p>\n```\n\nSecond `→` press moves cursor:\n```tsx\n<p>\n  <text>Before</text><text code>code</text><text>A|fter</text>\n</p>\n```\n\nThis allows users to position the cursor precisely at the boundary and choose whether new text should be inside or outside the code formatting.\n",
      "type": "registry:file",
      "target": "content/docs/plate/(guides)/plugin-rules.mdx"
    }
  ],
  "type": "registry:file"
}