The changes
If you look at the previous build.xml file you know processing content in phing will turn in spaghetti code very soon. So I moved it to the template task.
<?xml version="1.0" encoding="utf-8"?> | |
<project name="Ugling" basedir="." default="loop"> | |
<!-- set variables to use in the tasks --> | |
<property name="dir.raw.content" value="content" /> | |
<property name="dir.www.content" value="online/" /> | |
<property name="dir.templates" value="templates" /> | |
<taskdef name="markdown" classname="phing.tasks.ext.MarkdownTask" /> | |
<taskdef name="template" classname="phing.tasks.ext.TemplateTask" /> | |
<target name="loop"> | |
<markdown destination="${dir.www.content}" removefilesetdir="${dir.raw.content}"> | |
<fileset dir="${dir.raw.content}"> | |
<include name="*.md" /> | |
<include name="**/*.md" /> | |
</fileset> | |
</markdown> | |
<template templatesdir="${dir.templates}"> | |
<fileset dir="${dir.www.content}"> | |
<include name="*.html" /> | |
<include name="**/*.html" /> | |
</fileset> | |
</template> | |
</target> | |
</project> |
The new build.xml file is easy to read because now you just have two tasks.
To get the build result I want i needed to add an attribute to the markdown task, removefilesetdir.
For me it was a cleaner way to process the markdown files and add the processed files to another directory than the way the rSTTask documentation shows.
<?php | |
require_once 'phing/Task.php'; | |
require_once 'phing/util/FileUtils.php'; | |
require_once 'System.php'; | |
require_once "phing/tasks/ext/markdown/markdown.php"; | |
class MarkdownTask extends Task { | |
/** | |
* @var string Taskname for logger | |
*/ | |
protected $taskName = 'Markdown'; | |
/** | |
* Result format, defaults to "html". | |
* @see $supportedFormats for all possible options | |
* | |
* @var string | |
*/ | |
protected $format = 'html'; | |
/** | |
* Input file in markdown format. | |
* Required | |
* | |
* @var string | |
*/ | |
protected $file = null; | |
/** | |
* Output file or directory. May be omitted. | |
* When it ends with a slash, it is considered to be a directory | |
* | |
* @var string | |
*/ | |
protected $destination = null; | |
protected $removefilesetdir = null; | |
protected $filesets = array(); // all fileset objects assigned to this task | |
protected $mapperElement = null; | |
/** | |
* all filterchains objects assigned to this task | |
* | |
* @var array | |
*/ | |
protected $filterChains = array(); | |
/** | |
* mode to create directories with | |
* | |
* @var integer | |
*/ | |
protected $mode = 0755; | |
/** | |
* Only render files whole source files are newer than the | |
* target files | |
* | |
* @var boolean | |
*/ | |
protected $uptodate = false; | |
/** | |
* The main entry point method. | |
* | |
* @return void | |
*/ | |
public function main() | |
{ | |
if (count($this->filterChains)) { | |
$this->fileUtils = new FileUtils(); | |
} | |
if ($this->file != '') { | |
$file = $this->file; | |
$targetFile = $this->getTargetFile($file, $this->destination); | |
$this->render($file, $targetFile); | |
return; | |
} | |
if (!count($this->filesets)) { | |
throw new BuildException( | |
'"file" attribute or "fileset" subtag required' | |
); | |
} | |
// process filesets | |
$mapper = null; | |
if ($this->mapperElement !== null) { | |
$mapper = $this->mapperElement->getImplementation(); | |
} | |
$project = $this->getProject(); | |
foreach ($this->filesets as $fs) { | |
$ds = $fs->getDirectoryScanner($project); | |
$fromDir = $fs->getDir($project); | |
$srcFiles = $ds->getIncludedFiles(); | |
foreach ($srcFiles as $src) { | |
$file = new PhingFile($fromDir, $src); | |
if ($mapper !== null) { | |
$results = $mapper->main($file); | |
if ($results === null) { | |
throw new BuildException( | |
sprintf( | |
'No filename mapper found for "%s"', | |
$file | |
) | |
); | |
} | |
$targetFile = reset($results); | |
} else { | |
$targetFile = $this->getTargetFile($file, $this->destination); | |
} | |
$this->render($file, $targetFile); | |
} | |
} | |
} | |
/** | |
* Renders a single file and applies filters on it | |
* | |
* @param string $tool conversion tool to use | |
* @param string $source markdown source file | |
* @param string $targetFile target file name | |
* | |
* @return void | |
*/ | |
protected function render($source, $targetFile) | |
{ | |
if (count($this->filterChains) == 0) { | |
return $this->renderFile($source, $targetFile); | |
} | |
$tmpTarget = tempnam(sys_get_temp_dir(), 'tmp-'); | |
$this->renderFile($source, $tmpTarget); | |
$this->fileUtils->copyFile( | |
new PhingFile($tmpTarget), | |
new PhingFile($targetFile), | |
true, false, $this->filterChains, | |
$this->getProject(), $this->mode | |
); | |
unlink($tmpTarget); | |
} | |
/** | |
* Renders a single file with the markdown tool. | |
* | |
* @param string $tool conversion tool to use | |
* @param string $source markdown source file | |
* @param string $targetFile target file name | |
* | |
* @return void | |
* | |
* @throws BuildException When the conversion fails | |
*/ | |
protected function renderFile($source, $targetFile) | |
{ | |
if ($this->uptodate && file_exists($targetFile) | |
&& filemtime($source) <= filemtime($targetFile) | |
) { | |
//target is up to date | |
return; | |
} | |
//work around a bug in php by replacing /./ with / | |
$targetDir = str_replace('/./', '/', dirname($targetFile)); | |
if (!is_dir($targetDir)) { | |
mkdir($targetDir, $this->mode, true); | |
} | |
$arOutput = Markdown(file_get_contents($source)); | |
$retval = file_put_contents($targetFile,$arOutput); | |
if ( ! $retval) { | |
$this->log('File not rendered.', Project::MSG_INFO); | |
throw new BuildException('Rendering markdown failed'); | |
} | |
$this->log('File rendered.', Project::MSG_DEBUG); | |
} | |
/** | |
* Determines and returns the target file name from the | |
* input file and the configured destination name. | |
* | |
* @param string $file Input file | |
* @param string $destination Destination file or directory name, | |
* may be null | |
* | |
* @return string Target file name | |
* | |
* @uses $format | |
* @uses $targetExt | |
*/ | |
public function getTargetFile($file, $destination = null) | |
{ | |
if ($destination != '' | |
&& substr($destination, -1) !== '/' | |
&& substr($destination, -1) !== '\\' | |
) { | |
return $destination; | |
} | |
if (strtolower(substr($file, -3)) == '.md') { | |
$file = substr($file, 0, -3); | |
} | |
if($this->removefilesetdir) | |
$file = str_replace($this->removefilesetdir.DIRECTORY_SEPARATOR,'',$file); | |
return $destination . $file . '.' . $this->format; | |
} | |
/** | |
* The setter for the attribute "file" | |
* | |
* @param string $file Path of file to render | |
* | |
* @return void | |
*/ | |
public function setFile($file) | |
{ | |
$this->file = $file; | |
} | |
/** | |
* The setter for the attribute "destination" | |
* | |
* @param string $destination Output file or directory. When it ends | |
* with a slash, it is taken as directory. | |
* | |
* @return void | |
*/ | |
public function setDestination($destination) | |
{ | |
$this->destination = $destination; | |
} | |
/** | |
* The setter for the attribute "destination" | |
* | |
* @param string $destination Output file or directory. When it ends | |
* with a slash, it is taken as directory. | |
* | |
* @return void | |
*/ | |
public function setRemovefilesetdir($removefilesetdir) | |
{ | |
$this->removefilesetdir = $removefilesetdir; | |
} | |
/** | |
* The setter for the attribute "uptodate" | |
* | |
* @param string $uptodate True/false | |
* | |
* @return void | |
*/ | |
public function setUptodate($uptodate) | |
{ | |
$this->uptodate = (boolean)$uptodate; | |
} | |
/** | |
* Nested creator, creates a FileSet for this task | |
* | |
* @return object The created fileset object | |
*/ | |
public function createFileSet() | |
{ | |
$num = array_push($this->filesets, new FileSet()); | |
return $this->filesets[$num-1]; | |
} | |
/** | |
* Nested creator, creates one Mapper for this task | |
* | |
* @return Mapper The created Mapper type object | |
* | |
* @throws BuildException | |
*/ | |
public function createMapper() | |
{ | |
if ($this->mapperElement !== null) { | |
throw new BuildException( | |
'Cannot define more than one mapper', $this->location | |
); | |
} | |
$this->mapperElement = new Mapper($this->project); | |
return $this->mapperElement; | |
} | |
/** | |
* Creates a filterchain, stores and returns it | |
* | |
* @return FilterChain The created filterchain object | |
*/ | |
public function createFilterChain() | |
{ | |
$num = array_push($this->filterChains, new FilterChain($this->project)); | |
return $this->filterChains[$num-1]; | |
} | |
} |
The template engine vision
I want working with the generator to be as easy as possible so I try to stay away from code-like constructions as much as possible.
But at the same time it has to be as flexible as possible too.
The first problem I encounter is how can I provide a default template but allow specific templates at the same time.
Because i started to work with base template I got the idea that each directory could have a template.html that wraps all files in that directory and its subdirectories.
The only exception is the root template directory, it's required to have a template.html there.
To allow file specific templates it seemed the most logical to use the name of the file, so if your content file is called test.md and you want a file template you create a test.html file in the templates directory.
The template task today
I took my MarkdownTask.php code to start the TemplateTask.php code.
I concentrated on getting it to work so the code isn't that clean yet.
<?php | |
require_once 'phing/Task.php'; | |
require_once 'phing/util/FileUtils.php'; | |
require_once 'System.php'; | |
require_once "phing/tasks/ext/markdown/markdown.php"; | |
class TemplateTask extends Task { | |
/** | |
* @var string Taskname for logger | |
*/ | |
protected $taskName = 'Template'; | |
/** | |
* Input file in markdown format. | |
* Required | |
* | |
* @var string | |
*/ | |
protected $file = null; | |
/** | |
* Output file or directory. May be omitted. | |
* When it ends with a slash, it is considered to be a directory | |
* | |
* @var string | |
*/ | |
protected $destination = null; | |
/** | |
* templatefiles directory. required. | |
* | |
* @var string | |
*/ | |
protected $templatesDir; | |
/** | |
* default template content. required. | |
* | |
* @var string | |
*/ | |
protected $template_content; | |
/** | |
* default template placeholders. at least one required. | |
* | |
* @var array | |
*/ | |
protected $template_placeholders = array(); | |
protected $filesets = array(); // all fileset objects assigned to this task | |
protected $mapperElement = null; | |
/** | |
* all filterchains objects assigned to this task | |
* | |
* @var array | |
*/ | |
protected $filterChains = array(); | |
/** | |
* mode to create directories with | |
* | |
* @var integer | |
*/ | |
protected $mode = 0755; | |
/** | |
* Only render files whole source files are newer than the | |
* target files | |
* | |
* @var boolean | |
*/ | |
protected $uptodate = false; | |
/** | |
* The main entry point method. | |
* | |
* @return void | |
*/ | |
public function main() | |
{ | |
if( ! $this->templatesDir){ | |
throw new BuildException('The templates directory is required'); | |
} | |
$this->setTemplatePlaceholders('default'); | |
if (count($this->filterChains)) { | |
$this->fileUtils = new FileUtils(); | |
} | |
if ($this->file != '') { | |
$file = $this->file; | |
$targetFile = $this->getTargetFile($file, $this->destination); | |
$this->render($file, $targetFile); | |
return; | |
} | |
if (!count($this->filesets)) { | |
throw new BuildException( | |
'"file" attribute or "fileset" subtag required' | |
); | |
} | |
// process filesets | |
$mapper = null; | |
if ($this->mapperElement !== null) { | |
$mapper = $this->mapperElement->getImplementation(); | |
} | |
$project = $this->getProject(); | |
foreach ($this->filesets as $fs) { | |
$ds = $fs->getDirectoryScanner($project); | |
$fromDir = $fs->getDir($project); | |
$srcFiles = $ds->getIncludedFiles(); | |
foreach ($srcFiles as $src) { | |
$file = new PhingFile($fromDir, $src); | |
if ($mapper !== null) { | |
$results = $mapper->main($file); | |
if ($results === null) { | |
throw new BuildException( | |
sprintf( | |
'No filename mapper found for "%s"', | |
$file | |
) | |
); | |
} | |
$targetFile = reset($results); | |
} else { | |
$targetFile = $this->getTargetFile($file, $this->destination); | |
} | |
$this->render($file, $targetFile); | |
} | |
} | |
} | |
/* | |
* | |
*/ | |
protected function setTemplatePlaceholders($templateDir) | |
{ | |
if($templateDir == 'default'){ | |
if( ! file_exists($this->templatesDir.DIRECTORY_SEPARATOR.'template.html')){ | |
throw new BuildException('A template.html file must in the templates directory'); | |
} | |
$this->template_content = file_get_contents($this->templatesDir.DIRECTORY_SEPARATOR.'template.html'); | |
preg_match_all('/\[[A-Z]+\]/',$this->template_content,$placeholders); | |
if( ! $placeholders){ | |
throw new BuildException('At least a [CONTENT] placeholder is required'); | |
} | |
$this->template_placeholders = $placeholders[0]; | |
return true; | |
}else{ | |
} | |
} | |
/** | |
* Renders a single file and applies filters on it | |
* | |
* @param string $tool conversion tool to use | |
* @param string $source markdown source file | |
* @param string $targetFile target file name | |
* | |
* @return void | |
*/ | |
protected function render($source, $targetFile) | |
{ | |
if (count($this->filterChains) == 0) { | |
return $this->renderFile($source, $targetFile); | |
} | |
$tmpTarget = tempnam(sys_get_temp_dir(), 'tmp-'); | |
$this->renderFile($source, $tmpTarget); | |
$this->fileUtils->copyFile( | |
new PhingFile($tmpTarget), | |
new PhingFile($targetFile), | |
true, false, $this->filterChains, | |
$this->getProject(), $this->mode | |
); | |
unlink($tmpTarget); | |
} | |
/** | |
* Renders a single file with the markdown tool. | |
* | |
* @param string $tool conversion tool to use | |
* @param string $source markdown source file | |
* @param string $targetFile target file name | |
* | |
* @return void | |
* | |
* @throws BuildException When the conversion fails | |
*/ | |
protected function renderFile($source, $targetFile) | |
{ | |
if ($this->uptodate && file_exists($targetFile) | |
&& filemtime($source) <= filemtime($targetFile) | |
) { | |
//target is up to date | |
return; | |
} | |
//work around a bug in php by replacing /./ with / | |
$targetDir = str_replace('/./', '/', dirname($targetFile)); | |
if (!is_dir($targetDir)) { | |
mkdir($targetDir, $this->mode, true); | |
} | |
$arOutput = $this->wrapTemplate($source); | |
$retval = file_put_contents($targetFile,$arOutput); | |
if ( ! $retval) { | |
$this->log('File not rendered.', Project::MSG_INFO); | |
throw new BuildException('Rendering template failed'); | |
} | |
$this->log('File rendered.', Project::MSG_DEBUG); | |
} | |
protected function wrapTemplate($sourcefile) | |
{ | |
$template_content = $this->template_content; | |
$template_placeholders = $this->template_placeholders; | |
if(strpos($sourcefile,DIRECTORY_SEPARATOR) !== false){ | |
} | |
$placeholders = array_fill_keys($template_placeholders,''); | |
$placeholders['[CONTENT]'] = file_get_contents($sourcefile); | |
return strtr($template_content,$placeholders); | |
} | |
/** | |
* Determines and returns the target file name from the | |
* input file and the configured destination name. | |
* | |
* @param string $file Input file | |
* @param string $destination Destination file or directory name, | |
* may be null | |
* | |
* @return string Target file name | |
* | |
* @uses $format | |
* @uses $targetExt | |
*/ | |
public function getTargetFile($file, $destination = null) | |
{ | |
if ($destination != '' | |
&& substr($destination, -1) !== '/' | |
&& substr($destination, -1) !== '\\' | |
) { | |
return $destination; | |
} | |
return $destination . $file ; | |
} | |
/** | |
* The setter for the attribute "templatesdir" | |
* | |
* @param string $templatesDir directory of the templatefiles | |
* | |
* @return void | |
*/ | |
public function setTemplatesDir($templatesDir) | |
{ | |
$this->templatesDir = $templatesDir; | |
$this->template = $templatesDir.'/template.html'; | |
} | |
/** | |
* The setter for the attribute "uptodate" | |
* | |
* @param string $uptodate True/false | |
* | |
* @return void | |
*/ | |
public function setUptodate($uptodate) | |
{ | |
$this->uptodate = (boolean)$uptodate; | |
} | |
/** | |
* Nested creator, creates a FileSet for this task | |
* | |
* @return object The created fileset object | |
*/ | |
public function createFileSet() | |
{ | |
$num = array_push($this->filesets, new FileSet()); | |
return $this->filesets[$num-1]; | |
} | |
/** | |
* Nested creator, creates one Mapper for this task | |
* | |
* @return Mapper The created Mapper type object | |
* | |
* @throws BuildException | |
*/ | |
public function createMapper() | |
{ | |
if ($this->mapperElement !== null) { | |
throw new BuildException( | |
'Cannot define more than one mapper', $this->location | |
); | |
} | |
$this->mapperElement = new Mapper($this->project); | |
return $this->mapperElement; | |
} | |
/** | |
* Creates a filterchain, stores and returns it | |
* | |
* @return FilterChain The created filterchain object | |
*/ | |
public function createFilterChain() | |
{ | |
$num = array_push($this->filterChains, new FilterChain($this->project)); | |
return $this->filterChains[$num-1]; | |
} | |
} |
The default template code is working.
To prevent placeholders showing up in the online files they are set to an empty string before the actual content is added or generated.
Next time
The next blog post I will have the template vision working and have come up with a plan to add generated navigation content.
No comments:
Post a Comment