00001 <?php
00002
00026 class Node {
00027
00028 public $parent;
00029
00030 protected $parentTree;
00031
00032 public $whiteBefore = false;
00033
00034 public $whiteAfter = false;
00035
00036 function __construct($parent) {
00037 $this->parent = $parent;
00038 }
00039
00040 public function getParentTree() {
00041 if (!isset($this->parentTree)) {
00042 if (!is_null($this->parent)) {
00043 $this->parentTree = $this->parent->getParentTree();
00044 $this->parentTree[] = $this->parent;
00045 } else {
00046 $this->parentTree = array();
00047 }
00048 }
00049 return $this->parentTree;
00050 }
00051
00052 public function getLastCommonParent(Node $other) {
00053 $result = new LastCommonParentResult();
00054
00055 $myParents = $this->getParentTree();
00056 $otherParents = $other->getParentTree();
00057
00058 $i = 1;
00059 $isSame = true;
00060 $nbMyParents = count($myParents);
00061 $nbOtherParents = count($otherParents);
00062 while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) {
00063 if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) {
00064 $isSame = false;
00065 } else {
00066
00067 $i++;
00068 }
00069 }
00070
00071 $result->lastCommonParentDepth = $i - 1;
00072 $result->parent = $myParents[$i - 1];
00073
00074 if (!$isSame || $nbMyParents > $nbOtherParents) {
00075
00076
00077 $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]);
00078 $result->splittingNeeded = true;
00079 } else if ($nbMyParents <= $nbOtherParents) {
00080 $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this);
00081 }
00082 return $result;
00083 }
00084
00085 public function setParent($parent) {
00086 $this->parent = $parent;
00087 unset($this->parentTree);
00088 }
00089
00090 public function inPre() {
00091 $tree = $this->getParentTree();
00092 foreach ($tree as &$ancestor) {
00093 if ($ancestor->isPre()) {
00094 return true;
00095 }
00096 }
00097 return false;
00098 }
00099 }
00100
00105 class TagNode extends Node {
00106
00107 public $children = array();
00108
00109 public $qName;
00110
00111 public $attributes = array();
00112
00113 public $openingTag;
00114
00115 function __construct($parent, $qName, $attributes) {
00116 parent::__construct($parent);
00117 $this->qName = strtolower($qName);
00118 foreach($attributes as $key => &$value){
00119 $this->attributes[strtolower($key)] = $value;
00120 }
00121 return $this->openingTag = Xml::openElement($this->qName, $this->attributes);
00122 }
00123
00124 public function addChildAbsolute(Node $node, $index) {
00125 array_splice($this->children, $index, 0, array($node));
00126 }
00127
00128 public function getIndexOf(Node $child) {
00129
00130 foreach ($this->children as $key => &$value){
00131 if ($value === $child) {
00132 return $key;
00133 }
00134 }
00135 return null;
00136 }
00137
00138 public function getNbChildren() {
00139 return count($this->children);
00140 }
00141
00142 public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
00143 $nodes = array();
00144
00145 $allDeleted = false;
00146 $somethingDeleted = false;
00147 $hasNonDeletedDescendant = false;
00148
00149 if (empty($this->children)) {
00150 return $nodes;
00151 }
00152
00153 foreach ($this->children as &$child) {
00154 $allDeleted_local = false;
00155 $somethingDeleted_local = false;
00156 $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local);
00157 if ($somethingDeleted_local) {
00158 $nodes = array_merge($nodes, $childrenChildren);
00159 $somethingDeleted = true;
00160 }
00161 if (!$allDeleted_local) {
00162 $hasNonDeletedDescendant = true;
00163 }
00164 }
00165 if (!$hasNonDeletedDescendant) {
00166 $nodes = array($this);
00167 $allDeleted = true;
00168 }
00169 return $nodes;
00170 }
00171
00172 public function splitUntil(TagNode $parent, Node $split, $includeLeft) {
00173 $splitOccured = false;
00174 if ($parent !== $this) {
00175 $part1 = new TagNode(null, $this->qName, $this->attributes);
00176 $part2 = new TagNode(null, $this->qName, $this->attributes);
00177 $part1->setParent($this->parent);
00178 $part2->setParent($this->parent);
00179
00180 $onSplit = false;
00181 $pastSplit = false;
00182 foreach ($this->children as &$child)
00183 {
00184 if ($child === $split) {
00185 $onSplit = true;
00186 }
00187 if(!$pastSplit || ($onSplit && $includeLeft)) {
00188 $child->setParent($part1);
00189 $part1->children[] = $child;
00190 } else {
00191 $child->setParent($part2);
00192 $part2->children[] = $child;
00193 }
00194 if ($onSplit) {
00195 $onSplit = false;
00196 $pastSplit = true;
00197 }
00198 }
00199 $myindexinparent = $this->parent->getIndexOf($this);
00200 if (!empty($part1->children)) {
00201 $this->parent->addChildAbsolute($part1, $myindexinparent);
00202 }
00203 if (!empty($part2->children)) {
00204 $this->parent->addChildAbsolute($part2, $myindexinparent);
00205 }
00206 if (!empty($part1->children) && !empty($part2->children)) {
00207 $splitOccured = true;
00208 }
00209
00210 $this->parent->removeChild($myindexinparent);
00211
00212 if ($includeLeft) {
00213 $this->parent->splitUntil($parent, $part1, $includeLeft);
00214 } else {
00215 $this->parent->splitUntil($parent, $part2, $includeLeft);
00216 }
00217 }
00218 return $splitOccured;
00219
00220 }
00221
00222 private function removeChild($index) {
00223 unset($this->children[$index]);
00224 $this->children = array_values($this->children);
00225 }
00226
00227 public static $blocks = array('html', 'body','p','blockquote', 'h1',
00228 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table',
00229 'tbody', 'tr', 'td', 'th', 'br');
00230
00231 public function copyTree() {
00232 $newThis = new TagNode(null, $this->qName, $this->attributes);
00233 $newThis->whiteBefore = $this->whiteBefore;
00234 $newThis->whiteAfter = $this->whiteAfter;
00235 foreach ($this->children as &$child) {
00236 $newChild = $child->copyTree();
00237 $newChild->setParent($newThis);
00238 $newThis->children[] = $newChild;
00239 }
00240 return $newThis;
00241 }
00242
00243 public function getMatchRatio(TagNode $other) {
00244 $txtComp = new TextOnlyComparator($other);
00245 return $txtComp->getMatchRatio(new TextOnlyComparator($this));
00246 }
00247
00248 public function expandWhiteSpace() {
00249 $shift = 0;
00250 $spaceAdded = false;
00251
00252 $nbOriginalChildren = $this->getNbChildren();
00253 for ($i = 0; $i < $nbOriginalChildren; ++$i) {
00254 $child = $this->children[$i + $shift];
00255
00256 if ($child instanceof TagNode) {
00257 if (!$child->isPre()) {
00258 $child->expandWhiteSpace();
00259 }
00260 }
00261 if (!$spaceAdded && $child->whiteBefore) {
00262 $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild());
00263 $ws->setParent($this);
00264 $this->addChildAbsolute($ws,$i + ($shift++));
00265 }
00266 if ($child->whiteAfter) {
00267 $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild());
00268 $ws->setParent($this);
00269 $this->addChildAbsolute($ws,$i + 1 + ($shift++));
00270 $spaceAdded = true;
00271 } else {
00272 $spaceAdded = false;
00273 }
00274
00275 }
00276 }
00277
00278 public function getLeftMostChild() {
00279 if (empty($this->children)) {
00280 return $this;
00281 }
00282 return $this->children[0]->getLeftMostChild();
00283 }
00284
00285 public function getRightMostChild() {
00286 if (empty($this->children)) {
00287 return $this;
00288 }
00289 return $this->children[$this->getNbChildren() - 1]->getRightMostChild();
00290 }
00291
00292 public function isPre() {
00293 return 0 == strcasecmp($this->qName,'pre');
00294 }
00295
00296 public static function toDiffLine(TagNode $node) {
00297 return $node->openingTag;
00298 }
00299 }
00300
00305 class TextNode extends Node {
00306
00307 public $text;
00308
00309 public $modification;
00310
00311 function __construct($parent, $text) {
00312 parent::__construct($parent);
00313 $this->modification = new Modification(Modification::NONE);
00314 $this->text = $text;
00315 }
00316
00317 public function copyTree() {
00318 $clone = clone $this;
00319 $clone->setParent(null);
00320 return $clone;
00321 }
00322
00323 public function getLeftMostChild() {
00324 return $this;
00325 }
00326
00327 public function getRightMostChild() {
00328 return $this;
00329 }
00330
00331 public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
00332 if ($this->modification->type == Modification::REMOVED
00333 && $this->modification->id == $id){
00334 $somethingDeleted = true;
00335 $allDeleted = true;
00336 return array($this);
00337 }
00338 return array();
00339 }
00340
00341 public function isSameText($other) {
00342 if (is_null($other) || ! $other instanceof TextNode) {
00343 return false;
00344 }
00345 return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text);
00346 }
00347
00348 public static function toDiffLine(TextNode $node) {
00349 return str_replace('\n', ' ',$node->text);
00350 }
00351 }
00352
00357 class WhiteSpaceNode extends TextNode {
00358
00359 function __construct($parent, $s, Node $like = null) {
00360 parent::__construct($parent, $s);
00361 if(!is_null($like) && $like instanceof TextNode) {
00362 $newModification = clone $like->modification;
00363 $newModification->firstOfID = false;
00364 $this->modification = $newModification;
00365 }
00366 }
00367 }
00368
00373 class BodyNode extends TagNode {
00374
00375 function __construct() {
00376 parent::__construct(null, 'body', array());
00377 }
00378
00379 public function copyTree() {
00380 $newThis = new BodyNode();
00381 foreach ($this->children as &$child) {
00382 $newChild = $child->copyTree();
00383 $newChild->setParent($newThis);
00384 $newThis->children[] = $newChild;
00385 }
00386 return $newThis;
00387 }
00388
00389 public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
00390 $nodes = array();
00391 foreach ($this->children as &$child) {
00392 $childrenChildren = $child->getMinimalDeletedSet($id,
00393 $allDeleted, $somethingDeleted);
00394 $nodes = array_merge($nodes, $childrenChildren);
00395 }
00396 return $nodes;
00397 }
00398
00399 }
00400
00406 class ImageNode extends TextNode {
00407
00408 public $attributes;
00409
00410 function __construct(TagNode $parent, $attrs) {
00411 if(!array_key_exists('src', $attrs)) {
00412 HTMLDiffer::diffDebug( "Image without a source\n" );
00413 parent::__construct($parent, '<img></img>');
00414 }else{
00415 parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
00416 }
00417 $this->attributes = $attrs;
00418 }
00419
00420 public function isSameText($other) {
00421 if (is_null($other) || ! $other instanceof ImageNode) {
00422 return false;
00423 }
00424 return $this->text === $other->text;
00425 }
00426
00427 }
00428
00433 class DummyNode extends Node {
00434
00435 function __construct() {
00436
00437 }
00438
00439 }