Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'release/2.1.0'

  • Loading branch information...
commit 6ac9cf03e47a0ca38257ef0aca765d0733f3c62c 2 parents d99be34 + 638ed9a
@bobthecow authored
Showing with 1,432 additions and 177 deletions.
  1. +2 −2 .gitmodules
  2. +33 −0 CONTRIBUTING.markdown
  3. +64 −3 src/Mustache/Compiler.php
  4. +1 −1  src/Mustache/Context.php
  5. +125 −13 src/Mustache/Engine.php
  6. +48 −0 src/Mustache/LambdaHelper.php
  7. +12 −8 src/Mustache/Loader/FilesystemLoader.php
  8. +135 −0 src/Mustache/Logger.php
  9. +121 −0 src/Mustache/Logger/AbstractLogger.php
  10. +188 −0 src/Mustache/Logger/StreamLogger.php
  11. +22 −0 src/Mustache/Tokenizer.php
  12. +80 −2 test/Mustache/Test/EngineTest.php
  13. +30 −0 test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php
  14. +94 −0 test/Mustache/Test/FiveThree/Functional/FiltersTest.php
  15. +48 −42 test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php
  16. +39 −0 test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php
  17. +103 −96 test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php
  18. +1 −1  test/Mustache/Test/Functional/ExamplesTest.php
  19. +0 −3  test/Mustache/Test/Functional/MustacheInjectionTest.php
  20. +13 −0 test/Mustache/Test/Loader/FilesystemLoaderTest.php
  21. +60 −0 test/Mustache/Test/Logger/AbstractLoggerTest.php
  22. +206 −0 test/Mustache/Test/Logger/StreamLoggerTest.php
  23. +7 −6 test/fixtures/examples/partials/Partials.php
