aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/inc/TreeBuilder/PageTreeBuilder.php
blob: e6e9c97eb49154e393568ca481f4d475bfb7c02d (plain) (blame)
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
<?php

namespace dokuwiki\TreeBuilder;

use dokuwiki\File\PageResolver;
use dokuwiki\TreeBuilder\Node\AbstractNode;
use dokuwiki\TreeBuilder\Node\Top;
use dokuwiki\TreeBuilder\Node\WikiNamespace;
use dokuwiki\TreeBuilder\Node\WikiPage;
use dokuwiki\TreeBuilder\Node\WikiStartpage;
use dokuwiki\Utf8\PhpString;

/**
 * A tree builder for wiki pages and namespaces
 *
 * This replace the classic search_* functions approach and provides a way to create a traversable tree
 * of wiki pages and namespaces.
 *
 * The created hierarchy can either use WikiNamespace nodes or represent namespaces as WikiPage nodes
 * associated with the namespace's start page.
 */
class PageTreeBuilder extends AbstractBuilder
{
    /** @var array Used to remember already seen start pages */
    protected array $startpages = [];

    /** @var int Return WikiPage(startpage) instead of WikiNamespace(id) for namespaces */
    public const FLAG_NS_AS_STARTPAGE = 1;

    /** @var int Do not return Namespaces, will also disable recursion */
    public const FLAG_NO_NS = 2;

    /** @var int Do not return pages */
    public const FLAG_NO_PAGES = 4;

    /** @var int Do not filter out hidden pages */
    public const FLAG_KEEP_HIDDEN = 8;

    /** @var int The given namespace should be added as top element */
    public const FLAG_SELF_TOP = 16;

    /** @var string The top level namespace to iterate over */
    protected string $namespace;

    /** @var int The maximum depth to iterate into, -1 for infinite */
    protected int $maxdepth;


    /**
     * Constructor
     *
     * @param string $namespace The namespace to start from
     * @param int $maxdepth The maximum depth to iterate into, -1 for infinite
     */
    public function __construct(string $namespace, int $maxdepth = -1)
    {
        $this->namespace = $namespace;
        $this->maxdepth = $maxdepth;
    }

    /** @inheritdoc */
    public function generate(): void
    {
        $this->generated = true;

        $this->top = new Top();

        // add directly to top or add the namespace under the top element?
        if ($this->hasFlag(self::FLAG_SELF_TOP)) {
            $parent = $this->createNamespaceNode($this->namespace, noNS($this->namespace));
            $parent->setParent($this->top);
        } else {
            if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) {
                // do not add the namespace's own startpage in this mode
                $this->startpages[$this->getStartpage($this->namespace)] = 1;
            }

            $parent = $this->top;
        }

        // if FLAG_SELF_TOP, we need to run a recursion decision on the parent
        if ($parent instanceof Top || $this->applyRecursionDecision($parent, 0)) {
            $dir = $this->namespacePath($this->namespace);
            $this->createHierarchy($parent, $dir, $this->maxdepth);
        }

