Bläddra i källkod

Merge branch 'feature/whitespace-overhaul' into dev

Justin Hileman 15 år sedan
förälder
incheckning
2e335f8687

+ 100 - 36
Mustache.php

@@ -14,9 +14,6 @@
  */
 class Mustache {
 
-	public $_otag = '{{';
-	public $_ctag = '}}';
-
 	/**
 	 * Should this Mustache throw exceptions when it finds unexpected tags?
 	 *
@@ -85,6 +82,15 @@ class Mustache {
 	 */
 	const PRAGMA_UNESCAPED    = 'UNESCAPED';
 
+	/**
+	 * Constants used for section and tag RegEx
+	 */
+	const SECTION_TYPES = '\^#\/';
+	const TAG_TYPES = '#\^\/=!<>\\{&';
+
+	public $_otag = '{{';
+	public $_ctag = '}}';
+
 	protected $_tagRegEx;
 
 	protected $_template = '';
@@ -258,6 +264,23 @@ class Mustache {
 		return $template;
 	}
 
+	/**
+	 * Prepare a section RegEx string for the given opening/closing tags.
+	 *
+	 * @access protected
+	 * @param string $otag
+	 * @param string $ctag
+	 * @return string
+	 */
+	protected function _prepareSectionRegEx($otag, $ctag) {
+		return sprintf(
+			'/(?:(?<=\\n)[ \\t]*)?%s(?<type>[%s])(?<tag_name>.+?)%s\\n?/s',
+			preg_quote($otag, '/'),
+			self::SECTION_TYPES,
+			preg_quote($ctag, '/')
+		);
+	}
+
 	/**
 	 * Extract a section from $template.
 	 *
@@ -268,9 +291,7 @@ class Mustache {
 	 * @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';
+		$regEx = $this->_prepareSectionRegEx($this->_otag, $this->_ctag);
 
 		$section_start = null;
 		$section_type  = null;
@@ -280,12 +301,12 @@ class Mustache {
 
 		$section_stack = array();
 		$matches = array();
-		while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE, $search_offset)) {
+		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]);
+			$type     = $matches['type'][0];
+			$tag_name = trim($matches['tag_name'][0]);
 
 			$search_offset = $offset + strlen($match);
 
@@ -323,6 +344,22 @@ class Mustache {
 		}
 	}
 
+	/**
+	 * Prepare a pragma RegEx for the given opening/closing tags.
+	 *
+	 * @access protected
+	 * @param string $otag
+	 * @param string $ctag
+	 * @return string
+	 */
+	protected function _preparePragmaRegEx($otag, $ctag) {
+		return sprintf(
+			'/%s%%\\s*(?<pragma_name>[\\w_-]+)(?<options_string>(?: [\\w]+=[\\w]+)*)\\s*%s\\n?/s',
+			preg_quote($otag, '/'),
+			preg_quote($ctag, '/')
+		);
+	}
+
 	/**
 	 * Initialize pragmas and remove all pragma tags.
 	 *
@@ -338,10 +375,8 @@ class Mustache {
 			return $template;
 		}
 
-		$otag = preg_quote($this->_otag, '/');
-		$ctag = preg_quote($this->_ctag, '/');
-		$regex = '/' . $otag . '%\\s*([\\w_-]+)((?: [\\w]+=[\\w]+)*)\\s*' . $ctag . '\\n?/s';
-		return preg_replace_callback($regex, array($this, '_renderPragma'), $template);
+		$regEx = $this->_preparePragmaRegEx($this->_otag, $this->_ctag);
+		return preg_replace_callback($regEx, array($this, '_renderPragma'), $template);
 	}
 
 	/**
@@ -354,8 +389,8 @@ class Mustache {
 	 */
 	protected function _renderPragma($matches) {
 		$pragma         = $matches[0];
-		$pragma_name    = $matches[1];
-		$options_string = $matches[2];
+		$pragma_name    = $matches['pragma_name'];
+		$options_string = $matches['options_string'];
 
 		if (!in_array($pragma_name, $this->_pragmasImplemented)) {
 			throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
@@ -364,7 +399,7 @@ class Mustache {
 		$options = array();
 		foreach (explode(' ', trim($options_string)) as $o) {
 			if ($p = trim($o)) {
-				$p = explode('=', trim($p));
+				$p = explode('=', $p);
 				$options[$p[0]] = $p[1];
 			}
 		}
@@ -422,6 +457,23 @@ class Mustache {
 		return (isset($this->_throwsExceptions[$exception]) && $this->_throwsExceptions[$exception]);
 	}
 
+	/**
+	 * Prepare a tag RegEx for the given opening/closing tags.
+	 *
+	 * @access protected
+	 * @param string $otag
+	 * @param string $ctag
+	 * @return string
+	 */
+	protected function _prepareTagRegEx($otag, $ctag) {
+		return sprintf(
+			'/(?<whitespace>(?<=\\n)[ \\t]*)?%s(?<type>[%s]?)(?<tag_name>.+?)(?:\\2|})?%s(?:\\s*(?=\\n))?/s',
+			preg_quote($otag, '/'),
+			self::TAG_TYPES,
+			preg_quote($ctag, '/')
+		);
+	}
+
 	/**
 	 * Loop through and render individual Mustache tags.
 	 *
@@ -437,22 +489,31 @@ class Mustache {
 		$otag_orig = $this->_otag;
 		$ctag_orig = $this->_ctag;
 
-		$otag = preg_quote($this->_otag, '/');
-		$ctag = preg_quote($this->_ctag, '/');
-
-		$this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s";
+		$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);
 
 		$html = '';
 		$matches = array();
 		while (preg_match($this->_tagRegEx, $template, $matches, PREG_OFFSET_CAPTURE)) {
 			$tag      = $matches[0][0];
 			$offset   = $matches[0][1];
-			$modifier = $matches[1][0];
-			$tag_name = trim($matches[2][0]);
+			$modifier = $matches['type'][0];
+			$tag_name = trim($matches['tag_name'][0]);
+
+			if (isset($matches['whitespace']) && $matches['whitespace'][1] > -1) {
+				$whitespace = $matches['whitespace'][0];
+			} else {
+				$whitespace = null;
+			}
 
 			$html .= substr($template, 0, $offset);
-			$html .= $this->_renderTag($modifier, $tag_name);
-			$template = substr($template, $offset + strlen($tag));
+
+			$next_offset = $offset + strlen($tag);
+			if ((substr($html, -1) == "\n") && (substr($template, $next_offset, 1) == "\n")) {
+				$next_offset++;
+			}
+			$template = substr($template, $next_offset);
+
+			$html .= $this->_renderTag($modifier, $tag_name, $whitespace);
 		}
 
 		$this->_otag = $otag_orig;
@@ -473,7 +534,7 @@ class Mustache {
 	 * @throws MustacheException Unmatched section tag encountered.
 	 * @return string
 	 */
-	protected function _renderTag($modifier, $tag_name) {
+	protected function _renderTag($modifier, $tag_name, $whitespace) {
 		switch ($modifier) {
 			case '=':
 				return $this->_changeDelimiter($tag_name);
@@ -483,9 +544,13 @@ class Mustache {
 				break;
 			case '>':
 			case '<':
-				return $this->_renderPartial($tag_name);
+				return $this->_renderPartial($tag_name, $whitespace);
 				break;
 			case '{':
+				// strip the trailing } ...
+				if ($tag_name[(strlen($tag_name) - 1)] == '}') {
+					$tag_name = substr($tag_name, 0, -1);
+				}
 			case '&':
 				if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) {
 					return $this->_renderEscaped($tag_name);
@@ -548,9 +613,10 @@ class Mustache {
 	 * @param string $tag_name
 	 * @return string
 	 */
-	protected function _renderPartial($tag_name) {
+	protected function _renderPartial($tag_name, $whitespace = '') {
 		$view = clone($this);
-		return $view->render($this->_getPartial($tag_name));
+
+		return $whitespace . preg_replace('/\n(?!$)/s', "\n" . $whitespace, $view->render($this->_getPartial($tag_name)));
 	}
 
 	/**
@@ -562,13 +628,12 @@ class Mustache {
 	 * @return string
 	 */
 	protected function _changeDelimiter($tag_name) {
-		$tags = explode(' ', $tag_name);
-		$this->_otag = $tags[0];
-		$this->_ctag = $tags[1];
+		list($otag, $ctag) = explode(' ', $tag_name);
+		$this->_otag = $otag;
+		$this->_ctag = $ctag;
+
+		$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);
 
-		$otag  = preg_quote($this->_otag, '/');
-		$ctag  = preg_quote($this->_ctag, '/');
-		$this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s";
 		return '';
 	}
 
@@ -588,7 +653,6 @@ class Mustache {
 		$this->_context = $new;
 	}
 
-
 	/**
 	 * Remove the latest context from the stack.
 	 *
@@ -621,7 +685,7 @@ class Mustache {
 	 * @return string
 	 */
 	protected function _getVariable($tag_name) {
-		if ($this->_hasPragma(self::PRAGMA_DOT_NOTATION) && $tag_name != '.') {
+		if ($tag_name != '.' && strpos($tag_name, '.') !== false && $this->_hasPragma(self::PRAGMA_DOT_NOTATION)) {
 			$chunks = explode('.', $tag_name);
 			$first = array_shift($chunks);
 

+ 0 - 1
README.markdown

@@ -83,7 +83,6 @@ Known Issues
 
  * Sections don't respect delimiter changes -- `delimiters` example currently fails with an
    "unclosed section" exception.
- * Mustache isn't always very good at whitespace.
 
 
 See Also

+ 11 - 11
examples/complex/complex.mustache

@@ -1,16 +1,16 @@
 <h1>{{header}}</h1>
 {{#notEmpty}}
-  <ul>
-  {{#item}}
-    {{#current}}
-      <li><strong>{{name}}</strong></li>
-    {{/current}}
-    {{^current}}
-      <li><a href="{{url}}">{{name}}</a></li>
-    {{/current}}
-  {{/item}}
-  </ul>
+<ul>
+{{#item}}
+{{#current}}
+    <li><strong>{{name}}</strong></li>
+{{/current}}
+{{^current}}
+    <li><a href="{{url}}">{{name}}</a></li>
+{{/current}}
+{{/item}}
+</ul>
 {{/notEmpty}}
 {{#isEmpty}}
-  <p>The list is empty.</p>
+<p>The list is empty.</p>
 {{/isEmpty}}

+ 2 - 2
examples/complex/complex.txt

@@ -1,6 +1,6 @@
 <h1>Colors</h1>
 <ul>
-  <li><strong>red</strong></li>
+    <li><strong>red</strong></li>
     <li><a href="#Green">green</a></li>
     <li><a href="#Blue">blue</a></li>
-    </ul>
+</ul>

+ 2 - 2
examples/double_section/double_section.mustache

@@ -1,7 +1,7 @@
 {{#t}}
-  * first
+* first
 {{/t}}
 * {{two}}
 {{#t}}
-  * third
+* third
 {{/t}}

+ 4 - 7
examples/grand_parent_context/grand_parent_context.mustache

@@ -1,10 +1,7 @@
 {{grand_parent_id}}
 {{#parent_contexts}}
-{{grand_parent_id}}
-{{parent_id}}
-{{#child_contexts}}
-{{grand_parent_id}}
-{{parent_id}}
-{{child_id}}
-{{/child_contexts}}
+  {{parent_id}} ({{grand_parent_id}})
+  {{#child_contexts}}
+    {{child_id}} ({{parent_id}} << {{grand_parent_id}})
+  {{/child_contexts}}
 {{/parent_contexts}}

+ 6 - 16
examples/grand_parent_context/grand_parent_context.txt

@@ -1,17 +1,7 @@
 grand_parent1
-grand_parent1
-parent1
-grand_parent1
-parent1
-parent1-child1
-grand_parent1
-parent1
-parent1-child2
-grand_parent1
-parent2
-grand_parent1
-parent2
-parent2-child1
-grand_parent1
-parent2
-parent2-child2
+  parent1 (grand_parent1)
+    parent1-child1 (parent1 << grand_parent1)
+    parent1-child2 (parent1 << grand_parent1)
+  parent2 (grand_parent1)
+    parent2-child1 (parent2 << grand_parent1)
+    parent2-child2 (parent2 << grand_parent1)

+ 2 - 2
examples/inverted_double_section/inverted_double_section.mustache

@@ -1,7 +1,7 @@
 {{^t}}
-  * first
+* first
 {{/t}}
 * {{two}}
 {{^t}}
-  * third
+* third
 {{/t}}

+ 0 - 14
examples/sections_spaces/SectionsSpaces.php

@@ -1,14 +0,0 @@
-<?php
-
-class SectionsSpaces extends Mustache {
-	public $start = "It worked the first time.";
-
-	public function middle() {
-		return array(
-			array('item' => "And it worked the second time."),
-			array('item' => "As well as the third."),
-		);
-	}
-
-	public $final = "Then, surprisingly, it worked the final time.";
-}

+ 0 - 9
examples/sections_spaces/sections_spaces.mustache

@@ -1,9 +0,0 @@
- * {{ start }}
-{{# middle }}
- * {{ item }}
-{{/ middle }}
- * {{ final }}
-
- * {{ start }}
-{{# middle }} * {{ item }}{{/ middle }}
- * {{ final }}

+ 0 - 9
examples/sections_spaces/sections_spaces.txt

@@ -1,9 +0,0 @@
- * It worked the first time.
- * And it worked the second time.
- * As well as the third.
- * Then, surprisingly, it worked the final time.
-
- * It worked the first time.
- * And it worked the second time.
- * As well as the third.
- * Then, surprisingly, it worked the final time.

+ 5 - 1
test/MustachePragmaImplicitIteratorTest.php

@@ -11,7 +11,11 @@ class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase {
 		$m = $this->getMock('Mustache', array('_renderPragma'), array('{{%IMPLICIT-ITERATOR}}'));
 		$m->expects($this->exactly(1))
 			->method('_renderPragma')
-			->with(array('{{%IMPLICIT-ITERATOR}}', 'IMPLICIT-ITERATOR', null));
+			->with(array(
+				0 => '{{%IMPLICIT-ITERATOR}}',
+				1 => 'IMPLICIT-ITERATOR', 'pragma_name' => 'IMPLICIT-ITERATOR',
+				2 => null, 'options_string' => null
+			));
 		$m->render();
 	}
 

+ 0 - 1
test/MustacheTest.php

@@ -37,7 +37,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 
 	protected $knownIssues = array(
 		'Delimiters'     => "Known issue: sections don't respect delimiter changes",
-		'SectionsSpaces' => "Known issue: Mustache fails miserably at whitespace",
 	);
 
 	/**