1<?php 2/** 3 * texit multifunction Class 4 * Copyright (C) 2013 Elie Roux <elie.roux@telecom-bretagne.eu> 5 * 6 * This library is free software; you can redistribute it and/or 7 * modify it under the terms of the GNU Lesser General Public 8 * License as published by the Free Software Foundation; either 9 * version 2.1 of the License, or (at your option) any later version. 10 * 11 * This library is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 * Lesser General Public License for more details. 15 * 16 * You should have received a copy of the GNU Lesser General Public 17 * License along with this library; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 * -------------------------------------------------------------------- 20 * 21 */ 22if(!defined('DOKU_INC')) die(); 23if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 24if(!defined('PLUGIN_TEXIT')) define('PLUGIN_TEXIT',DOKU_PLUGIN.'texit/'); 25if(!defined('PLUGIN_TEXIT_CONF')) define('PLUGIN_TEXIT_CONF',PLUGIN_TEXIT.'conf/'); 26require_once(PLUGIN_TEXIT.'texitrender.php'); 27 28class config_plugin_texit { 29 var $id; 30 var $ns; 31 var $namespace_mode; 32 var $nsbpc; 33 var $conf; 34 var $mediadir; 35 var $texitdir; 36 var $prfix; 37 var $all_files; 38 var $texit_render_obj; // not initialized by constructor, done only if needed 39 var $bibfn; 40 /* 41 * I didn't use a helper plugin because I needed a constructor. 42 * This basically sets up the environment by computing base the filenames, etc. 43 * 44 */ 45 function __construct($id, $namespace_mode, $conf, $nsbpc_obj) { 46 $this->id = cleanID($id); 47 $this->ns = getNS(cleanID($id)); 48 $this->namespace_mode = $namespace_mode; 49 $this->nsbpc = $nsbpc_obj; 50 $this->conf = $conf; 51 $this->set_prefix(); 52 $this->_set_texit_dir(); 53 $this->_set_media_dir(); 54 $this->bibfn = $this->generate_bib(); 55 $this->get_all_files(); 56 $this->conf['latexentities'] = false; // we generate it at compile time 57 $this->texit_render_obj = false; 58 } 59 /* 60 * This function sets $this->latexentities to an array where keys are 61 * the initial characters and values are the characters escaped in 62 * LaTeX (ex: _ => \_). It gets in by calling conftohash on conf/entities.cfg 63 * in the plugin's directory. 64 */ 65 function get_entities() { 66 $basefn = PLUGIN_TEXIT_CONF.'entities.cfg'; 67 return $this->confToHash($basefn); 68 } 69 70 /** 71 * Builds a hash from a configfile 72 * 73 * If $lower is set to true all hash keys are converted to 74 * lower case. 75 * 76 * This is a modified version of Dokuwiki's function 77 * that doesn't consider # as a comment character (we 78 * need it for LaTeX entities). 79 */ 80 function confToHash($file) { 81 $conf = array(); 82 $lines = @file( $file ); 83 if ( !$lines ) return false; 84 foreach ( $lines as $line ) { 85 $line = trim($line); 86 if(empty($line)) continue; 87 $line = preg_split('/[\s\t]+/',$line,2); 88 // Build the associative array 89 $conf[$line[0]] = $line[1]; 90 } 91 return $conf; 92 } 93 94 /* 95 * This function (eventually) generates the file texit.bib, in the directory 96 * of the namespace pointed by the "reference-db-enable" configuration option 97 * of the refnotes plugin. 98 * 99 * To do so, it merges: 100 * - the BibTeX parts of all pages in the refnotes's database namespace 101 * ("refnotes" by default) 102 * - the conf/bibliography.bib in the texit plugin directory 103 */ 104 function generate_bib() { 105 global $conf; 106 $bibtext = ''; // we merge all the files in this string 107 $basefn = PLUGIN_TEXIT_CONF.'bibliography.bib'; 108 if (!is_callable("refnotes_configuration::getSetting")) { 109 // case where refnotes isn't available. In this case the 110 // file to include is just $basefn. 111 return $basefn; 112 } 113 // code coming from refnotes' syntax.php 114 $refnotes_nsdir = refnotes_configuration::getSetting('reference-db-namespace'); 115 $refnotes_nsdir = str_replace(':', '/', $refnotes_nsdir); 116 $refnotes_nsdir = trim($refnotes_nsdir, '/ '); 117 $destfn = $conf['datadir'].'/'.$refnotes_nsdir.'/texit.bib'; 118 $all_refnotes_pages = Array(); 119 $opts = array('listdirs' => false, 120 'listfiles' => true, 121 'pagesonly' => true, 122 'skipacl' => false, // to check for read right 123 'sneakyacl' => true, 124 'showhidden'=> false, 125 ); 126 // we cannot use $opts in search_list or in search_namespaces, see 127 // https://bugs.dokuwiki.org/index.php?do=details&task_id=2858 128 search($all_refnotes_pages,$conf['datadir'],'search_universal',$opts,$refnotes_nsdir); 129 // now all_refnotes_pages contains all the configuration pages of refnotes, that 130 // we'll have to merge... 131 // First step here is to see if we need to recompile anything: 132 if (is_readable($destfn)) { 133 // if the file is readable, then it might be up-to-date? 134 $needsupdate = false; 135 if (is_readable($basefn)) { 136 $needsupdate = $this->_needs_update($basefn, $destfn); 137 } 138 foreach ($all_refnotes_pages as $page) { 139 // A problem here: if the refnote page doesn't contain 140 // any bibtex code, the update will take place anyway, 141 // but it doesn't sound critical. 142 if ($this->_needs_update(wikiFN($page['id']), $destfn)) { 143 $needsupdate = true; 144 } 145 } 146 // if the file doesn't need update, we just return. 147 if (!$needsupdate) { 148 return $destfn; 149 } 150 } 151 if (is_readable($basefn)) { 152 $bibtext = file_get_contents($basefn); 153 } 154 foreach($all_refnotes_pages as $page) { 155 $fn = wikiFN($page['id']); 156 $bibtext .= $this->parse_refnotes_page($fn); 157 } 158 if (empty($bibtext)) { 159 return false; 160 } 161 file_put_contents($destfn, $bibtext); 162 // we return the filename where the bibliography is saved 163 return $destfn; 164 } 165 166 function parse_refnotes_page ($fn) { 167 $filestr = file_get_contents($fn); 168 preg_match_all('#(?<=<code bibtex>)(((?!</code>).)*)(?=</code>)#ms', $filestr, $matches); 169 $return = ''; 170 foreach($matches[0] as $match) { 171 $return .= $match."\n"; 172 } 173 return $return; 174 } 175 176 function set_prefix() { 177 if (!$this->conf['use_prefix']) { 178 $this->prefix = ''; 179 return; 180 } else { 181 if (!empty($this->conf['pre_prefix'])) { 182 $this->prefix = $this->conf['pre_prefix'].":"; 183 } 184 $this->prefix .= $this->ns; 185 if ($this->conf['prefix_separator']) { 186 $this->prefix = str_replace(':', $this->conf['prefix_separator'], $this->prefix); 187 $this->prefix .= $this->conf['prefix_separator']; 188 } // else we keep it this way 189 } 190 } 191 192 function _create_dir($path) { 193 global $conf; 194 $res = init_path($path); 195 if(empty($res)) { 196 // let's create it, recursively 197 $res = io_mkdir_p($path); 198 //$res = mkdir($path, $conf['dmode'], true); 199 if(!$res){ 200 die("Unable to create directory $path, please create it."); 201 } 202 } 203 } 204 205// This function escapes a filename so that it doesn't contain _ character: 206 function _escape_fn($fn) { 207 $bn = basename($fn); 208 $bn = str_replace('_', '-', $bn); 209 $dn = dirname($fn); 210 if ($dn == ".") { 211 return $bn; 212 } 213 return dirname($fn).'/'.$bn; 214 } 215 216 function _set_media_dir() { 217 global $conf; 218 $path = $conf['mediadir']; 219 $path .= '/'.str_replace(':','/',$this->ns); 220 // taken from init_paths in inc/init.php 221 $this->_create_dir($path); 222 $this->mediadir = $path; 223 } 224 225 function _set_texit_dir() { 226 global $conf; 227 $path = $this->conf['texitdir']; 228 // taken from init_paths in inc/init.php 229 $path = empty($path) ? $conf['datadir'].'/../texit' : $path; 230 $path .= '/'.str_replace(':','/',$this->ns); 231 $this->_create_dir($path); 232 $path = realpath($path); 233 $this->texitdir = $path; 234 } 235 236 function get_zip_fn() { 237 return $this->mediadir.'/'.$this->get_common_basename().".zip"; 238 } 239 240 function get_base_bib_fn() { 241 return $this->bibfn; 242 } 243 244 function get_dest_bib_fn() { 245 // we always call it texit.bib for practical reasons, this may 246 // change in the future 247 return $this->texitdir.'/'.'texit.bib'; 248 } 249 250 function get_pdf_media_fn() { 251 return $this->mediadir.'/'.$this->prefix.$this->get_common_basename().".pdf"; 252 } 253 254 function get_pdf_media_id() { 255 return $this->ns.':'.$this->prefix.$this->get_common_basename().".pdf"; 256 } 257 258 function get_pdf_texit_fn() { 259 return $this->texitdir.'/'.$this->get_common_basename().".pdf"; 260 } 261 262 263 264 /* This returns 'all' if in namespace-mode, or the escaped ID, without extension. 265 * 266 */ 267 function get_common_basename() { 268 if ($this->namespace_mode) { 269 return "all"; 270 } else { 271 return $this->_escape_fn(noNS($this->id)); 272 } 273 } 274 275 /* This returns the full path of the base header file we take as reference 276 * for this compilation. In case nothing is found, false is returned. 277 */ 278 function get_base_header_fn() { 279 // first we look for nsbpc headers 280 // the names are 'texit-namespace' or 'texit-page' 281 $header_name = "texit-page"; 282 if ($this->namespace_mode) { 283 $header_name = "texit-namespace"; 284 } 285 $found = $this->nsbpc->getConfFN($header_name, $this->ns); 286 if ($found) { 287 return $found; 288 } 289 // No nsbpc configuration was found, now looking in the conf/ directory of 290 // the plugin. Names are different here... 291 $header_name = "header-page.tex"; 292 if ($this->namespace_mode) { 293 $header_name = "header-namespace.tex"; 294 } 295 if (is_readable(PLUGIN_TEXIT_CONF.$header_name)) { 296 return PLUGIN_TEXIT_CONF.$header_name; 297 } 298 return false; 299 } 300 301 /* This returns the full path of the header file we want in the destination 302 * texit namespace. 303 */ 304 function get_dest_header_fn() { 305 if ($this->namespace_mode) { 306 return $this->texitdir."/all.tex"; 307 } else { 308 return $this->texitdir.'/'.$this->get_common_basename().".tex"; 309 } 310 } 311 /* This returns the full path of the base footer file we take as reference 312 * for this compilation, or false if there is no such file. 313 */ 314 function get_base_footer_fn() { 315 // first we look through nsbpc 316 $found = $this->nsbpc->getConfFN("texit-footer", $this->ns); 317 if ($found) { 318 return $found; 319 } 320 // No nsbpc configuration was found, now looking in the conf/ directory of 321 // the plugin. 322 if (is_readable(PLUGIN_TEXIT_CONF."footer.tex")) { 323 return PLUGIN_TEXIT_CONF."footer.tex"; 324 } 325 return false; 326 } 327 /* This returns the full path of the commands file we want in the destination 328 * texit namespace. 329 */ 330 function get_dest_footer_fn() { 331 return $this->texitdir."/footer.tex"; 332 } 333 /* This returns the full path of the base coommands file we take as reference 334 * for this compilation. 335 */ 336 function get_base_commands_fn() { 337 // first we look through nsbpc 338 $found = $this->nsbpc->getConfFN("texit-commands", $this->ns); 339 if ($found) { 340 return $found; 341 } 342 // No nsbpc configuration was found, now looking in the conf/ directory of 343 // the plugin. 344 if (is_readable(PLUGIN_TEXIT_CONF."commands.tex")) { 345 return PLUGIN_TEXIT_CONF."commands.tex"; 346 } 347 return false; 348 } 349 /* This returns the full path of the commands file we want in the destination 350 * texit namespace. 351 */ 352 function get_dest_commands_fn() { 353 return $this->texitdir."/commands.tex"; 354 } 355 356 /* This function returns an array of all IDs of pages to be rendered by TeXit. 357 * 358 */ 359 function get_all_IDs() { 360 global $conf; 361 if ($this->namespace_mode) { 362 $list = array(); 363 $nsdir = str_replace(':', '/', $this->ns); 364 $opts = array('listdirs' => false, 365 'listfiles' => true, 366 'pagesonly' => true, 367 'depth' => 1, 368 'skipacl' => false, // to check for read right 369 'sneakyacl' => true, 370 'showhidden'=> false, 371 ); 372 if ($this->conf['includestart'] == false) { 373 $opts['idmatch'] = "^((?!start$).)+$"; 374 } 375 search($list,$conf['datadir'],'search_universal',$opts,$nsdir); 376 return $list; 377 } else { 378 return array(array('id' => $this->id)); 379 } 380 } 381 382 /* Returns an array with base and destination filenames. Works with full paths. 383 * 384 * The returned array has the following structure: 385 * [base] => (type, fn) 386 * where: 387 * * base is the base filename (like /path/to/dkwiki/pages/ns/id.txt) 388 * * type is either "header", "commands", "tex" or "bib". 389 * * fn is the absolute destination filename (prefix included) 390 */ 391 function get_all_files() { 392 // this gives us all the page ids that need txt->tex conversion: 393 $id_array = $this->get_all_IDs(); 394 $result = array(); 395 // now we put them all in the $result array 396 foreach($id_array as $value) { 397 if (!is_array($value) || !$value['id']) { // I did'nt find any more elegant way to do so 398 continue; 399 } 400 $fn = wikiFN($value['id']); 401 $dest = $this->texitdir.'/'.noNS($value['id'])."-content.tex"; 402 $dest = $this->_escape_fn($dest); 403 $result[$fn] = array('type' => 'tex', 'fn' => $dest); 404 } 405 // and we add the header and command 406 $base = $this->get_base_header_fn(); 407 if (!$base) { 408 nice_die("TeXit: Unable to find a header file!"); 409 } 410 $result[$base] = array('type' => 'header', 'fn' => $this->get_dest_header_fn()); 411 $base = $this->get_base_commands_fn(); 412 if (!$base) { 413 nice_die("TeXit: Unable to find a commands file!"); 414 } 415 $result[$base] = array('type' => 'commands', 'fn' => $this->get_dest_commands_fn()); 416 $bib = $this->get_base_bib_fn(); 417 if ($bib) { // not mandatory 418 $result[$bib] = array('type' => 'bib', 'fn' => $this->get_dest_bib_fn()); 419 } 420 $footer = $this->get_base_footer_fn(); 421 if ($footer) { // not mandatory 422 $result[$footer] = array('type' => 'footer', 'fn' => $this->get_dest_footer_fn()); 423 } 424 $this->all_files = $result; 425 } 426 427 /* This function takes three arguments: 428 * * base is the full path of the base header file 429 * (for instance /path/to/dkwiki/lib/plugin/texit/conf/header-page.tex) 430 * * dest is the full path of the destination header file 431 * * all_files is the table returned by get_all_files() 432 * 433 * It reads $base, adds \input lines for $all_files and writes the result in 434 * $dest. 435 */ 436 function compile_header($base, $dest, $all_files) { 437 // first we simply copy the file 438 $this->simple_copy($base, $dest); 439 // we prepare a string to append at the end: 440 $toappend = "\n"; 441 // we spot the last value: 442 $beginning = 1; 443 $footer = false; 444 foreach($this->all_files as $value) { 445 switch($value['type']) { 446 case 'tex': 447 // between two different files, we call the \dokuinternspagedo 448 // macro, doing nothing by default. 449 if (!$beginning) { 450 $toappend .= "\\dokuinternspagedo\n\n"; 451 } 452 $toappend .= '\dokuinclude{'.basename($value['fn'], '.tex')."}\n\n"; 453 break; 454 case 'footer': 455 $footer = basename($value['fn'], '.tex'); 456 default: 457 break; 458 } 459 $beginning = 0; 460 } 461 if ($footer) { 462 $toappend .= "\dokuinclude{".$footer."}\n"; 463 } 464 $toappend .= "\n\\end{document}"; 465 // the we open it in append mode to write things at the end: 466 file_put_contents($dest, $toappend, FILE_APPEND); 467 } 468 469 /* This function takes two arguments: 470 * * base is the full path of the base page file 471 * (for instance /path/to/dkwiki/data/pages/ns/id.txt) 472 * * dest is the full path of the destination tex file 473 * 474 * It reads $base, renders it into TeX and writes $dest. 475 */ 476 function compile_tex($base, $dest) { 477 if (!$this->conf['latexentities']) 478 { 479 $this->conf['latexentities'] = $this->get_entities(); 480 } 481 if (!$this->texit_render_obj) 482 { 483 $this->texit_render_obj = new texitrender_plugin_texit($this); 484 } 485 $this->texit_render_obj->process($base, $dest); 486 } 487 488 /* This function takes two arguments: 489 * * base is the full path of the base file 490 * * dest is the full path of the destination tex file 491 * 492 * It copies $base into $dest. 493 */ 494 function simple_copy($base, $dest) { 495 if (!copy($base, $dest)) { 496 nice_die("TeXit: unable to copy $base into $dest."); 497 } 498 } 499 500 /* 501 * This functions returns true if $base is more recent that $dest, and 502 * false otherwise. If $dest doesn't exist, then we consider it needs 503 * update and thus return true. 504 */ 505 function _needs_update($base, $dest) { 506 if (!file_exists($dest) || !file_exists($dest)) { 507 return true; 508 } 509 return filemtime($base) > filemtime($dest); 510 } 511 512 /* This function sets the TeX compilation environment up by copying the files 513 * in the good folders and renames them. It uses file modification timestamps 514 * to evaluate if files need to be recompiled or recopied. 515 * 516 * The returned value is a boolean: true if something has been updated, and 517 * false otherwise. 518 */ 519 function setup_files() { 520 if (!is_array($this->all_files)) { 521 $this->get_all_files(); 522 } 523 if (!is_array($this->all_files)) { 524 die("TeXit: cannot analyze files"); 525 } 526 $needsupdate = false; 527 foreach($this->all_files as $base => $dest) { 528 $destfn = $dest['fn']; 529 if ($this->_needs_update($base, $destfn)) { 530 $needsupdate = true; 531 switch($dest['type']) { 532 case "header": 533 $this->compile_header($base, $destfn, $this->all_files); 534 break; 535 case "commands": 536 $this->simple_copy($base, $destfn); 537 break; 538 case "bib": 539 $this->simple_copy($base, $destfn); 540 break; 541 case "footer": 542 $this->simple_copy($base, $destfn); 543 break; 544 case "tex": 545 $this->compile_tex($base, $destfn); 546 break; 547 default: 548 break; 549 } 550 } 551 } 552 return $needsupdate; 553 } 554 555 /* This function calls latexmk with the good options on the good files. 556 */ 557 function _do_latexmk() { 558 if (!is_dir($this->texitdir)) { 559 die("TeXit: directory $this->texitdir doesn't exit"); 560 } 561 chdir($this->texitdir); 562 $basecmdline = ''; 563 if (isset($this->conf['latexmk_path']) 564 && trim($this->conf['latexmk_path']) != "") { 565 $basecmdline = $this->conf['latexmk_path'] . DIRECTORY_SEPARATOR; 566 } else { 567 $basecmdline = ''; 568 } 569 $cmdline = $basecmdline."latexmk -f "; 570 if ($this->bibfn) { 571 $cmdline .= "-bibtex "; 572 } 573 switch ($this->conf['latex_mode']) 574 { 575 case "latex": 576 // TODO: test, comes from http://users.phys.psu.edu/~collins/software/latexmk-jcc/ 577 $cmdline .= "-e '\$dvipdf = \"dvipdfm %O -o %D %S\";' -pdfdvi "; 578 break; 579 case "pdflatex": 580 $cmdline .= "-pdf "; 581 break; 582 case "lualatex": 583 $cmdline .= "-pdf -pdflatex=lualatex "; 584 break; 585 case "xelatex": 586 $cmdline .= "-latex=xelatex -e '\$dvipdf = \"dvipdfmx %O -o %D %S\";' -pdfdvi "; 587 break; 588 default: 589 // error 590 break; 591 } 592 $file = basename($this->get_dest_header_fn()); 593 $cmdline .= $file . ' 2>&1 '; 594 $ret = 0; 595 @exec($cmdline, $output, $ret); 596 if ($ret) { 597 print("<br/>TeXit error: latexmk returned error code ".$ret."<br/>\n<br/>Log:<br/>\n"); 598 print_r(implode("<br/>\n", $output)); 599 } 600 // at the end, we clean temporary files. There is currently no way to tell 601 // latexmk to clean at the end of the compilation... quite a shame... 602 // An email has been written to the author in this sense. 603 $cmdline = $basecmdline."latexmk -c 2>&1"; 604 $log = @exec($cmdline, $output, $ret); 605 if ($ret) { 606 print("<br/>TeXit error: latexmk -c returned error code ".$ret."<br/>\n<br/>Log:<br/>\n"); 607 print_r(implode("<br/>\n", $output)); 608 } 609 } 610 611 /* This function zips the good files in the texit namespace in a .zip archive 612 * in the media namespace. 613 */ 614 function compile_zip() { 615 $zipfn = $this->get_zip_fn(); 616 // if the file already exists and needs update, remove it. 617 if (@file_exists($zipfn)) { 618 unlink($zipfn); 619 } 620 $zip = new ZipArchive(); 621 if ($zip->open($zipfn, ZipArchive::CREATE) !== true) { 622 exit("Unable to create $zipfn\n"); 623 } 624 // First argument of addFile is the absolute, second is the name we want 625 // in the archive (in our case, the basename). 626 $zip->addFile($this->get_pdf_texit_fn(), basename($this->get_pdf_texit_fn())); 627 foreach($this->all_files as $base => $dest) { 628 $zip->addFile($dest['fn'], basename($dest['fn'])); 629 } 630 $zip->close(); 631 } 632 633 /* My mind is too used to C programming and thus this is a bit too 634 * iterative and not object-oriented enough... 635 * 636 * This function processes everything when the user asks for a PDF. 637 */ 638 function process() { 639 $needsupdate = $this->setup_files(); 640 $pdftexitfn = $this->get_pdf_texit_fn(); 641 $pdfmediafn = $this->get_pdf_media_fn(); 642 $pdfmediaid = $this->get_pdf_media_id(); 643 $zipfn = $this->get_zip_fn(); 644 if ($needsupdate || !@file_exists($pdftexitfn)) { 645 $this->_do_latexmk(); 646 } 647 // then copy the pdf to media 648 if ($needsupdate || !@file_exists($pdfmediafn)) { 649 $this->simple_copy($pdftexitfn, $pdfmediafn); 650 } 651 if ($this->conf['use_zip'] && ($needsupdate || !@file_exists($zipfn))) { 652 $this->compile_zip(); 653 } 654 return $this->id_to_url($pdfmediaid); 655 } 656 657 /* This returns an absolute URL from a media ID. 658 */ 659 function id_to_url($pdfmediaid) { 660 // internal dokuwiki function, defined in inc/common.php 661 return ml($pdfmediaid, '', true, '&', true); 662 } 663} 664 665?> 666