xref: /plugin/combo/syntax/minimap.php (revision bd93f1104c632036cc7e97f734e28b26a80f0b48)
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\LinkUtility;
10use ComboStrap\PluginUtility;
11
12if (!defined('DOKU_INC')) die();
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    const POWERED_BY = 'poweredby';
23
24    const STYLE_SNIPPET = <<<EOF
25<style>
26.nicon_folder_open {
27    background-image: url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" width="250" height="195"><g fill="rgb(204,204,204)" transform="translate(-7.897 -268.6)"><rect rx="0" y="286.829" x="12.897" height="175" width="200" opacity=".517"/><path d="M13.23 458.808l39.687-132.291h198.437l-39.687 132.291z" fill-rule="evenodd"/><rect rx="0" y="273.6" x="39.688" height="13" width="90"/></g></svg>');
28    display: inline-block;
29    width: 1.5em;
30    height: 1em;
31    vertical-align: middle;
32    content: "";
33    background-size: 100% 100%;
34}
35#minimap__plugin {
36    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
37    font-size: 14px;
38    line-height: 1.42857;
39}
40
41#minimap__plugin .panel-default {
42    border-color: #ddd;
43    box-sizing: border-box;
44}
45
46#minimap__plugin .panel {
47    box-sizing: border-box;
48    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
49    -moz-border-bottom-colors: none;
50    -moz-border-left-colors: none;
51    -moz-border-right-colors: none;
52    -moz-border-top-colors: none;
53    background-color: #fff;
54    border-image-outset: 0 0 0 0;
55    border-image-repeat: stretch stretch;
56    border-image-slice: 100% 100% 100% 100%;
57    border-image-source: none;
58    border-image-width: 1 1 1 1;
59    border-radius: 4px;
60    border: 1px solid;
61    margin-bottom: 20px;
62    display: block;
63    color: #ddd;
64}
65
66#minimap__plugin .panel-default > .panel-heading {
67    background: #f5f5f5 linear-gradient(to bottom, #f5f5f5 0px, #e8e8e8 100%) repeat-x;
68    border-color: #ddd;
69    color: #333;
70}
71
72#minimap__plugin .panel-heading {
73    border-bottom: 1px solid;
74    border-top-left-radius: 3px;
75    border-top-right-radius: 3px;
76    padding: 10px 15px;
77    box-sizing: border-box;
78    display: block;
79    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
80    font-size: 14px;
81    line-height: 1.42857;
82}
83
84#minimap__plugin .panel > .list-group, #minimap__plugin .panel > .panel-collapse > .list-group {
85    margin-bottom: 0;
86}
87
88#minimap__plugin .list-group {
89    border-radius: 4px;
90    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
91    padding-left: 0;
92    box-sizing: border-box;
93    color: #333;
94}
95
96#minimap__plugin .panel-heading + .list-group .list-group-item:first-child {
97    border-top-width: 0;
98}
99
100#minimap__plugin .panel > .list-group .list-group-item,
101#minimap__plugin .panel > .panel-collapse > .list-group .list-group-item {
102    border-bottom-width: 1px;
103    border-left-width: 0;
104    border-right-width: 0;
105    border-radius: 0;
106}
107
108#minimap__plugin .list-group-item {
109    -moz-border-bottom-colors: none;
110    -moz-border-left-colors: none;
111    -moz-border-right-colors: none;
112    -moz-border-top-colors: none;
113    background-color: #fff;
114    border-image-outset: 0 0 0 0;
115    border-image-repeat: stretch stretch;
116    border-image-slice: 100% 100% 100% 100%;
117    border-image-source: none;
118    border-image-width: 1 1 1 1;
119    /*border: solid #ddd;*/
120    display: block;
121    padding: 10px 15px;
122    position: relative;
123    box-sizing: border-box;
124    margin: 0 0 -1px;
125}
126
127#minimap__plugin .label-primary {
128    background-color: #337ab7;
129}
130
131#minimap__plugin .label {
132    border-radius: 0.25em;
133    color: #fff;
134    display: inline;
135    font-size: 75%;
136    font-weight: 700;
137    line-height: 1;
138    padding: 0.2em 0.6em 0.3em;
139    text-align: center;
140    vertical-align: baseline;
141    white-space: nowrap;
142    box-sizing: border-box;
143}
144
145/* Active link css */
146#minimap__plugin .list-group-item.active,
147#minimap__plugin .list-group-item.active:focus,
148#minimap__plugin .list-group-item.active:hover {
149    background: #f5f5f5 linear-gradient(to bottom, #f5f5f5 0px, #e8e8e8 100%) repeat-x;
150    border-color: #ddd;
151    color: #333;
152    text-shadow: none;
153}
154
155#minimap__plugin .list-group-item.active {
156    background-color: #e8e8e8 ! important;
157    z-index: 2;
158}
159
160
161#minimap__plugin .panel-body {
162    clear: both;
163    content: " ";
164    box-sizing: border-box;
165    display: table;
166    padding: 15px;
167    unicode-bidi: -moz-isolate;
168    color: #333;
169}
170
171#minimap__plugin .glyphicon {
172    /*already same color than the header*/
173    color: #d8d2d2;
174}
175
176#minimap__plugin .panel-footing {
177    display: flex;
178    padding: 0.10rem;
179    background: #f5f5f5 linear-gradient(to bottom,#f5f5f5 0px,#e8e8e8 100%) repeat-x;
180}
181
182#minimap__plugin .minimap_badge {
183
184    border-radius: 0.25em;
185    background-color: #d8d2d2;
186    font-size: 0.6rem;
187    padding: 0.25rem;
188    margin-left: auto !important;
189    margin-right: 0.3rem;
190    color: #1d4a71;
191    margin: 0.1rem;
192}
193</style>
194EOF;
195
196
197    function connectTo($aMode)
198    {
199        $pattern = '<' . self::MINIMAP_TAG_NAME . '[^>]*>';
200        $this->Lexer->addSpecialPattern($pattern, $aMode, PluginUtility::getModeForComponent($this->getPluginComponent()));
201    }
202
203    function getSort()
204    {
205        /**
206         * One less than the old one
207         */
208        return 149;
209    }
210
211    /**
212     * No p element please
213     * @return string
214     */
215    function getPType()
216    {
217        return 'block';
218    }
219
220    function getType()
221    {
222        // The spelling is wrong but this is a correct value
223        // https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
224        return 'substition';
225    }
226
227    /**
228     *
229     * The handle function goal is to parse the matched syntax through the pattern function
230     * and to return the result for use in the renderer
231     * This result is always cached until the page is modified.
232     * @param string $match
233     * @param int $state
234     * @param int $pos
235     * @param Doku_Handler $handler
236     * @return array|bool
237     * @see DokuWiki_Syntax_Plugin::handle()
238     *
239     */
240    function handle($match, $state, $pos, Doku_Handler $handler)
241    {
242
243        switch ($state) {
244
245            // As there is only one call to connect to in order to a add a pattern,
246            // there is only one state entering the function
247            // but I leave it for better understanding of the process flow
248            case DOKU_LEXER_SPECIAL :
249
250                // Parse the parameters
251                $match = substr($match, 8, -1); //9 = strlen("<minimap")
252
253                // Init
254                $parameters = array();
255                $parameters['substr'] = 1;
256                $parameters[self::INCLUDE_DIRECTORY_PARAMETERS] = $this->getConf(self::INCLUDE_DIRECTORY_PARAMETERS);
257                $parameters[self::SHOW_HEADER] = $this->getConf(self::SHOW_HEADER);
258
259
260                // /i not case sensitive
261                $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]{1}([^\`\"]*)[\'\"]{1}\\s*";
262                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
263                if ($result != 0) {
264                    foreach ($matches[1] as $key => $parameterKey) {
265                        $parameter = strtolower($parameterKey);
266                        $value = $matches[2][$key];
267                        if (in_array($parameter, [self::SHOW_HEADER, self::INCLUDE_DIRECTORY_PARAMETERS])) {
268                            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
269                        }
270                        $parameters[$parameter] = $value;
271                    }
272                }
273                // Cache the values
274                return array($state, $parameters);
275
276        }
277
278        return false;
279    }
280
281
282    function render($mode, Doku_Renderer $renderer, $data)
283    {
284
285        // The $data variable comes from the handle() function
286        //
287        // $mode = 'xhtml' means that we output html
288        // There is other mode such as metadata where you can output data for the headers (Not 100% sure)
289        if ($mode == 'xhtml') {
290
291            /** @var Doku_Renderer_xhtml $renderer */
292
293            // Unfold the $data array in two separates variables
294            list($state, $parameters) = $data;
295
296            // As there is only one call to connect to in order to a add a pattern,
297            // there is only one state entering the function
298            // but I leave it for better understanding of the process flow
299            switch ($state) {
300
301                case DOKU_LEXER_SPECIAL :
302
303                    if (!PluginUtility::htmlSnippetAlreadyAdded($renderer->info,self::MINIMAP_TAG_NAME)){
304                        $renderer->doc .= self::STYLE_SNIPPET;
305                    };
306
307                    global $ID;
308                    global $INFO;
309                    $callingId = $ID;
310                    // If mini-map is in a sidebar, we don't want the ID of the sidebar
311                    // but the ID of the page.
312                    if ($INFO != null) {
313                        $callingId = $INFO['id'];
314                    }
315
316                    $nameSpacePath = getNS($callingId); // The complete path to the directory
317                    if (array_key_exists(self::NAMESPACE_KEY_ATT, $parameters)) {
318                        $nameSpacePath = $parameters[self::NAMESPACE_KEY_ATT];
319                    }
320                    $currentNameSpace = curNS($callingId); // The name of the container directory
321                    $includeDirectory = $parameters[self::INCLUDE_DIRECTORY_PARAMETERS];
322                    $pagesOfNamespace = $this->getNamespaceChildren($nameSpacePath, $sort = 'natural', $listdirs = $includeDirectory);
323
324                    // Set the two possible home page for the namespace ie:
325                    //   - the name of the containing map ($homePageWithContainingMapName)
326                    //   - the start conf parameters ($homePageWithStartConf)
327                    global $conf;
328                    $parts = explode(':', $nameSpacePath);
329                    $lastContainingNameSpace = $parts[count($parts) - 1];
330                    $homePageWithContainingMapName = $nameSpacePath . ':' . $lastContainingNameSpace;
331                    $startConf = $conf['start'];
332                    $homePageWithStartConf = $nameSpacePath . ':' . $startConf;
333
334                    // Build the list of page
335                    $miniMapList = '<ul class="list-group">';
336                    $pageNum = 0;
337                    $startPageFound = false;
338                    $homePageFound = false;
339                    //$pagesCount = count($pagesOfNamespace); // number of pages in the namespace
340                    foreach ($pagesOfNamespace as $pageArray) {
341
342                        // The title of the page
343                        $title = '';
344
345                        // If it's a directory
346                        if ($pageArray['type'] == "d") {
347
348                            $pageId = $this->getNamespaceStartId($pageArray['id']);
349
350                        } else {
351
352                            $pageNum++;
353                            $pageId = $pageArray['id'];
354
355                        }
356                        $link = new LinkUtility($pageId);
357
358
359                        /**
360                         * Set name and title
361                         */
362                        // Name if the variable that it's shown. A part of it can be suppressed
363                        // Title will stay full in the link
364                        $h1TargetPage = $link->getInternalPage()->getH1();
365                        $title = $link->getInternalPage()->getTitle();
366
367                        $link->setName(noNSorNS($pageId));
368                        if ($h1TargetPage !=null) {
369                            $link->setName($h1TargetPage);
370                        } else {
371                            if ($title!=null) {
372                                $link->setName($title);
373                            }
374                        }
375                        $link->setTitle(noNSorNS($pageId));
376                        if ($title!=null) {
377                            $link->setTitle($title);
378                        }
379
380                        // If debug mode
381                        if ($parameters['debug']) {
382                            $link->setTitle($link->getTitle().' (' . $pageId . ')');
383                        }
384
385                        // Add the page number in the URL title
386                        $link->setTitle($link->getTitle() .' (' . $pageNum . ')');
387
388                        // Suppress the parts in the name with the regexp defines in the 'suppress' params
389                        if ($parameters['suppress']) {
390                            $substrPattern = '/' . $parameters['suppress'] . '/i';
391                            $replacement = '';
392                            $name = preg_replace($substrPattern, $replacement, $link->getName());
393                            $link->setName($name);
394                        }
395
396                        // See in which page we are
397                        // The style will then change
398                        $active = '';
399                        if ($callingId == $pageId) {
400                            $active = 'active';
401                        }
402
403                        // Not all page are printed
404                        // sidebar are not for instance
405
406                        // Are we in the root ?
407                        if ($pageArray['ns']) {
408                            $nameSpacePathPrefix = $pageArray['ns'] . ':';
409                        } else {
410                            $nameSpacePathPrefix = '';
411                        }
412                        $print = true;
413                        if ($pageArray['id'] == $nameSpacePathPrefix . $currentNameSpace) {
414                            // If the start page exists, the page with the same name
415                            // than the namespace must be shown
416                            if (page_exists($nameSpacePathPrefix . $startConf)) {
417                                $print = true;
418                            } else {
419                                $print = false;
420                            }
421                            $homePageFound = true;
422                        } else if ($pageArray['id'] == $nameSpacePathPrefix . $startConf) {
423                            $print = false;
424                            $startPageFound = true;
425                        } else if ($pageArray['id'] == $nameSpacePathPrefix . $conf['sidebar']) {
426                            $pageNum -= 1;
427                            $print = false;
428                        };
429
430
431                        // If the page must be printed, build the link
432                        if ($print) {
433
434                            // Open the item tag
435                            $miniMapList .= "<li class=\"list-group-item " . $active . "\">";
436
437                            // Add a glyphicon if it's a directory
438                            if ($pageArray['type'] == "d") {
439                                $miniMapList .= "<span class=\"nicon_folder_open\" aria-hidden=\"true\"></span>&nbsp;&nbsp;";
440                            }
441
442                            $miniMapList .= $link->render($renderer);;
443
444
445                            // Close the item
446                            $miniMapList .= "</li>";
447
448                        }
449
450                    }
451                    $miniMapList .= '</ul>'; // End list-group
452
453
454                    // Build the panel header
455                    $miniMapHeader = "";
456                    $startId = "";
457                    if ($startPageFound) {
458                        $startId = $homePageWithStartConf;
459                    } else {
460                        if ($homePageFound) {
461                            $startId = $homePageWithContainingMapName;
462                        }
463                    }
464
465                    $panelHeaderContent = "";
466                    if ($startId == "") {
467                        if ($parameters[self::SHOW_HEADER] == true) {
468                            $panelHeaderContent = 'No Home Page found';
469                        }
470                    } else {
471                        $startLink = new LinkUtility($startId);
472                        $startLink->setName($startId);
473                        $h1 = $startLink->getInternalPage()->getH1();
474                        if ($h1!=null){
475                            $startLink->setName($h1);
476                        }
477                        $panelHeaderContent = $startLink->render($renderer);
478                        // We are not counting the header page
479                        $pageNum--;
480                    }
481
482                    if ($panelHeaderContent != "") {
483                        $miniMapHeader .= '<div class="panel-heading">' . $panelHeaderContent . '  <span class="label label-primary">' . $pageNum . ' pages</span></div>';
484                    }
485
486                    if ($parameters['debug']) {
487                        $miniMapHeader .= '<div class="panel-body">' .
488                            '<B>Debug Information:</B><BR>' .
489                            'CallingId: (' . $callingId . ')<BR>' .
490                            'Suppress Option: (' . $parameters['suppress'] . ')<BR>' .
491                            '</div>';
492                    }
493
494                    // Header + list
495                    $renderer->doc .= '<div id="minimap__plugin"><div class="panel panel-default">'
496                        . $miniMapHeader
497                        . $miniMapList
498                        . '</div></div>';
499                    break;
500            }
501
502            return true;
503        }
504        return false;
505
506    }
507
508    /**
509     * Return all pages and/of sub-namespaces (subdirectory) of a namespace (ie directory)
510     * Adapted from feed.php
511     *
512     * @param $namespace The container of the pages
513     * @param string $sort 'natural' to use natural order sorting (default); 'date' to sort by filemtime
514     * @param $listdirs - Add the directory to the list of files
515     * @return array An array of the pages for the namespace
516     */
517    function getNamespaceChildren($namespace, $sort = 'natural', $listdirs = false)
518    {
519        require_once(DOKU_INC . 'inc/search.php');
520        global $conf;
521
522        $ns = ':' . cleanID($namespace);
523        // ns as a path
524        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
525
526        $data = array();
527
528        // Options of the callback function search_universal
529        // in the search.php file
530        $search_opts = array(
531            'depth' => 1,
532            'pagesonly' => true,
533            'listfiles' => true,
534            'listdirs' => $listdirs,
535            'firsthead' => true
536        );
537        // search_universal is a function in inc/search.php that accepts the $search_opts parameters
538        search($data, $conf['datadir'], 'search_universal', $search_opts, $ns, $lvl = 1, $sort);
539
540        return $data;
541    }
542
543    /**
544     * Return the id of the start page of a namespace
545     *
546     * @param $id an id of a namespace (directory)
547     * @return string the id of the home page
548     */
549    function getNamespaceStartId($id)
550    {
551
552        global $conf;
553
554        $id = $id . ":";
555
556        if (page_exists($id . $conf['start'])) {
557            // start page inside namespace
558            $homePageId = $id . $conf['start'];
559        } elseif (page_exists($id . noNS(cleanID($id)))) {
560            // page named like the NS inside the NS
561            $homePageId = $id . noNS(cleanID($id));
562        } elseif (page_exists($id)) {
563            // page like namespace exists
564            $homePageId = substr($id, 0, -1);
565        } else {
566            // fall back to default
567            $homePageId = $id . $conf['start'];
568        }
569        return $homePageId;
570    }
571
572    /**
573     * @param $get_called_class
574     * @return string
575     */
576    public static function getTagName($get_called_class)
577    {
578        list(/* $t */, /* $p */, $c) = explode('_', $get_called_class, 3);
579        return (isset($c) ? $c : '');
580    }
581
582    /**
583     * @return string - the tag
584     */
585    public static function getTag()
586    {
587        return self::getTagName(get_called_class());
588    }
589
590
591}
592