1<?php 2/* 3 * By Raphael Reitzig, 2012 4 * Edited by Hans-Nikolai Viessmann, 2016 5 * version 2.0 6 * code@verrech.net 7 * http://lmazy.verrech.net 8 */ 9?> 10<?php 11/* 12 This program is free software: you can redistribute it and/or modify 13 it under the terms of the GNU General Public License as published by 14 the Free Software Foundation, either version 3 of the License, or 15 (at your option) any later version. 16 17 This program is distributed in the hope that it will be useful, 18 but WITHOUT ANY WARRANTY; without even the implied warranty of 19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 GNU General Public License for more details. 21 22 You should have received a copy of the GNU General Public License 23 along with this program. If not, see <http://www.gnu.org/licenses/>. 24*/ 25?> 26<?php 27 28// Use the slightly modified BibTex parser from PEAR. 29require_once('lib/PEAR5.php'); 30require_once('lib/PEAR.php'); 31require_once('lib/BibTex.php'); 32 33// Some stupid functions 34require_once('helper.inc.php'); 35 36/** 37 * This class provides a method that parses bibtex files to 38 * other text formats based on a template language. See 39 * http://lmazy.verrech.net/bib2tpl/ 40 * for documentation. 41 * 42 * @author Raphael Reitzig 43 * @author Hans-Nikolai Viessmann <hv15@hw.ac.uk> 44 * @version 2.0 45 */ 46class BibtexConverter { 47 /** 48 * BibTex parser 49 * 50 * @access private 51 * @var Structures_BibTex 52 */ 53 private static $parser; 54 55 /** 56 * Options array. May contain the following pairs: 57 * only => array([$field => $regexp], ...) 58 * group => (none|firstauthor|entrytype|$field) 59 * order_groups => (asc|desc) 60 * sort_by => (DATE|$field) 61 * order => (asc|desc) 62 * lang => xy (where lang/xy.php exists) 63 * key => string (exposed in template at global-scope) 64 * @access private 65 * @var array 66 */ 67 private $options; 68 69 /** 70 * Callback to a function that takes a string (taken from a 71 * BibTeX field) and clears it up for output. 72 * @access private 73 * @var callback 74 */ 75 private $sanitise; 76 77 /** 78 * Helper object with support functions. 79 * @access private 80 * @var Helper 81 */ 82 private $helper; 83 84 /** 85 * Array with author names and replacement. 86 * @access private 87 * @var assoc. array 88 */ 89 private $authorlist; 90 91 /** 92 * Constructor. 93 * 94 * @access public 95 * @param array $options Options array. May contain the following pairs: 96 * - only => array([$field => $regexp], ...) 97 * - group => (none|year|firstauthor|entrytype|$field) 98 * - order_groups => (asc|desc) 99 * - sort_by => (DATE|$field) 100 * - order => (asc|desc) 101 * - lang => any string as long as proper lang/$s.php exists 102 * For details see documentation. 103 * @param callback $sanitise Callback to a function that takes a string (taken from a 104 * BibTeX field) and clears it up for output. Default is the 105 * identity function. 106 */ 107 function __construct($options=array(), $sanitise=null, $authors=null) { 108 // Default options 109 $this->options = array( 110 'only' => array(), 111 'group' => 'year', 112 'order_groups' => 'desc', 113 'sort_by' => 'DATE', 114 'order' => 'desc', 115 'lang' => 'en', 116 'key' => '' 117 ); 118 119 // lame replacement for non-constant default parameter 120 if ( !empty($sanitise) ) { 121 $this->sanitise = $sanitise; 122 } 123 else { 124 $this->sanitise = create_function('$i', 'return $i;'); 125 } 126 127 // Overwrite default options 128 foreach ( $this->options as $key => $value ) { 129 if ( !empty($options[$key]) ) { 130 $this->options[$key] = $options[$key]; 131 } 132 } 133 134 /* Load translations. 135 * We assume that the english language file is always there. 136 */ 137 if ( is_readable(dirname(__FILE__).'/lang/'.$this->options['lang'].'.php') ) { 138 require('lang/'.$this->options['lang'].'.php'); 139 } 140 else { 141 require('lang/en.php'); 142 } 143 $this->options['lang'] = $translations; 144 145 $this->helper = new Helper($this->options); 146 147 148 $this->authorlist = array(); 149 foreach(preg_split("/((\r?\n)|(\r\n?))/", $authors) as $line){ 150 $tmp = explode(" ",$line,2); 151 $this->authorlist[$tmp[1]] = "[[".$tmp[0]."|".$tmp[1]."]]"; 152 } 153 154 } 155 156 /** 157 * Parses the specified BibTeX string into an array with entries of the form 158 * $entrykey => $entry. The result can be used with BibtexConverter::convert. 159 * 160 * @access public 161 * @param string $bibtex BibTeX code 162 * @return array Array with data from passed BibTeX 163 */ 164 static function parse(&$bibtex) { 165 if ( !isset(self::$parser) ) { 166 self::$parser = new Structures_BibTex(array('removeCurlyBraces' => false)); 167 } 168 169 self::$parser->loadString($bibtex); 170 $stat = self::$parser->parse(); 171 172 if ( PEAR::isError($stat) ) { 173 return $stat; 174 } 175 176 $parsed = self::$parser->data; 177 $result = array(); 178 foreach ( $parsed as &$entry ) { 179 $result[$entry['entrykey']] = $entry; 180 } 181 182 return $result; 183 } 184 185 /** 186 * Parses the given BibTeX string and applies its data to the passed template string. 187 * If $bibtex is an array (which has to be parsed by BibtexConverter::parse) 188 * parsing is skipped. 189 * 190 * @access public 191 * @param string|array $bibtex BibTeX code or parsed array 192 * @param string $template template code 193 * @param array $replacementKeys An array with entries of the form $entrykey => $newKey. 194 * If an entrykey occurrs here, it will be replaced by 195 * its correspoding newKey in the output. 196 * @return string|PEAR_Error Result string or PEAR_Error on failure 197 */ 198 function convert($bibtex, &$template, &$replacementKeys=array()) { 199 // If there are no grouping tags, disable grouping. 200 if ( preg_match('/@\{group@/s', $template) + preg_match('/@\}group@/s', $template) < 2 ) { 201 $groupingDisabled = $this->options['group']; 202 $this->options['group'] = 'none'; 203 } 204 205 // If grouping is off, remove grouping tags. 206 if ( $this->options['group'] === 'none' ) { 207 $template = preg_replace(array('/@\{group@/s', '/@\}group@/s'), '', $template); 208 } 209 210 // Parse if necessary 211 if ( is_array($bibtex) ) { 212 $data = $bibtex; 213 } 214 else { 215 $data = self::parse($bibtex); 216 } 217 218 $data = $this->filter($data, $replacementKeys); 219 $data = $this->group($data); 220 $data = $this->sort($data); 221 $result = $this->translate($data, $template); 222 223 /* If grouping was disabled because of the template, restore the former 224 * setting for future calls. */ 225 if ( !empty($groupingDisabled) ) { 226 $this->options['group'] = $groupingDisabled; 227 } 228 229 return $result; 230 } 231 232 /** 233 * This function filters data from the specified array that should 234 * not be shown. Filter criteria are specified at object creation. 235 * 236 * Furthermore, entries whose entrytype is not translated in the specified 237 * language file are put into a distinct group. 238 * 239 * @access private 240 * @param array data Unfiltered data, that is array of entries 241 * @param replacementKeys An array with entries of the form $entrykey => $newKey. 242 * If an entrykey occurrs here, it will be replaced by 243 * its correspoding newKey in the output. 244 * @return array Filtered data as array of entries 245 */ 246 private function filter(&$data, &$replacementKeys=array()) { 247 $result = array(); 248 249 $id = 0; 250 foreach ( $data as $entry ) { 251 // Some additions/corrections 252 if ( empty($this->options['lang']['entrytypes'][$entry['entrytype']]) ) { 253 $entry['entrytype'] = $this->options['lang']['entrytypes']['unknown']; 254 } 255 256 // Check wether this entry should be included 257 $keep = true; 258 foreach ( $this->options['only'] as $field => $regexp ) { 259 if ( !empty($entry[$field]) ) { 260 $val = $field === 'author' 261 ? $entry['niceauthor'] 262 : $entry[$field]; 263 264 $keep = $keep && preg_match('/'.$regexp.'/i', $val); 265 } 266 else { 267 /* If the considered field does not even exist, consider this a fail. 268 * That enables to use $field => '.*' as existence check. */ 269 $keep = false; 270 } 271 } 272 273 if ( $keep === true ) { 274 if ( !empty($replacementKeys[$entry['entrykey']]) ) { 275 $entry['entrykey'] = $replacementKeys[$entry['entrykey']]; 276 } 277 278 $result[] = $entry; 279 } 280 } 281 282 return $result; 283 } 284 285 /** 286 * This function groups the passed entries according to the criteria 287 * passed at object creation. 288 * 289 * @access private 290 * @param array data An array of entries 291 * @return array An array of arrays of entries 292 */ 293 private function group(&$data) { 294 $result = array(); 295 296 if ( $this->options['group'] !== 'none' ) { 297 foreach ( $data as $entry ) { 298 if ( !empty($entry[$this->options['group']]) || $this->options['group'] === 'firstauthor' ) { 299 if ( $this->options['group'] === 'firstauthor' ) { 300 $target = $entry['author'][0]['nice']; 301 } 302 elseif ( $this->options['group'] === 'author' ) { 303 $target = $entry['niceauthor']; 304 } 305 else { 306 $target = $entry[$this->options['group']]; 307 } 308 } 309 else { 310 $target = $this->options['lang']['rest']; 311 } 312 313 if ( empty($result[$target]) ) { 314 $result[$target] = array(); 315 } 316 317 $result[$target][] = $entry; 318 } 319 } 320 else { 321 $result[$this->options['lang']['all']] = $data; 322 } 323 324 return $result; 325 } 326 327 /** 328 * This function sorts the passed group of entries and the individual 329 * groups if there are any. 330 * 331 * @access private 332 * @param array data An array of arrays of entries 333 * @return array A sorted array of sorted arrays of entries 334 */ 335 private function sort($data) { 336 // Sort groups if there are any 337 if ( $this->options['group'] !== 'none' ) { 338 uksort($data, array($this->helper, 'group_cmp')); 339 } 340 341 // Sort individual groups 342 foreach ( $data as &$group ) { 343 uasort($group, array($this->helper, 'entry_cmp')); 344 } 345 346 return $data; 347 } 348 349 /** 350 * This function inserts the specified data into the specified template. 351 * For template syntax see class documentation or examples. 352 * 353 * @access private 354 * @param array data An array of arrays of entries 355 * @param string template The used template 356 * @return string The data represented in terms of the template 357 */ 358 private function translate(&$data, &$template) { 359 $result = $template; 360 361 // Replace global values 362 $result = preg_replace(array('/@globalcount@/', '/@globalgroupcount@/', '/@globalkey@/'), 363 array(Helper::lcount($data, 2), count($data), $this->options['key']), 364 $result); 365 366 if ( $this->options['group'] !== 'none' ) { 367 $pattern = '/@\{group@(.*?)@\}group@/s'; 368 369 // Extract group templates 370 $group_tpl = array(); 371 preg_match($pattern, $result, $group_tpl); 372 373 // For all occurrences of an group template 374 while ( !empty($group_tpl) ) { 375 // Translate all groups 376 $groups = ''; 377 $id = 0; 378 foreach ( $data as $groupkey => $group ) { 379 $groups .= $this->translate_group($groupkey, $id++, $group, $group_tpl[1]); 380 } 381 382 $result = preg_replace($pattern, $groups, $result, 1); 383 preg_match($pattern, $result, $group_tpl); 384 } 385 386 return $result; 387 } 388 else { 389 $groups = ''; 390 foreach ( $data as $groupkey => $group ) { // loop will only be run once 391 $groups .= $this->translate_group($groupkey, 0, $group, $template); 392 } 393 return $groups; 394 } 395 } 396 397 /** 398 * This function translates one entry group 399 * 400 * @access private 401 * @param string key The rendered group's key 402 * @param int id A unique ID for this group 403 * @param array data Array of entries in this group 404 * @param string template The group part of the template 405 * @return string String representing the passed group wrt template 406 */ 407 private function translate_group($key, $id, &$data, $template) { 408 $result = $template; 409 410 // Replace group values 411 if ( $this->options['group'] === 'entrytype' ) { 412 $key = $this->options['lang']['entrytypes'][$key]; 413 } 414 $result = preg_replace(array('/@groupkey@/', '/@groupid@/', '/@groupcount@/'), 415 array($key, $id, count($data)), 416 $result); 417 418 $pattern = '/@\{entry@(.*?)@\}entry@/s'; 419 420 // Extract entry templates 421 $entry_tpl = array(); 422 preg_match($pattern, $result, $entry_tpl); 423 424 // For all occurrences of an entry template 425 while ( !empty($entry_tpl) ) { 426 // Translate all entries 427 $entries = ''; 428 foreach ( $data as $entry ) { 429 $entries .= $this->translate_entry($entry, $entry_tpl[1]); 430 } 431 432 $result = preg_replace($pattern, $entries, $result, 1); 433 preg_match($pattern, $result, $entry_tpl); 434 } 435 436 return $result; 437 } 438 439 /** 440 * This function translates one entry 441 * 442 * @access private 443 * @param array entry Array of fields 444 * @param string template The entry part of the template 445 * @return string String representing the passed entry wrt template 446 */ 447 private function translate_entry(&$entry, $template) { 448 $result = $template; 449 450 // Resolve all conditions 451 $result = $this->resolve_conditions($entry, $result); 452 453 // Replace all possible unconditional fields 454 $patterns = array(); 455 $replacements = array(); 456 457 foreach ( $entry as $key => $value ) { 458 if ( $key === 'author' ) { 459 $value = $entry['niceauthor']; 460 $value = $this->authorlink($value); 461 } 462 // Don't sanitize values associated with bibtex or url keys 463 if ( $key == 'bibtex' || $key == 'url') { 464 $patterns []= '/@'.$key.'@/'; 465 $replacements []= $value; 466 } 467 else { 468 $patterns []= '/@'.$key.'@/'; 469 $replacements []= call_user_func($this->sanitise, $value); 470 } 471 } 472 473 return preg_replace($patterns, $replacements, $result); 474 } 475 476 /** 477 * This function eliminates conditions in template parts. 478 * 479 * @access private 480 * @param array entry Entry with respect to which conditions are to be 481 * solved. 482 * @param string template The entry part of the template. 483 * @return string Template string without conditions. 484 */ 485 private function resolve_conditions(&$entry, &$string) { 486 $pattern = '/@\?(\w+)(?:(<=|>=|==|!=|~)(.*?))?@(.*?)(?:@:\1@(.*?))?@;\1@/s'; 487 /* There are two possibilities for mode: existential or value check 488 * Then, there can be an else part or not. 489 * Existential Value Check RegExp 490 * Group 1 field field \w+ 491 * Group 2 then operator .*? / <=|>=|==|!=|~ 492 * Group 3 [else] value .*? 493 * Group 4 --- then .*? 494 * Group 5 --- [else] .*? 495 */ 496 497 $match = array(); 498 499 /* Would like to do 500 * preg_match_all($pattern, $string, $matches); 501 * to get all matches at once but that results in Segmentation 502 * fault. Therefore iteratively: 503 */ 504 while ( preg_match($pattern, $string, $match) ) 505 { 506 $resolved = ''; 507 508 $evalcond = !empty($entry[$match[1]]); 509 $then = count($match) > 3 ? 4 : 2; 510 $else = count($match) > 3 ? 5 : 3; 511 512 if ( $evalcond && count($match) > 3 ) { 513 if ( $match[2] === '==' ) { 514 $evalcond = $entry[$match[1]] === $match[3]; 515 } 516 elseif ( $match[2] === '!=' ) { 517 $evalcond = $entry[$match[1]] !== $match[3]; 518 } 519 elseif ( $match[2] === '<=' ) { 520 $evalcond = is_numeric($entry[$match[1]]) 521 && is_numeric($match[3]) 522 && (int)$entry[$match[1]] <= (int)$match[3]; 523 } 524 elseif ( $match[2] === '>=' ) { 525 $evalcond = is_numeric($entry[$match[1]]) 526 && is_numeric($match[3]) 527 && (int)$entry[$match[1]] >= (int)$match[3]; 528 } 529 elseif ( $match[2] === '~' ) { 530 $evalcond = preg_match('/'.$match[3].'/', $entry[$match[1]]) > 0; 531 } 532 } 533 534 if ( $evalcond ) 535 { 536 $resolved = $match[$then]; 537 } 538 elseif ( !empty($match[$else]) ) 539 { 540 $resolved = $match[$else]; 541 } 542 543 // Recurse to cope with nested conditions 544 $resolved = $this->resolve_conditions($entry, $resolved); 545 546 $string = str_replace($match[0], $resolved, $string); 547 } 548 549 return $string; 550 } 551 552 /** 553 * This function adds links to co-author websites where available. 554 * 555 * @access private 556 * @param string data Formatted author line without links. 557 * @return string data Formatted author line with links. 558 */ 559 private function authorlink($data) { 560 $data = str_replace(array_keys($this->authorlist),$this->authorlist,$data); 561 return $data; 562 } 563} 564 565?> 566