1<?php 2 3/** 4 * reStructuredText rendering task for Phing, the PHP build tool. 5 * 6 * PHP version 5 7 * 8 * @category Tasks 9 * @package phing.tasks.ext 10 * @author Christian Weiske <cweiske@cweiske.de> 11 * @license LGPL v3 or later http://www.gnu.org/licenses/lgpl.html 12 * @link http://www.phing.info/ 13 * @version SVN: $Id: bc420f25ab51443575d2064ebc8b2d633a4b2f65 $ 14 */ 15 16require_once 'phing/Task.php'; 17require_once 'phing/util/FileUtils.php'; 18 19/** 20 * reStructuredText rendering task for Phing, the PHP build tool. 21 * 22 * PHP version 5 23 * 24 * @category Tasks 25 * @package phing.tasks.ext 26 * @author Christian Weiske <cweiske@cweiske.de> 27 * @license LGPL v3 or later http://www.gnu.org/licenses/lgpl.html 28 * @link http://www.phing.info/ 29 */ 30class rSTTask extends Task 31{ 32 /** 33 * @var string Taskname for logger 34 */ 35 protected $taskName = 'rST'; 36 37 /** 38 * Result format, defaults to "html". 39 * @see $supportedFormats for all possible options 40 * 41 * @var string 42 */ 43 protected $format = 'html'; 44 45 /** 46 * Array of supported output formats 47 * 48 * @var array 49 * @see $format 50 * @see $targetExt 51 */ 52 protected static $supportedFormats = array( 53 'html', 'latex', 'man', 'odt', 's5', 'xml' 54 ); 55 56 /** 57 * Maps formats to file extensions 58 * 59 * @var array 60 */ 61 protected static $targetExt = array( 62 'html' => 'html', 63 'latex' => 'tex', 64 'man' => '3', 65 'odt' => 'odt', 66 's5' => 'html', 67 'xml' => 'xml', 68 ); 69 70 /** 71 * Input file in rST format. 72 * Required 73 * 74 * @var string 75 */ 76 protected $file = null; 77 78 /** 79 * Additional rst2* tool parameters. 80 * 81 * @var string 82 */ 83 protected $toolParam = null; 84 85 /** 86 * Full path to the tool, i.e. /usr/local/bin/rst2html 87 * 88 * @var string 89 */ 90 protected $toolPath = null; 91 92 /** 93 * Output file or directory. May be omitted. 94 * When it ends with a slash, it is considered to be a directory 95 * 96 * @var string 97 */ 98 protected $destination = null; 99 100 protected $filesets = array(); // all fileset objects assigned to this task 101 protected $mapperElement = null; 102 103 /** 104 * all filterchains objects assigned to this task 105 * 106 * @var array 107 */ 108 protected $filterChains = array(); 109 110 /** 111 * mode to create directories with 112 * 113 * @var integer 114 */ 115 protected $mode = 0; 116 117 /** 118 * Only render files whole source files are newer than the 119 * target files 120 * 121 * @var boolean 122 */ 123 protected $uptodate = false; 124 125 /** 126 * Sets up this object internal stuff. i.e. the default mode 127 * 128 * @return object The rSTTask instance 129 * @access public 130 */ 131 function __construct() { 132 $this->mode = 0777 - umask(); 133 } 134 135 /** 136 * Init method: requires the PEAR System class 137 */ 138 public function init() 139 { 140 require_once 'System.php'; 141 } 142 143 /** 144 * The main entry point method. 145 * 146 * @return void 147 */ 148 public function main() 149 { 150 $tool = $this->getToolPath($this->format); 151 if (count($this->filterChains)) { 152 $this->fileUtils = new FileUtils(); 153 } 154 155 if ($this->file != '') { 156 $file = $this->file; 157 $targetFile = $this->getTargetFile($file, $this->destination); 158 $this->render($tool, $file, $targetFile); 159 return; 160 } 161 162 if (!count($this->filesets)) { 163 throw new BuildException( 164 '"file" attribute or "fileset" subtag required' 165 ); 166 } 167 168 // process filesets 169 $mapper = null; 170 if ($this->mapperElement !== null) { 171 $mapper = $this->mapperElement->getImplementation(); 172 } 173 174 $project = $this->getProject(); 175 foreach ($this->filesets as $fs) { 176 $ds = $fs->getDirectoryScanner($project); 177 $fromDir = $fs->getDir($project); 178 $srcFiles = $ds->getIncludedFiles(); 179 180 foreach ($srcFiles as $src) { 181 $file = new PhingFile($fromDir, $src); 182 if ($mapper !== null) { 183 $results = $mapper->main($file); 184 if ($results === null) { 185 throw new BuildException( 186 sprintf( 187 'No filename mapper found for "%s"', 188 $file 189 ) 190 ); 191 } 192 $targetFile = reset($results); 193 } else { 194 $targetFile = $this->getTargetFile($file, $this->destination); 195 } 196 $this->render($tool, $file, $targetFile); 197 } 198 } 199 } 200 201 202 203 /** 204 * Renders a single file and applies filters on it 205 * 206 * @param string $tool conversion tool to use 207 * @param string $source rST source file 208 * @param string $targetFile target file name 209 * 210 * @return void 211 */ 212 protected function render($tool, $source, $targetFile) 213 { 214 if (count($this->filterChains) == 0) { 215 return $this->renderFile($tool, $source, $targetFile); 216 } 217 218 $tmpTarget = tempnam(sys_get_temp_dir(), 'rST-'); 219 $this->renderFile($tool, $source, $tmpTarget); 220 221 $this->fileUtils->copyFile( 222 new PhingFile($tmpTarget), 223 new PhingFile($targetFile), 224 true, false, $this->filterChains, 225 $this->getProject(), $this->mode 226 ); 227 unlink($tmpTarget); 228 } 229 230 231 232 /** 233 * Renders a single file with the rST tool. 234 * 235 * @param string $tool conversion tool to use 236 * @param string $source rST source file 237 * @param string $targetFile target file name 238 * 239 * @return void 240 * 241 * @throws BuildException When the conversion fails 242 */ 243 protected function renderFile($tool, $source, $targetFile) 244 { 245 if ($this->uptodate && file_exists($targetFile) 246 && filemtime($source) <= filemtime($targetFile) 247 ) { 248 //target is up to date 249 return; 250 } 251 //work around a bug in php by replacing /./ with / 252 $targetDir = str_replace('/./', '/', dirname($targetFile)); 253 if (!is_dir($targetDir)) { 254 $this->log("Creating directory '$targetDir'", Project::MSG_VERBOSE); 255 mkdir($targetDir, $this->mode, true); 256 } 257 258 $cmd = $tool 259 . ' --exit-status=2' 260 . ' ' . $this->toolParam 261 . ' ' . escapeshellarg($source) 262 . ' ' . escapeshellarg($targetFile) 263 . ' 2>&1'; 264 265 $this->log('command: ' . $cmd, Project::MSG_VERBOSE); 266 exec($cmd, $arOutput, $retval); 267 if ($retval != 0) { 268 $this->log(implode("\n", $arOutput), Project::MSG_INFO); 269 throw new BuildException('Rendering rST failed'); 270 } 271 $this->log(implode("\n", $arOutput), Project::MSG_DEBUG); 272 } 273 274 275 276 /** 277 * Finds the rst2* binary path 278 * 279 * @param string $format Output format 280 * 281 * @return string Full path to rst2$format 282 * 283 * @throws BuildException When the tool cannot be found 284 */ 285 protected function getToolPath($format) 286 { 287 if ($this->toolPath !== null) { 288 return $this->toolPath; 289 } 290 291 $tool = 'rst2' . $format; 292 $path = System::which($tool); 293 if (!$path) { 294 throw new BuildException( 295 sprintf('"%s" not found. Install python-docutils.', $tool) 296 ); 297 } 298 299 return $path; 300 } 301 302 303 304 /** 305 * Determines and returns the target file name from the 306 * input file and the configured destination name. 307 * 308 * @param string $file Input file 309 * @param string $destination Destination file or directory name, 310 * may be null 311 * 312 * @return string Target file name 313 * 314 * @uses $format 315 * @uses $targetExt 316 */ 317 public function getTargetFile($file, $destination = null) 318 { 319 if ($destination != '' 320 && substr($destination, -1) !== '/' 321 && substr($destination, -1) !== '\\' 322 ) { 323 return $destination; 324 } 325 326 if (strtolower(substr($file, -4)) == '.rst') { 327 $file = substr($file, 0, -4); 328 } 329 330 return $destination . $file . '.' . self::$targetExt[$this->format]; 331 } 332 333 334 335 /** 336 * The setter for the attribute "file" 337 * 338 * @param string $file Path of file to render 339 * 340 * @return void 341 */ 342 public function setFile($file) 343 { 344 $this->file = $file; 345 } 346 347 348 349 /** 350 * The setter for the attribute "format" 351 * 352 * @param string $format Output format 353 * 354 * @return void 355 * 356 * @throws BuildException When the format is not supported 357 */ 358 public function setFormat($format) 359 { 360 if (!in_array($format, self::$supportedFormats)) { 361 throw new BuildException( 362 sprintf( 363 'Invalid output format "%s", allowed are: %s', 364 $format, 365 implode(', ', self::$supportedFormats) 366 ) 367 ); 368 } 369 $this->format = $format; 370 } 371 372 373 374 /** 375 * The setter for the attribute "destination" 376 * 377 * @param string $destination Output file or directory. When it ends 378 * with a slash, it is taken as directory. 379 * 380 * @return void 381 */ 382 public function setDestination($destination) 383 { 384 $this->destination = $destination; 385 } 386 387 /** 388 * The setter for the attribute "toolparam" 389 * 390 * @param string $param Additional rst2* tool parameters 391 * 392 * @return void 393 */ 394 public function setToolparam($param) 395 { 396 $this->toolParam = $param; 397 } 398 399 /** 400 * The setter for the attribute "toolpath" 401 * 402 * @param string $param Full path to tool path, i.e. /usr/local/bin/rst2html 403 * 404 * @return void 405 * 406 * @throws BuildException When the tool does not exist or is not executable 407 */ 408 public function setToolpath($path) 409 { 410 if (!file_exists($path)) { 411 $fullpath = System::which($path); 412 if ($fullpath === false) { 413 throw new BuildException( 414 'Tool does not exist. Path: ' . $path 415 ); 416 } 417 $path = $fullpath; 418 } 419 if (!is_executable($path)) { 420 throw new BuildException( 421 'Tool not executable. Path: ' . $path 422 ); 423 } 424 $this->toolPath = $path; 425 } 426 427 /** 428 * The setter for the attribute "uptodate" 429 * 430 * @param string $uptodate True/false 431 * 432 * @return void 433 */ 434 public function setUptodate($uptodate) 435 { 436 $this->uptodate = (boolean)$uptodate; 437 } 438 439 440 441 /** 442 * Add a set of files to be rendered. 443 * 444 * @param FileSet $fileset Set of rst files to render 445 * 446 * @return void 447 */ 448 public function addFileset(FileSet $fileset) 449 { 450 $this->filesets[] = $fileset; 451 } 452 453 454 455 /** 456 * Nested creator, creates one Mapper for this task 457 * 458 * @return Mapper The created Mapper type object 459 * 460 * @throws BuildException 461 */ 462 public function createMapper() 463 { 464 if ($this->mapperElement !== null) { 465 throw new BuildException( 466 'Cannot define more than one mapper', $this->location 467 ); 468 } 469 $this->mapperElement = new Mapper($this->project); 470 return $this->mapperElement; 471 } 472 473 474 475 /** 476 * Creates a filterchain, stores and returns it 477 * 478 * @return FilterChain The created filterchain object 479 */ 480 public function createFilterChain() 481 { 482 $num = array_push($this->filterChains, new FilterChain($this->project)); 483 return $this->filterChains[$num-1]; 484 } 485} 486