xref: /plugin/simplenavi/syntax.php (revision 74c26ce50567279fe74fda0495e87e809f905650)
1<?php
2
3use dokuwiki\File\PageResolver;
4use dokuwiki\Utf8\Sort;
5
6/**
7 * DokuWiki Plugin simplenavi (Syntax Component)
8 *
9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
10 * @author  Andreas Gohr <gohr@cosmocode.de>
11 */
12class syntax_plugin_simplenavi extends DokuWiki_Syntax_Plugin
13{
14    private $startpages = [];
15
16    /** @inheritdoc */
17    public function getType()
18    {
19        return 'substition';
20    }
21
22    /** @inheritdoc */
23    public function getPType()
24    {
25        return 'block';
26    }
27
28    /** @inheritdoc */
29    public function getSort()
30    {
31        return 155;
32    }
33
34    /** @inheritdoc */
35    public function connectTo($mode)
36    {
37        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
38    }
39
40    /** @inheritdoc */
41    public function handle($match, $state, $pos, Doku_Handler $handler)
42    {
43        return explode(' ', substr($match, 13, -2));
44    }
45
46    /** @inheritdoc */
47    public function render($format, Doku_Renderer $renderer, $data)
48    {
49        if ($format != 'xhtml') return false;
50
51        global $INFO;
52        $renderer->nocache();
53
54        // first data is namespace, rest is options
55        $ns = array_shift($data);
56        if ($ns && $ns[0] === '.') {
57            // resolve relative to current page
58            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
59        } else {
60            $ns = cleanID($ns);
61        }
62        // convert to path
63        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
64
65        $items = $this->getSortedItems(
66            $ns,
67            $INFO['id'],
68            $this->getConf('usetitle'),
69            $this->getConf('natsort'),
70            $this->getConf('nsfirst')
71        );
72
73        $class = 'plugin__simplenavi';
74        if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter';
75
76        $renderer->doc .= '<div class="' . $class . '">';
77        $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']);
78        $renderer->doc .= '</div>';
79
80        return true;
81    }
82
83    /**
84     * Fetch the items to display
85     *
86     * This returns a flat list suitable for html_buildlist()
87     *
88     * @param string $ns the namespace to search in
89     * @param string $current the current page, the tree will be expanded to this
90     * @param bool $useTitle Sort by the title instead of the ID?
91     * @param bool $useNatSort Use natural sorting or just sort by ASCII?
92     * @return array
93     */
94    public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst)
95    {
96        global $conf;
97
98        // execute search using our own callback
99        $items = [];
100        search(
101            $items,
102            $conf['datadir'],
103            [$this, 'cbSearch'],
104            [
105                'currentID' => $current,
106                'usetitle' => $useTitle,
107            ],
108            $ns,
109            1,
110            '' // no sorting, we do ourselves
111        );
112        if(!$items) return [];
113
114        // split into separate levels
115        $current = 1;
116        $parents = [];
117        $levels = [];
118        foreach ($items as $idx => $item) {
119            if ($current < $item['level']) {
120                // previous item was the parent
121                $parents[] = array_key_last($levels[$current]);
122            }
123            $current = $item['level'];
124            $levels[$item['level']][$idx] = $item;
125        }
126
127        // sort each level separately
128        foreach ($levels as $level => $items) {
129            uasort($items, function ($a, $b) use ($useNatSort, $nsFirst) {
130                return $this->itemComparator($a, $b, $useNatSort, $nsFirst);
131            });
132            $levels[$level] = $items;
133        }
134
135        // merge levels into a flat list again
136        $levels = array_reverse($levels, true);
137        foreach ($levels as $level => $items) {
138            if ($level == 1) break;
139
140            $parent = array_pop($parents);
141            $pos = array_search($parent, array_keys($levels[$level - 1])) + 1;
142
143            /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */
144            $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) +
145                $levels[$level] +
146                array_slice($levels[$level - 1], $pos, null, true);
147        }
148
149        return $levels[1];
150    }
151
152    /**
153     * Compare two items
154     *
155     * @param array $a
156     * @param array $b
157     * @param bool $useNatSort
158     * @param bool $nsFirst
159     * @return int
160     */
161    public function itemComparator($a, $b, $useNatSort, $nsFirst)
162    {
163        if ($nsFirst && $a['type'] != $b['type']) {
164            return $a['type'] == 'd' ? -1 : 1;
165        }
166
167        if ($useNatSort) {
168            return Sort::strcmp($a['title'], $b['title']);
169        } else {
170            return strcmp($a['title'], $b['title']);
171        }
172    }
173
174
175    /**
176     * Create a list openening
177     *
178     * @param array $item
179     * @return string
180     * @see html_buildlist()
181     */
182    public function cbList($item)
183    {
184        global $INFO;
185
186        if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) {
187            return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>';
188        } else {
189            return html_wikilink(':' . $item['id'], $item['title']);
190        }
191
192    }
193
194    /**
195     * Create a list item
196     *
197     * @param array $item
198     * @return string
199     * @see html_buildlist()
200     */
201    public function cbListItem($item)
202    {
203        if ($item['type'] == "f") {
204            return '<li class="level' . $item['level'] . '">';
205        } elseif ($item['open']) {
206            return '<li class="open">';
207        } else {
208            return '<li class="closed">';
209        }
210    }
211
212    /**
213     * Custom search callback
214     *
215     * @param $data
216     * @param $base
217     * @param $file
218     * @param $type
219     * @param $lvl
220     * @param array $opts - currentID is the currently shown page
221     * @return bool
222     */
223    public function cbSearch(&$data, $base, $file, $type, $lvl, $opts)
224    {
225        global $conf;
226        $return = true;
227
228        $id = pathID($file);
229
230        if ($type == 'd' && !(
231                preg_match('#^' . $id . '(:|$)#', $opts['currentID']) ||
232                preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID']))
233
234            )) {
235            //add but don't recurse
236            $return = false;
237        } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) {
238            //don't add
239            return false;
240        }
241
242        // for sneaky index, check access to the namespace's start page
243        if ($type == 'd' && $conf['sneaky_index']) {
244            $sp = (new PageResolver(''))->resolveId($id . ':');
245            if (auth_quickaclcheck($sp) < AUTH_READ) {
246                return false;
247            }
248        }
249
250        if ($type == 'd') {
251            // link directories to their start pages
252            $original = $id;
253            $id = "$id:";
254            $id = (new PageResolver(''))->resolveId($id);
255            $this->startpages[$id] = 1;
256
257            // if the resolve id is in the same namespace as the original it's a start page named like the dir
258            if (getNS($original) == getNS($id)) {
259                $useNS = $original;
260            }
261
262        } elseif (!empty($this->startpages[$id])) {
263            // skip already shown start pages
264            return false;
265        } elseif (noNS($id) == $conf['start']) {
266            // skip the main start page
267            return false;
268        }
269
270        //check hidden
271        if (isHiddenPage($id)) {
272            return false;
273        }
274
275        //check ACL
276        if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) {
277            return false;
278        }
279
280        $data[$id] = [
281            'id' => $id,
282            'type' => $type,
283            'level' => $lvl,
284            'open' => $return,
285            'title' => $this->getTitle($id, $opts['usetitle']),
286            'ns' => $useNS ?? (string)getNS($id),
287        ];
288
289        return $return;
290    }
291
292    /**
293     * Get the title for the given page ID
294     *
295     * @param string $id
296     * @param bool $usetitle - use the first heading as title
297     * @return string
298     */
299    protected function getTitle($id, $usetitle)
300    {
301        global $conf;
302
303        if ($usetitle) {
304            $p = p_get_first_heading($id);
305            if (!empty($p)) return $p;
306        }
307
308        $p = noNS($id);
309        if ($p == $conf['start'] || !$p) {
310            $p = noNS(getNS($id));
311            if (!$p) {
312                return $conf['start'];
313            }
314        }
315        return $p;
316    }
317}
318