MDL-49152 output: Templates for renderers (JS and PHP)
authorDamyon Wiese <damyon@moodle.com>
Sun, 22 Feb 2015 07:24:44 +0000 (15:24 +0800)
committerDamyon Wiese <damyon@moodle.com>
Sat, 14 Mar 2015 14:48:40 +0000 (22:48 +0800)
Mustache support for rendering templates from either php or js.

66 files changed:
lib/amd/build/ajax.min.js
lib/amd/build/mustache.min.js [new file with mode: 0644]
lib/amd/build/notification.min.js [new file with mode: 0644]
lib/amd/build/str.min.js [new file with mode: 0644]
lib/amd/build/templates.min.js [new file with mode: 0644]
lib/amd/build/url.min.js [new file with mode: 0644]
lib/amd/build/yui.min.js [new file with mode: 0644]
lib/amd/src/ajax.js
lib/amd/src/config.js
lib/amd/src/first.js
lib/amd/src/mustache.js [new file with mode: 0644]
lib/amd/src/notification.js [new file with mode: 0644]
lib/amd/src/str.js [new file with mode: 0644]
lib/amd/src/templates.js [new file with mode: 0644]
lib/amd/src/url.js [new file with mode: 0644]
lib/amd/src/yui.js [new file with mode: 0644]
lib/classes/output/external.php [new file with mode: 0644]
lib/classes/output/mustache_filesystem_loader.php [new file with mode: 0644]
lib/classes/output/mustache_javascript_helper.php [new file with mode: 0644]
lib/classes/output/mustache_pix_helper.php [new file with mode: 0644]
lib/classes/output/mustache_string_helper.php [new file with mode: 0644]
lib/classes/output/mustache_uniqid_helper.php [new file with mode: 0644]
lib/db/services.php
lib/mustache/CONTRIBUTING.md [new file with mode: 0644]
lib/mustache/LICENSE [new file with mode: 0644]
lib/mustache/README.md [new file with mode: 0644]
lib/mustache/composer.json [new file with mode: 0644]
lib/mustache/readme_moodle.txt [new file with mode: 0644]
lib/mustache/src/Mustache/Autoloader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/AbstractCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/FilesystemCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/NoopCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Compiler.php [new file with mode: 0644]
lib/mustache/src/Mustache/Context.php [new file with mode: 0644]
lib/mustache/src/Mustache/Engine.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/LogicException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/RuntimeException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/SyntaxException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownFilterException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownHelperException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php [new file with mode: 0644]
lib/mustache/src/Mustache/HelperCollection.php [new file with mode: 0644]
lib/mustache/src/Mustache/LambdaHelper.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/ArrayLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/CascadingLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/FilesystemLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/InlineLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/MutableLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/StringLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/AbstractLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/StreamLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Parser.php [new file with mode: 0644]
lib/mustache/src/Mustache/Template.php [new file with mode: 0644]
lib/mustache/src/Mustache/Tokenizer.php [new file with mode: 0644]
lib/outputcomponents.php
lib/outputfactories.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/templates/pix_icon.mustache [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt

index bd2e623..72274b8 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
diff --git a/lib/amd/build/mustache.min.js b/lib/amd/build/mustache.min.js
new file mode 100644 (file)
index 0000000..e321d41
Binary files /dev/null and b/lib/amd/build/mustache.min.js differ
diff --git a/lib/amd/build/notification.min.js b/lib/amd/build/notification.min.js
new file mode 100644 (file)
index 0000000..37952ec
Binary files /dev/null and b/lib/amd/build/notification.min.js differ
diff --git a/lib/amd/build/str.min.js b/lib/amd/build/str.min.js
new file mode 100644 (file)
index 0000000..bfdb826
Binary files /dev/null and b/lib/amd/build/str.min.js differ
diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js
new file mode 100644 (file)
index 0000000..c64573a
Binary files /dev/null and b/lib/amd/build/templates.min.js differ
diff --git a/lib/amd/build/url.min.js b/lib/amd/build/url.min.js
new file mode 100644 (file)
index 0000000..e287766
Binary files /dev/null and b/lib/amd/build/url.min.js differ
diff --git a/lib/amd/build/yui.min.js b/lib/amd/build/yui.min.js
new file mode 100644 (file)
index 0000000..c2bc6fa
Binary files /dev/null and b/lib/amd/build/yui.min.js differ
index 05863ea..4f2cb77 100644 (file)
  * In addition, it can batch multiple requests and return multiple responses.
  *
  * @module     core/ajax
+ * @class      ajax
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
  */
 define(['jquery', 'core/config'], function($, config) {
 
@@ -29,6 +31,8 @@ define(['jquery', 'core/config'], function($, config) {
      * Success handler. Called when the ajax call succeeds. Checks each response and
      * resolves or rejects the deferred from that request.
      *
+     * @method requestSuccess
+     * @private
      * @param {Object[]} responses Array of responses containing error, exception and data attributes.
      */
     var requestSuccess = function(responses) {
@@ -70,6 +74,8 @@ define(['jquery', 'core/config'], function($, config) {
     /**
      * Fail handler. Called when the ajax call fails. Rejects all deferreds.
      *
+     * @method requestFail
+     * @private
      * @param {jqXHR} jqXHR The ajax object.
      * @param {string} textStatus The status string.
      */
@@ -91,15 +97,23 @@ define(['jquery', 'core/config'], function($, config) {
         // Public variables and functions.
         /**
          * Make a series of ajax requests and return all the responses.
+         *
+         * @method call
          * @param {Object[]} Array of requests with each containing methodname and args properties.
          *                   done and fail callbacks can be set for each element in the array, or the
          *                   can be attached to the promises returned by this function.
-         * @return {Promise{}} Array of promises that will be resolved when the ajax call returns.
+         * @param {Boolean} async Optional, defaults to true.
+         *                  If false - this function will not return until the promises are resolved.
+         * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
-        call: function(requests) {
+        call: function(requests, async) {
             var ajaxRequestData = [],
                 i,
                 promises = [];
+
+            if (typeof async === "undefined") {
+                async = true;
+            }
             for (i = 0; i < requests.length; i++) {
                 var request = requests[i];
                 ajaxRequestData.push({
@@ -126,12 +140,20 @@ define(['jquery', 'core/config'], function($, config) {
                 data: ajaxRequestData,
                 context: requests,
                 dataType: 'json',
-                processData: false
+                processData: false,
+                async: async
             };
 
-            $.ajax(config.wwwroot + '/lib/ajax/service.php', settings)
-                .done(requestSuccess)
-                .fail(requestFail);
+            // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
+            if (async) {
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings)
+                    .done(requestSuccess)
+                    .fail(requestFail);
+            } else {
+                settings.success = requestSuccess;
+                settings.error = requestFail;
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings);
+            }
 
             return promises;
         }
index 848567f..203326b 100644 (file)
  * Expose the M.cfg global variable.
  *
  * @module     core/config
+ * @class      config
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
  */
 define(function() {
 
index 22188f3..704c0c5 100644 (file)
@@ -22,5 +22,6 @@
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
  */
 define(function() { });
diff --git a/lib/amd/src/mustache.js b/lib/amd/src/mustache.js
new file mode 100644 (file)
index 0000000..6c96ca4
--- /dev/null
@@ -0,0 +1,608 @@
+// The MIT License
+//
+// Copyright (c) 2009 Chris Wanstrath (Ruby)
+// Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// Description of import into Moodle:
+// Download from https://github.com/janl/mustache.js/releases
+// Copy mustache.js into lib/amd/src/ in Moodle folder.
+// Add the license as a comment to the file and these instructions.
+
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+/* jshint ignore:start */
+
+(function (global, factory) {
+  if (typeof exports === "object" && exports) {
+    factory(exports); // CommonJS
+  } else if (typeof define === "function" && define.amd) {
+    define(['exports'], factory); // AMD
+  } else {
+    factory(global.Mustache = {}); // <script>
+  }
+}(this, function (mustache) {
+
+  var Object_toString = Object.prototype.toString;
+  var isArray = Array.isArray || function (object) {
+    return Object_toString.call(object) === '[object Array]';
+  };
+
+  function isFunction(object) {
+    return typeof object === 'function';
+  }
+
+  function escapeRegExp(string) {
+    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+  }
+
+  // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
+  // See https://github.com/janl/mustache.js/issues/189
+  var RegExp_test = RegExp.prototype.test;
+  function testRegExp(re, string) {
+    return RegExp_test.call(re, string);
+  }
+
+  var nonSpaceRe = /\S/;
+  function isWhitespace(string) {
+    return !testRegExp(nonSpaceRe, string);
+  }
+
+  var entityMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;',
+    "/": '&#x2F;'
+  };
+
+  function escapeHtml(string) {
+    return String(string).replace(/[&<>"'\/]/g, function (s) {
+      return entityMap[s];
+    });
+  }
+
+  var whiteRe = /\s*/;
+  var spaceRe = /\s+/;
+  var equalsRe = /\s*=/;
+  var curlyRe = /\s*\}/;
+  var tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+  /**
+   * Breaks up the given `template` string into a tree of tokens. If the `tags`
+   * argument is given here it must be an array with two string values: the
+   * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
+   * course, the default is to use mustaches (i.e. mustache.tags).
+   *
+   * A token is an array with at least 4 elements. The first element is the
+   * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
+   * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
+   * all text that appears outside a symbol this element is "text".
+   *
+   * The second element of a token is its "value". For mustache tags this is
+   * whatever else was inside the tag besides the opening symbol. For text tokens
+   * this is the text itself.
+   *
+   * The third and fourth elements of the token are the start and end indices,
+   * respectively, of the token in the original template.
+   *
+   * Tokens that are the root node of a subtree contain two more elements: 1) an
+   * array of tokens in the subtree and 2) the index in the original template at
+   * which the closing tag for that section begins.
+   */
+  function parseTemplate(template, tags) {
+    if (!template)
+      return [];
+
+    var sections = [];     // Stack to hold section tokens
+    var tokens = [];       // Buffer to hold the tokens
+    var spaces = [];       // Indices of whitespace tokens on the current line
+    var hasTag = false;    // Is there a {{tag}} on the current line?
+    var nonSpace = false;  // Is there a non-space char on the current line?
+
+    // Strips all whitespace tokens array for the current line
+    // if there was a {{#tag}} on it and otherwise only space.
+    function stripSpace() {
+      if (hasTag && !nonSpace) {
+        while (spaces.length)
+          delete tokens[spaces.pop()];
+      } else {
+        spaces = [];
+      }
+
+      hasTag = false;
+      nonSpace = false;
+    }
+
+    var openingTagRe, closingTagRe, closingCurlyRe;
+    function compileTags(tags) {
+      if (typeof tags === 'string')
+        tags = tags.split(spaceRe, 2);
+
+      if (!isArray(tags) || tags.length !== 2)
+        throw new Error('Invalid tags: ' + tags);
+
+      openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
+      closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
+      closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
+    }
+
+    compileTags(tags || mustache.tags);
+
+    var scanner = new Scanner(template);
+
+    var start, type, value, chr, token, openSection;
+    while (!scanner.eos()) {
+      start = scanner.pos;
+
+      // Match any text between tags.
+      value = scanner.scanUntil(openingTagRe);
+
+      if (value) {
+        for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
+          chr = value.charAt(i);
+
+          if (isWhitespace(chr)) {
+            spaces.push(tokens.length);
+          } else {
+            nonSpace = true;
+          }
+
+          tokens.push([ 'text', chr, start, start + 1 ]);
+          start += 1;
+
+          // Check for whitespace on the current line.
+          if (chr === '\n')
+            stripSpace();
+        }
+      }
+
+      // Match the opening tag.
+      if (!scanner.scan(openingTagRe))
+        break;
+
+      hasTag = true;
+
+      // Get the tag type.
+      type = scanner.scan(tagRe) || 'name';
+      scanner.scan(whiteRe);
+
+      // Get the tag value.
+      if (type === '=') {
+        value = scanner.scanUntil(equalsRe);
+        scanner.scan(equalsRe);
+        scanner.scanUntil(closingTagRe);
+      } else if (type === '{') {
+        value = scanner.scanUntil(closingCurlyRe);
+        scanner.scan(curlyRe);
+        scanner.scanUntil(closingTagRe);
+        type = '&';
+      } else {
+        value = scanner.scanUntil(closingTagRe);
+      }
+
+      // Match the closing tag.
+      if (!scanner.scan(closingTagRe))
+        throw new Error('Unclosed tag at ' + scanner.pos);
+
+      token = [ type, value, start, scanner.pos ];
+      tokens.push(token);
+
+      if (type === '#' || type === '^') {
+        sections.push(token);
+      } else if (type === '/') {
+        // Check section nesting.
+        openSection = sections.pop();
+
+        if (!openSection)
+          throw new Error('Unopened section "' + value + '" at ' + start);
+
+        if (openSection[1] !== value)
+          throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
+      } else if (type === 'name' || type === '{' || type === '&') {
+        nonSpace = true;
+      } else if (type === '=') {
+        // Set the tags for the next time around.
+        compileTags(value);
+      }
+    }
+
+    // Make sure there are no open sections when we're done.
+    openSection = sections.pop();
+
+    if (openSection)
+      throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
+
+    return nestTokens(squashTokens(tokens));
+  }
+
+  /**
+   * Combines the values of consecutive text tokens in the given `tokens` array
+   * to a single token.
+   */
+  function squashTokens(tokens) {
+    var squashedTokens = [];
+
+    var token, lastToken;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      if (token) {
+        if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
+          lastToken[1] += token[1];
+          lastToken[3] = token[3];
+        } else {
+          squashedTokens.push(token);
+          lastToken = token;
+        }
+      }
+    }
+
+    return squashedTokens;
+  }
+
+  /**
+   * Forms the given array of `tokens` into a nested tree structure where
+   * tokens that represent a section have two additional items: 1) an array of
+   * all tokens that appear in that section and 2) the index in the original
+   * template that represents the end of that section.
+   */
+  function nestTokens(tokens) {
+    var nestedTokens = [];
+    var collector = nestedTokens;
+    var sections = [];
+
+    var token, section;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+      case '^':
+        collector.push(token);
+        sections.push(token);
+        collector = token[4] = [];
+        break;
+      case '/':
+        section = sections.pop();
+        section[5] = token[2];
+        collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
+        break;
+      default:
+        collector.push(token);
+      }
+    }
+
+    return nestedTokens;
+  }
+
+  /**
+   * A simple string scanner that is used by the template parser to find
+   * tokens in template strings.
+   */
+  function Scanner(string) {
+    this.string = string;
+    this.tail = string;
+    this.pos = 0;
+  }
+
+  /**
+   * Returns `true` if the tail is empty (end of string).
+   */
+  Scanner.prototype.eos = function () {
+    return this.tail === "";
+  };
+
+  /**
+   * Tries to match the given regular expression at the current position.
+   * Returns the matched text if it can match, the empty string otherwise.
+   */
+  Scanner.prototype.scan = function (re) {
+    var match = this.tail.match(re);
+
+    if (!match || match.index !== 0)
+      return '';
+
+    var string = match[0];
+
+    this.tail = this.tail.substring(string.length);
+    this.pos += string.length;
+
+    return string;
+  };
+
+  /**
+   * Skips all text until the given regular expression can be matched. Returns
+   * the skipped string, which is the entire tail if no match can be made.
+   */
+  Scanner.prototype.scanUntil = function (re) {
+    var index = this.tail.search(re), match;
+
+    switch (index) {
+    case -1:
+      match = this.tail;
+      this.tail = "";
+      break;
+    case 0:
+      match = "";
+      break;
+    default:
+      match = this.tail.substring(0, index);
+      this.tail = this.tail.substring(index);
+    }
+
+    this.pos += match.length;
+
+    return match;
+  };
+
+  /**
+   * Represents a rendering context by wrapping a view object and
+   * maintaining a reference to the parent context.
+   */
+  function Context(view, parentContext) {
+    this.view = view == null ? {} : view;
+    this.cache = { '.': this.view };
+    this.parent = parentContext;
+  }
+
+  /**
+   * Creates a new context using the given view with this context
+   * as the parent.
+   */
+  Context.prototype.push = function (view) {
+    return new Context(view, this);
+  };
+
+  /**
+   * Returns the value of the given name in this context, traversing
+   * up the context hierarchy if the value is absent in this context's view.
+   */
+  Context.prototype.lookup = function (name) {
+    var cache = this.cache;
+
+    var value;
+    if (name in cache) {
+      value = cache[name];
+    } else {
+      var context = this, names, index;
+
+      while (context) {
+        if (name.indexOf('.') > 0) {
+          value = context.view;
+          names = name.split('.');
+          index = 0;
+
+          while (value != null && index < names.length)
+            value = value[names[index++]];
+        } else if (typeof context.view == 'object') {
+          value = context.view[name];
+        }
+
+        if (value != null)
+          break;
+
+        context = context.parent;
+      }
+
+      cache[name] = value;
+    }
+
+    if (isFunction(value))
+      value = value.call(this.view);
+
+    return value;
+  };
+
+  /**
+   * A Writer knows how to take a stream of tokens and render them to a
+   * string, given a context. It also maintains a cache of templates to
+   * avoid the need to parse the same template twice.
+   */
+  function Writer() {
+    this.cache = {};
+  }
+
+  /**
+   * Clears all cached templates in this writer.
+   */
+  Writer.prototype.clearCache = function () {
+    this.cache = {};
+  };
+
+  /**
+   * Parses and caches the given `template` and returns the array of tokens
+   * that is generated from the parse.
+   */
+  Writer.prototype.parse = function (template, tags) {
+    var cache = this.cache;
+    var tokens = cache[template];
+
+    if (tokens == null)
+      tokens = cache[template] = parseTemplate(template, tags);
+
+    return tokens;
+  };
+
+  /**
+   * High-level method that is used to render the given `template` with
+   * the given `view`.
+   *
+   * The optional `partials` argument may be an object that contains the
+   * names and templates of partials that are used in the template. It may
+   * also be a function that is used to load partial templates on the fly
+   * that takes a single argument: the name of the partial.
+   */
+  Writer.prototype.render = function (template, view, partials) {
+    var tokens = this.parse(template);
+    var context = (view instanceof Context) ? view : new Context(view);
+    return this.renderTokens(tokens, context, partials, template);
+  };
+
+  /**
+   * Low-level method that renders the given array of `tokens` using
+   * the given `context` and `partials`.
+   *
+   * Note: The `originalTemplate` is only ever used to extract the portion
+   * of the original template that was contained in a higher-order section.
+   * If the template doesn't use higher-order sections, this argument may
+   * be omitted.
+   */
+  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+    var buffer = '';
+
+    // This function is used to render an arbitrary template
+    // in the current context by higher-order sections.
+    var self = this;
+    function subRender(template) {
+      return self.render(template, context, partials);
+    }
+
+    var token, value;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+        value = context.lookup(token[1]);
+
+        if (!value)
+          continue;
+
+        if (isArray(value)) {
+          for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
+            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+          }
+        } else if (typeof value === 'object' || typeof value === 'string') {
+          buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+        } else if (isFunction(value)) {
+          if (typeof originalTemplate !== 'string')
+            throw new Error('Cannot use higher-order sections without the original template');
+
+          // Extract the portion of the original template that the section contains.
+          value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+
+          if (value != null)
+            buffer += value;
+        } else {
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+        }
+
+        break;
+      case '^':
+        value = context.lookup(token[1]);
+
+        // Use JavaScript's definition of falsy. Include empty arrays.
+        // See https://github.com/janl/mustache.js/issues/186
+        if (!value || (isArray(value) && value.length === 0))
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+
+        break;
+      case '>':
+        if (!partials)
+          continue;
+
+        value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+
+        if (value != null)
+          buffer += this.renderTokens(this.parse(value), context, partials, value);
+
+        break;
+      case '&':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += value;
+
+        break;
+      case 'name':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += mustache.escape(value);
+
+        break;
+      case 'text':
+        buffer += token[1];
+        break;
+      }
+    }
+
+    return buffer;
+  };
+
+  mustache.name = "mustache.js";
+  mustache.version = "1.0.0";
+  mustache.tags = [ "{{", "}}" ];
+
+  // All high-level mustache.* functions use this writer.
+  var defaultWriter = new Writer();
+
+  /**
+   * Clears all cached templates in the default writer.
+   */
+  mustache.clearCache = function () {
+    return defaultWriter.clearCache();
+  };
+
+  /**
+   * Parses and caches the given template in the default writer and returns the
+   * array of tokens it contains. Doing this ahead of time avoids the need to
+   * parse templates on the fly as they are rendered.
+   */
+  mustache.parse = function (template, tags) {
+    return defaultWriter.parse(template, tags);
+  };
+
+  /**
+   * Renders the `template` with the given `view` and `partials` using the
+   * default writer.
+   */
+  mustache.render = function (template, view, partials) {
+    return defaultWriter.render(template, view, partials);
+  };
+
+  // This is here for backwards compatibility with 0.4.x.
+  mustache.to_html = function (template, view, partials, send) {
+    var result = mustache.render(template, view, partials);
+
+    if (isFunction(send)) {
+      send(result);
+    } else {
+      return result;
+    }
+  };
+
+  // Export the escaping function so that the user may override it.
+  // See https://github.com/janl/mustache.js/issues/244
+  mustache.escape = escapeHtml;
+
+  // Export these mainly for testing, but also for advanced usage.
+  mustache.Scanner = Scanner;
+  mustache.Context = Context;
+  mustache.Writer = Writer;
+
+}));
+/* jshint ignore:end */
diff --git a/lib/amd/src/notification.js b/lib/amd/src/notification.js
new file mode 100644 (file)
index 0000000..1880b80
--- /dev/null
@@ -0,0 +1,105 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Wrapper for the YUI M.core.notification class. Allows us to
+ * use the YUI version in AMD code until it is replaced.
+ *
+ * @module     core/notification
+ * @class      notification
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/yui'], function(Y) {
+
+    // Private variables and functions.
+
+    return /** @alias module:core/notification */ {
+        // Public variables and functions.
+        /**
+         * Wrap M.core.alert.
+         *
+         * @method alert
+         * @param {string} title
+         * @param {string} message
+         * @param {string} yesLabel
+         */
+        alert: function(title, message, yesLabel) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-alert', function () {
+                var alert = new M.core.alert({
+                    title : title,
+                    message : message,
+                    yesLabel: yesLabel
+                });
+
+                alert.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.confirm.
+         *
+         * @method confirm
+         * @param {string} title
+         * @param {string} question
+         * @param {string} yesLabel
+         * @param {string} noLabel
+         * @param {function} callback
+         */
+        confirm: function(title, question, yesLabel, noLabel, callback) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-confirm', function () {
+                var modal = new M.core.confirm({
+                    title : title,
+                    question : question,
+                    yesLabel: yesLabel,
+                    noLabel: noLabel
+                });
+
+                modal.on('complete-yes', function() {
+                    callback();
+                });
+                modal.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.exception.
+         *
+         * @method exception
+         * @param {Error} ex
+         */
+        exception: function(ex) {
+            // Fudge some parameters.
+            if (ex.backtrace) {
+                ex.lineNumber = ex.backtrace[0].line;
+                ex.fileName = ex.backtrace[0].file;
+                ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 20);
+                ex.stack = ex.debuginfo;
+                ex.name = ex.errorcode;
+            }
+            Y.use('moodle-core-notification-exception', function () {
+                var modal = new M.core.exception(ex);
+
+                modal.show();
+            });
+        }
+    };
+});
diff --git a/lib/amd/src/str.js b/lib/amd/src/str.js
new file mode 100644 (file)
index 0000000..d342de1
--- /dev/null
@@ -0,0 +1,141 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Fetch and render language strings.
+ * Hooks into the old M.str global - but can also fetch missing strings on the fly.
+ *
+ * @module     core/str
+ * @class      str
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['jquery', 'core/ajax'], function($, ajax) {
+
+
+    return /** @alias module:core/str */ {
+        // Public variables and functions.
+        /**
+         * Return a promise object that will be resolved into a string eventually (maybe immediately).
+         *
+         * @method get_string
+         * @param {string} key The language string key
+         * @param {string} component The language string component
+         * @param {string} param The param for variable expansion in the string.
+         * @param {string} lang The users language - if not passed it is deduced.
+         * @return {Promise}
+         */
+        get_string: function(key, component, param, lang) {
+            var deferred = $.Deferred();
+
+            var request = this.get_strings([{
+                key: key,
+                component: component,
+                param: param,
+                lang: lang
+            }]);
+
+            request.done(function(results) {
+                deferred.resolve(results[0]);
+            }).fail(function(ex) {
+                deferred.reject(ex);
+            });
+
+            return deferred.promise();
+        },
+
+        /**
+         * Make a batch request to load a set of strings
+         *
+         * @method get_strings
+         * @param {Object[]} requests Array of { key: key, component: component, param: param, lang: lang };
+         *                                      See get_string for more info on these args.
+         * @return {Promise}
+         */
+        get_strings: function(requests) {
+
+            var deferred = $.Deferred();
+            var results = [];
+            var i = 0;
+            var missing = false;
+            var request;
+
+            for (i = 0; i < requests.length; i++) {
+                request = requests[i];
+                if (typeof M.str[request.component] === "undefined" ||
+                        typeof M.str[request.component][request.key] === "undefined") {
+                    missing = true;
+                }
+            }
+
+            if (!missing) {
+                // We have all the strings already.
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    results[i] = M.util.get_string(request.key, request.component, request.param);
+                }
+                deferred.resolve(results);
+            } else {
+                // Something is missing, we might as well load them all.
+                var ajaxrequests = [];
+
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    if (typeof request.lang === "undefined") {
+                        request.lang = $('html').attr('lang');
+                    }
+                    ajaxrequests.push({
+                        methodname: 'core_get_string',
+                        args: {
+                            stringid: request.key,
+                            component: request.component,
+                            lang: request.lang,
+                            stringparams: []
+                        }
+                    });
+                }
+
+                var deferreds = ajax.call(ajaxrequests);
+                $.when.apply(null, deferreds).done(
+                    function() {
+                        // Turn the list of arguments (unknown length) into a real array.
+                        var i = 0;
+                        for (i = 0; i < arguments.length; i++) {
+                            request = requests[i];
+                            // Cache all the string templates.
+                            if (typeof M.str[request.component] === "undefined") {
+                                M.str[request.component] = [];
+                            }
+                            M.str[request.component][request.key] = arguments[i];
+                            // And set the results.
+                            results[i] = M.util.get_string(request.key, request.component, request.param).trim();
+                        }
+                        deferred.resolve(results);
+                    }
+                ).fail(
+                    function(ex) {
+                        deferred.reject(ex);
+                    }
+                );
+            }
+
+            return deferred.promise();
+        }
+    };
+});
diff --git a/lib/amd/src/templates.js b/lib/amd/src/templates.js
new file mode 100644 (file)
index 0000000..4f581e5
--- /dev/null
@@ -0,0 +1,374 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Template renderer for Moodle. Load and render Moodle templates with Mustache.
+ *
+ * @module     core/templates
+ * @package    core
+ * @class      templates
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define([ 'core/mustache',
+         'jquery',
+         'core/ajax',
+         'core/str',
+         'core/notification',
+         'core/url',
+         'core/config'
+       ],
+       function(mustache, $, ajax, str, notification, coreurl, config) {
+
+    // Private variables and functions.
+
+    /** @var {string[]} templateCache - Cache of already loaded templates */
+    var templateCache = {};
+
+    /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
+    var requiredStrings = [];
+
+    /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
+    var requiredJS = [];
+
+    /** @var {Number} uniqid Incrementing value that is changed for every call to render */
+    var uniqid = 1;
+
+    /** @var {String} themeName for the current render */
+    var currentThemeName = '';
+
+    /**
+     * Render image icons.
+     *
+     * @method pixHelper
+     * @private
+     * @param {string} sectionText The text to parse arguments from.
+     * @return {string}
+     */
+    var pixHelper = function(sectionText) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var text = '';
+        var result;
+
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            text = parts.join(',').trim();
+        }
+        var url = coreurl.imageUrl(key, component);
+
+        var templatecontext = {
+            attributes: [
+                { name: 'src', value: url},
+                { name: 'alt', value: text},
+                { name: 'class', value: 'smallicon'}
+            ]
+        };
+        // We forced loading of this early, so it will be in the cache.
+        var template = templateCache[currentThemeName + '/core/pix_icon'];
+        result = mustache.render(template, templatecontext, partialHelper);
+        return result.trim();
+    };
+
+    /**
+     * Load a partial from the cache or ajax.
+     *
+     * @method partialHelper
+     * @private
+     * @param {string} name The partial name to load.
+     * @return {string}
+     */
+    var partialHelper = function(name) {
+        var template = '';
+
+        getTemplate(name, false).done(
+            function(source) {
+                template = source;
+            }
+        ).fail(notification.exception);
+
+        return template;
+    };
+
+    /**
+     * Render blocks of javascript and save them in an array.
+     *
+     * @method jsHelper
+     * @private
+     * @param {string} sectionText The text to save as a js block.
+     * @param {function} helper Used to render the block.
+     * @return {string}
+     */
+    var jsHelper = function(sectionText, helper) {
+        requiredJS.push(helper(sectionText, this));
+        return '';
+    };
+
+    /**
+     * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
+     * into a get_string call.
+     *
+     * @method stringHelper
+     * @private
+     * @param {string} sectionText The text to parse the arguments from.
+     * @param {function} helper Used to render subsections of the text.
+     * @return {string}
+     */
+    var stringHelper = function(sectionText, helper) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var param = '';
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            param = parts.join(',').trim();
+        }
+
+        if (param !== '') {
+            // Allow variable expansion in the param part only.
+            param = helper(param, this);
+        }
+        // Allow json formatted $a arguments.
+        if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
+            param = JSON.parse(param);
+        }
+
+        var index = requiredStrings.length;
+        requiredStrings.push({key: key, component: component, param: param});
+        return '{{_s' + index + '}}';
+    };
+
+    /**
+     * Add some common helper functions to all context objects passed to templates.
+     * These helpers match exactly the helpers available in php.
+     *
+     * @method addHelpers
+     * @private
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName We set this multiple times, because there are async calls.
+     */
+    var addHelpers = function(context, themeName) {
+        currentThemeName = themeName;
+        requiredStrings = [];
+        requiredJS = [];
+        context.uniqid = uniqid++;
+        context.str = function() { return stringHelper; };
+        context.pix = function() { return pixHelper; };
+        context.js = function() { return jsHelper; };
+        context.globals = { config : config };
+        context.currentTheme = themeName;
+    };
+
+    /**
+     * Get all the JS blocks from the last rendered template.
+     *
+     * @method getJS
+     * @private
+     * @param {string[]} strings Replacement strings.
+     * @return {string}
+     */
+    var getJS = function(strings) {
+        var js = '';
+        if (requiredJS.length > 0) {
+            js = requiredJS.join(";\n");
+        }
+
+        var i = 0;
+
+        for (i = 0; i < strings.length; i++) {
+            js = js.replace('{{_s' + i + '}}', strings[i]);
+        }
+        // Re-render to get the final strings.
+        return js;
+    };
+
+    /**
+     * Render a template and then call the callback with the result.
+     *
+     * @method doRender
+     * @private
+     * @param {string} templateSource The mustache template to render.
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName Name of the current theme.
+     * @return {Promise} object
+     */
+    var doRender = function(templateSource, context, themeName) {
+        var deferred = $.Deferred();
+
+        currentThemeName = themeName;
+
+        // Make sure we fetch this first.
+        var loadPixTemplate = getTemplate('core/pix_icon', true);
+
+        loadPixTemplate.done(
+            function() {
+                addHelpers(context, themeName);
+                var result = '';
+                try {
+                    result = mustache.render(templateSource, context, partialHelper);
+                } catch (ex) {
+                    deferred.reject(ex);
+                }
+
+                if (requiredStrings.length > 0) {
+                    str.get_strings(requiredStrings).done(
+                        function(strings) {
+                            var i;
+
+                            // Why do we not do another call the render here?
+                            //
+                            // Because that would expose DOS holes. E.g.
+                            // I create an assignment called "{{fish" which
+                            // would get inserted in the template in the first pass
+                            // and cause the template to die on the second pass (unbalanced).
+                            for (i = 0; i < strings.length; i++) {
+                                result = result.replace('{{_s' + i + '}}', strings[i]);
+                            }
+                            deferred.resolve(result.trim(), getJS(strings));
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                } else {
+                    deferred.resolve(result.trim(), getJS([]));
+                }
+            }
+        ).fail(
+            function(ex) {
+                deferred.reject(ex);
+            }
+        );
+        return deferred.promise();
+    };
+
+    /**
+     * Load a template from the cache or ajax request.
+     *
+     * @method getTemplate
+     * @private
+     * @param {string} templateName - should consist of the component and the name of the template like this:
+     *                              core/menu (lib/templates/menu.mustache) or
+     *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+     * @return {Promise} JQuery promise object resolved when the template has been fetched.
+     */
+    var getTemplate = function(templateName, async) {
+        var deferred = $.Deferred();
+        var parts = templateName.split('/');
+        var component = parts.shift();
+        var name = parts.shift();
+
+        var searchKey = currentThemeName + '/' + templateName;
+
+        if (searchKey in templateCache) {
+            deferred.resolve(templateCache[searchKey]);
+        } else {
+            var promises = ajax.call([{
+                methodname: 'core_output_load_template',
+                args:{
+                    component: component,
+                    template: name,
+                    themename: currentThemeName
+                }
+            }], async);
+            promises[0].done(
+                function (templateSource) {
+                    templateCache[searchKey] = templateSource;
+                    deferred.resolve(templateSource);
+                }
+            ).fail(
+                function (ex) {
+                    deferred.reject(ex);
+                }
+            );
+        }
+        return deferred.promise();
+    };
+
+    return /** @alias module:core/templates */ {
+        // Public variables and functions.
+        /**
+         * Load a template and call doRender on it.
+         *
+         * @method render
+         * @private
+         * @param {string} templateName - should consist of the component and the name of the template like this:
+         *                              core/menu (lib/templates/menu.mustache) or
+         *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+         * @param {Object} context - Could be array, string or simple value for the context of the template.
+         * @param {string} themeName - Name of the current theme.
+         * @return {Promise} JQuery promise object resolved when the template has been rendered.
+         */
+        render: function(templateName, context, themeName) {
+            var deferred = $.Deferred();
+
+            if (typeof (themeName) === "undefined") {
+                // System context by default.
+                themeName = config.theme;
+            }
+
+            currentThemeName = themeName;
+
+            var loadTemplate = getTemplate(templateName, true);
+
+            loadTemplate.done(
+                function(templateSource) {
+                    var renderPromise = doRender(templateSource, context, themeName);
+
+                    renderPromise.done(
+                        function(result, js) {
+                            deferred.resolve(result, js);
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                }
+            ).fail(
+                function(ex) {
+                    deferred.reject(ex);
+                }
+            );
+            return deferred.promise();
+        },
+
+        /**
+         * Execute a block of JS returned from a template.
+         * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
+         *
+         * @method runTemplateJS
+         * @private
+         * @param {string} source - A block of javascript.
+         */
+        runTemplateJS: function(source) {
+            var newscript = $('<script>').attr('type','text/javascript').html(source);
+            $('head').append(newscript);
+        }
+    };
+});
diff --git a/lib/amd/src/url.js b/lib/amd/src/url.js
new file mode 100644 (file)
index 0000000..908c898
--- /dev/null
@@ -0,0 +1,91 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * URL utility functions.
+ *
+ * @module     core/url
+ * @package    core
+ * @class      url
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/config'], function(config) {
+
+
+    return /** @alias module:core/url */ {
+        // Public variables and functions.
+        /**
+         * Generate a style tag referencing this sheet and add it to the head of the page.
+         *
+         * @method fileUrl
+         * @param {string} sheet The style sheet name. Must exist in the theme, or one of it's parents.
+         * @return {string}
+         */
+        fileUrl: function(relativeScript, slashArg) {
+
+            var url = config.wwwroot + relativeScript;
+
+            // Force a /
+            if (slashArg.charAt(0) != '/') {
+                slashArg = '/' + slashArg;
+            }
+            if (config.slasharguments) {
+                url += slashArg;
+            } else {
+                url += '?file=' + encodeURIComponent(slashArg);
+            }
+            return url;
+        },
+
+        /**
+         * Take a path relative to the moodle basedir and do some fixing (see class moodle_url in php).
+         *
+         * @method relativeUrl
+         * @param {string} relativePath The path relative to the moodle basedir.
+         * @return {string}
+         */
+        relativeUrl: function(relativePath) {
+
+            if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://')) {
+                throw new Error('relativeUrl function does not accept absolute urls');
+            }
+
+            // Fix non-relative paths;
+            if (relativePath.charAt(0) != '/') {
+                relativePath = '/' + relativePath;
+            }
+
+            // Fix admin urls.
+            if (config.admin !== 'admin') {
+                relativePath = relativePath.replace(/^\/admin\//, '/' + config.admin + '/');
+            }
+            return config.wwwroot + relativePath;
+        },
+
+        /**
+         * Wrapper for image_url function.
+         *
+         * @method imageUrl
+         * @param {string} imagename The image name (e.g. t/edit).
+         * @param {string} component The component (e.g. mod_feedback).
+         * @return {string}
+         */
+        imageUrl: function(imagename, component) {
+            return M.util.image_url(imagename, component);
+        }
+    };
+});
diff --git a/lib/amd/src/yui.js b/lib/amd/src/yui.js
new file mode 100644 (file)
index 0000000..af58326
--- /dev/null
@@ -0,0 +1,30 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expose the global YUI variable. Note: This is only for scripts that are writing AMD
+ * wrappers for YUI functionality. This is not for plugins.
+ *
+ * @module     core/yui
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(function() {
+
+    // This module exposes only the global yui instance.
+    return /** @alias module:core/yui */ Y;
+});
diff --git a/lib/classes/output/external.php b/lib/classes/output/external.php
new file mode 100644 (file)
index 0000000..8e53119
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use core_component;
+use moodle_exception;
+use context_system;
+use theme_config;
+
+/**
+ * This class contains a list of webservice functions related to output.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class external extends external_api {
+    /**
+     * Returns description of load_template() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function load_template_parameters() {
+        return new external_function_parameters(
+                array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
+                      'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'),
+                      'themename' => new external_value(PARAM_ALPHANUMEXT, 'The current theme.'),
+                         )
+            );
+    }
+
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function load_template_is_allowed_from_ajax() {
+        return true;
+    }
+
+    /**
+     * Return a mustache template, and all the strings it requires.
+     *
+     * @param string $component The component that holds the template.
+     * @param string $templatename The name of the template.
+     * @param string $themename The name of the current theme.
+     * @return string the template
+     */
+    public static function load_template($component, $template, $themename) {
+        global $DB, $CFG, $PAGE;
+
+        $params = self::validate_parameters(self::load_template_parameters(),
+                                            array('component' => $component,
+                                                  'template' => $template,
+                                                  'themename' => $themename));
+
+        $component = $params['component'];
+        $template = $params['template'];
+        $themename = $params['themename'];
+
+        // Check if this is a valid component.
+        $componentdir = core_component::get_component_directory($component);
+        if (empty($componentdir)) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+        // Places to look.
+        $candidates = array();
+        // Theme dir.
+        $root = $CFG->dirroot;
+
+        $themeconfig = theme_config::load($themename);
+
+        $candidate = "${root}/theme/${themename}/templates/${component}/${template}.mustache";
+        $candidates[] = $candidate;
+        // Theme parents dir.
+        foreach ($themeconfig->parents as $theme) {
+            $candidate = "${root}/theme/${theme}/templates/${component}/${template}.mustache";
+            $candidates[] = $candidate;
+        }
+        // Component dir.
+        $candidate = "${componentdir}/templates/${template}.mustache";
+        $candidates[] = $candidate;
+        $templatestr = false;
+        foreach ($candidates as $candidate) {
+            if (file_exists($candidate)) {
+                $templatestr = file_get_contents($candidate);
+                break;
+            }
+        }
+        if ($templatestr === false) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+
+        return $templatestr;
+    }
+
+    /**
+     * Returns description of load_template() result value.
+     *
+     * @return external_description
+     */
+    public static function load_template_returns() {
+        return new external_value(PARAM_RAW, 'template');
+    }
+}
+
diff --git a/lib/classes/output/mustache_filesystem_loader.php b/lib/classes/output/mustache_filesystem_loader.php
new file mode 100644 (file)
index 0000000..efaab81
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use coding_exception;
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_filesystem_loader extends \Mustache_Loader_FilesystemLoader {
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     * Strips the leading component as we are already limited to the correct directories.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name) {
+        if (strpos($name, '/') === false) {
+            throw new coding_exception('Templates names must be specified as "componentname/templatename" (' . $name . ' requested) ');
+        }
+        list($component, $templatename) = explode('/', $name, 2);
+        return parent::getFileName($templatename);
+    }
+}
diff --git a/lib/classes/output/mustache_javascript_helper.php b/lib/classes/output/mustache_javascript_helper.php
new file mode 100644 (file)
index 0000000..7ba9b2a
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Store a list of JS calls to insert at the end of the page.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_javascript_helper {
+
+    /** @var page_requirements_manager $requires - Page requirements manager for collecting JS calls. */
+    private $requires = null;
+
+    /**
+     * Create new instance of mustache javascript helper.
+     *
+     * @param page_requirements_manager $requires Page requirements manager.
+     */
+    public function __construct($requires) {
+        $this->requires = $requires;
+    }
+
+    /**
+     * Add the block of text to the page requires so it is appended in the footer. The
+     * content of the block can contain further mustache tags which will be resolved.
+     *
+     * @param string $text The script content of the section.
+     * @param \Mustache_LambdaHelper $helper Used to render the content of this block.
+     */
+    public function help($text, \Mustache_LambdaHelper $helper) {
+        $this->requires->js_amd_inline($helper->render($text));
+    }
+}
diff --git a/lib/classes/output/mustache_pix_helper.php b/lib/classes/output/mustache_pix_helper.php
new file mode 100644 (file)
index 0000000..3b0d261
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper render pix icons.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use renderer_base;
+
+/**
+ * This class will call pix_icon with the section content.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_pix_helper {
+
+    /** @var renderer_base $renderer A reference to the renderer in use */
+    private $renderer;
+
+    /**
+     * Save a reference to the renderer.
+     * @param renderer_base $renderer
+     */
+    public function __construct(renderer_base $renderer) {
+        $this->renderer = $renderer;
+    }
+
+    /**
+     * Read a pix icon name from a template and get it from pix_icon.
+     *
+     * {{#pix}}t/edit,component,Anything else is alt text{{/pix}}
+     *
+     * The args are comma separated and only the first is required.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function pix($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+        $text = strtok("");
+        // Allow mustache tags in the last argument.
+        $text = $helper->render($text);
+
+        return trim($this->renderer->pix_icon($key, $text, $component));
+    }
+}
+
diff --git a/lib/classes/output/mustache_string_helper.php b/lib/classes/output/mustache_string_helper.php
new file mode 100644 (file)
index 0000000..1b5d52a
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use stdClass;
+
+/**
+ * This class will load language strings in a template.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_string_helper {
+
+    /**
+     * Read a lang string from a template and get it from get_string.
+     *
+     * Some examples for calling this from a template are:
+     *
+     * {{#str}}activity{{/str}}
+     * {{#str}}actionchoice, core, {{#str}}delete{{/str}}{{/str}} (Nested)
+     * {{#str}}addinganewto, core, {"what":"This", "to":"That"}{{/str}} (Complex $a)
+     *
+     * The args are comma separated and only the first is required.
+     * The last is a $a argument for get string. For complex data here, use JSON.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function str($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+
+        $a = new stdClass();
+
+        $next = strtok('');
+        $next = trim($next);
+        if ((strpos($next, '{') === 0) && (strpos($next, '{{') !== 0)) {
+            $rawjson = $helper->render($next);
+            $a = json_decode($rawjson);
+        } else {
+            $a = $helper->render($next);
+        }
+        return get_string($key, $component, $a);
+    }
+}
+
diff --git a/lib/classes/output/mustache_uniqid_helper.php b/lib/classes/output/mustache_uniqid_helper.php
new file mode 100644 (file)
index 0000000..0dbb773
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Lazy create a uniqid per instance of the class. The id is only generated
+ * when this class it converted to a string.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_uniqid_helper {
+
+    /** @var string $uniqid The unique id */
+    private $uniqid = null;
+
+    /**
+     * Init the random variable and return it as a string.
+     *
+     * @return string random id.
+     */
+    public function __toString() {
+        if ($this->uniqid === null) {
+            $this->uniqid = \html_writer::random_id(uniqid());
+        }
+        return $this->uniqid;
+    }
+}
index 0631dd8..00d90d1 100644 (file)
@@ -950,6 +950,13 @@ $functions = array(
         'type'        => 'write',
         'capabilities'=> 'moodle/calendar:manageentries', 'moodle/calendar:manageownentries', 'moodle/calendar:managegroupentries'
     ),
