Browse Source

Merge branch 'release/2.0.0'

Conflicts:
	.gitmodules
	Mustache.php
	test/fixtures/examples/i18n/I18n.php
	vendor/spec
Justin Hileman 13 years ago
parent
commit
af9ad63c23
100 changed files with 4478 additions and 2762 deletions
  1. 2 0
      .gitignore
  2. 5 2
      .gitmodules
  3. 5 0
      .travis.yml
  4. 0 931
      Mustache.php
  5. 0 85
      MustacheLoader.php
  6. 11 46
      README.markdown
  7. 35 36
      bin/create_example.php
  8. 21 0
      composer.json
  9. 0 13
      examples/child_context/ChildContext.php
  10. 0 7
      examples/comments/Comments.php
  11. 0 19
      examples/complex/complex.php
  12. 0 14
      examples/delimiters/Delimiters.php
  13. 0 20
      examples/dot_notation/DotNotation.php
  14. 0 9
      examples/double_section/DoubleSection.php
  15. 0 5
      examples/escaped/Escaped.php
  16. 0 24
      examples/grand_parent_context/GrandParentContext.php
  17. 0 5
      examples/implicit_iterator/ImplicitIterator.php
  18. 0 6
      examples/inverted_double_section/InvertedDoubleSection.php
  19. 0 5
      examples/inverted_section/InvertedSection.php
  20. 0 2
      examples/inverted_section/inverted_section.mustache
  21. 0 1
      examples/inverted_section/inverted_section.txt
  22. 0 13
      examples/partials/Partials.php
  23. 0 2
      examples/partials/partials.mustache
  24. 0 3
      examples/partials/partials.txt
  25. 0 19
      examples/partials_with_view_class/PartialsWithViewClass.php
  26. 0 2
      examples/partials_with_view_class/partials_with_view_class.mustache
  27. 0 3
      examples/partials_with_view_class/partials_with_view_class.txt
  28. 0 5
      examples/pragma_unescaped/PragmaUnescaped.php
  29. 0 3
      examples/pragma_unescaped/pragma_unescaped.mustache
  30. 0 2
      examples/pragma_unescaped/pragma_unescaped.txt
  31. 0 8
      examples/pragmas_in_partials/PragmasInPartials.php
  32. 0 3
      examples/pragmas_in_partials/pragmas_in_partials.mustache
  33. 0 2
      examples/pragmas_in_partials/pragmas_in_partials.txt
  34. 0 16
      examples/recursive_partials/RecursivePartials.php
  35. 0 16
      examples/section_iterator_objects/SectionIteratorObjects.php
  36. 0 26
      examples/section_magic_objects/SectionMagicObjects.php
  37. 0 16
      examples/section_objects/SectionObjects.php
  38. 0 14
      examples/sections/Sections.php
  39. 0 33
      examples/sections_nested/SectionsNested.php
  40. 0 12
      examples/simple/Simple.php
  41. 0 5
      examples/unescaped/Unescaped.php
  42. 0 5
      examples/utf8/UTF8.php
  43. 0 5
      examples/utf8_unescaped/UTF8Unescaped.php
  44. 0 37
      examples/whitespace/Whitespace.php
  45. 17 0
      phpunit.xml.dist
  46. 69 0
      src/Mustache/Autoloader.php
  47. 386 0
      src/Mustache/Compiler.php
  48. 149 0
      src/Mustache/Context.php
  49. 589 0
      src/Mustache/Engine.php
  50. 168 0
      src/Mustache/HelperCollection.php
  51. 26 0
      src/Mustache/Loader.php
  52. 79 0
      src/Mustache/Loader/ArrayLoader.php
  53. 118 0
      src/Mustache/Loader/FilesystemLoader.php
  54. 32 0
      src/Mustache/Loader/MutableLoader.php
  55. 42 0
      src/Mustache/Loader/StringLoader.php
  56. 88 0
      src/Mustache/Parser.php
  57. 149 0
      src/Mustache/Template.php
  58. 286 0
      src/Mustache/Tokenizer.php
  59. 36 0
      test/Mustache/Test/AutoloaderTest.php
  60. 103 0
      test/Mustache/Test/CompilerTest.php
  61. 119 0
      test/Mustache/Test/ContextTest.php
  62. 256 0
      test/Mustache/Test/EngineTest.php
  63. 71 0
      test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php
  64. 114 0
      test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php
  65. 40 0
      test/Mustache/Test/Functional/CallTest.php
  66. 142 0
      test/Mustache/Test/Functional/ExamplesTest.php
  67. 106 0
      test/Mustache/Test/Functional/HigherOrderSectionsTest.php
  68. 152 0
      test/Mustache/Test/Functional/MustacheInjectionTest.php
  69. 175 0
      test/Mustache/Test/Functional/MustacheSpecTest.php
  70. 110 0
      test/Mustache/Test/Functional/ObjectSectionTest.php
  71. 163 0
      test/Mustache/Test/HelperCollectionTest.php
  72. 52 0
      test/Mustache/Test/Loader/ArrayLoaderTest.php
  73. 51 0
      test/Mustache/Test/Loader/FilesystemLoaderTest.php
  74. 25 0
      test/Mustache/Test/Loader/StringLoaderTest.php
  75. 182 0
      test/Mustache/Test/ParserTest.php
  76. 55 0
      test/Mustache/Test/TemplateTest.php
  77. 144 0
      test/Mustache/Test/TokenizerTest.php
  78. 0 24
      test/MustacheCallTest.php
  79. 0 152
      test/MustacheExceptionTest.php
  80. 0 114
      test/MustacheHigherOrderSectionsTest.php
  81. 0 127
      test/MustacheInjectionTest.php
  82. 0 60
      test/MustacheLoaderTest.php
  83. 0 73
      test/MustacheObjectSectionTest.php
  84. 0 74
      test/MustachePragmaTest.php
  85. 0 19
      test/MustachePragmaUnescapedTest.php
  86. 0 174
      test/MustacheSpecTest.php
  87. 0 464
      test/MustacheTest.php
  88. 15 0
      test/bootstrap.php
  89. 15 0
      test/fixtures/autoloader/Mustache/Bar.php
  90. 15 0
      test/fixtures/autoloader/Mustache/Foo.php
  91. 15 0
      test/fixtures/autoloader/NonMustacheClass.php
  92. 0 1
      test/fixtures/bar.mustache
  93. 14 0
      test/fixtures/examples/child_context/ChildContext.php
  94. 0 0
      test/fixtures/examples/child_context/child_context.mustache
  95. 0 0
      test/fixtures/examples/child_context/child_context.txt
  96. 9 0
      test/fixtures/examples/comments/Comments.php
  97. 0 0
      test/fixtures/examples/comments/comments.mustache
  98. 0 0
      test/fixtures/examples/comments/comments.txt
  99. 0 0
      test/fixtures/examples/complex/complex.mustache
  100. 22 0
      test/fixtures/examples/complex/complex.php

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+composer.lock
+vendor

+ 5 - 2
.gitmodules

@@ -1,3 +1,6 @@
-[submodule "test/spec"]
-	path = test/spec
+[submodule "vendor/spec"]
+	path = vendor/spec
 	url = git://github.com/mustache/spec.git
 	url = git://github.com/mustache/spec.git
+[submodule "vendor/yaml"]
+	path = vendor/yaml
+	url = git://github.com/fabpot/yaml.git

+ 5 - 0
.travis.yml

@@ -0,0 +1,5 @@
+language: php
+php:
+  - 5.2
+  - 5.3
+  - 5.4

+ 0 - 931
Mustache.php

@@ -1,931 +0,0 @@
-<?php
-
-/**
- * A Mustache implementation in PHP.
- *
- * {@link http://defunkt.github.com/mustache}
- *
- * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
- * logic from template files. In fact, it is not even possible to embed logic in the template.
- *
- * This is very, very rad.
- *
- * @author Justin Hileman {@link http://justinhileman.com}
- */
-class Mustache {
-
-	const VERSION      = '1.1.0';
-	const SPEC_VERSION = '1.1.2';
-
-	/**
-	 * Should this Mustache throw exceptions when it finds unexpected tags?
-	 *
-	 * @see self::_throwsException()
-	 */
-	protected $_throwsExceptions = array(
-		MustacheException::UNKNOWN_VARIABLE         => false,
-		MustacheException::UNCLOSED_SECTION         => true,
-		MustacheException::UNEXPECTED_CLOSE_SECTION => true,
-		MustacheException::UNKNOWN_PARTIAL          => false,
-		MustacheException::UNKNOWN_PRAGMA           => true,
-	);
-
-	// Override the escaper function. Defaults to `htmlspecialchars`.
-	protected $_escape;
-
-	// Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8.
-	protected $_charset = 'UTF-8';
-
-	/**
-	 * Pragmas are macro-like directives that, when invoked, change the behavior or
-	 * syntax of Mustache.
-	 *
-	 * They should be considered extremely experimental. Most likely their implementation
-	 * will change in the future.
-	 */
-
-	/**
-	 * The {{%UNESCAPED}} pragma swaps the meaning of the {{normal}} and {{{unescaped}}}
-	 * Mustache tags. That is, once this pragma is activated the {{normal}} tag will not be
-	 * escaped while the {{{unescaped}}} tag will be escaped.
-	 *
-	 * Pragmas apply only to the current template. Partials, even those included after the
-	 * {{%UNESCAPED}} call, will need their own pragma declaration.
-	 *
-	 * This may be useful in non-HTML Mustache situations.
-	 */
-	const PRAGMA_UNESCAPED    = 'UNESCAPED';
-
-	/**
-	 * Constants used for section and tag RegEx
-	 */
-	const SECTION_TYPES = '\^#\/';
-	const TAG_TYPES = '#\^\/=!<>\\{&';
-
-	protected $_otag = '{{';
-	protected $_ctag = '}}';
-
-	protected $_tagRegEx;
-
-	protected $_template = '';
-	protected $_context  = array();
-	protected $_partials = array();
-	protected $_pragmas  = array();
-
-	protected $_pragmasImplemented = array(
-		self::PRAGMA_UNESCAPED
-	);
-
-	protected $_localPragmas = array();
-
-	/**
-	 * Mustache class constructor.
-	 *
-	 * This method accepts a $template string and a $view object. Optionally, pass an associative
-	 * array of partials as well.
-	 *
-	 * Passing an $options array allows overriding certain Mustache options during instantiation:
-	 *
-	 *     $options = array(
-	 *         // `escape` -- custom escaper callback; must be callable.
-	 *         'escape' => function($text) {
-	 *             return htmlspecialchars($text, ENT_COMPAT, 'UTF-8');
-	 *         },
-	 *
-	 *         // `charset` -- must be supported by `htmlspecialentities()`. defaults to 'UTF-8'
-	 *         'charset' => 'ISO-8859-1',
-	 *
-	 *         // opening and closing delimiters, as an array or a space-separated string
-	 *         'delimiters' => '<% %>',
-	 *
-	 *         // an array of pragmas to enable/disable
-	 *         'pragmas' => array(
-	 *             Mustache::PRAGMA_UNESCAPED => true
-	 *         ),
-	 *
-	 *         // an array of thrown exceptions to enable/disable
-	 *         'throws_exceptions' => array(
-	 *             MustacheException::UNKNOWN_VARIABLE         => false,
-	 *             MustacheException::UNCLOSED_SECTION         => true,
-	 *             MustacheException::UNEXPECTED_CLOSE_SECTION => true,
-	 *             MustacheException::UNKNOWN_PARTIAL          => false,
-	 *             MustacheException::UNKNOWN_PRAGMA           => true,
-	 *         ),
-	 *     );
-	 *
-	 * @access public
-	 * @param string $template (default: null)
-	 * @param mixed $view (default: null)
-	 * @param array $partials (default: null)
-	 * @param array $options (default: array())
-	 * @return void
-	 */
-	public function __construct($template = null, $view = null, $partials = null, array $options = null) {
-		if ($template !== null) $this->_template = $template;
-		if ($partials !== null) $this->_partials = $partials;
-		if ($view !== null)     $this->_context = array($view);
-		if ($options !== null)  $this->_setOptions($options);
-	}
-
-	/**
-	 * Helper function for setting options from constructor args.
-	 *
-	 * @access protected
-	 * @param array $options
-	 * @return void
-	 */
-	protected function _setOptions(array $options) {
-		if (isset($options['escape'])) {
-			if (!is_callable($options['escape'])) {
-				throw new InvalidArgumentException('Mustache constructor "escape" option must be callable');
-			}
-			$this->_escape = $options['escape'];
-		}
-
-		if (isset($options['charset'])) {
-			$this->_charset = $options['charset'];
-		}
-
-		if (isset($options['delimiters'])) {
-			$delims = $options['delimiters'];
-			if (!is_array($delims)) {
-				$delims = array_map('trim', explode(' ', $delims, 2));
-			}
-			$this->_otag = $delims[0];
-			$this->_ctag = $delims[1];
-		}
-
-		if (isset($options['pragmas'])) {
-			foreach ($options['pragmas'] as $pragma_name => $pragma_value) {
-				if (!in_array($pragma_name, $this->_pragmasImplemented, true)) {
-					throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
-				}
-			}
-			$this->_pragmas = $options['pragmas'];
-		}
-
-		if (isset($options['throws_exceptions'])) {
-			foreach ($options['throws_exceptions'] as $exception => $value) {
-				$this->_throwsExceptions[$exception] = $value;
-			}
-		}
-	}
-
-	/**
-	 * Mustache class clone method.
-	 *
-	 * A cloned Mustache instance should have pragmas, delimeters and root context
-	 * reset to default values.
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function __clone() {
-		$this->_otag = '{{';
-		$this->_ctag = '}}';
-		$this->_localPragmas = array();
-
-		if ($keys = array_keys($this->_context)) {
-			$last = array_pop($keys);
-			if ($this->_context[$last] instanceof Mustache) {
-				$this->_context[$last] =& $this;
-			}
-		}
-	}
-
-	/**
-	 * Render the given template and view object.
-	 *
-	 * Defaults to the template and view passed to the class constructor unless a new one is provided.
-	 * Optionally, pass an associative array of partials as well.
-	 *
-	 * @access public
-	 * @param string $template (default: null)
-	 * @param mixed $view (default: null)
-	 * @param array $partials (default: null)
-	 * @return string Rendered Mustache template.
-	 */
-	public function render($template = null, $view = null, $partials = null) {
-		if ($template === null) $template = $this->_template;
-		if ($partials !== null) $this->_partials = $partials;
-
-		$otag_orig = $this->_otag;
-		$ctag_orig = $this->_ctag;
-
-		if ($view) {
-			$this->_context = array($view);
-		} else if (empty($this->_context)) {
-			$this->_context = array($this);
-		}
-
-		$template = $this->_renderPragmas($template);
-		$template = $this->_renderTemplate($template);
-
-		$this->_otag = $otag_orig;
-		$this->_ctag = $ctag_orig;
-
-		return $template;
-	}
-
-	/**
-	 * Wrap the render() function for string conversion.
-	 *
-	 * @access public
-	 * @return string
-	 */
-	public function __toString() {
-		// PHP doesn't like exceptions in __toString.
-		// catch any exceptions and convert them to strings.
-		try {
-			$result = $this->render();
-			return $result;
-		} catch (Exception $e) {
-			return "Error rendering mustache: " . $e->getMessage();
-		}
-	}
-
-	/**
-	 * Internal render function, used for recursive calls.
-	 *
-	 * @access protected
-	 * @param string $template
-	 * @return string Rendered Mustache template.
-	 */
-	protected function _renderTemplate($template) {
-		if ($section = $this->_findSection($template)) {
-			list($before, $type, $tag_name, $content, $after) = $section;
-
-			$rendered_before = $this->_renderTags($before);
-
-			$rendered_content = '';
-			$val = $this->_getVariable($tag_name);
-			switch($type) {
-				// inverted section
-				case '^':
-					if (empty($val)) {
-						$rendered_content = $this->_renderTemplate($content);
-					}
-					break;
-
-				// regular section
-				case '#':
-					// higher order sections
-					if ($this->_varIsCallable($val)) {
-						$rendered_content = $this->_renderTemplate(call_user_func($val, $content));
-					} else if ($this->_varIsIterable($val)) {
-						foreach ($val as $local_context) {
-							$this->_pushContext($local_context);
-							$rendered_content .= $this->_renderTemplate($content);
-							$this->_popContext();
-						}
-					} else if ($val) {
-						if (is_array($val) || is_object($val)) {
-							$this->_pushContext($val);
-							$rendered_content = $this->_renderTemplate($content);
-							$this->_popContext();
-						} else {
-							$rendered_content = $this->_renderTemplate($content);
-						}
-					}
-					break;
-			}
-
-			return $rendered_before . $rendered_content . $this->_renderTemplate($after);
-		}
-
-		return $this->_renderTags($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(?:(?P<type>[%s])(?P<tag_name>.+?)|=(?P<delims>.*?)=)%s\\n?/s',
-			preg_quote($otag, '/'),
-			self::SECTION_TYPES,
-			preg_quote($ctag, '/')
-		);
-	}
-
-	/**
-	 * Extract the first section from $template.
-	 *
-	 * @access protected
-	 * @param string $template
-	 * @return array $before, $type, $tag_name, $content and $after
-	 */
-	protected function _findSection($template) {
-		$regEx = $this->_prepareSectionRegEx($this->_otag, $this->_ctag);
-
-		$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)) {
-			if (isset($matches['delims'][0])) {
-				list($otag, $ctag) = explode(' ', $matches['delims'][0]);
-				$regEx = $this->_prepareSectionRegEx($otag, $ctag);
-				$search_offset = $matches[0][1] + strlen($matches[0][0]);
-				continue;
-			}
-
-			$match    = $matches[0][0];
-			$offset   = $matches[0][1];
-			$type     = $matches['type'][0];
-			$tag_name = trim($matches['tag_name'][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)) {
-						// $before, $type, $tag_name, $content, $after
-						return array(
-							substr($template, 0, $section_start),
-							$section_type,
-							$tag_name,
-							substr($template, $content_start, $offset - $content_start),
-							substr($template, $search_offset),
-						);
-					}
-					break;
-			}
-		}
-
-		if (!empty($section_stack)) {
-			if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) {
-				throw new MustacheException('Unclosed section: ' . $section_stack[0], MustacheException::UNCLOSED_SECTION);
-			}
-		}
-	}
-
-	/**
-	 * 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*(?P<pragma_name>[\\w_-]+)(?P<options_string>(?: [\\w]+=[\\w]+)*)\\s*%s\\n?/s',
-			preg_quote($otag, '/'),
-			preg_quote($ctag, '/')
-		);
-	}
-
-	/**
-	 * Initialize pragmas and remove all pragma tags.
-	 *
-	 * @access protected
-	 * @param string $template
-	 * @return string
-	 */
-	protected function _renderPragmas($template) {
-		$this->_localPragmas = $this->_pragmas;
-
-		// no pragmas
-		if (strpos($template, $this->_otag . '%') === false) {
-			return $template;
-		}
-
-		$regEx = $this->_preparePragmaRegEx($this->_otag, $this->_ctag);
-		return preg_replace_callback($regEx, array($this, '_renderPragma'), $template);
-	}
-
-	/**
-	 * A preg_replace helper to remove {{%PRAGMA}} tags and enable requested pragma.
-	 *
-	 * @access protected
-	 * @param mixed $matches
-	 * @return void
-	 * @throws MustacheException unknown pragma
-	 */
-	protected function _renderPragma($matches) {
-		$pragma         = $matches[0];
-		$pragma_name    = $matches['pragma_name'];
-		$options_string = $matches['options_string'];
-
-		if (!in_array($pragma_name, $this->_pragmasImplemented)) {
-			if ($this->_throwsException(MustacheException::UNKNOWN_PRAGMA)) {
-				throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
-			} else {
-				return '';
-			}
-		}
-
-		$options = array();
-		foreach (explode(' ', trim($options_string)) as $o) {
-			if ($p = trim($o)) {
-				$p = explode('=', $p);
-				$options[$p[0]] = $p[1];
-			}
-		}
-
-		if (empty($options)) {
-			$this->_localPragmas[$pragma_name] = true;
-		} else {
-			$this->_localPragmas[$pragma_name] = $options;
-		}
-
-		return '';
-	}
-
-	/**
-	 * Check whether this Mustache has a specific pragma.
-	 *
-	 * @access protected
-	 * @param string $pragma_name
-	 * @return bool
-	 */
-	protected function _hasPragma($pragma_name) {
-		if (array_key_exists($pragma_name, $this->_localPragmas) && $this->_localPragmas[$pragma_name]) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-	/**
-	 * Return pragma options, if any.
-	 *
-	 * @access protected
-	 * @param string $pragma_name
-	 * @return mixed
-	 * @throws MustacheException Unknown pragma
-	 */
-	protected function _getPragmaOptions($pragma_name) {
-		if (!$this->_hasPragma($pragma_name)) {
-			if ($this->_throwsException(MustacheException::UNKNOWN_PRAGMA)) {
-				throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
-			}
-		}
-
-		return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array();
-	}
-
-	/**
-	 * Check whether this Mustache instance throws a given exception.
-	 *
-	 * Expects exceptions to be MustacheException error codes (i.e. class constants).
-	 *
-	 * @access protected
-	 * @param mixed $exception
-	 * @return void
-	 */
-	protected function _throwsException($exception) {
-		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, $first = false) {
-		return sprintf(
-			'/(?P<leading>(?:%s\\r?\\n)[ \\t]*)?%s(?P<type>[%s]?)(?P<tag_name>.+?)(?:\\2|})?%s(?P<trailing>\\s*(?:\\r?\\n|\\Z))?/s',
-			($first ? '\\A|' : ''),
-			preg_quote($otag, '/'),
-			self::TAG_TYPES,
-			preg_quote($ctag, '/')
-		);
-	}
-
-	/**
-	 * Loop through and render individual Mustache tags.
-	 *
-	 * @access protected
-	 * @param string $template
-	 * @return void
-	 */
-	protected function _renderTags($template) {
-		if (strpos($template, $this->_otag) === false) {
-			return $template;
-		}
-
-		$first = true;
-		$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag, true);
-
-		$html = '';
-		$matches = array();
-		while (preg_match($this->_tagRegEx, $template, $matches, PREG_OFFSET_CAPTURE)) {
-			$tag      = $matches[0][0];
-			$offset   = $matches[0][1];
-			$modifier = $matches['type'][0];
-			$tag_name = trim($matches['tag_name'][0]);
-
-			if (isset($matches['leading']) && $matches['leading'][1] > -1) {
-				$leading = $matches['leading'][0];
-			} else {
-				$leading = null;
-			}
-
-			if (isset($matches['trailing']) && $matches['trailing'][1] > -1) {
-				$trailing = $matches['trailing'][0];
-			} else {
-				$trailing = null;
-			}
-
-			$html .= substr($template, 0, $offset);
-
-			$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, $leading, $trailing);
-
-			if ($first == true) {
-				$first = false;
-				$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);
-			}
-		}
-
-		return $html . $template;
-	}
-
-	/**
-	 * Render the named tag, given the specified modifier.
-	 *
-	 * Accepted modifiers are `=` (change delimiter), `!` (comment), `>` (partial)
-	 * `{` or `&` (don't escape output), or none (render escaped output).
-	 *
-	 * @access protected
-	 * @param string $modifier
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @throws MustacheException Unmatched section tag encountered.
-	 * @return string
-	 */
-	protected function _renderTag($modifier, $tag_name, $leading, $trailing) {
-		switch ($modifier) {
-			case '=':
-				return $this->_changeDelimiter($tag_name, $leading, $trailing);
-				break;
-			case '!':
-				return $this->_renderComment($tag_name, $leading, $trailing);
-				break;
-			case '>':
-			case '<':
-				return $this->_renderPartial($tag_name, $leading, $trailing);
-				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, $leading, $trailing);
-				} else {
-					return $this->_renderUnescaped($tag_name, $leading, $trailing);
-				}
-				break;
-			case '#':
-			case '^':
-			case '/':
-				// remove any leftover section tags
-				return $leading . $trailing;
-				break;
-			default:
-				if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) {
-					return $this->_renderUnescaped($modifier . $tag_name, $leading, $trailing);
-				} else {
-					return $this->_renderEscaped($modifier . $tag_name, $leading, $trailing);
-				}
-				break;
-		}
-	}
-
-	/**
-	 * Returns true if any of its args contains the "\r" character.
-	 *
-	 * @access protected
-	 * @param string $str
-	 * @return boolean
-	 */
-	protected function _stringHasR($str) {
-		foreach (func_get_args() as $arg) {
-			if (strpos($arg, "\r") !== false) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	/**
-	 * Escape and return the requested tag.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @return string
-	 */
-	protected function _renderEscaped($tag_name, $leading, $trailing) {
-		$value = $this->_renderUnescaped($tag_name, '', '');
-		if (isset($this->_escape)) {
-			$rendered = call_user_func($this->_escape, $value);
-		} else {
-			$rendered = htmlentities($value, ENT_COMPAT, $this->_charset);
-		}
-
-		return $leading . $rendered . $trailing;
-	}
-
-	/**
-	 * Render a comment (i.e. return an empty string).
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @return string
-	 */
-	protected function _renderComment($tag_name, $leading, $trailing) {
-		if ($leading !== null && $trailing !== null) {
-			if (strpos($leading, "\n") === false) {
-				return '';
-			}
-			return $this->_stringHasR($leading, $trailing) ? "\r\n" : "\n";
-		}
-		return $leading . $trailing;
-	}
-
-	/**
-	 * Return the requested tag unescaped.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @return string
-	 */
-	protected function _renderUnescaped($tag_name, $leading, $trailing) {
-		$val = $this->_getVariable($tag_name);
-
-		if ($this->_varIsCallable($val)) {
-			$val = $this->_renderTemplate(call_user_func($val));
-		}
-
-		return $leading . $val . $trailing;
-	}
-
-	/**
-	 * Render the requested partial.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @return string
-	 */
-	protected function _renderPartial($tag_name, $leading, $trailing) {
-		$partial = $this->_getPartial($tag_name);
-		if ($leading !== null && $trailing !== null) {
-			$whitespace = trim($leading, "\r\n");
-			$partial = preg_replace('/(\\r?\\n)(?!$)/s', "\\1" . $whitespace, $partial);
-		}
-
-		$view = clone($this);
-
-		if ($leading !== null && $trailing !== null) {
-			return $leading . $view->render($partial);
-		} else {
-			return $leading . $view->render($partial) . $trailing;
-		}
-	}
-
-	/**
-	 * Change the Mustache tag delimiter. This method also replaces this object's current
-	 * tag RegEx with one using the new delimiters.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param string $leading Whitespace
-	 * @param string $trailing Whitespace
-	 * @return string
-	 */
-	protected function _changeDelimiter($tag_name, $leading, $trailing) {
-		list($otag, $ctag) = explode(' ', $tag_name);
-		$this->_otag = $otag;
-		$this->_ctag = $ctag;
-
-		$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);
-
-		if ($leading !== null && $trailing !== null) {
-			if (strpos($leading, "\n") === false) {
-				return '';
-			}
-			return $this->_stringHasR($leading, $trailing) ? "\r\n" : "\n";
-		}
-		return $leading . $trailing;
-	}
-
-	/**
-	 * Push a local context onto the stack.
-	 *
-	 * @access protected
-	 * @param array &$local_context
-	 * @return void
-	 */
-	protected function _pushContext(&$local_context) {
-		$new = array();
-		$new[] =& $local_context;
-		foreach (array_keys($this->_context) as $key) {
-			$new[] =& $this->_context[$key];
-		}
-		$this->_context = $new;
-	}
-
-	/**
-	 * Remove the latest context from the stack.
-	 *
-	 * @access protected
-	 * @return void
-	 */
-	protected function _popContext() {
-		$new = array();
-
-		$keys = array_keys($this->_context);
-		array_shift($keys);
-		foreach ($keys as $key) {
-			$new[] =& $this->_context[$key];
-		}
-		$this->_context = $new;
-	}
-
-	/**
-	 * Get a variable from the context array.
-	 *
-	 * If the view is an array, returns the value with array key $tag_name.
-	 * If the view is an object, this will check for a public member variable
-	 * named $tag_name. If none is available, this method will execute and return
-	 * any class method named $tag_name. Failing all of the above, this method will
-	 * return an empty string.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @throws MustacheException Unknown variable name.
-	 * @return string
-	 */
-	protected function _getVariable($tag_name) {
-		if ($tag_name === '.') {
-			return $this->_context[0];
-		} else if (strpos($tag_name, '.') !== false) {
-			$chunks = explode('.', $tag_name);
-			$first = array_shift($chunks);
-
-			$ret = $this->_findVariableInContext($first, $this->_context);
-			foreach ($chunks as $next) {
-				// Slice off a chunk of context for dot notation traversal.
-				$c = array($ret);
-				$ret = $this->_findVariableInContext($next, $c);
-			}
-			return $ret;
-		} else {
-			return $this->_findVariableInContext($tag_name, $this->_context);
-		}
-	}
-
-	/**
-	 * Get a variable from the context array. Internal helper used by getVariable() to abstract
-	 * variable traversal for dot notation.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @param array $context
-	 * @throws MustacheException Unknown variable name.
-	 * @return string
-	 */
-	protected function _findVariableInContext($tag_name, $context) {
-		foreach ($context as $view) {
-			if (is_object($view)) {
-				if (method_exists($view, $tag_name)) {
-					return $view->$tag_name();
-				} else if (isset($view->$tag_name)) {
-					return $view->$tag_name;
-				}
-			} else if (is_array($view) && array_key_exists($tag_name, $view)) {
-				return $view[$tag_name];
-			}
-		}
-
-		if ($this->_throwsException(MustacheException::UNKNOWN_VARIABLE)) {
-			throw new MustacheException("Unknown variable: " . $tag_name, MustacheException::UNKNOWN_VARIABLE);
-		} else {
-			return '';
-		}
-	}
-
-	/**
-	 * Retrieve the partial corresponding to the requested tag name.
-	 *
-	 * Silently fails (i.e. returns '') when the requested partial is not found.
-	 *
-	 * @access protected
-	 * @param string $tag_name
-	 * @throws MustacheException Unknown partial name.
-	 * @return string
-	 */
-	protected function _getPartial($tag_name) {
-		if ((is_array($this->_partials) || $this->_partials instanceof ArrayAccess) && isset($this->_partials[$tag_name])) {
-			return $this->_partials[$tag_name];
-		}
-
-		if ($this->_throwsException(MustacheException::UNKNOWN_PARTIAL)) {
-			throw new MustacheException('Unknown partial: ' . $tag_name, MustacheException::UNKNOWN_PARTIAL);
-		} else {
-			return '';
-		}
-	}
-
-	/**
-	 * Check whether the given $var should be iterated (i.e. in a section context).
-	 *
-	 * @access protected
-	 * @param mixed $var
-	 * @return bool
-	 */
-	protected function _varIsIterable($var) {
-		return $var instanceof Traversable || (is_array($var) && !array_diff_key($var, array_keys(array_keys($var))));
-	}
-
-	/**
-	 * Higher order sections helper: tests whether the section $var is a valid callback.
-	 *
-	 * In Mustache.php, a variable is considered 'callable' if the variable is:
-	 *
-	 *  1. an anonymous function.
-	 *  2. an object and the name of a public function, i.e. `array($SomeObject, 'methodName')`
-	 *  3. a class name and the name of a public static function, i.e. `array('SomeClass', 'methodName')`
-	 *
-	 * @access protected
-	 * @param mixed $var
-	 * @return bool
-	 */
-	protected function _varIsCallable($var) {
-	  return !is_string($var) && is_callable($var);
-	}
-}
-
-
-/**
- * MustacheException class.
- *
- * @extends Exception
- */
-class MustacheException extends Exception {
-
-	// An UNKNOWN_VARIABLE exception is thrown when a {{variable}} is not found
-	// in the current context.
-	const UNKNOWN_VARIABLE         = 0;
-
-	// An UNCLOSED_SECTION exception is thrown when a {{#section}} is not closed.
-	const UNCLOSED_SECTION         = 1;
-
-	// An UNEXPECTED_CLOSE_SECTION exception is thrown when {{/section}} appears
-	// without a corresponding {{#section}} or {{^section}}.
-	const UNEXPECTED_CLOSE_SECTION = 2;
-
-	// An UNKNOWN_PARTIAL exception is thrown whenever a {{>partial}} tag appears
-	// with no associated partial.
-	const UNKNOWN_PARTIAL          = 3;
-
-	// An UNKNOWN_PRAGMA exception is thrown whenever a {{%PRAGMA}} tag appears
-	// which can't be handled by this Mustache instance.
-	const UNKNOWN_PRAGMA           = 4;
-
-}

