浏览代码

Merge branch 'dev' into feature/mustache-spec

Justin Hileman 15 年之前
父节点
当前提交
ab29f6eef4

+ 70 - 15
Mustache.php

@@ -191,7 +191,7 @@ class Mustache {
 	 * @return string Rendered Mustache template.
 	 */
 	protected function _renderTemplate($template) {
-		$template = $this->_renderSection($template);
+		$template = $this->_renderSections($template);
 		return $this->_renderTags($template);
 	}
 
@@ -202,18 +202,9 @@ class Mustache {
 	 * @param string $template
 	 * @return string
 	 */
-	protected function _renderSection($template) {
-		$otag  = preg_quote($this->_otag, '/');
-		$ctag  = preg_quote($this->_ctag, '/');
-		$regex = '/' . $otag . '(\\^|\\#)\\s*(.+?)\\s*' . $ctag . '\\s*([\\s\\S]+?)' . $otag . '\\/\\s*\\2\\s*' . $ctag . '\\s*/ms';
-
-		$matches = array();
-		while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE)) {
-			$section  = $matches[0][0];
-			$offset   = $matches[0][1];
-			$type     = $matches[1][0];
-			$tag_name = trim($matches[2][0]);
-			$content  = $matches[3][0];
+	protected function _renderSections($template) {
+		while ($section_data = $this->_findSection($template)) {
+			list($section, $offset, $type, $tag_name, $content) = $section_data;
 
 			$replace = '';
 			$val = $this->_getVariable($tag_name);
@@ -267,6 +258,71 @@ class Mustache {
 		return $template;
 	}
 
+	/**
+	 * Extract a section from $template.
+	 *
+	 * This is a helper function to find sections needed by _renderSections.
+	 *
+	 * @access protected
+	 * @param string $template
+	 * @return array $section, $offset, $type, $tag_name and $content
+	 */
+	protected function _findSection($template) {
+		$otag  = preg_quote($this->_otag, '/');
+		$ctag  = preg_quote($this->_ctag, '/');
+		$regex = '/' . $otag . '([\\^\\#\\/])\\s*(.+?)\\s*' . $ctag . '\\s*/ms';
+
+		$section_start = null;
+		$section_type  = null;
+		$content_start = null;
+
+		$search_offset = 0;
+
+		$section_stack = array();
+		$matches = array();
+		while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE, $search_offset)) {
+
+			$match    = $matches[0][0];
+			$offset   = $matches[0][1];
+			$type     = $matches[1][0];
+			$tag_name = trim($matches[2][0]);
+
+			$search_offset = $offset + strlen($match);
+
+			switch ($type) {
+				case '^':
+				case '#':
+					if (empty($section_stack)) {
+						$section_start = $offset;
+						$section_type  = $type;
+						$content_start = $search_offset;
+					}
+					array_push($section_stack, $tag_name);
+					break;
+				case '/':
+					if (empty($section_stack) || ($tag_name !== array_pop($section_stack))) {
+						if ($this->_throwsException(MustacheException::UNEXPECTED_CLOSE_SECTION)) {
+							throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION);
+						}
+					}
+
+					if (empty($section_stack)) {
+						$section = substr($template, $section_start, $search_offset - $section_start);
+						$content = substr($template, $content_start, $offset - $content_start);
+
+						return array($section, $section_start, $section_type, $tag_name, $content);
+					}
+					break;
+			}
+		}
+
+		if (!empty($section_stack)) {
+			if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) {
+				throw new MustacheException('Unclosed section: ' . $section_stack[0], MustacheException::UNCLOSED_SECTION);
+			}
+		}
+	}
+
 	/**
 	 * Initialize pragmas and remove all pragma tags.
 	 *
@@ -353,7 +409,6 @@ class Mustache {
 		return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array();
 	}
 
-
 	/**
 	 * Check whether this Mustache instance throws a given exception.
 	 *
@@ -609,7 +664,7 @@ class Mustache {
 				} else if (isset($view->$tag_name)) {
 					return $view->$tag_name;
 				}
-			} else if (isset($view[$tag_name])) {
+			} else if (array_key_exists($tag_name, $view)) {
 				return $view[$tag_name];
 			}
 		}

+ 33 - 0
examples/sections_nested/SectionsNested.php

@@ -0,0 +1,33 @@
+<?php
+
+class SectionsNested extends Mustache {
+	public $name = 'Little Mac';
+
+	public function enemies() {
+		return array(
+			array(
+				'name' => 'Von Kaiser',
+				'enemies' => array(
+					array('name' => 'Super Macho Man'),
+					array('name' => 'Piston Honda'),
+					array('name' => 'Mr. Sandman'),
+				)
+			),
+			array(
+				'name' => 'Mike Tyson',
+				'enemies' => array(
+					array('name' => 'Soda Popinski'),
+					array('name' => 'King Hippo'),
+					array('name' => 'Great Tiger'),
+					array('name' => 'Glass Joe'),
+				)
+			),
+			array(
+				'name' => 'Don Flamenco',
+				'enemies' => array(
+					array('name' => 'Bald Bull'),
+				)
+			),
+		);
+	}
+}

+ 7 - 0
examples/sections_nested/sections_nested.mustache

@@ -0,0 +1,7 @@
+Enemies of {{ name }}:
+{{# enemies }}
+{{ name }} ... who also has enemies:
+{{# enemies }}
+--> {{ name }}
+{{/ enemies }}
+{{/ enemies }}

+ 12 - 0
examples/sections_nested/sections_nested.txt

@@ -0,0 +1,12 @@
+Enemies of Little Mac:
+Von Kaiser ... who also has enemies:
+--> Super Macho Man
+--> Piston Honda
+--> Mr. Sandman
+Mike Tyson ... who also has enemies:
+--> Soda Popinski
+--> King Hippo
+--> Great Tiger
+--> Glass Joe
+Don Flamenco ... who also has enemies:
+--> Bald Bull

+ 23 - 0
test/MustacheExceptionTest.php

@@ -15,6 +15,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
+	 * @group interpolation
 	 * @expectedException MustacheException
 	 */
 	public function testThrowsUnknownVariableException() {
@@ -22,6 +23,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
+	 * @group sections
 	 * @expectedException MustacheException
 	 */
 	public function testThrowsUnclosedSectionException() {
@@ -29,6 +31,15 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
+	 * @group sections
+	 * @expectedException MustacheException
+	 */
+	public function testThrowsUnclosedInvertedSectionException() {
+		$this->pickyMustache->render('{{^unclosed}}');
+	}
+
+	/**
+	 * @group sections
 	 * @expectedException MustacheException
 	 */
 	public function testThrowsUnexpectedCloseSectionException() {
@@ -36,6 +47,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
+	 * @group partials
 	 * @expectedException MustacheException
 	 */
 	public function testThrowsUnknownPartialException() {
@@ -43,25 +55,36 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
+	 * @group pragmas
 	 * @expectedException MustacheException
 	 */
 	public function testThrowsUnknownPragmaException() {
 		$this->pickyMustache->render('{{%SWEET-MUSTACHE-BRO}}');
 	}
 
+	/**
+	 * @group sections
+	 */
 	public function testDoesntThrowUnclosedSectionException() {
 		$this->assertEquals('', $this->slackerMustache->render('{{#unclosed}}'));
 	}
 
+	/**
+	 * @group sections
+	 */
 	public function testDoesntThrowUnexpectedCloseSectionException() {
 		$this->assertEquals('', $this->slackerMustache->render('{{/unopened}}'));
 	}
 
+	/**
+	 * @group partials
+	 */
 	public function testDoesntThrowUnknownPartialException() {
 		$this->assertEquals('', $this->slackerMustache->render('{{>impartial}}'));
 	}
 
 	/**
+	 * @group pragmas
 	 * @expectedException MustacheException
 	 */
 	public function testGetPragmaOptionsThrowsExceptionsIfItThinksYouHaveAPragmaButItTurnsOutYouDont() {

+ 3 - 0
test/MustacheObjectSectionTest.php

@@ -2,6 +2,9 @@
 
 require_once '../Mustache.php';
 
+/**
+ * @group sections
+ */
 class MustacheObjectSectionTest extends PHPUnit_Framework_TestCase {
 	public function testBasicObject() {
 		$alpha = new Alpha();

+ 3 - 0
test/MustachePragmaDotNotationTest.php

@@ -2,6 +2,9 @@
 
 require_once '../Mustache.php';
 
+/**
+ * @group pragmas
+ */
 class MustachePragmaDotNotationTest extends PHPUnit_Framework_TestCase {
 
 	public function testDotTraversal() {

+ 33 - 0
test/MustachePragmaImplicitIteratorTest.php

@@ -2,6 +2,9 @@
 
 require_once '../Mustache.php';
 
+/**
+ * @group pragmas
+ */
 class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase {
 
 	public function testEnablePragma() {
@@ -47,4 +50,34 @@ class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase {
 		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR iterator=i}}{{%DOT-NOTATION}}{{#items}}{{i.name}}{{/items}}'));
 	}
 
+	/**
+	 * @dataProvider recursiveSectionData
+	 */
+	public function testRecursiveSections($template, $view, $result) {
+		$m = new Mustache();
+		$this->assertEquals($result, $m->render($template, $view));
+	}
+
+	public function recursiveSectionData() {
+		return array(
+			array(
+				'{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{.}}{{/.}}{{/items}}',
+				array('items' => array(array('a', 'b', 'c'), array('d', 'e', 'f'))),
+				'abcdef'
+			),
+			array(
+				'{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#.}}{{.}}{{/.}}{{/.}}{{/items}}',
+				array('items' => array(array(array('a', 'b'), array('c')), array(array('d'), array('e', 'f')))),
+				'abcdef'
+			),
+			array(
+				'{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#items}}{{.}}{{/items}}{{/.}}{{/items}}',
+				array('items' => array(
+					array('items' => array('a', 'b', 'c')),
+					array('items' => array('d', 'e', 'f')),
+				)),
+				'abcdef'
+			),
+		);
+	}
 }

+ 3 - 0
test/MustachePragmaTest.php

@@ -2,6 +2,9 @@
 
 require_once '../Mustache.php';
 
+/**
+ * @group pragmas
+ */
 class MustachePragmaTest extends PHPUnit_Framework_TestCase {
 
 	public function testUnknownPragmaException() {

+ 3 - 0
test/MustachePragmaUnescapedTest.php

@@ -2,6 +2,9 @@
 
 require_once '../Mustache.php';
 
+/**
+ * @group pragmas
+ */
 class MustachePragmaUnescapedTest extends PHPUnit_Framework_TestCase {
 
 	public function testPragmaUnescaped() {

+ 35 - 18
test/MustacheTest.php

@@ -132,8 +132,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	/**
 	 * Test render() with data.
 	 *
-	 * @access public
-	 * @return void
+	 * @group interpolation
 	 */
 	public function testRenderWithData() {
 		$m = new Mustache('{{first_name}} {{last_name}}');
@@ -141,6 +140,9 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 		$this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa')));
 	}
 
+	/**
+	 * @group partials
+	 */
 	public function testRenderWithPartials() {
 		$m = new Mustache('{{>stache}}', null, array('stache' => '{{first_name}} {{last_name}}'));
 		$this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin')));
@@ -150,8 +152,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	/**
 	 * Mustache should allow newlines (and other whitespace) in comments and all other tags.
 	 *
-	 * @access public
-	 * @return void
+	 * @group comments
 	 */
 	public function testNewlinesInComments() {
 		$m = new Mustache("{{! comment \n \t still a comment... }}");
@@ -160,9 +161,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 
 	/**
 	 * Mustache should return the same thing when invoked multiple times.
-	 *
-	 * @access public
-	 * @return void
 	 */
 	public function testMultipleInvocations() {
 		$m = new Mustache('x');
@@ -176,8 +174,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	/**
 	 * Mustache should return the same thing when invoked multiple times.
 	 *
-	 * @access public
-	 * @return void
+	 * @group interpolation
 	 */
 	public function testMultipleInvocationsWithTags() {
 		$m = new Mustache('{{one}} {{two}}', array('one' => 'foo', 'two' => 'bar'));
@@ -190,9 +187,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 
 	/**
 	 * Mustache should not use templates passed to the render() method for subsequent invocations.
-	 *
-	 * @access public
-	 * @return void
 	 */
 	public function testResetTemplateForMultipleInvocations() {
 		$m = new Mustache('Sirve.');
@@ -205,15 +199,14 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	}
 
 	/**
-	 * testClone function.
+	 * Test the __clone() magic function.
 	 *
 	 * @group examples
 	 * @dataProvider getExamples
-	 * @access public
+	 *
 	 * @param string $class
 	 * @param string $template
 	 * @param string $output
-	 * @return void
 	 */
 	public function test__clone($class, $template, $output) {
 		if (isset($this->knownIssues[$class])) {
@@ -240,11 +233,10 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	 *
 	 * @group examples
 	 * @dataProvider getExamples
-	 * @access public
+	 *
 	 * @param string $class
 	 * @param string $template
 	 * @param string $output
-	 * @return void
 	 */
 	public function testExamples($class, $template, $output) {
 		if (isset($this->knownIssues[$class])) {
@@ -266,7 +258,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 	 * This whole mess will be refined later to be more intuitive and less prescriptive, but it'll
 	 * do for now. Especially since it means we can have unit tests :)
 	 *
-	 * @access public
 	 * @return array
 	 */
 	public function getExamples() {
@@ -317,6 +308,9 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 		return $ret;
 	}
 
+	/**
+	 * @group delimiters
+	 */
 	public function testCrazyDelimiters() {
 		$m = new Mustache(null, array('result' => 'success'));
 		$this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]'));
@@ -327,10 +321,33 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 		$this->assertEquals('success', $m->render('{{=// \\\\}}// result \\\\'));
 	}
 
+	/**
+	 * @group delimiters
+	 */
 	public function testResetDelimiters() {
 		$m = new Mustache(null, array('result' => 'success'));
 		$this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]'));
 		$this->assertEquals('success', $m->render('{{=<< >>=}}<< result >>'));
 		$this->assertEquals('success', $m->render('{{=<% %>=}}<% result %>'));
 	}
+
+	/**
+	 * @group sections
+	 * @dataProvider poorlyNestedSections
+	 * @expectedException MustacheException
+	 */
+	public function testPoorlyNestedSections($template) {
+		$m = new Mustache($template);
+		$m->render();
+	}
+
+	public function poorlyNestedSections() {
+		return array(
+			array('{{#foo}}'),
+			array('{{#foo}}{{/bar}}'),
+			array('{{#foo}}{{#bar}}{{/foo}}'),
+			array('{{#foo}}{{#bar}}{{/foo}}{{/bar}}'),
+			array('{{#foo}}{{/bar}}{{/foo}}'),
+		);
+	}
 }