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