+ 0 - 85
MustacheLoader.php

@@ -1,85 +0,0 @@
-<?php
-
-/**
- * A Mustache Partial filesystem loader.
- *
- * @author Justin Hileman {@link http://justinhileman.com}
- */
-class MustacheLoader implements ArrayAccess {
-
-	protected $baseDir;
-	protected $partialsCache = array();
-	protected $extension;
-
-	/**
-	 * MustacheLoader constructor.
-	 *
-	 * @access public
-	 * @param  string $baseDir   Base template directory.
-	 * @param  string $extension File extension for Mustache files (default: 'mustache')
-	 * @return void
-	 */
-	public function __construct($baseDir, $extension = 'mustache') {
-		if (!is_dir($baseDir)) {
-			throw new InvalidArgumentException('$baseDir must be a valid directory, ' . $baseDir . ' given.');
-		}
-
-		$this->baseDir   = $baseDir;
-		$this->extension = $extension;
-	}
-
-	/**
-	 * @param  string $offset Name of partial
-	 * @return boolean
-	 */
-	public function offsetExists($offset) {
-		return (isset($this->partialsCache[$offset]) || file_exists($this->pathName($offset)));
-	}
-	
-	/**
-	 * @throws InvalidArgumentException if the given partial doesn't exist
-	 * @param  string $offset Name of partial
-	 * @return string Partial template contents
-	 */
-	public function offsetGet($offset) {
-		if (!$this->offsetExists($offset)) {
-			throw new InvalidArgumentException('Partial does not exist: ' . $offset);
-		}
-
-		if (!isset($this->partialsCache[$offset])) {
-			$this->partialsCache[$offset] = file_get_contents($this->pathName($offset));
-		}
-
-		return $this->partialsCache[$offset];
-	}
-	
-	/**
-	 * MustacheLoader is an immutable filesystem loader. offsetSet throws a LogicException if called.
-	 *
-	 * @throws LogicException
-	 * @return void
-	 */
-	public function offsetSet($offset, $value) {
-		throw new LogicException('Unable to set offset: MustacheLoader is an immutable ArrayAccess object.');
-	}
-	
-	/**
-	 * MustacheLoader is an immutable filesystem loader. offsetUnset throws a LogicException if called.
-	 *
-	 * @throws LogicException
-	 * @return void
-	 */
-	public function offsetUnset($offset) {
-		throw new LogicException('Unable to unset offset: MustacheLoader is an immutable ArrayAccess object.');
-	}
-
-	/**
-	 * An internal helper for generating path names.
-	 * 
-	 * @param  string $file Partial name
-	 * @return string File path
-	 */
-	protected function pathName($file) {
-		return $this->baseDir . '/' . $file . '.' . $this->extension;
-	}
-}

+ 11 - 46
README.markdown

@@ -1,8 +1,9 @@
 Mustache.php
 Mustache.php
 ============
 ============
 
 
