Operational transformation format with support for concurrent rich text editing.
Cljot is designed for concurrent editing with centralized decision-making, i.e., it is primarily meant to be used with client-server topology where the canonical order of concurrent edits is decided by a central server. This poses some restrictions on true concurrency and entails an acknowledgement-based application of state changes.
Cljot is also heavily inspired by the Quill Delta format. Consequently, Cljot is mostly compatible with Deltas created with Quill Delta.
Cljot is based on deltas, which are vectors of operations applied successively. Three operations are provided:
- Insert: inserts new content, either text or non-opinionated map of attributes
- Retain: either keeps a given range intact or applies attributes to it
- Delete: removes a range
With these three operations, any number of different documents can flexibly be expressed. This covers representing both complete documents (as a lineage of operations that are applied to move from an empty document to the complete document) and changes between two document states.
The Cljot public API is contained within the cljot.delta
, cljot.compose
, and cljot.transform
namespaces.
Cljot models documents using the delta
abstraction.
A delta
is fundamentally a vector of operations.
Parameterless delta constructor gives a new, empty delta:
(delta) ;=> []
Operations can be conjed to a delta by using the insert
, retain
, and delete
functions and threading.
Used for appending content to a given delta:
(-> (delta) (insert "text"))
;=> [#cljot.delta.impl.ops.Insert{:value "text", :attributes nil}]
(-> (delta) (insert "text" {:bold true}))
;=> [#cljot.delta.impl.ops.Insert{:value "text", :attributes {:bold true}}]
Used for either "moving on" within a given delta (to apply some other operation after a given range) or applying attributes to the range:
(-> (delta) (retain 3) (insert "text"))
;=> [#cljot.delta.impl.ops.Retain{:value 3, :attributes nil}
; #cljot.delta.impl.ops.Insert{:value "text", :attributes nil}]
(-> (delta) (retain 1 {:bold true}))
;=> [#cljot.delta.impl.ops.Retain{:value 1, :attributes {:bold true}}]
Used for removing a given range:
(-> (delta) (delete 1))
;=> [#cljot.delta.impl.ops.Delete{:value 1}]
Cljot includes two central functions for supporting operational transformation: delta composition and transformation.
Composes two deltas:
(compose
(-> (delta)
(insert "a"))
(-> (delta)
(insert "b")))
;=> [#cljot.delta.impl.ops.Insert{:value "ba", :attributes nil}]
Transforms a given delta by another.
The order of the two concurrent deltas can be decided:
The first delta takes place first.
(transform-prioritising-a
(-> (delta)
(insert "a"))
(-> (delta)
(insert "b")))
;=> [#cljot.delta.impl.ops.Retain{:value 1, :attributes nil}
; #cljot.delta.impl.ops.Insert{:value "a", :attributes nil}]
The second delta takes place first.
(transform-prioritising-b
(-> (delta)
(insert "a"))
(-> (delta)
(insert "b")))
;=> [#cljot.delta.impl.ops.Insert{:value "a", :attributes nil}]
The transform
function is an alias for transform-prioritising-b
.
Copyright © 2018 Petri Myllys
Distributed under the Eclipse Public License 2.0.