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