InnerNode.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <?php
  2. namespace PHPHtmlParser\Dom;
  3. use PHPHtmlParser\Exceptions\ChildNotFoundException;
  4. use PHPHtmlParser\Exceptions\CircularException;
  5. use stringEncode\Encode;
  6. /**
  7. * Inner node of the html tree, might have children.
  8. *
  9. * @package PHPHtmlParser\Dom
  10. */
  11. abstract class InnerNode extends ArrayNode
  12. {
  13. /**
  14. * An array of all the children.
  15. *
  16. * @var array
  17. */
  18. protected $children = [];
  19. /**
  20. * Sets the encoding class to this node and propagates it
  21. * to all its children.
  22. *
  23. * @param Encode $encode
  24. * @return void
  25. */
  26. public function propagateEncoding(Encode $encode): void
  27. {
  28. $this->encode = $encode;
  29. $this->tag->setEncoding($encode);
  30. // check children
  31. foreach ($this->children as $id => $child) {
  32. /** @var AbstractNode $node */
  33. $node = $child['node'];
  34. $node->propagateEncoding($encode);
  35. }
  36. }
  37. /**
  38. * Checks if this node has children.
  39. *
  40. * @return bool
  41. */
  42. public function hasChildren(): bool
  43. {
  44. return ! empty($this->children);
  45. }
  46. /**
  47. * Returns the child by id.
  48. *
  49. * @param int $id
  50. * @return AbstractNode
  51. * @throws ChildNotFoundException
  52. */
  53. public function getChild(int $id): AbstractNode
  54. {
  55. if ( ! isset($this->children[$id])) {
  56. throw new ChildNotFoundException("Child '$id' not found in this node.");
  57. }
  58. return $this->children[$id]['node'];
  59. }
  60. /**
  61. * Returns a new array of child nodes
  62. *
  63. * @return array
  64. */
  65. public function getChildren(): array
  66. {
  67. $nodes = [];
  68. try {
  69. $child = $this->firstChild();
  70. do {
  71. $nodes[] = $child;
  72. $child = $this->nextChild($child->id());
  73. } while ( ! is_null($child));
  74. } catch (ChildNotFoundException $e) {
  75. // we are done looking for children
  76. }
  77. return $nodes;
  78. }
  79. /**
  80. * Counts children
  81. *
  82. * @return int
  83. */
  84. public function countChildren(): int
  85. {
  86. return count($this->children);
  87. }
  88. /**
  89. * Adds a child node to this node and returns the id of the child for this
  90. * parent.
  91. *
  92. * @param AbstractNode $child
  93. * @param Int $before
  94. * @return bool
  95. * @throws CircularException
  96. */
  97. public function addChild(AbstractNode $child, int $before = -1): bool
  98. {
  99. $key = null;
  100. // check integrity
  101. if ($this->isAncestor($child->id())) {
  102. throw new CircularException('Can not add child. It is my ancestor.');
  103. }
  104. // check if child is itself
  105. if ($child->id() == $this->id) {
  106. throw new CircularException('Can not set itself as a child.');
  107. }
  108. $next = null;
  109. if ($this->hasChildren()) {
  110. if (isset($this->children[$child->id()])) {
  111. // we already have this child
  112. return false;
  113. }
  114. if ($before >= 0) {
  115. if (!isset($this->children[$before])) {
  116. return false;
  117. }
  118. $key = $this->children[$before]['prev'];
  119. if($key){
  120. $this->children[$key]['next'] = $child->id();
  121. }
  122. $this->children[$before]['prev'] = $child->id();
  123. $next = $before;
  124. } else {
  125. $sibling = $this->lastChild();
  126. $key = $sibling->id();
  127. $this->children[$key]['next'] = $child->id();
  128. }
  129. }
  130. $keys = array_keys($this->children);
  131. $insert = [
  132. 'node' => $child,
  133. 'next' => $next,
  134. 'prev' => $key,
  135. ];
  136. $index = $key ? (array_search($key, $keys, true) + 1) : 0;
  137. array_splice($keys, $index, 0, $child->id());
  138. $children = array_values($this->children);
  139. array_splice($children, $index, 0, [$insert]);
  140. // add the child
  141. $this->children = array_combine($keys, $children);
  142. // tell child I am the new parent
  143. $child->setParent($this);
  144. //clear any cache
  145. $this->clear();
  146. return true;
  147. }
  148. /**
  149. * Insert element before child with provided id
  150. *
  151. * @param AbstractNode $child
  152. * @param int $id
  153. * @return bool
  154. */
  155. public function insertBefore(AbstractNode $child, int $id): bool
  156. {
  157. return $this->addChild($child, $id);
  158. }
  159. /**
  160. * Insert element before after with provided id
  161. *
  162. * @param AbstractNode $child
  163. * @param int $id
  164. * @return bool
  165. */
  166. public function insertAfter(AbstractNode $child, int $id): bool
  167. {
  168. if (!isset($this->children[$id])) {
  169. return false;
  170. }
  171. if ($this->children[$id]['next']) {
  172. return $this->addChild($child, $this->children[$id]['next']);
  173. }
  174. // clear cache
  175. $this->clear();
  176. return $this->addChild($child);
  177. }
  178. /**
  179. * Removes the child by id.
  180. *
  181. * @param int $id
  182. * @return InnerNode
  183. * @chainable
  184. */
  185. public function removeChild(int $id): InnerNode
  186. {
  187. if ( ! isset($this->children[$id])) {
  188. return $this;
  189. }
  190. // handle moving next and previous assignments.
  191. $next = $this->children[$id]['next'];
  192. $prev = $this->children[$id]['prev'];
  193. if ( ! is_null($next)) {
  194. $this->children[$next]['prev'] = $prev;
  195. }
  196. if ( ! is_null($prev)) {
  197. $this->children[$prev]['next'] = $next;
  198. }
  199. // remove the child
  200. unset($this->children[$id]);
  201. //clear any cache
  202. $this->clear();
  203. return $this;
  204. }
  205. /**
  206. * Check if has next Child
  207. *
  208. * @param int $id
  209. * @return mixed
  210. */
  211. public function hasNextChild(int $id)
  212. {
  213. $child= $this->getChild($id);
  214. return $this->children[$child->id()]['next'];
  215. }
  216. /**
  217. * Attempts to get the next child.
  218. *
  219. * @param int $id
  220. * @return AbstractNode
  221. * @uses $this->getChild()
  222. * @throws ChildNotFoundException
  223. */
  224. public function nextChild(int $id): AbstractNode
  225. {
  226. $child = $this->getChild($id);
  227. $next = $this->children[$child->id()]['next'];
  228. if (is_null($next)) {
  229. throw new ChildNotFoundException("Child '$id' next not found in this node.");
  230. }
  231. return $this->getChild($next);
  232. }
  233. /**
  234. * Attempts to get the previous child.
  235. *
  236. * @param int $id
  237. * @return AbstractNode
  238. * @uses $this->getChild()
  239. * @throws ChildNotFoundException
  240. */
  241. public function previousChild(int $id): AbstractNode
  242. {
  243. $child = $this->getchild($id);
  244. $next = $this->children[$child->id()]['prev'];
  245. if (is_null($next)) {
  246. throw new ChildNotFoundException("Child '$id' previous not found in this node.");
  247. }
  248. return $this->getChild($next);
  249. }
  250. /**
  251. * Checks if the given node id is a child of the
  252. * current node.
  253. *
  254. * @param int $id
  255. * @return bool
  256. */
  257. public function isChild(int $id): bool
  258. {
  259. foreach ($this->children as $childId => $child) {
  260. if ($id == $childId) {
  261. return true;
  262. }
  263. }
  264. return false;
  265. }
  266. /**
  267. * Removes the child with id $childId and replace it with the new child
  268. * $newChild.
  269. *
  270. * @param int $childId
  271. * @param AbstractNode $newChild
  272. * @throws ChildNotFoundException
  273. * @return void
  274. */
  275. public function replaceChild(int $childId, AbstractNode $newChild): void
  276. {
  277. $oldChild = $this->children[$childId];
  278. $newChild->prev = $oldChild['prev'];
  279. $newChild->next = $oldChild['next'];
  280. $keys = array_keys($this->children);
  281. $index = array_search($childId, $keys, true);
  282. $keys[$index] = $newChild->id();
  283. $this->children = array_combine($keys, $this->children);
  284. $this->children[$newChild->id()] = array(
  285. 'prev' => $oldChild['prev'],
  286. 'node' => $newChild,
  287. 'next' => $oldChild['next']
  288. );
  289. // chnge previous child id to new child
  290. if ($oldChild['prev'] && isset($this->children[$newChild->prev])) {
  291. $this->children[$oldChild['prev']]['next'] = $newChild->id();
  292. }
  293. // change next child id to new child
  294. if ($oldChild['next'] && isset($this->children[$newChild->next])) {
  295. $this->children[$oldChild['next']]['prev'] = $newChild->id();
  296. }
  297. // remove old child
  298. unset($this->children[$childId]);
  299. // clean out cache
  300. $this->clear();
  301. }
  302. /**
  303. * Shortcut to return the first child.
  304. *
  305. * @return AbstractNode
  306. * @uses $this->getChild()
  307. * @throws ChildNotFoundException
  308. */
  309. public function firstChild(): AbstractNode
  310. {
  311. if (count($this->children) == 0) {
  312. // no children
  313. throw new ChildNotFoundException("No children found in node.");
  314. }
  315. reset($this->children);
  316. $key = (int) key($this->children);
  317. return $this->getChild($key);
  318. }
  319. /**
  320. * Attempts to get the last child.
  321. *
  322. * @return AbstractNode
  323. * @uses $this->getChild()
  324. * @throws ChildNotFoundException
  325. */
  326. public function lastChild(): AbstractNode
  327. {
  328. if (count($this->children) == 0) {
  329. // no children
  330. throw new ChildNotFoundException("No children found in node.");
  331. }
  332. end($this->children);
  333. $key = key($this->children);
  334. return $this->getChild($key);
  335. }
  336. /**
  337. * Checks if the given node id is a descendant of the
  338. * current node.
  339. *
  340. * @param int $id
  341. * @return bool
  342. */
  343. public function isDescendant(int $id): bool
  344. {
  345. if ($this->isChild($id)) {
  346. return true;
  347. }
  348. foreach ($this->children as $childId => $child) {
  349. /** @var InnerNode $node */
  350. $node = $child['node'];
  351. if ($node instanceof InnerNode &&
  352. $node->hasChildren() &&
  353. $node->isDescendant($id)
  354. ) {
  355. return true;
  356. }
  357. }
  358. return false;
  359. }
  360. /**
  361. * Sets the parent node.
  362. *
  363. * @param InnerNode $parent
  364. * @return AbstractNode
  365. * @throws CircularException
  366. * @chainable
  367. */
  368. public function setParent(InnerNode $parent): AbstractNode
  369. {
  370. // check integrity
  371. if ($this->isDescendant($parent->id())) {
  372. throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.');
  373. }
  374. // clear cache
  375. $this->clear();
  376. return parent::setParent($parent);
  377. }
  378. }