-A [Mustache](http://defunkt.github.com/mustache/) implementation in PHP.
+A [Mustache](http://mustache.github.com/) implementation in PHP.
 
 
+[![Build Status](https://secure.travis-ci.org/bobthecow/mustache.php.png?branch=dev)](http://travis-ci.org/bobthecow/mustache.php)
 
 
 Usage
 Usage
 -----
 -----
@@ -11,16 +12,14 @@ A quick example:
 
 
 ```php
 ```php
 <?php
 <?php
-include('Mustache.php');
-$m = new Mustache;
-echo $m->render('Hello {{planet}}', array('planet' => 'World!'));
-// "Hello World!"
+$m = new Mustache_Engine;
+echo $m->render('Hello {{planet}}', array('planet' => 'World!')); // "Hello World!"
 ```
 ```
 
 
 
 
-And a more in-depth example--this is the canonical Mustache template:
+And a more in-depth example -- this is the canonical Mustache template:
 
 
-```
+```html+jinja
 Hello {{name}}
 Hello {{name}}
 You have just won ${{value}}!
 You have just won ${{value}}!
 {{#in_ca}}
 {{#in_ca}}
@@ -29,40 +28,12 @@ Well, ${{taxed_value}}, after taxes.
 ```
 ```
 
 
 
 
-Along with the associated Mustache class:
-
-```php
-<?php
-class Chris extends Mustache {
-    public $name = "Chris";
-    public $value = 10000;
-    
-    public function taxed_value() {
-        return $this->value - ($this->value * 0.4);
-    }
-
-    public $in_ca = true;
-}
-```
-
-
-Render it like so:
-
-```php
-<?php
-$chris = new Chris;
-echo $chris->render($template);
-```
-
-
-Here's the same thing, a different way:
-
-Create a view object--which could also be an associative array, but those don't do functions quite as well:
+Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
 
 
 ```php
 ```php
 <?php
 <?php
 class Chris {
 class Chris {
-    public $name = "Chris";
+    public $name  = "Chris";
     public $value = 10000;
     public $value = 10000;
 
 
     public function taxed_value() {
     public function taxed_value() {
@@ -78,21 +49,15 @@ And render it:
 
 
 ```php
 ```php
 <?php
 <?php
+$m = new Mustache_Engine;
 $chris = new Chris;
 $chris = new Chris;
-$m = new Mustache;
 echo $m->render($template, $chris);
 echo $m->render($template, $chris);
 ```
 ```
 
 
 
 
-Known Issues
-------------
-
- * As of Mustache spec v1.1.2, there are a couple of whitespace bugs around section tags... Despite these failing tests, this
-   version is actually *closer* to correct than previous releases.
-
-
 See Also
 See Also
 --------
 --------
 
 
+ * [Mustache.php wiki](https://github.com/bobthecow/mustache.php/wiki/Home).
  * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
  * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
- * [mustache(1)](http://mustache.github.com/mustache.1.html) and [mustache(5)](http://mustache.github.com/mustache.5.html) man pages.
+ * [mustache(5)](http://mustache.github.com/mustache.5.html) man page.

+ 35 - 36
bin/create_example.php

@@ -22,8 +22,7 @@ This creates a new example and the corresponding files in the examples/ director
 USAGE
 USAGE
 );
 );
 
 
-define('DS', DIRECTORY_SEPARATOR);
-define('EXAMPLE_PATH', realpath(dirname(__FILE__) . DS . ".." . DS . "examples"));
+define('EXAMPLE_PATH', realpath(dirname(__FILE__) . '/../test/fixtures/examples'));
 
 
 
 
 /**
 /**
@@ -39,10 +38,10 @@ define('EXAMPLE_PATH', realpath(dirname(__FILE__) . DS . ".." . DS . "examples")
  * @return string
  * @return string
  */
  */
 function getLowerCaseName($name) {
 function getLowerCaseName($name) {
-	return preg_replace_callback("/([A-Z])/", create_function (
-		'$match',
-		'return "_" . strtolower($match[1]);'
-	), lcfirst($name));
+    return preg_replace_callback("/([A-Z])/", create_function (
+        '$match',
+        'return "_" . strtolower($match[1]);'
+    ), lcfirst($name));
 }
 }
 
 
 /**
 /**
@@ -58,10 +57,10 @@ function getLowerCaseName($name) {
  * @return string
  * @return string
  */
  */
 function getUpperCaseName($name) {
 function getUpperCaseName($name) {
-	return preg_replace_callback("/_([a-z])/", create_function (
-		'$match',
-		'return strtoupper($match{1});'
-	), ucfirst($name));
+    return preg_replace_callback("/_([a-z])/", create_function (
+        '$match',
+        'return strtoupper($match{1});'
+    ), ucfirst($name));
 }
 }
 
 
 
 
@@ -73,8 +72,8 @@ function getUpperCaseName($name) {
  * @return mixed
  * @return mixed
  */
  */
 function out($value) {
 function out($value) {
-	echo $value . "\n";
-	return $value;
+    echo $value . "\n";
+    return $value;
 }
 }
 
 
 /**
 /**
@@ -90,8 +89,8 @@ function out($value) {
  * @return string
  * @return string
  */
  */
 function buildPath($directory, $filename = null,  $extension = null) {
 function buildPath($directory, $filename = null,  $extension = null) {
-	return out(EXAMPLE_PATH . DS . $directory.
-					($extension !== null && $filename !== null ? DS . $filename. "." . $extension : ""));
+    return out(EXAMPLE_PATH . '/' . $directory.
+                    ($extension !== null && $filename !== null ? '/' . $filename. "." . $extension : ""));
 }
 }
 
 
 /**
 /**
@@ -103,9 +102,9 @@ function buildPath($directory, $filename = null,  $extension = null) {
  * @return void
  * @return void
  */
  */
 function createDirectory($directory) {
 function createDirectory($directory) {
-	if(!@mkdir(buildPath($directory))) {
-		die("FAILED to create directory\n");
-	}
+    if(!@mkdir(buildPath($directory))) {
+        die("FAILED to create directory\n");
+    }
 }
 }
 
 
 /**
 /**
@@ -120,13 +119,13 @@ function createDirectory($directory) {
  * @return void
  * @return void
  */
  */
 function createFile($directory, $filename, $extension, $content = "") {
 function createFile($directory, $filename, $extension, $content = "") {
-	$handle = @fopen(buildPath($directory, $filename, $extension), "w");
-	if($handle) {
-		fwrite($handle, $content);
-		fclose($handle);
-	} else {
-		die("FAILED to create file\n");
-	}
+    $handle = @fopen(buildPath($directory, $filename, $extension), "w");
+    if($handle) {
+        fwrite($handle, $content);
+        fclose($handle);
+    } else {
+        die("FAILED to create file\n");
+    }
 }
 }
 
 
 
 
@@ -144,29 +143,29 @@ function createFile($directory, $filename, $extension, $content = "") {
  * @return void
  * @return void
  */
  */
 function main($example_name) {
 function main($example_name) {
-	$lowercase = getLowerCaseName($example_name);
-	$uppercase = getUpperCaseName($example_name);
-	createDirectory($lowercase);
-	createFile($lowercase, $lowercase, "mustache");
-	createFile($lowercase, $lowercase, "txt");
-	createFile($lowercase, $uppercase, "php", <<<CONTENT
+    $lowercase = getLowerCaseName($example_name);
+    $uppercase = getUpperCaseName($example_name);
+    createDirectory($lowercase);
+    createFile($lowercase, $lowercase, "mustache");
+    createFile($lowercase, $lowercase, "txt");
+    createFile($lowercase, $uppercase, "php", <<<CONTENT
 <?php
 <?php
 
 
-class {$uppercase} extends Mustache {
+class {$uppercase} {
 
 
 }
 }
 
 
 CONTENT
 CONTENT
-	);
+    );
 }
 }
 
 
 // check if enougth arguments are given
 // check if enougth arguments are given
 if(count($argv) > 1) {
 if(count($argv) > 1) {
-	// get the name of the example
-	$example_name = $argv[1];
+    // get the name of the example
+    $example_name = $argv[1];
 
 
-	main($example_name);
+    main($example_name);
 
 
 } else {
 } else {
-	echo USAGE;
+    echo USAGE;
 }
 }

+ 21 - 0
composer.json

@@ -0,0 +1,21 @@
+{
+    "name": "mustache/mustache",
+    "description": "A Mustache implementation in PHP.",
+    "keywords": ["templating", "mustache"],
+    "homepage": "https://github.com/bobthecow/mustache.php",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Justin Hileman",
+            "email": "justin@justinhileman.info",
+            "homepage": "http://justinhileman.com"
+        }
+    ],
+    "require": {
+        "php": ">=5.2.4"
+    },
+    "autoload": {
+        "psr-0": { "Mustache": "src/" }
+    }
+}

+ 0 - 13
examples/child_context/ChildContext.php

@@ -1,13 +0,0 @@
-<?php
-
-class ChildContext extends Mustache {
-	public $parent = array(
-		'child' => 'child works',
-	);
-	
-	public $grandparent = array(
-		'parent' => array(
-			'child' => 'grandchild works',
-		),
-	);
-}

+ 0 - 7
examples/comments/Comments.php

@@ -1,7 +0,0 @@
-<?php
-
-class Comments extends Mustache {
-	public function title() {
-		return 'A Comedy of Errors';
-	}
-}

+ 0 - 19
examples/complex/complex.php

@@ -1,19 +0,0 @@
-<?php
-
-class Complex extends Mustache {
-	public $header = 'Colors';
-
-	public $item = array(
-		array('name' => 'red', 'current' => true, 'url' => '#Red'),
-		array('name' => 'green', 'current' => false, 'url' => '#Green'),
-		array('name' => 'blue', 'current' => false, 'url' => '#Blue'),
-	);
-
-	public function notEmpty() {
-		return !($this->isEmpty());
-	}
-
-	public function isEmpty() {
-		return count($this->item) === 0;
-	}
-}

+ 0 - 14
examples/delimiters/Delimiters.php

@@ -1,14 +0,0 @@
-<?php
-
-class Delimiters 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 - 20
examples/dot_notation/DotNotation.php

@@ -1,20 +0,0 @@
-<?php
-
-/**
- * DotNotation example class. Uses DOT_NOTATION pragma.
- *
- * @extends Mustache
- */
-class DotNotation extends Mustache {
-	public $person = array(
-		'name' => array('first' => 'Chris', 'last' => 'Firescythe'),
-		'age' => 24,
-		'hobbies' => array('Cycling', 'Fishing'),
-		'hometown' => array(
-			'city' => 'Cincinnati',
-			'state' => 'OH',
-		),
-	);
-
-	public $normal = 'Normal';
-}

+ 0 - 9
examples/double_section/DoubleSection.php

@@ -1,9 +0,0 @@
-<?php
-
-class DoubleSection extends Mustache {
-	public function t() {
-		return true;
-	}
-
-	public $two = "second";
-}

+ 0 - 5
examples/escaped/Escaped.php

@@ -1,5 +0,0 @@
-<?php
-
-class Escaped extends Mustache {
-	public $title = '"Bear" > "Shark"';
-}

+ 0 - 24
examples/grand_parent_context/GrandParentContext.php

@@ -1,24 +0,0 @@
-<?php
-
-class GrandParentContext extends Mustache {
-	public $grand_parent_id = 'grand_parent1';
-	public $parent_contexts = array();
-	
-	public function __construct() {
-		parent::__construct();
-		
-		$this->parent_contexts[] = array('parent_id' => 'parent1', 'child_contexts' => array(
-			array('child_id' => 'parent1-child1'),
-			array('child_id' => 'parent1-child2')
-		));
-		
-		$parent2 = new stdClass();
-		$parent2->parent_id = 'parent2';
-		$parent2->child_contexts = array(
-			array('child_id' => 'parent2-child1'),
-			array('child_id' => 'parent2-child2')
-		);
-		
-		$this->parent_contexts[] = $parent2;
-	}
-}

+ 0 - 5
examples/implicit_iterator/ImplicitIterator.php

@@ -1,5 +0,0 @@
-<?php
-
-class ImplicitIterator extends Mustache {
-	protected $data = array('Donkey Kong', 'Luigi', 'Mario', 'Peach', 'Yoshi');
-}

+ 0 - 6
examples/inverted_double_section/InvertedDoubleSection.php

@@ -1,6 +0,0 @@
-<?php
-
-class InvertedDoubleSection extends Mustache {
-	public $t = false;
-	public $two = 'second';
-}

+ 0 - 5
examples/inverted_section/InvertedSection.php

@@ -1,5 +0,0 @@
-<?php
-
-class InvertedSection extends Mustache {
-	public $repo = array();
-}

+ 0 - 2
examples/inverted_section/inverted_section.mustache

@@ -1,2 +0,0 @@
-{{#repo}}<b>{{name}}</b>{{/repo}}
-{{^repo}}No repos :({{/repo}}

+ 0 - 1
examples/inverted_section/inverted_section.txt

@@ -1 +0,0 @@
-No repos :(

+ 0 - 13
examples/partials/Partials.php

@@ -1,13 +0,0 @@
-<?php
-
-class Partials extends Mustache {
-	public $name = 'ilmich';
-	public $data = array(
-		array('name' => 'federica', 'age' => 27, 'gender' => 'female'),
-		array('name' => 'marco', 'age' => 32, 'gender' => 'male'),
-	);
-
-	protected $_partials = array(
-		'children' => "{{#data}}{{name}} - {{age}} - {{gender}}\n{{/data}}",
-	);
-}

+ 0 - 2
examples/partials/partials.mustache

@@ -1,2 +0,0 @@
-Children of {{name}}:
-{{>children}}

+ 0 - 3
examples/partials/partials.txt

@@ -1,3 +0,0 @@
-Children of ilmich:
-federica - 27 - female
-marco - 32 - male

+ 0 - 19
examples/partials_with_view_class/PartialsWithViewClass.php

@@ -1,19 +0,0 @@
-<?php
-
-class PartialsWithViewClass extends Mustache {
-	public function __construct($template = null, $view = null, $partials = null) {
-		// Use an object of an arbitrary class as a View for this Mustache instance:
-		$view = new StdClass();
-		$view->name = 'ilmich';
-		$view->data = array(
-			array('name' => 'federica', 'age' => 27, 'gender' => 'female'),
-			array('name' => 'marco', 'age' => 32, 'gender' => 'male'),
-		);
-
-		$partials = array(
-			'children' => "{{#data}}{{name}} - {{age}} - {{gender}}\n{{/data}}",
-		);
-
-		parent::__construct($template, $view, $partials);
-	}
-}

+ 0 - 2
examples/partials_with_view_class/partials_with_view_class.mustache

@@ -1,2 +0,0 @@
-Children of {{name}}:
-{{>children}}

+ 0 - 3
examples/partials_with_view_class/partials_with_view_class.txt

@@ -1,3 +0,0 @@
-Children of ilmich:
-federica - 27 - female
-marco - 32 - male

+ 0 - 5
examples/pragma_unescaped/PragmaUnescaped.php

@@ -1,5 +0,0 @@
-<?php
-
-class PragmaUnescaped extends Mustache {
-	public $vs = 'Bear > Shark';
-}

+ 0 - 3
examples/pragma_unescaped/pragma_unescaped.mustache

@@ -1,3 +0,0 @@
-{{%UNESCAPED}}
-{{vs}}
-{{{vs}}}

+ 0 - 2
examples/pragma_unescaped/pragma_unescaped.txt

@@ -1,2 +0,0 @@
-Bear > Shark
-Bear &gt; Shark

+ 0 - 8
examples/pragmas_in_partials/PragmasInPartials.php

@@ -1,8 +0,0 @@
-<?php
-
-class PragmasInPartials extends Mustache {
-	public $say = '< RAWR!! >';
-	protected $_partials = array(
-		'dinosaur' => '{{say}}'
-	);
-}

+ 0 - 3
examples/pragmas_in_partials/pragmas_in_partials.mustache

@@ -1,3 +0,0 @@
-{{%UNESCAPED}}
-{{say}}
-{{>dinosaur}}

+ 0 - 2
examples/pragmas_in_partials/pragmas_in_partials.txt

@@ -1,2 +0,0 @@
-< RAWR!! >
-&lt; RAWR!! &gt;

+ 0 - 16
examples/recursive_partials/RecursivePartials.php

@@ -1,16 +0,0 @@
-<?php
-
-class RecursivePartials extends Mustache {
-	protected $_partials = array(
-		'child' => " > {{ name }}{{#child}}{{>child}}{{/child}}",
-	);
-
-	public $name  = 'George';
-	public $child = array(
-		'name'  => 'Dan',
-		'child' => array(
-			'name'  => 'Justin',
-			'child' => false,
-		)
-	);
-}

+ 0 - 16
examples/section_iterator_objects/SectionIteratorObjects.php

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

+ 0 - 26
examples/section_magic_objects/SectionMagicObjects.php

@@ -1,26 +0,0 @@
-<?php
-
-class SectionMagicObjects extends Mustache {
-	public $start = "It worked the first time.";
-
-	public function middle() {
-		return new MagicObject();
-	}
-
-	public $final = "Then, surprisingly, it worked the final time.";
-}
-
-class MagicObject {
-	protected $_data = array(
-		'foo' => 'And it worked the second time.',
-		'bar' => 'As well as the third.'
-	);
-
-	public function __get($key) {
-		return isset($this->_data[$key]) ? $this->_data[$key] : NULL;
-	}
-
-	public function __isset($key) {
-		return isset($this->_data[$key]);
-	}
-}

+ 0 - 16
examples/section_objects/SectionObjects.php

@@ -1,16 +0,0 @@
-<?php
-
-class SectionObjects extends Mustache {
-	public $start = "It worked the first time.";
-
-	public function middle() {
-		return new SectionObject;
-	}
-
-	public $final = "Then, surprisingly, it worked the final time.";
-}
-
-class SectionObject {
-	public $foo = 'And it worked the second time.';
-	public $bar = 'As well as the third.';
-}

+ 0 - 14
examples/sections/Sections.php

@@ -1,14 +0,0 @@
-<?php
-
-class Sections 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 - 33
examples/sections_nested/SectionsNested.php

@@ -1,33 +0,0 @@
-<?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'),
-				)
-			),
-		);
-	}
-}

+ 0 - 12
examples/simple/Simple.php

@@ -1,12 +0,0 @@
-<?php
-
-class Simple extends Mustache {
-	public $name = "Chris";
-	public $value = 10000;
-
-	public function taxed_value() {
-		return $this->value - ($this->value * 0.4);
-	}
-
-	public $in_ca = true;
-};

+ 0 - 5
examples/unescaped/Unescaped.php

@@ -1,5 +0,0 @@
-<?php
-
-class Unescaped extends Mustache {
-	public $title = "Bear > Shark";
-}

+ 0 - 5
examples/utf8/UTF8.php

@@ -1,5 +0,0 @@
-<?php
-
-class UTF8Unescaped extends Mustache {
-	public $test = '中文又来啦';
-}

+ 0 - 5
examples/utf8_unescaped/UTF8Unescaped.php

@@ -1,5 +0,0 @@
-<?php
-
-class UTF8 extends Mustache {
-	public $test = '中文又来啦';
-}

+ 0 - 37
examples/whitespace/Whitespace.php

@@ -1,37 +0,0 @@
-<?php
-
-/**
- * Whitespace test for tag names.
- *
- * Per http://github.com/janl/mustache.js/issues/issue/34/#comment_244396
- * tags should strip leading and trailing whitespace in key names.
- *
- * `{{> tag }}` and `{{> tag}}` and `{{>tag}}` should all be equivalent.
- *
- * @extends Mustache
- */
-class Whitespace extends Mustache {
-	public $foo = 'alpha';
-
-	public $bar = 'beta';
-
-	public function baz() {
-		return 'gamma';
-	}
-
-	public function qux() {
-		return array(
-			array('key with space' => 'A'),
-			array('key with space' => 'B'),
-			array('key with space' => 'C'),
-			array('key with space' => 'D'),
-			array('key with space' => 'E'),
-			array('key with space' => 'F'),
-			array('key with space' => 'G'),
-		);
-	}
-
-	protected $_partials = array(
-		'alphabet' => " * {{.}}\n",
-	);
-}

+ 17 - 0
phpunit.xml.dist

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false" colors="true" bootstrap="./test/bootstrap.php">
+	<testsuite name="Mustache">
+		<directory suffix="Test.php">./test</directory>
+		<exclude>./test/Mustache/Test/FiveThree</exclude>
+	</testsuite>
+
+	<testsuite name="Mustache FiveThree">
+		<directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">./test/Mustache/Test/FiveThree</directory>
+	</testsuite>
+
+	<filter>
+		<whitelist>
+			<directory suffix=".php">./src/Mustache</directory>
+		</whitelist>
+	</filter>
+</phpunit>

+ 69 - 0
src/Mustache/Autoloader.php

@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache class autoloader.
+ */
+class Mustache_Autoloader
+{
+
+    private $baseDir;
+
+    /**
+     * Autoloader constructor.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     */
+    public function __construct($baseDir = null)
+    {
+        if ($baseDir === null) {
+            $this->baseDir = dirname(__FILE__).'/..';
+        } else {
+            $this->baseDir = rtrim($baseDir, '/');
+        }
+    }
+
+    /**
+     * Register a new instance as an SPL autoloader.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     *
+     * @return Mustache_Autoloader Registered Autoloader instance
+     */
+    public static function register($baseDir = null)
+    {
+        $loader = new self($baseDir);
+        spl_autoload_register(array($loader, 'autoload'));
+
+        return $loader;
+    }
+
+    /**
+     * Autoload Mustache classes.
+     *
+     * @param string $class
+     */
+    public function autoload($class)
+    {
+        if ($class[0] === '\\') {
+            $class = substr($class, 1);
+        }
+
+        if (strpos($class, 'Mustache') !== 0) {
+            return;
+        }
+
+        $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
+        if (is_file($file)) {
+            require $file;
+        }
+    }
+}

+ 386 - 0
src/Mustache/Compiler.php

@@ -0,0 +1,386 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Compiler class.
+ *
+ * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
+ */
+class Mustache_Compiler
+{
+
+    private $sections;
+    private $source;
+    private $indentNextLine;
+    private $customEscape;
+    private $charset;
+
+    /**
+     * Compile a Mustache token parse tree into PHP source code.
+     *
+     * @param string $source       Mustache Template source code
+     * @param string $tree         Parse tree of Mustache tokens
+     * @param string $name         Mustache Template class name
+     * @param bool   $customEscape (default: false)
+     * @param string $charset      (default: 'UTF-8')
+     *
+     * @return string Generated PHP source code
+     */
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
+    {
+        $this->sections       = array();
+        $this->source         = $source;
+        $this->indentNextLine = true;
+        $this->customEscape   = $customEscape;
+        $this->charset        = $charset;
+
+        return $this->writeCode($tree, $name);
+    }
+
+    /**
+     * Helper function for walking the Mustache token parse tree.
+     *
+     * @throws InvalidArgumentException upon encountering unknown token types.
+     *
+     * @param array $tree  Parse tree of Mustache tokens
+     * @param int   $level (default: 0)
+     *
+     * @return string Generated PHP source code
+     */
+    private function walk(array $tree, $level = 0)
+    {
+        $code = '';
+        $level++;
+        foreach ($tree as $node) {
+            switch ($node[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_SECTION:
+                    $code .= $this->section(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_INVERTED:
+                    $code .= $this->invertedSection(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                case Mustache_Tokenizer::T_PARTIAL_2:
+                    $code .= $this->partial(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_UNESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED_2:
+                    $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
+                    break;
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    break;
+
+                case Mustache_Tokenizer::T_ESCAPED:
+                    $code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
+                    break;
+
+                case Mustache_Tokenizer::T_TEXT:
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                    break;
+
+                default:
+                    throw new InvalidArgumentException('Unknown node type: '.json_encode($node));
+            }
+        }
+
+        return $code;
+    }
+
+    const KLASS = '<?php
+
+        class %s extends Mustache_Template
+        {
+            public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false)
+            {
+                $buffer = \'\';
+        %s
+
+                if ($escape) {
+                    return %s;
+                } else {
+                    return $buffer;
+                }
+            }
+        %s
+        }';
+
+    /**
+     * Generate Mustache Template class PHP source.
+     *
+     * @param array  $tree Parse tree of Mustache tokens
+     * @param string $name Mustache Template class name
+     *
+     * @return string Generated PHP source code
+     */
+    private function writeCode($tree, $name)
+    {
+        $code     = $this->walk($tree);
+        $sections = implode("\n", $this->sections);
+
+        return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $this->getEscape('$buffer'), $sections);
+    }
+
+    const SECTION_CALL = '
+        // %s section
+        $buffer .= $this->section%s($context, $indent, $context->%s(%s));
+    ';
+
+    const SECTION = '
+        private function section%s(Mustache_Context $context, $indent, $value) {
+            $buffer = \'\';
+            if (!is_string($value) && is_callable($value)) {
+                $source = %s;
+                $buffer .= $this->mustache
+                    ->loadLambda((string) call_user_func($value, $source)%s)
+                    ->renderInternal($context, $indent);
+            } elseif (!empty($value)) {
+                $values = $this->isIterable($value) ? $value : array($value);
+                foreach ($values as $value) {
+                    $context->push($value);%s
+                    $context->pop();
+                }
+            }
+
+            return $buffer;
+        }';
+
+    /**
+     * Generate Mustache Template section PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated section PHP source code
+     */
+    private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $method = $this->getFindMethod($id);
+        $id     = var_export($id, true);
+        $source = var_export(substr($this->source, $start, $end - $start), true);
+
+        if ($otag !== '{{' || $ctag !== '}}') {
+            $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+        } else {
+            $delims = '';
+        }
+
+        $key    = ucfirst(md5($delims."\n".$source));
+
+        if (!isset($this->sections[$key])) {
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $source, $delims, $this->walk($nodes, 2));
+        }
+
+        return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
+    }
+
+    const INVERTED_SECTION = '
+        // %s inverted section
+        $value = $context->%s(%s);
+        if (empty($value)) {
+            %s
+        }';
+
+    /**
+     * Generate Mustache Template inverted section PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $level
+     *
+     * @return string Generated inverted section PHP source code
+     */
+    private function invertedSection($nodes, $id, $level)
+    {
+        $method = $this->getFindMethod($id);
+        $id     = var_export($id, true);
+
+        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
+    }
+
+    const PARTIAL = '
+        if ($partial = $this->mustache->loadPartial(%s)) {
+            $buffer .= $partial->renderInternal($context, %s);
+        }
+    ';
+
+    /**
+     * Generate Mustache Template partial call PHP source.
+     *
+     * @param string $id     Partial name
+     * @param string $indent Whitespace indent to apply to partial
+     * @param int    $level
+     *
+     * @return string Generated partial call PHP source code
+     */
+    private function partial($id, $indent, $level)
+    {
+        return sprintf(
+            $this->prepare(self::PARTIAL, $level),
+            var_export($id, true),
+            var_export($indent, true)
+        );
+    }
+
+    const VARIABLE = '
+        $value = $context->%s(%s);
+        if (!is_string($value) && is_callable($value)) {
+            $value = $this->mustache
+                ->loadLambda((string) call_user_func($value))
+                ->renderInternal($context, $indent);
+        }
+        $buffer .= %s%s;
+    ';
+
+    /**
+     * Generate Mustache Template variable interpolation PHP source.
+     *
+     * @param string  $id     Variable name
+     * @param boolean $escape Escape the variable value for output?
+     * @param int     $level
+     *
+     * @return string Generated variable interpolation PHP source
+     */
+    private function variable($id, $escape, $level)
+    {
+        $method = $this->getFindMethod($id);
+        $id     = ($method !== 'last') ? var_export($id, true) : '';
+        $value  = $escape ? $this->getEscape() : '$value';
+
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value);
+    }
+
+    const LINE = '$buffer .= "\n";';
+    const TEXT = '$buffer .= %s%s;';
+
+    /**
+     * Generate Mustache Template output Buffer call PHP source.
+     *
+     * @param string $text
+     * @param int    $level
+     *
+     * @return string Generated output Buffer call PHP source
+     */
+    private function text($text, $level)
+    {
+        if ($text === "\n") {
+            $this->indentNextLine = true;
+
+            return $this->prepare(self::LINE, $level);
+        } else {
+            return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        }
+    }
+
+    /**
+     * Prepare PHP source code snippet for output.
+     *
+     * @param string  $text
+     * @param int     $bonus          Additional indent level (default: 0)
+     * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
+     *
+     * @return string PHP source code snippet
+     */
+    private function prepare($text, $bonus = 0, $prependNewline = true)
+    {
+        $text = ($prependNewline ? "\n" : '').trim($text);
+        if ($prependNewline) {
+            $bonus++;
+        }
+
+        return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
+    }
+
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
+    const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
+
+    /**
+     * Get the current escaper.
+     *
+     * @param string $value (default: '$value')
+     *
+     * @return string Either a custom callback, or an inline call to `htmlspecialchars`
+     */
+    private function getEscape($value = '$value')
+    {
+        if ($this->customEscape) {
+            return sprintf(self::CUSTOM_ESCAPE, $value);
+        } else {
+            return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
+        }
+    }
+
+    /**
+     * Select the appropriate Context `find` method for a given $id.
+     *
+     * The return value will be one of `find`, `findDot` or `last`.
+     *
+     * @see Mustache_Context::find
+     * @see Mustache_Context::findDot
+     * @see Mustache_Context::last
+     *
+     * @param string $id Variable name
+     *
+     * @return string `find` method name
+     */
+    private function getFindMethod($id)
+    {
+        if ($id === '.') {
+            return 'last';
+        } elseif (strpos($id, '.') === false) {
+            return 'find';
+        } else {
+            return 'findDot';
+        }
+    }
+
+    const LINE_INDENT = '$indent . ';
+
+    /**
+     * Get the current $indent prefix to write to the buffer.
+     *
+     * @return string "$indent . " or ""
+     */
+    private function flushIndent()
+    {
+        if ($this->indentNextLine) {
+            $this->indentNextLine = false;
+
+            return self::LINE_INDENT;
+        } else {
+            return '';
+        }
+    }
+}

+ 149 - 0
src/Mustache/Context.php

@@ -0,0 +1,149 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template rendering Context.
+ */
+class Mustache_Context
+{
+    private $stack = array();
+
+    /**
+     * Mustache rendering Context constructor.
+     *
+     * @param mixed $context Default rendering context (default: null)
+     */
+    public function __construct($context = null)
+    {
+        if ($context !== null) {
+            $this->stack = array($context);
+        }
+    }
+
+    /**
+     * Push a new Context frame onto the stack.
+     *
+     * @param mixed $value Object or array to use for context
+     */
+    public function push($value)
+    {
+        array_push($this->stack, $value);
+    }
+
+    /**
+     * Pop the last Context frame from the stack.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function pop()
+    {
+        return array_pop($this->stack);
+    }
+
+    /**
+     * Get the last Context frame.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function last()
+    {
+        return end($this->stack);
+    }
+
+    /**
+     * Find a variable in the Context stack.
+     *
+     * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
+     * rendering context, look for a variable with the given name:
+     *
+     *  * If the Context frame is an associative array which contains the key $id, returns the value of that element.
+     *  * If the Context frame is an object, this will check first for a public method, then a public property named
+     *    $id. Failing both of these, it will try `__isset` and `__get` magic methods.
+     *  * If a value named $id is not found in any Context frame, returns an empty string.
+     *
+     * @param string $id Variable name
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function find($id)
+    {
+        return $this->findVariableInStack($id, $this->stack);
+    }
+
+    /**
+     * Find a 'dot notation' variable in the Context stack.
+     *
+     * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
+     * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
+     * result. For example, given the following context stack:
+     *
+     *     $data = array(
+     *         'name' => 'Fred',
+     *         'child' => array(
+     *             'name' => 'Bob'
+     *         ),
+     *     );
+     *
+     * ... and the Mustache following template:
+     *
+     *     {{ child.name }}
+     *
+     * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
+     * Context frames.
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        $value  = $this->findVariableInStack($first, $this->stack);
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
+    /**
+     * Helper function to find a variable in the Context stack.
+     *
+     * @see Mustache_Context::find
+     *
+     * @param string $id    Variable name
+     * @param array  $stack Context stack
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    private function findVariableInStack($id, array $stack)
+    {
+        for ($i = count($stack) - 1; $i >= 0; $i--) {
+            if (is_object($stack[$i])) {
+                if (method_exists($stack[$i], $id)) {
+                    return $stack[$i]->$id();
+                } elseif (isset($stack[$i]->$id)) {
+                    return $stack[$i]->$id;
+                }
+            } elseif (is_array($stack[$i]) && array_key_exists($id, $stack[$i])) {
+                return $stack[$i][$id];
+            }
+        }
+
+        return '';
+    }
+}

+ 589 - 0
src/Mustache/Engine.php

@@ -0,0 +1,589 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache implementation in PHP.
+ *
+ * {@link http://defunkt.github.com/mustache}
+ *
+ * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
+ * logic from template files. In fact, it is not even possible to embed logic in the template.
+ *
+ * This is very, very rad.
+ *
+ * @author Justin Hileman {@link http://justinhileman.com}
+ */
+class Mustache_Engine
+{
+    const VERSION      = '2.0.0';
+    const SPEC_VERSION = '1.1.2';
+
+    // Template cache
+    private $templates = array();
+
+    // Environment
+    private $templateClassPrefix = '__Mustache_';
+    private $cache = null;
+    private $loader;
+    private $partialsLoader;
+    private $helpers;
+    private $escape;
+    private $charset = 'UTF-8';
+
+    /**
+     * Mustache class constructor.
+     *
+     * Passing an $options array allows overriding certain Mustache options during instantiation:
+     *
+     *     $options = array(
+     *         // The class prefix for compiled templates. Defaults to '__Mustache_'
+     *         'template_class_prefix' => '__MyTemplates_',
+     *
+     *         // A cache directory for compiled templates. Mustache will not cache templates unless this is set
+     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+     *
+     *         // A Mustache template loader instance. Uses a StringLoader if not specified
+     *         'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+     *
+     *         // A Mustache loader instance for partials.
+     *         'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+     *
+     *         // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
+     *         // efficient or lazy as a Filesystem (or database) loader.
+     *         'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
+     *
+     *         // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
+     *         // sections), or any other valid Mustache context value. They will be prepended to the context stack,
+     *         // so they will be available in any template loaded by this Mustache instance.
+     *         'helpers' => array('i18n' => function($text) {
+     *              // do something translatey here...
+     *          }),
+     *
+     *         // An 'escape' callback, responsible for escaping double-mustache variables.
+     *         'escape' => function($value) {
+     *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
+     *         },
+     *
+     *         // character set for `htmlspecialchars`. Defaults to 'UTF-8'
+     *         'charset' => 'ISO-8859-1',
+     *     );
+     *
+     * @param array $options (default: array())
+     */
+    public function __construct(array $options = array())
+    {
+        if (isset($options['template_class_prefix'])) {
+            $this->templateClassPrefix = $options['template_class_prefix'];
+        }
+
+        if (isset($options['cache'])) {
+            $this->cache = $options['cache'];
+        }
+
+        if (isset($options['loader'])) {
+            $this->setLoader($options['loader']);
+        }
+
+        if (isset($options['partials_loader'])) {
+            $this->setPartialsLoader($options['partials_loader']);
+        }
+
+        if (isset($options['partials'])) {
+            $this->setPartials($options['partials']);
+        }
+
+        if (isset($options['helpers'])) {
+            $this->setHelpers($options['helpers']);
+        }
+
+        if (isset($options['escape'])) {
+            if (!is_callable($options['escape'])) {
+                throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+            }
+
+            $this->escape = $options['escape'];
+        }
+
+        if (isset($options['charset'])) {
+            $this->charset = $options['charset'];
+        }
+    }
+
+    /**
+     * Shortcut 'render' invocation.
+     *
+     * Equivalent to calling `$mustache->loadTemplate($template)->render($data);`
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Template::render
+     *
+     * @param string $template
+     * @param mixed  $data
+     *
+     * @return string Rendered template
+     */
+    public function render($template, $data)
+    {
+        return $this->loadTemplate($template)->render($data);
+    }
+
+    /**
+     * Get the current Mustache escape callback.
+     *
+     * @return mixed Callable or null
+     */
+    public function getEscape()
+    {
+        return $this->escape;
+    }
+
+    /**
+     * Get the current Mustache character set.
+     *
+     * @return string
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Set the Mustache template Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function setLoader(Mustache_Loader $loader)
+    {
+        $this->loader = $loader;
+    }
+
+    /**
+     * Get the current Mustache template Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * a StringLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getLoader()
+    {
+        if (!isset($this->loader)) {
+            $this->loader = new Mustache_Loader_StringLoader;
+        }
+
+        return $this->loader;
+    }
+
+    /**
+     * Set the Mustache partials Loader instance.
+     *
+     * @param Mustache_Loader $partialsLoader
+     */
+    public function setPartialsLoader(Mustache_Loader $partialsLoader)
+    {
+        $this->partialsLoader = $partialsLoader;
+    }
+
+    /**
+     * Get the current Mustache partials Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * an ArrayLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getPartialsLoader()
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader;
+        }
+
+        return $this->partialsLoader;
+    }
+
+    /**
+     * Set partials for the current partials Loader instance.
+     *
+     * @throws RuntimeException If the current Loader instance is immutable
+     *
+     * @param array $partials (default: array())
+     */
+    public function setPartials(array $partials = array())
+    {
+        $loader = $this->getPartialsLoader();
+        if (!$loader instanceof Mustache_Loader_MutableLoader) {
+            throw new RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+        }
+
+        $loader->setTemplates($partials);
+    }
+
+    /**
+     * Set an array of Mustache helpers.
+     *
+     * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
+     * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
+     * any template loaded by this Mustache instance.
+     *
+     * @throws InvalidArgumentException if $helpers is not an array or Traversable
+     *
+     * @param array|Traversable $helpers
+     */
+    public function setHelpers($helpers)
+    {
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new InvalidArgumentException('setHelpers expects an array of helpers');
+        }
+
+        $this->getHelpers()->clear();
+
+        foreach ($helpers as $name => $helper) {
+            $this->addHelper($name, $helper);
+        }
+    }
+
+    /**
+     * Get the current set of Mustache helpers.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @return Mustache_HelperCollection
+     */
+    public function getHelpers()
+    {
+        if (!isset($this->helpers)) {
+            $this->helpers = new Mustache_HelperCollection;
+        }
+
+        return $this->helpers;
+    }
+
+    /**
+     * Add a new Mustache helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function addHelper($name, $helper)
+    {
+        $this->getHelpers()->add($name, $helper);
+    }
+
+    /**
+     * Get a Mustache helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function getHelper($name)
+    {
+        return $this->getHelpers()->get($name);
+    }
+
+    /**
+     * Check whether this Mustache instance has a helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return boolean True if the helper is present
+     */
+    public function hasHelper($name)
+    {
+        return $this->getHelpers()->has($name);
+    }
+
+    /**
+     * Remove a helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     */
+    public function removeHelper($name)
+    {
+        $this->getHelpers()->remove($name);
+    }
+
+    /**
+     * Set the Mustache Tokenizer instance.
+     *
+     * @param Mustache_Tokenizer $tokenizer
+     */
+    public function setTokenizer(Mustache_Tokenizer $tokenizer)
+    {
+        $this->tokenizer = $tokenizer;
+    }
+
+    /**
+     * Get the current Mustache Tokenizer instance.
+     *
+     * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Tokenizer
+     */
+    public function getTokenizer()
+    {
+        if (!isset($this->tokenizer)) {
+            $this->tokenizer = new Mustache_Tokenizer;
+        }
+
+        return $this->tokenizer;
+    }
+
+    /**
+     * Set the Mustache Parser instance.
+     *
+     * @param Mustache_Parser $parser
+     */
+    public function setParser(Mustache_Parser $parser)
+    {
+        $this->parser = $parser;
+    }
+
+    /**
+     * Get the current Mustache Parser instance.
+     *
+     * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Parser
+     */
+    public function getParser()
+    {
+        if (!isset($this->parser)) {
+            $this->parser = new Mustache_Parser;
+        }
+
+        return $this->parser;
+    }
+
+    /**
+     * Set the Mustache Compiler instance.
+     *
+     * @param Mustache_Compiler $compiler
+     */
+    public function setCompiler(Mustache_Compiler $compiler)
+    {
+        $this->compiler = $compiler;
+    }
+
+    /**
+     * Get the current Mustache Compiler instance.
+     *
+     * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Compiler
+     */
+    public function getCompiler()
+    {
+        if (!isset($this->compiler)) {
+            $this->compiler = new Mustache_Compiler;
+        }
+
+        return $this->compiler;
+    }
+
+    /**
+     * Helper method to generate a Mustache template class.
+     *
+     * @param string $source
+     *
+     * @return string Mustache Template class name
+     */
+    public function getTemplateClassName($source)
+    {
+        return $this->templateClassPrefix . md5(sprintf(
+            'version:%s,escape:%s,charset:%s,source:%s',
+            self::VERSION,
+            isset($this->escape) ? 'custom' : 'default',
+            $this->charset,
+            $source
+        ));
+    }
+
+    /**
+     * Load a Mustache Template by name.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadTemplate($name)
+    {
+        return $this->loadSource($this->getLoader()->load($name));
+    }
+
+    /**
+     * Load a Mustache partial Template by name.
+     *
+     * This is a helper method used internally by Template instances for loading partial templates. You can most likely
+     * ignore it completely.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadPartial($name)
+    {
+        try {
+            return $this->loadSource($this->getPartialsLoader()->load($name));
+        } catch (InvalidArgumentException $e) {
+            // If the named partial cannot be found, return null.
+        }
+    }
+
+    /**
+     * Load a Mustache lambda Template by source.
+     *
+     * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
+     * likely ignore it completely.
+     *
+     * @param string $source
+     * @param string $delims (default: null)
+     *
+     * @return Mustache_Template
+     */
+    public function loadLambda($source, $delims = null)
+    {
+        if ($delims !== null) {
+            $source = $delims . "\n" . $source;
+        }
+
+        return $this->loadSource($source);
+    }
+
+    /**
+     * Instantiate and return a Mustache Template instance by source.
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Engine::loadPartial
+     * @see Mustache_Engine::loadLambda
+     *
+     * @param string $source
+     *
+     * @return Mustache_Template
+     */
+    private function loadSource($source)
+    {
+        $className = $this->getTemplateClassName($source);
+
+        if (!isset($this->templates[$className])) {
+            if (!class_exists($className, false)) {
+                if ($fileName = $this->getCacheFilename($source)) {
+                    if (!is_file($fileName)) {
+                        $this->writeCacheFile($fileName, $this->compile($source));
+                    }
+
+                    require_once $fileName;
+                } else {
+                    eval('?>'.$this->compile($source));
+                }
+            }
+
+            $this->templates[$className] = new $className($this);
+        }
+
+        return $this->templates[$className];
+    }
+
+    /**
+     * Helper method to tokenize a Mustache template.
+     *
+     * @see Mustache_Tokenizer::scan
+     *
+     * @param string $source
+     *
+     * @return array Tokens
+     */
+    private function tokenize($source)
+    {
+        return $this->getTokenizer()->scan($source);
+    }
+
+    /**
+     * Helper method to parse a Mustache template.
+     *
+     * @see Mustache_Parser::parse
+     *
+     * @param string $source
+     *
+     * @return array Token tree
+     */
+    private function parse($source)
+    {
+        return $this->getParser()->parse($this->tokenize($source));
+    }
+
+    /**
+     * Helper method to compile a Mustache template.
+     *
+     * @see Mustache_Compiler::compile
+     *
+     * @param string $source
+     *
+     * @return string generated Mustache template class code
+     */
+    private function compile($source)
+    {
+        $tree = $this->parse($source);
+        $name = $this->getTemplateClassName($source);
+
+        return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
+    }
+
+    /**
+     * Helper method to generate a Mustache Template class cache filename.
+     *
+     * @param string $source
+     *
+     * @return string Mustache Template class cache filename
+     */
+    private function getCacheFilename($source)
+    {
+        if ($this->cache) {
+            return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source));
+        }
+    }
+
+    /**
+     * Helper method to dump a generated Mustache Template subclass to the file cache.
+     *
+     * @throws RuntimeException if unable to write to $fileName.
+     *
+     * @param string $fileName
+     * @param string $source
+     *
+     * @codeCoverageIgnore
+     */
+    private function writeCacheFile($fileName, $source)
+    {
+        if (!is_dir(dirname($fileName))) {
+            mkdir(dirname($fileName), 0777, true);
+        }
+
+        $tempFile = tempnam(dirname($fileName), basename($fileName));
+        if (false !== @file_put_contents($tempFile, $source)) {
+            if (@rename($tempFile, $fileName)) {
+                chmod($fileName, 0644);
+
+                return;
+            }
+        }
+
+        throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+    }
+}

+ 168 - 0
src/Mustache/HelperCollection.php

@@ -0,0 +1,168 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class Mustache_HelperCollection
+{
+    private $helpers = array();
+
+    /**
+     * Helper Collection constructor.
+     *
+     * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
+     *
+     * @throws InvalidArgumentException if the $helpers argument isn't an array or Traversable
+     *
+     * @param array|Traversable $helpers (default: null)
+     */
+    public function __construct($helpers = null)
+    {
+        if ($helpers !== null) {
+            if (!is_array($helpers) && !$helpers instanceof Traversable) {
+                throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+            }
+
+            foreach ($helpers as $name => $helper) {
+                $this->add($name, $helper);
+            }
+        }
+    }
+
+    /**
+     * Magic mutator.
+     *
+     * @see Mustache_HelperCollection::add
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function __set($name, $helper)
+    {
+        $this->add($name, $helper);
+    }
+
+    /**
+     * Add a helper to this collection.
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function add($name, $helper)
+    {
+        $this->helpers[$name] = $helper;
+    }
+
+    /**
+     * Magic accessor.
+     *
+     * @see Mustache_HelperCollection::get
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function __get($name)
+    {
+        return $this->get($name);
+    }
+
+    /**
+     * Get a helper by name.
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function get($name)
+    {
+        if (!$this->has($name)) {
+            throw new InvalidArgumentException('Unknown helper: '.$name);
+        }
+
+        return $this->helpers[$name];
+    }
+
+    /**
+     * Magic isset().
+     *
+     * @see Mustache_HelperCollection::has
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function __isset($name)
+    {
+        return $this->has($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function has($name)
+    {
+        return array_key_exists($name, $this->helpers);
+    }
+
+    /**
+     * Magic unset().
+     *
+     * @see Mustache_HelperCollection::remove
+     *
+     * @param string $name
+     */
+    public function __unset($name)
+    {
+        $this->remove($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @throws InvalidArgumentException if the requested helper is not present.
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (!$this->has($name)) {
+            throw new InvalidArgumentException('Unknown helper: '.$name);
+        }
+
+        unset($this->helpers[$name]);
+    }
+
+    /**
+     * Clear the helper collection.
+     *
+     * Removes all helpers from this collection
+     */
+    public function clear()
+    {
+        $this->helpers = array();
+    }
+
+    /**
+     * Check whether the helper collection is empty.
+     *
+     * @return boolean True if the collection is empty
+     */
+    public function isEmpty()
+    {
+        return empty($this->helpers);
+    }
+}

+ 26 - 0
src/Mustache/Loader.php

@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template Loader interface.
+ */
+interface Mustache_Loader
+{
+
+    /**
+     * Load a Template by name.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name);
+}

+ 79 - 0
src/Mustache/Loader/ArrayLoader.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template array Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source by name from an initial array:
+ *
+ *     $loader = new ArrayLoader(
+ *         'foo' => '{{ bar }}',
+ *         'baz' => 'Hey {{ qux }}!'
+ *     );
+ *
+ *     $tpl = $loader->load('foo'); // '{{ bar }}'
+ *
+ * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
+ * is set. It can also be used as a quick-and-dirty Template loader.
+ *
+ * @implements Loader
+ * @implements MutableLoader
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+
+    /**
+     * ArrayLoader constructor.
+     *
+     * @param array $templates Associative array of Template source (default: array())
+     */
+    public function __construct(array $templates = array())
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Load a Template.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            throw new InvalidArgumentException('Template '.$name.' not found.');
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates)
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template)
+    {
+        $this->templates[$name] = $template;
+    }
+}

+ 118 - 0
src/Mustache/Loader/FilesystemLoader.php

@@ -0,0 +1,118 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template filesystem Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source from the filesystem by name:
+ *
+ *     $loader = new FilesystemLoader(dirname(__FILE__).'/views');
+ *     $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
+ *
+ * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
+ *
+ *     $m = new Mustache(array(
+ *          'loader'          => new FilesystemLoader(dirname(__FILE__).'/views'),
+ *          'partials_loader' => new FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ *     ));
+ *
+ * @implements Loader
+ */
+class Mustache_Loader_FilesystemLoader implements Mustache_Loader
+{
+    private $baseDir;
+    private $extension = '.mustache';
+    private $templates = array();
+
+    /**
+     * Mustache filesystem Loader constructor.
+     *
+     * Passing an $options array allows overriding certain Loader options during instantiation:
+     *
+     *     $options = array(
+     *         // The filename extension used for Mustache templates. Defaults to '.mustache'
+     *         'extension' => '.ms',
+     *     );
+     *
+     * @throws RuntimeException if $baseDir does not exist.
+     *
+     * @param string $baseDir Base directory containing Mustache template files.
+     * @param array  $options Array of Loader options (default: array())
+     */
+    public function __construct($baseDir, array $options = array())
+    {
+        $this->baseDir = rtrim(realpath($baseDir), '/');
+
+        if (!is_dir($this->baseDir)) {
+            throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
+        }
+
+        if (isset($options['extension'])) {
+            $this->extension = '.' . ltrim($options['extension'], '.');
+        }
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     *     $loader = new FilesystemLoader(dirname(__FILE__).'/views');
+     *     $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            $this->templates[$name] = $this->loadFile($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Helper function for loading a Mustache file by name.
+     *
+     * @throws InvalidArgumentException if a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    private function loadFile($name)
+    {
+        $fileName = $this->getFileName($name);
+
+        if (!file_exists($fileName)) {
+            throw new InvalidArgumentException('Template '.$name.' not found.');
+        }
+
+        return file_get_contents($fileName);
+    }
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    private function getFileName($name)
+    {
+        $fileName = $this->baseDir . '/' . $name;
+        if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
+            $fileName .= $this->extension;
+        }
+
+        return $fileName;
+    }
+}

+ 32 - 0
src/Mustache/Loader/MutableLoader.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template mutable Loader interface.
+ */
+interface Mustache_Loader_MutableLoader
+{
+
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates);
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template);
+}

+ 42 - 0
src/Mustache/Loader/StringLoader.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template string Loader implementation.
+ *
+ * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
+ *
+ *     $loader = new StringLoader;
+ *     $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
+ *
+ * This is the default Template Loader instance used by Mustache:
+ *
+ *     $m = new Mustache;
+ *     $tpl = $m->loadTemplate('{{ foo }}');
+ *     echo $tpl->render(array('foo' => 'bar')); // "bar"
+ *
+ * @implements Loader
+ */
+class Mustache_Loader_StringLoader implements Mustache_Loader
+{
+
+    /**
+     * Load a Template by source.
+     *
+     * @param string $name Mustache Template source
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        return $name;
+    }
+}

+ 88 - 0
src/Mustache/Parser.php

@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Parser class.
+ *
+ * This class is responsible for turning a set of Mustache tokens into a parse tree.
+ */
+class Mustache_Parser
+{
+
+    /**
+     * Process an array of Mustache tokens and convert them into a parse tree.
+     *
+     * @param array $tokens Set of Mustache tokens
+     *
+     * @return array Mustache token parse tree
+     */
+    public function parse(array $tokens = array())
+    {
+        return $this->buildTree(new ArrayIterator($tokens));
+    }
+
+    /**
+     * Helper method for recursively building a parse tree.
+     *
+     * @param ArrayIterator $tokens Stream of Mustache tokens
+     * @param array         $parent Parent token (default: null)
+     *
+     * @return array Mustache Token parse tree
+     *
+     * @throws LogicException when nesting errors or mismatched section tags are encountered.
+     */
+    private function buildTree(ArrayIterator $tokens, array $parent = null)
+    {
+        $nodes = array();
+
+        do {
+            $token = $tokens->current();
+            $tokens->next();
+
+            if ($token === null) {
+                continue;
+            } else {
+                switch ($token[Mustache_Tokenizer::TYPE]) {
+                    case Mustache_Tokenizer::T_SECTION:
+                    case Mustache_Tokenizer::T_INVERTED:
+                        $nodes[] = $this->buildTree($tokens, $token);
+                        break;
+
+                    case Mustache_Tokenizer::T_END_SECTION:
+                        if (!isset($parent)) {
+                            throw new LogicException('Unexpected closing tag: /'. $token[Mustache_Tokenizer::NAME]);
+                        }
+
+                        if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
+                            throw new LogicException('Nesting error: ' . $parent[Mustache_Tokenizer::NAME] . ' vs. ' . $token[Mustache_Tokenizer::NAME]);
+                        }
+
+                        $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
+                        $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+                        return $parent;
+                        break;
+
+                    default:
+                        $nodes[] = $token;
+                        break;
+                }
+            }
+
+        } while ($tokens->valid());
+
+        if (isset($parent)) {
+            throw new LogicException('Missing closing tag: ' . $parent[Mustache_Tokenizer::NAME]);
+        }
+
+        return $nodes;
+    }
+}

+ 149 - 0
src/Mustache/Template.php

@@ -0,0 +1,149 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Template class.
+ *
+ * @abstract
+ */
+abstract class Mustache_Template
+{
+
+    /**
+     * @var Mustache_Engine
+     */
+    protected $mustache;
+
+    /**
+     * Mustache Template constructor.
+     *
+     * @param Mustache_Engine $mustache
+     */
+    public function __construct(Mustache_Engine $mustache)
+    {
+        $this->mustache = $mustache;
+    }
+
+    /**
+     * Mustache Template instances can be treated as a function and rendered by simply calling them:
+     *
+     *     $m = new Mustache_Engine;
+     *     $tpl = $m->loadTemplate('Hello, {{ name }}!');
+     *     echo $tpl(array('name' => 'World')); // "Hello, World!"
+     *
+     * @see Mustache_Template::render
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function __invoke($context = array())
+    {
+        return $this->render($context);
+    }
+
+    /**
+     * Render this template given the rendering context.
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($context = array())
+    {
+        return $this->renderInternal($this->prepareContextStack($context));
+    }
+
+    /**
+     * Internal rendering method implemented by Mustache Template concrete subclasses.
+     *
+     * This is where the magic happens :)
+     *
+     * @param Mustache_Context $context
+     * @param string           $indent  (default: '')
+     * @param bool             $escape  (default: false)
+     *
+     * @return string Rendered template
+     */
+    abstract public function renderInternal(Mustache_Context $context, $indent = '', $escape = false);
+
+    /**
+     * Tests whether a value should be iterated over (e.g. in a section context).
+     *
+     * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
+     * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
+     * Java, Python, etc.
+     *
+     * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
+     * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
+     * (associative array). In other words, this will be iterated over:
+     *
+     *     $items = array(
+     *         array('name' => 'foo'),
+     *         array('name' => 'bar'),
+     *         array('name' => 'baz'),
+     *     );
+     *
+     * ... but this will be used as a section context block:
+     *
+     *     $items = array(
+     *         1        => array('name' => 'foo'),
+     *         'banana' => array('name' => 'bar'),
+     *         42       => array('name' => 'baz'),
+     *     );
+     *
+     * @param mixed $value
+     *
+     * @return boolean True if the value is 'iterable'
+     */
+    protected function isIterable($value)
+    {
+        if (is_object($value)) {
+            return $value instanceof Traversable;
+        } elseif (is_array($value)) {
+            $i = 0;
+            foreach ($value as $k => $v) {
+                if ($k !== $i++) {
+                    return false;
+                }
+            }
+
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Helper method to prepare the Context stack.
+     *
+     * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
+     *
+     * @param mixed $context Optional first context frame (default: null)
+     *
+     * @return Mustache_Context
+     */
+    protected function prepareContextStack($context = null)
+    {
+        $stack = new Mustache_Context;
+
+        $helpers = $this->mustache->getHelpers();
+        if (!$helpers->isEmpty()) {
+            $stack->push($helpers);
+        }
+
+        if (!empty($context)) {
+            $stack->push($context);
+        }
+
+        return $stack;
+    }
+}

+ 286 - 0
src/Mustache/Tokenizer.php

@@ -0,0 +1,286 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Tokenizer class.
+ *
+ * This class is responsible for turning raw template source into a set of Mustache tokens.
+ */
+class Mustache_Tokenizer
+{
+
+    // Finite state machine states
+    const IN_TEXT     = 0;
+    const IN_TAG_TYPE = 1;
+    const IN_TAG      = 2;
+
+    // Token types
+    const T_SECTION      = '#';
+    const T_INVERTED     = '^';
+    const T_END_SECTION  = '/';
+    const T_COMMENT      = '!';
+    const T_PARTIAL      = '>';
+    const T_PARTIAL_2    = '<';
+    const T_DELIM_CHANGE = '=';
+    const T_ESCAPED      = '_v';
+    const T_UNESCAPED    = '{';
+    const T_UNESCAPED_2  = '&';
+    const T_TEXT         = '_t';
+
+    // Valid token types
+    private static $tagTypes = array(
+        self::T_SECTION      => true,
+        self::T_INVERTED     => true,
+        self::T_END_SECTION  => true,
+        self::T_COMMENT      => true,
+        self::T_PARTIAL      => true,
+        self::T_PARTIAL_2    => true,
+        self::T_DELIM_CHANGE => true,
+        self::T_ESCAPED      => true,
+        self::T_UNESCAPED    => true,
+        self::T_UNESCAPED_2  => true,
+    );
+
+    // Interpolated tags
+    private static $interpolatedTags = array(
+        self::T_ESCAPED      => true,
+        self::T_UNESCAPED    => true,
+        self::T_UNESCAPED_2  => true,
+    );
+
+    // Token properties
+    const TYPE   = 'type';
+    const NAME   = 'name';
+    const OTAG   = 'otag';
+    const CTAG   = 'ctag';
+    const INDEX  = 'index';
+    const END    = 'end';
+    const INDENT = 'indent';
+    const NODES  = 'nodes';
+    const VALUE  = 'value';
+
+    private $state;
+    private $tagType;
+    private $tag;
+    private $buffer;
+    private $tokens;
+    private $seenTag;
+    private $lineStart;
+    private $otag;
+    private $ctag;
+
+    /**
+     * Scan and tokenize template source.
+     *
+     * @param string $text       Mustache template source to tokenize
+     * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
+     *
+     * @return array Set of Mustache tokens
+     */
+    public function scan($text, $delimiters = null)
+    {
+        $this->reset();
+
+        if ($delimiters = trim($delimiters)) {
+            list($otag, $ctag) = explode(' ', $delimiters);
+            $this->otag = $otag;
+            $this->ctag = $ctag;
+        }
+
+        $len = strlen($text);
+        for ($i = 0; $i < $len; $i++) {
+            switch ($this->state) {
+                case self::IN_TEXT:
+                    if ($this->tagChange($this->otag, $text, $i)) {
+                        $i--;
+                        $this->flushBuffer();
+                        $this->state = self::IN_TAG_TYPE;
+                    } else {
+                        if ($text[$i] == "\n") {
+                            $this->filterLine();
+                        } else {
+                            $this->buffer .= $text[$i];
+                        }
+                    }
+                    break;
+
+                case self::IN_TAG_TYPE:
+
+                    $i += strlen($this->otag) - 1;
+                    if (isset(self::$tagTypes[$text[$i + 1]])) {
+                        $tag = $text[$i + 1];
+                        $this->tagType = $tag;
+                    } else {
+                        $tag = null;
+                        $this->tagType = self::T_ESCAPED;
+                    }
+
+                    if ($this->tagType === self::T_DELIM_CHANGE) {
+                        $i = $this->changeDelimiters($text, $i);
+                        $this->state = self::IN_TEXT;
+                    } else {
+                        if ($tag !== null) {
+                            $i++;
+                        }
+                        $this->state = self::IN_TAG;
+                    }
+                    $this->seenTag = $i;
+                    break;
+
+                default:
+                    if ($this->tagChange($this->ctag, $text, $i)) {
+                        $this->tokens[] = array(
+                            self::TYPE  => $this->tagType,
+                            self::NAME  => trim($this->buffer),
+                            self::OTAG  => $this->otag,
+                            self::CTAG  => $this->ctag,
+                            self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
+                        );
+
+                        $this->buffer = '';
+                        $i += strlen($this->ctag) - 1;
+                        $this->state = self::IN_TEXT;
+                        if ($this->tagType == self::T_UNESCAPED) {
+                            if ($this->ctag == '}}') {
+                                $i++;
+                            } else {
+                                // Clean up `{{{ tripleStache }}}` style tokens.
+                                $lastName = $this->tokens[count($this->tokens) - 1][self::NAME];
+                                if (substr($lastName, -1) === '}') {
+                                    $this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1));
+                                }
+                            }
+                        }
+                    } else {
+                        $this->buffer .= $text[$i];
+                    }
+                    break;
+            }
+        }
+
+        $this->filterLine(true);
+
+        return $this->tokens;
+    }
+
+    /**
+     * Helper function to reset tokenizer internal state.
+     */
+    private function reset()
+    {
+        $this->state     = self::IN_TEXT;
+        $this->tagType   = null;
+        $this->tag       = null;
+        $this->buffer    = '';
+        $this->tokens    = array();
+        $this->seenTag   = false;
+        $this->lineStart = 0;
+        $this->otag      = '{{';
+        $this->ctag      = '}}';
+    }
+
+    /**
+     * Flush the current buffer to a token.
+     */
+    private function flushBuffer()
+    {
+        if (!empty($this->buffer)) {
+            $this->tokens[] = array(self::TYPE  => self::T_TEXT, self::VALUE => $this->buffer);
+            $this->buffer   = '';
+        }
+    }
+
+    /**
+     * Test whether the current line is entirely made up of whitespace.
+     *
+     * @return boolean True if the current line is all whitespace
+     */
+    private function lineIsWhitespace()
+    {
+        $tokensCount = count($this->tokens);
+        for ($j = $this->lineStart; $j < $tokensCount; $j++) {
+            $token = $this->tokens[$j];
+            if (isset(self::$tagTypes[$token[self::TYPE]])) {
+                if (isset(self::$interpolatedTags[$token[self::TYPE]])) {
+                    return false;
+                }
+            } elseif ($token[self::TYPE] == self::T_TEXT) {
+                if (preg_match('/\S/', $token[self::VALUE])) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Filter out whitespace-only lines and store indent levels for partials.
+     *
+     * @param bool $noNewLine Suppress the newline? (default: false)
+     */
+    private function filterLine($noNewLine = false)
+    {
+        $this->flushBuffer();
+        if ($this->seenTag && $this->lineIsWhitespace()) {
+            $tokensCount = count($this->tokens);
+            for ($j = $this->lineStart; $j < $tokensCount; $j++) {
+                if ($this->tokens[$j][self::TYPE] == self::T_TEXT) {
+                    if (isset($this->tokens[$j+1]) && $this->tokens[$j+1][self::TYPE] == self::T_PARTIAL) {
+                        $this->tokens[$j+1][self::INDENT] = $this->tokens[$j][self::VALUE];
+                    }
+
+                    $this->tokens[$j] = null;
+                }
+            }
+        } elseif (!$noNewLine) {
+            $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n");
+        }
+
+        $this->seenTag   = false;
+        $this->lineStart = count($this->tokens);
+    }
+
+    /**
+     * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
+     *
+     * @param string $text  Mustache template source
+     * @param int    $index Current tokenizer index
+     *
+     * @return int New index value
+     */
+    private function changeDelimiters($text, $index)
+    {
+        $startIndex = strpos($text, '=', $index) + 1;
+        $close      = '='.$this->ctag;
+        $closeIndex = strpos($text, $close, $index);
+
+        list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex)));
+        $this->otag = $otag;
+        $this->ctag = $ctag;
+
+        return $closeIndex + strlen($close) - 1;
+    }
+
+    /**
+     * Test whether it's time to change tags.
+     *
+     * @param string $tag   Current tag name
+     * @param string $text  Mustache template source
+     * @param int    $index Current tokenizer index
+     *
+     * @return boolean True if this is a closing section tag
+     */
+    private function tagChange($tag, $text, $index)
+    {
+        return substr($text, $index, strlen($tag)) === $tag;
+    }
+}

+ 36 - 0
test/Mustache/Test/AutoloaderTest.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_AutoloaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testRegister()
+    {
+        $loader = Mustache_Autoloader::register();
+        $this->assertTrue(spl_autoload_unregister(array($loader, 'autoload')));
+    }
+
+    public function testAutoloader()
+    {
+        $loader = new Mustache_Autoloader(dirname(__FILE__).'/../../fixtures/autoloader');
+
+        $this->assertNull($loader->autoload('NonMustacheClass'));
+        $this->assertFalse(class_exists('NonMustacheClass'));
+
+        $loader->autoload('Mustache_Foo');
+        $this->assertTrue(class_exists('Mustache_Foo'));
+
+        $loader->autoload('\Mustache_Bar');
+        $this->assertTrue(class_exists('Mustache_Bar'));
+    }
+}

+ 103 - 0
test/Mustache/Test/CompilerTest.php

@@ -0,0 +1,103 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @dataProvider getCompileValues
+     */
+    public function testCompile($source, array $tree, $name, $customEscaper, $charset, $expected)
+    {
+        $compiler = new Mustache_Compiler;
+
+        $compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset);
+        foreach ($expected as $contains) {
+            $this->assertContains($contains, $compiled);
+        }
+    }
+
+    public function getCompileValues()
+    {
+        return array(
+            array('', array(), 'Banana', false, 'ISO-8859-1', array(
+                "\nclass Banana extends Mustache_Template",
+                'return htmlspecialchars($buffer, ENT_COMPAT, \'ISO-8859-1\');',
+                'return $buffer;',
+            )),
+
+            array('', array($this->createTextToken('TEXT')), 'Monkey', false, 'UTF-8', array(
+                "\nclass Monkey extends Mustache_Template",
+                'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
+                '$buffer .= $indent . \'TEXT\';',
+                'return $buffer;',
+            )),
+
+            array('', array($this->createTextToken('TEXT')), 'Monkey', true, 'ISO-8859-1', array(
+                "\nclass Monkey extends Mustache_Template",
+                '$buffer .= $indent . \'TEXT\';',
+                'return call_user_func($this->mustache->getEscape(), $buffer);',
+                'return $buffer;',
+            )),
+
+            array(
+                '',
+                array(
+                    $this->createTextToken('foo'),
+                    $this->createTextToken("\n"),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME => 'name',
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME => '.',
+                    ),
+                    $this->createTextToken("'bar'"),
+                ),
+                'Monkey',
+                false,
+                'UTF-8',
+                array(
+                    "\nclass Monkey extends Mustache_Template",
+                    '$buffer .= $indent . \'foo\'',
+                    '$buffer .= "\n"',
+                    '$value = $context->find(\'name\');',
+                    '$buffer .= htmlspecialchars($value, ENT_COMPAT, \'UTF-8\');',
+                    '$value = $context->last();',
+                    '$buffer .= \'\\\'bar\\\'\';',
+                    'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
+                    'return $buffer;',
+                )
+            ),
+        );
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testCompilerThrowsUnknownNodeTypeException()
+    {
+        $compiler = new Mustache_Compiler;
+        $compiler->compile('', array(array(Mustache_Tokenizer::TYPE => 'invalid')), 'SomeClass');
+    }
+
+    private function createTextToken($value)
+    {
+        return array(
+            Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+            Mustache_Tokenizer::VALUE => $value,
+        );
+    }
+}

+ 119 - 0
test/Mustache/Test/ContextTest.php

@@ -0,0 +1,119 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_ContextTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $one = new Mustache_Context;
+        $this->assertSame('', $one->find('foo'));
+        $this->assertSame('', $one->find('bar'));
+
+        $two = new Mustache_Context(array(
+            'foo' => 'FOO',
+            'bar' => '<BAR>'
+        ));
+        $this->assertEquals('FOO', $two->find('foo'));
+        $this->assertEquals('<BAR>', $two->find('bar'));
+
+        $obj = new StdClass;
+        $obj->name = 'NAME';
+        $three = new Mustache_Context($obj);
+        $this->assertSame($obj, $three->last());
+        $this->assertEquals('NAME', $three->find('name'));
+    }
+
+    public function testPushPopAndLast()
+    {
+        $context = new Mustache_Context;
+        $this->assertFalse($context->last());
+
+        $dummy = new Mustache_Test_TestDummy;
+        $context->push($dummy);
+        $this->assertSame($dummy, $context->last());
+        $this->assertSame($dummy, $context->pop());
+        $this->assertFalse($context->last());
+
+        $obj = new StdClass;
+        $context->push($dummy);
+        $this->assertSame($dummy, $context->last());
+        $context->push($obj);
+        $this->assertSame($obj, $context->last());
+        $this->assertSame($obj, $context->pop());
+        $this->assertSame($dummy, $context->pop());
+        $this->assertFalse($context->last());
+    }
+
+    public function testFind()
+    {
+        $context = new Mustache_Context;
+
+        $dummy = new Mustache_Test_TestDummy;
+
+        $obj = new StdClass;
+        $obj->name = 'obj';
+
+        $arr = array(
+            'a' => array('b' => array('c' => 'see')),
+            'b' => 'bee',
+        );
+
+        $string = 'some arbitrary string';
+
+        $context->push($dummy);
+        $this->assertEquals('dummy', $context->find('name'));
+
+        $context->push($obj);
+        $this->assertEquals('obj', $context->find('name'));
+
+        $context->pop();
+        $this->assertEquals('dummy', $context->find('name'));
+
+        $dummy->name = 'dummyer';
+        $this->assertEquals('dummyer', $context->find('name'));
+
+        $context->push($arr);
+        $this->assertEquals('bee', $context->find('b'));
+        $this->assertEquals('see', $context->findDot('a.b.c'));
+
+        $dummy->name = 'dummy';
+
+        $context->push($string);
+        $this->assertSame($string, $context->last());
+        $this->assertEquals('dummy', $context->find('name'));
+        $this->assertEquals('see', $context->findDot('a.b.c'));
+        $this->assertEquals('<foo>', $context->find('foo'));
+        $this->assertEquals('<bar>', $context->findDot('bar'));
+    }
+}
+
+class Mustache_Test_TestDummy
+{
+    public $name = 'dummy';
+
+    public function __invoke()
+    {
+        // nothing
+    }
+
+    public static function foo()
+    {
+        return '<foo>';
+    }
+
+    public function bar()
+    {
+        return '<bar>';
+    }
+}

+ 256 - 0
test/Mustache/Test/EngineTest.php

@@ -0,0 +1,256 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
+{
+
+    private static $tempDir;
+
+    public static function setUpBeforeClass()
+    {
+        self::$tempDir = sys_get_temp_dir() . '/mustache_test';
+        if (file_exists(self::$tempDir)) {
+            self::rmdir(self::$tempDir);
+        }
+    }
+
+    public function testConstructor()
+    {
+        $loader         = new Mustache_Loader_StringLoader;
+        $partialsLoader = new Mustache_Loader_ArrayLoader;
+        $mustache       = new Mustache_Engine(array(
+            'template_class_prefix' => '__whot__',
+            'cache' => self::$tempDir,
+            'loader' => $loader,
+            'partials_loader' => $partialsLoader,
+            'partials' => array(
+                'foo' => '{{ foo }}',
+            ),
+            'helpers' => array(
+                'foo' => array($this, 'getFoo'),
+                'bar' => 'BAR',
+            ),
+            'escape'  => 'strtoupper',
+            'charset' => 'ISO-8859-1',
+        ));
+
+        $this->assertSame($loader, $mustache->getLoader());
+        $this->assertSame($partialsLoader, $mustache->getPartialsLoader());
+        $this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
+        $this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}'));
+        $this->assertEquals('strtoupper', $mustache->getEscape());
+        $this->assertEquals('ISO-8859-1', $mustache->getCharset());
+        $this->assertTrue($mustache->hasHelper('foo'));
+        $this->assertTrue($mustache->hasHelper('bar'));
+        $this->assertFalse($mustache->hasHelper('baz'));
+    }
+
+    public static function getFoo()
+    {
+        return 'foo';
+    }
+
+    public function testRender()
+    {
+        $source = '{{ foo }}';
+        $data   = array('bar' => 'baz');
+        $output = 'TEH OUTPUT';
+
+        $template = $this->getMockBuilder('Mustache_Template')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $mustache = new MustacheStub;
+        $mustache->template = $template;
+
+        $template->expects($this->once())
+            ->method('render')
+            ->with($data)
+            ->will($this->returnValue($output));
+
+        $this->assertEquals($output, $mustache->render($source, $data));
+        $this->assertEquals($source, $mustache->source);
+    }
+
+    public function testSettingServices()
+    {
+        $loader    = new Mustache_Loader_StringLoader;
+        $tokenizer = new Mustache_Tokenizer;
+        $parser    = new Mustache_Parser;
+        $compiler  = new Mustache_Compiler;
+        $mustache  = new Mustache_Engine;
+
+        $this->assertNotSame($loader, $mustache->getLoader());
+        $mustache->setLoader($loader);
+        $this->assertSame($loader, $mustache->getLoader());
+
+        $this->assertNotSame($loader, $mustache->getPartialsLoader());
+        $mustache->setPartialsLoader($loader);
+        $this->assertSame($loader, $mustache->getPartialsLoader());
+
+        $this->assertNotSame($tokenizer, $mustache->getTokenizer());
+        $mustache->setTokenizer($tokenizer);
+        $this->assertSame($tokenizer, $mustache->getTokenizer());
+
+        $this->assertNotSame($parser, $mustache->getParser());
+        $mustache->setParser($parser);
+        $this->assertSame($parser, $mustache->getParser());
+
+        $this->assertNotSame($compiler, $mustache->getCompiler());
+        $mustache->setCompiler($compiler);
+        $this->assertSame($compiler, $mustache->getCompiler());
+    }
+
+    /**
+     * @group functional
+     */
+    public function testCache()
+    {
+        $mustache = new Mustache_Engine(array(
+            'template_class_prefix' => '__whot__',
+            'cache' => self::$tempDir,
+        ));
+
+        $source    = '{{ foo }}';
+        $template  = $mustache->loadTemplate($source);
+        $className = $mustache->getTemplateClassName($source);
+        $fileName  = self::$tempDir . '/' . $className . '.php';
+        $this->assertInstanceOf($className, $template);
+        $this->assertFileExists($fileName);
+        $this->assertContains("\nclass $className extends Mustache_Template", file_get_contents($fileName));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     * @dataProvider getBadEscapers
+     */
+    public function testNonCallableEscapeThrowsException($escape)
+    {
+        new Mustache_Engine(array('escape' => $escape));
+    }
+
+    public function getBadEscapers()
+    {
+        return array(
+            array('nothing'),
+            array('foo', 'bar'),
+        );
+    }
+
+    /**
+     * @expectedException RuntimeException
+     */
+    public function testImmutablePartialsLoadersThrowException()
+    {
+        $mustache = new Mustache_Engine(array(
+            'partials_loader' => new Mustache_Loader_StringLoader,
+        ));
+
+        $mustache->setPartials(array('foo' => '{{ foo }}'));
+    }
+
+    public function testMissingPartialsTreatedAsEmptyString()
+    {
+        $mustache = new Mustache_Engine(array(
+            'partials_loader' => new Mustache_Loader_ArrayLoader(array(
+                'foo' => 'FOO',
+                'baz' => 'BAZ',
+            ))
+        ));
+
+        $this->assertEquals('FOOBAZ', $mustache->render('{{>foo}}{{>bar}}{{>baz}}', array()));
+    }
+
+    public function testHelpers()
+    {
+        $foo = array($this, 'getFoo');
+        $bar = 'BAR';
+        $mustache = new Mustache_Engine(array('helpers' => array(
+            'foo' => $foo,
+            'bar' => $bar,
+        )));
+
+        $helpers = $mustache->getHelpers();
+        $this->assertTrue($mustache->hasHelper('foo'));
+        $this->assertTrue($mustache->hasHelper('bar'));
+        $this->assertTrue($helpers->has('foo'));
+        $this->assertTrue($helpers->has('bar'));
+        $this->assertSame($foo, $mustache->getHelper('foo'));
+        $this->assertSame($bar, $mustache->getHelper('bar'));
+
+        $mustache->removeHelper('bar');
+        $this->assertFalse($mustache->hasHelper('bar'));
+        $mustache->addHelper('bar', $bar);
+        $this->assertSame($bar, $mustache->getHelper('bar'));
+
+        $baz = array($this, 'wrapWithUnderscores');
+        $this->assertFalse($mustache->hasHelper('baz'));
+        $this->assertFalse($helpers->has('baz'));
+
+        $mustache->addHelper('baz', $baz);
+        $this->assertTrue($mustache->hasHelper('baz'));
+        $this->assertTrue($helpers->has('baz'));
+
+        // ... and a functional test
+        $tpl = $mustache->loadTemplate('{{foo}} - {{bar}} - {{#baz}}qux{{/baz}}');
+        $this->assertEquals('foo - BAR - __qux__', $tpl->render());
+        $this->assertEquals('foo - BAR - __qux__', $tpl->render(array('qux' => "won't mess things up")));
+    }
+
+    public static function wrapWithUnderscores($text)
+    {
+        return '__'.$text.'__';
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testSetHelpersThrowsExceptions()
+    {
+        $mustache = new Mustache_Engine;
+        $mustache->setHelpers('monkeymonkeymonkey');
+    }
+
+    private static function rmdir($path)
+    {
+        $path = rtrim($path, '/').'/';
+        $handle = opendir($path);
+        while (($file = readdir($handle)) !== false) {
+            if ($file == '.' || $file == '..') {
+                continue;
+            }
+
+            $fullpath = $path.$file;
+            if (is_dir($fullpath)) {
+                self::rmdir($fullpath);
+            } else {
+                unlink($fullpath);
+            }
+        }
+
+        closedir($handle);
+        rmdir($path);
+    }
+}
+
+class MustacheStub extends Mustache_Engine {
+    public $source;
+    public $template;
+    public function loadTemplate($source)
+    {
+        $this->source = $source;
+
+        return $this->template;
+    }
+}

+ 71 - 0
test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php

@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group lambdas
+ * @group functional
+ */
+class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase {
+
+	private $mustache;
+
+	public function setUp() {
+		$this->mustache = new Mustache_Engine;
+	}
+
+	public function testAnonymousFunctionSectionCallback() {
+		$tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}');
+
+		$foo = new Mustache_Test_FiveThree_Functional_Foo;
+		$foo->name = 'Mario';
+		$foo->wrapper = function($text) {
+			return sprintf('<div class="anonymous">%s</div>', $text);
+		};
+
+		$this->assertEquals(sprintf('<div class="anonymous">%s</div>', $foo->name), $tpl->render($foo));
+	}
+
+	public function testSectionCallback() {
+		$one = $this->mustache->loadTemplate('{{name}}');
+		$two = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}');
+
+		$foo = new Mustache_Test_FiveThree_Functional_Foo;
+		$foo->name = 'Luigi';
+
+		$this->assertEquals($foo->name, $one->render($foo));
+		$this->assertEquals(sprintf('<em>%s</em>', $foo->name), $two->render($foo));
+	}
+
+	public function testViewArrayAnonymousSectionCallback() {
+		$tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}');
+
+		$data = array(
+			'name' => 'Bob',
+			'wrap' => function($text) {
+				return sprintf('[[%s]]', $text);
+			}
+		);
+
+		$this->assertEquals(sprintf('[[%s]]', $data['name']), $tpl->render($data));
+	}
+}
+
+class Mustache_Test_FiveThree_Functional_Foo {
+	public $name  = 'Justin';
+	public $lorem = 'Lorem ipsum dolor sit amet,';
+	public $wrap;
+
+	public function __construct() {
+		$this->wrap = function($text) {
+			return sprintf('<em>%s</em>', $text);
+		};
+	}
+}

