dokuwiki/inc/TreeBuilder/AbstractBuilder.php
<?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());
}
}