xref: /plugin/simplenavi/syntax.php (revision 10f2bde61ac988c176739e1cd98b79140baaed9e)
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;
25*10f2bde6SAndreas 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),
81*10f2bde6SAndreas Gohr            in_array('filter', $data)
824c5e7fe5SAndreas Gohr        );
834c5e7fe5SAndreas Gohr
848a3822efSAndreas Gohr        $tree = $this->getTree();
85*10f2bde6SAndreas Gohr
86*10f2bde6SAndreas Gohr        $class = 'plugin__simplenavi';
87*10f2bde6SAndreas Gohr        if ($this->filter) {
88*10f2bde6SAndreas Gohr            $class .= ' plugin__simplenavi_filter';
89*10f2bde6SAndreas Gohr        }
90*10f2bde6SAndreas Gohr
91*10f2bde6SAndreas Gohr        $renderer->doc .= '<div class="' . $class . '">';
92ba73b2c8SAndreas Gohr        $this->renderTree($renderer, $tree->getTop());
93*10f2bde6SAndreas 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,
109*10f2bde6SAndreas Gohr        int    $peek = 0,
110*10f2bde6SAndreas 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;
119*10f2bde6SAndreas 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
208ba73b2c8SAndreas Gohr        return $node;
209ba73b2c8SAndreas Gohr    }
210ba73b2c8SAndreas Gohr
211ba73b2c8SAndreas Gohr
212ba73b2c8SAndreas Gohr    /**
213*10f2bde6SAndreas Gohr     * Render the tree
214ba73b2c8SAndreas Gohr     *
215ba73b2c8SAndreas Gohr     * @param Doku_Renderer $R The current renderer
216ba73b2c8SAndreas Gohr     * @param AbstractNode $top The top node of the tree (use getTop() to get it)
217ba73b2c8SAndreas Gohr     * @param int $level current nesting level, starting at 1
218ba73b2c8SAndreas Gohr     * @return void
219ba73b2c8SAndreas Gohr     */
220ba73b2c8SAndreas Gohr    protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1)
221ba73b2c8SAndreas Gohr    {
222ba73b2c8SAndreas Gohr        $R->listu_open();
223ba73b2c8SAndreas Gohr        foreach ($top->getChildren() as $node) {
224ba73b2c8SAndreas Gohr            $isfolder = $node instanceof WikiNamespace;
225ba73b2c8SAndreas Gohr            $incurrent = $node->getProperty('is_current', false);
226ba73b2c8SAndreas Gohr
227ba73b2c8SAndreas Gohr            $R->listitem_open(1, $isfolder);
228ba73b2c8SAndreas Gohr            $R->listcontent_open();
229ba73b2c8SAndreas Gohr            if ($incurrent) $R->strong_open();
230ba73b2c8SAndreas Gohr
231ba73b2c8SAndreas Gohr            if (((int)$node->getProperty('permission', 0)) < AUTH_READ) {
232ba73b2c8SAndreas Gohr                $R->cdata($node->getTitle());
233ba73b2c8SAndreas Gohr            } else {
234ba73b2c8SAndreas Gohr                $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation');
235ba73b2c8SAndreas Gohr            }
236ba73b2c8SAndreas Gohr
237ba73b2c8SAndreas Gohr            if ($incurrent) $R->strong_close();
238ba73b2c8SAndreas Gohr            $R->listcontent_close();
239ba73b2c8SAndreas Gohr            if ($node->hasChildren()) {
240ba73b2c8SAndreas Gohr                $this->renderTree($R, $node, $level + 1);
241ba73b2c8SAndreas Gohr            }
242ba73b2c8SAndreas Gohr            $R->listitem_close();
243ba73b2c8SAndreas Gohr        }
244ba73b2c8SAndreas Gohr        $R->listu_close();
2451169a1acSAndreas Gohr    }
2461169a1acSAndreas Gohr
247d8ce5486SAndreas Gohr    /**
248ba73b2c8SAndreas Gohr     * Check if the given parent ID is a parent of the child ID
249d8ce5486SAndreas Gohr     *
250ba73b2c8SAndreas Gohr     * @param string $child
251ba73b2c8SAndreas Gohr     * @param string $parent
252d8ce5486SAndreas Gohr     * @return bool
253d8ce5486SAndreas Gohr     */
254ba73b2c8SAndreas Gohr    protected function isParent(string $child, string $parent)
255d8ce5486SAndreas Gohr    {
256ea3588fbSAndreas Gohr (aider)        // Empty parent is considered a parent of all pages
257ea3588fbSAndreas Gohr (aider)        if ($parent === '') {
258ea3588fbSAndreas Gohr (aider)            return true;
259ea3588fbSAndreas Gohr (aider)        }
260ea3588fbSAndreas Gohr (aider)
261ba73b2c8SAndreas Gohr        $child = explode(':', $child);
262ba73b2c8SAndreas Gohr        $parent = explode(':', $parent);
263ba73b2c8SAndreas Gohr        return array_slice($child, 0, count($parent)) === $parent;
2641169a1acSAndreas Gohr    }
2651169a1acSAndreas Gohr
266d418c031SAndreas Gohr
267d418c031SAndreas Gohr    /**
268d8ce5486SAndreas Gohr     * Get the title for the given page ID
269d8ce5486SAndreas Gohr     *
270d8ce5486SAndreas Gohr     * @param string $id
271d8ce5486SAndreas Gohr     * @return string
272d8ce5486SAndreas Gohr     */
2734c5e7fe5SAndreas Gohr    protected function getTitle($id)
274d8ce5486SAndreas Gohr    {
275e306992cSAndreas Gohr        global $conf;
276e306992cSAndreas Gohr
2774c5e7fe5SAndreas Gohr        if ($this->usetitle) {
278e306992cSAndreas Gohr            $p = p_get_first_heading($id);
279303e1405SMichael Große            if (!empty($p)) return $p;
280e75a33bfSAndreas Gohr        }
281e306992cSAndreas Gohr
282e306992cSAndreas Gohr        $p = noNS($id);
283d8ce5486SAndreas Gohr        if ($p == $conf['start'] || !$p) {
284e306992cSAndreas Gohr            $p = noNS(getNS($id));
285d8ce5486SAndreas Gohr            if (!$p) {
286e306992cSAndreas Gohr                return $conf['start'];
287e306992cSAndreas Gohr            }
288e306992cSAndreas Gohr        }
289e306992cSAndreas Gohr        return $p;
290e306992cSAndreas Gohr    }
2911169a1acSAndreas Gohr}
292