2 /* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 * or see http://www.gnu.org/
22 * Any element in the DOM tree of an HTML document.
29 function __construct($parent){
30 $this->parent
= $parent;
31 if(!is_null($parent)){
32 $parent->addChild($this);
36 public function getParent(){
40 public function getParentTree(){
41 if(!is_null($this->parent
)){
42 $parentTree = $this->parent
->getParentTree();
43 $parentTree[] = $this->parent
;
50 public abstract function getMinimalDeletedSet($id);
52 public function detectIgnorableWhiteSpace(){
56 public function getLastCommonParent(Node
$other){
58 throw new Exception('The given node is NULL');
60 $result = new LastCommonParentResult();
62 $myParents = $this->getParentTree();
63 $otherParents = $other->getParentTree();
67 while ($isSame && $i < sizeof($myParents) && $i < sizeof($otherParents)) {
68 if (!$myParents[$i]->isSameTag($otherParents[$i])) {
71 // After the while, the index i-1 must be the last common parent
76 $result->setLastCommonParentDepth($i - 1);
77 $result->setLastCommonParent($myParents[$i - 1]);
80 $result->setIndexInLastCommonParent($myParents[$i - 1]->getIndexOf($myParents[$i]));
81 $result->setSplittingNeeded();
82 } else if (sizeof($myParents) < sizeof($otherParents)) {
83 $result->setIndexInLastCommonParent($myParents[$i - 1]->getIndexOf($this));
84 } else if (sizeof($myParents) > sizeof($otherParents)) {
85 // All tags matched but there are tags left in this tree
86 $result->setIndexInLastCommonParent($myParents[$i - 1]->getIndexOf($myParents[$i]));
87 $result->setSplittingNeeded();
89 // All tags matched untill the very last one in both trees
90 // or there were no tags besides the BODY
91 $result->setIndexInLastCommonParent($myParents[$i - 1]->getIndexOf($this));
96 public function setParent($parent) {
97 $this->parent
= $parent;
100 public abstract function copyTree();
102 public function inPre() {
103 $tree = $this->getParentTree();
104 foreach ($tree as $ancestor) {
105 if ($ancestor->isPre()) {
112 private $whiteBefore = false;
114 private $whiteAfter = false;
116 public function isWhiteBefore() {
117 return $this->whiteBefore
;
120 public function setWhiteBefore($whiteBefore) {
121 $this->whiteBefore
= $whiteBefore;
124 public function isWhiteAfter() {
125 return $this->whiteAfter
;
128 public function setWhiteAfter($whiteAfter) {
129 $this->whiteAfter
= $whiteAfter;
132 public abstract function getLeftMostChild();
134 public abstract function getRightMostChild();
138 * Node that can contain other nodes. Represents an HTML tag.
140 class TagNode
extends Node
{
142 public $children = array();
146 protected $attributes = array();
148 function __construct($parent, $qName, /*array*/ $attributes) {
149 parent
::__construct($parent);
150 $this->qName
= strtolower($qName);
151 foreach($attributes as $key => $value){
152 $this->attributes
[strtolower($key)] = $value;
156 public function addChild(Node
$node, $index=-1) {
157 if ($node->getParent() !== $this)
159 'The new child must have this node as a parent.');
161 $this->children
[] = $node;
163 array_splice($this->children
,$index,0,array($node));
167 public function getIndexOf(Node
$child) {
168 // don't trust array_search with objects
169 foreach($this->children
as $key=>$value){
170 if($value === $child){
177 public function getChild($i) {
178 return $this->children
[$i];
181 public function getNbChildren() {
182 return count($this->children
);
185 public function getQName() {
189 public function getAttributes() {
190 return $this->attributes
;
193 public function isSameTag(TagNode
$other) {
196 return $this->getOpeningTag() === $other->getOpeningTag();
199 public function getOpeningTag() {
200 $s = '<'.$this->getQName();
201 foreach ($this->attributes
as $attribute => $value) {
202 $s .= ' ' . $attribute . '="' . $value . '"';
207 public function getEndTag() {
208 return '</' . $this->getQName() +
'>"';
211 public function getMinimalDeletedSet($id) {
214 if ($this->getNbChildren() == 0)
217 $hasNotDeletedDescendant = false;
219 foreach ($this->children
as $child) {
220 $childrenChildren = $child->getMinimalDeletedSet($id);
221 $nodes = array_merge($nodes, $childrenChildren);
222 if (!$hasNotDeletedDescendant
223 && !(count($childrenChildren) == 1 && $childrenChildren[0]===$child)) {
224 // This child is not entirely deleted
225 $hasNotDeletedDescendant = true;
228 if (!$hasNotDeletedDescendant) {
229 $nodes = array($this);
234 public function toString() {
235 return $this->getOpeningTag();
238 public function splitUntill(TagNode
$parent, Node
$split, $includeLeft) {
239 $splitOccured = false;
240 if ($parent !== $this) {
241 $part1 = new TagNode(NULL, $this->getQName(), $this->getAttributes());
242 $part2 = new TagNode(NULL, $this->getQName(), $this->getAttributes());
243 $part1->setParent($this->getParent());
244 $part2->setParent($this->getParent());
247 $nbChildren = $this->getNbChildren();
248 while ($i < $nbChildren && $this->children
[$i] !== $split) {
249 $this->children
[$i]->setParent($part1);
250 $part1->addChild($this->children
[$i]);
253 if ($i < $nbChildren) {
255 $this->children
[$i]->setParent($part1);
256 $part1->addChild($this->children
[$i]);
258 $this->children
[$i]->setParent($part2);
259 $part2->addChild($this->children
[$i]);
263 while ($i < $nbChildren) {
264 $this->children
[$i]->setParent($part2);
265 $part2->addChild($this->children
[$i]);
268 $myindexinparent = $this->parent
->getIndexOf($this);
269 if ($part1->getNbChildren() > 0)
270 $this->parent
->addChild($part1,$myindexinparent);
272 if ($part2->getNbChildren() > 0)
273 $this->parent
->addChild($part2,$myindexinparent);
275 if ($part1->getNbChildren() > 0 && $part2->getNbChildren() > 0) {
276 $splitOccured = true;
279 $this->parent
->removeChild($myindexinparent);
282 $this->parent
->splitUntill($parent, $part1, $includeLeft);
284 $this->parent
->splitUntill($parent, $part2, $includeLeft);
286 return $splitOccured;
290 private function removeChild($index) {
291 unset($this->children
[$index]);
292 $this->children
= array_values($this->children
);
295 public static $blocks = array('html'=>TRUE,'body'=>TRUE,'p'=>TRUE,'blockquote'=>TRUE,
296 'h1'=>TRUE,'h2'=>TRUE,'h3'=>TRUE,'h4'=>TRUE,'h5'=>TRUE,'pre'=>TRUE,'div'=>TRUE,'ul'=>TRUE,'ol'=>TRUE,'li'=>TRUE,
297 'table'=>TRUE,'tbody'=>TRUE,'tr'=>TRUE,'td'=>TRUE,'th'=>TRUE,'br'=>TRUE);
299 public static function isBlockLevelName($qName) {
300 return array_key_exists(strtolower($qName),self
::$blocks);
303 public static function isBlockLevelNode(Node
$node) {
304 if(! $node instanceof TagNode
)
306 return self
::isBlockLevelName($node->getQName());
309 public function isBlockLevel() {
310 return  self
::isBlockLevelNode($this);
313 public static function isInlineName($qName) {
314 return !self
::isBlockLevelName($qName);
317 public static function isInlineNode(Node
$node) {
318 return !self
::isBlockLevelNode($node);
321 public function isInline() {
322 return self
::isInlineNode($this);
325 public function copyTree() {
326 $newThis = new TagNode(NULL, $this->getQName(), $this->getAttributes());
327 $newThis->setWhiteBefore($this->isWhiteBefore());
328 $newThis->setWhiteAfter($this->isWhiteAfter());
329 foreach($this->children
as $child) {
330 $newChild = $child->copyTree();
331 $newChild->setParent($newThis);
332 $newThis->addChild($newChild);
337 public function getMatchRatio(TagNode
$other) {
338 $txtComp = new TextOnlyComparator($other);
339 return $txtComp->getMatchRatio(new TextOnlyComparator($this));
342 public function expandWhiteSpace() {
346 $nbOriginalChildren = $this->getNbChildren();
347 for ($i = 0; $i < $nbOriginalChildren; ++
$i) {
348 $child = $this->getChild($i +
$shift);
350 if($child instanceof TagNode
){
351 if (!$child->isPre()) {
352 $child->expandWhiteSpace();
355 if (!$spaceAdded && $child->isWhiteBefore()) {
356 $ws = new WhiteSpaceNode(NULL, ' ', $child->getLeftMostChild());
357 $ws->setParent($this);
358 $this->addChild($ws,$i +
($shift++
));
360 if ($child->isWhiteAfter()) {
361 $ws = new WhiteSpaceNode(NULL, ' ', $child->getRightMostChild());
362 $ws->setParent($this);
363 $this->addChild($ws,$i +
1 +
($shift++
));
372 public function getLeftMostChild() {
373 if ($this->getNbChildren() < 1)
375 return $this->getChild(0)->getLeftMostChild();
379 public function getRightMostChild() {
380 if ($this->getNbChildren() < 1)
382 return $this->getChild($this->getNbChildren() - 1)->getRightMostChild();
385 public function isPre() {
386 return 0 == strcasecmp($this->getQName(),'pre');
389 public static function toDiffLine(TagNode
$node){
390 return $node->getOpeningTag();
395 * Represents a piece of text in the HTML file.
397 class TextNode
extends Node
{
401 private $modification;
403 function __construct($parent, $s) {
404 parent
::__construct($parent);
405 $this->modification
= new Modification(Modification
::NONE
);
409 public function copyTree() {
410 $clone = clone $this;
411 $clone->setParent(NULL);
415 public function getLeftMostChild() {
419 public function getRightMostChild() {
423 public function getMinimalDeletedSet($id) {
424 if ($this->getModification()->getType() == Modification
::REMOVED
425 && $this->getModification()->getID() == $id){
431 public function getModification() {
432 return $this->modification
;
435 public function getText() {
439 public function isSameText($other) {
440 if (is_null($other) ||
! $other instanceof TextNode
){
443 return str_replace('\n', ' ',$this->getText()) === str_replace('\n', ' ',$other->getText());
446 public function setModification(Modification
$m) {
447 //wfDebug("Registered modification for node '$this->s' as ".Modification::typeToString($m->getType()));
448 $this->modification
= $m;
451 public function toString() {
452 return $this->getText();
455 public static function toDiffLine(TextNode
$node){
456 return str_replace('\n', ' ',$node->getText());
460 class WhiteSpaceNode
extends TextNode
{
462 function __construct($parent, $s, Node
$like = NULL) {
463 parent
::__construct($parent, $s);
464 if(!is_null($like) && $like instanceof TextNode
){
465 $newModification = clone $like->getModification();
466 $newModification->setFirstOfID(false);
467 $this->setModification($newModification);
471 public static function isWhiteSpace($c) {
485 * Represents the root of a HTML document.
487 class BodyNode
extends TagNode
{
489 function __construct() {
490 parent
::__construct(NULL, 'body', array());
493 public function copyTree() {
494 $newThis = new BodyNode();
495 foreach ($this->children
as $child) {
496 $newChild = $child->copyTree();
497 $newChild->setParent($newThis);
498 $newThis->addChild($newChild);
503 public function getMinimalDeletedSet($id) {
505 foreach ($this->children
as $child) {
506 $childrenChildren = $child->getMinimalDeletedSet($id);
507 $nodes = array_merge($nodes, $childrenChildren);
515 * Represents an image in HTML. Even though images do not contain any text they
516 * are independent visible objects on the page. They are logically a TextNode.
518 class ImageNode
extends TextNode
{
522 function __construct(TagNode
$parent, /*array*/ $attrs) {
523 if(!array_key_exists('src',$attrs)){
524 //wfDebug('Image without a source:');
525 foreach($attrs as $key => $value){
526 //wfDebug("$key = $value");
528 parent
::__construct($parent, '<img></img>');
530 parent
::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
532 $this->attributes
= $attrs;
535 public function isSameText($other) {
536 if (is_null($other) ||
! $other instanceof ImageNode
)
538 return $this->getText() === $other->getText();
541 public function getAttributes() {
542 return $this->attributes
;
548 * When detecting the last common parent of two nodes, all results are stored as
549 * a LastCommonParentResult.
551 class LastCommonParentResult
{
556 public function getLastCommonParent() {
557 return $this->parent
;
560 public function setLastCommonParent(TagNode
$parent) {
561 $this->parent
= $parent;
565 private $splittingNeeded = false;
567 public function isSplittingNeeded() {
568 return $this->splittingNeeded
;
571 public function setSplittingNeeded() {
572 $this->splittingNeeded
= true;
576 private $lastCommonParentDepth = -1;
578 public function getLastCommonParentDepth() {
579 return $this->lastCommonParentDepth
;
582 public function setLastCommonParentDepth($depth) {
583 $this->lastCommonParentDepth
= $depth;
587 private $indexInLastCommonParent = -1;
589 public function getIndexInLastCommonParent() {
590 return $this->indexInLastCommonParent
;
593 public function setIndexInLastCommonParent($index) {
594 $this->indexInLastCommonParent
= $index;
613 private $firstOfID = false;
615 function __construct($type) {
619 public function copy() {
620 $newM = new Modification($this->getType());
621 $newM->setID($this->getID());
622 $newM->setChanges($this->getChanges());
623 $newM->setFirstOfID($this->isFirstOfID());
624 $newM->setNext($this->getNext());
625 $newM->setPrevious($this->getPrevious());
629 public function getType() {
633 public function setID($id) {
637 public function getID() {
641 public function setPrevious($m) {
645 public function getPrevious() {
646 return $this->prevMod
;
649 public function setNext($m) {
653 public function getNext() {
654 return $this->nextMod
;
659 public function setChanges($changes) {
660 $this->changes
= $changes;
663 public function getChanges() {
664 return $this->changes
;
667 public function isFirstOfID() {
668 return $this->firstOfID
;
671 public function setFirstOfID($firstOfID) {
672 $this->firstOfID
= $firstOfID;
675 public static function typeToString($type){
677 case self
::NONE
: return 'none';
678 case self
::REMOVED
: return 'removed';
679 case self
::ADDED
: return 'added';
680 case self
::CHANGED
: return 'changed';
685 class DomTreeBuilder
{
687 private $textNodes = array();
691 private $currentParent;
693 private $newWord = "";
695 protected $bodyStarted = false;
697 protected $bodyEnded = false;
699 private $whiteSpaceBeforeThis = false;
701 private $lastSibling;
703 function __construct(){
704 $this->bodyNode
= $this->currentParent
= new BodyNode();
707 public function getBodyNode() {
708 return $this->bodyNode
;
711 public function getTextNodes() {
712 return $this->textNodes
;
716 * Must be called manually
718 public function endDocument() {
720 //wfDebug(sizeof($this->textNodes) . ' text nodes in document.');
723 public function startElement($parser, $name, /*array*/ $attributes) {
724 if(!strcasecmp($name, 'body')==0){
725 //wfDebug("Starting $name node.");
728 $newTagNode = new TagNode($this->currentParent
, $name, $attributes);
729 $this->currentParent
= $newTagNode;
730 $this->lastSibling
= NULL;
731 if ($this->whiteSpaceBeforeThis
&& $newTagNode->isInline()) {
732 $newTagNode->setWhiteBefore(true);
734 $this->whiteSpaceBeforeThis
= false;
738 public function endElement($parser, $name) {
739 if(!strcasecmp($name, 'body')==0){
740 //wfDebug("Ending $name node.");
741 if (0 == strcasecmp($name,'img')) {
742 // Insert a dummy leaf for the image
743 $img = new ImageNode($this->currentParent
, $this->currentParent
->getAttributes());
744 $img->setWhiteBefore($this->whiteSpaceBeforeThis
);
745 $this->lastSibling
= $img;
746 $this->textNodes
[] = $img;
749 if ($this->currentParent
->isInline()) {
750 $this->lastSibling
= $this->currentParent
;
752 $this->lastSibling
= NULL;
754 $this->currentParent
= $this->currentParent
->getParent();
755 $this->whiteSpaceBeforeThis
= false;
757 $this->endDocument();
761 public function characters($parser, $data){
762 //wfDebug('Parsing '. strlen($data).' characters.');
763 $array = str_split($data);
764 foreach($array as $c) {
765 if (self
::isDelimiter($c)) {
767 if (WhiteSpaceNode
::isWhiteSpace($c) && !$this->currentParent
->isPre()
768 && !$this->currentParent
->inPre()) {
769 if (!is_null($this->lastSibling
)){
770 $this->lastSibling
->setWhiteAfter(true);
772 $this->whiteSpaceBeforeThis
= true;
774 $textNode = new TextNode($this->currentParent
, $c);
775 $textNode->setWhiteBefore($this->whiteSpaceBeforeThis
);
776 $this->whiteSpaceBeforeThis
= false;
777 $this->lastSibling
= $textNode;
778 $this->textNodes
[] = $textNode;
781 $this->newWord
.= $c;
787 private function endWord() {
788 if (strlen($this->newWord
) > 0) {
789 $node = new TextNode($this->currentParent
, $this->newWord
);
790 $node->setWhiteBefore($this->whiteSpaceBeforeThis
);
791 $this->whiteSpaceBeforeThis
= false;
792 $this->lastSibling
= $node;
793 $this->textNodes
[] = $node;
798 public static function isDelimiter($c) {
827 return WhiteSpaceNode
::isWhiteSpace($c);
831 public function getDiffLines(){
832 return array_map(array('TextNode','toDiffLine'), $this->textNodes
);
836 class TextNodeDiffer
{
841 private $oldTextNodes;
842 private $oldBodyNode;
844 private $lastModified = array();
846 function __construct(DomTreeBuilder
$tree, DomTreeBuilder
$oldTree) {
847 $this->textNodes
= $tree->getTextNodes();
848 $this->bodyNode
= $tree->getBodyNode();
849 $this->oldTextNodes
= $oldTree->getTextNodes();
850 $this->oldBodyNode
= $oldTree->getBodyNode();
853 public function getBodyNode() {
854 return $this->bodyNode
;
859 public function markAsNew($start, $end) {
863 if ($this->whiteAfterLastChangedPart
)
864 $this->textNodes
[$start]->setWhiteBefore(false);
866 $nextLastModified = array();
868 for ($i = $start; $i < $end; ++
$i) {
869 $mod = new Modification(Modification
::ADDED
);
870 $mod->setID($this->newID
);
871 if (sizeof($this->lastModified
) > 0) {
872 $mod->setPrevious($this->lastModified
[0]);
873 if (is_null($this->lastModified
[0]->getNext())) {
874 foreach ($this->lastModified
as $lastMod) {
875 $lastMod->setNext($mod);
879 $nextLastModified[] = $mod;
880 $this->textNodes
[$i]->setModification($mod);
883 $this->textNodes
[$start]->getModification()->setFirstOfID(true);
886 $this->lastModified
= $nextLastModified;
889 private $changedID = 0;
891 private $changedIDUsed = false;
893 public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) {
897 if ($this->changedIDUsed
) {
899 $this->changedIDUsed
= false;
902 $nextLastModified = array();
905 while ($i < $rightend) {
906 $acthis = new AncestorComparator($this->textNodes
[$i]->getParentTree());
907 $acother = new AncestorComparator($this->oldTextNodes
[$j]->getParentTree());
908 $result = $acthis->getResult($acother);
909 unset($acthis, $acother);
911 $nbLastModified = sizeof($this->lastModified
);
912 if ($result->isChanged()) {
913 $mod = new Modification(Modification
::CHANGED
);
915 if (!$this->changedIDUsed
) {
916 $mod->setFirstOfID(true);
917 if (sizeof($nextLastModified) > 0) {
918 $this->lastModified
= $nextLastModified;
919 $nextLastModified = array();
921 } else if (!is_null($result->getChanges()) && $result->getChanges() != $this->changes
) {
923 $mod->setFirstOfID(true);
924 if (sizeof($nextLastModified) > 0) {
925 $this->lastModified
= $nextLastModified;
926 $nextLastModified = array();
930 if ($nbLastModified > 0) {
931 $mod->setPrevious($this->lastModified
[0]);
932 if (is_null($this->lastModified
[0]->getNext())) {
933 foreach ($this->lastModified
as $lastMod) {
934 $lastMod->setNext($mod);
938 $nextLastModified[] = $mod;
940 $mod->setChanges($result->getChanges());
941 $mod->setID($this->changedID
);
943 $this->textNodes
[$i]->setModification($mod);
944 $this->changes
= $result->getChanges();
945 $this->changedIDUsed
= true;
946 } else if ($this->changedIDUsed
) {
948 $this->changedIDUsed
= false;
953 if (sizeof($nextLastModified) > 0){
954 $this->lastModified
= $nextLastModified;
958 // used to remove the whitespace between a red and green block
959 private $whiteAfterLastChangedPart = false;
961 private $deletedID = 0;
963 public function markAsDeleted($start, $end, $before) {
968 if ($before > 0 && $this->textNodes
[$before - 1]->isWhiteAfter()) {
969 $this->whiteAfterLastChangedPart
= true;
971 $this->whiteAfterLastChangedPart
= false;
974 $nextLastModified = array();
976 for ($i = $start; $i < $end; ++
$i) {
977 $mod = new Modification(Modification
::REMOVED
);
978 $mod->setID($this->deletedID
);
979 if (sizeof($this->lastModified
) > 0) {
980 $mod->setPrevious($this->lastModified
[0]);
981 if (is_null($this->lastModified
[0]->getNext())) {
982 foreach ($this->lastModified
as $lastMod) {
983 $lastMod->setNext($mod);
987 $nextLastModified[] = $mod;
989 // oldTextNodes is used here because we're going to move its deleted
992 $this->oldTextNodes
[$i]->setModification($mod);
994 $this->oldTextNodes
[$start]->getModification()->setFirstOfID(true);
996 $deletedNodes = $this->oldBodyNode
->getMinimalDeletedSet($this->deletedID
);
998 //wfDebug("Minimal set of deleted nodes of size " . sizeof($deletedNodes));
1000 // Set prevLeaf to the leaf after which the old HTML needs to be
1003 $prevLeaf = $this->textNodes
[$before - 1];
1005 // Set nextLeaf to the leaf before which the old HTML needs to be
1007 if ($before < sizeof($this->textNodes
)){
1008 $nextLeaf = $this->textNodes
[$before];
1011 while (sizeof($deletedNodes) > 0) {
1012 if (isset($prevLeaf)) {
1013 $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
1015 $prevResult = new LastCommonParentResult();
1016 $prevResult->setLastCommonParent($this->getBodyNode());
1017 $prevResult->setIndexInLastCommonParent(0);
1019 if (isset($nextleaf)) {
1020 $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[sizeof($deletedNodes) - 1]);
1022 $nextResult = new LastCommonParentResult();
1023 $nextResult->setLastCommonParent($this->getBodyNode());
1024 $nextResult->setIndexInLastCommonParent($this->getBodyNode()->getNbChildren());
1027 if ($prevResult->getLastCommonParentDepth() == $nextResult->getLastCommonParentDepth()) {
1028 // We need some metric to choose which way to add-...
1029 if ($deletedNodes[0]->getParent() === $deletedNodes[sizeof($deletedNodes) - 1]->getParent()
1030 && $prevResult->getLastCommonParent() === $nextResult->getLastCommonParent()) {
1031 // The difference is not in the parent
1032 $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() +
1);
1034 // The difference is in the parent, so compare them
1035 // now THIS is tricky
1036 $distancePrev = $deletedNodes[0]->getParent()->getMatchRatio($prevResult->getLastCommonParent());
1037 $distanceNext = $deletedNodes[sizeof($deletedNodes) - 1]->getParent()->getMatchRatio($nextResult->getLastCommonParent());
1039 if ($distancePrev <= $distanceNext) {
1040 $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() +
1);
1042 $nextResult->setLastCommonParentDepth($nextResult->getLastCommonParentDepth() +
1);
1048 if ($prevResult->getLastCommonParentDepth() > $nextResult->getLastCommonParentDepth()) {
1049 // Inserting at the front
1050 if ($prevResult->isSplittingNeeded()) {
1051 $prevLeaf->getParent()->splitUntill($prevResult->getLastCommonParent(), $prevLeaf, true);
1053 $prevLeaf = $deletedNodes[0]->copyTree();
1054 unset($deletedNodes[0]);
1055 $deletedNodes = array_values($deletedNodes);
1056 $prevLeaf->setParent($prevResult->getLastCommonParent());
1057 $prevResult->getLastCommonParent()->addChild($prevLeaf,$prevResult->getIndexInLastCommonParent() +
1);
1058 } else if ($prevResult->getLastCommonParentDepth() < $nextResult->getLastCommonParentDepth()) {
1059 // Inserting at the back
1060 if ($nextResult->isSplittingNeeded()) {
1061 $splitOccured = $nextLeaf->getParent()->splitUntill($nextResult->getLastCommonParent(), $nextLeaf, false);
1062 if ($splitOccured) {
1063 // The place where to insert is shifted one place to the
1065 $nextResult->setIndexInLastCommonParent($nextResult->getIndexInLastCommonParent() +
1);
1068 $nextLeaf = $deletedNodes[sizeof(deletedNodes
) - 1]->copyTree();
1069 unset($deletedNodes[sizeof(deletedNodes
) - 1]);
1070 $deletedNodes = array_values($deletedNodes);
1071 $nextLeaf->setParent($nextResult->getLastCommonParent());
1072 $nextResult->getLastCommonParent()->addChild($nextLeaf,$nextResult->getIndexInLastCommonParent());
1074 throw new Exception("Uh?");
1076 $this->lastModified
= $nextLastModified;
1080 public function expandWhiteSpace() {
1081 $this->getBodyNode()->expandWhiteSpace();
1084 public function lengthNew(){
1085 return sizeof($this->textNodes
);
1088 public function lengthOld(){
1089 return sizeof($this->oldTextNodes
);
1097 function __construct($output){
1098 $this->output
= $output;
1101 function htmlDiff($from, $to){
1102 // Create an XML parser
1103 $xml_parser = xml_parser_create('');
1105 $domfrom = new DomTreeBuilder();
1107 // Set the functions to handle opening and closing tags
1108 xml_set_element_handler($xml_parser, array($domfrom,"startElement"), array($domfrom,"endElement"));
1110 // Set the function to handle blocks of character data
1111 xml_set_character_data_handler($xml_parser, array($domfrom,"characters"));
1114 //wfDebug('Parsing '.strlen($from)." characters worth of HTML");
1115 if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer
::hackDocType().'<body>', FALSE)
1116 ||
!xml_parse($xml_parser, $from, FALSE)
1117 ||
!xml_parse($xml_parser, '</body>', TRUE)){
1118 wfDebug(sprintf("XML error: %s at line %d",xml_error_string(xml_get_error_code($xml_parser)),xml_get_current_line_number($xml_parser)));
1120 xml_parser_free($xml_parser);
1123 $xml_parser = xml_parser_create('');
1125 $domto = new DomTreeBuilder();
1127 // Set the functions to handle opening and closing tags
1128 xml_set_element_handler($xml_parser, array($domto,"startElement"), array($domto,"endElement"));
1130 // Set the function to handle blocks of character data
1131 xml_set_character_data_handler($xml_parser, array($domto,"characters"));
1133 //wfDebug('Parsing '.strlen($to)." characters worth of HTML");
1134 if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer
::hackDocType().'<body>', FALSE)
1135 ||
!xml_parse($xml_parser, $to, FALSE)
1136 ||
!xml_parse($xml_parser, '</body>', TRUE)){
1137 wfDebug(sprintf("XML error in HTML diff: %s at line %d",xml_error_string(xml_get_error_code($xml_parser)),xml_get_current_line_number($xml_parser)));
1139 xml_parser_free($xml_parser);
1142 $diffengine = new _DiffEngine();
1143 $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines()));
1144 unset($xml_parser,$diffengine);
1146 $domdiffer = new TextNodeDiffer($domto, $domfrom);
1148 $currentIndexLeft = 0;
1149 $currentIndexRight = 0;
1150 foreach ($differences as $d) {
1151 if ($d->leftstart
> $currentIndexLeft) {
1152 $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart
,
1153 $currentIndexRight, $d->rightstart
);
1155 if ($d->leftlength
> 0) {
1156 $domdiffer->markAsDeleted($d->leftstart
, $d->leftend
, $d->rightstart
);
1158 $domdiffer->markAsNew($d->rightstart
, $d->rightend
);
1160 $currentIndexLeft = $d->leftend
;
1161 $currentIndexRight = $d->rightend
;
1163 if ($currentIndexLeft < $domdiffer->lengthOld()) {
1164 $domdiffer->handlePossibleChangedPart($currentIndexLeft,$domdiffer->lengthOld(), $currentIndexRight,$domdiffer->lengthNew());
1167 $domdiffer->expandWhiteSpace();
1168 $output = new HTMLOutput('htmldiff', $this->output
);
1169 $output->parse($domdiffer->getBodyNode());
1172 private function preProcess(/*array*/ $differences){
1173 $newRanges = array();
1175 $nbDifferences = sizeof($differences);
1176 for ($i = 0; $i < $nbDifferences; ++
$i) {
1177 $leftStart = $differences[$i]->leftstart
;
1178 $leftEnd = $differences[$i]->leftend
;
1179 $rightStart = $differences[$i]->rightstart
;
1180 $rightEnd = $differences[$i]->rightend
;
1182 $leftLength = $leftEnd - $leftStart;
1183 $rightLength = $rightEnd - $rightStart;
1185 while ($i +
1 < $nbDifferences && self
::score($leftLength, $differences[$i +
1]->leftlength
,
1186 $rightLength, $differences[$i +
1]->rightlength
) > ($differences[$i +
1]->leftstart
- $leftEnd)) {
1187 $leftEnd = $differences[$i +
1]->leftend
;
1188 $rightEnd = $differences[$i +
1]->rightend
;
1189 $leftLength = $leftEnd - $leftStart;
1190 $rightLength = $rightEnd - $rightStart;
1193 $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd);
1199 * Heuristic to merge differences for readability.
1201 public static function score($ll, $nll, $rl, $nrl) {
1202 if (($ll == 0 && $nll == 0)
1203 ||
($rl == 0 && $nrl == 0)){
1206 $numbers = array($ll, $nll, $rl, $nrl);
1208 foreach ($numbers as $number) {
1209 while ($number > 3) {
1217 return $d / (1.5 * sizeof($numbers));
1222 class TextOnlyComparator
{
1224 public $leafs = array();
1226 function _construct(TagNode
$tree) {
1227 $this->addRecursive($tree);
1228 $this->leafs
= array_map(array('TextNode','toDiffLine'), $this->leafs
);
1231 private function addRecursive(TagNode
$tree) {
1232 foreach ($tree->children
as $child) {
1233 if ($child instanceof TagNode
) {
1234 $this->addRecursive($child);
1235 } else if ($child instanceof TextNode
) {
1236 $this->leafs
[] = $node;
1241 public function getMatchRatio(TextOnlyComparator
$other) {
1242 $nbOthers = sizeof($other->leafs
);
1243 $nbThis = sizeof($this->leafs
);
1244 if($nbOthers == 0 ||
$nbThis == 0){
1248 $diffengine = new _DiffEngine();
1249 $diffengine->diff_local($this->leafs
, $other->leafs
);
1251 $distanceThis = array_sum($diffengine->xchanged
);
1252 $distanceOther = array_sum($diffengine->ychanged
);
1254 return ((0.0 +
$distanceOther) / $nbOthers +
(0.0 +
$distanceThis)
1259 class AncestorComparatorResult
{
1261 private $changed = false;
1263 private $changes = "";
1265 public function isChanged() {
1266 return $this->changed
;
1269 public function setChanged($changed) {
1270 $this->changed
= $changed;
1273 public function getChanges() {
1274 return $this->changes
;
1277 public function setChanges($changes) {
1278 $this->changes
= $changes;
1283 * A comparator used when calculating the difference in ancestry of two Nodes.
1285 class AncestorComparator
{
1288 public $ancestorsText;
1290 function __construct(/*array*/ $ancestors) {
1291 $this->ancestors
= $ancestors;
1292 $this->ancestorsText
= array_map(array('TagNode','toDiffLine'), $ancestors);
1295 private $compareTxt = "";
1297 public function getCompareTxt() {
1298 return $this->compareTxt
;
1301 public function getResult(AncestorComparator
$other) {
1302 $result = new AncestorComparatorResult();
1304 $diffengine = new _DiffEngine();
1305 $differences = $diffengine->diff_range($this->ancestorsText
, $other->ancestorsText
);
1307 if (sizeof($differences) == 0){
1310 $changeTxt = new ChangeTextGenerator($this, $other);
1312 $result->setChanged(true);
1313 $result->setChanges($changeTxt->getChanged($differences)->toString());
1319 class ChangeTextGenerator
{
1326 function __construct(AncestorComparator
$old, AncestorComparator
$new) {
1329 $this->factory
= new TagToStringFactory();
1332 public function getChanged(/*array*/ $differences) {
1333 $txt = new ChangeText
;
1335 $rootlistopened = false;
1337 if (sizeof($differences) > 1) {
1338 $txt->addHtml('<ul class="changelist">');
1339 $rootlistopened = true;
1342 $nbDifferences = sizeof($differences);
1343 for ($j = 0; $j < $nbDifferences; ++
$j) {
1344 $d = $differences[$j];
1346 $lvl1listopened = false;
1348 if ($rootlistopened) {
1349 $txt->addHtml('<li>');
1352 if ($d->leftlength +
$d->rightlength
> 1) {
1353 $txt->addHtml('<ul class="changelist">');
1354 $lvl1listopened = true;
1357 // left are the old ones
1358 for ($i = $d->leftstart
; $i < $d->leftend
; ++
$i) {
1359 if ($lvl1listopened){
1360 $txt->addHtml('<li>');
1362 // add a bullet for a old tag
1363 $this->addTagOld($txt, $this->old
->ancestors
[$i]);
1365 if ($lvl1listopened){
1366 $txt->addHtml('</li>');
1370 // right are the new ones
1371 for ($i = $d->rightstart
; $i < $d->rightend
; ++
$i) {
1372 if ($lvl1listopened){
1373 $txt->addHtml('<li>');
1376 // add a bullet for a new tag
1377 $this->addTagNew($txt, $this->new->ancestors
[$i]);
1379 if ($lvl1listopened){
1380 $txt->addHtml('</li>');
1385 if ($lvl1listopened) {
1386 $txt->addHtml('</ul>');
1389 if ($rootlistopened) {
1390 $txt->addHtml('</li>');
1394 if ($rootlistopened) {
1395 $txt->addHtml('</ul>');
1402 private function addTagOld(ChangeText
$txt, TagNode
$ancestor) {
1403 $this->factory
->create($ancestor)->getRemovedDescription($txt);
1406 private function addTagNew(ChangeText
$txt, TagNode
$ancestor) {
1407 $this->factory
->create($ancestor)->getAddedDescription($txt);
1415 const newLine
= "<br/>";
1417 public function addText($s) {
1418 $s = $this->clean($s);
1422 public function addHtml($s) {
1426 public function addNewLine() {
1427 $this->addHtml(self
::newLine
);
1430 public function toString() {
1434 private function clean($s) {
1435 return htmlspecialchars($s);
1439 class TagToStringFactory
{
1441 private static $containerTags = array(
1445 'blockquote' => TRUE,
1470 // in-line tags that can be considered containers not styles
1475 private static $styleTags = array(
1493 public function create(TagNode
$node) {
1494 $sem = $this->getChangeSemantic($node->getQName());
1495 if (0 == strcasecmp($node->getQName(),'a')){
1496 return new AnchorToString($node, $sem);
1498 if (0 == strcasecmp($node->getQName(),'img')){
1499 return new NoContentTagToString($node, $sem);
1501 return new TagToString($node, $sem);
1504 protected function getChangeSemantic($qname) {
1505 if (array_key_exists(strtolower($qname),self
::$containerTags)){
1508 if (array_key_exists(strtolower($qname),self
::$styleTags)){
1511 return self
::UNKNOWN
;
1521 function __construct(TagNode
$node, $sem) {
1522 $this->node
= $node;
1526 public function getDescription() {
1527 return $this->getString('diff-' . $this->node
->getQName());
1530 public function getRemovedDescription(ChangeText
$txt) {
1532 if ($this->sem
== TagToStringFactory
::MOVED
) {
1533 $txt->addText($this->getMovedOutOf() . ' ' . strtolower($this->getArticle()) . ' ');
1534 $txt->addHtml('<b>');
1535 $txt->addText(strtolower($this->getDescription()));
1536 $txt->addHtml('</b>');
1537 } else if ($this->sem
== TagToStringFactory
::STYLE
) {
1538 $txt->addHtml('<b>');
1539 $txt->addText($this->getDescription());
1540 $txt->addHtml('</b>');
1541 $txt->addText(' ' . strtolower($this->getStyleRemoved()));
1543 $txt->addHtml('<b>');
1544 $txt->addText($this->getDescription());
1545 $txt->addHtml('</b>');
1546 $txt->addText(' ' . strtolower($this->getRemoved()));
1548 $this->addAttributes($txt, $this->node
->getAttributes());
1552 public function getAddedDescription(ChangeText
$txt) {
1554 if ($this->sem
== TagToStringFactory
::MOVED
) {
1555 $txt->addText($this->getMovedTo() . ' ' . strtolower($this->getArticle()) . ' ');
1556 $txt->addHtml('<b>');
1557 $txt->addText(strtolower($this->getDescription()));
1558 $txt->addHtml('</b>');
1559 } else if ($this->sem
== TagToStringFactory
::STYLE
) {
1560 $txt->addHtml('<b>');
1561 $txt->addText($this->getDescription());
1562 $txt->addHtml('</b>');
1563 $txt->addText(' ' . strtolower($this->getStyleAdded()));
1565 $txt->addHtml('<b>');
1566 $txt->addText($this->getDescription());
1567 $txt->addHtml('</b>');
1568 $txt->addText(' ' . strtolower($this->getAdded()));
1570 $this->addAttributes($txt, $this->node
->getAttributes());
1574 protected function getMovedTo() {
1575 return $this->getString('diff-movedto');
1578 protected function getStyleAdded() {
1579 return $this->getString('diff-styleadded');
1582 protected function getAdded() {
1583 return $this->getString('diff-added');
1586 protected function getMovedOutOf() {
1587 return $this->getString('diff-movedoutof');
1590 protected function getStyleRemoved() {
1591 return $this->getString('diff-styleremoved');
1594 protected function getRemoved() {
1595 return $this->getString('diff-removed');
1598 protected function addAttributes(ChangeText
$txt, array $attributes) {
1599 if (sizeof($attributes) < 1)
1602 $keys = array_keys($attributes);
1604 $txt->addText(' ' . strtolower($this->getWith()) . ' '
1605 . $this->translateArgument($keys[0]) . ' '
1606 . $attributes[$keys[0]]);
1607 for ($i = 1; $i < sizeof($attributes) - 1; $i++
) {
1608 $txt->addText(', ' . $this->translateArgument($keys[$i]) . ' '
1609 . $attributes[$keys[$i]]);
1611 if (sizeof($attributes) > 1) {
1613 . strtolower($this->getAnd())
1615 . $this->translateArgument($keys[sizeof($attributes) - 1]) . ' '
1616 . $attributes[$keys[sizeof($attributes) - 1]]);
1620 private function getAnd() {
1621 return $this->getString('diff-and');
1624 private function getWith() {
1625 return $this->getString('diff-with');
1628 protected function translateArgument($name) {
1629 if (0 == strcasecmp($name,'src'))
1630 return strtolower($this->getSource());
1631 if (0 == strcasecmp($name,'width'))
1632 return strtolower($this->getWidth());
1633 if (0 == strcasecmp($name,'height'))
1634 return strtolower($this->getHeight());
1638 private function getHeight() {
1639 return $this->getString('diff-height');
1642 private function getWidth() {
1643 return $this->getString('diff-width');
1646 protected function getSource() {
1647 return $this->getString('diff-source');
1650 protected function getArticle() {
1651 return $this->getString('diff-' . $this->node
->getQName() . '-article');
1654 public static $bundle = array(
1655 'diff-movedto' => 'Moved to',
1656 'diff-styleadded' => 'Style added',
1657 'diff-added' => 'Added',
1658 'diff-changedto' => 'Changed to',
1659 'diff-movedoutof' => 'Moved out of',
1660 'diff-styleremoved' => 'Style removed',
1661 'diff-removed' => 'Removed',
1662 'diff-changedfrom' => 'Changed from',
1663 'diff-source' => 'Source',
1664 'diff-withdestination' => 'With destination',
1665 'diff-and' => 'And',
1666 'diff-with' => 'With',
1667 'diff-width' => 'Width',
1668 'diff-height' => 'Height',
1669 'diff-html-article' => 'A',
1670 'diff-html' => 'Html page',
1671 'diff-body-article' => 'A',
1672 'diff-body' => 'Html document',
1673 'diff-p-article' => 'A',
1674 'diff-p' => 'Paragraph',
1675 'diff-blockquote-article' => 'A',
1676 'diff-blockquote' => 'Quote',
1677 'diff-h1-article' => 'A',
1678 'diff-h1' => 'Heading (level 1)',
1679 'diff-h2-article' => 'A',
1680 'diff-h2' => 'Heading (level 2)',
1681 'diff-h3-article' => 'A',
1682 'diff-h3' => 'Heading (level 3)',
1683 'diff-h4-article' => 'A',
1684 'diff-h4' => 'Heading (level 4)',
1685 'diff-h5-article' => 'A',
1686 'diff-h5' => 'Heading (level 5)',
1687 'diff-pre-article' => 'A',
1688 'diff-pre' => 'Preformatted block',
1689 'diff-div-article' => 'A',
1690 'diff-div' => 'Division',
1691 'diff-ul-article' => 'An',
1692 'diff-ul' => 'Unordered list',
1693 'diff-ol-article' => 'An',
1694 'diff-ol' => 'Ordered list',
1695 'diff-li-article' => 'A',
1696 'diff-li' => 'List item',
1697 'diff-table-article' => 'A',
1698 'diff-table' => 'Table',
1699 'diff-tbody-article' => 'A',
1700 'diff-tbody' => "Table's content",
1701 'diff-tr-article' => 'A',
1703 'diff-td-article' => 'A',
1704 'diff-td' => 'Cell',
1705 'diff-th-article' => 'A',
1706 'diff-th' => 'Header',
1707 'diff-br-article' => 'A',
1708 'diff-br' => 'Break',
1709 'diff-hr-article' => 'A',
1710 'diff-hr' => 'Horizontal rule',
1711 'diff-code-article' => 'A',
1712 'diff-code' => 'Computer code block',
1713 'diff-dl-article' => 'A',
1714 'diff-dl' => 'Definition list',
1715 'diff-dt-article' => 'A',
1716 'diff-dt' => 'Definition term',
1717 'diff-dd-article' => 'A',
1718 'diff-dd' => 'Definition',
1719 'diff-input-article' => 'An',
1720 'diff-input' => 'Input',
1721 'diff-form-article' => 'A',
1722 'diff-form' => 'Form',
1723 'diff-img-article' => 'An',
1724 'diff-img' => 'Image',
1725 'diff-span-article' => 'A',
1726 'diff-span' => 'Span',
1727 'diff-a-article' => 'A',
1729 'diff-i' => 'Italics',
1731 'diff-strong' => 'Strong',
1732 'diff-em' => 'Emphasis',
1733 'diff-font' => 'Font',
1734 'diff-big' => 'Big',
1735 'diff-del' => 'Deleted',
1736 'diff-tt' => 'Fixed width',
1737 'diff-sub' => 'Subscript',
1738 'diff-sup' => 'Superscript',
1739 'diff-strike' => 'Strikethrough'
1742 public function getString($key) {
1743 return self
::$bundle[$key];
1747 class NoContentTagToString
extends TagToString
{
1749 function __construct(TagNode
$node, $sem) {
1750 parent
::__construct($node, $sem);
1753 public function getAddedDescription(ChangeText
$txt) {
1754 $txt.addText($this->getChangedTo() . ' ' +
strtolower($this->getArticle()) . ' ');
1755 $txt.addHtml('<b>');
1756 $txt.addText(strtolower($this->getDescription()));
1757 $txt.addHtml('</b>');
1759 $this->addAttributes($txt, $this->node
->getAttributes());
1763 private function getChangedTo() {
1764 return $this->getString('diff-changedto');
1767 public function getRemovedDescription(ChangeText
$txt) {
1768 $txt.addText($this->getChangedFrom() . ' ' +
strtolower($this->getArticle()) . ' ');
1769 $txt.addHtml('<b>');
1770 $txt.addText(strtolower($this->getDescription()));
1771 $txt.addHtml('</b>');
1773 $this->addAttributes($txt, $this->node
->getAttributes());
1777 private function getChangedFrom() {
1778 return $this->getString('diff-changedfrom');
1782 class AnchorToString
extends TagToString
{
1784 function __construct(TagNode
$node, $sem) {
1785 parent
::__construct($node, $sem);
1788 protected function addAttributes(ChangeText
$txt, array $attributes) {
1789 if (array_key_exists('href',$attributes)) {
1790 $txt->addText(' ' . strtolower($this->getWithDestination()) . ' ' . $attributes['href']);
1791 unset($attributes['href']);
1793 parent
::addAttributes($txt, $attributes);
1796 private function getWithDestination() {
1797 return $this->getString('diff-withdestination');
1803 * Takes a branch root and creates an HTML file for it.
1810 function __construct($prefix, $handler) {
1811 $this->prefix
= $prefix;
1812 $this->handler
= $handler;
1815 public function parse(TagNode
$node) {
1817 if (0 != strcasecmp($node->getQName(),'img') && 0 != strcasecmp($node->getQName(),'body')) {
1818 $this->handler
->startElement($node->getQName(), $node->getAttributes());
1821 $newStarted = false;
1822 $remStarted = false;
1823 $changeStarted = false;
1826 foreach ($node->children
as $child) {
1827 if ($child instanceof TagNode
) {
1829 $this->handler
->endElement('span');
1830 $newStarted = false;
1831 } else if ($changeStarted) {
1832 $this->handler
->endElement('span');
1833 $changeStarted = false;
1834 } else if ($remStarted) {
1835 $this->handler
->endElement('span');
1836 $remStarted = false;
1838 $this->parse($child);
1839 } else if ($child instanceof TextNode
) {
1840 $mod = $child->getModification();
1842 if ($newStarted && ($mod->getType() != Modification
::ADDED ||
$mod->isFirstOfID())) {
1843 $this->handler
->endElement('span');
1844 $newStarted = false;
1845 } else if ($changeStarted && ($mod->getType() != Modification
::CHANGED ||
$mod->getChanges() != $changeTXT ||
$mod->isFirstOfID())) {
1846 $this->handler
->endElement('span');
1847 $changeStarted = false;
1848 } else if ($remStarted && ($mod->getType() != Modification
::REMOVED ||
$mod ->isFirstOfID())) {
1849 $this->handler
->endElement('span');
1850 $remStarted = false;
1853 // no else because a removed part can just be closed and a new
1855 if (!$newStarted && $mod->getType() == Modification
::ADDED
) {
1856 $attrs = array('class'=>'diff-html-added');
1857 if ($mod->isFirstOfID()) {
1858 $attrs['id'] = 'added-' . $this->prefix
. '-' . $mod->getID();
1860 $this->addAttributes($mod, $attrs);
1861 $attrs['onclick'] = 'return tipA(constructToolTipA(this));';
1862 $this->handler
->startElement('span', $attrs);
1864 } else if (!$changeStarted && $mod->getType() == Modification
::CHANGED
) {
1865 $attrs = array('class'=>'diff-html-changed');
1866 if ($mod->isFirstOfID()) {
1867 $attrs['id'] = 'changed-' . $this->prefix
. '-' . $mod->getID();
1869 $this->addAttributes($mod, $attrs);
1870 $attrs['onclick'] = 'return tipC(constructToolTipC(this));';
1871 $this->handler
->startElement('span', $attrs);
1874 $this->handler
->startElement('span', array('class'=>'tip'));
1875 $this->handler
->characters($mod->getChanges());
1876 $this->handler
->endElement('span');
1878 $changeStarted = true;
1879 $changeTXT = $mod->getChanges();
1880 } else if (!$remStarted && $mod->getType() == Modification
::REMOVED
) {
1881 $attrs = array('class'=>'diff-html-removed');
1882 if ($mod->isFirstOfID()) {
1883 $attrs['id'] = 'removed-' . $this->prefix
. '-' . $mod->getID();
1885 $this->addAttributes($mod, $attrs);
1886 $attrs['onclick'] = 'return tipR(constructToolTipR(this));';
1887 $this->handler
->startElement('span', $attrs);
1891 $chars = $child->getText();
1893 if ($child instanceof ImageNode
) {
1894 $this->writeImage($child);
1896 $this->handler
->characters($chars);
1903 $this->handler
->endElement('span');
1904 $newStarted = false;
1905 } else if ($changeStarted) {
1906 $this->handler
->endElement('span');
1907 $changeStarted = false;
1908 } else if ($remStarted) {
1909 $this->handler
->endElement('span');
1910 $remStarted = false;
1913 if (0 != strcasecmp($node->getQName(),'img')
1914 && 0 != strcasecmp($node->getQName(),'body'))
1915 $this->handler
->endElement($node->getQName());
1918 private function writeImage(ImageNode
$imgNode){
1919 $attrs = $imgNode->getAttributes();
1920 if ($imgNode->getModification()->getType() == Modification
::REMOVED
)
1921 $attrs['changeType']='diff-removed-image';
1922 else if ($imgNode->getModification()->getType() == Modification
::ADDED
)
1923 $attrs['changeType'] = 'diff-added-image';
1924 $attrs['onload'] = 'updateOverlays()';
1925 $attrs['onError'] = 'updateOverlays()';
1926 $attrs['onAbort'] = 'updateOverlays()';
1927 $this->handler
->startElement('img', $attrs);
1928 $this->handler
->endElement('img');
1931 private function addAttributes(Modification
$mod, /*array*/ &$attrs) {
1932 if (is_null($mod->getPrevious())) {
1933 $previous = 'first-' . $this->prefix
;
1935 $previous = Modification
::typeToString($mod->getPrevious()->getType()) . '-' . $this->prefix
. '-'
1936 . $mod->getPrevious()->getID();
1938 $attrs['previous'] = $previous;
1940 $changeId = Modification
::typeToString($mod->getType()) . '-' +
$this->prefix
. '-' . $mod->getID();
1941 $attrs['changeId'] = $changeId;
1943 if (is_null($mod->getNext())) {
1944 $next = 'last-' . $this->prefix
;
1946 $next = Modification
::typeToString($mod->getNext()->getType()) . '-' . $this->prefix
. '-'
1947 . $mod->getNext()->getID();
1949 $attrs['next'] = $next;
1953 class EchoingContentHandler
{
1955 function startElement($qname, /*array*/ $arguments){
1957 foreach($arguments as $key => $value){
1958 echo ' '.$key.'="'.Sanitizer
::encodeAttribute($value).'"';
1963 function endElement($qname){
1964 echo '</'.$qname.'>';
1967 function characters($chars){
1973 class DelegatingContentHandler
{
1977 function __construct($delegate){
1978 $this->delegate
=$delegate;
1981 function startElement($qname, /*array*/ $arguments){
1982 $this->delegate
->addHtml('<'.$qname) ;
1983 foreach($arguments as $key => $value){
1984 $this->delegate
->addHtml(' '.$key.'="'. Sanitizer
::encodeAttribute($value) .'"');
1986 $this->delegate
->addHtml('>');
1989 function endElement($qname){
1990 $this->delegate
->addHtml('</'.$qname.'>');
1993 function characters($chars){
1994 $this->delegate
->addHtml( $chars );