Plite constructs canonical document shapes before publication. Plugins add focused corrections for app-specific invariants; there is no public operation-by-operation normalizer loop.
Plite enforces a small structural contract. It keeps browser selection, canonical changes, history, and collaboration deterministic.
null; use tx.nodes.unset(...) to delete an optional property explicitly.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.
DocumentChangeCommands 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.
tx.nodes.set(...) treats null and undefined as property deletions:
editor.update((tx) => {
tx.nodes.set({ url: null }, { at: path });
});editor.update((tx) => {
tx.nodes.set({ url: null }, { at: path });
});This deletes url; it does not store a JSON null.
Use unset to make the deletion explicit:
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 });
});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 "plitejs";
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 "plitejs";
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.
Reusable app-specific repairs belong in editor plugins. Each correction
declares the node event that can make its invariant stale: children,
content, or properties.
import { ElementApi, definePlugin } from "plitejs";
const ValidLinkPlugin = definePlugin("valid-link", {
corrections: [
{
event: "properties",
correct({ entry: [node, path], tx }) {
if (
ElementApi.isElement(node) &&
node.type === "link" &&
typeof node.url === "string" &&
node.url.trim() === ""
) {
tx.nodes.unset("url", { at: path });
}
},
},
],
});import { ElementApi, definePlugin } from "plitejs";
const ValidLinkPlugin = definePlugin("valid-link", {
corrections: [
{
event: "properties",
correct({ entry: [node, path], tx }) {
if (
ElementApi.isElement(node) &&
node.type === "link" &&
typeof node.url === "string" &&
node.url.trim() === ""
) {
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.
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.