1<?php
2
3/**
4 * NavBox Plugin for DokuWiki (Syntax Component)
5 *
6 * This plugin enables the ability to have a 'navbox' of related articles
7 * similar to the way Wikipedia does on some pages.
8 *
9 * Wikipedia Example: https://en.wikipedia.org/wiki/Singapore
10 * Scroll to the bottom to see "Singapore Articles" section
11 *
12 * @license GPL 2 https://www.gnu.org/licenses/gpl-2.0.html
13 * @author Jovin Sveinbjornsson
14 * @author Midgard Apps <hello@midgardapps.com>
15 */
16
17// Must be run within DokuWiki
18if (!defined('DOKU_INC')) die();
19
20class syntax_plugin_navbox extends DokuWiki_Syntax_Plugin {
21
22    /**
23     * What kind of syntax?
24     */
25    public function getType() {
26        return 'container';
27    }
28
29    /**
30     * How do we handle paragraphs?
31     */
32    public function getPType() {
33        return 'block';
34    }
35
36    /*
37     * When should this be executed?
38     */
39    public function getSort() {
40        return 205;
41    }
42
43    public function getAllowedTypes() {
44        return array('container', 'formatting', 'substition', 'disabled', 'protected', 'paragraphs');
45    }
46
47    /**
48     * Connect Lookup pattern to lexer
49     *
50     * @param string $mode Parser mode
51     */
52    public function connectTo($mode) {
53        $this->Lexer->addSpecialPattern('<navbox>.*?</navbox>', $mode, 'plugin_navbox');
54    }
55
56    /**
57     * Handler to match the data and kick off rendering
58     *
59     * @param string $match The text matched by the patterns
60     * @param int $state The lexer state for the match
61     * @param int $pos The character position of the matched text
62     * @param Doku_Handler $handler The Doku_Handler object
63     *
64     * @return array Data for the renderer
65     */
66    public function handle($match, $state, $pos, Doku_Handler $handler) {
67        // Remove the <naxbox> and </navbox>
68        $match = substr($match, 8, -9);
69        // Separate the content into individual lines
70        $lines = explode("\n", $match);
71        // We'll store all our variables in here for processing later
72        $navbox = array();
73        // Switches
74        $groupType = 0; // 0 = none, 1 = group, 2 = subgroup
75        $autoSub = false;
76        // Temporary Variables
77        $currentGroup = array();
78        $current = '';
79        $currentSub = '';
80
81
82        // Loop over while we continue to have more to process
83        while(count($lines) > 0) {
84            // Clean up and work only with the current line, remove it from the remaining array
85            $line = trim(array_shift($lines));
86            // If it's not valid, skip
87            if (strlen($line) < 1) continue;
88
89            // This if/else cascade proceeds in Specific -> Less Specific for syntax
90            if (strpos($line, '### !') !== false) {
91                // Subgroup with Advanced Syntax
92                // Turn on the 'subgroup' flag
93                $autoSub = true;
94            } else if (strpos($line, '### ') !== false) {
95                // Subgroup
96                // Name our Subgroup
97                $currentSub = substr($line, 4);
98                // Set the group type so we an add links appropriately
99                $groupType = 2;
100                // No further processing required
101                continue;
102            } else if (strpos($line, '## ') !== false) {
103                // Group
104                // Check if we already have a group, if so, do this
105                if (!empty($currentGroup)) {
106                    // Store the current group
107                    $navbox[$current] = $currentGroup;
108                    // Start a new group
109                    $currentGroup = array();
110                    // Clear the Subgroup name too
111                    $currentSub = '';
112                }
113                // Name our new group
114                $current = substr($line, 3);
115                // Set the group type so we can add links appropriately
116                $groupType = 1;
117                // No further processing required
118                continue;
119            } else if (strpos($line, '# ') !== false) {
120                // Title
121                // Store the title
122                $navbox['title'] = substr($line, 2);
123                // No further processing required
124                continue;
125            } else if (substr($line, 0, 2) == '[[') {
126                // We have a list of links
127                // These are the valid separators for the links, also no separators are valid too
128                $separators = [',', ';'];
129                // If we are dealign with a Group
130                if ($groupType == 1) {
131                    // Store the links in the 'default' section
132                    $currentGroup['default'] = str_replace($separators, '', $line);
133                } else if ($groupType == 2) {
134                    // We are dealing with a Subgroup instead
135                    // Store the links in the current Subgroup
136                    $currentGroup[$currentSub] = str_replace($separators, '', $line);
137                }
138            } else {
139                // This is a automated flag, unset all switches
140                $autoSub = false;
141                $groupType = 0;
142            }
143
144            // The below will identify what kind of automated generation is required
145            if (strpos($line, '!ns') !== false) {
146                // A Namespace listing
147                // Offset if auto space is used
148                $offset = 0;
149                if ($autoSub) {
150                    $offset = 4;
151                }
152                // Get the current namespace
153                $namespace = pageinfo()['namespace'];
154                // If the +n parameter is used, change the namspace
155                if (strpos($line, '+n') !== false) {
156                    // Custom Namespace
157                    $namespace = substr($line, 8 + $offset, -2);
158                }
159                // Get the lowest level namespace, this is our automatic title
160                $title = array_pop(explode(':', $namespace));
161                // If the +t parameter is used, change the title
162                if (strpos($line, '+t') !== false) {
163                    // Custom Title
164                    $title = substr($line, 6 + $offset);
165                }
166                // If the +nt parameter is used, change the namespace and title
167                if (strpos($line, '+nt') !== false) {
168                    // Find where the namespace begins
169                    $nsStart = strpos($line, '[[') + 2;
170                    // Find where the title begins
171                    $tStart = strpos($line, '|') + 1;
172                    // Extract the title
173                    $title = substr($line, $tStart, -2);
174                    // Extract the namespace
175                    $namespace = substr($line, $nsStart, ($tStart - $nsStart - 1));
176                }
177                // String for the working directory of the namespace
178                $dir = './data/pages/'.str_replace(':', '/', $namespace);
179                // Instantiate our Links variable
180                $links = '';
181                // Look in the directory and get all .txt files (doku pages)
182                foreach (glob($dir.'/*.txt') as $filename) {
183                    // Store each file as a new markup link
184                    $links .= '[['.str_replace('/', ':', substr($filename, 13, -4)).']]';
185                }
186                // Identify if this should be a subgroup
187                if ($autoSub) {
188                    // Append to the parent group
189                    $currentGroup[$title] = $links;
190                } else {
191                    // Add as a main level group
192                    $navbox[$title]['default'] = $links;
193                }
194            } else if (strpos($line, '!tree') !== false) {
195                // The hierarchy of this page
196            } else if (strpos($line, '!tag') !== false) {
197                // Tag listing, need to use the pagelist plugin for this one
198                // This is a stretch goal, well and truly
199            }
200
201            // We are working on the last line group, store our groups
202            if (count($lines) == 1) {
203                if (!empty($currentGroup)) {
204                    $navbox[$current] = $currentGroup;
205                }
206            }
207        }
208        //echo '<pre>';
209        //var_dump($navbox);
210        //echo '</pre>';
211
212        return $navbox;
213    }
214
215    /**
216     * Handles the actual output creation
217     *
218     * @param string $mode Renderer mode (supported modes: xhtml)
219     * @param Doku_Renderer $renderer The renderer
220     * @param array $data The data from the handler() function
221     *
222     * @return bool If rendering was successful
223     */
224    public function render($mode, Doku_Renderer $renderer, $data) {
225        if ($mode != 'xhtml') return false;
226        // Prevent caching
227        $renderer->info['cache'] = false;
228
229        // Build the beginnings of the table
230        $html = '<div class="pgnb_container"><table class="pgnb_table"><tr><th class="pgnb_title" colspan="2"><span class="pgnb_title_text">';
231
232        // Placeholder for our xhtml formatted URL
233        $url = '';
234
235        // Add in the title, parse it first to generate any URLs present
236        $html .= $this->urlRender($data['title']);
237        // Prepare for the groups
238        $html .= '</span></th></tr>';
239
240        // Get rid of the title to iterate over the groups
241        array_shift($data);
242
243        // Placeholder for our Group
244        $ghtml = '';
245
246        // Go through each item group and build their row
247        foreach ($data as $group => $items) {
248            // Placeholder for group HTML while we build it,  Add in the group title, and prepare for the items
249            $ghtml = '<tr><th class="pgnb_group_title">'.$this->urlRender($group).'</th><td class="pgnb_group">';
250
251            // Flag for formatting the child table
252            $subgroupPresent = false;
253
254            // Iterate over each subgroup, there will always be a 'default'
255            foreach ($items as $subgroup => $subitems) {
256                // Render all the links
257                $urls = $this->urlRender($subitems);
258                // Format into the list
259                $urls = str_replace("<a", "<li><a", $urls);
260                $urls = str_replace("</a>", "</a></li>", $urls);
261
262                // The base group
263                if ($subgroup == 'default') {
264                    // Append the list of URLs
265                    $ghtml .= '<div style="padding:0.25em;"><ul class="pgnb_list">'.$urls.'</ul></div>';
266                } else {
267                    // We are working with a subgroup, additional HTML tags required
268                    // If we don't already have a child table for the subgroups, create one
269                    if (!$subgroupPresent) {
270                        // This is our first subgroup
271                        $ghtml .= '<table class="pgnb_child_table">';
272                        // Turn on the switch
273                        $subgroupPresent = true;
274                    }
275                    // Append the row for the subgroup
276                    $ghtml .= '<tr><th class="pgnb_subgroup_title">'.$this->urlRender($subgroup).'</th><td class="pgnb_group"><div style=padding:0.25em;"<ul class="pgnb_list">'.$urls."</ul></div></td></tr>";
277                }
278            }
279
280            // We had subgroups, close off the child table
281            if ($subgroupPresent) {
282                $ghtml .= '</table>';
283            }
284
285            // Close the group
286            $ghtml .= '</td></tr>';
287
288            // Append the group to our HTML
289            $html .= $ghtml;
290            // Reset our placeholder
291            $ghtml = '';
292        }
293
294        // Close out the table
295        $html .= '</table></div>';
296
297        $renderer->doc .= $html;
298
299        return true;
300    }
301
302    /**
303     * Handles rendering of DokuWiki links to URLs for all kinds of URL
304     *
305     * @param string $item The DokuWiki markup to be converted
306     *
307     * @return string The XHTML rendering of the markup
308     */
309    private function urlRender($item) {
310        // Create the parser
311        $urlParser = & new Doku_Parser();
312        // Add a handler
313        $urlParser->Handler = & new Doku_Handler();
314        // Add all the parsing modes for various URLs
315        $urlParser->addMode('camelcaselink',new Doku_Parser_Mode_CamelCaseLink());
316        $urlParser->addMode('internallink',new Doku_Parser_Mode_InternalLink());
317        $urlParser->addMode('media',new Doku_Parser_Mode_Media());
318        $urlParser->addMode('externallink',new Doku_Parser_Mode_ExternalLink());
319        $urlParser->addMode('emaillink',new Doku_Parser_Mode_EmailLink());
320        $urlParser->addMode('windowssharelink',new Doku_Parser_Mode_WindowsShareLink());
321        $urlParser->addMode('filelink',new Doku_Parser_Mode_FileLink());
322        $urlParser->addMode('eol',new Doku_Parser_Mode_Eol());
323        // Parse the string into instructions
324        $instructions = $urlParser->parse($item);
325        // Create the renderer
326        $urlRenderer = & new Doku_Renderer_XHTML();
327        // Iterate over each instruction
328        foreach ($instructions as $instruction) {
329            // Execute the callback against the renderer
330            call_user_func_array(array(&$urlRenderer, $instruction[0]), $instruction[1]);
331        }
332        // Extract the XHTML data
333        $url = $urlRenderer->doc;
334        // Return the XHTML excluding the <p> and </p> tags
335        return substr($url, 5, strlen($url)-11);
336    }
337}
338
339?>