1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the league/commonmark package. 7 * 8 * (c) Colin O'Dell <colinodell@gmail.com> 9 * 10 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) 11 * - (c) John MacFarlane 12 * 13 * For the full copyright and license information, please view the LICENSE 14 * file that was distributed with this source code. 15 */ 16 17namespace League\CommonMark\Node; 18 19use Dflydev\DotAccessData\Data; 20use League\CommonMark\Exception\InvalidArgumentException; 21 22abstract class Node 23{ 24 /** @psalm-readonly */ 25 public Data $data; 26 27 /** @psalm-readonly-allow-private-mutation */ 28 protected int $depth = 0; 29 30 /** @psalm-readonly-allow-private-mutation */ 31 protected ?Node $parent = null; 32 33 /** @psalm-readonly-allow-private-mutation */ 34 protected ?Node $previous = null; 35 36 /** @psalm-readonly-allow-private-mutation */ 37 protected ?Node $next = null; 38 39 /** @psalm-readonly-allow-private-mutation */ 40 protected ?Node $firstChild = null; 41 42 /** @psalm-readonly-allow-private-mutation */ 43 protected ?Node $lastChild = null; 44 45 public function __construct() 46 { 47 $this->data = new Data([ 48 'attributes' => [], 49 ]); 50 } 51 52 public function previous(): ?Node 53 { 54 return $this->previous; 55 } 56 57 public function next(): ?Node 58 { 59 return $this->next; 60 } 61 62 public function parent(): ?Node 63 { 64 return $this->parent; 65 } 66 67 protected function setParent(?Node $node = null): void 68 { 69 $this->parent = $node; 70 $this->depth = $node === null ? 0 : $node->depth + 1; 71 } 72 73 /** 74 * Inserts the $sibling node after $this 75 */ 76 public function insertAfter(Node $sibling): void 77 { 78 $sibling->detach(); 79 $sibling->next = $this->next; 80 81 if ($sibling->next) { 82 $sibling->next->previous = $sibling; 83 } 84 85 $sibling->previous = $this; 86 $this->next = $sibling; 87 $sibling->setParent($this->parent); 88 89 if (! $sibling->next && $sibling->parent) { 90 $sibling->parent->lastChild = $sibling; 91 } 92 } 93 94 /** 95 * Inserts the $sibling node before $this 96 */ 97 public function insertBefore(Node $sibling): void 98 { 99 $sibling->detach(); 100 $sibling->previous = $this->previous; 101 102 if ($sibling->previous) { 103 $sibling->previous->next = $sibling; 104 } 105 106 $sibling->next = $this; 107 $this->previous = $sibling; 108 $sibling->setParent($this->parent); 109 110 if (! $sibling->previous && $sibling->parent) { 111 $sibling->parent->firstChild = $sibling; 112 } 113 } 114 115 public function replaceWith(Node $replacement): void 116 { 117 $replacement->detach(); 118 $this->insertAfter($replacement); 119 $this->detach(); 120 } 121 122 public function detach(): void 123 { 124 if ($this->previous) { 125 $this->previous->next = $this->next; 126 } elseif ($this->parent) { 127 $this->parent->firstChild = $this->next; 128 } 129 130 if ($this->next) { 131 $this->next->previous = $this->previous; 132 } elseif ($this->parent) { 133 $this->parent->lastChild = $this->previous; 134 } 135 136 $this->parent = null; 137 $this->next = null; 138 $this->previous = null; 139 $this->depth = 0; 140 } 141 142 public function hasChildren(): bool 143 { 144 return $this->firstChild !== null; 145 } 146 147 public function firstChild(): ?Node 148 { 149 return $this->firstChild; 150 } 151 152 public function lastChild(): ?Node 153 { 154 return $this->lastChild; 155 } 156 157 /** 158 * @return Node[] 159 */ 160 public function children(): iterable 161 { 162 $children = []; 163 for ($current = $this->firstChild; $current !== null; $current = $current->next) { 164 $children[] = $current; 165 } 166 167 return $children; 168 } 169 170 public function appendChild(Node $child): void 171 { 172 if ($this->lastChild) { 173 $this->lastChild->insertAfter($child); 174 } else { 175 $child->detach(); 176 $child->setParent($this); 177 $this->lastChild = $this->firstChild = $child; 178 } 179 } 180 181 /** 182 * Adds $child as the very first child of $this 183 */ 184 public function prependChild(Node $child): void 185 { 186 if ($this->firstChild) { 187 $this->firstChild->insertBefore($child); 188 } else { 189 $child->detach(); 190 $child->setParent($this); 191 $this->lastChild = $this->firstChild = $child; 192 } 193 } 194 195 /** 196 * Detaches all child nodes of given node 197 */ 198 public function detachChildren(): void 199 { 200 foreach ($this->children() as $children) { 201 $children->setParent(null); 202 } 203 204 $this->firstChild = $this->lastChild = null; 205 } 206 207 /** 208 * Replace all children of given node with collection of another 209 * 210 * @param iterable<Node> $children 211 */ 212 public function replaceChildren(iterable $children): void 213 { 214 $this->detachChildren(); 215 foreach ($children as $item) { 216 $this->appendChild($item); 217 } 218 } 219 220 public function getDepth(): int 221 { 222 return $this->depth; 223 } 224 225 public function walker(): NodeWalker 226 { 227 return new NodeWalker($this); 228 } 229 230 public function iterator(int $flags = 0): NodeIterator 231 { 232 return new NodeIterator($this, $flags); 233 } 234 235 /** 236 * Clone the current node and its children 237 * 238 * WARNING: This is a recursive function and should not be called on deeply-nested node trees! 239 */ 240 public function __clone() 241 { 242 // Cloned nodes are detached from their parents, siblings, and children 243 $this->parent = null; 244 $this->previous = null; 245 $this->next = null; 246 // But save a copy of the children since we'll need that in a moment 247 $children = $this->children(); 248 $this->detachChildren(); 249 250 // The original children get cloned and re-added 251 foreach ($children as $child) { 252 $this->appendChild(clone $child); 253 } 254 } 255 256 public static function assertInstanceOf(Node $node): void 257 { 258 if (! $node instanceof static) { 259 throw new InvalidArgumentException(\sprintf('Incompatible node type: expected %s, got %s', static::class, \get_class($node))); 260 } 261 } 262} 263