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