178a26510SAndreas Gohr<?php 278a26510SAndreas Gohr 378a26510SAndreas Gohrnamespace dokuwiki\TreeBuilder; 478a26510SAndreas Gohr 578a26510SAndreas Gohruse dokuwiki\test\mock\Doku_Renderer; 678a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\AbstractNode; 778a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\ExternalLink; 878a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\Top; 978a26510SAndreas Gohr 1078a26510SAndreas Gohr/** 1178a26510SAndreas Gohr * Abstract class to generate a tree 1278a26510SAndreas Gohr */ 1378a26510SAndreas Gohrabstract class AbstractBuilder 1478a26510SAndreas Gohr{ 1578a26510SAndreas Gohr protected bool $generated = false; 1678a26510SAndreas Gohr 1778a26510SAndreas Gohr /** @var AbstractNode[] flat list of all nodes the generator found */ 1878a26510SAndreas Gohr protected array $nodes = []; 1978a26510SAndreas Gohr 2078a26510SAndreas Gohr /** @var Top top level element to access the tree */ 2178a26510SAndreas Gohr protected Top $top; 2278a26510SAndreas Gohr 2378a26510SAndreas Gohr /** @var callable|null A callback to modify or filter out nodes */ 2478a26510SAndreas Gohr protected $nodeProcessor; 2578a26510SAndreas Gohr 2678a26510SAndreas Gohr /** @var callable|null A callback to decide if recursion should happen */ 2778a26510SAndreas Gohr protected $recursionDecision; 2878a26510SAndreas Gohr 2978a26510SAndreas Gohr /** 3078a26510SAndreas Gohr * @var int configuration flags 3178a26510SAndreas Gohr */ 3278a26510SAndreas Gohr protected int $flags = 0; 3378a26510SAndreas Gohr 3478a26510SAndreas Gohr /** 3578a26510SAndreas Gohr * Generate the page tree. Needs to be called once the object is created. 3678a26510SAndreas Gohr * 3778a26510SAndreas Gohr * Sets the $generated flag to true. 3878a26510SAndreas Gohr * 3978a26510SAndreas Gohr * @return void 4078a26510SAndreas Gohr */ 4178a26510SAndreas Gohr abstract public function generate(): void; 4278a26510SAndreas Gohr 4378a26510SAndreas Gohr /** 4478a26510SAndreas Gohr * Set a callback to set additional properties on the nodes 4578a26510SAndreas Gohr * 4678a26510SAndreas Gohr * The callback receives a Node as parameter and must return a Node. 4778a26510SAndreas Gohr * If the callback returns null, the node will not be added to the tree. 4878a26510SAndreas Gohr * The callback may use the setProperty() method to set additional properties on the node. 4978a26510SAndreas Gohr * The callback can also return a completely different node, which will be added to the tree instead 5078a26510SAndreas Gohr * of the original node. 5178a26510SAndreas Gohr * 5278a26510SAndreas Gohr * @param callable|null $builder A callback to set additional properties on the nodes 5378a26510SAndreas Gohr */ 5478a26510SAndreas Gohr public function setNodeProcessor(?callable $builder): void 5578a26510SAndreas Gohr { 5678a26510SAndreas Gohr if ($builder !== null && !is_callable($builder)) { 5778a26510SAndreas Gohr throw new \InvalidArgumentException('Property builder must be callable'); 5878a26510SAndreas Gohr } 5978a26510SAndreas Gohr $this->nodeProcessor = $builder; 6078a26510SAndreas Gohr } 6178a26510SAndreas Gohr 6278a26510SAndreas Gohr /** 6378a26510SAndreas Gohr * Set a callback to decide if recursion should happen 6478a26510SAndreas Gohr * 6578a26510SAndreas Gohr * The callback receives a Node as parameter and the current recursion depth. 6678a26510SAndreas Gohr * The node will NOT have it's children set. 6778a26510SAndreas Gohr * The callback must return true to have any children added, false to skip them. 6878a26510SAndreas Gohr * 6978a26510SAndreas Gohr * @param callable|null $filter 7078a26510SAndreas Gohr * @return void 7178a26510SAndreas Gohr */ 7278a26510SAndreas Gohr public function setRecursionDecision(?callable $filter): void 7378a26510SAndreas Gohr { 7478a26510SAndreas Gohr if ($filter !== null && !is_callable($filter)) { 7578a26510SAndreas Gohr throw new \InvalidArgumentException('Recursion-filter must be callable'); 7678a26510SAndreas Gohr } 7778a26510SAndreas Gohr $this->recursionDecision = $filter; 7878a26510SAndreas Gohr } 7978a26510SAndreas Gohr 8078a26510SAndreas Gohr /** 8178a26510SAndreas Gohr * Add a configuration flag 8278a26510SAndreas Gohr * 8378a26510SAndreas Gohr * @param int $flag 8478a26510SAndreas Gohr * @return void 8578a26510SAndreas Gohr */ 8678a26510SAndreas Gohr public function addFlag(int $flag): void 8778a26510SAndreas Gohr { 8878a26510SAndreas Gohr $this->flags |= $flag; 8978a26510SAndreas Gohr } 9078a26510SAndreas Gohr 9178a26510SAndreas Gohr /** 9278a26510SAndreas Gohr * Check if a flag is set 9378a26510SAndreas Gohr * 9478a26510SAndreas Gohr * @param int $flag 9578a26510SAndreas Gohr * @return bool 9678a26510SAndreas Gohr */ 9778a26510SAndreas Gohr public function hasFlag(int $flag): bool 9878a26510SAndreas Gohr { 9978a26510SAndreas Gohr return ($this->flags & $flag) === $flag; 10078a26510SAndreas Gohr } 10178a26510SAndreas Gohr 10278a26510SAndreas Gohr /** 10378a26510SAndreas Gohr * Check if a flag is NOT set 10478a26510SAndreas Gohr * 10578a26510SAndreas Gohr * @param int $flag 10678a26510SAndreas Gohr * @return bool 10778a26510SAndreas Gohr */ 10878a26510SAndreas Gohr public function hasNotFlag(int $flag): bool 10978a26510SAndreas Gohr { 11078a26510SAndreas Gohr return ($this->flags & $flag) !== $flag; 11178a26510SAndreas Gohr } 11278a26510SAndreas Gohr 11378a26510SAndreas Gohr /** 11478a26510SAndreas Gohr * Remove a configuration flag 11578a26510SAndreas Gohr * 11678a26510SAndreas Gohr * @param int $flag 11778a26510SAndreas Gohr * @return void 11878a26510SAndreas Gohr */ 11978a26510SAndreas Gohr public function removeFlag(int $flag): void 12078a26510SAndreas Gohr { 12178a26510SAndreas Gohr $this->flags &= ~$flag; 12278a26510SAndreas Gohr } 12378a26510SAndreas Gohr 12478a26510SAndreas Gohr /** 12578a26510SAndreas Gohr * Access the top element 12678a26510SAndreas Gohr * 12778a26510SAndreas Gohr * Use it's children to iterate over the page hierarchy 12878a26510SAndreas Gohr * 12978a26510SAndreas Gohr * @return Top 13078a26510SAndreas Gohr */ 13178a26510SAndreas Gohr public function getTop(): Top 13278a26510SAndreas Gohr { 13378a26510SAndreas Gohr if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 13478a26510SAndreas Gohr return $this->top; 13578a26510SAndreas Gohr } 13678a26510SAndreas Gohr 13778a26510SAndreas Gohr /** 13878a26510SAndreas Gohr * Get a flat list of all nodes in the tree 13978a26510SAndreas Gohr * 14078a26510SAndreas Gohr * This is a cached version of top->getDescendants() with the ID as key of the returned array. 14178a26510SAndreas Gohr * 14278a26510SAndreas Gohr * @return AbstractNode[] 14378a26510SAndreas Gohr */ 14478a26510SAndreas Gohr public function getAll(): array 14578a26510SAndreas Gohr { 14678a26510SAndreas Gohr if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 147*a8e9ec06SAndreas Gohr if ($this->nodes === []) { 14878a26510SAndreas Gohr $this->nodes = []; 14978a26510SAndreas Gohr foreach ($this->top->getDescendants() as $node) { 15078a26510SAndreas Gohr $this->nodes[$node->getId()] = $node; 15178a26510SAndreas Gohr } 15278a26510SAndreas Gohr } 15378a26510SAndreas Gohr 15478a26510SAndreas Gohr return $this->nodes; 15578a26510SAndreas Gohr } 15678a26510SAndreas Gohr 15778a26510SAndreas Gohr /** 15878a26510SAndreas Gohr * Get a flat list of all nodes that do NOT have children 15978a26510SAndreas Gohr * 16078a26510SAndreas Gohr * @return AbstractNode[] 16178a26510SAndreas Gohr */ 16278a26510SAndreas Gohr public function getLeaves(): array 16378a26510SAndreas Gohr { 16478a26510SAndreas Gohr if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 165*a8e9ec06SAndreas Gohr return array_filter($this->getAll(), fn($page) => !$page->getChildren()); 16678a26510SAndreas Gohr } 16778a26510SAndreas Gohr 16878a26510SAndreas Gohr /** 16978a26510SAndreas Gohr * Get a flat list of all nodes that DO have children 17078a26510SAndreas Gohr * 17178a26510SAndreas Gohr * @return AbstractNode[] 17278a26510SAndreas Gohr */ 17378a26510SAndreas Gohr public function getBranches(): array 17478a26510SAndreas Gohr { 17578a26510SAndreas Gohr if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 176*a8e9ec06SAndreas Gohr return array_filter($this->getAll(), fn($page) => (bool) $page->getChildren()); 17778a26510SAndreas Gohr } 17878a26510SAndreas Gohr 17978a26510SAndreas Gohr /** 18078a26510SAndreas Gohr * Sort the tree 18178a26510SAndreas Gohr * 18278a26510SAndreas Gohr * The given comparator function will be called with two nodes as arguments and needs to 18378a26510SAndreas Gohr * return an integer less than, equal to, or greater than zero if the first argument is considered 18478a26510SAndreas Gohr * to be respectively less than, equal to, or greater than the second. 18578a26510SAndreas Gohr * 18678a26510SAndreas Gohr * Pass in one of the TreeSort comparators or your own. 18778a26510SAndreas Gohr * 18878a26510SAndreas Gohr * @param callable $comparator 18978a26510SAndreas Gohr * @return void 19078a26510SAndreas Gohr */ 19178a26510SAndreas Gohr public function sort(callable $comparator): void 19278a26510SAndreas Gohr { 19378a26510SAndreas Gohr if (!$this->generated) throw new \RuntimeException('need to call generate() first'); 19478a26510SAndreas Gohr $this->top->sort($comparator); 19578a26510SAndreas Gohr $this->nodes = []; // reset the cache 19678a26510SAndreas Gohr } 19778a26510SAndreas Gohr 19878a26510SAndreas Gohr /** 19978a26510SAndreas Gohr * Render the tree on the given renderer 20078a26510SAndreas Gohr * 20178a26510SAndreas Gohr * This is mostly an example implementation. You probably want to implement your own. 20278a26510SAndreas Gohr * 20378a26510SAndreas Gohr * @param Doku_Renderer $R The current renderer 20478a26510SAndreas Gohr * @param AbstractNode $top The node to start from, use null to start from the top node 20578a26510SAndreas Gohr * @param int $level current nesting level, starting at 1 20678a26510SAndreas Gohr * @return void 20778a26510SAndreas Gohr */ 20878a26510SAndreas Gohr public function render(Doku_Renderer $R, $top = null, $level = 1): void 20978a26510SAndreas Gohr { 21078a26510SAndreas Gohr if ($top === null) $top = $this->getTop(); 21178a26510SAndreas Gohr 21278a26510SAndreas Gohr $R->listu_open(); 21378a26510SAndreas Gohr foreach ($top->getChildren() as $node) { 21478a26510SAndreas Gohr $R->listitem_open(1, $node->hasChildren()); 21578a26510SAndreas Gohr $R->listcontent_open(); 216*a8e9ec06SAndreas Gohr if ($node instanceof ExternalLink) { 21778a26510SAndreas Gohr $R->externallink($node->getId(), $node->getTitle()); 21878a26510SAndreas Gohr } else { 21978a26510SAndreas Gohr $R->internallink($node->getId(), $node->getTitle()); 22078a26510SAndreas Gohr } 22178a26510SAndreas Gohr $R->listcontent_close(); 22278a26510SAndreas Gohr if ($node->hasChildren()) { 22378a26510SAndreas Gohr $this->render($R, $node, $level + 1); 22478a26510SAndreas Gohr } 22578a26510SAndreas Gohr $R->listitem_close(); 22678a26510SAndreas Gohr } 22778a26510SAndreas Gohr $R->listu_close(); 22878a26510SAndreas Gohr } 22978a26510SAndreas Gohr 23078a26510SAndreas Gohr /** 23178a26510SAndreas Gohr * @param AbstractNode $node 23278a26510SAndreas Gohr * @return AbstractNode|null 23378a26510SAndreas Gohr */ 23478a26510SAndreas Gohr protected function applyNodeProcessor(AbstractNode $node): ?AbstractNode 23578a26510SAndreas Gohr { 23678a26510SAndreas Gohr if ($this->nodeProcessor === null) return $node; 23778a26510SAndreas Gohr $result = call_user_func($this->nodeProcessor, $node); 23878a26510SAndreas Gohr if (!$result instanceof AbstractNode) return null; 23978a26510SAndreas Gohr return $result; 24078a26510SAndreas Gohr } 24178a26510SAndreas Gohr 24278a26510SAndreas Gohr /** 24378a26510SAndreas Gohr * @param AbstractNode $node 24478a26510SAndreas Gohr * @return bool should children be added? 24578a26510SAndreas Gohr */ 24678a26510SAndreas Gohr protected function applyRecursionDecision(AbstractNode $node, int $depth): bool 24778a26510SAndreas Gohr { 24878a26510SAndreas Gohr if ($this->recursionDecision === null) return true; 24978a26510SAndreas Gohr return (bool)call_user_func($this->recursionDecision, $node, $depth); 25078a26510SAndreas Gohr } 25178a26510SAndreas Gohr 25278a26510SAndreas Gohr /** 25378a26510SAndreas Gohr * "prints" the tree 25478a26510SAndreas Gohr * 25578a26510SAndreas Gohr * @return array 25678a26510SAndreas Gohr */ 25778a26510SAndreas Gohr public function __toString(): string 25878a26510SAndreas Gohr { 259*a8e9ec06SAndreas Gohr return implode("\n", $this->getAll()); 26078a26510SAndreas Gohr } 26178a26510SAndreas Gohr} 262