1<?php 2 3namespace dokuwiki\TreeBuilder; 4 5 6use dokuwiki\test\mock\Doku_Renderer; 7use dokuwiki\TreeBuilder\Node\AbstractNode; 8use dokuwiki\TreeBuilder\Node\ExternalLink; 9use dokuwiki\TreeBuilder\Node\Top; 10 11/** 12 * Abstract class to generate a tree 13 */ 14abstract class AbstractBuilder 15{ 16 protected bool $generated = false; 17 18 /** @var AbstractNode[] flat list of all nodes the generator found */ 19 protected array $nodes = []; 20 21 /** @var Top top level element to access the tree */ 22 protected Top $top; 23 24 /** @var callable|null A callback to modify or filter out nodes */ 25 protected $nodeProcessor; 26 27 /** @var callable|null A callback to decide if recursion should happen */ 28 protected $recursionDecision; 29 30 /** 31 * @var int configuration flags 32 */ 33 protected int $flags = 0; 34 35 /** 36 * Generate the page tree. Needs to be called once the object is created. 37 * 38 * Sets the $generated flag to true. 39 * 40 * @return void 41 */ 42 abstract public function generate(): void; 43 44 /** 45 * Set a callback to set additional properties on the nodes 46 * 47 * The callback receives a Node as parameter and must return a Node. 48 * If the callback returns null, the node will not be added to the tree. 49 * The callback may use the setProperty() method to set additional properties on the node. 50 * The callback can also return a completely different node, which will be added to the tree instead 51 * of the original node. 52 * 53 * @param callable|null $builder A callback to set additional properties on the nodes 54 */ 55 public function setNodeProcessor(?callable $builder): void 56 { 57 if ($builder !== null && !is_callable($builder)) { 58 throw new \InvalidArgumentException('Property builder must be callable'); 59 } 60 $this->nodeProcessor = $builder; 61 } 62 63 /** 64 * Set a callback to decide if recursion should happen 65 * 66 * The callback receives a Node as parameter and the current recursion depth. 67 * The node will NOT have it's children set. 68 * The callback must return true to have any children added, false to skip them. 69 * 70 * @param callable|null $filter 71 * @return void 72 */ 73 public function setRecursionDecision(?callable $filter): void 74 { 75 if ($filter !== null && !is_callable($filter)) { 76 throw new \InvalidArgumentException('Recursion-filter must be callable'); 77 } 78 $this->recursionDecision = $filter; 79 } 80 81 /** 82 * Add a configuration flag 83 * 84 * @param int $flag 85 * @return void 86 */ 87 public function addFlag(int $flag): void 88 { 89 $this->flags |= $flag; 90 } 91 92 /** 93 * Check if a flag is set 94 * 95 * @param int $flag 96 * @return bool 97 */ 98 public function hasFlag(int $flag): bool 99 { 100 return ($this->flags & $flag) === $flag; 101 } 102 103 /** 104 * Check if a flag is NOT set 105 * 106 * @param int $flag 107 * @return bool 108 */ 109 public function hasNotFlag(int $flag): bool 110 { 111 return ($this->flags & $flag) !== $flag; 112 } 113 114 /** 115 * Remove a configuration flag 116 * 117 * @param int $flag 118 * @return void 119 */ 120 public function removeFlag(int $flag): void 121 { 122 $this->flags &= ~$flag; 123 } 124 125 /** 126 * Access the top element 127 * 128 * Use it's children to iterate over the page hierarchy 129 * 130 * @return Top 131 */ 132 public function getTop(): Top 133 { 134 if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 135 return $this->top; 136 } 137 138 /** 139 * Get a flat list of all nodes in the tree 140 * 141 * This is a cached version of top->getDescendants() with the ID as key of the returned array. 142 * 143 * @return AbstractNode[] 144 */ 145 public function getAll(): array 146 { 147 if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 148 if (empty($this->nodes)) { 149 $this->nodes = []; 150 foreach ($this->top->getDescendants() as $node) { 151 $this->nodes[$node->getId()] = $node; 152 } 153 } 154 155 return $this->nodes; 156 } 157 158 /** 159 * Get a flat list of all nodes that do NOT have children 160 * 161 * @return AbstractNode[] 162 */ 163 public function getLeaves(): array 164 { 165 if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 166 return array_filter($this->getAll(), function ($page) { 167 return !$page->getChildren(); 168 }); 169 } 170 171 /** 172 * Get a flat list of all nodes that DO have children 173 * 174 * @return AbstractNode[] 175 */ 176 public function getBranches(): array 177 { 178 if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 179 return array_filter($this->getAll(), function ($page) { 180 return !!$page->getChildren(); 181 }); 182 } 183 184 /** 185 * Sort the tree 186 * 187 * The given comparator function will be called with two nodes as arguments and needs to 188 * return an integer less than, equal to, or greater than zero if the first argument is considered 189 * to be respectively less than, equal to, or greater than the second. 190 * 191 * Pass in one of the TreeSort comparators or your own. 192 * 193 * @param callable $comparator 194 * @return void 195 */ 196 public function sort(callable $comparator): void 197 { 198 if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 199 $this->top->sort($comparator); 200 $this->nodes = []; // reset the cache 201 } 202 203 /** 204 * Render the tree on the given renderer 205 * 206 * This is mostly an example implementation. You probably want to implement your own. 207 * 208 * @param Doku_Renderer $R The current renderer 209 * @param AbstractNode $top The node to start from, use null to start from the top node 210 * @param int $level current nesting level, starting at 1 211 * @return void 212 */ 213 public function render(Doku_Renderer $R, $top = null, $level = 1): void 214 { 215 if ($top === null) $top = $this->getTop(); 216 217 $R->listu_open(); 218 foreach ($top->getChildren() as $node) { 219 $R->listitem_open(1, $node->hasChildren()); 220 $R->listcontent_open(); 221 if (is_a($node, ExternalLink::class)) { 222 $R->externallink($node->getId(), $node->getTitle()); 223 } else { 224 $R->internallink($node->getId(), $node->getTitle()); 225 } 226 $R->listcontent_close(); 227 if ($node->hasChildren()) { 228 $this->render($R, $node, $level + 1); 229 } 230 $R->listitem_close(); 231 } 232 $R->listu_close(); 233 } 234 235 /** 236 * @param AbstractNode $node 237 * @return AbstractNode|null 238 */ 239 protected function applyNodeProcessor(AbstractNode $node): ?AbstractNode 240 { 241 if ($this->nodeProcessor === null) return $node; 242 $result = call_user_func($this->nodeProcessor, $node); 243 if (!$result instanceof AbstractNode) return null; 244 return $result; 245 } 246 247 /** 248 * @param AbstractNode $node 249 * @return bool should children be added? 250 */ 251 protected function applyRecursionDecision(AbstractNode $node, int $depth): bool 252 { 253 if ($this->recursionDecision === null) return true; 254 return (bool)call_user_func($this->recursionDecision, $node, $depth); 255 } 256 257 /** 258 * "prints" the tree 259 * 260 * @return array 261 */ 262 public function __toString(): string 263 { 264 return join("\n", $this->getAll()); 265 } 266} 267