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