+ 114 - 0
test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php

@@ -0,0 +1,114 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A PHPUnit test case wrapping the Mustache Spec
+ *
+ * @group mustache-spec
+ * @group functional
+ */
+class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCase {
+
+	private static $mustache;
+
+	public static function setUpBeforeClass() {
+		self::$mustache = new Mustache_Engine;
+	}
+
+	/**
+	 * For some reason data providers can't mark tests skipped, so this test exists
+	 * simply to provide a 'skipped' test if the `spec` submodule isn't initialized.
+	 */
+	public function testSpecInitialized() {
+		if (!file_exists(dirname(__FILE__).'/../../../../../vendor/spec/specs/')) {
+			$this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"');
+		}
+	}
+
+	/**
+	 * @group lambdas
+	 * @dataProvider loadLambdasSpec
+	 */
+	public function testLambdasSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($this->prepareLambdasSpec($data)), $desc);
+	}
+
+	public function loadLambdasSpec() {
+		return $this->loadSpec('~lambdas');
+	}
+
+	/**
+	 * Extract and lambdafy any 'lambda' values found in the $data array.
+	 */
+	private function prepareLambdasSpec($data) {
+		foreach ($data as $key => $val) {
+			if ($key === 'lambda') {
+				if (!isset($val['php'])) {
+					$this->markTestSkipped(sprintf('PHP lambda test not implemented for this test.'));
+				}
+
+				$func = $val['php'];
+				$data[$key] = function($text = null) use ($func) {
+					return eval($func);
+				};
+			} else if (is_array($val)) {
+				$data[$key] = $this->prepareLambdasSpec($val);
+			}
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Data provider for the mustache spec test.
+	 *
+	 * Loads YAML files from the spec and converts them to PHPisms.
+	 *
+	 * @access public
+	 * @return array
+	 */
+	private function loadSpec($name) {
+		$filename = dirname(__FILE__) . '/../../../../../vendor/spec/specs/' . $name . '.yml';
+		if (!file_exists($filename)) {
+			return array();
+		}
+
+		$data = array();
+		$yaml = new sfYamlParser;
+		$file = file_get_contents($filename);
+
+		// @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain.
+		if ($name === '~lambdas') {
+			$file = str_replace(" !code\n", "\n", $file);
+		}
+
+		$spec = $yaml->parse($file);
+
+		foreach ($spec['tests'] as $test) {
+			$data[] = array(
+				$test['name'] . ': ' . $test['desc'],
+				$test['template'],
+				isset($test['partials']) ? $test['partials'] : array(),
+				$test['data'],
+				$test['expected'],
+			);
+		}
+
+		return $data;
+	}
+
+	private static function loadTemplate($source, $partials) {
+		self::$mustache->setPartials($partials);
+
+		return self::$mustache->loadTemplate($source);
+	}
+}

+ 40 - 0
test/Mustache/Test/Functional/CallTest.php

@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group magic_methods
+ * @group functional
+ */
+class Mustache_Test_Functional_CallTest extends PHPUnit_Framework_TestCase
+{
+
+    public function testCallEatsContext()
+    {
+        $m = new Mustache_Engine;
+        $tpl = $m->loadTemplate('{{# foo }}{{ label }}: {{ name }}{{/ foo }}');
+
+        $foo = new Mustache_Test_Functional_ClassWithCall();
+        $foo->name = 'Bob';
+
+        $data = array('label' => 'name', 'foo' => $foo);
+
+        $this->assertEquals('name: Bob', $tpl->render($data));
+    }
+}
+
+class Mustache_Test_Functional_ClassWithCall
+{
+    public $name;
+    public function __call($method, $args)
+    {
+        return 'unknown value';
+    }
+}

+ 142 - 0
test/Mustache/Test/Functional/ExamplesTest.php

@@ -0,0 +1,142 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group examples
+ * @group functional
+ */
+class Mustache_Test_Functional_ExamplesTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Test everything in the `examples` directory.
+     *
+     * @dataProvider getExamples
+     *
+     * @param string $context
+     * @param string $source
+     * @param array  $partials
+     * @param string $expected
+     */
+    public function testExamples($context, $source, $partials, $expected)
+    {
+        $mustache = new Mustache_Engine(array(
+            'partials' => $partials
+        ));
+        $this->assertEquals($expected, $mustache->loadTemplate($source)->render($context));
+    }
+
+    /**
+     * Data provider for testExamples method.
+     *
+     * Loads examples from the test fixtures directory.
+     *
+     * This examples directory should contain any number of subdirectories, each of which contains
+     * three files: one Mustache class (.php), one Mustache template (.mustache), and one output file
+     * (.txt). Optionally, the directory may contain a folder full of partials.
+     *
+     * @return array
+     */
+    public function getExamples()
+    {
+        $path     = realpath(dirname(__FILE__).'/../../../fixtures/examples');
+        $examples = array();
+
+        $handle   = opendir($path);
+        while (($file = readdir($handle)) !== false) {
+            if ($file == '.' || $file == '..') {
+                continue;
+            }
+
+            $fullpath = $path.'/'.$file;
+            if (is_dir($fullpath)) {
+                $examples[$file] = $this->loadExample($fullpath);
+            }
+        }
+        closedir($handle);
+
+        return $examples;
+    }
+
+    /**
+     * Helper method to load an example given the full path.
+     *
+     * @param string $path
+     *
+     * @return array arguments for testExamples
+     */
+    private function loadExample($path)
+    {
+        $context  = null;
+        $source   = null;
+        $partials = array();
+        $expected = null;
+
+        $handle = opendir($path);
+        while (($file = readdir($handle)) !== false) {
+            $fullpath = $path.'/'.$file;
+            $info = pathinfo($fullpath);
+
+            if (is_dir($fullpath) && $info['basename'] == 'partials') {
+                // load partials
+                $partials = $this->loadPartials($fullpath);
+            } elseif (is_file($fullpath)) {
+                // load other files
+                switch ($info['extension']) {
+                    case 'php':
+                        require_once($fullpath);
+                        $context = new $info['filename'];
+                        break;
+
+                    case 'mustache':
+                        $source   = file_get_contents($fullpath);
+                        break;
+
+                    case 'txt':
+                        $expected = file_get_contents($fullpath);
+                        break;
+                }
+            }
+        }
+        closedir($handle);
+
+        return array($context, $source, $partials, $expected);
+    }
+
+    /**
+     * Helper method to load partials given an example directory.
+     *
+     * @param string $path
+     *
+     * @return array  $partials
+     */
+    private function loadPartials($path)
+    {
+        $partials = array();
+
+        $handle = opendir($path);
+        while (($file = readdir($handle)) !== false) {
+            if ($file == '.' || $file == '..') {
+                continue;
+            }
+
+            $fullpath = $path.'/'.$file;
+            $info = pathinfo($fullpath);
+
+            if ($info['extension'] === 'mustache') {
+                $partials[$info['filename']] = file_get_contents($fullpath);
+            }
+        }
+        closedir($handle);
+
+        return $partials;
+    }
+}

