From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • Anchor API
  • Location API
  • Path API
  • PointEntry API
  • Point API
  • Range API
  • Selection API
  • Location Types APIs
  • Span API
  • Editor
  • Element API
  • NodeEntry API
  • Node API
  • Node Types APIs
  • Text API
  • Debug Value Scrubbing
  • Transforms API
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • FAQ
  • Resources

Normalizing

PreviousNext

Keep complex Plite documents valid with built-in constraints and focused repairs.

Plite constructs canonical document shapes before publication. Extensions add focused corrections for app-specific invariants; there is no public operation-by-operation normalizer loop.

Canonical Constraints

Plite enforces a small structural contract. It keeps browser selection, canonical changes, history, and collaboration deterministic.

  1. Every Element has a text descendant. Even void elements keep an empty text child so selections can always point into text.
  2. Adjacent text nodes with the same properties merge. Construction emits one canonical leaf instead of retaining equivalent neighbors.
  3. Blocks contain either blocks, or inline/text content. A block cannot mix block children with inline/text children.
  4. Inline nodes have text on both sides. Empty text nodes are inserted before, after, or between inline nodes when needed.
  5. The editor root contains block nodes. Top-level inline or text nodes are removed.
  6. Nodes must be JSON-serializable. Documents and changes need to move cleanly through storage and collaboration layers.
  7. Node property values should not be null. Use optional properties instead so the document has one unambiguous property shape.

These constraints are the base document contract, not app preferences. Construction and contextual slice fitting produce this shape directly. Imported values are validated against the compiled schema instead of being accepted and repaired later.

SerializingUsing TypeScript

On This Page

Canonical ConstraintsWrite Canonical ShapesGroup Related WritesExtension CorrectionsRepairing A Whole Document
Build your editor
Production-ready AI template and reusable components.
Get all-access
DocumentChange

Write Canonical Shapes

Commands should create the shape they intend to read later in the same transaction. Corrections run during transaction closeout, so command logic must not depend on correction timing.

Avoid writes that leave invalid property values:

editor.update((tx) => {
  tx.nodes.set({ url: null }, { at: path });
});
editor.update((tx) => {
  tx.nodes.set({ url: null }, { at: path });
});

null is not a valid node property value. The write fails validation instead of becoming an ambiguous document state.

Prefer a repair that produces a valid shape:

editor.update((tx) => {
  tx.nodes.unset("url", { at: path });
});
editor.update((tx) => {
  tx.nodes.unset("url", { at: path });
});

Or remove the invalid wrapper:

editor.update((tx) => {
  tx.nodes.unwrap({ at: path });
});
editor.update((tx) => {
  tx.nodes.unwrap({ at: path });
});

Group Related Writes

Put related structural writes in one editor.update(...). The callback reads one isolated draft, and Plite publishes one canonical DocumentChange.

import { ElementApi, type Editor } from "@platejs/plite";
 
const LIST_TYPES = ["numbered-list", "bulleted-list"];
 
function changeBlockType(editor: Editor, type: string) {
  editor.update((tx) => {
    const isActive = isBlockActive(tx, type);
    const isList = LIST_TYPES.includes(type);
 
    tx.nodes.unwrap({
      match: (node) =>
        ElementApi.isElement(node) && LIST_TYPES.includes(node.type),
      split: true,
    });
 
    tx.nodes.set({
      type: isActive ? "paragraph" : isList ? "list-item" : type,
    });
 
    if (!isActive && isList) {
      tx.nodes.wrap({ type, children: [] });
    }
  });
}
import { ElementApi, type Editor } from "@platejs/plite";
 
const LIST_TYPES = ["numbered-list", "bulleted-list"];
 
function changeBlockType(editor: Editor, type: string) {
  editor.update((tx) => {
    const isActive = isBlockActive(tx, type);
    const isList = LIST_TYPES.includes(type);
 
    tx.nodes.unwrap({
      match: (node) =>
        ElementApi.isElement(node) && LIST_TYPES.











The callback reads the live draft. Plite runs deterministic corrections over the changed ranges, then publishes one immutable commit.

Extension Corrections

Reusable app-specific repairs belong in editor extensions. Each correction declares the node event that can make its invariant stale: children, content, or properties.

import { ElementApi, defineExtension } from "@platejs/plite";
 
const ValidLinkExtension = defineExtension("valid-link", {
  corrections: [
    {
      event: "properties",
      correct({ entry: [node, path], tx }) {
        if (
          ElementApi.isElement(node) &&
          node.type === "link" &&
          node.url === null
        ) {
          tx.nodes.unset("url", { at: path });
        }
      },
    },
  ],
});
import { ElementApi, defineExtension } from "@platejs/plite";
 
const ValidLinkExtension = defineExtension("valid-link", {
  corrections: [
    {
      event: "properties",
      correct({ entry: [node, path], tx }) {
        if (
          ElementApi.isElement(node) &&
          node.type === "link" &&
          node.url === null
        ) {
          tx.nodes.unset("url", { at: path });
        }
      },
    },
  ],
});

Corrections are event-indexed and start from the transaction's changed ranges. When a correction changes another relevant entry, the bounded worklist queues that entry. This is targeted maintenance, not a whole-document pass after every operation.

Repairing A Whole Document

Use editor.update.value.repair() when raw initial data or a newly installed correction may need an all-root maintenance pass.

editor.update.value.repair();
editor.update.value.repair();

Repair starts its own history-skipped update, scans the primary document and every named root, and publishes nothing when the value is already canonical. It cannot run inside another editor.update(...). Feature commands should rely on normal changed-range closeout instead.

Use transaction methods inside corrections and fix one concrete violation per entry. Avoid direct mutable editor fields.

includes
(node.type),
split: true,
});
tx.nodes.set({
type: isActive ? "paragraph" : isList ? "list-item" : type,
});
if (!isActive && isList) {
tx.nodes.wrap({ type, children: [] });
}
});
}