1<?php
2
3
4/**
5 * Part of Subject Index plugin:
6 *
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author	   Symon Bent <hendrybadao@gmail.com>
10 */
11// must be run within Dokuwiki
12if(!defined('DOKU_INC')) die();
13
14if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
15if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
16if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
17
18require_once(DOKU_PLUGIN . 'syntax.php');
19require_once(DOKU_PLUGIN . 'subjectindex/inc/common.php');
20
21
22class syntax_plugin_subjectindex_index extends DokuWiki_Syntax_Plugin {
23
24    function getType() {
25        return 'substition';
26    }
27
28
29    function getPType() {
30        return 'block';
31    }
32
33
34    function getSort() {
35        return 98;
36    }
37
38
39    function connectTo($mode) {
40        // allow for multi-line syntax (clearer when writing many options)
41        $this->Lexer->addSpecialPattern('\{\{subjectindex>(?m).*?(?-m)\}\}', $mode, 'plugin_subjectindex_index');
42    }
43
44
45    function handle($match, $state, $pos, Doku_Handler $handler) {
46        global $ID;
47
48        $match = substr($match, 15, -2); // strip "{{subjectindex>...}}" markup
49
50        $opt = array();
51
52        // defaults
53        $opt['abstract']    = true;      // show snippet (abstract) of page content
54        $opt['border']      = 'none';    // show borders around table and between columns
55        $opt['cols']        = 1;         // number of columns in a SubjectIndex display page (max=12)
56        $opt['default']     = false;     // whether this display index page is the default for this index section number
57        $opt['hideatoz']    = false;     // turn off the A,B,C main headings
58        $opt['proper']      = false;     // use proper-case for page names
59        $opt['title']       = false;     // use title (first heading) instead of page name
60        $opt['section']     = 0;         // which section to use and display (0-9)...hopefully 10 is enough
61        $opt['showorder']   = false;     // display any bullet numbers used for ordering
62        $opt['label']       = '';        // table header label at top
63        $opt['regex']       = null;      // a regex for filtering the index list
64        $opt['hidejump']    = false;     // hide the 'jump to top' link
65
66        // remove any trailing spaces caused by multi-line syntax
67        $args = explode(';', $match);
68        $args = array_map('trim', $args);
69
70        foreach ($args as $arg) {
71            list($key, $value) = explode('=', $arg);
72            $key = strtolower($key);
73            switch ($key) {
74                case 'abstract':
75                case 'default':
76                case 'hideatoz':
77                case 'proper':
78                case 'showorder':
79                case 'showcount':
80                case 'hidejump':
81                case 'title':
82                    $opt[strtolower($key)] = true;
83                    break;
84                case 'border':
85                    switch ($value) {
86                        case 'none':
87                        case 'inside':
88                        case 'outside':
89                        case 'both':
90                            $opt['border'] = $value;
91                            break;
92                        default:
93                            $opt['border'] = 'both';
94                    }
95                    break;
96                case 'cols':
97                    if ($value < 1) {
98                        $value = 1;
99                    } elseif ($value > 12) {
100                        $value = 12;
101                    }
102                    $opt['cols'] = $value;
103                    break;
104                case 'section':
105                    $opt['section'] = ($value < 0) ? 0 : $value;
106                    break;
107                case 'label':
108                case 'regex':
109                    $opt[$key] = $value;
110                    break;
111                default:
112            }
113        }
114        // update the list of default target pages for entry links
115        if ($opt['default'] === true) {
116            SI_Utils::set_target_page($ID, $opt['section']);
117        }
118        return $opt;
119    }
120
121
122    function render($mode, Doku_Renderer $renderer, $opt) {
123        if ($mode == 'xhtml') {
124            $renderer->info['cache'] = false;
125
126            require_once (DOKU_INC . 'inc/indexer.php');
127            $all_pages = idx_getIndex('page', '');
128
129            $all_entries = SI_Utils::get_index();
130            if ($all_entries->is_empty()) {
131                $renderer->doc .= $this->getLang('empty_index');
132                return false;
133            }
134
135            // grab items for chosen index section only
136            $section_entries = $all_entries->filtered($opt['section'], $opt['regex']);
137            $count = count($section_entries->paths);
138            $lines = $this->_create_index($section_entries, $all_pages, $opt['hideatoz'], $opt['proper']);
139            $renderer->doc .= $this->_render_index($lines, $opt, $count);
140            return true;
141        } else {
142            return false;
143        }
144    }
145
146
147    // first build a list of valid subject entries to be rendered
148    private function _create_index(SI_Index $section_entries, $all_pages, $hideAtoZ, $proper) {
149
150        $lines = array();
151        $links = array();
152        $prev_path = '';
153
154        list($next_entry, $next_pid) = $section_entries->current();
155
156        do {
157
158            $entry = $anchor = $next_entry;
159            $pid = $next_pid;
160
161            // cache the next entry for comparison purposes later
162            list($next_entry, $next_pid) = $section_entries->next();
163
164            // remove any trailing whitespace which could falsify the later comparison
165            $page = rtrim($all_pages[$pid], "\n\r");
166
167            // skip to next page if it is not valid: exists, accessible, permitted
168            if ( ! SI_Utils::is_valid_page($page)) {
169                continue;
170            }
171
172            // note: all comparisons are case-less
173            // (this is an A-Z index after all humans don't distinguish between case when searching)
174            // Need to do this check BEFORE adding the A-Z headings below, because $entry is modified
175            $next_differs = strcasecmp($entry, $next_entry) !== 0;
176
177            // Create the A-Z heading
178            if ( ! $hideAtoZ) {
179                $matches = array();
180                $matched = preg_match('/(^\d+\.)?(.).+/', $entry, $matches);    // check for ordered entries 1st
181                if ($matched > 0) {
182                    $entry = $matches[1] . strtoupper($matches[2]) . '/' . $entry;
183                } else {
184                    $entry = strtoupper($entry[0]) . '/' . $entry;
185                }
186            }
187
188            $cur_node = strtok($entry, '/');
189            $cur_path = '';
190            $heading = 1;    // html heading number 1-6
191
192            do {
193                $is_heading = $is_link = false;
194
195                // build headers by adding each node
196                $cur_path .= (empty($cur_path)) ? $cur_node : '/' . $cur_node;
197
198                // we can add the page link(s) only if this is the final level;
199                // links take priority over headings!
200                $next_node = strtok('/');
201                if ($next_node === false) {
202                    $links[] = $page;
203                    $is_link = true;
204                // we only make headings if they are completely different from the previous
205                } elseif (strpos($prev_path, $cur_path) !== 0) {
206                    $is_heading = true;
207                }
208
209                //  the next_differs check ensures that links will be grouped
210                if (($is_link && $next_differs) || $is_heading) {
211                    if ($proper) {
212                        $cur_node = ucwords($cur_node);
213                    }
214                    if ($is_link) {
215                        $anchor = SI_Utils::valid_id($anchor);
216                        $lines[] = array($heading, $cur_node, $links, $anchor);
217                        $links = array();
218                    } else {
219                        $lines[] = array($heading, $cur_node, '' ,'');
220                    }
221                }
222                // forgive the magic no's = html h1 to h6 is fixed anyway
223                $heading = ($heading > 5) ? 6 : $heading + 1;
224                $cur_node = $next_node;
225
226            } while ($next_node !== false);
227
228            $prev_path = $entry;
229        } while ($section_entries->valid());
230
231        return $lines;
232    }
233
234
235    private function _render_index($lines, $opt, $count){
236        $links = '';
237        $label = '';
238        $show_count = '';
239        $show_jump = '';
240
241        // now render the subject index table
242
243        $outer_border = ($opt['border'] == 'outside' || $opt['border'] == 'both') ? 'border' : '';
244        $inner_border = ($opt['border'] == 'inside' || $opt['border'] == 'both') ? 'inner-border' : '';
245
246        // fixed point to jump back to at top of the table
247        $top_id = 'top-' . mt_rand();
248
249        if ($opt['label'] != '') {
250            $label = '<h1 class="title">' . $opt['label'] . '</h1>' . DOKU_LF;
251        }
252
253        // optional columns width adjustments
254        if ($count > SUBJ_IDX_HONOUR_COLS) {
255            $cols = $opt['cols'];
256        } else {
257            $cols = 1;
258        }
259        if (is_numeric($cols)) {
260            $col_style = 'column-count:' . $cols . '; -moz-column-count:' . $cols . '; -webkit-column-count:' . $cols . ';';
261        } else {
262            $col_style = 'column-width:' . $cols . '; -moz-column-width:' . $cols . '; -webkit-column-width:' . $cols . ';';
263        }
264
265        if ($opt['showcount'] === true) {
266            $show_count = '<div class="count">' . $count . ' ∞</div>' . DOKU_LF;
267        }
268        if ($opt['hidejump'] === false) {
269            $show_jump = '<a class="jump" href="#' . $top_id . '">' . $this->getLang('link_to_top') . '</a>' . DOKU_LF;
270        }
271
272        $subjectindex = '';
273        foreach ($lines as $line) {
274
275            // grab each entry line
276            list($heading, $cur_node, $pages, $anchor) = $line;
277
278            // remove the ordering number from the entry if requested
279            if ( ! $opt['showorder']) {
280                $matched = preg_match('/^\d+\.(.+)/', $cur_node, $matches);
281                if ($matched > 0) {
282                    $cur_node = $matches[1];
283                }
284            }
285            $indent_style = 'margin-left:' . ($heading - 1) * 10 . 'px';
286            $entry = '<h' . $heading . ' style="' . $indent_style . '"';
287
288            // render page links
289            if ( ! empty($pages)) {
290                $cnt = 0;
291                $freq = '';
292                foreach($pages as $page) {
293                    if ( ! empty($links)) {
294                        $links .= ' | ';
295                    }
296                    $links .= $this->_render_wikilink($page, $opt['proper'], $opt['title'], $opt['abstract'], $anchor);
297                    $cnt++;
298                }
299                if ($cnt > 1) {
300                    $freq = '<span class="frequency">' . count($pages) . '</span>';
301                }
302                $anchor = ' id="' . $anchor . '"';
303                $entry .= $anchor . '>' . $cur_node . $freq . '<span class="links">' . $links . '</span></h' . $heading . '>';
304
305                $links = '';
306
307            // render headings
308            } else {
309                $entry .= '>' . $cur_node . '</h' . $heading . '>';
310            }
311            $subjectindex .= $entry . DOKU_LF;
312        }
313
314        // actual rendering to wiki page
315        $render = '<div class="subjectindex ' . $outer_border . '" id="' . $top_id . '">' . DOKU_LF;
316        $render .= $label . DOKU_LF;;
317        $render .= '<div class="inner ' . $inner_border . '" style="' . $col_style . '">' . DOKU_LF;;
318        $render .= $subjectindex;
319        $render .= $show_count . $show_jump;
320        $render .= '</div></div>' . DOKU_LF;
321        return $render;
322    }
323
324
325    /**
326     * Renders a complete page link, plus tooltip, abstract, casing, etc...
327     *
328     * @param string $id
329     * @param bool $proper
330     * @param bool $title
331     * @param mixed $abstract
332     * @param string $anchor
333     * @return string
334     */
335    private function _render_wikilink($id, $proper, $title, $abstract, $anchor) {
336
337        $id = (strpos($id, ':') === false) ? ':' . $id : $id;   // : needed for root pages
338
339        // does the user want to see the "title" instead "pagename"
340        if ($title) {
341            $value = p_get_metadata($id, 'title', true);
342            $name = (empty($value)) ? $this->_proper(noNS($id)) : $value;
343        } elseif ($proper) {
344            $name = $this->_proper(noNS($id));
345        } else {
346            $name = '';
347        }
348
349        $link = html_wikilink($id, $name);
350        $link = $this->_add_page_anchor($link, $anchor);
351        // show the "abstract" as a tooltip
352        if ($abstract) {
353            $link = $this->_add_tooltip($link, $id);
354        }
355        return $link;
356    }
357
358
359    private function _proper($id) {
360         $id = str_replace(':', ': ', $id);
361         $id = str_replace('_', ' ', $id);
362         $id = ucwords($id);
363         $id = str_replace(': ', ':', $id);
364         return $id;
365    }
366
367
368    /**
369     * Swap normal link title (popup) for a more useful preview
370     *
371     * @param string $link  display name
372     * @param string $id    page id
373     * @return string
374     */
375    private function _add_tooltip($link, $id) {
376        $tooltip = $this->_get_abstract($id);
377        if (!empty($tooltip)) {
378            $tooltip = str_replace("\n", '  ', $tooltip);
379            $link = preg_replace('/title=\".+?\"/', 'title="' . $tooltip . '"', $link, 1);
380        }
381        return $link;
382    }
383
384
385    private function _get_abstract($id) {
386        $meta = \p_get_metadata($id, 'description abstract', true);
387        $meta = ( ! empty($meta)) ? htmlspecialchars($meta, ENT_NOQUOTES, 'UTF-8') : '';
388        return $meta;
389    }
390
391
392    private function _add_page_anchor($link, $anchor) {
393        $link = preg_replace('/\" class/', '#' . $anchor . '" class', $link, 1);
394        return $link;
395    }
396}