+ 106 - 0
test/Mustache/Test/Functional/HigherOrderSectionsTest.php

@@ -0,0 +1,106 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group lambdas
+ * @group functional
+ */
+class Mustache_Test_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase
+{
+
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->mustache = new Mustache_Engine;
+    }
+
+    public function testRuntimeSectionCallback()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#doublewrap}}{{name}}{{/doublewrap}}');
+
+        $foo = new Mustache_Test_Functional_Foo;
+        $foo->doublewrap = array($foo, 'wrapWithBoth');
+
+        $this->assertEquals(sprintf('<strong><em>%s</em></strong>', $foo->name), $tpl->render($foo));
+    }
+
+    public function testStaticSectionCallback()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#trimmer}}    {{name}}    {{/trimmer}}');
+
+        $foo = new Mustache_Test_Functional_Foo;
+        $foo->trimmer = array(get_class($foo), 'staticTrim');
+
+        $this->assertEquals($foo->name, $tpl->render($foo));
+    }
+
+    public function testViewArraySectionCallback()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#trim}}    {{name}}    {{/trim}}');
+
+        $foo = new Mustache_Test_Functional_Foo;
+
+        $data = array(
+            'name' => 'Bob',
+            'trim' => array(get_class($foo), 'staticTrim'),
+        );
+
+        $this->assertEquals($data['name'], $tpl->render($data));
+    }
+
+    public function testMonsters()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#title}}{{title}} {{/title}}{{name}}');
+
+        $frank = new Mustache_Test_Functional_Monster();
+        $frank->title = 'Dr.';
+        $frank->name  = 'Frankenstein';
+        $this->assertEquals('Dr. Frankenstein', $tpl->render($frank));
+
+        $dracula = new Mustache_Test_Functional_Monster();
+        $dracula->title = 'Count';
+        $dracula->name  = 'Dracula';
+        $this->assertEquals('Count Dracula', $tpl->render($dracula));
+    }
+}
+
+class Mustache_Test_Functional_Foo
+{
+    public $name = 'Justin';
+    public $lorem = 'Lorem ipsum dolor sit amet,';
+
+    public function wrapWithEm($text)
+    {
+        return sprintf('<em>%s</em>', $text);
+    }
+
+    public function wrapWithStrong($text)
+    {
+        return sprintf('<strong>%s</strong>', $text);
+    }
+
+    public function wrapWithBoth($text)
+    {
+        return self::wrapWithStrong(self::wrapWithEm($text));
+    }
+
+    public static function staticTrim($text)
+    {
+        return trim($text);
+    }
+}
+
+class Mustache_Test_Functional_Monster
+{
+    public $title;
+    public $name;
+}