        // if FLAG_SELF_TOP, we need to add the parent to the top
        if (!$parent instanceof Top) {
            $this->addNodeToHierarchy($this->top, $parent);
        }
    }

    /**
     * Recursive function to create the page hierarchy
     *
     * @param AbstractNode $parent results are added as children to this element
     * @param string $dir The directory relative to the page directory
     * @param int $depth Current depth, recursion stops at 0
     * @return void
     */
    protected function createHierarchy(AbstractNode $parent, string $dir, int $depth)
    {
        // Process namespaces (subdirectories)
        if ($this->hasNotFlag(self::FLAG_NO_NS)) {
            $this->processNamespaces($parent, $dir, $depth);
        }

        // Process pages (files)
        if ($this->hasNotFlag(self::FLAG_NO_PAGES)) {
            $this->processPages($parent, $dir);
        }
    }

    /**
     * Process namespaces (subdirectories) and add them to the hierarchy
     *
     * @param AbstractNode $parent Parent node to add children to
     * @param string $dir Current directory path
     * @param int $depth Current depth level
     * @return void
     */
    protected function processNamespaces(AbstractNode $parent, string $dir, int $depth)
    {
        global $conf;
        $base = $conf['datadir'] . '/';

        $dirs = glob($base . $dir . '/*', GLOB_ONLYDIR);
        foreach ($dirs as $subdir) {
            $subdir = basename($subdir);
            $id = pathID($dir . '/' . $subdir);

            $node = $this->createNamespaceNode($id, $subdir);

            // Recurse into subdirectory if depth and filter allows
            if ($depth !== 0 && $this->applyRecursionDecision($node, $this->maxdepth - $depth)) {
                $this->createHierarchy($node, $dir . '/' . $subdir, $depth - 1);
            }

            // Add to hierarchy
            $this->addNodeToHierarchy($parent, $node);
        }
    }

    /**
     * Create a namespace node based on the flags
     *
     * @param string $id
     * @param string $title
     * @return AbstractNode
     */
    protected function createNamespaceNode(string $id, string $title): AbstractNode
    {
        if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) {
            $ns = $id;
            $id = $this->getStartpage($id); // use the start page for the namespace
            $this->startpages[$id] = 1; // mark as seen
            $node = new WikiStartpage($id, $title, $ns);
        } else {
            $node = new WikiNamespace($id, $title);
        }
        return $node;
    }

    /**
     * Process pages (files) and add them to the hierarchy
     *
     * @param AbstractNode $parent Parent node to add children to
     * @param string $dir Current directory path
     * @return void
     */
    protected function processPages(AbstractNode $parent, string $dir)
    {
        global $conf;
        $base = $conf['datadir'] . '/';

        $files = glob($base . $dir . '/*.txt');
        foreach ($files as $file) {
            $file = basename($file);
            $id = pathID($dir . '/' . $file);

            // Skip already shown start pages
            if (isset($this->startpages[$id])) {
                continue;
            }

            $page = new WikiPage($id, $file);

            // Add to hierarchy
            $this->addNodeToHierarchy($parent, $page);
        }
    }

    /**
     * Run custom node processor and add it to the hierarchy
     *
     * @param AbstractNode $parent Parent node
     * @param AbstractNode $node Node to add
     * @return void
     */
    protected function addNodeToHierarchy(AbstractNode $parent, AbstractNode $node): void
    {
        $node->setParent($parent); // set the parent even when not added, yet
        $node = $this->applyNodeProcessor($node);
        if ($node instanceof AbstractNode) {
            $parent->addChild($node);
        }
    }

    /**
     * Get the start page for the given namespace
     *
     * @param string $ns The namespace to get the start page for
     * @return string The start page id
     */
    protected function getStartpage(string $ns): string
    {
        $id = $ns . ':';
        return (new PageResolver(''))->resolveId($id);
    }

    /**
     * Get the file path for the given namespace relative to the page directory
     *
     * @param string $namespace
     * @return string
     */
    protected function namespacePath(string $namespace): string
    {
        global $conf;

        $base = $conf['datadir'] . '/';
        $dir = wikiFN($namespace . ':xxx');
        $dir = substr($dir, strlen($base));
        $dir = dirname($dir); // remove the 'xxx' part
        if($dir === '.') $dir = ''; // dirname returns '.' for root namespace
        return $dir;
    }

    /** @inheritdoc */
    protected function applyRecursionDecision(AbstractNode $node, int $depth): bool
    {
        // automatically skip hidden elements unless disabled by flag
        if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) {
            return false;
        }
        return parent::applyRecursionDecision($node, $depth);
    }

    /** @inheritdoc */
    protected function applyNodeProcessor(AbstractNode $node): ?AbstractNode
    {
        // automatically skip hidden elements unless disabled by flag
        if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) {
            return null;
        }
        return parent::applyNodeProcessor($node);
    }


}