Overview
Operations are atomic changes to the document model. They can be grouped into transactions to ensure consistency and enable undo/redo functionality. Each operation type has specific properties and behaviors.
Operation characteristics:
- Immutable - operations don't modify existing state
- Invertible - each operation has an inverse
- Composable - operations can be combined in transactions
- Validatable - operations can be validated before application
Operation Interface
All operations follow a common interface:
interface Operation {
type: string;
path: Path;
data?: any;
inverse?: Operation; // Pre-computed inverse for efficiency
metadata?: {
source?: 'user' | 'programmatic';
timestamp?: number;
[key: string]: any;
};
}
// Path represents position in document tree
type Path = number[];
// Example paths:
// [0] - first child of root
// [0, 1] - second child of first child
// [0, 1, 2] - third child of second child of first childInsert Operations
Insert operations add content to the document at a specific path.
Delete Operations
Delete operations remove content from the document.
Format Operations
Format operations modify text styling without changing content.
Replace Operations
Replace operations combine delete and insert in a single operation.
Move Operations
Move operations relocate content within the document.
Node Structure Operations
Operations that modify node structure and relationships.
Split Node
Split a node at a position into two nodes
Merge Nodes
Merge two adjacent nodes into one
Wrap
Wrap nodes with another node
Unwrap
Remove wrapper node and promote children
Set Node Type
Change node type while preserving content
Update Attributes
Update node attributes without changing structure
Composite Operations
Composite operations combine multiple operations for complex transformations.
Operation Inversion
Every operation must have an inverse for undo functionality.
class OperationInverter {
invert(operation: Operation): Operation {
switch (operation.type) {
case 'insertText':
return {
type: 'deleteText',
path: operation.path,
length: operation.text.length
};
case 'deleteText':
return {
type: 'insertText',
path: operation.path,
text: operation.deletedContent || ''
};
case 'insertNode':
return {
type: 'deleteNode',
path: operation.path
};
case 'deleteNode':
return {
type: 'insertNode',
path: operation.path,
node: operation.deletedNode
};
case 'applyFormat':
return {
type: 'removeFormat',
path: operation.path,
length: operation.length,
format: operation.format
};
case 'removeFormat':
return {
type: 'applyFormat',
path: operation.path,
length: operation.length,
format: operation.format,
value: operation.previousValue
};
case 'replace':
return {
type: 'replace',
path: operation.path,
length: typeof operation.content === 'string'
? operation.content.length
: 1,
content: operation.deletedContent
};
case 'move':
return {
type: 'move',
fromPath: operation.toPath,
toPath: operation.fromPath
};
case 'splitNode':
return {
type: 'mergeNodes',
path: operation.path,
targetPath: [operation.path[0] + 1],
position: operation.position
};
case 'mergeNodes':
return {
type: 'splitNode',
path: operation.path,
position: operation.position
};
case 'wrap':
return {
type: 'unwrap',
path: operation.path,
wrapperType: operation.wrapper.type,
preservedWrapper: operation.wrapper
};
case 'unwrap':
return {
type: 'wrap',
path: operation.path,
wrapper: operation.preservedWrapper!
};
case 'updateAttributes':
return {
type: 'updateAttributes',
path: operation.path,
attributes: operation.previousAttributes || {},
previousAttributes: operation.attributes
};
case 'setNodeType':
return {
type: 'setNodeType',
path: operation.path,
nodeType: operation.previousType!,
previousType: operation.nodeType,
attributes: operation.previousAttributes,
previousAttributes: operation.attributes
};
default:
throw new Error(`Unknown operation type: ${operation.type}`);
}
}
}
// Pre-compute inverse for efficiency
function createOperationWithInverse(operation: Operation): Operation {
const inverter = new OperationInverter();
return {
...operation,
inverse: inverter.invert(operation)
};
}