View
4 .gitmodules
@@ -1,6 +1,6 @@
[submodule "vendor/spec"]
path = vendor/spec
- url = git://github.com/mustache/spec.git
+ url = https://github.com/mustache/spec.git
[submodule "vendor/yaml"]
path = vendor/yaml
- url = git://github.com/fabpot/yaml.git
+ url = https://github.com/fabpot/yaml.git
View
33 CONTRIBUTING.markdown
@@ -0,0 +1,33 @@
+# Contributions welcome!
+
+
+### Here's a quick guide:
+
+ 1. [Fork the repo on GitHub](https://github.com/bobthecow/mustache.php).
+
+ 2. Run the test suite. We only take pull requests with passing tests, and it's great to know that you have a clean slate. Make sure you have PHPUnit 3.5+, then run `phpunit` from the project directory.
+
+ 3. Add tests for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, add a test!
+
+ 4. Make the tests pass.
+
+ 5. Push your fork to GitHub and submit a pull request against the `dev` branch.
+
+
+### You can do some things to increase the chance that your pull request is accepted the first time:
+
+ * Submit one pull request per fix or feature.
+ * To help with that, do all your work in a feature branch (e.g. `feature/my-alsome-feature`).
+ * Follow the conventions you see used in the project.
+ * Use `phpcs --standard=PSR2` to check your changes against the coding standard.
+ * Write tests that fail without your code, and pass with it.
+ * Don't bump version numbers. Those will be updated — per [semver](http://semver.org) — once your change is merged into `master`.
+ * Update any documentation: docblocks, README, examples, etc.
+ * ... Don't update the wiki until your change is merged and released, but make a note in your pull request so we don't forget.
+
+
+### Mustache.php follows the PSR-* coding standards:
+
+ * [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md)
+ * [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
+ * [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
View
67 src/Mustache/Compiler.php
@@ -22,6 +22,7 @@ class Mustache_Compiler
private $indentNextLine;
private $customEscape;
private $charset;
+ private $pragmas;
/**
* Compile a Mustache token parse tree into PHP source code.
@@ -36,6 +37,7 @@ class Mustache_Compiler
*/
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
{
+ $this->pragmas = array();
$this->sections = array();
$this->source = $source;
$this->indentNextLine = true;
@@ -61,6 +63,10 @@ private function walk(array $tree, $level = 0)
$level++;
foreach ($tree as $node) {
switch ($node[Mustache_Tokenizer::TYPE]) {
+ case Mustache_Tokenizer::T_PRAGMA:
+ $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+ break;
+
case Mustache_Tokenizer::T_SECTION:
$code .= $this->section(
$node[Mustache_Tokenizer::NODES],
@@ -118,8 +124,11 @@ private function walk(array $tree, $level = 0)
class %s extends Mustache_Template
{
+ private $lambdaHelper;
+
public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false)
{
+ $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
$buffer = \'\';
%s
@@ -159,7 +168,7 @@ private function section%s(Mustache_Context $context, $indent, $value) {
if (!is_string($value) && is_callable($value)) {
$source = %s;
$buffer .= $this->mustache
- ->loadLambda((string) call_user_func($value, $source)%s)
+ ->loadLambda((string) call_user_func($value, $source, $this->lambdaHelper)%s)
->renderInternal($context, $indent);
} elseif (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value);
@@ -260,7 +269,7 @@ private function partial($id, $indent, $level)
$value = $this->mustache
->loadLambda((string) call_user_func($value))
->renderInternal($context, $indent);
- }
+ }%s
$buffer .= %s%s;
';
@@ -275,11 +284,63 @@ private function partial($id, $indent, $level)
*/
private function variable($id, $escape, $level)
{
+ $filters = '';
+
+ if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
+ list($id, $filters) = $this->getFilters($id, $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);
+ return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+ }
+
+ /**
+ * Generate Mustache Template variable filtering PHP source.
+ *
+ * @param string $id Variable name
+ * @param int $level
+ *
+ * @return string Generated variable filtering PHP source
+ */
+ private function getFilters($id, $level)
+ {
+ $filters = array_map('trim', explode('|', $id));
+ $id = array_shift($filters);
+
+ return array($id, $this->getFilter($filters, $level));
+ }
+
+ const FILTER = '
+ $filter = $context->%s(%s);
+ if (is_string($filter) || !is_callable($filter)) {
+ throw new UnexpectedValueException(%s);
+ }
+ $value = call_user_func($filter, $value);%s
+ ';
+
+ /**
+ * Generate PHP source for a single filter.
+ *
+ * @param array $filters
+ * @param int $level
+ *
+ * @return string Generated filter PHP source
+ */
+ private function getFilter(array $filters, $level)
+ {
+ if (empty($filters)) {
+ return '';
+ }
+
+ $name = array_shift($filters);
+ $method = $this->getFindMethod($name);
+ $filter = ($method !== 'last') ? var_export($name, true) : '';
+ $msg = var_export(sprintf('Filter not found: %s', $name), true);
+
+ return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level));
}
const LINE = '$buffer .= "\n";';
View
2  src/Mustache/Context.php
@@ -133,7 +133,7 @@ public function findDot($id)
private function findVariableInStack($id, array $stack)
{
for ($i = count($stack) - 1; $i >= 0; $i--) {
- if (is_object($stack[$i])) {
+ if (is_object($stack[$i]) && !$stack[$i] instanceof Closure) {
if (method_exists($stack[$i], $id)) {
return $stack[$i]->$id();
} elseif (isset($stack[$i]->$id)) {
View
138 src/Mustache/Engine.php
@@ -23,8 +23,10 @@
*/
class Mustache_Engine
{
- const VERSION = '2.0.2';
- const SPEC_VERSION = '1.1.2';
+ const VERSION = '2.1.0';
+ const SPEC_VERSION = '1.1.2';
+
+ const PRAGMA_FILTERS = 'FILTERS';
// Template cache
private $templates = array();
@@ -32,11 +34,13 @@ class Mustache_Engine
// Environment
private $templateClassPrefix = '__Mustache_';
private $cache = null;
+ private $cacheFileMode = null;
private $loader;
private $partialsLoader;
private $helpers;
private $escape;
private $charset = 'UTF-8';
+ private $logger;
/**
* Mustache class constructor.
@@ -44,13 +48,17 @@ class Mustache_Engine
* Passing an $options array allows overriding certain Mustache options during instantiation:
*
* $options = array(
- * // The class prefix for compiled templates. Defaults to '__Mustache_'
+ * // 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
+ * // Override default permissions for cache files. Defaults to using the system-defined umask. It is
+ * // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
+ * 'cache_file_mode' => 0666,
+ *
+ * // 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.
@@ -64,16 +72,21 @@ class Mustache_Engine
* // 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...
- * }),
+ * // 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'
+ * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
* 'charset' => 'ISO-8859-1',
+ *
+ * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
+ * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
+ * // available as well:
+ * 'logger' => new Mustache_StreamLogger('php://stderr'),
* );
*
* @param array $options (default: array())
@@ -88,6 +101,10 @@ public function __construct(array $options = array())
$this->cache = $options['cache'];
}
+ if (isset($options['cache_file_mode'])) {
+ $this->cacheFileMode = $options['cache_file_mode'];
+ }
+
if (isset($options['loader'])) {
$this->setLoader($options['loader']);
}
@@ -115,6 +132,10 @@ public function __construct(array $options = array())
if (isset($options['charset'])) {
$this->charset = $options['charset'];
}
+
+ if (isset($options['logger'])) {
+ $this->setLogger($options['logger']);
+ }
}
/**
@@ -320,6 +341,30 @@ public function removeHelper($name)
}
/**
+ * Set the Mustache Logger instance.
+ *
+ * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+ */
+ public function setLogger($logger = null)
+ {
+ if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+ throw new InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+ }
+
+ $this->logger = $logger;
+ }
+
+ /**
+ * Get the current Mustache Logger instance.
+ *
+ * @return Mustache_Logger|Psr\Log\LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
* Set the Mustache Tokenizer instance.
*
* @param Mustache_Tokenizer $tokenizer
@@ -442,7 +487,12 @@ public function loadPartial($name)
try {
return $this->loadSource($this->getPartialsLoader()->load($name));
} catch (InvalidArgumentException $e) {
- // If the named partial cannot be found, return null.
+ // If the named partial cannot be found, log then return null.
+ $this->log(
+ Mustache_Logger::WARNING,
+ 'Partial not found: "{name}"',
+ array('name' => $name)
+ );
}
}
@@ -485,15 +535,33 @@ private function loadSource($source)
if (!class_exists($className, false)) {
if ($fileName = $this->getCacheFilename($source)) {
if (!is_file($fileName)) {
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Writing "{className}" class to template cache: "{fileName}"',
+ array('className' => $className, 'fileName' => $fileName)
+ );
+
$this->writeCacheFile($fileName, $this->compile($source));
}
require_once $fileName;
} else {
+ $this->log(
+ Mustache_Logger::WARNING,
+ 'Template cache disabled, evaluating "{className}" class at runtime',
+ array('className' => $className)
+ );
+
eval('?>'.$this->compile($source));
}
}
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Instantiating template: "{className}"',
+ array('className' => $className)
+ );
+
$this->templates[$className] = new $className($this);
}
@@ -542,6 +610,12 @@ private function compile($source)
$tree = $this->parse($source);
$name = $this->getTemplateClassName($source);
+ $this->log(
+ Mustache_Logger::INFO,
+ 'Compiling template to "{className}" class',
+ array('className' => $name)
+ );
+
return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
}
@@ -562,7 +636,7 @@ private function getCacheFilename($source)
/**
* Helper method to dump a generated Mustache Template subclass to the file cache.
*
- * @throws RuntimeException if unable to write to $fileName.
+ * @throws RuntimeException if unable to create the cache directory or write to $fileName.
*
* @param string $fileName
* @param string $source
@@ -571,19 +645,57 @@ private function getCacheFilename($source)
*/
private function writeCacheFile($fileName, $source)
{
- if (!is_dir(dirname($fileName))) {
- mkdir(dirname($fileName), 0777, true);
+ $dirName = dirname($fileName);
+ if (!is_dir($dirName)) {
+ $this->log(
+ Mustache_Logger::INFO,
+ 'Creating Mustache template cache directory: "{dirName}"',
+ array('dirName' => $dirName)
+ );
+
+ @mkdir($dirName, 0777, true);
+ if (!is_dir($dirName)) {
+ throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+ }
+
}
- $tempFile = tempnam(dirname($fileName), basename($fileName));
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Caching compiled template to "{fileName}"',
+ array('fileName' => $fileName)
+ );
+
+ $tempFile = tempnam($dirName, basename($fileName));
if (false !== @file_put_contents($tempFile, $source)) {
if (@rename($tempFile, $fileName)) {
- chmod($fileName, 0644);
+ $mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask());
+ @chmod($fileName, $mode);
return;
}
+
+ $this->log(
+ Mustache_Logger::ERROR,
+ 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
+ array('tempName' => $tempFile, 'fileName' => $fileName)
+ );
}
throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
}
+
+ /**
+ * Add a log record if logging is enabled.
+ *
+ * @param integer $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ */
+ private function log($level, $message, array $context = array())
+ {
+ if (isset($this->logger)) {
+ $this->logger->log($level, $message, $context);
+ }
+ }
}
View
48 src/Mustache/LambdaHelper.php
@@ -0,0 +1,48 @@
+<?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 Lambda Helper.
+ *
+ * Passed to section and interpolation lambdas, giving them access to a `render`
+ * method for rendering a string with the current context.
+ */
+class Mustache_LambdaHelper
+{
+ private $mustache;
+ private $context;
+
+ /**
+ * Mustache Lambda Helper constructor.
+ *
+ * @param Mustache_Engine $mustache Mustache engine instance.
+ * @param Mustache_Context $context Rendering context.
+ */
+ public function __construct(Mustache_Engine $mustache, Mustache_Context $context)
+ {
+ $this->mustache = $mustache;
+ $this->context = $context;
+ }
+
+ /**
+ * Render a string as a Mustache template with the current rendering context.
+ *
+ * @param string $string
+ *
+ * @return Rendered template.
+ */
+ public function render($string)
+ {
+ return $this->mustache
+ ->loadLambda((string) $string)
+ ->renderInternal($this->context);
+ }
+}
View
20 src/Mustache/Loader/FilesystemLoader.php
@@ -12,19 +12,19 @@
/**
* Mustache Template filesystem Loader implementation.
*
- * An ArrayLoader instance loads Mustache Template source from the filesystem by name:
+ * A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
*
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
+ * $loader = new Mustache_Loader_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'),
+ * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
* ));
*
- * @implements Loader
+ * @implements Mustache_Loader
*/
class Mustache_Loader_FilesystemLoader implements Mustache_Loader
{
@@ -55,15 +55,19 @@ public function __construct($baseDir, array $options = array())
throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
}
- if (isset($options['extension'])) {
- $this->extension = '.' . ltrim($options['extension'], '.');
+ if (array_key_exists('extension', $options)) {
+ if (empty($options['extension'])) {
+ $this->extension = '';
+ } else {
+ $this->extension = '.' . ltrim($options['extension'], '.');
+ }
}
}
/**
* Load a Template by name.
*
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
+ * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
*
* @param string $name
View
135 src/Mustache/Logger.php
@@ -0,0 +1,135 @@
+<?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.
+ */
+
+/**
+ * Describes a Mustache logger instance
+ *
+ * This is identical to the Psr\Log\LoggerInterface.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data, the only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface Mustache_Logger
+{
+ /**
+ * Psr\Log compatible log levels
+ */
+ const EMERGENCY = 'emergency';
+ const ALERT = 'alert';
+ const CRITICAL = 'critical';
+ const ERROR = 'error';
+ const WARNING = 'warning';
+ const NOTICE = 'notice';
+ const INFO = 'info';
+ const DEBUG = 'debug';
+
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function emergency($message, array $context = array());
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function alert($message, array $context = array());
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function critical($message, array $context = array());
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function error($message, array $context = array());
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function warning($message, array $context = array());
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function notice($message, array $context = array());
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function info($message, array $context = array());
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function debug($message, array $context = array());
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ * @return null
+ */
+ public function log($level, $message, array $context = array());
+}
View
121 src/Mustache/Logger/AbstractLogger.php
@@ -0,0 +1,121 @@
+<?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.
+ */
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * This is identical to the Psr\Log\AbstractLogger.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger
+{
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function emergency($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function alert($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::ALERT, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function critical($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::CRITICAL, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function error($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::ERROR, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function warning($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::WARNING, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function notice($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::NOTICE, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function info($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::INFO, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function debug($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::DEBUG, $message, $context);
+ }
+}
View
188 src/Mustache/Logger/StreamLogger.php
@@ -0,0 +1,188 @@
+<?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 Stream Logger.
+ *
+ * The Stream Logger wraps a file resource instance (such as a stream) or a
+ * stream URL. All log messages over the threshold level will be appended to
+ * this stream.
+ *
+ * Hint: Try `php://stderr` for your stream URL.
+ */
+class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
+{
+ protected static $levels = array(
+ self::DEBUG => 100,
+ self::INFO => 200,
+ self::NOTICE => 250,
+ self::WARNING => 300,
+ self::ERROR => 400,
+ self::CRITICAL => 500,
+ self::ALERT => 550,
+ self::EMERGENCY => 600,
+ );
+
+ protected $stream = null;
+ protected $url = null;
+
+ /**
+ * @throws InvalidArgumentException if the logging level is unknown.
+ *
+ * @param string $stream Resource instance or URL
+ * @param integer $level The minimum logging level at which this handler will be triggered
+ */
+ public function __construct($stream, $level = Mustache_Logger::ERROR)
+ {
+ $this->setLevel($level);
+
+ if (is_resource($stream)) {
+ $this->stream = $stream;
+ } else {
+ $this->url = $stream;
+ }
+ }
+
+ /**
+ * Close stream resources.
+ */
+ public function __destruct()
+ {
+ if (is_resource($this->stream)) {
+ fclose($this->stream);
+ }
+ }
+
+ /**
+ * Set the minimum logging level.
+ *
+ * @throws InvalidArgumentException if the logging level is unknown.
+ *
+ * @param integer $level The minimum logging level which will be written
+ */
+ public function setLevel($level)
+ {
+ if (!array_key_exists($level, self::$levels)) {
+ throw new InvalidArgumentException('Unexpected logging level: ' . $level);
+ }
+
+ $this->level = $level;
+ }
+
+ /**
+ * Get the current minimum logging level.
+ *
+ * @return integer
+ */
+ public function getLevel()
+ {
+ return $this->level;
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @throws InvalidArgumentException if the logging level is unknown.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = array())
+ {
+ if (!array_key_exists($level, self::$levels)) {
+ throw new InvalidArgumentException('Unexpected logging level: ' . $level);
+ }
+
+ if (self::$levels[$level] >= self::$levels[$this->level]) {
+ $this->writeLog($level, $message, $context);
+ }
+ }
+
+ /**
+ * Write a record to the log.
+ *
+ * @param integer $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ */
+ protected function writeLog($level, $message, array $context = array())
+ {
+ if (!is_resource($this->stream)) {
+ if (!isset($this->url)) {
+ throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
+ }
+
+ $this->stream = fopen($this->url, 'a');
+ if (!is_resource($this->stream)) {
+ // @codeCoverageIgnoreStart
+ throw new UnexpectedValueException(sprintf('The stream or file "%s" could not be opened.', $this->url));
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ fwrite($this->stream, self::formatLine($level, $message, $context));
+ }
+
+ /**
+ * Gets the name of the logging level.
+ *
+ * @throws InvalidArgumentException if the logging level is unknown.
+ *
+ * @param integer $level
+ *
+ * @return string
+ */
+ protected static function getLevelName($level)
+ {
+ return strtoupper($level);
+ }
+
+ /**
+ * Format a log line for output.
+ *
+ * @param integer $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ *
+ * @return string
+ */
+ protected static function formatLine($level, $message, array $context = array())
+ {
+ return sprintf(
+ "%s: %s\n",
+ self::getLevelName($level),
+ self::interpolateMessage($message, $context)
+ );
+ }
+
+ /**
+ * Interpolate context values into the message placeholders.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return string
+ */
+ protected static function interpolateMessage($message, array $context = array())
+ {
+ $message = (string) $message;
+
+ // build a replacement array with braces around the context keys
+ $replace = array();
+ foreach ($context as $key => $val) {
+ $replace['{' . $key . '}'] = $val;
+ }
+
+ // interpolate replacement values into the the message and return
+ return strtr($message, $replace);
+ }
+}
View
22 src/Mustache/Tokenizer.php
@@ -34,6 +34,7 @@ class Mustache_Tokenizer
const T_UNESCAPED = '{';
const T_UNESCAPED_2 = '&';
const T_TEXT = '_t';
+ const T_PRAGMA = '%';
// Valid token types
private static $tagTypes = array(
@@ -47,6 +48,7 @@ class Mustache_Tokenizer
self::T_ESCAPED => true,
self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true,
+ self::T_PRAGMA => true,
);
// Interpolated tags
@@ -67,6 +69,7 @@ class Mustache_Tokenizer
const NODES = 'nodes';
const VALUE = 'value';
+ private $pragmas;
private $state;
private $tagType;
private $tag;
@@ -126,6 +129,9 @@ public function scan($text, $delimiters = null)
if ($this->tagType === self::T_DELIM_CHANGE) {
$i = $this->changeDelimiters($text, $i);
$this->state = self::IN_TEXT;
+ } elseif ($this->tagType === self::T_PRAGMA) {
+ $i = $this->addPragma($text, $i);
+ $this->state = self::IN_TEXT;
} else {
if ($tag !== null) {
$i++;
@@ -168,6 +174,13 @@ public function scan($text, $delimiters = null)
$this->filterLine(true);
+ foreach ($this->pragmas as $pragma) {
+ array_unshift($this->tokens, array(
+ self::TYPE => self::T_PRAGMA,
+ self::NAME => $pragma,
+ ));
+ }
+
return $this->tokens;
}
@@ -185,6 +198,7 @@ private function reset()
$this->lineStart = 0;
$this->otag = '{{';
$this->ctag = '}}';
+ $this->pragmas = array();
}
/**
@@ -270,6 +284,14 @@ private function changeDelimiters($text, $index)
return $closeIndex + strlen($close) - 1;
}
+ private function addPragma($text, $index)
+ {
+ $end = strpos($text, $this->ctag, $index);
+ $this->pragmas[] = trim(substr($text, $index + 2, $end - $index - 2));
+
+ return $end + strlen($this->ctag) - 1;
+ }
+
/**
* Test whether it's time to change tags.
*
View
82 test/Mustache/Test/EngineTest.php
@@ -27,11 +27,14 @@ public static function setUpBeforeClass()
public function testConstructor()
{
+ $logger = new Mustache_Logger_StreamLogger(tmpfile());
$loader = new Mustache_Loader_StringLoader;
$partialsLoader = new Mustache_Loader_ArrayLoader;
$mustache = new Mustache_Engine(array(
'template_class_prefix' => '__whot__',
- 'cache' => self::$tempDir,
+ 'cache' => self::$tempDir,
+ 'cache_file_mode' => 777,
+ 'logger' => $logger,
'loader' => $loader,
'partials_loader' => $partialsLoader,
'partials' => array(
@@ -45,6 +48,7 @@ public function testConstructor()
'charset' => 'ISO-8859-1',
));
+ $this->assertSame($logger, $mustache->getLogger());
$this->assertSame($loader, $mustache->getLoader());
$this->assertSame($partialsLoader, $mustache->getPartialsLoader());
$this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
@@ -85,12 +89,17 @@ public function testRender()
public function testSettingServices()
{
+ $logger = new Mustache_Logger_StreamLogger(tmpfile());
$loader = new Mustache_Loader_StringLoader;
$tokenizer = new Mustache_Tokenizer;
$parser = new Mustache_Parser;
$compiler = new Mustache_Compiler;
$mustache = new Mustache_Engine;
+ $this->assertNotSame($logger, $mustache->getLogger());
+ $mustache->setLogger($logger);
+ $this->assertSame($logger, $mustache->getLogger());
+
$this->assertNotSame($loader, $mustache->getLoader());
$mustache->setLoader($loader);
$this->assertSame($loader, $mustache->getLoader());
@@ -222,6 +231,74 @@ public function testSetHelpersThrowsExceptions()
$mustache->setHelpers('monkeymonkeymonkey');
}
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSetLoggerThrowsExceptions()
+ {
+ $mustache = new Mustache_Engine;
+ $mustache->setLogger(new StdClass);
+ }
+
+ public function testPartialLoadFailLogging()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $mustache = new Mustache_Engine(array(
+ 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::WARNING),
+ 'partials' => array(
+ 'foo' => 'FOO',
+ 'bar' => 'BAR',
+ ),
+ ));
+
+ $result = $mustache->render('{{> foo }}{{> bar }}{{> baz }}', array());
+ $this->assertEquals('FOOBAR', $result);
+
+ $this->assertContains('WARNING: Partial not found: "baz"', file_get_contents($name));
+ }
+
+ public function testCacheWarningLogging()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $mustache = new Mustache_Engine(array(
+ 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::WARNING)
+ ));
+
+ $result = $mustache->render('{{ foo }}', array('foo' => 'FOO'));
+ $this->assertEquals('FOO', $result);
+
+ $this->assertContains('WARNING: Template cache disabled, evaluating', file_get_contents($name));
+ }
+
+ public function testLoggingIsNotTooAnnoying()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $mustache = new Mustache_Engine(array(
+ 'logger' => new Mustache_Logger_StreamLogger($name)
+ ));
+
+ $result = $mustache->render('{{ foo }}{{> bar }}', array('foo' => 'FOO'));
+ $this->assertEquals('FOO', $result);
+
+ $this->assertEmpty(file_get_contents($name));
+ }
+
+ public function testVerboseLoggingIsVerbose()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $mustache = new Mustache_Engine(array(
+ 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::DEBUG)
+ ));
+
+ $result = $mustache->render('{{ foo }}{{> bar }}', array('foo' => 'FOO'));
+ $this->assertEquals('FOO', $result);
+
+ $log = file_get_contents($name);
+
+ $this->assertContains("DEBUG: Instantiating template: ", $log);
+ $this->assertContains("WARNING: Partial not found: \"bar\"", $log);
+ }
+
private static function rmdir($path)
{
$path = rtrim($path, '/').'/';
@@ -244,7 +321,8 @@ private static function rmdir($path)
}
}
-class MustacheStub extends Mustache_Engine {
+class MustacheStub extends Mustache_Engine
+{
public $source;
public $template;
public function loadTemplate($source)
View
30 test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php
@@ -0,0 +1,30 @@
+<?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_ClosuresQuirksTest extends PHPUnit_Framework_TestCase
+{
+ private $mustache;
+
+ public function setUp()
+ {
+ $this->mustache = new Mustache_Engine;
+ }
+
+ public function testClosuresDontLikeItWhenYouTouchTheirProperties()
+ {
+ $tpl = $this->mustache->loadTemplate('{{ foo.bar }}');
+ $this->assertEquals('', $tpl->render(array('foo' => function() { return 'FOO'; })));
+ }
+}
View
94 test/Mustache/Test/FiveThree/Functional/FiltersTest.php
@@ -0,0 +1,94 @@
+<?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 filters
+ * @group functional
+ */
+class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase
+{
+
+ private $mustache;
+
+ public function setUp()
+ {
+ $this->mustache = new Mustache_Engine;
+ }
+
+ public function testSingleFilter()
+ {
+ $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate }}');
+
+ $this->mustache->addHelper('longdate', function(\DateTime $value) {
+ return $value->format('Y-m-d h:m:s');
+ });
+
+ $foo = new \StdClass;
+ $foo->date = new DateTime('1/1/2000');
+
+ $this->assertEquals('2000-01-01 12:01:00', $tpl->render($foo));
+ }
+
+ public function testChainedFilters()
+ {
+ $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate | withbrackets }}');
+
+ $this->mustache->addHelper('longdate', function(\DateTime $value) {
+ return $value->format('Y-m-d h:m:s');
+ });
+
+ $this->mustache->addHelper('withbrackets', function($value) {
+ return sprintf('[[%s]]', $value);
+ });
+
+ $foo = new \StdClass;
+ $foo->date = new DateTime('1/1/2000');
+
+ $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo));
+ }
+
+ public function testInterpolateFirst()
+ {
+ $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar }}');
+ $this->assertEquals('win!', $tpl->render(array(
+ 'foo' => 'FOO',
+ 'bar' => function($value) {
+ return ($value === 'FOO') ? 'win!' : 'fail :(';
+ },
+ )));
+ }
+
+ /**
+ * @expectedException UnexpectedValueException
+ * @dataProvider getBrokenPipes
+ */
+ public function testThrowsExceptionForBrokenPipes($tpl, $data)
+ {
+ $this->mustache
+ ->loadTemplate(sprintf('{{%% FILTERS }}{{ %s }}', $tpl))
+ ->render($data);
+ }
+
+ public function getBrokenPipes()
+ {
+ return array(
+ array('foo | bar', array()),
+ array('foo | bar', array('foo' => 'FOO')),
+ array('foo | bar', array('foo' => 'FOO', 'bar' => 'BAR')),
+ array('foo | bar', array('foo' => 'FOO', 'bar' => array(1, 2))),
+ array('foo | bar | baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; })),
+ array('foo | bar | baz', array('foo' => 'FOO', 'baz' => function() { return 'BAZ'; })),
+ array('foo | bar | baz', array('bar' => function() { return 'BAR'; })),
+ array('foo | bar | baz', array('baz' => function() { return 'BAZ'; })),
+ array('foo | bar.baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; }, 'baz' => function() { return 'BAZ'; })),
+ );
+ }
+}
View
90 test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php
@@ -13,59 +13,65 @@
* @group lambdas
* @group functional
*/
-class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase {
+class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase
+{
+ private $mustache;
- private $mustache;
+ public function setUp()
+ {
+ $this->mustache = new Mustache_Engine;
+ }
- public function setUp() {
- $this->mustache = new Mustache_Engine;
- }
+ public function testAnonymousFunctionSectionCallback()
+ {
+ $tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}');
- 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);
+ };
- $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));
+ }
- $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}}');
- 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';
- $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));
+ }
- $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}}');
- public function testViewArrayAnonymousSectionCallback() {
- $tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}');
+ $data = array(
+ 'name' => 'Bob',
+ 'wrap' => function($text) {
+ return sprintf('[[%s]]', $text);
+ }
+ );
- $data = array(
- 'name' => 'Bob',
- 'wrap' => function($text) {
- return sprintf('[[%s]]', $text);
- }
- );
-
- $this->assertEquals(sprintf('[[%s]]', $data['name']), $tpl->render($data));
- }
+ $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;
+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);
- };
- }
+ public function __construct()
+ {
+ $this->wrap = function($text) {
+ return sprintf('<em>%s</em>', $text);
+ };
+ }
}
View
39 test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php
@@ -0,0 +1,39 @@
+<?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_LambdaHelperTest extends PHPUnit_Framework_TestCase
+{
+ private $mustache;
+
+ public function setUp()
+ {
+ $this->mustache = new Mustache_Engine;
+ }
+
+ public function testSectionLambdaHelper()
+ {
+ $one = $this->mustache->loadTemplate('{{name}}');
+ $two = $this->mustache->loadTemplate('{{#lambda}}{{name}}{{/lambda}}');
+
+ $foo = new StdClass;
+ $foo->name = 'Mario';
+ $foo->lambda = function($text, $mustache) {
+ return strtoupper($mustache->render($text));
+ };
+
+ $this->assertEquals('Mario', $one->render($foo));
+ $this->assertEquals('MARIO', $two->render($foo));
+ }
+}
View
199 test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php
@@ -15,100 +15,107 @@
* @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);
- }
+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);
+ };
+ } elseif (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);
+ }
}
View
2  test/Mustache/Test/Functional/ExamplesTest.php
@@ -116,7 +116,7 @@ private function loadExample($path)
*
* @param string $path
*
- * @return array $partials
+ * @return array $partials
*/
private function loadPartials($path)
{
View
3  test/Mustache/Test/Functional/MustacheInjectionTest.php
@@ -49,7 +49,6 @@ public function testUnescapedInterpolationInjection()
$this->assertEquals('{{ b }}', $tpl->render($data));
}
-
// sections
public function testSectionInjection()
@@ -78,7 +77,6 @@ public function testUnescapedSectionInjection()
$this->assertEquals('{{ c }}', $tpl->render($data));
}
-
// partials
public function testPartialInjection()
@@ -111,7 +109,6 @@ public function testPartialUnescapedInjection()
$this->assertEquals('{{ b }}', $tpl->render($data));
}
-
// lambdas
public function testLambdaInterpolationInjection()
View
13 test/Mustache/Test/Loader/FilesystemLoaderTest.php
@@ -30,6 +30,19 @@ public function testLoadTemplates()
$this->assertEquals('two contents', $loader->load('two.mustache'));
}
+ public function testEmptyExtensionString()
+ {
+ $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates');
+
+ $loader = new Mustache_Loader_FilesystemLoader($baseDir, array('extension' => ''));
+ $this->assertEquals('one contents', $loader->load('one.mustache'));
+ $this->assertEquals('alpha contents', $loader->load('alpha.ms'));
+
+ $loader = new Mustache_Loader_FilesystemLoader($baseDir, array('extension' => null));
+ $this->assertEquals('two contents', $loader->load('two.mustache'));
+ $this->assertEquals('beta contents', $loader->load('beta.ms'));
+ }
+
/**
* @expectedException RuntimeException
*/
View
60 test/Mustache/Test/Logger/AbstractLoggerTest.php
@@ -0,0 +1,60 @@
+<?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_Logger_AbstractLoggerTest extends PHPUnit_Framework_TestCase
+{
+ public function testEverything()
+ {
+ $logger = new Mustache_Test_Logger_TestLogger;
+
+ $logger->emergency('emergency message');
+ $logger->alert('alert message');
+ $logger->critical('critical message');
+ $logger->error('error message');
+ $logger->warning('warning message');
+ $logger->notice('notice message');
+ $logger->info('info message');
+ $logger->debug('debug message');
+
+ $expected = array(
+ array(Mustache_Logger::EMERGENCY, 'emergency message', array()),
+ array(Mustache_Logger::ALERT, 'alert message', array()),
+ array(Mustache_Logger::CRITICAL, 'critical message', array()),
+ array(Mustache_Logger::ERROR, 'error message', array()),
+ array(Mustache_Logger::WARNING, 'warning message', array()),
+ array(Mustache_Logger::NOTICE, 'notice message', array()),
+ array(Mustache_Logger::INFO, 'info message', array()),
+ array(Mustache_Logger::DEBUG, 'debug message', array()),
+ );
+
+ $this->assertEquals($expected, $logger->log);
+ }
+}
+
+class Mustache_Test_Logger_TestLogger extends Mustache_Logger_AbstractLogger
+{
+ public $log = array();
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = array())
+ {
+ $this->log[] = array($level, $message, $context);
+ }
+}
View
206 test/Mustache/Test/Logger/StreamLoggerTest.php
@@ -0,0 +1,206 @@
+<?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_Logger_StreamLoggerTest extends PHPUnit_Framework_TestCase
+{
+ public function testAcceptsFilename()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $logger = new Mustache_Logger_StreamLogger($name);
+ $logger->log(Mustache_Logger::CRITICAL, 'message');
+
+ $this->assertEquals("CRITICAL: message\n", file_get_contents($name));
+ }
+
+ public function testAcceptsResource()
+ {
+ $name = tempnam(sys_get_temp_dir(), 'mustache-test');
+ $file = fopen($name, 'a');
+ $logger = new Mustache_Logger_StreamLogger($file);
+ $logger->log(Mustache_Logger::CRITICAL, 'message');
+
+ $this->assertEquals("CRITICAL: message\n", file_get_contents($name));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testPrematurelyClosedStreamThrowsException()
+ {
+ $stream = tmpfile();
+ $logger = new Mustache_Logger_StreamLogger($stream);
+ fclose($stream);
+
+ $logger->log(Mustache_Logger::CRITICAL, 'message');
+ }
+
+ /**
+ * @dataProvider getLevels
+ */
+ public function testLoggingThresholds($logLevel, $level, $shouldLog)
+ {
+ $stream = tmpfile();
+ $logger = new Mustache_Logger_StreamLogger($stream, $logLevel);
+ $logger->log($level, "logged");
+
+ rewind($stream);
+ $result = fread($stream, 1024);
+
+ if ($shouldLog) {
+ $this->assertContains("logged", $result);
+ } else {
+ $this->assertEmpty($result);
+ }
+ }
+
+ public function getLevels()
+ {
+ // $logLevel, $level, $shouldLog
+ return array(
+ // identities
+ array(Mustache_Logger::EMERGENCY, Mustache_Logger::EMERGENCY, true),
+ array(Mustache_Logger::ALERT, Mustache_Logger::ALERT, true),
+ array(Mustache_Logger::CRITICAL, Mustache_Logger::CRITICAL, true),
+ array(Mustache_Logger::ERROR, Mustache_Logger::ERROR, true),
+ array(Mustache_Logger::WARNING, Mustache_Logger::WARNING, true),
+ array(Mustache_Logger::NOTICE, Mustache_Logger::NOTICE, true),
+ array(Mustache_Logger::INFO, Mustache_Logger::INFO, true),
+ array(Mustache_Logger::DEBUG, Mustache_Logger::DEBUG, true),
+
+ // one above
+ array(Mustache_Logger::ALERT, Mustache_Logger::EMERGENCY, true),
+ array(Mustache_Logger::CRITICAL, Mustache_Logger::ALERT, true),
+ array(Mustache_Logger::ERROR, Mustache_Logger::CRITICAL, true),
+ array(Mustache_Logger::WARNING, Mustache_Logger::ERROR, true),
+ array(Mustache_Logger::NOTICE, Mustache_Logger::WARNING, true),
+ array(Mustache_Logger::INFO, Mustache_Logger::NOTICE, true),
+ array(Mustache_Logger::DEBUG, Mustache_Logger::INFO, true),
+
+ // one below
+ array(Mustache_Logger::EMERGENCY, Mustache_Logger::ALERT, false),
+ array(Mustache_Logger::ALERT, Mustache_Logger::CRITICAL, false),
+ array(Mustache_Logger::CRITICAL, Mustache_Logger::ERROR, false),
+ array(Mustache_Logger::ERROR, Mustache_Logger::WARNING, false),
+ array(Mustache_Logger::WARNING, Mustache_Logger::NOTICE, false),
+ array(Mustache_Logger::NOTICE, Mustache_Logger::INFO, false),
+ array(Mustache_Logger::INFO, Mustache_Logger::DEBUG, false),
+ );
+ }
+
+ /**
+ * @dataProvider getLogMessages
+ */
+ public function testLogging($level, $message, $context, $expected)
+ {
+ $stream = tmpfile();
+ $logger = new Mustache_Logger_StreamLogger($stream, Mustache_Logger::DEBUG);
+ $logger->log($level, $message, $context);
+
+ rewind($stream);
+ $result = fread($stream, 1024);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function getLogMessages()
+ {
+ // $level, $message, $context, $expected
+ return array(
+ array(Mustache_Logger::DEBUG, 'debug message', array(), "DEBUG: debug message\n"),
+ array(Mustache_Logger::INFO, 'info message', array(), "INFO: info message\n"),
+ array(Mustache_Logger::NOTICE, 'notice message', array(), "NOTICE: notice message\n"),
+ array(Mustache_Logger::WARNING, 'warning message', array(), "WARNING: warning message\n"),
+ array(Mustache_Logger::ERROR, 'error message', array(), "ERROR: error message\n"),
+ array(Mustache_Logger::CRITICAL, 'critical message', array(), "CRITICAL: critical message\n"),
+ array(Mustache_Logger::ALERT, 'alert message', array(), "ALERT: alert message\n"),
+ array(Mustache_Logger::EMERGENCY, 'emergency message', array(), "EMERGENCY: emergency message\n"),
+
+ // with context
+ array(
+ Mustache_Logger::ERROR,
+ 'error message',
+ array('name' => 'foo', 'number' => 42),
+ "ERROR: error message\n"
+ ),
+
+ // with interpolation
+ array(
+ Mustache_Logger::ERROR,
+ 'error {name}-{number}',
+ array('name' => 'foo', 'number' => 42),
+ "ERROR: error foo-42\n"
+ ),
+
+ // with iterpolation false positive
+ array(
+ Mustache_Logger::ERROR,
+ 'error {nothing}',
+ array('name' => 'foo', 'number' => 42),
+ "ERROR: error {nothing}\n"
+ ),
+
+ // with interpolation injection
+ array(
+ Mustache_Logger::ERROR,
+ '{foo}',
+ array('foo' => '{bar}', 'bar' => 'FAIL'),
+ "ERROR: {bar}\n"
+ ),
+ );
+ }
+
+ public function testChangeLoggingLevels()
+ {
+ $stream = tmpfile();
+ $logger = new Mustache_Logger_StreamLogger($stream);
+
+ $logger->setLevel(Mustache_Logger::ERROR);
+ $this->assertEquals(Mustache_Logger::ERROR, $logger->getLevel());
+
+ $logger->log(Mustache_Logger::WARNING, 'ignore this');
+
+ $logger->setLevel(Mustache_Logger::INFO);
+ $this->assertEquals(Mustache_Logger::INFO, $logger->getLevel());
+
+ $logger->log(Mustache_Logger::WARNING, 'log this');
+
+ $logger->setLevel(Mustache_Logger::CRITICAL);
+ $this->assertEquals(Mustache_Logger::CRITICAL, $logger->getLevel());
+
+ $logger->log(Mustache_Logger::ERROR, 'ignore this');
+
+ rewind($stream);
+ $result = fread($stream, 1024);
+
+ $this->assertEquals("WARNING: log this\n", $result);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testThrowsInvalidArgumentExceptionWhenSettingUnknownLevels()
+ {
+ $logger = new Mustache_Logger_StreamLogger(tmpfile());
+ $logger->setLevel('bacon');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testThrowsInvalidArgumentExceptionWhenLoggingUnknownLevels()
+ {
+ $logger = new Mustache_Logger_StreamLogger(tmpfile());
+ $logger->log('bacon', 'CODE BACON ERROR!');
+ }
+}
View
13 test/fixtures/examples/partials/Partials.php
@@ -1,9 +1,10 @@
<?php
-class Partials {
- public $page = array(
- 'title' => 'Page Title',
- 'subtitle' => 'Page Subtitle',
- 'content' => 'Lorem ipsum dolor sit amet.',
- );
+class Partials
+{
+ public $page = array(
+ 'title' => 'Page Title',
+ 'subtitle' => 'Page Subtitle',
+ 'content' => 'Lorem ipsum dolor sit amet.',
+ );
}
Please sign in to comment.
Something went wrong with that request. Please try again.