xref: /plugin/simplenavi/syntax.php (revision a2e567776f298dcdd7d4e640b1852bc3062ba4d3)
11169a1acSAndreas Gohr<?php
2d8ce5486SAndreas Gohr
3d418c031SAndreas Gohruse dokuwiki\Extension\SyntaxPlugin;
4d8ce5486SAndreas Gohruse dokuwiki\File\PageResolver;
5ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\AbstractNode;
6ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiNamespace;
7ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiStartpage;
8ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\PageTreeBuilder;
9ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\TreeSort;
10d8ce5486SAndreas Gohr
111169a1acSAndreas Gohr/**
121169a1acSAndreas Gohr * DokuWiki Plugin simplenavi (Syntax Component)
131169a1acSAndreas Gohr *
141169a1acSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
151169a1acSAndreas Gohr * @author  Andreas Gohr <gohr@cosmocode.de>
161169a1acSAndreas Gohr */
17d418c031SAndreas Gohrclass syntax_plugin_simplenavi extends SyntaxPlugin
18d8ce5486SAndreas Gohr{
194c5e7fe5SAndreas Gohr    protected string $ns;
204c5e7fe5SAndreas Gohr    protected string $currentID;
214c5e7fe5SAndreas Gohr    protected bool $usetitle;
224c5e7fe5SAndreas Gohr    protected string $sort;
234c5e7fe5SAndreas Gohr    protected bool $home;
248a3822efSAndreas Gohr    protected int $peek = 0;
2510f2bde6SAndreas Gohr    protected bool $filter = false;
261169a1acSAndreas Gohr
27d8ce5486SAndreas Gohr    /** @inheritdoc */
28d8ce5486SAndreas Gohr    public function getType()
29d8ce5486SAndreas Gohr    {
301169a1acSAndreas Gohr        return 'substition';
311169a1acSAndreas Gohr    }
321169a1acSAndreas Gohr
33d8ce5486SAndreas Gohr    /** @inheritdoc */
34d8ce5486SAndreas Gohr    public function getPType()
35d8ce5486SAndreas Gohr    {
361169a1acSAndreas Gohr        return 'block';
371169a1acSAndreas Gohr    }
381169a1acSAndreas Gohr
39d8ce5486SAndreas Gohr    /** @inheritdoc */
40d8ce5486SAndreas Gohr    public function getSort()
41d8ce5486SAndreas Gohr    {
421169a1acSAndreas Gohr        return 155;
431169a1acSAndreas Gohr    }
441169a1acSAndreas Gohr
45d8ce5486SAndreas Gohr    /** @inheritdoc */
46d8ce5486SAndreas Gohr    public function connectTo($mode)
47d8ce5486SAndreas Gohr    {
481169a1acSAndreas Gohr        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
491169a1acSAndreas Gohr    }
501169a1acSAndreas Gohr
51d8ce5486SAndreas Gohr    /** @inheritdoc */
52d8ce5486SAndreas Gohr    public function handle($match, $state, $pos, Doku_Handler $handler)
53d8ce5486SAndreas Gohr    {
545655937aSAndreas Gohr        return explode(' ', substr($match, 13, -2));
551169a1acSAndreas Gohr    }
561169a1acSAndreas Gohr
57d8ce5486SAndreas Gohr    /** @inheritdoc */
58d8ce5486SAndreas Gohr    public function render($format, Doku_Renderer $renderer, $data)
59d8ce5486SAndreas Gohr    {
60d8ce5486SAndreas Gohr        if ($format != 'xhtml') return false;
614c5e7fe5SAndreas Gohr        $renderer->nocache();
621169a1acSAndreas Gohr
631169a1acSAndreas Gohr        global $INFO;
641169a1acSAndreas Gohr
65b3e02951SAndreas Gohr        // first data is namespace, rest is options
665655937aSAndreas Gohr        $ns = array_shift($data);
675655937aSAndreas Gohr        if ($ns && $ns[0] === '.') {
685655937aSAndreas Gohr            // resolve relative to current page
695655937aSAndreas Gohr            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
705655937aSAndreas Gohr        } else {
715655937aSAndreas Gohr            $ns = cleanID($ns);
725655937aSAndreas Gohr        }
73b3e02951SAndreas Gohr
744c5e7fe5SAndreas Gohr        $this->initState(
754c5e7fe5SAndreas Gohr            $ns,
764c5e7fe5SAndreas Gohr            $INFO['id'],
774c5e7fe5SAndreas Gohr            (bool)$this->getConf('usetitle'),
784c5e7fe5SAndreas Gohr            $this->getConf('sort'),
798a3822efSAndreas Gohr            in_array('home', $data),
808a3822efSAndreas Gohr            $this->getConf('peek', 0),
8110f2bde6SAndreas Gohr            in_array('filter', $data)
824c5e7fe5SAndreas Gohr        );
834c5e7fe5SAndreas Gohr
848a3822efSAndreas Gohr        $tree = $this->getTree();
8510f2bde6SAndreas Gohr
8610f2bde6SAndreas Gohr        $class = 'plugin__simplenavi';
8710f2bde6SAndreas Gohr        if ($this->filter) {
8810f2bde6SAndreas Gohr            $class .= ' plugin__simplenavi_filter';
8910f2bde6SAndreas Gohr        }
9010f2bde6SAndreas Gohr
9110f2bde6SAndreas Gohr        $renderer->doc .= '<div class="' . $class . '">';
92ba73b2c8SAndreas Gohr        $this->renderTree($renderer, $tree->getTop());
9310f2bde6SAndreas Gohr        $renderer->doc .= '</div>';
941169a1acSAndreas Gohr
951169a1acSAndreas Gohr        return true;
961169a1acSAndreas Gohr    }
971169a1acSAndreas Gohr
98d8ce5486SAndreas Gohr    /**
994c5e7fe5SAndreas Gohr     * Initialize the configuration state of the plugin
1004c5e7fe5SAndreas Gohr     *
1014c5e7fe5SAndreas Gohr     * Also used in testing
1024c5e7fe5SAndreas Gohr     */
1034c5e7fe5SAndreas Gohr    public function initState(
1044c5e7fe5SAndreas Gohr        string $ns,
1054c5e7fe5SAndreas Gohr        string $currentID,
1064c5e7fe5SAndreas Gohr        bool   $usetitle,
1074c5e7fe5SAndreas Gohr        string $sort,
1088a3822efSAndreas Gohr        bool   $home,
10910f2bde6SAndreas Gohr        int    $peek = 0,
11010f2bde6SAndreas Gohr        bool   $filter = false
1114c5e7fe5SAndreas Gohr    )
1124c5e7fe5SAndreas Gohr    {
1134c5e7fe5SAndreas Gohr        $this->ns = $ns;
1144c5e7fe5SAndreas Gohr        $this->currentID = $currentID;
1154c5e7fe5SAndreas Gohr        $this->usetitle = $usetitle;
1164c5e7fe5SAndreas Gohr        $this->sort = $sort;
1174c5e7fe5SAndreas Gohr        $this->home = $home;
1188a3822efSAndreas Gohr        $this->peek = $peek;
11910f2bde6SAndreas Gohr        $this->filter = $filter;
1204c5e7fe5SAndreas Gohr    }
1214c5e7fe5SAndreas Gohr
1224c5e7fe5SAndreas Gohr    /**
1234c5e7fe5SAndreas Gohr     * Create the tree
1244c5e7fe5SAndreas Gohr     *
125ba73b2c8SAndreas Gohr     * @return PageTreeBuilder
126e75a33bfSAndreas Gohr     */
1274c5e7fe5SAndreas Gohr    protected function getTree(): PageTreeBuilder
128e75a33bfSAndreas Gohr    {
1294c5e7fe5SAndreas Gohr        $tree = new PageTreeBuilder($this->ns);
130ba73b2c8SAndreas Gohr        $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE);
1314c5e7fe5SAndreas Gohr        if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP);
132ba73b2c8SAndreas Gohr        $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision']));
133ba73b2c8SAndreas Gohr        $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor']));
134ba73b2c8SAndreas Gohr        $tree->generate();
1354c5e7fe5SAndreas Gohr
1364c5e7fe5SAndreas Gohr        switch ($this->sort) {
1374c5e7fe5SAndreas Gohr            case 'id':
1384c5e7fe5SAndreas Gohr                $tree->sort(TreeSort::SORT_BY_ID);
1394c5e7fe5SAndreas Gohr                break;
1404c5e7fe5SAndreas Gohr            case 'title':
1414c5e7fe5SAndreas Gohr                $tree->sort(TreeSort::SORT_BY_TITLE);
1424c5e7fe5SAndreas Gohr                break;
1434c5e7fe5SAndreas Gohr            case 'ns_id':
1444c5e7fe5SAndreas Gohr                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID);
1454c5e7fe5SAndreas Gohr                break;
1464c5e7fe5SAndreas Gohr            default:
147ba73b2c8SAndreas Gohr                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE);
1484c5e7fe5SAndreas Gohr                break;
1494c5e7fe5SAndreas Gohr        }
1504c5e7fe5SAndreas Gohr
151ba73b2c8SAndreas Gohr        return $tree;
152e75a33bfSAndreas Gohr    }
153e75a33bfSAndreas Gohr
154e75a33bfSAndreas Gohr
155e75a33bfSAndreas Gohr    /**
156ba73b2c8SAndreas Gohr     * Callback for the PageTreeBuilder to decide if we want to recurse into a node
157d8ce5486SAndreas Gohr     *
158ba73b2c8SAndreas Gohr     * @param AbstractNode $node
159ba73b2c8SAndreas Gohr     * @param int $depth
160ba73b2c8SAndreas Gohr     * @return bool
161d8ce5486SAndreas Gohr     */
162ba73b2c8SAndreas Gohr    protected function treeRecursionDecision(AbstractNode $node, int $depth): bool
163d8ce5486SAndreas Gohr    {
164ba73b2c8SAndreas Gohr        if ($node instanceof WikiStartpage) {
165ba73b2c8SAndreas Gohr            $id = $node->getNs(); // use the namespace for startpages
166492ddc4eSAndreas Gohr        } else {
167ba73b2c8SAndreas Gohr            $id = $node->getId();
168492ddc4eSAndreas Gohr        }
169ba73b2c8SAndreas Gohr
1704c5e7fe5SAndreas Gohr        $is_current = $this->isParent($this->currentID, $id);
171ba73b2c8SAndreas Gohr        $node->setProperty('is_current', $is_current);
172ba73b2c8SAndreas Gohr
173ba73b2c8SAndreas Gohr        // always recurse into the current page path
174ba73b2c8SAndreas Gohr        if ($is_current) return true;
175ba73b2c8SAndreas Gohr
1768a3822efSAndreas Gohr        // should we peek deeper to see if there's something readable?
1778a3822efSAndreas Gohr        if ($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ) {
1788a3822efSAndreas Gohr            return true;
1798a3822efSAndreas Gohr        }
180ba73b2c8SAndreas Gohr
181ba73b2c8SAndreas Gohr        return false;
1821169a1acSAndreas Gohr    }
1831169a1acSAndreas Gohr
184d8ce5486SAndreas Gohr    /**
185ba73b2c8SAndreas Gohr     * Callback for the PageTreeBuilder to process a node
186d8ce5486SAndreas Gohr     *
187ba73b2c8SAndreas Gohr     * @param AbstractNode $node
188ba73b2c8SAndreas Gohr     * @return AbstractNode|null
189d8ce5486SAndreas Gohr     */
190ba73b2c8SAndreas Gohr    protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode
191d8ce5486SAndreas Gohr    {
192ba73b2c8SAndreas Gohr        $perm = auth_quickaclcheck($node->getId());
193ba73b2c8SAndreas Gohr        $node->setProperty('permission', $perm);
1944c5e7fe5SAndreas Gohr        $node->setTitle($this->getTitle($node->getId()));
195ba73b2c8SAndreas Gohr
196ba73b2c8SAndreas Gohr
197ba73b2c8SAndreas Gohr        if ($node->hasChildren()) {
198ba73b2c8SAndreas Gohr            // this node has children, we add it to the tree regardless of the permission
199ba73b2c8SAndreas Gohr            // permissions are checked again when rendering
200ba73b2c8SAndreas Gohr            return $node;
2011169a1acSAndreas Gohr        }
202ba73b2c8SAndreas Gohr
203ba73b2c8SAndreas Gohr        if ($perm < AUTH_READ) {
204ba73b2c8SAndreas Gohr            // no children, no permission. No need to add it to the tree
205ba73b2c8SAndreas Gohr            return null;
206ba73b2c8SAndreas Gohr        }
207ba73b2c8SAndreas Gohr
208*a2e56777SAndreas Gohr        // don't show hidden pages
209*a2e56777SAndreas Gohr        if(isHiddenPage($node->getId())) return null;
210*a2e56777SAndreas Gohr
211ba73b2c8SAndreas Gohr        return $node;
212ba73b2c8SAndreas Gohr    }
213ba73b2c8SAndreas Gohr
214ba73b2c8SAndreas Gohr
215ba73b2c8SAndreas Gohr    /**
21610f2bde6SAndreas Gohr     * Render the tree
217ba73b2c8SAndreas Gohr     *
218ba73b2c8SAndreas Gohr     * @param Doku_Renderer $R The current renderer
219ba73b2c8SAndreas Gohr     * @param AbstractNode $top The top node of the tree (use getTop() to get it)
220ba73b2c8SAndreas Gohr     * @param int $level current nesting level, starting at 1
221ba73b2c8SAndreas Gohr     * @return void
222ba73b2c8SAndreas Gohr     */
223ba73b2c8SAndreas Gohr    protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1)
224ba73b2c8SAndreas Gohr    {
225ba73b2c8SAndreas Gohr        $R->listu_open();
226ba73b2c8SAndreas Gohr        foreach ($top->getChildren() as $node) {
227ba73b2c8SAndreas Gohr            $isfolder = $node instanceof WikiNamespace;
228ba73b2c8SAndreas Gohr            $incurrent = $node->getProperty('is_current', false);
229ba73b2c8SAndreas Gohr
230ba73b2c8SAndreas Gohr            $R->listitem_open(1, $isfolder);
231ba73b2c8SAndreas Gohr            $R->listcontent_open();
232ba73b2c8SAndreas Gohr            if ($incurrent) $R->strong_open();
233ba73b2c8SAndreas Gohr
234ba73b2c8SAndreas Gohr            if (((int)$node->getProperty('permission', 0)) < AUTH_READ) {
235ba73b2c8SAndreas Gohr                $R->cdata($node->getTitle());
236ba73b2c8SAndreas Gohr            } else {
237ba73b2c8SAndreas Gohr                $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation');
238ba73b2c8SAndreas Gohr            }
239ba73b2c8SAndreas Gohr
240ba73b2c8SAndreas Gohr            if ($incurrent) $R->strong_close();
241ba73b2c8SAndreas Gohr            $R->listcontent_close();
242ba73b2c8SAndreas Gohr            if ($node->hasChildren()) {
243ba73b2c8SAndreas Gohr                $this->renderTree($R, $node, $level + 1);
244ba73b2c8SAndreas Gohr            }
245ba73b2c8SAndreas Gohr            $R->listitem_close();
246ba73b2c8SAndreas Gohr        }
247ba73b2c8SAndreas Gohr        $R->listu_close();
2481169a1acSAndreas Gohr    }
2491169a1acSAndreas Gohr
250d8ce5486SAndreas Gohr    /**
251ba73b2c8SAndreas Gohr     * Check if the given parent ID is a parent of the child ID
252d8ce5486SAndreas Gohr     *
253ba73b2c8SAndreas Gohr     * @param string $child
254ba73b2c8SAndreas Gohr     * @param string $parent
255d8ce5486SAndreas Gohr     * @return bool
256d8ce5486SAndreas Gohr     */
257ba73b2c8SAndreas Gohr    protected function isParent(string $child, string $parent)
258d8ce5486SAndreas Gohr    {
259ea3588fbSAndreas Gohr (aider)        // Empty parent is considered a parent of all pages
260ea3588fbSAndreas Gohr (aider)        if ($parent === '') {
261ea3588fbSAndreas Gohr (aider)            return true;
262ea3588fbSAndreas Gohr (aider)        }
263ea3588fbSAndreas Gohr (aider)
264ba73b2c8SAndreas Gohr        $child = explode(':', $child);
265ba73b2c8SAndreas Gohr        $parent = explode(':', $parent);
266ba73b2c8SAndreas Gohr        return array_slice($child, 0, count($parent)) === $parent;
2671169a1acSAndreas Gohr    }
2681169a1acSAndreas Gohr
269d418c031SAndreas Gohr
270d418c031SAndreas Gohr    /**
271d8ce5486SAndreas Gohr     * Get the title for the given page ID
272d8ce5486SAndreas Gohr     *
273d8ce5486SAndreas Gohr     * @param string $id
274d8ce5486SAndreas Gohr     * @return string
275d8ce5486SAndreas Gohr     */
2764c5e7fe5SAndreas Gohr    protected function getTitle($id)
277d8ce5486SAndreas Gohr    {
278e306992cSAndreas Gohr        global $conf;
279e306992cSAndreas Gohr
2804c5e7fe5SAndreas Gohr        if ($this->usetitle) {
281e306992cSAndreas Gohr            $p = p_get_first_heading($id);
282303e1405SMichael Große            if (!empty($p)) return $p;
283e75a33bfSAndreas Gohr        }
284e306992cSAndreas Gohr
285e306992cSAndreas Gohr        $p = noNS($id);
286d8ce5486SAndreas Gohr        if ($p == $conf['start'] || !$p) {
287e306992cSAndreas Gohr            $p = noNS(getNS($id));
288d8ce5486SAndreas Gohr            if (!$p) {
289e306992cSAndreas Gohr                return $conf['start'];
290e306992cSAndreas Gohr            }
291e306992cSAndreas Gohr        }
292e306992cSAndreas Gohr        return $p;
293e306992cSAndreas Gohr    }
2941169a1acSAndreas Gohr}
295