Commit 5c420197 authored by Taddeus Kroes's avatar Taddeus Kroes

Added template parser classes.

- The Template class represents a template file, consisting of blocks and
  variables.
- The Node class is used to create a tree structure of the blocks in a
  template file, and of the data that is mapped on the blocks.
parent 08ec5a5b
...@@ -69,9 +69,10 @@ class FileNotFoundError extends \RuntimeException { ...@@ -69,9 +69,10 @@ class FileNotFoundError extends \RuntimeException {
* Sets an error message of the form 'File "path/to/file.php" does not exist.'. * Sets an error message of the form 'File "path/to/file.php" does not exist.'.
* *
* @param string $path Path to the file that does not exist. * @param string $path Path to the file that does not exist.
* @param bool $is_dir Whether the path points to a directory (defaults to false).
*/ */
function __construct($path) { function __construct($path, $is_dir=false) {
$this->message = sprintf('File "%s" does not exist.', $path); $this->message = sprintf('%s "%s" does not exist.', $is_dir ? 'Directory' : 'File', $path);
} }
} }
......
<?php
/**
* Tree data structure, used for rendering purposes.
*
* @author Taddeus Kroes
* @version 1.0
* @date 13-07-2012
*/
namespace BasicWeb;
require_once 'base.php';
/**
* Tree node.
*
* Each tree node has a (non-unique) name, a list of variables, and zero or
* more children.
*
* @package BasicWeb
*/
class Node extends Base {
/**
* The number of Node instances, used to create unique node id's.
*
* @var int
*/
private static $count = 0;
/**
* The unique id of this Bloc.
*
* @var int
*/
private $id;
/**
* The node's name.
*
* @var string
*/
private $name;
/**
* An optional parent node.
*
* If NULL, this node is the root of the data tree.
*
* @var Node
*/
private $parent_node;
/**
* Child nodes.
*
* @var array
*/
private $children = array();
/**
* Variables in this node.
*
* All variables in a node are also available in its descendants through
* {@link get()}.
*
* @var array
*/
private $variables = array();
/**
* Constructor.
*
* The id of the node is determined by the node counter.
*
* @param string $name The node's name.
* @param Node|null &$parent_node A parent node (optional).
* @param int|null $id An id to assign. If none is specified, a new unique
* id is generated.
* @uses $count
*/
function __construct($name='', Node &$parent_node=null, $id=null) {
$this->id = $id ? $id : ++self::$count;
$this->name = $name;
$this->parent_node = $parent_node;
}
/**
* Get the node's unique id.
*
* @return int The node's id.
*/
function get_id() {
return $this->id;
}
/**
* Get the node's name.
*
* @return string The node's name.
*/
function get_name() {
return $this->name;
}
/**
* Get the node's parent.
*
* @return Node|null The parent node if any, NULL otherwise.
*/
function get_parent() {
return $this->parent_node;
}
/**
* Get the node's children.
*
* @return array A list of child nodes.
*/
function get_children() {
return $this->children;
}
/**
* Check if a node is the same instance or a copy of this node.
*
* @param Node $node The node to compare this node to.
* @return bool Whether the nodes have the same unique id.
*/
function is(Node $node) {
return $node->get_id() == $this->id;
}
/**
* Check if this node is the root node of the tree.
*
* A node is the root node if it has no parent.
*
* @return bool Whether this node is the root node.
*/
function is_root() {
return $this->parent_node === null;
}
/**
* Check if this node is a leaf node of the tree.
*
* A node is a leaf if it has no children.
*
* @return bool Whether this node is a leaf node.
*/
function is_leaf() {
return !count($this->children);
}
/**
* Add a child node.
*
* @param Node &$node The child node to add.
* @param bool $set_parent Whether to set this node as the child's parent
* (defaults to TRUE).
*/
function add_child(Node &$node, $set_parent=true) {
$this->children[] = $node;
$set_parent && $node->set_parent($this);
}
/**
* Add a child node.
*
* @param string $name The name of the node to add.
* @param array $data Data to set in the created node (optional).
* @return Node The created node.
*/
function add($name, array $data=array()) {
$node = new self($name, $this);
$this->add_child($node, false);
return $node->set($data);
}
/**
* Remove a child node.
*
* @param Node &$child The node to remove.
*/
function remove_child(Node &$child) {
foreach( $this->children as $i => $node )
$node->is($child) && array_splice($this->children, $i, 1);
}
/**
* Remove this node from its parent.
*
* @throws \RuntimeException If the node has no parent.
* @return Node This node.
*/
function remove() {
if( $this->is_root() )
throw new \RuntimeException('Cannot remove the root node of a tree.');
$this->parent_node->remove_child($this);
foreach( $this->children as $child )
$child->set_parent(null);
return $this;
}
/**
* Set the node's parent.
*
* Removes this node as child of the original parent, if a parent was
* already set.
*
* @param Node|null $parent The parent node to set.
* @return Node This node.
*/
function set_parent($parent) {
if( $this->parent_node !== null )
$this->parent_node->remove_child($this);
$this->parent_node = &$parent;
return $this;
}
/**
* Set the value of one or more variables in the node.
*
* @param string|array $name Either a single variable name, or a set of name/value pairs.
* @param mixed $value The value of a single variable to set.
* @return Node This node.
*/
function set($name, $value=null) {
if( is_array($name) ) {
foreach( $name as $var => $val )
$this->variables[$var] = $val;
} else {
$this->variables[$name] = $value;
}
return $this;
}
/**
* Get the value of a variable.
*
* @param string $name The name of the variable to get the value of.
* @return mixed The value of the variable if it exists, NULL otherwise.
*/
function get($name) {
// Variable inside this node?
if( isset($this->variables[$name]) )
return $this->variables[$name];
// Variable in one of ancestors?
if( $this->parent_node !== null )
return $this->parent_node->get($name);
// All nodes up to the tree's root node do not contain the variable
return null;
}
/**
* Set the value of a variable.
*
* This method provides a shortcut for {@link set()}.
*
* @param string $name The name of the variable to set the value of.
* @param mixed $value The value to set.
*/
function __set($name, $value) {
$this->set($name, $value);
}
/**
* Get the value of a variable.
*
* This method provides a shortcut for {@link get()}.
*
* @param string $name The name of the variable to get the value of.
* @return mixed The value of the variable if it exists, NULL otherwise.
*/
function __get($name) {
return $this->get($name);
}
/**
* Find all child nodes that have the specified name.
*
* @param string $name The name of the nodes to find.
* @return array The positively matched nodes.
*/
function find($name) {
$has_name = function($child) use ($name) {
return $child->get_name() == $name;
};
return array_values(array_filter($this->children, $has_name));
}
/**
* Create a copy of this node.
*
* The copy will have the same list of children and variables. In case of
* a 'deep copy', the list of children is also cloned recursively.
*
* @param bool $deep Whether to create a deep copy.
* @return Node A copy of this node.
*/
function copy($deep=false) {
$copy = new self($this->name, $this->parent_node, $this->id);
$copy->set($this->variables);
foreach( $this->children as $child ) {
if( $deep ) {
$child_copy = $child->copy(true);
$copy->add_child($child_copy);
} else {
$copy->add_child($child, false);
}
}
return $copy;
}
}
?>
\ No newline at end of file
This diff is collapsed.
foofoo
foobar
foobar
foobaz
foofoo
foobaz
bar
foofoo
first_foobar_var
first_foobaz_var
foofoo
second_foobar_var
third_foobar_var
second_foobaz_var
baz
\ No newline at end of file
bar
{block:foo}
foofoo
{block:bar}
{foobar}
{end}
{foobaz:strtolower}
{end}
baz
\ No newline at end of file
foo
my_foobar_variable
bar
my_foobaz_variable
baz
\ No newline at end of file
{block:foo}
foofoo
{block:bar}
foobar
{end}
foobaz
{end}
\ No newline at end of file
test
\ No newline at end of file
bar
{block:foo}
foofoo
{block:bar}
{foobar}
{end}
{foobaz:strtolower}
{end}
baz
\ No newline at end of file
{block:foo}
foofoo
{block:bar}
foobar
{end}
foobaz
\ No newline at end of file
{block:foo}
foofoo
foobar
{end}
{end}
\ No newline at end of file
foo
{foobar}
bar
{foobaz:strtolower}
baz
\ No newline at end of file
<?php
require_once 'node.php';
use \BasicWeb\Node;
class NodeTest extends PHPUnit_Framework_TestCase {
var $autoloader;
function setUp() {
$this->root = new Node('test node');
}
function test_get_id() {
$this->assertEquals($this->root->get_id(), 1);
$this->assertEquals(Node::create('')->get_id(), 2);
}
function test_get_name() {
$this->assertEquals($this->root->get_name(), 'test node');
$this->assertEquals(Node::create('second node')->get_name(), 'second node');
}
function test_get_parent() {
$this->assertNull($this->root->get_parent());
$this->assertSame(Node::create('', $this->root)->get_parent(), $this->root);
}
function test_is() {
$mirror = $this->root;
$this->assertTrue($mirror->is($this->root));
$this->assertFalse(Node::create('')->is($this->root));
}
function test_is_root() {
$this->assertTrue($this->root->is_root());
$this->assertFalse(Node::create('', $this->root)->is_root());
}
function test_add_child() {
$node = new Node('');
$this->root->add_child($node);
$this->assertAttributeEquals(array($node), 'children', $this->root);
$this->assertSame($node->get_parent(), $this->root);
}
/**
* @depends test_add_child
*/
function test_get_children() {
$this->assertEquals($this->root->get_children(), array());
$node = new Node('');
$this->root->add_child($node);
$this->assertSame($this->root->get_children(), array($node));
}
function test_add_child_no_set_parent() {
$node = new Node('');
$this->root->add_child($node, false);
$this->assertAttributeEquals(array($node), 'children', $this->root);
$this->assertNull($node->get_parent());
}
/**
* @depends test_add_child
*/
function test_is_leaf() {
$node = new Node('');
$this->root->add_child($node);
$this->assertTrue($node->is_leaf());
$this->assertFalse($this->root->is_leaf());
}
/**
* @depends test_add_child
*/
function test_add() {
$node = $this->root->add('name', array('foo' => 'bar'));
$this->assertEquals($node->get_name(), 'name');
$this->assertEquals($node->get('foo'), 'bar');
$this->assertSame($node->get_parent(), $this->root);
}
/**
* @depends test_add
*/
function test_remove_child() {
$node1 = $this->root->add('name', array('foo' => 'bar'));
$node2 = $this->root->add('name', array('foo' => 'bar'));
$this->root->remove_child($node2);
$this->assertAttributeSame(array($node1), 'children', $this->root);
}
/**
* @depends test_remove_child
*/
function test_remove_leaf() {
$node1 = $this->root->add('name', array('foo' => 'bar'));
$node2 = $this->root->add('name', array('foo' => 'bar'));
$node1->remove();
$this->assertAttributeSame(array($node2), 'children', $this->root);
}
/**
* @depends test_remove_leaf
*/
function test_remove_node() {
$node = $this->root->add('node');
$leaf = $node->add('leaf');
$node->remove();
$this->assertAttributeEquals(array(), 'children', $this->root);
$this->assertNull($leaf->get_parent());
}
/**
* @depends test_remove_child
* @expectedException \RuntimeException
*/
function test_remove_root() {
$node1 = $this->root->add('name', array('foo' => 'bar'));
$node2 = $this->root->add('name', array('foo' => 'bar'));
$this->root->remove();
$this->assertAttributeSame(array($node2), 'children', $this->root);
}
function test_set_single() {
$this->root->set('foo', 'bar');
$this->assertAttributeEquals(array('foo' => 'bar'), 'variables', $this->root);
$this->root->set('bar', 'baz');
$this->assertAttributeEquals(array('foo' => 'bar', 'bar' => 'baz'), 'variables', $this->root);
}
function test_set_return() {
$this->assertSame($this->root->set('foo', 'bar'), $this->root);
}
function test_set_multiple() {
$this->root->set(array('foo' => 'bar'));
$this->assertAttributeEquals(array('foo' => 'bar'), 'variables', $this->root);
$this->root->set(array('bar' => 'baz'));
$this->assertAttributeEquals(array('foo' => 'bar', 'bar' => 'baz'), 'variables', $this->root);
}
/**
* @depends test_set_single
*/
function test___set() {
$this->root->foo = 'bar';
$this->assertAttributeEquals(array('foo' => 'bar'), 'variables', $this->root);
$this->root->bar = 'baz';
$this->assertAttributeEquals(array('foo' => 'bar', 'bar' => 'baz'), 'variables', $this->root);
}
/**
* @depends test_set_multiple
*/
function test_get_direct() {
$this->root->set(array('foo' => 'bar', 'bar' => 'baz'));
$this->assertEquals($this->root->get('foo'), 'bar');
$this->assertEquals($this->root->get('bar'), 'baz');
}
/**
* @depends test_get_direct
*/
function test___get() {
$this->root->set(array('foo' => 'bar', 'bar' => 'baz'));
$this->assertEquals($this->root->foo, 'bar');
$this->assertEquals($this->root->bar, 'baz');
}
/**
* @depends test_set_single
*/
function test_get_ancestor() {
$this->root->set('foo', 'bar');
$node = $this->root->add('');
$this->assertEquals($node->get('foo'), 'bar');
}
function test_get_failure() {
$this->assertNull($this->root->get('foo'));
}
/**
* @depends test_get_name
*/
function test_find() {
$node1 = $this->root->add('foo');
$node2 = $this->root->add('bar');
$node3 = $this->root->add('foo');
$this->assertSame($this->root->find('foo'), array($node1, $node3));
}
/**
* @depends test_set_multiple
*/
function test_copy_simple() {
$copy = $this->root->copy();
$this->assertEquals($this->root, $copy);
$this->assertNotSame($this->root, $copy);
}
/**
* @depends test_copy_simple
*/
function test_copy_shallow() {
$child = $this->root->add('');
$copy = $this->root->copy();
$this->assertAttributeSame(array($child), 'children', $copy);
}
/**
* @depends test_get_children
* @depends test_copy_simple
*/
function test_copy_deep() {
$child = $this->root->add('foo');
$copy = $this->root->copy(true);
$copy_children = $copy->get_children();
$child_copy = reset($copy_children);
$this->assertNotSame($copy_children, $this->root->get_children());
$this->assertSame($child_copy->get_parent(), $copy);
}
}
?>
\ No newline at end of file
<?php
require_once 'template.php';
use BasicWeb\Template;
use BasicWeb\Node;
define('TEMPLATES_DIR', 'tests/_files/templates/');
class TemplateTest extends PHPUnit_Framework_TestCase {
/**
* @depends test_add_root_success
*/
function setUp() {
Template::set_root(TEMPLATES_DIR);
$this->tpl = new Template('foo');
$this->data = new Node();
$object = new stdClass();
$object->foo = 'bar';
$object->bar = 'baz';
$this->data->set(array(
'foo' => 'bar',
'bar' => 'baz',
'FOO' => 'BAR',
'true' => true,
'false' => false,
'array' => array('foo' => 'bar', 'bar' => 'baz'),
'object' => $object,
'foobar' => 'my_foobar_variable',
'foobaz' => 'MY_FOOBAZ_VARIABLE',
));
}
/**
* @expectedException BasicWeb\FileNotFoundError
* @expectedExceptionMessage Directory "non_existing_folder/" does not exist.
*/
function test_add_root_failure() {
Template::add_root('non_existing_folder');
}
function assert_include_path_equals($expected) {
$include_path = new ReflectionProperty('BasicWeb\Template', 'include_path');
$include_path->setAccessible(true);
$this->assertEquals($expected, $include_path->getValue());
}
function test_clear_include_path() {
Template::clear_include_path();
$this->assert_include_path_equals(array());
}
/**
* @depends test_clear_include_path
*/
function test_add_root_success() {
Template::clear_include_path();
Template::add_root(TEMPLATES_DIR);
$this->assert_include_path_equals(array(TEMPLATES_DIR));
Template::add_root('tests/_files');
$this->assert_include_path_equals(array(TEMPLATES_DIR, 'tests/_files/'));
}
/**
* @depends test_add_root_success
*/
function test_set_root() {
Template::clear_include_path();
Template::add_root(TEMPLATES_DIR);
Template::add_root('tests/_files');
Template::set_root(TEMPLATES_DIR);
$this->assert_include_path_equals(array(TEMPLATES_DIR));
}
/**
* @expectedException RuntimeException
*/
function test_non_existing_template() {
$bar = new Template('bar');
}
function test_other_root() {
Template::add_root('tests/_files/other_templates');
new Template('bar');
}
function test_get_path() {
$this->assertEquals(TEMPLATES_DIR.'foo.tpl', $this->tpl->get_path());
}
function get_property($object, $property_name) {
$rp = new ReflectionProperty($object, $property_name);
$rp->setAccessible(true);
return $rp->getValue($object);
}
function test_parse_blocks_simple() {
$root_block = $this->get_property($this->tpl, 'root_block');
$this->assert_is_block_node($root_block, null, 1);
list($child) = $root_block->get_children();
$this->assert_is_html_node($child, 'test');
}
function assert_is_html_node($node, $content) {
$this->assertEquals('html', $node->get_name());
$this->assertEquals($content, $node->get('content'));
$this->assertEquals(array(), $node->get_children());
}
function assert_is_block_node($node, $block_name, $child_count) {
$this->assertEquals('block', $node->get_name());
$this->assertSame($block_name, $node->get('name'));
$this->assertNull($node->get('content'));
$this->assertEquals($child_count, count($node->get_children()));
}
function assert_is_variable_node($node, $brackets_content) {
$this->assertEquals('variable', $node->get_name());
$this->assertEquals($brackets_content, $node->get('content'));
$this->assertEquals(array(), $node->get_children());
}
/**
* @depends test_parse_blocks_simple
*/
function test_parse_blocks_blocks() {
$tpl = new Template('blocks');
$root_block = $this->get_property($tpl, 'root_block');
$this->assert_is_block_node($root_block, null, 3);
list($before, $foo, $after) = $root_block->get_children();
$this->assert_is_html_node($before, '');
$this->assert_is_block_node($foo, 'foo', 3);
$this->assert_is_html_node($after, '');
list($foofoo, $bar, $foobaz) = $foo->get_children();
$this->assert_is_html_node($foofoo, "\nfoofoo\n\t");
$this->assert_is_block_node($bar, 'bar', 1);
$this->assert_is_html_node($foobaz, "\nfoobaz\n");
list($foobar) = $bar->get_children();
$this->assert_is_html_node($foobar, "\n\tfoobar\n\t");
}
/**
* @depends test_parse_blocks_blocks
* @expectedException BasicWeb\ParseError
* @expectedExceptionMessage Parse error in file tests/_files/templates/unexpected_end.tpl, line 5: unexpected {end}
*/
function test_parse_blocks_unexpected_end() {
new Template('unexpected_end');
}
/**
* @depends test_parse_blocks_blocks
* @expectedException BasicWeb\ParseError
* @expectedExceptionMessage Parse error in file tests/_files/templates/missing_end.tpl, line 6: missing {end}
*/
function test_parse_blocks_missing_end() {
new Template('missing_end');
}
/**
* @depends test_parse_blocks_simple
*/
function test_parse_blocks_variables() {
$tpl = new Template('variables');
$root_block = $this->get_property($tpl, 'root_block');
$this->assert_is_block_node($root_block, null, 5);
list($foo, $foobar, $bar, $foobaz, $baz) = $root_block->get_children();
$this->assert_is_html_node($foo, "foo\n");
$this->assert_is_variable_node($foobar, 'foobar');
$this->assert_is_html_node($bar, "\nbar\n");
$this->assert_is_variable_node($foobaz, 'foobaz:strtolower');
$this->assert_is_html_node($baz, "\nbaz");
}
/**
* @depends test_parse_blocks_blocks
* @depends test_parse_blocks_variables
*/
function test_parse_blocks_full() {
$tpl = new Template('full');
$root_block = $this->get_property($tpl, 'root_block');
$this->assert_is_block_node($root_block, null, 3);
list($bar, $foo, $baz) = $root_block->get_children();
$this->assert_is_html_node($bar, "bar\n");
$this->assert_is_block_node($foo, 'foo', 5);
$this->assert_is_html_node($baz, "\nbaz");
list($foofoo, $bar, $first_space, $foobaz, $second_space) = $foo->get_children();
$this->assert_is_html_node($foofoo, "\nfoofoo\n\t");
$this->assert_is_block_node($bar, 'bar', 3);
$this->assert_is_html_node($first_space, "\n");
$this->assert_is_variable_node($foobaz, 'foobaz:strtolower');
$this->assert_is_html_node($second_space, "\n");
list($space_before, $foobar, $space_after) = $bar->get_children();
$this->assert_is_html_node($space_before, "\n\t");
$this->assert_is_variable_node($foobar, 'foobar');
$this->assert_is_html_node($space_after, "\n\t");
}
function assert_replaces($expected, $variable) {
$rm = new ReflectionMethod('BasicWeb\Template', 'replace_variable');
$rm->setAccessible(true);
$this->assertEquals($expected, $rm->invoke(null, $variable, $this->data));
}
function test_replace_variable_simple() {
$this->assert_replaces('bar', 'foo');
}
/**
* @depends test_replace_variable_simple
*/
function test_replace_variable_helper_functions() {
$this->assert_replaces('Bar', 'foo:ucfirst');
$this->assert_replaces('bar', 'FOO:strtolower');
$this->assert_replaces('Bar', 'FOO:strtolower:ucfirst');
}
/**
* @expectedException UnexpectedValueException
* @expectedExceptionMessage Helper function "idonotexist" is not callable.
*/
function test_replace_variable_non_callable_helper_function() {
$this->assert_replaces(null, 'foo:idonotexist');
}
function test_replace_variable_not_found() {
$this->assert_replaces('{idonotexist}', 'idonotexist');
}
function test_replace_variable_associative_array() {
$this->assert_replaces('bar', 'array.foo');
$this->assert_replaces('baz', 'array.bar');
}
function test_replace_variable_object_property() {
$this->assert_replaces('bar', 'object.foo');
$this->assert_replaces('baz', 'object.bar');
}
function test_replace_variable_if_statement() {
$this->assert_replaces('bar', 'if:true:foo');
$this->assert_replaces('', 'if:false:foo');
$this->assert_replaces('bar', 'if:true:foo:else:bar');
$this->assert_replaces('baz', 'if:false:foo:else:bar');
$this->assert_replaces('Bar', 'if:false:foo:else:FOO:strtolower:ucfirst');
}
/*function assert_block_renders($expected_file, $block, $data) {
$rm = new ReflectionMethod('BasicWeb\Template', 'render_block');
$rm->setAccessible(true);
$expected_file = "tests/_files/rendered/$expected_file.html";
$this->assertStringEqualsFile($expected_file, $rm->invoke(null, $block, $data));
}*/
function assert_renders($expected_file, $tpl) {
$expected_file = "tests/_files/rendered/$expected_file.html";
$this->assertStringEqualsFile($expected_file, $tpl->render());
}
function test_render_simple() {
$this->assertEquals('test', $this->tpl->render());
}
/**
* @depends test_replace_variable_helper_functions
*/
function test_render_variable() {
$tpl = new Template('variables');
$tpl->set(array(
'foobar' => 'my_foobar_variable',
'foobaz' => 'MY_FOOBAZ_VARIABLE'
));
$this->assert_renders('variables', $tpl);
}
/**
* @depends test_render_simple
*/
function test_render_blocks() {
$tpl = new Template('blocks');
$foo = $tpl->add('foo');
$foo->add('bar');
$foo->add('bar');
$tpl->add('foo');
$this->assert_renders('blocks', $tpl);
}
/**
* @depends test_render_variable
* @depends test_render_blocks
*/
function test_render_full() {
$tpl = new Template('full');
$first_foo = $tpl->add('foo')->set('foobaz', 'FIRST_FOOBAZ_VAR');
$first_foo->add('bar')->set('foobar', 'first_foobar_var');
$second_foo = $tpl->add('foo')->set('foobaz', 'SECOND_FOOBAZ_VAR');
$second_foo->add('bar')->set('foobar', 'second_foobar_var');
$second_foo->add('bar')->set('foobar', 'third_foobar_var');
$this->assert_renders('full', $tpl);
}
}
?>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment