1<?php
2/**
3 * Plugin minimap : Displays mini-map for namespace
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author  Nicolas GERARD
7 */
8if (!defined('DOKU_INC')) die();
9
10
11class syntax_plugin_minimap extends DokuWiki_Syntax_Plugin
12{
13
14    const PLUGIN_NAME = 'minimap';
15    const INCLUDE_DIRECTORY_PARAMETERS = 'includedirectory';
16    const SHOW_HEADER = 'showheader';
17    const NAMESPACE = 'namespace';
18    const POWERED_BY = 'poweredby';
19
20    function connectTo($aMode)
21    {
22        $pattern = '<' . $this->getPluginName() . '[^>]*>';
23        $this->Lexer->addSpecialPattern($pattern, $aMode, 'plugin_' . $this->getPluginName());
24    }
25
26    function getSort()
27    {
28        return 150;
29    }
30
31    /**
32     * No p element please
33     * @return string
34     */
35    function getPType()
36    {
37        return 'block';
38    }
39
40    function getType()
41    {
42        // The spelling is wrong but this is a correct value
43        // https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
44        return 'substition';
45    }
46
47    /**
48     *
49     * The handle function goal is to parse the matched syntax through the pattern function
50     * and to return the result for use in the renderer
51     * This result is always cached until the page is modified.
52     * @param string $match
53     * @param int $state
54     * @param int $pos
55     * @param Doku_Handler $handler
56     * @return array|bool
57     * @see DokuWiki_Syntax_Plugin::handle()
58     *
59     */
60    function handle($match, $state, $pos, Doku_Handler $handler)
61    {
62
63        switch ($state) {
64
65            // As there is only one call to connect to in order to a add a pattern,
66            // there is only one state entering the function
67            // but I leave it for better understanding of the process flow
68            case DOKU_LEXER_SPECIAL :
69
70                // Parse the parameters
71                $match = utf8_substr($match, 8, -1); //9 = strlen("<minimap")
72
73                // Init
74                $parameters = array();
75                $parameters['substr'] = 1;
76                $parameters[self::INCLUDE_DIRECTORY_PARAMETERS] = $this->getConf(self::INCLUDE_DIRECTORY_PARAMETERS);
77                $parameters[self::SHOW_HEADER] = $this->getConf(self::SHOW_HEADER);
78
79
80                // /i not case sensitive
81                $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]{1}([^\`\"]*)[\'\"]{1}\\s*";
82                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
83                if ($result != 0) {
84                    foreach ($matches[1] as $key => $parameterKey) {
85                        $parameter = strtolower($parameterKey);
86                        $value = $matches[2][$key];
87                        if (in_array($parameter, [self::SHOW_HEADER, self::INCLUDE_DIRECTORY_PARAMETERS])) {
88                            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
89                        }
90                        $parameters[$parameter] = $value;
91                    }
92                }
93                // Cache the values
94                return array($state, $parameters);
95
96        }
97
98        return false;
99    }
100
101
102    function render($mode, Doku_Renderer $renderer, $data)
103    {
104
105        // The $data variable comes from the handle() function
106        //
107        // $mode = 'xhtml' means that we output html
108        // There is other mode such as metadata where you can output data for the headers (Not 100% sure)
109        if ($mode == 'xhtml') {
110
111            // Unfold the $data array in two separates variables
112            list($state, $parameters) = $data;
113
114            // As there is only one call to connect to in order to a add a pattern,
115            // there is only one state entering the function
116            // but I leave it for better understanding of the process flow
117            switch ($state) {
118
119                case DOKU_LEXER_SPECIAL :
120
121                    global $ID;
122                    global $INFO;
123                    $callingId = $ID;
124                    // If mini-map is in a sidebar, we don't want the ID of the sidebar
125                    // but the ID of the page.
126                    if ($INFO != null) {
127                        $callingId = $INFO['id'];
128                    }
129
130                    $nameSpacePath = getNS($callingId); // The complete path to the directory
131                    if (array_key_exists(self::NAMESPACE, $parameters)) {
132                        $nameSpacePath = $parameters[self::NAMESPACE];
133                    }
134                    $currentNameSpace = curNS($callingId); // The name of the container directory
135                    $includeDirectory = $parameters[self::INCLUDE_DIRECTORY_PARAMETERS];
136                    $pagesOfNamespace = $this->getNamespaceChildren($nameSpacePath, $sort = 'natural', $listdirs = $includeDirectory);
137
138                    // Set the two possible home page for the namespace ie:
139                    //   - the name of the containing map ($homePageWithContainingMapName)
140                    //   - the start conf parameters ($homePageWithStartConf)
141                    global $conf;
142                    $parts = explode(':', $nameSpacePath);
143                    $lastContainingNameSpace = $parts[count($parts) - 1];
144                    $homePageWithContainingMapName = $nameSpacePath . ':' . $lastContainingNameSpace;
145                    $startConf = $conf['start'];
146                    $homePageWithStartConf = $nameSpacePath . ':' . $startConf;
147
148                    // Build the list of page
149                    $miniMapList = '<ul class="list-group">';
150                    $pageNum = 0;
151                    $startPageFound = false;
152                    $homePageFound = false;
153                    //$pagesCount = count($pagesOfNamespace); // number of pages in the namespace
154                    foreach ($pagesOfNamespace as $page) {
155
156                        // The title of the page
157                        $title = '';
158
159                        // If it's a directory
160                        if ($page['type'] == "d") {
161
162                            $pageId = $this->getNamespaceStartId($page['id']);
163
164                        } else {
165
166                            $pageNum++;
167                            $pageId = $page['id'];
168
169                        }
170
171                        // The title of the page
172                        if (useHeading('navigation')) {
173                            // $title = $page['title'] can not be used to retrieve the title
174                            // because it didn't encode the HTML tag
175                            // for instance if <math></math> is used, the output must have &lgt ...
176                            // otherwise browser may add quote and the math plugin will not work
177                            // May be a solution was just to encode the output
178                            $title = tpl_pagetitle($pageId, true);
179                        }
180
181                        // Name if the variable that it's shown. A part of it can be suppressed
182                        // Title will stay full in the link
183                        $name = noNSorNS($pageId);
184                        if ($title) {
185                            $name = $title;
186                        } else {
187                            $title = $name;
188                        }
189
190                        // If debug mode
191                        if ($parameters['debug']) {
192                            $title .= ' (' . $pageId . ')';
193                        }
194
195                        // Add the page number in the URL title
196                        $title .= ' (' . $pageNum . ')';
197
198                        // Suppress the parts in the name with the regexp defines in the 'suppress' params
199                        if ($parameters['suppress']) {
200                            $substrPattern = '/' . $parameters['suppress'] . '/i';
201                            $replacement = '';
202                            $name = preg_replace($substrPattern, $replacement, $name);
203                        }
204
205                        // See in which page we are
206                        // The style will then change
207                        $active = '';
208                        if ($callingId == $pageId) {
209                            $active = 'active';
210                        }
211
212                        // Not all page are printed
213                        // sidebar are not for instance
214
215                        // Are we in the root ?
216                        if ($page['ns']) {
217                            $nameSpacePathPrefix = $page['ns'] . ':';
218                        } else {
219                            $nameSpacePathPrefix = '';
220                        }
221                        $print = true;
222                        if ($page['id'] == $nameSpacePathPrefix . $currentNameSpace) {
223                            // If the start page exists, the page with the same name
224                            // than the namespace must be shown
225                            if (page_exists($nameSpacePathPrefix . $startConf)) {
226                                $print = true;
227                            } else {
228                                $print = false;
229                            }
230                            $homePageFound = true;
231                        } else if ($page['id'] == $nameSpacePathPrefix . $startConf) {
232                            $print = false;
233                            $startPageFound = true;
234                        } else if ($page['id'] == $nameSpacePathPrefix . $conf['sidebar']) {
235                            $pageNum -= 1;
236                            $print = false;
237                        };
238
239
240                        // If the page must be printed, build the link
241                        if ($print) {
242
243                            // Open the item tag
244                            $miniMapList .= "<li class=\"list-group-item " . $active . "\">";
245
246                            // Add a glyphicon if it's a directory
247                            if ($page['type'] == "d") {
248                                $miniMapList .= "<span class=\"nicon_folder_open\" aria-hidden=\"true\"></span>&nbsp;&nbsp;";
249                            }
250
251                            // Add the link
252                            $miniMapList .= tpl_link(
253                                wl($pageId),
254                                ucfirst($name), // First letter upper case
255                                'title="' . $title . '"',
256                                $return = true
257                            );
258
259                            // Close the item
260                            $miniMapList .= "</li>";
261
262                        }
263
264                    }
265                    $miniMapList .= '</ul>'; // End list-group
266
267
268                    // Build the panel header
269                    $miniMapHeader = "";
270                    $startId = "";
271                    if ($startPageFound) {
272                        $startId = $homePageWithStartConf;
273                    } else {
274                        if ($homePageFound) {
275                            $startId = $homePageWithContainingMapName;
276                        }
277                    }
278
279                    $panelHeaderContent = "";
280                    if ($startId == "") {
281                        if ($parameters[self::SHOW_HEADER] == true) {
282                            $panelHeaderContent = 'No Home Page found';
283                        }
284                    } else {
285                        $panelHeaderContent = tpl_link(
286                            wl($startId),
287                            tpl_pagetitle($startId, true),
288                            'title="' . $startId . '"',
289                            $return = true);
290                        // We are not counting the header page
291                        $pageNum--;
292                    }
293
294                    if ($panelHeaderContent != "") {
295                        $miniMapHeader .= '<div class="panel-heading">' . $panelHeaderContent . '  <span class="label label-primary">' . $pageNum . ' pages</span></div>';
296                    }
297
298                    if ($parameters['debug']) {
299                        $miniMapHeader .= '<div class="panel-body">' .
300                            '<B>Debug Information:</B><BR>' .
301                            'CallingId: (' . $callingId . ')<BR>' .
302                            'Suppress Option: (' . $parameters['suppress'] . ')<BR>' .
303                            '</div>';
304                    }
305
306                    $poweredBy = '<div class="panel-footing"><a class="minimap_badge" href="https://gerardnico.com/dokuwiki/minimap">'.$this->getConf(self::POWERED_BY).'</a></div>';
307                    // Header + list
308                    $renderer->doc .= '<div id="minimap__plugin"><div class="panel panel-default">'
309                        . $miniMapHeader
310                        . $miniMapList
311                        . $poweredBy
312                        . '</div></div>';
313                    break;
314            }
315
316            return true;
317        }
318        return false;
319
320    }
321
322    /**
323     * Return all pages and/of sub-namespaces (subdirectory) of a namespace (ie directory)
324     * Adapted from feed.php
325     *
326     * @param $namespace The container of the pages
327     * @param string $sort 'natural' to use natural order sorting (default); 'date' to sort by filemtime
328     * @param $listdirs - Add the directory to the list of files
329     * @return array An array of the pages for the namespace
330     */
331    function getNamespaceChildren($namespace, $sort = 'natural', $listdirs = false)
332    {
333        require_once(DOKU_INC . 'inc/search.php');
334        global $conf;
335
336        $ns = ':' . cleanID($namespace);
337        // ns as a path
338        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
339
340        $data = array();
341
342        // Options of the callback function search_universal
343        // in the search.php file
344        $search_opts = array(
345            'depth' => 1,
346            'pagesonly' => true,
347            'listfiles' => true,
348            'listdirs' => $listdirs,
349            'firsthead' => true
350        );
351        // search_universal is a function in inc/search.php that accepts the $search_opts parameters
352        search($data, $conf['datadir'], 'search_universal', $search_opts, $ns, $lvl = 1, $sort);
353
354        return $data;
355    }
356
357    /**
358     * Return the id of the start page of a namespace
359     *
360     * @param $id an id of a namespace (directory)
361     * @return string the id of the home page
362     */
363    function getNamespaceStartId($id)
364    {
365
366        global $conf;
367
368        $id = $id . ":";
369
370        if (page_exists($id . $conf['start'])) {
371            // start page inside namespace
372            $homePageId = $id . $conf['start'];
373        } elseif (page_exists($id . noNS(cleanID($id)))) {
374            // page named like the NS inside the NS
375            $homePageId = $id . noNS(cleanID($id));
376        } elseif (page_exists($id)) {
377            // page like namespace exists
378            $homePageId = substr($id, 0, -1);
379        } else {
380            // fall back to default
381            $homePageId = $id . $conf['start'];
382        }
383        return $homePageId;
384    }
385
386    /**
387     * @param $get_called_class
388     * @return string
389     */
390    public static function getTagName($get_called_class)
391    {
392        list(/* $t */, /* $p */, $c) = explode('_', $get_called_class, 3);
393        return (isset($c) ? $c : '');
394    }
395
396    /**
397     * @return string - the tag
398     */
399    public static function getTag()
400    {
401        return self::getTagName(get_called_class());
402    }
403
404
405}
406