+ 152 - 0
test/Mustache/Test/Functional/MustacheInjectionTest.php

@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group mustache_injection
+ * @group functional
+ */
+class Mustache_Test_Functional_MustacheInjectionTest extends PHPUnit_Framework_TestCase
+{
+
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->mustache = new Mustache_Engine;
+    }
+
+    // interpolation
+
+    public function testInterpolationInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{ a }}');
+
+        $data = array(
+            'a' => '{{ b }}',
+            'b' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ b }}', $tpl->render($data));
+    }
+
+    public function testUnescapedInterpolationInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{{ a }}}');
+
+        $data = array(
+            'a' => '{{ b }}',
+            'b' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ b }}', $tpl->render($data));
+    }
+
+
+    // sections
+
+    public function testSectionInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{# a }}{{ b }}{{/ a }}');
+
+        $data = array(
+            'a' => true,
+            'b' => '{{ c }}',
+            'c' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ c }}', $tpl->render($data));
+    }
+
+    public function testUnescapedSectionInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{# a }}{{{ b }}}{{/ a }}');
+
+        $data = array(
+            'a' => true,
+            'b' => '{{ c }}',
+            'c' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ c }}', $tpl->render($data));
+    }
+
+
+    // partials
+
+    public function testPartialInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{> partial }}');
+        $this->mustache->setPartials(array(
+            'partial' => '{{ a }}',
+        ));
+
+        $data = array(
+            'a' => '{{ b }}',
+            'b' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ b }}', $tpl->render($data));
+    }
+
+    public function testPartialUnescapedInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{> partial }}');
+        $this->mustache->setPartials(array(
+            'partial' => '{{{ a }}}',
+        ));
+
+        $data = array(
+            'a' => '{{ b }}',
+            'b' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ b }}', $tpl->render($data));
+    }
+
+
+    // lambdas
+
+    public function testLambdaInterpolationInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{ a }}');
+
+        $data = array(
+            'a' => array($this, 'lambdaInterpolationCallback'),
+            'b' => '{{ c }}',
+            'c' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ c }}', $tpl->render($data));
+    }
+
+    public static function lambdaInterpolationCallback()
+    {
+        return '{{ b }}';
+    }
+
+    public function testLambdaSectionInjection()
+    {
+        $tpl = $this->mustache->loadTemplate('{{# a }}b{{/ a }}');
+
+        $data = array(
+            'a' => array($this, 'lambdaSectionCallback'),
+            'b' => '{{ c }}',
+            'c' => 'FAIL'
+        );
+
+        $this->assertEquals('{{ c }}', $tpl->render($data));
+    }
+
+    public static function lambdaSectionCallback($text)
+    {
+        return '{{ ' . $text . ' }}';
+    }
+}

+ 175 - 0
test/Mustache/Test/Functional/MustacheSpecTest.php

@@ -0,0 +1,175 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A PHPUnit test case wrapping the Mustache Spec
+ *
+ * @group mustache-spec
+ * @group functional
+ */
+class Mustache_Test_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCase
+{
+
+    private static $mustache;
+
+    public static function setUpBeforeClass()
+    {
+        self::$mustache = new Mustache_Engine;
+    }
+
+    /**
+     * For some reason data providers can't mark tests skipped, so this test exists
+     * simply to provide a 'skipped' test if the `spec` submodule isn't initialized.
+     */
+    public function testSpecInitialized()
+    {
+        if (!file_exists(dirname(__FILE__).'/../../../../vendor/spec/specs/')) {
+            $this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"');
+        }
+    }
+
+    /**
+     * @group comments
+     * @dataProvider loadCommentSpec
+     */
+    public function testCommentSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadCommentSpec()
+    {
+        return $this->loadSpec('comments');
+    }
+
+    /**
+     * @group delimiters
+     * @dataProvider loadDelimitersSpec
+     */
+    public function testDelimitersSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadDelimitersSpec()
+    {
+        return $this->loadSpec('delimiters');
+    }
+
+    /**
+     * @group interpolation
+     * @dataProvider loadInterpolationSpec
+     */
+    public function testInterpolationSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadInterpolationSpec()
+    {
+        return $this->loadSpec('interpolation');
+    }
+
+    /**
+     * @group inverted
+     * @group inverted-sections
+     * @dataProvider loadInvertedSpec
+     */
+    public function testInvertedSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadInvertedSpec()
+    {
+        return $this->loadSpec('inverted');
+    }
+
+    /**
+     * @group partials
+     * @dataProvider loadPartialsSpec
+     */
+    public function testPartialsSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadPartialsSpec()
+    {
+        return $this->loadSpec('partials');
+    }
+
+    /**
+     * @group sections
+     * @dataProvider loadSectionsSpec
+     */
+    public function testSectionsSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadSectionsSpec()
+    {
+        return $this->loadSpec('sections');
+    }
+
+    /**
+     * Data provider for the mustache spec test.
+     *
+     * Loads YAML files from the spec and converts them to PHPisms.
+     *
+     * @access public
+     * @return array
+     */
+    private function loadSpec($name)
+    {
+        $filename = dirname(__FILE__) . '/../../../../vendor/spec/specs/' . $name . '.yml';
+        if (!file_exists($filename)) {
+            return array();
+        }
+
+        $data = array();
+        $yaml = new sfYamlParser;
+        $file = file_get_contents($filename);
+
+        // @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain.
+        if ($name === '~lambdas') {
+            $file = str_replace(" !code\n", "\n", $file);
+        }
+
+        $spec = $yaml->parse($file);
+
+        foreach ($spec['tests'] as $test) {
+            $data[] = array(
+                $test['name'] . ': ' . $test['desc'],
+                $test['template'],
+                isset($test['partials']) ? $test['partials'] : array(),
+                $test['data'],
+                $test['expected'],
+            );
+        }
+
+        return $data;
+    }
+
+    private static function loadTemplate($source, $partials)
+    {
+        self::$mustache->setPartials($partials);
+
+        return self::$mustache->loadTemplate($source);
+    }
+}

+ 110 - 0
test/Mustache/Test/Functional/ObjectSectionTest.php

@@ -0,0 +1,110 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group sections
+ * @group functional
+ */
+class Mustache_Test_Functional_ObjectSectionTest extends PHPUnit_Framework_TestCase
+{
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->mustache = new Mustache_Engine;
+    }
+
+    public function testBasicObject()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+        $this->assertEquals('Foo', $tpl->render(new Mustache_Test_Functional_Alpha));
+    }
+
+    /**
+     * @group magic_methods
+     */
+    public function testObjectWithGet()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+        $this->assertEquals('Foo', $tpl->render(new Mustache_Test_Functional_Beta));
+    }
+
+    /**
+     * @group magic_methods
+     */
+    public function testSectionObjectWithGet()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#bar}}{{#foo}}{{name}}{{/foo}}{{/bar}}');
+        $this->assertEquals('Foo', $tpl->render(new Mustache_Test_Functional_Gamma));
+    }
+
+    public function testSectionObjectWithFunction()
+    {
+        $tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+        $alpha = new Mustache_Test_Functional_Alpha;
+        $alpha->foo = new Mustache_Test_Functional_Delta;
+        $this->assertEquals('Foo', $tpl->render($alpha));
+    }
+}
+
+class Mustache_Test_Functional_Alpha
+{
+    public $foo;
+
+    public function __construct()
+    {
+        $this->foo = new StdClass();
+        $this->foo->name = 'Foo';
+        $this->foo->number = 1;
+    }
+}
+
+class Mustache_Test_Functional_Beta
+{
+    protected $_data = array();
+
+    public function __construct()
+    {
+        $this->_data['foo'] = new StdClass();
+        $this->_data['foo']->name = 'Foo';
+        $this->_data['foo']->number = 1;
+    }
+
+    public function __isset($name)
+    {
+        return array_key_exists($name, $this->_data);
+    }
+
+    public function __get($name)
+    {
+        return $this->_data[$name];
+    }
+}
+
+class Mustache_Test_Functional_Gamma
+{
+    public $bar;
+
+    public function __construct()
+    {
+        $this->bar = new Mustache_Test_Functional_Beta;
+    }
+}
+
+class Mustache_Test_Functional_Delta
+{
+    protected $_name = 'Foo';
+
+    public function name()
+    {
+        return $this->_name;
+    }
+}

+ 163 - 0
test/Mustache/Test/HelperCollectionTest.php

@@ -0,0 +1,163 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Test_HelperCollectionTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $foo = array($this, 'getFoo');
+        $bar = 'BAR';
+
+        $helpers = new Mustache_HelperCollection(array(
+            'foo' => $foo,
+            'bar' => $bar,
+        ));
+
+        $this->assertSame($foo, $helpers->get('foo'));
+        $this->assertSame($bar, $helpers->get('bar'));
+    }
+
+    public static function getFoo()
+    {
+        echo 'foo';
+    }
+
+    public function testAccessorsAndMutators()
+    {
+        $foo = array($this, 'getFoo');
+        $bar = 'BAR';
+
+        $helpers = new Mustache_HelperCollection;
+        $this->assertTrue($helpers->isEmpty());
+        $this->assertFalse($helpers->has('foo'));
+        $this->assertFalse($helpers->has('bar'));
+
+        $helpers->add('foo', $foo);
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertTrue($helpers->has('foo'));
+        $this->assertFalse($helpers->has('bar'));
+
+        $helpers->add('bar', $bar);
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertTrue($helpers->has('foo'));
+        $this->assertTrue($helpers->has('bar'));
+
+        $helpers->remove('foo');
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertFalse($helpers->has('foo'));
+        $this->assertTrue($helpers->has('bar'));
+    }
+
+    public function testMagicMethods()
+    {
+        $foo = array($this, 'getFoo');
+        $bar = 'BAR';
+
+        $helpers = new Mustache_HelperCollection;
+        $this->assertTrue($helpers->isEmpty());
+        $this->assertFalse($helpers->has('foo'));
+        $this->assertFalse($helpers->has('bar'));
+        $this->assertFalse(isset($helpers->foo));
+        $this->assertFalse(isset($helpers->bar));
+
+        $helpers->foo = $foo;
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertTrue($helpers->has('foo'));
+        $this->assertFalse($helpers->has('bar'));
+        $this->assertTrue(isset($helpers->foo));
+        $this->assertFalse(isset($helpers->bar));
+
+        $helpers->bar = $bar;
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertTrue($helpers->has('foo'));
+        $this->assertTrue($helpers->has('bar'));
+        $this->assertTrue(isset($helpers->foo));
+        $this->assertTrue(isset($helpers->bar));
+
+        unset($helpers->foo);
+        $this->assertFalse($helpers->isEmpty());
+        $this->assertFalse($helpers->has('foo'));
+        $this->assertTrue($helpers->has('bar'));
+        $this->assertFalse(isset($helpers->foo));
+        $this->assertTrue(isset($helpers->bar));
+    }
+
+    /**
+     * @dataProvider getInvalidHelperArguments
+     */
+    public function testHelperCollectionIsntAfraidToThrowExceptions($helpers = array(), $actions = array(), $exception = null)
+    {
+        if ($exception) {
+            $this->setExpectedException($exception);
+        }
+
+        $helpers = new Mustache_HelperCollection($helpers);
+
+        foreach ($actions as $method => $args) {
+            call_user_func_array(array($helpers, $method), $args);
+        }
+    }
+
+    public function getInvalidHelperArguments()
+    {
+        return array(
+            array(
+                'not helpers',
+                array(),
+                'InvalidArgumentException',
+            ),
+            array(
+                array(),
+                array('get' => array('foo')),
+                'InvalidArgumentException',
+            ),
+            array(
+                array('foo' => 'FOO'),
+                array('get' => array('foo')),
+                null,
+            ),
+            array(
+                array('foo' => 'FOO'),
+                array('get' => array('bar')),
+                'InvalidArgumentException',
+            ),
+            array(
+                array('foo' => 'FOO'),
+                array(
+                    'add' => array('bar', 'BAR'),
+                    'get' => array('bar'),
+                ),
+                null,
+            ),
+            array(
+                array('foo' => 'FOO'),
+                array(
+                    'get'    => array('foo'),
+                    'remove' => array('foo'),
+                ),
+                null,
+            ),
+            array(
+                array('foo' => 'FOO'),
+                array(
+                    'remove' => array('foo'),
+                    'get'    => array('foo'),
+                ),
+                'InvalidArgumentException',
+            ),
+            array(
+                array(),
+                array('remove' => array('foo')),
+                'InvalidArgumentException',
+            ),
+        );
+    }
+}

