Class: RichText::Delta
- Inherits:
-
Object
- Object
- RichText::Delta
- Defined in:
- lib/rich-text/delta.rb
Overview
A Delta is made up of an array of operations. All methods maintain the property that Deltas are represented in the most compact form. For example two consecutive insert operations with the same attributes will be merged into one. Thus a vanilla deep Hash/Array comparison can be used to determine Delta equality.
A Delta with only insert operations can be used to represent a fully formed document. This can be thought of as a Delta applied to an empty document.
Instance Attribute Summary collapse
- #ops ⇒ Array<Op> readonly
Instance Method Summary collapse
-
#+(other) ⇒ Object
The non-destructive version of #concat.
-
#==(other) ⇒ Boolean
(also: #eql?)
A Delta is equal to another if all the ops are equal.
-
#chop! ⇒ Delta
Modifies self by removing the last op if it was a retain without attributes.
-
#compose(other) ⇒ Delta
(also: #|)
Returns a Delta that is equivalent to first applying the operations of
self, then applying the operations ofotheron top of that. -
#concat(other) ⇒ Delta
Modifies
selfby the concatenating this and another document Delta's operations. -
#delete(value) ⇒ Delta
Appends a delete operation.
-
#diff(other) ⇒ Delta
(also: #-)
Returns a Delta representing the difference between two documents.
-
#each_char {|char, attributes| ... } ⇒ Enumerator, Delta
Yields char + attribute pairs of at most length = 1 to the block, or returns an enumerator which will do the same.
-
#each_line {|delta| ... } ⇒ Enumerator, Delta
Yields Delta objects corresponding to each
\n-separated line in the document, each including a trailing newline (except for the last if no trailing newline is present overall). -
#each_op {|op| ... } ⇒ Enumerator, Delta
Yields each operation in the delta, as-is.
-
#each_slice(size = 1) {|op| ... } ⇒ Enumerator, Delta
Yields ops of at most
sizelength to the block, or returns an enumerator which will do the same. -
#include?(other) ⇒ Boolean
Returns true if
otheris a substring ofself. -
#initialize(data = []) ⇒ Delta
constructor
Parses a new Delta object from incoming data.
-
#insert(value, attributes = {}) ⇒ Delta
Appends an insert operation.
-
#insert_only? ⇒ Boolean
(also: #document?)
Returns true if all operations are inserts, i.e.
-
#inspect ⇒ String
Returns a String useful for debugging that includes details of each contained operation.
-
#length ⇒ Integer
The sum of the lengths of each operation.
-
#push(op) ⇒ Delta
(also: #<<)
Adds a new operation to the end of the delta, possibly merging it with the previously-last op if the types and attributes match, and ensuring that inserts always come before deletes.
-
#retain(value, attributes = {}) ⇒ Delta
Appends a retain operation.
-
#slice(start = 0, len = length) ⇒ Delta
(also: #[])
Returns a copy containing a subset of operations, measured in number of characters.
-
#to_h ⇒ Hash
The Hash representation of this object, by converting each contained op into a Hash.
-
#to_html(options = {}) ⇒ Object
Returns an HTML representation of this delta.
-
#to_json(*args) ⇒ String
The JSON representation of this object, by delegating to #to_h.
-
#to_plaintext(embed_str: '!') ⇒ String
Returns a plain text representation of this delta (lossy).
-
#trailing_newline? ⇒ Boolean
Returns true if the last operation is a string insert that ends with a
\ncharacter. -
#transform(other, priority) ⇒ Object
(also: #^)
Transform other Delta against own operations, such that transformation property 1 (TP1) holds:.
-
#transform_position(index, priority) ⇒ Integer
Transform an index against the current delta.
Constructor Details
#initialize(data = []) ⇒ Delta
Parses a new Delta object from incoming data.
23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/rich-text/delta.rb', line 23 def initialize(data = []) if data.is_a?(String) @ops = [Op.new(:insert, data)] elsif data.is_a?(Array) @ops = data.map { |h| Op.parse(h) } elsif data.is_a?(Hash) && (data.key?('ops') || data.key?(:ops)) @ops = (data['ops'] || data[:ops]).map { |h| Op.parse(h) } else ArgumentError.new("Please provide either String, Array or Hash with an 'ops' key containing an Array") end @ops end |
Instance Attribute Details
Instance Method Details
#+(other) ⇒ Object
The non-destructive version of #concat
317 318 319 |
# File 'lib/rich-text/delta.rb', line 317 def +(other) dup.concat(other) end |
#==(other) ⇒ Boolean Also known as: eql?
A Delta is equal to another if all the ops are equal.
485 486 487 |
# File 'lib/rich-text/delta.rb', line 485 def ==(other) other.is_a?(RichText::Delta) && @ops == other.ops end |
#chop! ⇒ Delta
Modifies self by removing the last op if it was a retain without attributes.
117 118 119 120 121 122 123 |
# File 'lib/rich-text/delta.rb', line 117 def chop! last_op = @ops.last if last_op && last_op.retain? && !last_op.attributes? @ops.pop end return self end |
#compose(other) ⇒ Delta Also known as: |
Returns a Delta that is equivalent to first applying the operations of self, then applying the operations of other on top of that.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/rich-text/delta.rb', line 268 def compose(other) iter_a = Iterator.new(@ops) iter_b = Iterator.new(other.ops) delta = Delta.new while iter_a.next? || iter_b.next? if iter_b.peek.insert? delta.push(iter_b.next) elsif iter_a.peek.delete? delta.push(iter_a.next) else len = [iter_a.peek.length, iter_b.peek.length].min op_a = iter_a.next(len) op_b = iter_b.next(len) if op_b.retain? if op_a.retain? attrs = Attributes.compose(op_a.attributes, op_b.attributes, true) delta.push(Op.new(:retain, len, attrs)) else attrs = Attributes.compose(op_a.attributes, op_b.attributes, false) delta.push(Op.new(:insert, op_a.value, attrs)) end elsif op_b.delete? && op_a.retain? delta.push(op_b) end end end delta.chop! end |
#concat(other) ⇒ Delta
Modifies self by the concatenating this and another document Delta's operations.
Correctly handles the case of merging the last operation of self with the first operation of other, if possible.
The behavior is not defined when either self or other has non-insert operations.
307 308 309 310 311 312 313 |
# File 'lib/rich-text/delta.rb', line 307 def concat(other) if other.length > 0 push(other.ops.first) @ops.concat(other.ops.slice(1..-1)) end self end |
#delete(value) ⇒ Delta
Appends a delete operation. A no-op if value <= 0.
54 55 56 57 |
# File 'lib/rich-text/delta.rb', line 54 def delete(value) return self if value <= 0 push(Op.new(:delete, value)) end |
#diff(other) ⇒ Delta Also known as: -
Returns a Delta representing the difference between two documents.
The behavior is not defined when either self or other has non-insert operations.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/rich-text/delta.rb', line 330 def diff(other) delta = Delta.new return delta if self == other iter = Iterator.new(@ops) other_iter = Iterator.new(other.ops) Diff.new(self, other) do |kind, len| while len > 0 case kind when :insert op_len = [len, other_iter.peek.length].min delta.push(other_iter.next(op_len)) when :delete op_len = [len, iter.peek.length].min iter.next(op_len) delta.delete(op_len) when :retain op_len = [iter.peek.length, other_iter.peek.length, len].min this_op = iter.next(op_len) other_op = other_iter.next(op_len) if this_op.value == other_op.value delta.retain(op_len, Attributes.diff(this_op.attributes, other_op.attributes)) else delta.push(other_op).delete(op_len) end end len -= op_len end end delta.chop! end |
#each_char {|char, attributes| ... } ⇒ Enumerator, Delta
Yields char + attribute pairs of at most length = 1 to the block, or returns an enumerator which will do the same. Non-string inserts will result in that value being yielded instead of a string. The behavior is not defined with non-insert operations.
168 169 170 171 172 |
# File 'lib/rich-text/delta.rb', line 168 def each_char return enum_for(:each_char) unless block_given? each_slice(1) { |op| yield op.value, op.attributes } self end |
#each_line {|delta| ... } ⇒ Enumerator, Delta
Yields RichText::Delta objects corresponding to each \n-separated line in the document, each including a trailing newline (except for the last if no trailing newline is present overall).
The behavior is not defined with non-insert operations.
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/rich-text/delta.rb', line 181 def each_line return enum_for(:each_line) unless block_given? iter = Iterator.new(@ops) line = Delta.new while iter.next? op = iter.next if !op.insert?(String) line.push(op) next end offset = 0 while idx = op.value.index("\n", offset) line.push op.slice(offset, idx - offset + 1) yield line line = Delta.new offset = idx + 1 end if offset < op.value.length line.push op.slice(offset) end end yield line if line.length > 0 end |
#each_op {|op| ... } ⇒ Enumerator, Delta
Yields each operation in the delta, as-is.
213 214 215 216 217 |
# File 'lib/rich-text/delta.rb', line 213 def each_op return enum_for(:each_op) unless block_given? @ops.each { |op| yield op } self end |
#each_slice(size = 1) {|op| ... } ⇒ Enumerator, Delta
Yields ops of at most size length to the block, or returns an enumerator which will do the same
154 155 156 157 158 |
# File 'lib/rich-text/delta.rb', line 154 def each_slice(size = 1) return enum_for(:each_slice, size) unless block_given? Iterator.new(@ops).each(size) { |op| yield op } self end |
#include?(other) ⇒ Boolean
Not implemented yet
Returns true if other is a substring of self
143 144 145 |
# File 'lib/rich-text/delta.rb', line 143 def include?(other) raise NotImplementedError.new("TODO") end |
#insert(value, attributes = {}) ⇒ Delta
Appends an insert operation. A no-op if the provided value is the empty string.
44 45 46 47 |
# File 'lib/rich-text/delta.rb', line 44 def insert(value, attributes = {}) return self if value.is_a?(String) && value.length == 0 push(Op.new(:insert, value, attributes)) end |
#insert_only? ⇒ Boolean Also known as: document?
Returns true if all operations are inserts, i.e. a fully-composed document
127 128 129 |
# File 'lib/rich-text/delta.rb', line 127 def insert_only? @ops.all?(&:insert?) end |
#inspect ⇒ String
Returns a String useful for debugging that includes details of each contained operation.
476 477 478 479 480 |
# File 'lib/rich-text/delta.rb', line 476 def inspect str = "#<#{self.class.name} [" str << @ops.map { |o| o.inspect(false) }.join(", ") str << "]>" end |
#length ⇒ Integer
Returns the sum of the lengths of each operation.
223 224 225 |
# File 'lib/rich-text/delta.rb', line 223 def length @ops.reduce(0) { |sum, op| sum + op.length } end |
#push(op) ⇒ Delta Also known as: <<
Adds a new operation to the end of the delta, possibly merging it with the previously-last op if the types and attributes match, and ensuring that inserts always come before deletes.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/rich-text/delta.rb', line 73 def push(op) index = @ops.length last_op = @ops[index - 1] if last_op if last_op.delete? && op.delete? @ops[index - 1] = Op.new(:delete, last_op.value + op.value) return self end # Since it does not matter if we insert before or after deleting at the # same index, always prefer to insert first if last_op.delete? && op.insert? index -= 1 last_op = @ops[index - 1] if !last_op @ops.unshift(op) return self end end if last_op.attributes == op.attributes if last_op.insert?(String) && op.insert?(String) @ops[index - 1] = Op.new(:insert, last_op.value + op.value, last_op.attributes) return self elsif last_op.retain? && op.retain? @ops[index - 1] = Op.new(:retain, last_op.value + op.value, last_op.attributes) return self end end end if index == @ops.length @ops.push(op) else @ops[index, 0] = op end return self end |
#retain(value, attributes = {}) ⇒ Delta
Appends a retain operation. A no-op if value <= 0.
65 66 67 68 |
# File 'lib/rich-text/delta.rb', line 65 def retain(value, attributes = {}) return self if value <= 0 push(Op.new(:retain, value, attributes)) end |
#slice(start = 0, len = length) ⇒ Delta Also known as: []
Returns a copy containing a subset of operations, measured in number of characters. An operation may be subdivided if needed to return just the requested length. Non-string inserts cannot be subdivided (naturally, as they have length 1).
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/rich-text/delta.rb', line 237 def slice(start = 0, len = length) if start.is_a?(Range) len = start.size start = start.first end delta = Delta.new start = [0, length + start].max if start < 0 finish = start + len iter = Iterator.new(@ops) idx = 0 while idx < finish && iter.next? if idx < start op = iter.next(start - idx) else op = iter.next(finish - idx) delta.push(op) end idx += op.length end return delta end |
#to_h ⇒ Hash
Returns the Hash representation of this object, by converting each contained op into a Hash.
442 443 444 |
# File 'lib/rich-text/delta.rb', line 442 def to_h { :ops => @ops.map(&:to_h) } end |
#to_html(options = {}) ⇒ Object
Support options that control how rich-text attributes are converted into HTML tags and attributes.
Returns an HTML representation of this delta.
468 469 470 |
# File 'lib/rich-text/delta.rb', line 468 def to_html( = {}) HTML.render(self, ) end |
#to_json(*args) ⇒ String
Returns the JSON representation of this object, by delegating to #to_h.
447 448 449 |
# File 'lib/rich-text/delta.rb', line 447 def to_json(*args) to_h.to_json(*args) end |
#to_plaintext(embed_str: '!') ⇒ String
Returns a plain text representation of this delta (lossy). The behavior is not defined with non-insert operations.
455 456 457 458 459 460 461 462 463 |
# File 'lib/rich-text/delta.rb', line 455 def to_plaintext(embed_str: '!') @ops.each_with_object('') do |op, str| if op.insert?(String) str << op.value elsif str << end end end |
#trailing_newline? ⇒ Boolean
Returns true if the last operation is a string insert that ends with a \n character.
134 135 136 137 |
# File 'lib/rich-text/delta.rb', line 134 def trailing_newline? return false unless @ops.last && @ops.last.insert?(String) @ops.last.value.end_with?("\n") end |
#transform(other, priority) ⇒ Object Also known as: ^
Transform other Delta against own operations, such that transformation property 1 (TP1) holds:
self.compose(self.transform(other, true)) == other.compose(other.transform(self, false))
If called with a number, then acts as an alias for #transform_position
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/rich-text/delta.rb', line 385 def transform(other, priority) return transform_position(other, priority) if other.is_a?(Integer) iter = Iterator.new(@ops) other_iter = Iterator.new(other.ops) delta = Delta.new while iter.next? || other_iter.next? if iter.peek.insert? && (priority || !other_iter.peek.insert?) delta.retain iter.next.length elsif other_iter.peek.insert? delta.push other_iter.next else len = [iter.peek.length, other_iter.peek.length].min op = iter.next(len) other_op = other_iter.next(len) if op.delete? # Our delete makes their delete redundant, or removes their retain next elsif other_op.delete? delta.push(other_op) else # We either retain their retain or insert delta.retain(len, Attributes.transform(op.attributes, other_op.attributes, priority)) end end end delta.chop! end |
#transform_position(index, priority) ⇒ Integer
Transform an index against the current delta. Useful for shifting cursor & selection positions in response to remote changes.
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/rich-text/delta.rb', line 425 def transform_position(index, priority) iter = Iterator.new(@ops) offset = 0 while iter.next? && offset <= index op = iter.next if op.delete? index -= [op.length, index - offset].min next elsif op.insert? && (offset < index || !priority) index += op.length end offset += op.length end return index end |