Class: Goat::DOMTools::DOMDiff
Class Method Summary collapse
-
.diff(old, new) ⇒ Object
the constraints: - arrays might be tags or they might be arrays - we can’t directly address textual entities; they must be addressed by their enclosing tag (i.e. <li>foo</li> -> <li>bar</li> is a change to an li tag, not a change to the contents of an li tag. Why, you ask? Here: > $(“#foo”).innerHTML “<span>blub</span>foo<span>qux</span>bar” > $(“#foo”).children [<span>blub</span>, <span>qux</span>] Text isn’t a thing in the DOM tree. So, in order to traverse, we have to restrict ourselves to DOM nodes, i.e. tags, not raw textual elements in the tree. I don’t know why the DOM works this way. - we only try to deal with changes where the parent element of the changed element has a [unique] ID. We could address by route through the tree, but we don’t attempt to store this. - we can’t always tell in advance where we should make a change. For example, [:ul, [:li, [:a, => ‘hello’, [:b, ‘test’]]]] changing to: [:ul, [:li, [:a, => ‘hello’, [:b, ‘blub’]]]] When we’re looking at the ul, and before we know what exactly is different below us, it may be the case that the change can be encapsulated more neatly somewhere in the subtree, but it may not – and even if the change is nice and isolated further down, there might not be a parent ID for us to hook off. This is what NoParentDOMID exceptions are used for. - we want to ignore artefacts from randomization of IDs. (i.e. cases where nothing has changed except the dom_XXXXXXXX ID of some element. This is pretty easy intuitively, but a bit more subtle in practice. The obvious thing to do is to use an alternate comparison function, instead of ==, but this may be slow, and would (worse) need a lot of care to make sure that your code (and outside libraries, such as Diff::LCS, which we use) never call it. Next obvious thing is to create a subclass of Hash, make all hash attributes (=> ‘dom_XXXXXXX’, :class => ‘…’) instances of it, and then redefine == on the subclass. This doesn’t work, though, because ruby’s crappy C implementation won’t dispatch to the subclass’s == – for hashes, arrays, etc., the comparison code is wired in and can’t be overridden. So instead, we have our subclass actually remove the unwanted elements, and store them in a separate @deleted ivar. The crappy part of this is that you need to manually go through the dom and stick them all back in the “actual” hash once you’ve finished diffing. - we *don’t* want to remove IDs that don’t change. For example, during a rerender, we preserve the ID of the rerendered-component (just not the IDs of any child components). If we deleted the IDs of these, constraint #3 would mean we’d miss a lot of potential diffs, since we’d think there were changes we couldn’t make. (I.e. think a child’s parent doesn’t have an ID when in fact it does.) One todo here is to make the differ check the @deleted ivar of a DOMAttrs instance for IDs, since they’re perfectly valid hooks to use when removing or adding components. If you do this, conflicts become much more likely though – say you have multiple changes to apply; a node whose ID you depend on may be removed..
- .dom_diff(old, new, id) ⇒ Object
- .preproc(tree, id) ⇒ Object
- .unproc(tree) ⇒ Object
Instance Method Summary collapse
- #cmp(old, new, par) ⇒ Object
- #cmp_array(old, new, par) ⇒ Object
- #cmp_node(old, new, par) ⇒ Object
- #diff ⇒ Object
-
#initialize(old, new) ⇒ DOMDiff
constructor
A new instance of DOMDiff.
- #transpose(old, new, par) ⇒ Object
Methods included from Goat::DOMTools
attrs, body, car_tag, dom_components, dom_node?, domid, expanded_dom, find_component, inject_prefixes, is_attrs?, nested_body?, normalized_tags, tag, transpose, traverse
Methods included from DiffTools
#added, #dom_node?, #lcs_is_addition?, #lcs_is_removal?, #lcs_is_replacement?, #lcs_old_and_new, #removed, #type_of
Constructor Details
#initialize(old, new) ⇒ DOMDiff
Returns a new instance of DOMDiff.
522 523 524 525 |
# File 'lib/goat/dom.rb', line 522 def initialize(old, new) @old = old @new = new end |
Class Method Details
.diff(old, new) ⇒ Object
the constraints:
-
arrays might be tags or they might be arrays
-
we can’t directly address textual entities; they must be addressed by their enclosing tag (i.e. <li>foo</li> -> <li>bar</li> is a change to an li tag, not a change to the contents of an li tag. Why, you ask? Here: > $(“#foo”).innerHTML “<span>blub</span>foo<span>qux</span>bar” > $(“#foo”).children
- <span>blub</span>, <span>qux</span>
-
Text isn’t a thing in the DOM tree. So, in order to traverse, we have to restrict ourselves to DOM nodes, i.e. tags, not raw textual elements in the tree. I don’t know why the DOM works this way.
-
we only try to deal with changes where the parent element of the changed element has a
- unique
-
ID. We could address by route through the tree, but we don’t attempt to store
this.
-
we can’t always tell in advance where we should make a change. For example,
- :ul, [:li, [:a, => ‘hello’, [:b, ‘test’]]]
-
changing to:
- :ul, [:li, [:a, => ‘hello’, [:b, ‘blub’]]]
-
When we’re looking at the ul, and before we know what exactly is different below us, it may be the case that the change can be encapsulated more neatly somewhere in the subtree, but it may not – and even if the change is nice and isolated further down, there might not be a parent ID for us to hook off. This is what NoParentDOMID exceptions are used for.
-
we want to ignore artefacts from randomization of IDs. (i.e. cases where nothing has changed except the dom_XXXXXXXX ID of some element. This is pretty easy intuitively, but a bit more subtle in practice. The obvious thing to do is to use an alternate comparison function, instead of ==, but this may be slow, and would (worse) need a lot of care to make sure that your code (and outside libraries, such as Diff::LCS, which we use) never call it. Next obvious thing is to create a subclass of Hash, make all hash attributes (=> ‘dom_XXXXXXX’, :class => ‘…’) instances of it, and then redefine == on the subclass. This doesn’t work, though, because ruby’s crappy C implementation won’t dispatch to the subclass’s == – for hashes, arrays, etc., the comparison code is wired in and can’t be overridden. So instead, we have our subclass actually remove the unwanted elements, and store them in a separate @deleted ivar. The crappy part of this is that you need to manually go through the dom and stick them all back in the “actual” hash once you’ve finished diffing.
-
we *don’t* want to remove IDs that don’t change. For example, during a rerender, we preserve the ID of the rerendered-component (just not the IDs of any child components). If we deleted the IDs of these, constraint #3 would mean we’d miss a lot of potential diffs, since we’d think there were changes we couldn’t make. (I.e. think a child’s parent doesn’t have an ID when in fact it does.) One todo here is to make the differ check the @deleted ivar of a DOMAttrs instance for IDs, since they’re perfectly valid hooks to use when removing or adding components. If you do this, conflicts become much more likely though – say you have multiple changes to apply; a node whose ID you depend on may be removed.
492 493 494 |
# File 'lib/goat/dom.rb', line 492 def self.diff(old, new) d = self.new(old, new).diff end |
.dom_diff(old, new, id) ⇒ Object
510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/goat/dom.rb', line 510 def self.dom_diff(old, new, id) trees = [] [old, new].each {|tree| trees << preproc(tree, id)} diff(*trees).map do |diff| if diff.first == :add diff[0..-2] + [unproc(diff[3])] else diff end end end |
Instance Method Details
#cmp(old, new, par) ⇒ Object
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 |
# File 'lib/goat/dom.rb', line 599 def cmp(old, new, par) return [] if old == new told, tnew = type_of(old), type_of(new) if told != tnew if told == :string || tnew == :string raise TextualTranspose else return transpose(old, new, par) end end if told == :string || tnew == :string && old != new raise TextualTranspose end type = told if type == :node cmp_node(old, new, par) elsif type == :array cmp_array(old, new, par) else raise "Unknown thing: #{old.inspect}" end end |
#cmp_array(old, new, par) ⇒ Object
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
# File 'lib/goat/dom.rb', line 551 def cmp_array(old, new, par) if old.size == 1 && new.size == 1 return cmp(old.first, new.first, par) end patch = Diff::LCS.diff(old, new).flatten(1) left, right = patch if patch.size == 2 && lcs_is_replacement?(left, right) # if we're just replacing an element, maybe we can do it more elegantly. e.g., # [:p, [:li, 'foo'], [:li, 'bar']] -> [:p, [:li, 'foo'], [:li, 'bar'], [:li, 'baz']] # can be represented as a replacement of the p tag itself, or the insertion of # [:li, 'baz']. If the patch looks like a simple replacement, we delve deeper. dold, dnew = lcs_old_and_new(left, right) old, new = dold.element, dnew.element begin return cmp(old, new, domid(old)) rescue NoParentDOMID, TextualTranspose # fine fine if you hate fun we can just continue on our merry way below end end a, b, off = 0, 0, 0 chgs = patch.map do |ch| if lcs_is_addition?(ch) while b < ch.position a += 1; b += 1; off += 1 end b += 1 mut = [added(ch.element, par, off)] off += 1 mut elsif lcs_is_removal?(ch) while a < ch.position a += 1; b += 1; off += 1 end a += 1 [removed(ch.element, par, off)] else raise "Bad diff: #{ch.inspect}" end end chgs.flatten(1) end |
#cmp_node(old, new, par) ⇒ Object
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/goat/dom.rb', line 535 def cmp_node(old, new, par) oattrs = attrs(old) if tag(old) == tag(new) && \ oattrs == attrs(new) begin cmp(body(old), body(new), oattrs ? domid(old) : nil) rescue NoParentDOMID => e transpose(old, new, par) rescue TextualTranspose => e transpose(old, new, par) end else transpose(old, new, par) end end |
#diff ⇒ Object
527 528 529 |
# File 'lib/goat/dom.rb', line 527 def diff cmp(@old, @new, nil) end |
#transpose(old, new, par) ⇒ Object
531 532 533 |
# File 'lib/goat/dom.rb', line 531 def transpose(old, new, par) [removed(old, par, 0), added(new, par, 0)] end |