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