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