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}