xref: /dokuwiki/inc/TreeBuilder/AbstractBuilder.php (revision a8e9ec06570f392919a588ec98f916aab604b5a1)
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