Composition / foldkit-composition
Editing: Operations
A Document changes by Operations, the way a Model changes by Messages:
Document + Operation → Document, or a refusal with no partial resultts
const { Op, root, region } = Composition
const result = Composition.apply(
Site,
page,
Op.insert({
id: NodeId.make('subtitle'),
block: 'Heading',
props: { text: 'Welcome', level: 2 },
at: region(NodeId.make('intro'), 'body', 1),
}),
)
// Result.succeed({ document, changed: ['subtitle', 'intro'], removed: [] })
// or Result.fail({ code: 'composition:region-rejects', message: '…' })| Operation | What it does |
|---|---|
Op.insert({ id, block, props, at }) | a new node at a position |
Op.insertTree({ tree, at }) | a subtree, such as a Pattern or a paste |
Op.remove(id) | the node and everything it holds |
Op.move(id, to) | the node elsewhere, keeping every id |
Op.duplicate({ id, ids, at }) | a copy of the subtree under the ids given |
Op.setProp(id, prop, value), Op.unsetProp(id, prop) | one prop, checked against the Block |
Op.setWhen, Op.setAppearance, Op.setAction | the reserved fields, stored and cleared |
Op.batch(ops) | several, in order, all or none |
- Positions are
root(index)orregion(parent, name, index). An index counts after the node being moved is taken out, so moving the first of three to index 2 puts it last. applychecks what the Operation causes: a Region that would not accept the node or has no room, a Region left below its bound, a node put inside itself, a taken id, props the Block refuses. It does not refuse an edit for something already wrong elsewhere, so an author keeps working beside a Block the deployment no longer knows. A prop of such a Block cannot be set, since nothing can check it.applynever mints an id. Every id an Operation creates is in it, so a replay creates the same nodes.Composition.newIds(count)is an Effect that makes random ones, to run in a Command.Appliedsays which nodes were added or changed, a parent whose Region changed included, and which were removed, so a view can redraw only those.- Operations are data:
Composition.Operationis their Schema, for a log, an agent's tool or a replay. Composition.takeTree(document, id)takes a subtree, andComposition.rekey(tree, ids)renames every id in it, which is how a paste is inserted twice without a collision. A reference to a node the tree does not hold is kept as it is, forapplyto refuse by name.Composition.treeRefusal(catalog, tree)checks a tree alone, by its own ids, before it is rekeyed: what a paste should say is wrong with it.- Any text is an id,
constructorand__proto__included:apply,validate,indexand the Renderer read nodes by own key only, so a stored Document naming one it lacks iscomposition:missing-node, not a crash.