+ 52 - 0
test/Mustache/Test/Loader/ArrayLoaderTest.php

@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_ArrayLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $loader = new Mustache_Loader_ArrayLoader(array(
+            'foo' => 'bar'
+        ));
+
+        $this->assertEquals('bar', $loader->load('foo'));
+    }
+
+    public function testSetAndLoadTemplates()
+    {
+        $loader = new Mustache_Loader_ArrayLoader(array(
+            'foo' => 'bar'
+        ));
+        $this->assertEquals('bar', $loader->load('foo'));
+
+        $loader->setTemplate('baz', 'qux');
+        $this->assertEquals('qux', $loader->load('baz'));
+
+        $loader->setTemplates(array(
+            'foo' => 'FOO',
+            'baz' => 'BAZ',
+        ));
+        $this->assertEquals('FOO', $loader->load('foo'));
+        $this->assertEquals('BAZ', $loader->load('baz'));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testMissingTemplatesThrowExceptions()
+    {
+        $loader = new Mustache_Loader_ArrayLoader;
+        $loader->load('not_a_real_template');
+    }
+}

+ 51 - 0
test/Mustache/Test/Loader/FilesystemLoaderTest.php

@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_FilesystemLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates');
+        $loader = new Mustache_Loader_FilesystemLoader($baseDir, array('extension' => '.ms'));
+        $this->assertEquals('alpha contents', $loader->load('alpha'));
+        $this->assertEquals('beta contents', $loader->load('beta.ms'));
+    }
+
+    public function testLoadTemplates()
+    {
+        $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates');
+        $loader = new Mustache_Loader_FilesystemLoader($baseDir);
+        $this->assertEquals('one contents', $loader->load('one'));
+        $this->assertEquals('two contents', $loader->load('two.mustache'));
+    }
+
+    /**
+     * @expectedException RuntimeException
+     */
+    public function testMissingBaseDirThrowsException()
+    {
+        $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/not_a_directory');
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testMissingTemplateThrowsException()
+    {
+        $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates');
+        $loader = new Mustache_Loader_FilesystemLoader($baseDir);
+
+        $loader->load('fake');
+    }
+}

+ 25 - 0
test/Mustache/Test/Loader/StringLoaderTest.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_StringLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testLoadTemplates()
+    {
+        $loader = new Mustache_Loader_StringLoader;
+
+        $this->assertEquals('foo', $loader->load('foo'));
+        $this->assertEquals('{{ bar }}', $loader->load('{{ bar }}'));
+        $this->assertEquals("\n{{! comment }}\n", $loader->load("\n{{! comment }}\n"));
+    }
+}

+ 182 - 0
test/Mustache/Test/ParserTest.php

@@ -0,0 +1,182 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @dataProvider getTokenSets
+     */
+    public function testParse($tokens, $expected)
+    {
+        $parser = new Mustache_Parser;
+        $this->assertEquals($expected, $parser->parse($tokens));
+    }
+
+    public function getTokenSets()
+    {
+        return array(
+            array(
+                array(),
+                array()
+            ),
+
+            array(
+                array(array(
+                    Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                    Mustache_Tokenizer::VALUE => 'text'
+                )),
+                array(array(
+                    Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                    Mustache_Tokenizer::VALUE => 'text'
+                )),
+            ),
+
+            array(
+                array(array(
+                    Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                    Mustache_Tokenizer::NAME => 'name'
+                )),
+                array(array(
+                    Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                    Mustache_Tokenizer::NAME => 'name'
+                )),
+            ),
+
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'foo'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
+                        Mustache_Tokenizer::INDEX => 123,
+                        Mustache_Tokenizer::NAME  => 'parent'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME  => 'name'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::INDEX => 456,
+                        Mustache_Tokenizer::NAME  => 'parent'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'bar'
+                    ),
+                ),
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'foo'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                        Mustache_Tokenizer::END   => 456,
+                        Mustache_Tokenizer::NODES => array(
+                            array(
+                                Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                                Mustache_Tokenizer::NAME => 'name'
+                            ),
+                        ),
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'bar'
+                    ),
+                ),
+            ),
+
+        );
+    }
+
+    /**
+     * @dataProvider getBadParseTrees
+     * @expectedException LogicException
+     */
+    public function testParserThrowsExceptions($tokens)
+    {
+        $parser = new Mustache_Parser;
+        $parser->parse($tokens);
+    }
+
+    public function getBadParseTrees()
+    {
+        return array(
+            // no close
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                ),
+            ),
+
+            // no close inverted
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                ),
+            ),
+
+            // no opening inverted
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                ),
+            ),
+
+            // weird nesting
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
+                        Mustache_Tokenizer::NAME  => 'child',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME  => 'child',
+                        Mustache_Tokenizer::INDEX => 123,
+                    ),
+                ),
+            ),
+        );
+    }
+}

+ 55 - 0
test/Mustache/Test/TemplateTest.php

@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_TemplateTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $mustache = new Mustache_Engine;
+        $template = new Mustache_Test_TemplateStub($mustache);
+        $this->assertSame($mustache, $template->getMustache());
+    }
+
+    public function testRendering()
+    {
+        $rendered = '<< wheee >>';
+        $mustache = new Mustache_Engine;
+        $template = new Mustache_Test_TemplateStub($mustache);
+        $template->rendered = $rendered;
+        $context  = new Mustache_Context;
+
+        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+            $this->assertEquals($rendered, $template());
+        }
+
+        $this->assertEquals($rendered, $template->render());
+        $this->assertEquals($rendered, $template->renderInternal($context));
+        $this->assertEquals($rendered, $template->render(array('foo' => 'bar')));
+    }
+}
+
+class Mustache_Test_TemplateStub extends Mustache_Template
+{
+    public $rendered;
+
+    public function getMustache()
+    {
+        return $this->mustache;
+    }
+
+    public function renderInternal(Mustache_Context $context, $indent = '', $escape = false)
+    {
+        return $this->rendered;
+    }
+}

+ 144 - 0
test/Mustache/Test/TokenizerTest.php

@@ -0,0 +1,144 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @dataProvider getTokens
+     */
+    public function testScan($text, $delimiters, $expected)
+    {
+        $tokenizer = new Mustache_Tokenizer;
+        $this->assertSame($expected, $tokenizer->scan($text, $delimiters));
+    }
+
+    public function getTokens()
+    {
+        return array(
+            array(
+                'text',
+                null,
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'text',
+                    ),
+                ),
+            ),
+
+            array(
+                'text',
+                '<<< >>>',
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => 'text',
+                    ),
+                ),
+            ),
+
+            array(
+                '{{ name }}',
+                null,
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME  => 'name',
+                        Mustache_Tokenizer::OTAG  => '{{',
+                        Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::INDEX => 10,
+                    )
+                )
+            ),
+
+            array(
+                '{{ name }}',
+                '<<< >>>',
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => '{{ name }}',
+                    ),
+                ),
+            ),
+
+            array(
+                '<<< name >>>',
+                '<<< >>>',
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME  => 'name',
+                        Mustache_Tokenizer::OTAG  => '<<<',
+                        Mustache_Tokenizer::CTAG  => '>>>',
+                        Mustache_Tokenizer::INDEX => 12,
+                    )
+                )
+            ),
+
+            array(
+                "{{{ a }}}\n{{# b }}  \n{{= | | =}}| c ||/ b |\n|{ d }|",
+                null,
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_UNESCAPED,
+                        Mustache_Tokenizer::NAME  => 'a',
+                        Mustache_Tokenizer::OTAG  => '{{',
+                        Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::INDEX => 8,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => "\n",
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
+                        Mustache_Tokenizer::NAME  => 'b',
+                        Mustache_Tokenizer::OTAG  => '{{',
+                        Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::INDEX => 18,
+                    ),
+                    null,
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME  => 'c',
+                        Mustache_Tokenizer::OTAG  => '|',
+                        Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::INDEX => 37,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME  => 'b',
+                        Mustache_Tokenizer::OTAG  => '|',
+                        Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::INDEX => 37,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::VALUE => "\n",
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_UNESCAPED,
+                        Mustache_Tokenizer::NAME  => 'd',
+                        Mustache_Tokenizer::OTAG  => '|',
+                        Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::INDEX => 51,
+                    ),
+
+                )
+            ),
+        );
+    }
+}

+ 0 - 24
test/MustacheCallTest.php

@@ -1,24 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-class MustacheCallTest extends PHPUnit_Framework_TestCase {
-
-	public function testCallEatsContext() {
-		$foo = new ClassWithCall();
-		$foo->name = 'Bob';
-
-		$template = '{{# foo }}{{ label }}: {{ name }}{{/ foo }}';
-		$data = array('label' => 'name', 'foo' => $foo);
-		$m = new Mustache($template, $data);
-
-		$this->assertEquals('name: Bob', $m->render());
-	}
-}
-
-class ClassWithCall {
-	public $name;
-	public function __call($method, $args) {
-		return 'unknown value';
-	}
-}

+ 0 - 152
test/MustacheExceptionTest.php