+
+    'core_output_load_template' => array(
+        'classname'   => 'core\output\external',
+        'methodname'  => 'load_template',
+        'description' => 'Load a template for a renderable',
+        'type'        => 'read'
+    ),
 );
 
 $services = array(
diff --git a/lib/mustache/CONTRIBUTING.md b/lib/mustache/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..c0b323d
--- /dev/null
@@ -0,0 +1,35 @@
+# Contributions welcome!
+
+
+### Here's a quick guide:
+
+ 1. [Fork the repo on GitHub](https://github.com/bobthecow/mustache.php).
+
+ 2. Update submodules: `git submodule update --init`
+
+ 3. 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.
+
+ 4. 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!
+
+ 5. Make the tests pass.
+
+ 6. 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)
diff --git a/lib/mustache/LICENSE b/lib/mustache/LICENSE
new file mode 100644 (file)
index 0000000..0bdbc04
--- /dev/null
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2010-2014 Justin Hileman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/mustache/README.md b/lib/mustache/README.md
new file mode 100644 (file)
index 0000000..b33b355
--- /dev/null
@@ -0,0 +1,71 @@
+Mustache.php
+============
+
+A [Mustache](http://mustache.github.com/) implementation in PHP.
+
+[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg)](http://travis-ci.org/bobthecow/mustache.php)
+[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+
+
+Usage
+-----
+
+A quick example:
+
+```php
+<?php
+$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:
+
+```html+jinja
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{taxed_value}}, after taxes.
+{{/in_ca}}
+```
+
+
+Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
+
+```php
+<?php
+class Chris {
+    public $name  = "Chris";
+    public $value = 10000;
+
+    public function taxed_value() {
+        return $this->value - ($this->value * 0.4);
+    }
+
+    public $in_ca = true;
+}
+```
+
+
+And render it:
+
+```php
+<?php
+$m = new Mustache_Engine;
+$chris = new Chris;
+echo $m->render($template, $chris);
+```
+
+
+And That's Not All!
+-------------------
+
+Read [the Mustache.php documentation](https://github.com/bobthecow/mustache.php/wiki/Home) for more information.
+
+
+See Also
+--------
+
+ * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
+ * [mustache(5)](http://mustache.github.com/mustache.5.html) man page.
diff --git a/lib/mustache/composer.json b/lib/mustache/composer.json
new file mode 100644 (file)
index 0000000..2969d03
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "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"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "*"
+    },
+    "autoload": {
+        "psr-0": { "Mustache": "src/" }
+    }
+}
diff --git a/lib/mustache/readme_moodle.txt b/lib/mustache/readme_moodle.txt
new file mode 100644 (file)
index 0000000..31ce19d
--- /dev/null
@@ -0,0 +1,11 @@
+Description of Mustache library import into moodle.
+
+Download from https://github.com/bobthecow/mustache.php
+
+Delete folder "test"
+
+Delete phpunit.xml.dist
+
+Delete hidden files ".*"
+
+Delete folder "bin"
diff --git a/lib/mustache/src/Mustache/Autoloader.php b/lib/mustache/src/Mustache/Autoloader.php
new file mode 100644 (file)
index 0000000..8c8e906
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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) {
+            $baseDir = dirname(__FILE__).'/..';
+        }
+
+        // realpath doesn't always work, for example, with stream URIs
+        $realDir = realpath($baseDir);
+        if (is_dir($realDir)) {
+            $this->baseDir = $realDir;
+        } else {
+            $this->baseDir = $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;
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache.php b/lib/mustache/src/Mustache/Cache.php
new file mode 100644 (file)
index 0000000..c8fc5d5
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache interface.
+ *
+ * Interface for caching and loading Mustache_Template classes
+ * generated by the Mustache_Compiler.
+ */
+interface Mustache_Cache
+{
+    /**
+     * Load a compiled Mustache_Template class from cache.
+     *
+     * @param string $key
+     *
+     * @return boolean indicates successfully class load
+     */
+    public function load($key);
+
+    /**
+     * Cache and load a compiled Mustache_Template class.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value);
+}
diff --git a/lib/mustache/src/Mustache/Cache/AbstractCache.php b/lib/mustache/src/Mustache/Cache/AbstractCache.php
new file mode 100644 (file)
index 0000000..98b6451
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Cache class.
+ *
+ * Provides logging support to child implementations.
+ *
+ * @abstract
+ */
+abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
+{
+    private $logger = null;
+
+    /**
+     * Get the current logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set a 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 Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * 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
+     */
+    protected function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/FilesystemCache.php b/lib/mustache/src/Mustache/Cache/FilesystemCache.php
new file mode 100644 (file)
index 0000000..120ad2d
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache filesystem implementation.
+ *
+ * A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
+ *
+ *     $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache');
+ *     $cache->cache($className, $compiledSource);
+ *
+ * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
+ */
+class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
+{
+    private $baseDir;
+    private $fileMode;
+
+    /**
+     * Filesystem cache constructor.
+     *
+     * @param string $baseDir  Directory for compiled templates.
+     * @param int    $fileMode Override default permissions for cache files. Defaults to using the system-defined umask.
+     */
+    public function __construct($baseDir, $fileMode = null)
+    {
+        $this->baseDir = $baseDir;
+        $this->fileMode = $fileMode;
+    }
+
+    /**
+     * Load the class from cache using `require_once`.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        $fileName = $this->getCacheFilename($key);
+        if (!is_file($fileName)) {
+            return false;
+        }
+
+        require_once $fileName;
+
+        return true;
+    }
+
+    /**
+     * Cache and load the compiled class
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $fileName = $this->getCacheFilename($key);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Writing to template cache: "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $this->writeFile($fileName, $value);
+        $this->load($key);
+    }
+
+    /**
+     * Build the cache filename.
+     * Subclasses should override for custom cache directory structures.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    protected function getCacheFilename($name)
+    {
+        return sprintf('%s/%s.php', $this->baseDir, $name);
+    }
+
+    /**
+     * Create cache directory
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to create directory
+     *
+     * @param string $fileName
+     *
+     * @return string
+     */
+    private function buildDirectoryForFilename($fileName)
+    {
+        $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 Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+            }
+        }
+
+        return $dirName;
+    }
+
+    /**
+     * Write cache file
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to write file
+     *
+     * @param string $fileName
+     * @param string $value
+     *
+     * @return void
+     */
+    private function writeFile($fileName, $value)
+    {
+        $dirName = $this->buildDirectoryForFilename($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, $value)) {
+            if (@rename($tempFile, $fileName)) {
+                $mode = isset($this->fileMode) ? $this->fileMode : (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 Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/NoopCache.php b/lib/mustache/src/Mustache/Cache/NoopCache.php
new file mode 100644 (file)
index 0000000..d3a7e1f
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache in-memory implementation.
+ *
+ * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
+ * recommended for production use.
+ */
+class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
+{
+    /**
+     * Loads nothing. Move along.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        return false;
+    }
+
+    /**
+     * Loads the compiled Mustache Template class without caching.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $this->log(
+            Mustache_Logger::WARNING,
+            'Template cache disabled, evaluating "{className}" class at runtime',
+            array('className' => $key)
+        );
+        eval('?>' . $value);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Compiler.php b/lib/mustache/src/Mustache/Compiler.php
new file mode 100644 (file)
index 0000000..72d3414
--- /dev/null
@@ -0,0 +1,646 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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 $pragmas;
+    private $defaultPragmas = array();
+    private $sections;
+    private $source;
+    private $indentNextLine;
+    private $customEscape;
+    private $entityFlags;
+    private $charset;
+    private $strictCallables;
+
+    /**
+     * 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')
+     * @param bool   $strictCallables (default: false)
+     * @param int    $entityFlags     (default: ENT_COMPAT)
+     *
+     * @return string Generated PHP source code
+     */
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
+    {
+        $this->pragmas         = $this->defaultPragmas;
+        $this->sections        = array();
+        $this->source          = $source;
+        $this->indentNextLine  = true;
+        $this->customEscape    = $customEscape;
+        $this->entityFlags     = $entityFlags;
+        $this->charset         = $charset;
+        $this->strictCallables = $strictCallables;
+
+        return $this->writeCode($tree, $name);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->pragmas[$pragma] = true;
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper function for walking the Mustache token parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException 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_PRAGMA:
+                    $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                    $code .= $this->section(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $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],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $code .= $this->partial(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $code .= $this->parent(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $node[Mustache_Tokenizer::NODES],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_ARG:
+                    $code .= $this->blockArg(
+                        $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_BLOCK_VAR:
+                    $code .= $this->blockVar(
+                        $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_COMMENT:
+                    break;
+
+                case Mustache_Tokenizer::T_ESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED_2:
+                    $code .= $this->variable(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_TEXT:
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                    break;
+
+                default:
+                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
+            }
+        }
+
+        return $code;
+    }
+
+    const KLASS = '<?php
+
+        class %s extends Mustache_Template
+        {
+            private $lambdaHelper;%s
+
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        %s
+        }';
+
+    const KLASS_NO_LAMBDAS = '<?php
+
+        class %s extends Mustache_Template
+        {%s
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        }';
+
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
+    /**
+     * 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);
+        $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
+
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
+    }
+
+    const BLOCK_VAR = '
+        $value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
+        if ($value && !is_array($value) && !is_object($value)) {
+            $buffer .= $value;
+        } else {
+            %s
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block variable 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 PHP source code
+     */
+    private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $id = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $this->walk($nodes, 2));
+    }
+
+    const BLOCK_ARG = '
+        // %s block_arg
+        $value = $this->section%s($context, $indent, true);
+        $newContext[%s] = %s$value;
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block argument 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 PHP source code
+     */
+    private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $key = $this->section($nodes, $id, array(), $start, $end, $otag, $ctag, $level, true);
+        $id  = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
+    }
+
+    const SECTION_CALL = '
+        // %s section
+        $value = $context->%s(%s);%s
+        $buffer .= $this->section%s($context, $indent, $value);
+    ';
+
+    const SECTION = '
+        private function section%s(Mustache_Context $context, $indent, $value)
+        {
+            $buffer = \'\';
+            if (%s) {
+                $source = %s;
+                $result = call_user_func($value, $source, $this->lambdaHelper);
+                if (strpos($result, \'{{\') === false) {
+                    $buffer .= $result;
+                } else {
+                    $buffer .= $this->mustache
+                        ->loadLambda((string) $result%s)
+                        ->renderInternal($context);
+                }
+            } 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 string[] $filters Array of filters
+     * @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
+     * @param bool     $arg     (default: false)
+     *
+     * @return string Generated section PHP source code
+     */
+    private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level, $arg = false)
+    {
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
+
+        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, $callable, $source, $delims, $this->walk($nodes, 2));
+        }
+
+        if ($arg === true) {
+            return $key;
+        } else {
+            $method  = $this->getFindMethod($id);
+            $id      = var_export($id, true);
+            $filters = $this->getFilters($filters, $level);
+
+            return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
+        }
+    }
+
+    const INVERTED_SECTION = '
+        // %s inverted section
+        $value = $context->%s(%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 string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated inverted section PHP source code
+     */
+    private function invertedSection($nodes, $id, $filters, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = var_export($id, true);
+        $filters = $this->getFilters($filters, $level);
+
+        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
+    }
+
+    const PARTIAL_INDENT = ', $indent . %s';
+    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)
+    {
+        if ($indent !== '') {
+            $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
+        } else {
+            $indentParam = '';
+        }
+
+        return sprintf(
+            $this->prepare(self::PARTIAL, $level),
+            var_export($id, true),
+            $indentParam
+        );
+    }
+
+    const PARENT = '
+        %s
+
+        if ($parent = $this->mustache->LoadPartial(%s)) {
+            $context->pushBlockContext($newContext);
+            $buffer .= $parent->renderInternal($context, $indent);
+            $context->popBlockContext();
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance parent call PHP source.
+     *
+     * @param string $id       Parent tag name
+     * @param string $indent   Whitespace indent to apply to parent
+     * @param array  $children Child nodes
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function parent($id, $indent, array $children, $level)
+    {
+        $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
+
+        return sprintf(
+            $this->prepare(self::PARENT, $level),
+            $this->walk($realChildren, $level),
+            var_export($id, true),
+            var_export($indent, true)
+        );
+    }
+
+    /**
+     * Helper method for filtering out non-block-arg tokens.
+     *
+     * @param array $node
+     *
+     * @return boolean True if $node is a block arg token.
+     */
+    private static function onlyBlockArgs(array $node)
+    {
+        return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
+    }
+
+    const VARIABLE = '
+        $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
+        $buffer .= %s%s;
+    ';
+
+    /**
+     * Generate Mustache Template variable interpolation PHP source.
+     *
+     * @param string   $id      Variable name
+     * @param string[] $filters Array of filters
+     * @param boolean  $escape  Escape the variable value for output?
+     * @param int      $level
+     *
+     * @return string Generated variable interpolation PHP source
+     */
+    private function variable($id, $filters, $escape, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = ($method !== 'last') ? var_export($id, true) : '';
+        $filters = $this->getFilters($filters, $level);
+        $value   = $escape ? $this->getEscape() : '$value';
+
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+    }
+
+    const FILTER = '
+        $filter = $context->%s(%s);
+        if (!(%s)) {
+            throw new Mustache_Exception_UnknownFilterException(%s);
+        }
+        $value = call_user_func($filter, $value);%s
+    ';
+
+    /**
+     * Generate Mustache Template variable filtering PHP source.
+     *
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated filter PHP source
+     */
+    private function getFilters(array $filters, $level)
+    {
+        if (empty($filters)) {
+            return '';
+        }
+
+        $name     = array_shift($filters);
+        $method   = $this->getFindMethod($name);
+        $filter   = ($method !== 'last') ? var_export($name, true) : '';
+        $callable = $this->getCallable('$filter');
+        $msg      = var_export($name, true);
+
+        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
+    }
+
+    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)
+    {
+        $indentNextLine = (substr($text, -1) === "\n");
+        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        $this->indentNextLine = $indentNextLine;
+
+        return $code;
+    }
+
+    /**
+     * 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)
+     * @param boolean $appendNewline  Append a newline to the snippet? (default: false)
+     *
+     * @return string PHP source code snippet
+     */
+    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
+    {
+        $text = ($prependNewline ? "\n" : '').trim($text);
+        if ($prependNewline) {
+            $bonus++;
+        }
+        if ($appendNewline) {
+            $text .= "\n";
+        }
+
+        return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
+    }
+
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %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);
+        }
+
+        return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), 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';
+        }
+
+        if (strpos($id, '.') === false) {
+            return 'find';
+        }
+
+        return 'findDot';
+    }
+
+    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
+    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+    /**
+     * Helper function to compile strict vs lax "is callable" logic.
+     *
+     * @param string $variable (default: '$value')
+     *
+     * @return string "is callable" logic
+     */
+    private function getCallable($variable = '$value')
+    {
+        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+        return sprintf($tpl, $variable, $variable);
+    }
+
+    const LINE_INDENT = '$indent . ';
+
+    /**
+     * Get the current $indent prefix to write to the buffer.
+     *
+     * @return string "$indent . " or ""
+     */
+    private function flushIndent()
+    {
+        if (!$this->indentNextLine) {
+            return '';
+        }
+
+        $this->indentNextLine = false;
+
+        return self::LINE_INDENT;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Context.php b/lib/mustache/src/Mustache/Context.php
new file mode 100644 (file)
index 0000000..db03acc
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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();
+    private $blockStack = 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);
+    }
+
+    /**
+     * Push a new Context frame onto the block context stack.
+     *
+     * @param mixed $value Object or array to use for block context
+     */
+    public function pushBlockContext($value)
+    {
+        array_push($this->blockStack, $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);
+    }
+
+    /**
+     * Pop the last block Context frame from the stack.
+     *
+     * @return mixed Last block Context frame (object or array)
+     */
+    public function popBlockContext()
+    {
+        return array_pop($this->blockStack);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Find an argument in the block context stack.
+     *
+     * @param string $id
+     *
+     * @return mixed Variable value, or '' if not found.
+     */
+    public function findInBlock($id)
+    {
+        foreach ($this->blockStack as $context) {
+            if (array_key_exists($id, $context)) {
+                return $context[$id];
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * 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--) {
+            $frame = &$stack[$i];
+
+            switch (gettype($frame)) {
+                case 'object':
+                    if (!($frame instanceof Closure)) {
+                        // Note that is_callable() *will not work here*
+                        // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
+                        if (method_exists($frame, $id)) {
+                            return $frame->$id();
+                        }
+
+                        if (isset($frame->$id)) {
+                            return $frame->$id;
+                        }
+
+                        if ($frame instanceof ArrayAccess && isset($frame[$id])) {
+                            return $frame[$id];
+                        }
+                    }
+                    break;
+
+                case 'array':
+                    if (array_key_exists($id, $frame)) {
+                        return $frame[$id];
+                    }
+                    break;
+            }
+        }
+
+        return '';
+    }
+}
diff --git a/lib/mustache/src/Mustache/Engine.php b/lib/mustache/src/Mustache/Engine.php
new file mode 100644 (file)
index 0000000..4cac394
--- /dev/null
@@ -0,0 +1,785 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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.7.0';
+    const SPEC_VERSION   = '1.1.2';
+
+    const PRAGMA_FILTERS = 'FILTERS';
+    const PRAGMA_BLOCKS  = 'BLOCKS';
+
+    // Known pragmas
+    private static $knownPragmas = array(
+        self::PRAGMA_FILTERS => true,
+        self::PRAGMA_BLOCKS  => true,
+    );
+
+    // Template cache
+    private $templates = array();
+
+    // Environment
+    private $templateClassPrefix = '__Mustache_';
+    private $cache;
+    private $lambdaCache;
+    private $cacheLambdaTemplates = false;
+    private $loader;
+    private $partialsLoader;
+    private $helpers;
+    private $escape;
+    private $entityFlags = ENT_COMPAT;
+    private $charset = 'UTF-8';
+    private $logger;
+    private $strictCallables = false;
+    private $pragmas = array();
+
+    // Services
+    private $tokenizer;
+    private $parser;
+    private $compiler;
+
+    /**
+     * 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 Mustache cache instance or a cache directory string for compiled templates.
+     *         // Mustache will not cache templates unless this is set.
+     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+     *
+     *         // 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,
+     *
+     *         // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
+     *         // sections are often too dynamic to benefit from caching.
+     *         'cache_lambda_templates' => true,
+     *
+     *         // 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');
+     *         },
+     *
+     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
+     *         'entity_flags' => ENT_QUOTES,
+     *
+     *         // 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_Logger_StreamLogger('php://stderr'),
+     *
+     *         // Only treat Closure instances and invokable classes as callable. If true, values like
+     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
+     *         // This currently defaults to false, but will default to true in v3.0.
+     *         'strict_callables' => true,
+     *
+     *         // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
+     *         // templates.
+     *         'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
+     *     );
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
+     *
+     * @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'])) {
+            $cache = $options['cache'];
+
+            if (is_string($cache)) {
+                $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
+                $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
+            }
+
+            $this->setCache($cache);
+        }
+
+        if (isset($options['cache_lambda_templates'])) {
+            $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
+        }
+
+        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 Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+            }
+
+            $this->escape = $options['escape'];
+        }
+
+        if (isset($options['entity_flags'])) {
+          $this->entityFlags = $options['entity_flags'];
+        }
+
+        if (isset($options['charset'])) {
+            $this->charset = $options['charset'];
+        }
+
+        if (isset($options['logger'])) {
+            $this->setLogger($options['logger']);
+        }
+
+        if (isset($options['strict_callables'])) {
+            $this->strictCallables = $options['strict_callables'];
+        }
+
+        if (isset($options['pragmas'])) {
+            foreach ($options['pragmas'] as $pragma) {
+                if (!isset(self::$knownPragmas[$pragma])) {
+                    throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
+                }
+                $this->pragmas[$pragma] = true;
+            }
+        }
+    }
+
+    /**
+     * Shortcut 'render' invocation.
+     *
+     * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Template::render
+     *
+     * @param string $template
+     * @param mixed  $context  (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($template, $context = array())
+    {
+        return $this->loadTemplate($template)->render($context);
+    }
+
+    /**
+     * Get the current Mustache escape callback.
+     *
+     * @return callable|null
+     */
+    public function getEscape()
+    {
+        return $this->escape;
+    }
+
+    /**
+     * Get the current Mustache entitity type to escape.
+     *
+     * @return int
+     */
+    public function getEntityFlags()
+    {
+        return $this->entityFlags;
+    }
+
+    /**
+     * Get the current Mustache character set.
+     *
+     * @return string
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Get the current globally enabled pragmas.
+     *
+     * @return array
+     */
+    public function getPragmas()
+    {
+        return array_keys($this->pragmas);
+    }
+
+    /**
+     * 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 Mustache_Exception_RuntimeException If the current Loader instance is immutable
+     *
+     * @param array $partials (default: array())
+     */
+    public function setPartials(array $partials = array())
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+        }
+
+        $this->partialsLoader->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 Mustache_Exception_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 Mustache_Exception_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 Logger instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
+     *
+     * @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 Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        if ($this->getCache()->getLogger() === null) {
+            $this->getCache()->setLogger($logger);
+        }
+
+        $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
+     */
+    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;
+    }
+
+    /**
+     * Set the Mustache Cache instance.
+     *
+     * @param Mustache_Cache $cache
+     */
+    public function setCache(Mustache_Cache $cache)
+    {
+        if (isset($this->logger) && $cache->getLogger() === null) {
+            $cache->setLogger($this->getLogger());
+        }
+
+        $this->cache = $cache;
+    }
+
+    /**
+     * Get the current Mustache Cache instance.
+     *
+     * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Cache
+     */
+    public function getCache()
+    {
+        if (!isset($this->cache)) {
+            $this->setCache(new Mustache_Cache_NoopCache());
+        }
+
+        return $this->cache;
+    }
+
+    /**
+     * Get the current Lambda Cache instance.
+     *
+     * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
+     *
+     * @see Mustache_Engine::getCache
+     *
+     * @return Mustache_Cache
+     */
+    protected function getLambdaCache()
+    {
+        if ($this->cacheLambdaTemplates) {
+            return $this->getCache();
+        }
+
+        if (!isset($this->lambdaCache)) {
+            $this->lambdaCache = new Mustache_Cache_NoopCache();
+        }
+
+        return $this->lambdaCache;
+    }
+
+    /**
+     * 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,entity_flags:%i,charset:%s,strict_callables:%s,pragmas:%s,source:%s',
+            self::VERSION,
+            isset($this->escape) ? 'custom' : 'default',
+            $this->entityFlags,
+            $this->charset,
+            $this->strictCallables ? 'true' : 'false',
+            implode(' ', $this->getPragmas()),
+            $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 {
+            if (isset($this->partialsLoader)) {
+                $loader = $this->partialsLoader;
+            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+                $loader = $this->loader;
+            } else {
+                throw new Mustache_Exception_UnknownTemplateException($name);
+            }
+
+            return $this->loadSource($loader->load($name));
+        } catch (Mustache_Exception_UnknownTemplateException $e) {
+            // If the named partial cannot be found, log then return null.
+            $this->log(
+                Mustache_Logger::WARNING,
+                'Partial not found: "{name}"',
+                array('name' => $e->getTemplateName())
+            );
+        }
+    }
+
+    /**
+     * 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, $this->getLambdaCache());
+    }
+
+    /**
+     * Instantiate and return a Mustache Template instance by source.
+     *
+     * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
+     * the 'cache_lambda_templates' configuration option.
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Engine::loadPartial
+     * @see Mustache_Engine::loadLambda
+     *
+     * @param string         $source
+     * @param Mustache_Cache $cache  (default: null)
+     *
+     * @return Mustache_Template
+     */
+    private function loadSource($source, Mustache_Cache $cache = null)
+    {
+        $className = $this->getTemplateClassName($source);
+
+        if (!isset($this->templates[$className])) {
+            if ($cache === null) {
+                $cache = $this->getCache();
+            }
+
+            if (!class_exists($className, false)) {
+                if (!$cache->load($className)) {
+                    $compiled = $this->compile($source);
+                    $cache->cache($className, $compiled);
+                }
+            }
+
+            $this->log(
+                Mustache_Logger::DEBUG,
+                'Instantiating template: "{className}"',
+                array('className' => $className)
+            );
+
+            $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)
+    {
+        $parser = $this->getParser();
+        $parser->setPragmas($this->getPragmas());
+
+        return $parser->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);
+
+        $this->log(
+            Mustache_Logger::INFO,
+            'Compiling template to "{className}" class',
+            array('className' => $name)
+        );
+
+        $compiler = $this->getCompiler();
+        $compiler->setPragmas($this->getPragmas());
+
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+    }
+
+    /**
+     * 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);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception.php b/lib/mustache/src/Mustache/Exception.php
new file mode 100644 (file)
index 0000000..8a2b01c
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php b/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
new file mode 100644 (file)
index 0000000..9bd1107
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/LogicException.php b/lib/mustache/src/Mustache/Exception/LogicException.php
new file mode 100644 (file)
index 0000000..255ce54
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/RuntimeException.php b/lib/mustache/src/Mustache/Exception/RuntimeException.php
new file mode 100644 (file)
index 0000000..a3c48f7
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/SyntaxException.php b/lib/mustache/src/Mustache/Exception/SyntaxException.php
new file mode 100644 (file)
index 0000000..7a16f7e
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+    protected $token;
+
+    /**
+     * @param string $msg
+     * @param array  $token
+     */
+    public function __construct($msg, array $token)
+    {
+        $this->token = $token;
+        parent::__construct($msg);
+    }
+
+    /**
+     * @return array
+     */
+    public function getToken()
+    {
+        return $this->token;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownFilterException.php b/lib/mustache/src/Mustache/Exception/UnknownFilterException.php
new file mode 100644 (file)
index 0000000..b9a315a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+    protected $filterName;
+
+    /**
+     * @param string $filterName
+     */
+    public function __construct($filterName)
+    {
+        $this->filterName = $filterName;
+        parent::__construct(sprintf('Unknown filter: %s', $filterName));
+    }
+
+    public function getFilterName()
+    {
+        return $this->filterName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownHelperException.php b/lib/mustache/src/Mustache/Exception/UnknownHelperException.php
new file mode 100644 (file)
index 0000000..226d774
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $helperName;
+
+    /**
+     * @param string $helperName
+     */
+    public function __construct($helperName)
+    {
+        $this->helperName = $helperName;
+        parent::__construct(sprintf('Unknown helper: %s', $helperName));
+    }
+
+    public function getHelperName()
+    {
+        return $this->helperName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php b/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
new file mode 100644 (file)
index 0000000..5dafe89
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $templateName;
+
+    /**
+     * @param string $templateName
+     */
+    public function __construct($templateName)
+    {
+        $this->templateName = $templateName;
+        parent::__construct(sprintf('Unknown template: %s', $templateName));
+    }
+
+    public function getTemplateName()
+    {
+        return $this->templateName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/HelperCollection.php b/lib/mustache/src/Mustache/HelperCollection.php
new file mode 100644 (file)
index 0000000..c7c7950
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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 Mustache_Exception_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) {
+            return;
+        }
+
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_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.
+     *
+     * @throws Mustache_Exception_UnknownHelperException If helper does not exist.
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function get($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($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 Mustache_Exception_UnknownHelperException if the requested helper is not present.
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($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);
+    }
+}
diff --git a/lib/mustache/src/Mustache/LambdaHelper.php b/lib/mustache/src/Mustache/LambdaHelper.php
new file mode 100644 (file)
index 0000000..7cd8092
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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 as the second argument to section lambdas (higher order sections),
+ * 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 string Rendered template.
+     */
+    public function render($string)
+    {
+        return $this->mustache
+            ->loadLambda((string) $string)
+            ->renderInternal($this->context);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader.php b/lib/mustache/src/Mustache/Loader.php
new file mode 100644 (file)
index 0000000..e75ee3f
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name);
+}
diff --git a/lib/mustache/src/Mustache/Loader/ArrayLoader.php b/lib/mustache/src/Mustache/Loader/ArrayLoader.php
new file mode 100644 (file)
index 0000000..e7ece91
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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.
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+    private $templates;
+
+    /**
+     * ArrayLoader constructor.
+     *
+     * @param array $templates Associative array of Template source (default: array())
+     */
+    public function __construct(array $templates = array())
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Load a Template.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        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;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/CascadingLoader.php b/lib/mustache/src/Mustache/Loader/CascadingLoader.php
new file mode 100644 (file)
index 0000000..d02a273
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template cascading loader implementation, which delegates to other
+ * Loader instances.
+ */
+class Mustache_Loader_CascadingLoader implements Mustache_Loader
+{
+    private $loaders;
+
+    /**
+     * Construct a CascadingLoader with an array of loaders:
+     *
+     *     $loader = new Mustache_Loader_CascadingLoader(array(
+     *         new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
+     *         new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
+     *     ));
+     *
+     * @param Mustache_Loader[] $loaders
+     */
+    public function __construct(array $loaders = array())
+    {
+        $this->loaders = array();
+        foreach ($loaders as $loader) {
+            $this->addLoader($loader);
+        }
+    }
+
+    /**
+     * Add a Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function addLoader(Mustache_Loader $loader)
+    {
+        $this->loaders[] = $loader;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        foreach ($this->loaders as $loader) {
+            try {
+                return $loader->load($name);
+            } catch (Mustache_Exception_UnknownTemplateException $e) {
+                // do nothing, check the next loader.
+            }
+        }
+
+        throw new Mustache_Exception_UnknownTemplateException($name);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/FilesystemLoader.php b/lib/mustache/src/Mustache/Loader/FilesystemLoader.php
new file mode 100644 (file)
index 0000000..7cbf9cd
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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.
+ *
+ * A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
+ *
+ *     $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 Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ *          'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ *     ));
+ */
+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 Mustache_Exception_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 = $baseDir;
+
+        if (strpos($this->baseDir, '://') === false) {
+            $this->baseDir = realpath($this->baseDir);
+        }
+
+        if (!is_dir($this->baseDir)) {
+            throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
+        }
+
+        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 Mustache_Loader_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 Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    protected function loadFile($name)
+    {
+        $fileName = $this->getFileName($name);
+
+        if (!file_exists($fileName)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return file_get_contents($fileName);
+    }
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name)
+    {
+        $fileName = $this->baseDir . '/' . $name;
+        if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
+            $fileName .= $this->extension;
+        }
+
+        return $fileName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/InlineLoader.php b/lib/mustache/src/Mustache/Loader/InlineLoader.php
new file mode 100644 (file)
index 0000000..9e4ab42
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template loader for inline templates.
+ *
+ * With the InlineLoader, templates can be defined at the end of any PHP source
+ * file:
+ *
+ *     $loader  = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ *     $hello   = $loader->load('hello');
+ *     $goodbye = $loader->load('goodbye');
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ planet }}!
+ *
+ *     @@ goodbye
+ *     Goodbye, cruel {{ planet }}
+ *
+ * Templates are deliniated by lines containing only `@@ name`.
+ *
+ * The InlineLoader is well-suited to micro-frameworks such as Silex:
+ *
+ *     $app->register(new MustacheServiceProvider, array(
+ *         'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
+ *     ));
+ *
+ *     $app->get('/{name}', function ($name) use ($app) {
+ *         return $app['mustache']->render('hello', compact('name'));
+ *     })
+ *     ->value('name', 'world');
+ *
+ *     // ...
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ name }}!
+ *
+ */
+class Mustache_Loader_InlineLoader implements Mustache_Loader
+{
+    protected $fileName;
+    protected $offset;
+    protected $templates;
+
+    /**
+     * The InlineLoader requires a filename and offset to process templates.
+     * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
+     * perfectly suited to the job:
+     *
+     *     $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+     *
+     * Note that this only works if the loader is instantiated inside the same
+     * file as the inline templates. If the templates are located in another
+     * file, it would be necessary to manually specify the filename and offset.
+     *
+     * @param string $fileName The file to parse for inline templates
+     * @param int    $offset   A string offset for the start of the templates.
+     *                         This usually coincides with the `__halt_compiler`
+     *                         call, and the `__COMPILER_HALT_OFFSET__`.
+     */
+    public function __construct($fileName, $offset)
+    {
+        if (!is_file($fileName)) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
+        }
+
+        if (!is_int($offset) || $offset < 0) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
+        }
+
+        $this->fileName = $fileName;
+        $this->offset   = $offset;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        $this->loadTemplates();
+
+        if (!array_key_exists($name, $this->templates)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Parse and load templates from the end of a source file.
+     */
+    protected function loadTemplates()
+    {
+        if ($this->templates === null) {
+            $this->templates = array();
+            $data = file_get_contents($this->fileName, false, null, $this->offset);
+            foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
+                if (trim($chunk)) {
+                    list($name, $content)         = explode("\n", $chunk, 2);
+                    $this->templates[trim($name)] = trim($content);
+                }
+            }
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/MutableLoader.php b/lib/mustache/src/Mustache/Loader/MutableLoader.php
new file mode 100644 (file)
index 0000000..78901be
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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
+     *
+     * @return void
+     */
+    public function setTemplates(array $templates);
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     *
+     * @return void
+     */
+    public function setTemplate($name, $template);
+}
diff --git a/lib/mustache/src/Mustache/Loader/StringLoader.php b/lib/mustache/src/Mustache/Loader/StringLoader.php
new file mode 100644 (file)
index 0000000..72d105d
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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"
+ */
+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;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Logger.php b/lib/mustache/src/Mustache/Logger.php
new file mode 100644 (file)
index 0000000..2e5d674
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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());
+}
diff --git a/lib/mustache/src/Mustache/Logger/AbstractLogger.php b/lib/mustache/src/Mustache/Logger/AbstractLogger.php
new file mode 100644 (file)
index 0000000..3dd96e7
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Logger/StreamLogger.php b/lib/mustache/src/Mustache/Logger/StreamLogger.php
new file mode 100644 (file)
index 0000000..d422340
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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 $level;
+    protected $stream = null;
+    protected $url    = null;
+
+    /**
+     * @throws InvalidArgumentException if the logging level is unknown.
+     *
+     * @param resource|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 Mustache_Exception_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 Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        $this->level = $level;
+    }
+
+    /**
+     * Get the current minimum logging level.
+     *
+     * @return integer
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @throws Mustache_Exception_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 Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        if (self::$levels[$level] >= self::$levels[$this->level]) {
+            $this->writeLog($level, $message, $context);
+        }
+    }
+
+    /**
+     * Write a record to the log.
+     *
+     * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present.
+     * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened.
+     *
+     * @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 Mustache_Exception_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 Mustache_Exception_RuntimeException(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())
+    {
+        if (strpos($message, '{') === false) {
+            return $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);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Parser.php b/lib/mustache/src/Mustache/Parser.php
new file mode 100644 (file)
index 0000000..0c134ec
--- /dev/null
@@ -0,0 +1,317 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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
+{
+    private $lineNum;
+    private $lineTokens;
+    private $pragmas;
+    private $defaultPragmas = array();
+
+    private $pragmaFilters;
+    private $pragmaBlocks;
+
+    /**
+     * 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())
+    {
+        $this->lineNum    = -1;
+        $this->lineTokens = 0;
+        $this->pragmas    = $this->defaultPragmas;
+
+        $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
+        $this->pragmaBlocks  = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
+
+        return $this->buildTree($tokens);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->enablePragma($pragma);
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper method for recursively building a parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered.
+     *
+     * @param array &$tokens Set of Mustache tokens
+     * @param array $parent  Parent token (default: null)
+     *
+     * @return array Mustache Token parse tree
+     */
+    private function buildTree(array &$tokens, array $parent = null)
+    {
+        $nodes = array();
+
+        while (!empty($tokens)) {
+            $token = array_shift($tokens);
+
+            if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
+                $this->lineTokens++;
+            } else {
+                $this->lineNum    = $token[Mustache_Tokenizer::LINE];
+                $this->lineTokens = 0;
+            }
+
+            if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
+                list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
+                if (!empty($filters)) {
+                    $token[Mustache_Tokenizer::NAME]    = $name;
+                    $token[Mustache_Tokenizer::FILTERS] = $filters;
+                }
+            }
+
+            switch ($token[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_DELIM_CHANGE:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                case Mustache_Tokenizer::T_INVERTED:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_END_SECTION:
+                    if (!isset($parent)) {
+                        $msg = sprintf(
+                            'Unexpected closing tag: /%s on line %d',
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
+                        $msg = sprintf(
+                            'Nesting error: %s (on line %d) vs. %s (on line %d)',
+                            $parent[Mustache_Tokenizer::NAME],
+                            $parent[Mustache_Tokenizer::LINE],
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
+                    $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+                    return $parent;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    //store the whitespace prefix for laters!
+                    if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
+                        $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
+                    }
+                    $nodes[] = $token;
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    if ($this->pragmaBlocks) {
+                        // BLOCKS pragma is enabled, let's do this!
+                        if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+                            $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
+                        }
+                        $this->clearStandaloneLines($nodes, $tokens);
+                        $nodes[] = $this->buildTree($tokens, $token);
+                    } else {
+                        // pretend this was just a normal "escaped" token...
+                        $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
+                        // TODO: figure out how to figure out if there was a space after this dollar:
+                        $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
+                        $nodes[] = $token;
+                    }
+                    break;
+
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->enablePragma($token[Mustache_Tokenizer::NAME]);
+                    // no break
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $token;
+                    break;
+
+                default:
+                    $nodes[] = $token;
+                    break;
+            }
+        }
+
+        if (isset($parent)) {
+            $msg = sprintf(
+                'Missing closing tag: %s opened on line %d',
+                $parent[Mustache_Tokenizer::NAME],
+                $parent[Mustache_Tokenizer::LINE]
+            );
+            throw new Mustache_Exception_SyntaxException($msg, $parent);
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Clear standalone line tokens.
+     *
+     * Returns a whitespace token for indenting partials, if applicable.
+     *
+     * @param array $nodes  Parsed nodes.
+     * @param array $tokens Tokens to be parsed.
+     *
+     * @return array|null Resulting indent token, if any.
+     */
+    private function clearStandaloneLines(array &$nodes, array &$tokens)
+    {
+        if ($this->lineTokens > 1) {
+            // this is the third or later node on this line, so it can't be standalone
+            return;
+        }
+
+        $prev = null;
+        if ($this->lineTokens === 1) {
+            // this is the second node on this line, so it can't be standalone
+            // unless the previous node is whitespace.
+            if ($prev = end($nodes)) {
+                if (!$this->tokenIsWhitespace($prev)) {
+                    return;
+                }
+            }
+        }
+
+        if ($next = reset($tokens)) {
+            // If we're on a new line, bail.
+            if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
+                return;
+            }
+
+            // If the next token isn't whitespace, bail.
+            if (!$this->tokenIsWhitespace($next)) {
+                return;
+            }
+
+            if (count($tokens) !== 1) {
+                // Unless it's the last token in the template, the next token
+                // must end in newline for this to be standalone.
+                if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+                    return;
+                }
+            }
+
+            // Discard the whitespace suffix
+            array_shift($tokens);
+        }
+
+        if ($prev) {
+            // Return the whitespace prefix, if any
+            return array_pop($nodes);
+        }
+    }
+
+    /**
+     * Check whether token is a whitespace token.
+     *
+     * True if token type is T_TEXT and value is all whitespace characters.
+     *
+     * @param array $token
+     *
+     * @return boolean True if token is a whitespace token
+     */
+    private function tokenIsWhitespace(array $token)
+    {
+        if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
+            return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+        }
+
+        return false;
+    }
+
+    /**
+     * Check whether a token is allowed inside a parent tag.
+     *
+     * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag.
+     *
+     * @param array|null $parent
+     * @param array      $token
+     */
+    private function checkIfTokenIsAllowedInParent($parent, array $token)
+    {
+        if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+            throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
+        }
+    }
+
+    /**
+     * Split a tag name into name and filters.
+     *
+     * @param string $name
+     *
+     * @return array [Tag name, Array of filters]
+     */
+    private function getNameAndFilters($name)
+    {
+        $filters = array_map('trim', explode('|', $name));
+        $name    = array_shift($filters);
+
+        return array($name, $filters);
+    }
+
+    /**
+     * Enable a pragma.
+     *
+     * @param string $name
+     */
+    private function enablePragma($name)
+    {
+        $this->pragmas[$name] = true;
+
+        switch ($name) {
+            case Mustache_Engine::PRAGMA_BLOCKS:
+                $this->pragmaBlocks = true;
+                break;
+
+            case Mustache_Engine::PRAGMA_FILTERS:
+                $this->pragmaFilters = true;
+                break;
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Template.php b/lib/mustache/src/Mustache/Template.php
new file mode 100644 (file)
index 0000000..e7156cf
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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;
+
+    /**
+     * @var boolean
+     */
+    protected $strictCallables = false;
+
+    /**
+     * 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 :)
+     *
+     * NOTE: This method is not part of the Mustache.php public API.
+     *
+     * @param Mustache_Context $context
+     * @param string           $indent  (default: '')
+     *
+     * @return string Rendered template
+     */
+    abstract public function renderInternal(Mustache_Context $context, $indent = '');
+
+    /**
+     * 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)
+    {
+        switch (gettype($value)) {
+            case 'object':
+                return $value instanceof Traversable;
+
+            case 'array':
+                $i = 0;
+                foreach ($value as $k => $v) {
+                    if ($k !== $i++) {
+                        return false;
+                    }
+                }
+
+                return true;
+
+            default:
+                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;
+    }
+
+    /**
+     * Resolve a context value.
+     *
+     * Invoke the value if it is callable, otherwise return the value.
+     *
+     * @param mixed            $value
+     * @param Mustache_Context $context
+     * @param string           $indent
+     *
+     * @return string
+     */
+    protected function resolveValue($value, Mustache_Context $context, $indent = '')
+    {
+        if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
+            return $this->mustache
+                ->loadLambda((string) call_user_func($value))
+                ->renderInternal($context, $indent);
+        }
+
+        return $value;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Tokenizer.php b/lib/mustache/src/Mustache/Tokenizer.php
new file mode 100644 (file)
index 0000000..3175a03
--- /dev/null
@@ -0,0 +1,331 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 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_PARENT       = '<';
+    const T_DELIM_CHANGE = '=';
+    const T_ESCAPED      = '_v';
+    const T_UNESCAPED    = '{';
+    const T_UNESCAPED_2  = '&';
+    const T_TEXT         = '_t';
+    const T_PRAGMA       = '%';
+    const T_BLOCK_VAR    = '$';
+    const T_BLOCK_ARG    = '$arg';
+
+    // 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_PARENT       => true,
+        self::T_DELIM_CHANGE => true,
+        self::T_ESCAPED      => true,
+        self::T_UNESCAPED    => true,
+        self::T_UNESCAPED_2  => true,
+        self::T_PRAGMA       => true,
+        self::T_BLOCK_VAR    => 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 LINE    = 'line';
+    const INDEX   = 'index';
+    const END     = 'end';
+    const INDENT  = 'indent';
+    const NODES   = 'nodes';
+    const VALUE   = 'value';
+    const FILTERS = 'filters';
+
+    private $state;
+    private $tagType;
+    private $tag;
+    private $buffer;
+    private $tokens;
+    private $seenTag;
+    private $line;
+    private $otag;
+    private $ctag;
+    private $otagLen;
+    private $ctagLen;
+
+    /**
+     * Scan and tokenize template source.
+     *
+     * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered.
+     *
+     * @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)
+    {
+        // Setting mbstring.func_overload makes things *really* slow.
+        // Let's do everyone a favor and scan this string as ASCII instead.
+        $encoding = null;
+        if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
+            $encoding = mb_internal_encoding();
+            mb_internal_encoding('ASCII');
+        }
+
+        $this->reset();
+
+        if ($delimiters = trim($delimiters)) {
+            $this->setDelimiters($delimiters);
+        }
+
+        $len = strlen($text);
+        for ($i = 0; $i < $len; $i++) {
+            switch ($this->state) {
+                case self::IN_TEXT:
+                    if ($this->tagChange($this->otag, $this->otagLen, $text, $i)) {
+                        $i--;
+                        $this->flushBuffer();
+                        $this->state = self::IN_TAG_TYPE;
+                    } else {
+                        $char = $text[$i];
+                        $this->buffer .= $char;
+                        if ($char === "\n") {
+                            $this->flushBuffer();
+                            $this->line++;
+                        }
+                    }
+                    break;
+
+                case self::IN_TAG_TYPE:
+                    $i += $this->otagLen - 1;
+                    $char = $text[$i + 1];
+                    if (isset(self::$tagTypes[$char])) {
+                        $tag = $char;
+                        $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;
+                    } elseif ($this->tagType === self::T_PRAGMA) {
+                        $i = $this->addPragma($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, $this->ctagLen, $text, $i)) {
+                        $token = array(
+                            self::TYPE  => $this->tagType,
+                            self::NAME  => trim($this->buffer),
+                            self::OTAG  => $this->otag,
+                            self::CTAG  => $this->ctag,
+                            self::LINE  => $this->line,
+                            self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen
+                        );
+
+                        if ($this->tagType === self::T_UNESCAPED) {
+                            // Clean up `{{{ tripleStache }}}` style tokens.
+                            if ($this->ctag === '}}') {
+                                if (($i + 2 < $len) && $text[$i + 2] === '}') {
+                                    $i++;
+                                } else {
+                                    $msg = sprintf(
+                                        'Mismatched tag delimiters: %s on line %d',
+                                        $token[self::NAME],
+                                        $token[self::LINE]
+                                    );
+
+                                    throw new Mustache_Exception_SyntaxException($msg, $token);
+                                }
+                            } else {
+                                $lastName = $token[self::NAME];
+                                if (substr($lastName, -1) === '}') {
+                                    $token[self::NAME] = trim(substr($lastName, 0, -1));
+                                } else {
+                                    $msg = sprintf(
+                                        'Mismatched tag delimiters: %s on line %d',
+                                        $token[self::NAME],
+                                        $token[self::LINE]
+                                    );
+
+                                    throw new Mustache_Exception_SyntaxException($msg, $token);
+                                }
+                            }
+                        }
+
+                        $this->buffer = '';
+                        $i += $this->ctagLen - 1;
+                        $this->state = self::IN_TEXT;
+                        $this->tokens[] = $token;
+                    } else {
+                        $this->buffer .= $text[$i];
+                    }
+                    break;
+            }
+        }
+
+        $this->flushBuffer();
+
+        // Restore the user's encoding...
+        if ($encoding) {
+            mb_internal_encoding($encoding);
+        }
+
+        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->line    = 0;
+        $this->otag    = '{{';
+        $this->ctag    = '}}';
+        $this->otagLen = 2;
+        $this->ctagLen = 2;
+    }
+
+    /**
+     * Flush the current buffer to a token.
+     */
+    private function flushBuffer()
+    {
+        if (strlen($this->buffer) > 0) {
+            $this->tokens[] = array(
+                self::TYPE  => self::T_TEXT,
+                self::LINE  => $this->line,
+                self::VALUE => $this->buffer
+            );
+            $this->buffer   = '';
+        }
+    }
+
+    /**
+     * 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);
+
+        $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
+
+        $this->tokens[] = array(
+            self::TYPE => self::T_DELIM_CHANGE,
+            self::LINE => $this->line,
+        );
+
+        return $closeIndex + strlen($close) - 1;
+    }
+
+    /**
+     * Set the current Mustache `otag` and `ctag` delimiters.
+     *
+     * @param string $delimiters
+     */
+    private function setDelimiters($delimiters)
+    {
+        list($otag, $ctag) = explode(' ', $delimiters);
+        $this->otag = $otag;
+        $this->ctag = $ctag;
+        $this->otagLen = strlen($otag);
+        $this->ctagLen = strlen($ctag);
+    }
+
+    /**
+     * Add pragma token.
+     *
+     * Pragmas are hoisted to the front of the template, so all pragma tokens
+     * will appear at the front of the token list.
+     *
+     * @param string $text
+     * @param int    $index
+     *
+     * @return int New index value
+     */
+    private function addPragma($text, $index)
+    {
+        $end    = strpos($text, $this->ctag, $index);
+        $pragma = trim(substr($text, $index + 2, $end - $index - 2));
+
+        // Pragmas are hoisted to the front of the template.
+        array_unshift($this->tokens, array(
+            self::TYPE => self::T_PRAGMA,
+            self::NAME => $pragma,
+            self::LINE => 0,
+        ));
+
+        return $end + $this->ctagLen - 1;
+    }
+
+    /**
+     * Test whether it's time to change tags.
+     *
+     * @param string $tag    Current tag name
+     * @param int    $tagLen Current tag name length
+     * @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, $tagLen, $text, $index)
+    {
+        return substr($text, $index, $tagLen) === $tag;
+    }
+}
index 38b4e94..fb10ae0 100644 (file)
@@ -39,6 +39,28 @@ interface renderable {
     // intentionally empty
 }
 
+/**
+ * Interface marking other classes having the ability to export their data for use by templates.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @package core
+ * @category output
+ * @since 2.9
+ */
+interface templatable {
+
+    /**
+     * Function to export the renderer data in a format that is suitable for a
+     * mustache template. This means:
+     * 1. No complex types - only stdClass, array, int, string, float, bool
+     * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks).
+     *
+     * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+     * @return stdClass|array
+     */
+    public function export_for_template(renderer_base $output);
+}
+
 /**
  * Data structure representing a file picker.
  *
@@ -602,6 +624,16 @@ class single_button implements renderable {
      */
     var $actions = array();
 
+    /**
+     * @var array $params URL Params
+     */
+    var $params;
+
+    /**
+     * @var string Action id
+     */
+    var $actionid;
+
     /**
      * Constructor
      * @param moodle_url $url
index 2d7cabf..5ac7300 100644 (file)
@@ -337,7 +337,10 @@ class standard_renderer_factory extends renderer_factory_base {
             throw new coding_exception('Request for an unknown renderer class. Searched for: ' . var_export($classnames, true));
         }
 
-        return new $classname($page, $target);
+        $renderer = new $classname($page, $target);
+        $renderer->set_component($component);
+        $renderer->set_subtype($subtype);
+        return $renderer;
     }
 }
 
@@ -401,7 +404,10 @@ class theme_overridden_renderer_factory extends renderer_factory_base {
                         $newclassname = $prefix . '_' . $classnamedetails['classname'] . $suffix;
                     }
                     if (class_exists($newclassname)) {
-                        return new $newclassname($page, $target);
+                        $renderer = new $newclassname($page, $target);
+                        $renderer->set_component($component);
+                        $renderer->set_subtype($subtype);
+                        return $renderer;
                     }
                 }
             }
@@ -412,7 +418,10 @@ class theme_overridden_renderer_factory extends renderer_factory_base {
                 if (class_exists($newclassname)) {
                     // Use the specialised renderer for given target, default renderer might also decide
                     // to implement support for more targets.
-                    return new $newclassname($page, $target);
+                    $renderer = new $newclassname($page, $target);
+                    $renderer->set_component($component);
+                    $renderer->set_subtype($subtype);
+                    return $renderer;
                 }
             }
         }
@@ -427,7 +436,10 @@ class theme_overridden_renderer_factory extends renderer_factory_base {
                         $newclassname = $prefix . '_' . $classnamedetails['classname'];
                     }
                     if (class_exists($newclassname)) {
-                        return new $newclassname($page, $target);
+                        $renderer = new $newclassname($page, $target);
+                        $renderer->set_component($component);
+                        $renderer->set_subtype($subtype);
+                        return $renderer;
                     }
                 }
             }
@@ -438,7 +450,10 @@ class theme_overridden_renderer_factory extends renderer_factory_base {
             if ($classnamedetails['validwithoutprefix']) {
                 $newclassname = $classnamedetails['classname'];
                 if (class_exists($newclassname)) {
-                    return new $newclassname($page, $target);
+                    $renderer = new $newclassname($page, $target);
+                    $renderer->set_component($component);
+                    $renderer->set_subtype($subtype);
+                    return $renderer;
                 }
             }
         }
index e30a99f..89b2423 100644 (file)
@@ -66,6 +66,129 @@ class renderer_base {
      */
     protected $target;
 
+    /**
+     * @var Mustache_Engine $mustache The mustache template compiler
+     */
+    private $mustache;
+
+    /**
+     * @var string $component The component used when requesting this renderer.
+     */
+    private $component;
+
+    /**
+     * @var string $subtype The subtype used when requesting this renderer.
+     */
+    private $subtype;
+
+    /**
+     * This is not done in the constructor because that would be a
+     * compatibility breaking change, and we can just pass this always in the
+     * renderer factory, immediately after creating the renderer.
+     * @since 2.9
+     * @param string $subtype
+     */
+    public function set_subtype($subtype) {
+        $this->subtype = $subtype;
+    }
+
+    /**
+     * This is not done in the constructor because that would be a
+     * compatibility breaking change, and we can just pass this always in the
+     * renderer factory, immediately after creating the renderer.
+     * @since 2.9
+     * @param string $component
+     */
+    public function set_component($component) {
+        $this->component = $component;
+    }
+
+    /**
+     * Return an instance of the mustache class.
+     *
+     * @since 2.9
+     * @return Mustache_Engine
+     */
+    protected function get_mustache() {
+        global $CFG;
+
+        if ($this->mustache === null) {
+            require_once($CFG->dirroot . '/lib/mustache/src/Mustache/Autoloader.php');
+            Mustache_Autoloader::register();
+
+            $themename = $this->page->theme->name;
+            $themerev = theme_get_revision();
+            $target = $this->target;
+
+            $cachedir = make_localcache_directory("mustache/$themerev/$themename/$target");
+            $loaderoptions = array();
+
+            // Where are all the places we should look for templates?
+
+            $suffix = $this->component;
+            if ($this->subtype !== null) {
+                $suffix .= '_' . $this->subtype;
+            }
+
+            // Start with an empty list.
+            $loader = new Mustache_Loader_CascadingLoader(array());
+            $loaderdir = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $suffix;
+            if (is_dir($loaderdir)) {
+                $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
+            }
+
+            // Search each of the parent themes second.
+            foreach ($this->page->theme->parents as $parent) {
+                $loaderdir = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $suffix;
+                if (is_dir($loaderdir)) {
+                    $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
+                }
+            }
+
+            // Look in a components templates dir for a base implementation.
+
+            $compdirectory = core_component::get_component_directory($suffix);
+            if ($compdirectory) {
+                $loaderdir = $compdirectory . '/templates';
+                if (is_dir($loaderdir)) {
+                    $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
+                }
+            }
+
+            // Look in the core templates dir as a final fallback.
+
+            $compdirectory = $CFG->libdir;
+            if ($compdirectory) {
+                $loaderdir = $compdirectory . '/templates';
+                if (is_dir($loaderdir)) {
+                    $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
+                }
+            }
+
+            $stringhelper = new \core\output\mustache_string_helper();
+            $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
+            $pixhelper = new \core\output\mustache_pix_helper($this);
+
+            // We only expose the variables that are exposed to JS templates.
+            $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
+
+            $helpers = array('config' => $safeconfig,
+                             'str' => array($stringhelper, 'str'),
+                             'js' => array($jshelper, 'help'),
+                             'pix' => array($pixhelper, 'pix'));
+
+            $this->mustache = new Mustache_Engine(array(
+                'cache' => $cachedir,
+                'escape' => 's',
+                'loader' => $loader,
+                'helpers' => $helpers));
+
+        }
+
+        return $this->mustache;
+    }
+
+
     /**
      * Constructor
      *
@@ -83,6 +206,39 @@ class renderer_base {
         $this->target = $target;
     }
 
+    /**
+     * Renders a template by name with the given context.
+     *
+     * The provided data needs to be array/stdClass made up of only simple types.
+     * Simple types are array,stdClass,bool,int,float,string
+     *
+     * @since 2.9
+     * @param array|stdClass $context Context containing data for the template.
+     * @return string|boolean
+     */
+    public function render_from_template($templatename, $context) {
+        static $templatecache = array();
+        $mustache = $this->get_mustache();
+
+        // Provide 1 random value that will not change within a template
+        // but will be different from template to template. This is useful for
+        // e.g. aria attributes that only work with id attributes and must be
+        // unique in a page.
+        $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
+        if (isset($templatecache[$templatename])) {
+            $template = $templatecache[$templatename];
+        } else {
+            try {
+                $template = $mustache->loadTemplate($templatename);
+                $templatecache[$templatename] = $template;
+            } catch (Mustache_Exception_UnknownTemplateException $e) {
+                throw new moodle_exception('Unknown template: ' . $templatename);
+            }
+        }
+        return trim($template->render($context));
+    }
+
+
     /**
      * Returns rendered widget.
      *
@@ -2016,7 +2172,11 @@ class core_renderer extends renderer_base {
     protected function render_pix_icon(pix_icon $icon) {
         $attributes = $icon->attributes;
         $attributes['src'] = $this->pix_url($icon->pix, $icon->component);
-        return html_writer::empty_tag('img', $attributes);
+        $templatecontext = array();
+        foreach ($attributes as $name => $value) {
+            $templatecontext[] = array('name' => $name, 'value' => $value);
+        }
+        return $this->render_from_template('core/pix_icon', array('attributes' => $templatecontext));
     }
 
     /**
index d255e6f..9a6df4f 100644 (file)
@@ -293,6 +293,42 @@ class page_requirements_manager {
         $this->js_module($this->find_module('core_filepicker'));
     }
 
+    /**
+     * Return the safe config values that get set for javascript in "M.cfg".
+     *
+     * @since 2.9
+     * @return array List of safe config values that are available to javascript.
+     */
+    public function get_config_for_javascript(moodle_page $page, renderer_base $renderer) {
+        global $CFG;
+
+        if (empty($this->M_cfg)) {
+            // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot.
+            // Otherwise, in some situations, users will get warnings about insecure content
+            // on secure pages from their web browser.
+
+            $this->M_cfg = array(
+                'wwwroot'             => $CFG->httpswwwroot, // Yes, really. See above.
+                'sesskey'             => sesskey(),
+                'loadingicon'         => $renderer->pix_url('i/loading_small', 'moodle')->out(false),
+                'themerev'            => theme_get_revision(),
+                'slasharguments'      => (int)(!empty($CFG->slasharguments)),
+                'theme'               => $page->theme->name,
+                'jsrev'               => $this->get_jsrev(),
+                'admin'               => $CFG->admin,
+                'svgicons'            => $page->theme->use_svg_icons()
+            );
+            if ($CFG->debugdeveloper) {
+                $this->M_cfg['developerdebug'] = true;
+            }
+            if (defined('BEHAT_SITE_RUNNING')) {
+                $this->M_cfg['behatsiterunning'] = true;
+            }
+
+        }
+        return $this->M_cfg;
+    }
+
     /**
      * Initialise with the bits of JavaScript that every Moodle page should have.
      *
@@ -302,26 +338,8 @@ class page_requirements_manager {
     protected function init_requirements_data(moodle_page $page, core_renderer $renderer) {
         global $CFG;
 
-        // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot.
-        // Otherwise, in some situations, users will get warnings about insecure content
-        // on secure pages from their web browser.
-
-        $this->M_cfg = array(
-            'wwwroot'             => $CFG->httpswwwroot, // Yes, really. See above.
-            'sesskey'             => sesskey(),
-            'loadingicon'         => $renderer->pix_url('i/loading_small', 'moodle')->out(false),
-            'themerev'            => theme_get_revision(),
-            'slasharguments'      => (int)(!empty($CFG->slasharguments)),
-            'theme'               => $page->theme->name,
-            'jsrev'               => $this->get_jsrev(),
-            'svgicons'            => $page->theme->use_svg_icons()
-        );
-        if ($CFG->debugdeveloper) {
-            $this->M_cfg['developerdebug'] = true;
-        }
-        if (defined('BEHAT_SITE_RUNNING')) {
-            $this->M_cfg['behatsiterunning'] = true;
-        }
+        // Init the js config.
+        $this->get_config_for_javascript($page, $renderer);
 
         // Accessibility stuff.
         $this->skip_link_to('maincontent', get_string('tocontent', 'access'));
diff --git a/lib/templates/pix_icon.mustache b/lib/templates/pix_icon.mustache
new file mode 100644 (file)
index 0000000..8b77104
--- /dev/null
@@ -0,0 +1,31 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    Moodle pix_icon template.
+
+    The purpose of this template is to render a pix_icon.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * attributes Array of name / value pairs.
+}}
+<img {{#attributes}}{{name}}="{{value}}" {{/attributes}}/>
index 3417906..8a252d0 100644 (file)
     <license>MIT</license>
     <version>1.2.0</version>
   </library>
+  <library>
+    <location>mustache</location>
+    <name>Mustache</name>
+    <license>MIT</license>
+    <version>2.7.0</version>
+  </library>
+  <library>
+    <location>amd/src/mustache.js</location>
+    <name>Mustache.js</name>
+    <license>MIT</license>
+    <version>1.0.0</version>
+  </library>
 </libraries>
index 06b25b0..4a22f63 100644 (file)
@@ -3,6 +3,7 @@ information provided here is intended especially for developers.
 
 === 2.9 ===
 
+* Support for rendering templates from php or javascript has been added. See MDL-49152.
 * Support for loading AMD javascript modules has been added. See MDL-49046.
 * Webservice core_course_delete_courses now return warning messages on any failures and does not try to rollback the entire deletion.
 * \core\event\course_viewed 'other' argument renamed from coursesectionid to coursesectionnumber as it contains the section number.