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