@@ -1,152 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-class MustacheExceptionTest extends PHPUnit_Framework_TestCase {
-
-	const TEST_CLASS = 'Mustache';
-
-	protected $pickyMustache;
-	protected $slackerMustache;
-
-	public function setUp() {
-		$this->pickyMustache      = new PickyMustache();
-		$this->slackerMustache    = new SlackerMustache();
-	}
-
-	/**
-	 * @group interpolation
-	 * @expectedException MustacheException
-	 */
-	public function testThrowsUnknownVariableException() {
-		$this->pickyMustache->render('{{not_a_variable}}');
-	}
-
-	/**
-	 * @group sections
-	 * @expectedException MustacheException
-	 */
-	public function testThrowsUnclosedSectionException() {
-		$this->pickyMustache->render('{{#unclosed}}');
-	}
-
-	/**
-	 * @group sections
-	 * @expectedException MustacheException
-	 */
-	public function testThrowsUnclosedInvertedSectionException() {
-		$this->pickyMustache->render('{{^unclosed}}');
-	}
-
-	/**
-	 * @group sections
-	 * @expectedException MustacheException
-	 */
-	public function testThrowsUnexpectedCloseSectionException() {
-		$this->pickyMustache->render('{{/unopened}}');
-	}
-
-	/**
-	 * @group partials
-	 * @expectedException MustacheException
-	 */
-	public function testThrowsUnknownPartialException() {
-		$this->pickyMustache->render('{{>impartial}}');
-	}
-
-	/**
-	 * @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() {
-		$mustache = new TestableMustache();
-		$mustache->testableGetPragmaOptions('PRAGMATIC');
-	}
-
-	public function testOverrideThrownExceptionsViaConstructorOptions() {
-		$exceptions = array(
-			MustacheException::UNKNOWN_VARIABLE,
-			MustacheException::UNCLOSED_SECTION,
-			MustacheException::UNEXPECTED_CLOSE_SECTION,
-			MustacheException::UNKNOWN_PARTIAL,
-			MustacheException::UNKNOWN_PRAGMA,
-		);
-
-		$one = new TestableMustache(null, null, null, array(
-			'throws_exceptions' => array_fill_keys($exceptions, true)
-		));
-
-		$thrownExceptions = $one->getThrownExceptions();
-		foreach ($exceptions as $exception) {
-			$this->assertTrue($thrownExceptions[$exception]);
-		}
-
-		$two = new TestableMustache(null, null, null, array(
-			'throws_exceptions' => array_fill_keys($exceptions, false)
-		));
-
-		$thrownExceptions = $two->getThrownExceptions();
-		foreach ($exceptions as $exception) {
-			$this->assertFalse($thrownExceptions[$exception]);
-		}
-	}
-}
-
-class PickyMustache extends Mustache {
-	protected $_throwsExceptions = array(
-		MustacheException::UNKNOWN_VARIABLE         => true,
-		MustacheException::UNCLOSED_SECTION         => true,
-		MustacheException::UNEXPECTED_CLOSE_SECTION => true,
-		MustacheException::UNKNOWN_PARTIAL          => true,
-		MustacheException::UNKNOWN_PRAGMA           => true,
-	);
-}
-
-class SlackerMustache extends Mustache {
-	protected $_throwsExceptions = array(
-		MustacheException::UNKNOWN_VARIABLE         => false,
-		MustacheException::UNCLOSED_SECTION         => false,
-		MustacheException::UNEXPECTED_CLOSE_SECTION => false,
-		MustacheException::UNKNOWN_PARTIAL          => false,
-		MustacheException::UNKNOWN_PRAGMA           => false,
-	);
-}
-
-class TestableMustache extends Mustache {
-	public function testableGetPragmaOptions($pragma_name) {
-		return $this->_getPragmaOptions($pragma_name);
-	}
-
-	public function getThrownExceptions() {
-		return $this->_throwsExceptions;
-	}
-}

+ 0 - 114
test/MustacheHigherOrderSectionsTest.php

@@ -1,114 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase {
-
-	public function setUp() {
-		$this->foo = new Foo();
-	}
-
-	public function testAnonymousFunctionSectionCallback() {
-		if (version_compare(PHP_VERSION, '5.3.0', '<')) {
-			$this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3');
-			return;
-		}
-
-		$this->foo->wrapper = function($text) {
-			return sprintf('<div class="anonymous">%s</div>', $text);
-		};
-
-		$this->assertEquals(
-			sprintf('<div class="anonymous">%s</div>', $this->foo->name),
-			$this->foo->render('{{#wrapper}}{{name}}{{/wrapper}}')
-		);
-	}
-
-	public function testSectionCallback() {
-		$this->assertEquals(sprintf('%s', $this->foo->name), $this->foo->render('{{name}}'));
-		$this->assertEquals(sprintf('<em>%s</em>', $this->foo->name), $this->foo->render('{{#wrap}}{{name}}{{/wrap}}'));
-	}
-
-	public function testRuntimeSectionCallback() {
-		$this->foo->double_wrap = array($this->foo, 'wrapWithBoth');
-		$this->assertEquals(
-			sprintf('<strong><em>%s</em></strong>', $this->foo->name),
-			$this->foo->render('{{#double_wrap}}{{name}}{{/double_wrap}}')
-		);
-	}
-
-	public function testStaticSectionCallback() {
-		$this->foo->trimmer = array(get_class($this->foo), 'staticTrim');
-		$this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}}    {{name}}    {{/trimmer}}'));
-	}
-
-	public function testViewArraySectionCallback() {
-		$data = array(
-			'name' => 'Bob',
-			'trim' => array(get_class($this->foo), 'staticTrim'),
-		);
-		$this->assertEquals($data['name'], $this->foo->render('{{#trim}}    {{name}}    {{/trim}}', $data));
-	}
-
-	public function testViewArrayAnonymousSectionCallback() {
-		if (version_compare(PHP_VERSION, '5.3.0', '<')) {
-			$this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3');
-			return;
-		}
-		$data = array(
-			'name' => 'Bob',
-			'wrap' => function($text) {
-				return sprintf('[[%s]]', $text);
-			}
-		);
-		$this->assertEquals(
-			sprintf('[[%s]]', $data['name']),
-			$this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data)
-		);
-	}
-
-	public function testMonsters() {
-		$frank = new Monster();
-		$frank->title = 'Dr.';
-		$frank->name  = 'Frankenstein';
-		$this->assertEquals('Dr. Frankenstein', $frank->render());
-
-		$dracula = new Monster();
-		$dracula->title = 'Count';
-		$dracula->name  = 'Dracula';
-		$this->assertEquals('Count Dracula', $dracula->render());
-	}
-}
-
-class Foo extends Mustache {
-	public $name = 'Justin';
-	public $lorem = 'Lorem ipsum dolor sit amet,';
-	public $wrap;
-
-	public function __construct($template = null, $view = null, $partials = null) {
-		$this->wrap = array($this, 'wrapWithEm');
-		parent::__construct($template, $view, $partials);
-	}
-
-	public function wrapWithEm($text) {
-		return sprintf('<em>%s</em>', $text);
-	}
-
-	public function wrapWithStrong($text) {
-		return sprintf('<strong>%s</strong>', $text);
-	}
-
-	public function wrapWithBoth($text) {
-		return self::wrapWithStrong(self::wrapWithEm($text));
-	}
-
-	public static function staticTrim($text) {
-		return trim($text);
-	}
-}
-
-class Monster extends Mustache {
-	public $_template = '{{#title}}{{title}} {{/title}}{{name}}';
-	public $title;
-	public $name;
-}

+ 0 - 127
test/MustacheInjectionTest.php

@@ -1,127 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-/**
- * @group mustache_injection
- */
-class MustacheInjectionSectionTest extends PHPUnit_Framework_TestCase {
-
-    // interpolation
-
-    public function testInterpolationInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{ a }}';
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function testUnescapedInterpolationInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{{ a }}}';
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-
-    // sections
-
-    public function testSectionInjection() {
-        $data = array(
-            'a' => true,
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}{{ b }}{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function testUnescapedSectionInjection() {
-        $data = array(
-            'a' => true,
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}{{{ b }}}{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-
-    // partials
-
-    public function testPartialInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{> partial }}';
-        $partials = array(
-            'partial' => '{{ a }}',
-        );
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data, $partials));
-    }
-
-    public function testPartialUnescapedInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{> partial }}';
-        $partials = array(
-            'partial' => '{{{ a }}}',
-        );
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data, $partials));
-    }
-
-
-    // lambdas
-
-    public function testLambdaInterpolationInjection() {
-        $data = array(
-            'a' => array($this, 'interpolationLambda'),
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function interpolationLambda() {
-        return '{{ b }}';
-    }
-
-    public function testLambdaSectionInjection() {
-        $data = array(
-            'a' => array($this, 'sectionLambda'),
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}b{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function sectionLambda($content) {
-        return '{{ ' . $content . ' }}';
-    }
-
-}

+ 0 - 60
test/MustacheLoaderTest.php

@@ -1,60 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-require_once '../MustacheLoader.php';
-
-/**
- * @group loader
- */
-class MustacheLoaderTest extends PHPUnit_Framework_TestCase {
-
-	public function testTheActualFilesystemLoader() {
-		$loader = new MustacheLoader(dirname(__FILE__).'/fixtures');
-		$this->assertEquals(file_get_contents(dirname(__FILE__).'/fixtures/foo.mustache'), $loader['foo']);
-		$this->assertEquals(file_get_contents(dirname(__FILE__).'/fixtures/bar.mustache'), $loader['bar']);
-	}
-
-	public function testMustacheUsesFilesystemLoader() {
-		$template = '{{> foo }} {{> bar }}';
-		$data = array(
-			'truthy' => true,
-			'foo'    => 'FOO',
-			'bar'    => 'BAR',
-		);
-		$output = 'FOO BAR';
-		$m = new Mustache();
-		$partials = new MustacheLoader(dirname(__FILE__).'/fixtures');
-		$this->assertEquals($output, $m->render($template, $data, $partials));
-	}
-
-	public function testMustacheUsesDifferentLoadersToo() {
-		$template = '{{> foo }} {{> bar }}';
-		$data = array(
-			'truthy' => true,
-			'foo'    => 'FOO',
-			'bar'    => 'BAR',
-		);
-		$output = 'FOO BAR';
-		$m = new Mustache();
-		$partials = new DifferentMustacheLoader();
-		$this->assertEquals($output, $m->render($template, $data, $partials));
-	}
-}
-
-class DifferentMustacheLoader implements ArrayAccess {
-	protected $partials = array(
-		'foo' => '{{ foo }}',
-		'bar' => '{{# truthy }}{{ bar }}{{/ truthy }}',
-	);
-
-	public function offsetExists($offset) {
-		return isset($this->partials[$offset]);
-	}
-
-	public function offsetGet($offset) {
-		return $this->partials[$offset];
-	}
-
-	public function offsetSet($offset, $value) {}
-	public function offsetUnset($offset) {}
-}

+ 0 - 73
test/MustacheObjectSectionTest.php

@@ -1,73 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-/**
- * @group sections
- */
-class MustacheObjectSectionTest extends PHPUnit_Framework_TestCase {
-	public function testBasicObject() {
-		$alpha = new Alpha();
-		$this->assertEquals('Foo', $alpha->render('{{#foo}}{{name}}{{/foo}}'));
-	}
-
-	public function testObjectWithGet() {
-		$beta = new Beta();
-		$this->assertEquals('Foo', $beta->render('{{#foo}}{{name}}{{/foo}}'));
-	}
-
-	public function testSectionObjectWithGet() {
-		$gamma = new Gamma();
-		$this->assertEquals('Foo', $gamma->render('{{#bar}}{{#foo}}{{name}}{{/foo}}{{/bar}}'));
-	}
-
-	public function testSectionObjectWithFunction() {
-		$alpha = new Alpha();
-		$alpha->foo = new Delta();
-		$this->assertEquals('Foo', $alpha->render('{{#foo}}{{name}}{{/foo}}'));
-	}
-}
-
-class Alpha extends Mustache {
-	public $foo;
-
-	public function __construct() {
-		$this->foo = new StdClass();
-		$this->foo->name = 'Foo';
-		$this->foo->number = 1;
-	}
-}
-
-class Beta extends Mustache {
-	protected $_data = array();
-
-	public function __construct() {
-		$this->_data['foo'] = new StdClass();
-		$this->_data['foo']->name = 'Foo';
-		$this->_data['foo']->number = 1;
-	}
-
-	public function __isset($name) {
-		return array_key_exists($name, $this->_data);
-	}
-
-	public function __get($name) {
-		return $this->_data[$name];
-	}
-}
-
-class Gamma extends Mustache {
-	public $bar;
-
-	public function __construct() {
-		$this->bar = new Beta();
-	}
-}
-
-class Delta extends Mustache {
-	protected $_name = 'Foo';
-
-	public function name() {
-		return $this->_name;
-	}
-}

+ 0 - 74
test/MustachePragmaTest.php

@@ -1,74 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-/**
- * @group pragmas
- */
-class MustachePragmaTest extends PHPUnit_Framework_TestCase {
-
-	public function testUnknownPragmaException() {
-		$m = new Mustache();
-
-		try {
-			$m->render('{{%I-HAVE-THE-GREATEST-MUSTACHE}}');
-		} catch (MustacheException $e) {
-			$this->assertEquals(MustacheException::UNKNOWN_PRAGMA, $e->getCode(), 'Caught exception code was not MustacheException::UNKNOWN_PRAGMA');
-			return;
-		}
-
-		$this->fail('Mustache should have thrown an unknown pragma exception');
-	}
-
-	public function testSuppressUnknownPragmaException() {
-		$m = new LessWhinyMustache();
-
-		try {
-			$this->assertEquals('', $m->render('{{%I-HAVE-THE-GREATEST-MUSTACHE}}'));
-		} catch (MustacheException $e) {
-			if ($e->getCode() == MustacheException::UNKNOWN_PRAGMA) {
-				$this->fail('Mustache should have thrown an unknown pragma exception');
-			} else {
-				throw $e;
-			}
-		}
-	}
-
-	public function testPragmaReplace() {
-		$m = new Mustache();
-		$this->assertEquals('', $m->render('{{%UNESCAPED}}'), 'Pragma tag not removed');
-	}
-
-	public function testPragmaReplaceMultiple() {
-		$m = new Mustache();
-
-		$this->assertEquals('', $m->render('{{%  UNESCAPED  }}'), 'Pragmas should allow whitespace');
-		$this->assertEquals('', $m->render('{{% 	UNESCAPED 	foo=bar  }}'), 'Pragmas should allow whitespace');
-		$this->assertEquals('', $m->render("{{%UNESCAPED}}\n{{%UNESCAPED}}"), 'Multiple pragma tags not removed');
-		$this->assertEquals(' ', $m->render('{{%UNESCAPED}} {{%UNESCAPED}}'), 'Multiple pragma tags not removed');
-	}
-
-	public function testPragmaReplaceNewline() {
-		$m = new Mustache();
-		$this->assertEquals('', $m->render("{{%UNESCAPED}}\n"), 'Trailing newline after pragma tag not removed');
-		$this->assertEquals("\n", $m->render("\n{{%UNESCAPED}}\n"), 'Too many newlines removed with pragma tag');
-		$this->assertEquals("1\n23", $m->render("1\n2{{%UNESCAPED}}\n3"), 'Wrong newline removed with pragma tag');
-	}
-
-	public function testPragmaReset() {
-		$m = new Mustache('', array('symbol' => '>>>'));
-		$this->assertEquals('>>>', $m->render('{{{symbol}}}'));
-		$this->assertEquals('>>>', $m->render('{{%UNESCAPED}}{{symbol}}'));
-		$this->assertEquals('>>>', $m->render('{{{symbol}}}'));
-	}
-}
-
-class LessWhinyMustache extends Mustache {
-	protected $_throwsExceptions = array(
-		MustacheException::UNKNOWN_VARIABLE         => false,
-		MustacheException::UNCLOSED_SECTION         => true,
-		MustacheException::UNEXPECTED_CLOSE_SECTION => true,
-		MustacheException::UNKNOWN_PARTIAL          => false,
-		MustacheException::UNKNOWN_PRAGMA           => false,
-	);
-}

+ 0 - 19
test/MustachePragmaUnescapedTest.php

@@ -1,19 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-/**
- * @group pragmas
- */
-class MustachePragmaUnescapedTest extends PHPUnit_Framework_TestCase {
-
-	public function testPragmaUnescaped() {
-		$m = new Mustache(null, array('title' => 'Bear > Shark'));
-		
-		$this->assertEquals('Bear > Shark', $m->render('{{%UNESCAPED}}{{title}}'));
-		$this->assertEquals('Bear &gt; Shark', $m->render('{{title}}'));
-		$this->assertEquals('Bear &gt; Shark', $m->render('{{%UNESCAPED}}{{{title}}}'));
-		$this->assertEquals('Bear > Shark', $m->render('{{{title}}}'));
-	}
-
-}

+ 0 - 174
test/MustacheSpecTest.php

@@ -1,174 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-require_once './lib/yaml/lib/sfYamlParser.php';
-
-/**
- * A PHPUnit test case wrapping the Mustache Spec
- *
- * @group mustache-spec
- */
-class MustacheSpecTest extends PHPUnit_Framework_TestCase {
-
-	/**
-	 * For some reason data providers can't mark tests skipped, so this test exists
-	 * simply to provide a 'skipped' test if the `spec` submodule isn't initialized.
-	 */
-	public function testSpecInitialized() {
-		$spec_dir = dirname(__FILE__) . '/spec/specs/';
-		if (!file_exists($spec_dir)) {
-			$this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"');
-		}
-	}
-
-	/**
-	 * @group comments
-	 * @dataProvider loadCommentSpec
-	 */
-	public function testCommentSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * @group delimiters
-	 * @dataProvider loadDelimitersSpec
-	 */
-	public function testDelimitersSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * @group interpolation
-	 * @dataProvider loadInterpolationSpec
-	 */
-	public function testInterpolationSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * @group inverted-sections
-	 * @dataProvider loadInvertedSpec
-	 */
-	public function testInvertedSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * @group lambdas
-	 * @dataProvider loadLambdasSpec
-	 */
-	public function testLambdasSpec($desc, $template, $data, $partials, $expected) {
-		if (!version_compare(PHP_VERSION, '5.3.0', '>=')) {
-			$this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.');
-		}
-
-		$data = $this->prepareLambdasSpec($data);
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * Extract and lambdafy any 'lambda' values found in the $data array.
-	 */
-	protected function prepareLambdasSpec($data) {
-		foreach ($data as $key => $val) {
-			if ($key === 'lambda') {
-				if (!isset($val['php'])) {
-					$this->markTestSkipped(sprintf('PHP lambda test not implemented for this test.'));
-				}
-
-				$func = $val['php'];
-				$data[$key] = function($text = null) use ($func) { return eval($func); };
-			} else if (is_array($val)) {
-				$data[$key] = $this->prepareLambdasSpec($val);
-			}
-		}
-		return $data;
-	}
-
-	/**
-	 * @group partials
-	 * @dataProvider loadPartialsSpec
-	 */
-	public function testPartialsSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	/**
-	 * @group sections
-	 * @dataProvider loadSectionsSpec
-	 */
-	public function testSectionsSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	public function loadCommentSpec() {
-		return $this->loadSpec('comments');
-	}
-
-	public function loadDelimitersSpec() {
-		return $this->loadSpec('delimiters');
-	}
-
-	public function loadInterpolationSpec() {
-		return $this->loadSpec('interpolation');
-	}
-
-	public function loadInvertedSpec() {
-		return $this->loadSpec('inverted');
-	}
-
-	public function loadLambdasSpec() {
-		return $this->loadSpec('~lambdas');
-	}
-
-	public function loadPartialsSpec() {
-		return $this->loadSpec('partials');
-	}
-
-	public function loadSectionsSpec() {
-		return $this->loadSpec('sections');
-	}
-
-	/**
-	 * Data provider for the mustache spec test.
-	 *
-	 * Loads YAML files from the spec and converts them to PHPisms.
-	 *
-	 * @access public
-	 * @return array
-	 */
-	protected function loadSpec($name) {
-		$filename = dirname(__FILE__) . '/spec/specs/' . $name . '.yml';
-		if (!file_exists($filename)) {
-			return array();
-		}
-
-		$data = array();
-		$yaml = new sfYamlParser();
-		$file = file_get_contents($filename);
-
-		// @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain.
-		if ($name === '~lambdas') {
-			$file = str_replace(" !code\n", "\n", $file);
-		}
-
-		$spec = $yaml->parse($file);
-		foreach ($spec['tests'] as $test) {
-			$data[] = array(
-				$test['name'] . ': ' . $test['desc'],
-				$test['template'],
-				$test['data'],
-				isset($test['partials']) ? $test['partials'] : array(),
-				$test['expected'],
-			);
-		}
-		return $data;
-	}
-}

+ 0 - 464
test/MustacheTest.php

@@ -1,464 +0,0 @@
-<?php
-
-require_once '../Mustache.php';
-
-/**
- * A PHPUnit test case for Mustache.php.
- *
- * This is a very basic, very rudimentary unit test case. It's probably more important to have tests
- * than to have elegant tests, so let's bear with it for a bit.
- *
- * This class assumes an example directory exists at `../examples` with the following structure:
- *
- * @code
- *    examples
- *        foo
- *            Foo.php
- *            foo.mustache
- *            foo.txt
- *        bar
- *            Bar.php
- *            bar.mustache
- *            bar.txt
- * @endcode
- *
- * To use this test:
- *
- *  1. {@link http://www.phpunit.de/manual/current/en/installation.html Install PHPUnit}
- *  2. run phpunit from the `test` directory:
- *        `phpunit MustacheTest`
- *  3. Fix bugs. Lather, rinse, repeat.
- *
- * @extends PHPUnit_Framework_TestCase
- */
-class MustacheTest extends PHPUnit_Framework_TestCase {
-
-	const TEST_CLASS = 'Mustache';
-
-	protected $knownIssues = array(
-		// Just the whitespace ones...
-	);
-
-	/**
-	 * Test Mustache constructor.
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function test__construct() {
-		$template = '{{#mustaches}}{{#last}}and {{/last}}{{type}}{{^last}}, {{/last}}{{/mustaches}}';
-		$data     = array(
-			'mustaches' => array(
-				array('type' => 'Natural'),
-				array('type' => 'Hungarian'),
-				array('type' => 'Dali'),
-				array('type' => 'English'),
-				array('type' => 'Imperial'),
-				array('type' => 'Freestyle', 'last' => 'true'),
-			)
-		);
-		$output = 'Natural, Hungarian, Dali, English, Imperial, and Freestyle';
-
-		$m1 = new Mustache();
-		$this->assertEquals($output, $m1->render($template, $data));
-
-		$m2 = new Mustache($template);
-		$this->assertEquals($output, $m2->render(null, $data));
-
-		$m3 = new Mustache($template, $data);
-		$this->assertEquals($output, $m3->render());
-
-		$m4 = new Mustache(null, $data);
-		$this->assertEquals($output, $m4->render($template));
-	}
-
-	/**
-	 * @dataProvider constructorOptions
-	 */
-	public function testConstructorOptions($options, $charset, $delimiters, $pragmas) {
-		$mustache = new MustacheExposedOptionsStub(null, null, null, $options);
-		$this->assertEquals($charset,    $mustache->getCharset());
-		$this->assertEquals($delimiters, $mustache->getDelimiters());
-		$this->assertEquals($pragmas,    $mustache->getPragmas());
-	}
-
-	public function constructorOptions() {
-		return array(
-			array(
-				array(),
-				'UTF-8',
-				array('{{', '}}'),
-				array(),
-			),
-			array(
-				array(
-					'charset'    => 'UTF-8',
-					'delimiters' => '<< >>',
-					'pragmas'    => array(Mustache::PRAGMA_UNESCAPED => true)
-				),
-				'UTF-8',
-				array('<<', '>>'),
-				array(Mustache::PRAGMA_UNESCAPED => true),
-			),
-			array(
-				array(
-					'charset'    => 'cp866',
-					'delimiters' => array('[[[[', ']]]]'),
-					'pragmas'    => array(Mustache::PRAGMA_UNESCAPED => true)
-				),
-				'cp866',
-				array('[[[[', ']]]]'),
-				array(Mustache::PRAGMA_UNESCAPED => true),
-			),
-		);
-	}
-
-	/**
-	 * @expectedException MustacheException
-	 */
-	public function testConstructorInvalidPragmaOptionsThrowExceptions() {
-		$mustache = new Mustache(null, null, null, array('pragmas' => array('banana phone' => true)));
-	}
-
-	/**
-	 * Test __toString() function.
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function test__toString() {
-		$m = new Mustache('{{first_name}} {{last_name}}', array('first_name' => 'Karl', 'last_name' => 'Marx'));
-
-		$this->assertEquals('Karl Marx', $m->__toString());
-		$this->assertEquals('Karl Marx', (string) $m);
-
-		$m2 = $this->getMock(self::TEST_CLASS, array('render'), array());
-		$m2->expects($this->once())
-			->method('render')
-			->will($this->returnValue('foo'));
-
-		$this->assertEquals('foo', $m2->render());
-	}
-
-	public function test__toStringException() {
-		$m = $this->getMock(self::TEST_CLASS, array('render'), array());
-		$m->expects($this->once())
-			->method('render')
-			->will($this->throwException(new Exception));
-
-		try {
-			$out = (string) $m;
-		} catch (Exception $e) {
-			$this->fail('__toString should catch all exceptions');
-		}
-	}
-
-	/**
-	 * Test render().
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function testRender() {
-		$m = new Mustache();
-
-		$this->assertEquals('', $m->render(''));
-		$this->assertEquals('foo', $m->render('foo'));
-		$this->assertEquals('', $m->render(null));
-
-		$m2 = new Mustache('foo');
-		$this->assertEquals('foo', $m2->render());
-
-		$m3 = new Mustache('');
-		$this->assertEquals('', $m3->render());
-
-		$m3 = new Mustache();
-		$this->assertEquals('', $m3->render(null));
-	}
-
-	/**
-	 * Test render() with data.
-	 *
-	 * @group interpolation
-	 */
-	public function testRenderWithData() {
-		$m = new Mustache('{{first_name}} {{last_name}}');
-		$this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin')));
-		$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')));
-		$this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa')));
-	}
-
-	/**
-	 * @group interpolation
-	 * @dataProvider interpolationData
-	 */
-	public function testDoubleRenderMustacheTags($template, $context, $expected) {
-		$m = new Mustache($template, $context);
-		$this->assertEquals($expected, $m->render());
-	}
-
-	public function interpolationData() {
-		return array(
-			array(
-				'{{#a}}{{=<% %>=}}{{b}} c<%={{ }}=%>{{/a}}',
-				array('a' => array(array('b' => 'Do Not Render'))),
-				'{{b}} c'
-			),
-			array(
-				'{{#a}}{{b}}{{/a}}',
-				array('a' => array('b' => '{{c}}'), 'c' => 'FAIL'),
-				'{{c}}'
-			),
-		);
-	}
-
-	/**
-	 * Mustache should allow newlines (and other whitespace) in comments and all other tags.
-	 *
-	 * @group comments
-	 */
-	public function testNewlinesInComments() {
-		$m = new Mustache("{{! comment \n \t still a comment... }}");
-		$this->assertEquals('', $m->render());
-	}
-
-	/**
-	 * Mustache should return the same thing when invoked multiple times.
-	 */
-	public function testMultipleInvocations() {
-		$m = new Mustache('x');
-		$first = $m->render();
-		$second = $m->render();
-
-		$this->assertEquals('x', $first);
-		$this->assertEquals($first, $second);
-	}
-
-	/**
-	 * Mustache should return the same thing when invoked multiple times.
-	 *
-	 * @group interpolation
-	 */
-	public function testMultipleInvocationsWithTags() {
-		$m = new Mustache('{{one}} {{two}}', array('one' => 'foo', 'two' => 'bar'));
-		$first = $m->render();
-		$second = $m->render();
-
-		$this->assertEquals('foo bar', $first);
-		$this->assertEquals($first, $second);
-	}
-
-	/**
-	 * Mustache should not use templates passed to the render() method for subsequent invocations.
-	 */
-	public function testResetTemplateForMultipleInvocations() {
-		$m = new Mustache('Sirve.');
-		$this->assertEquals('No sirve.', $m->render('No sirve.'));
-		$this->assertEquals('Sirve.', $m->render());
-
-		$m2 = new Mustache();
-		$this->assertEquals('No sirve.', $m2->render('No sirve.'));
-		$this->assertEquals('', $m2->render());
-	}
-
-	/**
-	 * Test the __clone() magic function.
-	 *
-	 * @group examples
-	 * @dataProvider getExamples
-	 *
-	 * @param string $class
-	 * @param string $template
-	 * @param string $output
-	 */
-	public function test__clone($class, $template, $output) {
-		if (isset($this->knownIssues[$class])) {
-			return $this->markTestSkipped($this->knownIssues[$class]);
-		}
-
-		$m = new $class;
-		$n = clone $m;
-
-		$n_output = $n->render($template);
-
-		$o = clone $n;
-
-		$this->assertEquals($m->render($template), $n_output);
-		$this->assertEquals($n_output, $o->render($template));
-
-		$this->assertNotSame($m, $n);
-		$this->assertNotSame($n, $o);
-		$this->assertNotSame($m, $o);
-	}
-
-	/**
-	 * Test everything in the `examples` directory.
-	 *
-	 * @group examples
-	 * @dataProvider getExamples
-	 *
-	 * @param string $class
-	 * @param string $template
-	 * @param string $output
-	 */
-	public function testExamples($class, $template, $output) {
-		if (isset($this->knownIssues[$class])) {
-			return $this->markTestSkipped($this->knownIssues[$class]);
-		}
-
-		$m = new $class;
-		$this->assertEquals($output, $m->render($template));
-	}
-
-	/**
-	 * Data provider for testExamples method.
-	 *
-	 * Assumes that an `examples` directory exists inside parent directory.
-	 * This examples directory should contain any number of subdirectories, each of which contains
-	 * three files: one Mustache class (.php), one Mustache template (.mustache), and one output file
-	 * (.txt).
-	 *
-	 * 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 :)
-	 *
-	 * @return array
-	 */
-	public function getExamples() {
-		$basedir = dirname(__FILE__) . '/../examples/';
-
-		$ret = array();
-
-		$files = new RecursiveDirectoryIterator($basedir);
-		while ($files->valid()) {
-
-			if ($files->hasChildren() && $children = $files->getChildren()) {
-				$example  = $files->getSubPathname();
-				$class    = null;
-				$template = null;
-				$output   = null;
-
-				foreach ($children as $file) {
-					if (!$file->isFile()) continue;
-
-					$filename = $file->getPathname();
-					$info = pathinfo($filename);
-
-					if (isset($info['extension'])) {
-						switch($info['extension']) {
-							case 'php':
-								$class = $info['filename'];
-								include_once($filename);
-								break;
-
-							case 'mustache':
-								$template = file_get_contents($filename);
-								break;
-
-							case 'txt':
-								$output = file_get_contents($filename);
-								break;
-						}
-					}
-				}
-
-				if (!empty($class)) {
-					$ret[$example] = array($class, $template, $output);
-				}
-			}
-
-			$files->next();
-		}
-		return $ret;
-	}
-
-	/**
-	 * @group delimiters
-	 */
-	public function testCrazyDelimiters() {
-		$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 $}'));
-		$this->assertEquals('success', $m->render('{{=<.. ..>=}}<.. result ..>'));
-		$this->assertEquals('success', $m->render('{{=^^ ^^}}^^ result ^^'));
-		$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 delimiters
-	 */
-	public function testStickyDelimiters() {
-		$m = new Mustache(null, array('result' => 'FAIL'));
-		$this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}{{ result }}[[={{ }}=]]'));
-		$this->assertEquals('{{#result}}{{/result}}', $m->render('{{=[[ ]]=}}{{#result}}{{/result}}[[={{ }}=]]'));
-		$this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}[[#result]]{{ result }}[[/result]][[={{ }}=]]'));
-		$this->assertEquals('{{ result }}', $m->render('{{#result}}{{=[[ ]]=}}{{ result }}[[/result]][[^result]][[={{ }}=]][[ result ]]{{/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}}'),
-		);
-	}
-
-	/**
-	 * Ensure that Mustache doesn't double-render sections (allowing mustache injection).
-	 *
-	 * @group sections
-	 */
-	public function testMustacheInjection() {
-		$template = '{{#foo}}{{bar}}{{/foo}}';
-		$view = array(
-			'foo' => true,
-			'bar' => '{{win}}',
-			'win' => 'FAIL',
-		);
-
-		$m = new Mustache($template, $view);
-		$this->assertEquals('{{win}}', $m->render());
-	}
-}
-
-class MustacheExposedOptionsStub extends Mustache {
-	public function getPragmas() {
-		return $this->_pragmas;
-	}
-	public function getCharset() {
-		return $this->_charset;
-	}
-	public function getDelimiters() {
-		return array($this->_otag, $this->_ctag);
-	}
-}

+ 15 - 0
test/bootstrap.php

@@ -0,0 +1,15 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require dirname(__FILE__).'/../src/Mustache/Autoloader.php';
+Mustache_Autoloader::register();
+
+require dirname(__FILE__).'/../vendor/yaml/lib/sfYamlParser.php';

+ 15 - 0
test/fixtures/autoloader/Mustache/Bar.php

@@ -0,0 +1,15 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Bar
+{
+    // nada
+}

+ 15 - 0
test/fixtures/autoloader/Mustache/Foo.php

@@ -0,0 +1,15 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Foo
+{
+    // nada
+}

+ 15 - 0
test/fixtures/autoloader/NonMustacheClass.php

@@ -0,0 +1,15 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class NonMustacheClass
+{
+    // noop
+}

+ 0 - 1
test/fixtures/bar.mustache

@@ -1 +0,0 @@
-{{# truthy }}{{ bar }}{{/ truthy }}

+ 14 - 0
test/fixtures/examples/child_context/ChildContext.php

@@ -0,0 +1,14 @@
+<?php
+
+class ChildContext
+{
+    public $parent = array(
+        'child' => 'child works',
+    );
+
+    public $grandparent = array(
+        'parent' => array(
+            'child' => 'grandchild works',
+        ),
+    );
+}

+ 0 - 0
examples/child_context/child_context.mustache → test/fixtures/examples/child_context/child_context.mustache


+ 0 - 0
examples/child_context/child_context.txt → test/fixtures/examples/child_context/child_context.txt


+ 9 - 0
test/fixtures/examples/comments/Comments.php

@@ -0,0 +1,9 @@
+<?php
+
+class Comments
+{
+    public function title()
+    {
+        return 'A Comedy of Errors';
+    }
+}

+ 0 - 0
examples/comments/comments.mustache → test/fixtures/examples/comments/comments.mustache


+ 0 - 0
examples/comments/comments.txt → test/fixtures/examples/comments/comments.txt


+ 0 - 0
examples/complex/complex.mustache → test/fixtures/examples/complex/complex.mustache


+ 22 - 0
test/fixtures/examples/complex/complex.php

@@ -0,0 +1,22 @@
+<?php
+
+class Complex
+{
+    public $header = 'Colors';
+
+    public $item = array(
+        array('name' => 'red', 'current' => true, 'url' => '#Red'),
+        array('name' => 'green', 'current' => false, 'url' => '#Green'),
+        array('name' => 'blue', 'current' => false, 'url' => '#Blue'),
+    );
+
+    public function notEmpty()
+    {
+        return !($this->isEmpty());
+    }
+
+    public function isEmpty()
+    {
+        return count($this->item) === 0;
+    }
+}

Some files were not shown because too many files changed in this diff