xref: /plugin/simplenavi/syntax.php (revision d418c0312966259c1e7c5b4672e7cb5733092796)
11169a1acSAndreas Gohr<?php
2d8ce5486SAndreas Gohr
3*d418c031SAndreas Gohruse dokuwiki\Extension\SyntaxPlugin;
4d8ce5486SAndreas Gohruse dokuwiki\File\PageResolver;
5e75a33bfSAndreas Gohruse dokuwiki\Utf8\Sort;
6d8ce5486SAndreas Gohr
71169a1acSAndreas Gohr/**
81169a1acSAndreas Gohr * DokuWiki Plugin simplenavi (Syntax Component)
91169a1acSAndreas Gohr *
101169a1acSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
111169a1acSAndreas Gohr * @author  Andreas Gohr <gohr@cosmocode.de>
121169a1acSAndreas Gohr */
13*d418c031SAndreas Gohrclass syntax_plugin_simplenavi extends SyntaxPlugin
14d8ce5486SAndreas Gohr{
15d8ce5486SAndreas Gohr    private $startpages = [];
161169a1acSAndreas Gohr
17d8ce5486SAndreas Gohr    /** @inheritdoc */
18d8ce5486SAndreas Gohr    public function getType()
19d8ce5486SAndreas Gohr    {
201169a1acSAndreas Gohr        return 'substition';
211169a1acSAndreas Gohr    }
221169a1acSAndreas Gohr
23d8ce5486SAndreas Gohr    /** @inheritdoc */
24d8ce5486SAndreas Gohr    public function getPType()
25d8ce5486SAndreas Gohr    {
261169a1acSAndreas Gohr        return 'block';
271169a1acSAndreas Gohr    }
281169a1acSAndreas Gohr
29d8ce5486SAndreas Gohr    /** @inheritdoc */
30d8ce5486SAndreas Gohr    public function getSort()
31d8ce5486SAndreas Gohr    {
321169a1acSAndreas Gohr        return 155;
331169a1acSAndreas Gohr    }
341169a1acSAndreas Gohr
35d8ce5486SAndreas Gohr    /** @inheritdoc */
36d8ce5486SAndreas Gohr    public function connectTo($mode)
37d8ce5486SAndreas Gohr    {
381169a1acSAndreas Gohr        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
391169a1acSAndreas Gohr    }
401169a1acSAndreas Gohr
41d8ce5486SAndreas Gohr    /** @inheritdoc */
42d8ce5486SAndreas Gohr    public function handle($match, $state, $pos, Doku_Handler $handler)
43d8ce5486SAndreas Gohr    {
445655937aSAndreas Gohr        return explode(' ', substr($match, 13, -2));
451169a1acSAndreas Gohr    }
461169a1acSAndreas Gohr
47d8ce5486SAndreas Gohr    /** @inheritdoc */
48d8ce5486SAndreas Gohr    public function render($format, Doku_Renderer $renderer, $data)
49d8ce5486SAndreas Gohr    {
50d8ce5486SAndreas Gohr        if ($format != 'xhtml') return false;
511169a1acSAndreas Gohr
521169a1acSAndreas Gohr        global $INFO;
53b3e02951SAndreas Gohr        $renderer->nocache();
541169a1acSAndreas Gohr
55b3e02951SAndreas Gohr        // first data is namespace, rest is options
565655937aSAndreas Gohr        $ns = array_shift($data);
575655937aSAndreas Gohr        if ($ns && $ns[0] === '.') {
585655937aSAndreas Gohr            // resolve relative to current page
595655937aSAndreas Gohr            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
605655937aSAndreas Gohr        } else {
615655937aSAndreas Gohr            $ns = cleanID($ns);
625655937aSAndreas Gohr        }
63b3e02951SAndreas Gohr
64e75a33bfSAndreas Gohr        $items = $this->getSortedItems(
65e75a33bfSAndreas Gohr            $ns,
66e75a33bfSAndreas Gohr            $INFO['id'],
67e75a33bfSAndreas Gohr            $this->getConf('usetitle'),
68e58e2f72SAndreas Gohr            $this->getConf('natsort'),
69*d418c031SAndreas Gohr            $this->getConf('nsfirst'),
70*d418c031SAndreas Gohr            in_array('home', $data)
71e75a33bfSAndreas Gohr        );
721169a1acSAndreas Gohr
73b3e02951SAndreas Gohr        $class = 'plugin__simplenavi';
74b3e02951SAndreas Gohr        if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter';
75b3e02951SAndreas Gohr
76b3e02951SAndreas Gohr        $renderer->doc .= '<div class="' . $class . '">';
77d8ce5486SAndreas Gohr        $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']);
78d8ce5486SAndreas Gohr        $renderer->doc .= '</div>';
791169a1acSAndreas Gohr
801169a1acSAndreas Gohr        return true;
811169a1acSAndreas Gohr    }
821169a1acSAndreas Gohr
83d8ce5486SAndreas Gohr    /**
84e75a33bfSAndreas Gohr     * Fetch the items to display
85e75a33bfSAndreas Gohr     *
86e75a33bfSAndreas Gohr     * This returns a flat list suitable for html_buildlist()
87e75a33bfSAndreas Gohr     *
88e75a33bfSAndreas Gohr     * @param string $ns the namespace to search in
89e75a33bfSAndreas Gohr     * @param string $current the current page, the tree will be expanded to this
90e75a33bfSAndreas Gohr     * @param bool $useTitle Sort by the title instead of the ID?
91e75a33bfSAndreas Gohr     * @param bool $useNatSort Use natural sorting or just sort by ASCII?
92*d418c031SAndreas Gohr     * @param bool $nsFirst Sort namespaces before pages?
93*d418c031SAndreas Gohr     * @param bool $home Add namespace's start page as top level item?
94e75a33bfSAndreas Gohr     * @return array
95e75a33bfSAndreas Gohr     */
96*d418c031SAndreas Gohr    public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst, $home)
97e75a33bfSAndreas Gohr    {
98e75a33bfSAndreas Gohr        global $conf;
99e75a33bfSAndreas Gohr
100*d418c031SAndreas Gohr        // convert to path
101*d418c031SAndreas Gohr        $nspath = utf8_encodeFN(str_replace(':', '/', $ns));
102*d418c031SAndreas Gohr
103*d418c031SAndreas Gohr        // get the start page of the main namespace, this adds it to the list of seen pages in $this->startpages
104*d418c031SAndreas Gohr        // and will skip it by default in the search callback
105*d418c031SAndreas Gohr        $startPage = $this->getMainStartPage($ns, $useTitle);
106*d418c031SAndreas Gohr
107e75a33bfSAndreas Gohr        $items = [];
108*d418c031SAndreas Gohr        if ($home) {
109*d418c031SAndreas Gohr            // when home is requested, add the start page as top level item
110*d418c031SAndreas Gohr            $items[$startPage['id']] = $startPage;
111*d418c031SAndreas Gohr            $minlevel = 0;
112*d418c031SAndreas Gohr        } else {
113*d418c031SAndreas Gohr            $minlevel = 1;
114*d418c031SAndreas Gohr        }
115*d418c031SAndreas Gohr
116*d418c031SAndreas Gohr        // execute search using our own callback
117e75a33bfSAndreas Gohr        search(
118e75a33bfSAndreas Gohr            $items,
119e75a33bfSAndreas Gohr            $conf['datadir'],
120e75a33bfSAndreas Gohr            [$this, 'cbSearch'],
121e75a33bfSAndreas Gohr            [
122e75a33bfSAndreas Gohr                'currentID' => $current,
123e75a33bfSAndreas Gohr                'usetitle' => $useTitle,
124e75a33bfSAndreas Gohr            ],
125*d418c031SAndreas Gohr            $nspath,
126e75a33bfSAndreas Gohr            1,
127e75a33bfSAndreas Gohr            '' // no sorting, we do ourselves
128e75a33bfSAndreas Gohr        );
12974c26ce5SAndreas Gohr        if (!$items) return [];
130e75a33bfSAndreas Gohr
131e75a33bfSAndreas Gohr        // split into separate levels
132e75a33bfSAndreas Gohr        $parents = [];
133e75a33bfSAndreas Gohr        $levels = [];
134*d418c031SAndreas Gohr        $curLevel = $minlevel;
135e75a33bfSAndreas Gohr        foreach ($items as $idx => $item) {
136*d418c031SAndreas Gohr            if ($curLevel < $item['level']) {
137e75a33bfSAndreas Gohr                // previous item was the parent
138*d418c031SAndreas Gohr                $parents[] = array_key_last($levels[$curLevel]);
139e75a33bfSAndreas Gohr            }
140*d418c031SAndreas Gohr            $curLevel = $item['level'];
141e75a33bfSAndreas Gohr            $levels[$item['level']][$idx] = $item;
142e75a33bfSAndreas Gohr        }
143e75a33bfSAndreas Gohr
144e75a33bfSAndreas Gohr        // sort each level separately
145e75a33bfSAndreas Gohr        foreach ($levels as $level => $items) {
146*d418c031SAndreas Gohr            uasort($items, fn($a, $b) => $this->itemComparator($a, $b, $useNatSort, $nsFirst));
147e75a33bfSAndreas Gohr            $levels[$level] = $items;
148e75a33bfSAndreas Gohr        }
149e75a33bfSAndreas Gohr
150e75a33bfSAndreas Gohr        // merge levels into a flat list again
151e75a33bfSAndreas Gohr        $levels = array_reverse($levels, true);
152*d418c031SAndreas Gohr        foreach (array_keys($levels) as $level) {
153*d418c031SAndreas Gohr            if ($level == $minlevel) break;
154e75a33bfSAndreas Gohr
155e75a33bfSAndreas Gohr            $parent = array_pop($parents);
156e75a33bfSAndreas Gohr            $pos = array_search($parent, array_keys($levels[$level - 1])) + 1;
157e75a33bfSAndreas Gohr
158e58e2f72SAndreas Gohr            /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */
159e75a33bfSAndreas Gohr            $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) +
160e75a33bfSAndreas Gohr                $levels[$level] +
161e75a33bfSAndreas Gohr                array_slice($levels[$level - 1], $pos, null, true);
162e75a33bfSAndreas Gohr        }
163e75a33bfSAndreas Gohr
164*d418c031SAndreas Gohr        return $levels[$minlevel];
165e75a33bfSAndreas Gohr    }
166e75a33bfSAndreas Gohr
167e75a33bfSAndreas Gohr    /**
168e75a33bfSAndreas Gohr     * Compare two items
169e75a33bfSAndreas Gohr     *
170e75a33bfSAndreas Gohr     * @param array $a
171e75a33bfSAndreas Gohr     * @param array $b
172e75a33bfSAndreas Gohr     * @param bool $useNatSort
173e58e2f72SAndreas Gohr     * @param bool $nsFirst
174e75a33bfSAndreas Gohr     * @return int
175e75a33bfSAndreas Gohr     */
176e58e2f72SAndreas Gohr    public function itemComparator($a, $b, $useNatSort, $nsFirst)
177e75a33bfSAndreas Gohr    {
178e58e2f72SAndreas Gohr        if ($nsFirst && $a['type'] != $b['type']) {
179e58e2f72SAndreas Gohr            return $a['type'] == 'd' ? -1 : 1;
180e58e2f72SAndreas Gohr        }
181e58e2f72SAndreas Gohr
182e75a33bfSAndreas Gohr        if ($useNatSort) {
183e75a33bfSAndreas Gohr            return Sort::strcmp($a['title'], $b['title']);
184e75a33bfSAndreas Gohr        } else {
185e75a33bfSAndreas Gohr            return strcmp($a['title'], $b['title']);
186e75a33bfSAndreas Gohr        }
187e75a33bfSAndreas Gohr    }
188e75a33bfSAndreas Gohr
189e75a33bfSAndreas Gohr
190e75a33bfSAndreas Gohr    /**
191d8ce5486SAndreas Gohr     * Create a list openening
192d8ce5486SAndreas Gohr     *
193d8ce5486SAndreas Gohr     * @param array $item
194d8ce5486SAndreas Gohr     * @return string
195d8ce5486SAndreas Gohr     * @see html_buildlist()
196d8ce5486SAndreas Gohr     */
197d8ce5486SAndreas Gohr    public function cbList($item)
198d8ce5486SAndreas Gohr    {
199492ddc4eSAndreas Gohr        global $INFO;
200492ddc4eSAndreas Gohr
201492ddc4eSAndreas Gohr        if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) {
202e75a33bfSAndreas Gohr            return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>';
203492ddc4eSAndreas Gohr        } else {
204e75a33bfSAndreas Gohr            return html_wikilink(':' . $item['id'], $item['title']);
205492ddc4eSAndreas Gohr        }
2061169a1acSAndreas Gohr    }
2071169a1acSAndreas Gohr
208d8ce5486SAndreas Gohr    /**
209d8ce5486SAndreas Gohr     * Create a list item
210d8ce5486SAndreas Gohr     *
211d8ce5486SAndreas Gohr     * @param array $item
212d8ce5486SAndreas Gohr     * @return string
213d8ce5486SAndreas Gohr     * @see html_buildlist()
214d8ce5486SAndreas Gohr     */
215d8ce5486SAndreas Gohr    public function cbListItem($item)
216d8ce5486SAndreas Gohr    {
2171169a1acSAndreas Gohr        if ($item['type'] == "f") {
2181169a1acSAndreas Gohr            return '<li class="level' . $item['level'] . '">';
2191169a1acSAndreas Gohr        } elseif ($item['open']) {
2201169a1acSAndreas Gohr            return '<li class="open">';
2211169a1acSAndreas Gohr        } else {
2221169a1acSAndreas Gohr            return '<li class="closed">';
2231169a1acSAndreas Gohr        }
2241169a1acSAndreas Gohr    }
2251169a1acSAndreas Gohr
226d8ce5486SAndreas Gohr    /**
227d8ce5486SAndreas Gohr     * Custom search callback
228d8ce5486SAndreas Gohr     *
229d8ce5486SAndreas Gohr     * @param $data
230d8ce5486SAndreas Gohr     * @param $base
231d8ce5486SAndreas Gohr     * @param $file
232d8ce5486SAndreas Gohr     * @param $type
233d8ce5486SAndreas Gohr     * @param $lvl
234e75a33bfSAndreas Gohr     * @param array $opts - currentID is the currently shown page
235d8ce5486SAndreas Gohr     * @return bool
236d8ce5486SAndreas Gohr     */
237d8ce5486SAndreas Gohr    public function cbSearch(&$data, $base, $file, $type, $lvl, $opts)
238d8ce5486SAndreas Gohr    {
2391169a1acSAndreas Gohr        global $conf;
2401169a1acSAndreas Gohr        $return = true;
2411169a1acSAndreas Gohr
2421169a1acSAndreas Gohr        $id = pathID($file);
2431169a1acSAndreas Gohr
244*d418c031SAndreas Gohr        if (
245*d418c031SAndreas Gohr            $type == 'd' && !(
246e75a33bfSAndreas Gohr                preg_match('#^' . $id . '(:|$)#', $opts['currentID']) ||
247e75a33bfSAndreas Gohr                preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID']))
2481169a1acSAndreas Gohr
249*d418c031SAndreas Gohr            )
250*d418c031SAndreas Gohr        ) {
2511169a1acSAndreas Gohr            //add but don't recurse
2521169a1acSAndreas Gohr            $return = false;
253303e1405SMichael Große        } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) {
2541169a1acSAndreas Gohr            //don't add
2551169a1acSAndreas Gohr            return false;
2561169a1acSAndreas Gohr        }
2571169a1acSAndreas Gohr
258660b56c3SAndreas Gohr        // for sneaky index, check access to the namespace's start page
259660b56c3SAndreas Gohr        if ($type == 'd' && $conf['sneaky_index']) {
260660b56c3SAndreas Gohr            $sp = (new PageResolver(''))->resolveId($id . ':');
261660b56c3SAndreas Gohr            if (auth_quickaclcheck($sp) < AUTH_READ) {
2621169a1acSAndreas Gohr                return false;
2631169a1acSAndreas Gohr            }
264660b56c3SAndreas Gohr        }
2651169a1acSAndreas Gohr
2661169a1acSAndreas Gohr        if ($type == 'd') {
2671169a1acSAndreas Gohr            // link directories to their start pages
268e75a33bfSAndreas Gohr            $original = $id;
2691169a1acSAndreas Gohr            $id = "$id:";
270d8ce5486SAndreas Gohr            $id = (new PageResolver(''))->resolveId($id);
2711169a1acSAndreas Gohr            $this->startpages[$id] = 1;
272e75a33bfSAndreas Gohr
273e75a33bfSAndreas Gohr            // if the resolve id is in the same namespace as the original it's a start page named like the dir
274*d418c031SAndreas Gohr            if (getNS($original) === getNS($id)) {
275e75a33bfSAndreas Gohr                $useNS = $original;
276e75a33bfSAndreas Gohr            }
277303e1405SMichael Große        } elseif (!empty($this->startpages[$id])) {
2781169a1acSAndreas Gohr            // skip already shown start pages
2791169a1acSAndreas Gohr            return false;
2801169a1acSAndreas Gohr        }
2811169a1acSAndreas Gohr
2821169a1acSAndreas Gohr        //check hidden
2831169a1acSAndreas Gohr        if (isHiddenPage($id)) {
2841169a1acSAndreas Gohr            return false;
2851169a1acSAndreas Gohr        }
2861169a1acSAndreas Gohr
2871169a1acSAndreas Gohr        //check ACL
2881169a1acSAndreas Gohr        if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) {
2891169a1acSAndreas Gohr            return false;
2901169a1acSAndreas Gohr        }
2911169a1acSAndreas Gohr
292e75a33bfSAndreas Gohr        $data[$id] = [
293d8ce5486SAndreas Gohr            'id' => $id,
2941169a1acSAndreas Gohr            'type' => $type,
2951169a1acSAndreas Gohr            'level' => $lvl,
296d8ce5486SAndreas Gohr            'open' => $return,
297e75a33bfSAndreas Gohr            'title' => $this->getTitle($id, $opts['usetitle']),
298e75a33bfSAndreas Gohr            'ns' => $useNS ?? (string)getNS($id),
299e75a33bfSAndreas Gohr        ];
300e75a33bfSAndreas Gohr
3011169a1acSAndreas Gohr        return $return;
3021169a1acSAndreas Gohr    }
3031169a1acSAndreas Gohr
304d8ce5486SAndreas Gohr    /**
305*d418c031SAndreas Gohr     * @param string $id
306*d418c031SAndreas Gohr     * @param bool $useTitle
307*d418c031SAndreas Gohr     * @return array
308*d418c031SAndreas Gohr     */
309*d418c031SAndreas Gohr    protected function getMainStartPage($ns, $useTitle)
310*d418c031SAndreas Gohr    {
311*d418c031SAndreas Gohr        $resolver = new PageResolver('');
312*d418c031SAndreas Gohr        $id = $resolver->resolveId($ns . ':');
313*d418c031SAndreas Gohr
314*d418c031SAndreas Gohr        $item = [
315*d418c031SAndreas Gohr            'id' => $id,
316*d418c031SAndreas Gohr            'type' => 'd',
317*d418c031SAndreas Gohr            'level' => 0,
318*d418c031SAndreas Gohr            'open' => true,
319*d418c031SAndreas Gohr            'title' => $this->getTitle($id, $useTitle),
320*d418c031SAndreas Gohr            'ns' => $ns,
321*d418c031SAndreas Gohr        ];
322*d418c031SAndreas Gohr        $this->startpages[$id] = 1;
323*d418c031SAndreas Gohr        return $item;
324*d418c031SAndreas Gohr    }
325*d418c031SAndreas Gohr
326*d418c031SAndreas Gohr    /**
327d8ce5486SAndreas Gohr     * Get the title for the given page ID
328d8ce5486SAndreas Gohr     *
329d8ce5486SAndreas Gohr     * @param string $id
330e75a33bfSAndreas Gohr     * @param bool $usetitle - use the first heading as title
331d8ce5486SAndreas Gohr     * @return string
332d8ce5486SAndreas Gohr     */
333e75a33bfSAndreas Gohr    protected function getTitle($id, $usetitle)
334d8ce5486SAndreas Gohr    {
335e306992cSAndreas Gohr        global $conf;
336e306992cSAndreas Gohr
337e75a33bfSAndreas Gohr        if ($usetitle) {
338e306992cSAndreas Gohr            $p = p_get_first_heading($id);
339303e1405SMichael Große            if (!empty($p)) return $p;
340e75a33bfSAndreas Gohr        }
341e306992cSAndreas Gohr
342e306992cSAndreas Gohr        $p = noNS($id);
343d8ce5486SAndreas Gohr        if ($p == $conf['start'] || !$p) {
344e306992cSAndreas Gohr            $p = noNS(getNS($id));
345d8ce5486SAndreas Gohr            if (!$p) {
346e306992cSAndreas Gohr                return $conf['start'];
347e306992cSAndreas Gohr            }
348e306992cSAndreas Gohr        }
349e306992cSAndreas Gohr        return $p;
350e306992cSAndreas Gohr    }
3511169a1acSAndreas Gohr}
352