xref: /plugin/simplenavi/syntax.php (revision 10f2bde61ac988c176739e1cd98b79140baaed9e)
1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4use dokuwiki\File\PageResolver;
5use dokuwiki\TreeBuilder\Node\AbstractNode;
6use dokuwiki\TreeBuilder\Node\WikiNamespace;
7use dokuwiki\TreeBuilder\Node\WikiStartpage;
8use dokuwiki\TreeBuilder\PageTreeBuilder;
9use dokuwiki\TreeBuilder\TreeSort;
10
11/**
12 * DokuWiki Plugin simplenavi (Syntax Component)
13 *
14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
15 * @author  Andreas Gohr <gohr@cosmocode.de>
16 */
17class syntax_plugin_simplenavi extends SyntaxPlugin
18{
19    protected string $ns;
20    protected string $currentID;
21    protected bool $usetitle;
22    protected string $sort;
23    protected bool $home;
24    protected int $peek = 0;
25    protected bool $filter = false;
26
27    /** @inheritdoc */
28    public function getType()
29    {
30        return 'substition';
31    }
32
33    /** @inheritdoc */
34    public function getPType()
35    {
36        return 'block';
37    }
38
39    /** @inheritdoc */
40    public function getSort()
41    {
42        return 155;
43    }
44
45    /** @inheritdoc */
46    public function connectTo($mode)
47    {
48        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
49    }
50
51    /** @inheritdoc */
52    public function handle($match, $state, $pos, Doku_Handler $handler)
53    {
54        return explode(' ', substr($match, 13, -2));
55    }
56
57    /** @inheritdoc */
58    public function render($format, Doku_Renderer $renderer, $data)
59    {
60        if ($format != 'xhtml') return false;
61        $renderer->nocache();
62
63        global $INFO;
64
65        // first data is namespace, rest is options
66        $ns = array_shift($data);
67        if ($ns && $ns[0] === '.') {
68            // resolve relative to current page
69            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
70        } else {
71            $ns = cleanID($ns);
72        }
73
74        $this->initState(
75            $ns,
76            $INFO['id'],
77            (bool)$this->getConf('usetitle'),
78            $this->getConf('sort'),
79            in_array('home', $data),
80            $this->getConf('peek', 0),
81            in_array('filter', $data)
82        );
83
84        $tree = $this->getTree();
85
86        $class = 'plugin__simplenavi';
87        if ($this->filter) {
88            $class .= ' plugin__simplenavi_filter';
89        }
90
91        $renderer->doc .= '<div class="' . $class . '">';
92        $this->renderTree($renderer, $tree->getTop());
93        $renderer->doc .= '</div>';
94
95        return true;
96    }
97
98    /**
99     * Initialize the configuration state of the plugin
100     *
101     * Also used in testing
102     */
103    public function initState(
104        string $ns,
105        string $currentID,
106        bool   $usetitle,
107        string $sort,
108        bool   $home,
109        int    $peek = 0,
110        bool   $filter = false
111    )
112    {
113        $this->ns = $ns;
114        $this->currentID = $currentID;
115        $this->usetitle = $usetitle;
116        $this->sort = $sort;
117        $this->home = $home;
118        $this->peek = $peek;
119        $this->filter = $filter;
120    }
121
122    /**
123     * Create the tree
124     *
125     * @return PageTreeBuilder
126     */
127    protected function getTree(): PageTreeBuilder
128    {
129        $tree = new PageTreeBuilder($this->ns);
130        $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE);
131        if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP);
132        $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision']));
133        $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor']));
134        $tree->generate();
135
136        switch ($this->sort) {
137            case 'id':
138                $tree->sort(TreeSort::SORT_BY_ID);
139                break;
140            case 'title':
141                $tree->sort(TreeSort::SORT_BY_TITLE);
142                break;
143            case 'ns_id':
144                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID);
145                break;
146            default:
147                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE);
148                break;
149        }
150
151        return $tree;
152    }
153
154
155    /**
156     * Callback for the PageTreeBuilder to decide if we want to recurse into a node
157     *
158     * @param AbstractNode $node
159     * @param int $depth
160     * @return bool
161     */
162    protected function treeRecursionDecision(AbstractNode $node, int $depth): bool
163    {
164        if ($node instanceof WikiStartpage) {
165            $id = $node->getNs(); // use the namespace for startpages
166        } else {
167            $id = $node->getId();
168        }
169
170        $is_current = $this->isParent($this->currentID, $id);
171        $node->setProperty('is_current', $is_current);
172
173        // always recurse into the current page path
174        if ($is_current) return true;
175
176        // should we peek deeper to see if there's something readable?
177        if ($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ) {
178            return true;
179        }
180
181        return false;
182    }
183
184    /**
185     * Callback for the PageTreeBuilder to process a node
186     *
187     * @param AbstractNode $node
188     * @return AbstractNode|null
189     */
190    protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode
191    {
192        $perm = auth_quickaclcheck($node->getId());
193        $node->setProperty('permission', $perm);
194        $node->setTitle($this->getTitle($node->getId()));
195
196
197        if ($node->hasChildren()) {
198            // this node has children, we add it to the tree regardless of the permission
199            // permissions are checked again when rendering
200            return $node;
201        }
202
203        if ($perm < AUTH_READ) {
204            // no children, no permission. No need to add it to the tree
205            return null;
206        }
207
208        return $node;
209    }
210
211
212    /**
213     * Render the tree
214     *
215     * @param Doku_Renderer $R The current renderer
216     * @param AbstractNode $top The top node of the tree (use getTop() to get it)
217     * @param int $level current nesting level, starting at 1
218     * @return void
219     */
220    protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1)
221    {
222        $R->listu_open();
223        foreach ($top->getChildren() as $node) {
224            $isfolder = $node instanceof WikiNamespace;
225            $incurrent = $node->getProperty('is_current', false);
226
227            $R->listitem_open(1, $isfolder);
228            $R->listcontent_open();
229            if ($incurrent) $R->strong_open();
230
231            if (((int)$node->getProperty('permission', 0)) < AUTH_READ) {
232                $R->cdata($node->getTitle());
233            } else {
234                $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation');
235            }
236
237            if ($incurrent) $R->strong_close();
238            $R->listcontent_close();
239            if ($node->hasChildren()) {
240                $this->renderTree($R, $node, $level + 1);
241            }
242            $R->listitem_close();
243        }
244        $R->listu_close();
245    }
246
247    /**
248     * Check if the given parent ID is a parent of the child ID
249     *
250     * @param string $child
251     * @param string $parent
252     * @return bool
253     */
254    protected function isParent(string $child, string $parent)
255    {
256        // Empty parent is considered a parent of all pages
257        if ($parent === '') {
258            return true;
259        }
260
261        $child = explode(':', $child);
262        $parent = explode(':', $parent);
263        return array_slice($child, 0, count($parent)) === $parent;
264    }
265
266
267    /**
268     * Get the title for the given page ID
269     *
270     * @param string $id
271     * @return string
272     */
273    protected function getTitle($id)
274    {
275        global $conf;
276
277        if ($this->usetitle) {
278            $p = p_get_first_heading($id);
279            if (!empty($p)) return $p;
280        }
281
282        $p = noNS($id);
283        if ($p == $conf['start'] || !$p) {
284            $p = noNS(getNS($id));
285            if (!$p) {
286                return $conf['start'];
287            }
288        }
289        return $p;
290    }
291}
292