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