diff --git a/admin/tool/templatelibrary/amd/build/display.min.js b/admin/tool/templatelibrary/amd/build/display.min.js
index 76ec8130610..9ac8f6ba001 100644
--- a/admin/tool/templatelibrary/amd/build/display.min.js
+++ b/admin/tool/templatelibrary/amd/build/display.min.js
@@ -1,2 +1,2 @@
-define ("tool_templatelibrary/display",["jquery","core/ajax","core/log","core/notification","core/templates","core/config","core/str"],function(a,b,c,d,e,f,g){var h=function(a,b){if(!a){return!1}var c="@template "+b,d=0,e=[];e=a.match(/{{!([\s\S]*?)}}/g);if(null!==e){for(d=0;d.\n\n/**\n * This module adds ajax display functions to the template library page.\n *\n * @module tool_templatelibrary/display\n * @package tool_templatelibrary\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates', 'core/config', 'core/str'],\n function($, ajax, log, notification, templates, config, str) {\n\n /**\n * Search through a template for a template docs comment.\n *\n * @param {String} templateSource The raw template\n * @param {String} templateName The name of the template used to search for docs tag\n * @return {String|boolean} the correct comment or false\n */\n var findDocsSection = function(templateSource, templateName) {\n\n if (!templateSource) {\n return false;\n }\n // Find the comment section marked with @template component/template.\n var marker = \"@template \" + templateName,\n i = 0,\n sections = [];\n\n sections = templateSource.match(/{{!([\\s\\S]*?)}}/g);\n\n // If no sections match - show the entire file.\n if (sections !== null) {\n for (i = 0; i < sections.length; i++) {\n var section = sections[i];\n var start = section.indexOf(marker);\n if (start !== -1) {\n // Remove {{! and }} from start and end.\n var offset = start + marker.length + 1;\n section = section.substr(offset, section.length - 2 - offset);\n return section;\n }\n }\n }\n // No matching comment.\n return false;\n };\n\n /**\n * Handle a template loaded response.\n *\n * @param {String} templateName The template name\n * @param {String} source The template source\n * @param {String} originalSource The original template source (not theme overridden)\n */\n var templateLoaded = function(templateName, source, originalSource) {\n str.get_string('templateselected', 'tool_templatelibrary', templateName).done(function(s) {\n $('[data-region=\"displaytemplateheader\"]').text(s);\n }).fail(notification.exception);\n\n // Find the comment section marked with @template component/template.\n var docs = findDocsSection(source, templateName);\n\n if (docs === false) {\n // Docs was not in theme template, try original.\n docs = findDocsSection(originalSource, templateName);\n }\n\n // If we found a docs section, limit the template library to showing this section.\n if (docs) {\n source = docs;\n }\n\n $('[data-region=\"displaytemplatesource\"]').text(source);\n\n // Now search the text for a json example.\n\n var example = source.match(/Example context \\(json\\):([\\s\\S]*)/);\n var context = false;\n if (example) {\n var rawJSON = example[1].trim();\n try {\n context = $.parseJSON(rawJSON);\n } catch (e) {\n log.debug('Could not parse json example context for template.');\n log.debug(e);\n }\n }\n if (context) {\n templates.render(templateName, context).done(function(html, js) {\n templates.replaceNodeContents($('[data-region=\"displaytemplateexample\"]'), html, js);\n }).fail(notification.exception);\n } else {\n str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {\n $('[data-region=\"displaytemplateexample\"]').text(s);\n }).fail(notification.exception);\n }\n };\n\n /**\n * Load the a template source from Moodle.\n *\n * @param {String} templateName\n */\n var loadTemplate = function(templateName) {\n var parts = templateName.split('/');\n var component = parts.shift();\n var name = parts.shift();\n\n var promises = ajax.call([{\n methodname: 'core_output_load_template',\n args: {\n component: component,\n template: name,\n themename: config.theme,\n includecomments: true\n }\n }, {\n methodname: 'tool_templatelibrary_load_canonical_template',\n args: {\n component: component,\n template: name\n }\n }], true, false);\n\n // When returns a new promise that is resolved when all the passed in promises are resolved.\n // The arguments to the done become the values of each resolved promise.\n $.when.apply($, promises)\n .done(function(source, originalSource) {\n templateLoaded(templateName, source, originalSource);\n })\n .fail(notification.exception);\n };\n\n // Add the event listeners.\n $('[data-region=\"list-templates\"]').on('click', '[data-templatename]', function(e) {\n var templatename = $(this).data('templatename');\n e.preventDefault();\n loadTemplate(templatename);\n });\n\n // This module does not expose anything.\n return {};\n});\n"],"file":"display.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/display.js"],"names":["define","$","ajax","log","notification","templates","config","str","findDocsSection","templateSource","templateName","marker","i","sections","match","length","section","start","indexOf","offset","substr","templateLoaded","source","originalSource","get_string","done","s","text","fail","exception","docs","example","context","rawJSON","trim","parseJSON","e","debug","render","html","js","replaceNodeContents","loadTemplate","parts","split","component","shift","name","join","promises","call","methodname","args","template","themename","theme","includecomments","when","apply","on","templatename","data","preventDefault"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module adds ajax display functions to the template library page.\n *\n * @module tool_templatelibrary/display\n * @package tool_templatelibrary\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates', 'core/config', 'core/str'],\n function($, ajax, log, notification, templates, config, str) {\n\n /**\n * Search through a template for a template docs comment.\n *\n * @param {String} templateSource The raw template\n * @param {String} templateName The name of the template used to search for docs tag\n * @return {String|boolean} the correct comment or false\n */\n var findDocsSection = function(templateSource, templateName) {\n\n if (!templateSource) {\n return false;\n }\n // Find the comment section marked with @template component/template.\n var marker = \"@template \" + templateName,\n i = 0,\n sections = [];\n\n sections = templateSource.match(/{{!([\\s\\S]*?)}}/g);\n\n // If no sections match - show the entire file.\n if (sections !== null) {\n for (i = 0; i < sections.length; i++) {\n var section = sections[i];\n var start = section.indexOf(marker);\n if (start !== -1) {\n // Remove {{! and }} from start and end.\n var offset = start + marker.length + 1;\n section = section.substr(offset, section.length - 2 - offset);\n return section;\n }\n }\n }\n // No matching comment.\n return false;\n };\n\n /**\n * Handle a template loaded response.\n *\n * @param {String} templateName The template name\n * @param {String} source The template source\n * @param {String} originalSource The original template source (not theme overridden)\n */\n var templateLoaded = function(templateName, source, originalSource) {\n str.get_string('templateselected', 'tool_templatelibrary', templateName).done(function(s) {\n $('[data-region=\"displaytemplateheader\"]').text(s);\n }).fail(notification.exception);\n\n // Find the comment section marked with @template component/template.\n var docs = findDocsSection(source, templateName);\n\n if (docs === false) {\n // Docs was not in theme template, try original.\n docs = findDocsSection(originalSource, templateName);\n }\n\n // If we found a docs section, limit the template library to showing this section.\n if (docs) {\n source = docs;\n }\n\n $('[data-region=\"displaytemplatesource\"]').text(source);\n\n // Now search the text for a json example.\n\n var example = source.match(/Example context \\(json\\):([\\s\\S]*)/);\n var context = false;\n if (example) {\n var rawJSON = example[1].trim();\n try {\n context = $.parseJSON(rawJSON);\n } catch (e) {\n log.debug('Could not parse json example context for template.');\n log.debug(e);\n }\n }\n if (context) {\n templates.render(templateName, context).done(function(html, js) {\n templates.replaceNodeContents($('[data-region=\"displaytemplateexample\"]'), html, js);\n }).fail(notification.exception);\n } else {\n str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {\n $('[data-region=\"displaytemplateexample\"]').text(s);\n }).fail(notification.exception);\n }\n };\n\n /**\n * Load the a template source from Moodle.\n *\n * @param {String} templateName\n */\n var loadTemplate = function(templateName) {\n var parts = templateName.split('/');\n var component = parts.shift();\n var name = parts.join('/');\n\n var promises = ajax.call([{\n methodname: 'core_output_load_template',\n args: {\n component: component,\n template: name,\n themename: config.theme,\n includecomments: true\n }\n }, {\n methodname: 'tool_templatelibrary_load_canonical_template',\n args: {\n component: component,\n template: name\n }\n }], true, false);\n\n // When returns a new promise that is resolved when all the passed in promises are resolved.\n // The arguments to the done become the values of each resolved promise.\n $.when.apply($, promises)\n .done(function(source, originalSource) {\n templateLoaded(templateName, source, originalSource);\n })\n .fail(notification.exception);\n };\n\n // Add the event listeners.\n $('[data-region=\"list-templates\"]').on('click', '[data-templatename]', function(e) {\n var templatename = $(this).data('templatename');\n e.preventDefault();\n loadTemplate(templatename);\n });\n\n // This module does not expose anything.\n return {};\n});\n"],"file":"display.min.js"}
\ No newline at end of file
diff --git a/admin/tool/templatelibrary/amd/src/display.js b/admin/tool/templatelibrary/amd/src/display.js
index 39793660d40..222e4bdd44b 100644
--- a/admin/tool/templatelibrary/amd/src/display.js
+++ b/admin/tool/templatelibrary/amd/src/display.js
@@ -119,7 +119,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
var loadTemplate = function(templateName) {
var parts = templateName.split('/');
var component = parts.shift();
- var name = parts.shift();
+ var name = parts.join('/');
var promises = ajax.call([{
methodname: 'core_output_load_template',
diff --git a/admin/tool/templatelibrary/classes/api.php b/admin/tool/templatelibrary/classes/api.php
index 7ae2b0e8408..f0d7a43ce4f 100644
--- a/admin/tool/templatelibrary/classes/api.php
+++ b/admin/tool/templatelibrary/classes/api.php
@@ -99,13 +99,34 @@ class api {
foreach ($templatedirs as $templatecomponent => $dirs) {
foreach ($dirs as $dir) {
+ if (!is_dir($dir) || !is_readable($dir)) {
+ continue;
+ }
+ $dir = realpath($dir);
+
// List it.
- $files = glob($dir . '/*.mustache');
+ $directory = new \RecursiveDirectoryIterator($dir);
+ $files = new \RecursiveIteratorIterator($directory);
foreach ($files as $file) {
- $templatename = basename($file, '.mustache');
- if ($search == '' || strpos($templatename, $search) !== false) {
- $results[$templatecomponent . '/' . $templatename] = 1;
+ if (!$file->isFile()) {
+ continue;
+ }
+ $filename = substr($file->getRealpath(), strlen($dir) + 1);
+ if (strpos($templatecomponent, 'theme_') === 0) {
+ if (strpos($filename, '/') !== false && strpos($filename, 'local/') !== 0) {
+ // Skip any template in a sub-directory of a theme which is not in a local directory.
+ // These are theme overrides of core templates.
+ // Note: There is a rare edge case where a theme may override a template and then have additional
+ // dependant templates and these will not be shown.
+ continue;
+ }
+ }
+ $templatename = str_replace('.mustache', '', $filename);
+ $componenttemplatename = "{$templatecomponent}/{$templatename}";
+
+ if ($search == '' || strpos($componenttemplatename, $search) !== false) {
+ $results[$componenttemplatename] = 1;
}
}
}
@@ -152,5 +173,4 @@ class api {
return $templatestr;
}
-
}
diff --git a/admin/tool/templatelibrary/classes/external.php b/admin/tool/templatelibrary/classes/external.php
index f1c72f55937..edda9f2dc4a 100644
--- a/admin/tool/templatelibrary/classes/external.php
+++ b/admin/tool/templatelibrary/classes/external.php
@@ -105,7 +105,7 @@ class external extends external_api {
public static function load_canonical_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'))
+ 'template' => new external_value(PARAM_SAFEPATH, 'name of the template'))
);
}
diff --git a/admin/tool/templatelibrary/version.php b/admin/tool/templatelibrary/version.php
index c8d15190c23..625a195b03c 100644
--- a/admin/tool/templatelibrary/version.php
+++ b/admin/tool/templatelibrary/version.php
@@ -21,6 +21,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2019052002; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'tool_templatelibrary'; // Full name of the plugin (used for diagnostics).
diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js
index 1affa27f687..a2dcac93bb1 100644
--- a/lib/amd/build/templates.min.js
+++ b/lib/amd/build/templates.min.js
@@ -1,2 +1,2 @@
-define ("core/templates",["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/config","core/localstorage","core/icon_system","core/event","core/yui","core/log","core/truncate","core/user_date","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p=0,q={},r={},s={},t={},u=[],v=!1,w=function(a){if(a in r){return r[a]}if(a in q){r[a]=b.Deferred().resolve(q[a]).promise();return r[a]}var c=h.get("core_template/"+a);if(c){q[a]=c;r[a]=b.Deferred().resolve(c).promise();return r[a]}return null},x=function(){if(!u.length){return}if(v){return}v=!0;var a=u.slice(),e=b.Deferred(),f=[],g=a.map(function(a){var c=a.component,i=a.name,j=a.searchKey,k=a.theme,l=a.deferred,m=null,n=w(j);if(n){m=n}else{f.push({methodname:"core_output_load_template_with_dependencies",args:{component:c,template:i,themename:k,lang:b("html").attr("lang").replace(/-/g,"_")}});var o=f.length-1;m=e.promise().then(function(a){g[j]=a[o].then(function(a){var b=null;a.templates.forEach(function(a){var d=[k,a.component,a.name].join("/");q[d]=a.value;h.set("core_template/"+d,a.value);if(a.component==c&&a.name==i){b=a.value}});if(a.strings.length){d.cache_strings(a.strings.map(function(a){return{component:a.component,key:a.name,value:a.value}}))}return b});return g[j]})}return m.then(function(a){return l.resolve(a)}).catch(function(a){l.reject(a);throw a})});if(f.length){e.resolve(c.call(f,!0,!1,!1,0,M.cfg.themerev))}else{e.resolve()}b.when.apply(null,g).then(function(){u.splice(0,a.length);v=!1;x()}).catch(function(){u.splice(0,a.length);v=!1;x()})},y=function(){this.requiredStrings=[];this.requiredJS=[];this.requiredDates=[];this.currentThemeName=""};y.prototype.requiredStrings=null;y.prototype.requiredDates=[];y.prototype.requiredJS=null;y.prototype.currentThemeName="";y.prototype.getTemplate=function(a){var c=this.currentThemeName,d=c+"/"+a,e=w(d);if(e){return e}var f=u.filter(function(a){return a.searchKey==d});if(f.length){return f[0].deferred.promise()}var g=a.split("/"),h=g.shift(),i=g.shift(),j=b.Deferred();u.push({component:h,name:i,theme:c,searchKey:d,deferred:j});x();return j.promise()};y.prototype.partialHelper=function(a){var b=this.currentThemeName+"/"+a;if(!(b in q)){e.exception(new Error("Failed to pre-fetch the template: "+a))}return q[b]};y.prototype.renderIcon=function(a,c,d){var f=g.iconsystemmodule,h=b.Deferred();require([f],function(a){var b=new a;if(!(b instanceof i)){h.reject("Invalid icon system specified"+g.iconsystemmodule)}else{t=b;b.init().then(h.resolve).catch(e.exception)}});return h.then(function(a){return this.getTemplate(a.getTemplateName())}.bind(this)).then(function(b){return t.renderIcon(a,c,d,b)})};y.prototype.pixHelper=function(a,b,c){var d=b.split(","),e="",f="",g="";if(0=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g,"
");return"\""+d+"\""};y.prototype.shortenTextHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=e[1].trim(),g=e[2].trim(),h=c(g,a);return m.truncate(h,{length:f,words:!0,ellipsis:"..."})};y.prototype.userDateHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=c(e[1].trim(),a),g=c(e[2].trim(),a),h=this.requiredDates.length;this.requiredDates.push({timestamp:f,format:g});return"[[_t_"+h+"]]"};y.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=function(){return this.stringHelper.bind(this,a)}.bind(this);a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this);a.js=function(){return this.jsHelper.bind(this,a)}.bind(this);a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this);a.shortentext=function(){return this.shortenTextHelper.bind(this,a)}.bind(this);a.userdate=function(){return this.userDateHelper.bind(this,a)}.bind(this);a.globals={config:g};a.currentTheme=b};y.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},A=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}z(d);j.notifyFilterContentUpdated(g)}};y.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4=}}$1<%={{ }}=%>").replace(/(\r\n|\r|\n)/g,"
");return"\""+d+"\""};y.prototype.shortenTextHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=e[1].trim(),g=e[2].trim(),h=c(g,a);return m.truncate(h,{length:f,words:!0,ellipsis:"..."})};y.prototype.userDateHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=c(e[1].trim(),a),g=c(e[2].trim(),a),h=this.requiredDates.length;this.requiredDates.push({timestamp:f,format:g});return"[[_t_"+h+"]]"};y.prototype.addHelpers=function(a,b){this.currentThemeName=b;this.requiredStrings=[];this.requiredJS=[];a.uniqid=p++;a.str=function(){return this.stringHelper.bind(this,a)}.bind(this);a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this);a.js=function(){return this.jsHelper.bind(this,a)}.bind(this);a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this);a.shortentext=function(){return this.shortenTextHelper.bind(this,a)}.bind(this);a.userdate=function(){return this.userDateHelper.bind(this,a)}.bind(this);a.globals={config:g};a.currentTheme=b};y.prototype.getJS=function(){var a="";if(0").attr("type","text/javascript").html(a);b("head").append(c)}},A=function(a,c,d,e){var f=b(a);if(f.length){var g=b(c),h=null;if(e){h=new k.NodeList(f.children().get());h.destroy(!0);f.empty();f.append(g)}else{h=new k.NodeList(f.get());h.destroy(!0);f.replaceWith(g)}z(d);j.notifyFilterContentUpdated(g)}};y.prototype.scanForPartials=function(b){var c=a.parse(b),d=[],e=function(a,b){var c,d;for(c=0;c"==d[0]||"<"==d[0]){b.push(d[1])}if(4.\n\n/**\n * Template renderer for Moodle. Load and render Moodle templates with Mustache.\n *\n * @module core/templates\n * @package core\n * @class templates\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 2.9\n */\ndefine([\n 'core/mustache',\n 'jquery',\n 'core/ajax',\n 'core/str',\n 'core/notification',\n 'core/url',\n 'core/config',\n 'core/localstorage',\n 'core/icon_system',\n 'core/event',\n 'core/yui',\n 'core/log',\n 'core/truncate',\n 'core/user_date',\n 'core/pending',\n ],\n function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate,\n Pending) {\n\n // Module variables.\n /** @var {Number} uniqInstances Count of times this constructor has been called. */\n var uniqInstances = 0;\n\n /** @var {String[]} templateCache - Cache of already loaded template strings */\n var templateCache = {};\n\n /** @var {Promise[]} templatePromises - Cache of already loaded template promises */\n var templatePromises = {};\n\n /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */\n var cachePartialPromises = {};\n\n /** @var {Object} iconSystem - Object extending core/iconsystem */\n var iconSystem = {};\n\n /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */\n var loadTemplateBuffer = [];\n\n /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */\n var isLoadingTemplates = false;\n\n /**\n * Search the various caches for a template promise for the given search key.\n * The search key should be in the format // e.g. boost/core/modal.\n *\n * If the template is found in any of the caches it will populate the other caches with\n * the same data as well.\n *\n * @param {String} searchKey The template search key in the format // e.g. boost/core/modal\n * @return {Object} jQuery promise resolved with the template source\n */\n var getTemplatePromiseFromCache = function(searchKey) {\n // First try the cache of promises.\n if (searchKey in templatePromises) {\n return templatePromises[searchKey];\n }\n\n // Check the module cache.\n if (searchKey in templateCache) {\n // Add this to the promises cache for future.\n templatePromises[searchKey] = $.Deferred().resolve(templateCache[searchKey]).promise();\n return templatePromises[searchKey];\n }\n\n // Now try local storage.\n var cached = storage.get('core_template/' + searchKey);\n if (cached) {\n // Add this to the module cache for future.\n templateCache[searchKey] = cached;\n // Add this to the promises cache for future.\n templatePromises[searchKey] = $.Deferred().resolve(cached).promise();\n return templatePromises[searchKey];\n }\n\n return null;\n };\n\n /**\n * Take all of the templates waiting in the buffer and load them from the server\n * or from the cache.\n *\n * All of the templates that need to be loaded from the server will be batched up\n * and sent in a single network request.\n */\n var processLoadTemplateBuffer = function() {\n if (!loadTemplateBuffer.length) {\n return;\n }\n\n if (isLoadingTemplates) {\n return;\n }\n\n isLoadingTemplates = true;\n // Grab any templates waiting in the buffer.\n var templatesToLoad = loadTemplateBuffer.slice();\n // This will be resolved with the list of promises for the server request.\n var serverRequestsDeferred = $.Deferred();\n var requests = [];\n // Get a list of promises for each of the templates we need to load.\n var templatePromises = templatesToLoad.map(function(templateData) {\n var component = templateData.component;\n var name = templateData.name;\n var searchKey = templateData.searchKey;\n var theme = templateData.theme;\n var templateDeferred = templateData.deferred;\n var promise = null;\n\n // Double check to see if this template happened to have landed in the\n // cache as a dependency of an earlier template.\n var cachedPromise = getTemplatePromiseFromCache(searchKey);\n if (cachedPromise) {\n // We've seen this template so immediately resolve the existing promise.\n promise = cachedPromise;\n } else {\n // We haven't seen this template yet so we need to request it from\n // the server.\n requests.push({\n methodname: 'core_output_load_template_with_dependencies',\n args: {\n component: component,\n template: name,\n themename: theme,\n lang: $('html').attr('lang').replace(/-/g, '_')\n }\n });\n // Remember the index in the requests list for this template so that\n // we can get the appropriate promise back.\n var index = requests.length - 1;\n\n // The server deferred will be resolved with a list of all of the promises\n // that were sent in the order that they were added to the requests array.\n promise = serverRequestsDeferred.promise()\n .then(function(promises) {\n // The promise for this template will be the one that matches the index\n // for it's entry in the requests array.\n //\n // Make sure the promise is added to the promises cache for this template\n // search key so that we don't request it again.\n templatePromises[searchKey] = promises[index].then(function(response) {\n var templateSource = null;\n\n // Process all of the template dependencies for this template and add\n // them to the caches so that we don't request them again later.\n response.templates.forEach(function(data) {\n // Generate the search key for this template in the response so that we\n // can add it to the caches.\n var tempSearchKey = [theme, data.component, data.name].join('/');\n // Cache all of the dependent templates because we'll need them to render\n // the requested template.\n templateCache[tempSearchKey] = data.value;\n storage.set('core_template/' + tempSearchKey, data.value);\n\n if (data.component == component && data.name == name) {\n // This is the original template that was requested so remember it to return.\n templateSource = data.value;\n }\n });\n\n if (response.strings.length) {\n // If we have strings that the template needs then warm the string cache\n // with them now so that we don't need to re-fetch them.\n str.cache_strings(response.strings.map(function(data) {\n return {\n component: data.component,\n key: data.name,\n value: data.value\n };\n }));\n }\n\n // Return the original template source that the user requested.\n return templateSource;\n });\n\n return templatePromises[searchKey];\n });\n }\n\n return promise\n .then(function(source) {\n // When we've successfully loaded the template then resolve the deferred\n // in the buffer so that all of the calling code can proceed.\n return templateDeferred.resolve(source);\n })\n .catch(function(error) {\n // If there was an error loading the template then reject the deferred\n // in the buffer so that all of the calling code can proceed.\n templateDeferred.reject(error);\n // Rethrow for anyone else listening.\n throw error;\n });\n });\n\n if (requests.length) {\n // We have requests to send so resolve the deferred with the promises.\n serverRequestsDeferred.resolve(ajax.call(requests, true, false, false, 0, M.cfg.themerev));\n } else {\n // Nothing to load so we can resolve our deferred.\n serverRequestsDeferred.resolve();\n }\n\n // Once we've finished loading all of the templates then recurse to process\n // any templates that may have been added to the buffer in the time that we\n // were fetching.\n $.when.apply(null, templatePromises)\n .then(function() {\n // Remove the templates we've loaded from the buffer.\n loadTemplateBuffer.splice(0, templatesToLoad.length);\n isLoadingTemplates = false;\n processLoadTemplateBuffer();\n return;\n })\n .catch(function() {\n // Remove the templates we've loaded from the buffer.\n loadTemplateBuffer.splice(0, templatesToLoad.length);\n isLoadingTemplates = false;\n processLoadTemplateBuffer();\n });\n };\n\n /**\n * Constructor\n *\n * Each call to templates.render gets it's own instance of this class.\n */\n var Renderer = function() {\n this.requiredStrings = [];\n this.requiredJS = [];\n this.requiredDates = [];\n this.currentThemeName = '';\n };\n // Class variables and functions.\n\n /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */\n Renderer.prototype.requiredStrings = null;\n\n /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */\n Renderer.prototype.requiredDates = [];\n\n /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */\n Renderer.prototype.requiredJS = null;\n\n /** @var {String} themeName for the current render */\n Renderer.prototype.currentThemeName = '';\n\n /**\n * Load a template.\n *\n * @method getTemplate\n * @private\n * @param {string} templateName - should consist of the component and the name of the template like this:\n * core/menu (lib/templates/menu.mustache) or\n * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)\n * @return {Promise} JQuery promise object resolved when the template has been fetched.\n */\n Renderer.prototype.getTemplate = function(templateName) {\n var currentTheme = this.currentThemeName;\n var searchKey = currentTheme + '/' + templateName;\n\n // If we haven't already seen this template then buffer it.\n var cachedPromise = getTemplatePromiseFromCache(searchKey);\n if (cachedPromise) {\n return cachedPromise;\n }\n\n // Check the buffer to seee if this template has already been added.\n var existingBufferRecords = loadTemplateBuffer.filter(function(record) {\n return record.searchKey == searchKey;\n });\n if (existingBufferRecords.length) {\n // This template is already in the buffer so just return the existing\n // promise. No need to add it to the buffer again.\n return existingBufferRecords[0].deferred.promise();\n }\n\n // This is the first time this has been requested so let's add it to the buffer\n // to be loaded.\n var parts = templateName.split('/');\n var component = parts.shift();\n var name = parts.shift();\n var deferred = $.Deferred();\n\n // Add this template to the buffer to be loaded.\n loadTemplateBuffer.push({\n component: component,\n name: name,\n theme: currentTheme,\n searchKey: searchKey,\n deferred: deferred\n });\n\n // We know there is at least one thing in the buffer so kick off a processing run.\n processLoadTemplateBuffer();\n return deferred.promise();\n };\n\n /**\n * Load a partial from the cache or ajax.\n *\n * @method partialHelper\n * @private\n * @param {string} name The partial name to load.\n * @return {string}\n */\n Renderer.prototype.partialHelper = function(name) {\n\n var searchKey = this.currentThemeName + '/' + name;\n\n if (!(searchKey in templateCache)) {\n notification.exception(new Error('Failed to pre-fetch the template: ' + name));\n }\n\n return templateCache[searchKey];\n };\n\n /**\n * Render a single image icon.\n *\n * @method renderIcon\n * @private\n * @param {string} key The icon key.\n * @param {string} component The component name.\n * @param {string} title The icon title\n * @return {Promise}\n */\n Renderer.prototype.renderIcon = function(key, component, title) {\n // Preload the module to do the icon rendering based on the theme iconsystem.\n var modulename = config.iconsystemmodule;\n\n // RequireJS does not return a promise.\n var ready = $.Deferred();\n require([modulename], function(System) {\n var system = new System();\n if (!(system instanceof IconSystem)) {\n ready.reject('Invalid icon system specified' + config.iconsystemmodule);\n } else {\n iconSystem = system;\n system.init().then(ready.resolve).catch(notification.exception);\n }\n });\n\n return ready.then(function(iconSystem) {\n return this.getTemplate(iconSystem.getTemplateName());\n }.bind(this)).then(function(template) {\n return iconSystem.renderIcon(key, component, title, template);\n });\n };\n\n /**\n * Render image icons.\n *\n * @method pixHelper\n * @private\n * @param {object} context The mustache context\n * @param {string} sectionText The text to parse arguments from.\n * @param {function} helper Used to render the alt attribute of the text.\n * @return {string}\n */\n Renderer.prototype.pixHelper = function(context, sectionText, helper) {\n var parts = sectionText.split(',');\n var key = '';\n var component = '';\n var text = '';\n\n if (parts.length > 0) {\n key = helper(parts.shift().trim(), context);\n }\n if (parts.length > 0) {\n component = helper(parts.shift().trim(), context);\n }\n if (parts.length > 0) {\n text = helper(parts.join(',').trim(), context);\n }\n\n var templateName = iconSystem.getTemplateName();\n\n var searchKey = this.currentThemeName + '/' + templateName;\n var template = templateCache[searchKey];\n\n // The key might have been escaped by the JS Mustache engine which\n // converts forward slashes to HTML entities. Let us undo that here.\n key = key.replace(///gi, '/');\n\n return iconSystem.renderIcon(key, component, text, template);\n };\n\n /**\n * Render blocks of javascript and save them in an array.\n *\n * @method jsHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to save as a js block.\n * @param {function} helper Used to render the block.\n * @return {string}\n */\n Renderer.prototype.jsHelper = function(context, sectionText, helper) {\n this.requiredJS.push(helper(sectionText, context));\n return '';\n };\n\n /**\n * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}\n * into a get_string call.\n *\n * @method stringHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @return {string}\n */\n Renderer.prototype.stringHelper = function(context, sectionText, helper) {\n var parts = sectionText.split(',');\n var key = '';\n var component = '';\n var param = '';\n if (parts.length > 0) {\n key = parts.shift().trim();\n }\n if (parts.length > 0) {\n component = parts.shift().trim();\n }\n if (parts.length > 0) {\n param = parts.join(',').trim();\n }\n\n if (param !== '') {\n // Allow variable expansion in the param part only.\n param = helper(param, context);\n }\n // Allow json formatted $a arguments.\n if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {\n param = JSON.parse(param);\n }\n\n var index = this.requiredStrings.length;\n this.requiredStrings.push({key: key, component: component, param: param});\n\n // The placeholder must not use {{}} as those can be misinterpreted by the engine.\n return '[[_s' + index + ']]';\n };\n\n /**\n * Quote helper used to wrap content in quotes, and escape all quotes present in the content.\n *\n * @method quoteHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @return {string}\n */\n Renderer.prototype.quoteHelper = function(context, sectionText, helper) {\n var content = helper(sectionText.trim(), context);\n\n // Escape the {{ and the \".\n // This involves wrapping {{, and }} in change delimeter tags.\n content = content\n .replace('\"', '\\\\\"')\n .replace(/([\\{\\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')\n .replace(/(\\r\\n|\\r|\\n)/g, '
')\n ;\n return '\"' + content + '\"';\n };\n\n /**\n * Shorten text helper to truncate text and append a trailing ellipsis.\n *\n * @method shortenTextHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @return {string}\n */\n Renderer.prototype.shortenTextHelper = function(context, sectionText, helper) {\n // Non-greedy split on comma to grab section text into the length and\n // text parts.\n var regex = /(.*?),(.*)/;\n var parts = sectionText.match(regex);\n // The length is the part matched in the first set of parethesis.\n var length = parts[1].trim();\n // The length is the part matched in the second set of parethesis.\n var text = parts[2].trim();\n var content = helper(text, context);\n return Truncate.truncate(content, {\n length: length,\n words: true,\n ellipsis: '...'\n });\n };\n\n /**\n * User date helper to render user dates from timestamps.\n *\n * @method userDateHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @return {string}\n */\n Renderer.prototype.userDateHelper = function(context, sectionText, helper) {\n // Non-greedy split on comma to grab the timestamp and format.\n var regex = /(.*?),(.*)/;\n var parts = sectionText.match(regex);\n var timestamp = helper(parts[1].trim(), context);\n var format = helper(parts[2].trim(), context);\n var index = this.requiredDates.length;\n\n this.requiredDates.push({\n timestamp: timestamp,\n format: format\n });\n\n return '[[_t_' + index + ']]';\n };\n\n /**\n * Add some common helper functions to all context objects passed to templates.\n * These helpers match exactly the helpers available in php.\n *\n * @method addHelpers\n * @private\n * @param {Object} context Simple types used as the context for the template.\n * @param {String} themeName We set this multiple times, because there are async calls.\n */\n Renderer.prototype.addHelpers = function(context, themeName) {\n this.currentThemeName = themeName;\n this.requiredStrings = [];\n this.requiredJS = [];\n context.uniqid = (uniqInstances++);\n context.str = function() {\n return this.stringHelper.bind(this, context);\n }.bind(this);\n context.pix = function() {\n return this.pixHelper.bind(this, context);\n }.bind(this);\n context.js = function() {\n return this.jsHelper.bind(this, context);\n }.bind(this);\n context.quote = function() {\n return this.quoteHelper.bind(this, context);\n }.bind(this);\n context.shortentext = function() {\n return this.shortenTextHelper.bind(this, context);\n }.bind(this);\n context.userdate = function() {\n return this.userDateHelper.bind(this, context);\n }.bind(this);\n context.globals = {config: config};\n context.currentTheme = themeName;\n };\n\n /**\n * Get all the JS blocks from the last rendered template.\n *\n * @method getJS\n * @private\n * @return {string}\n */\n Renderer.prototype.getJS = function() {\n var js = '';\n if (this.requiredJS.length > 0) {\n js = this.requiredJS.join(\";\\n\");\n }\n\n return js;\n };\n\n /**\n * Treat strings in content.\n *\n * The purpose of this method is to replace the placeholders found in a string\n * with the their respective translated strings.\n *\n * Previously we were relying on String.replace() but the complexity increased with\n * the numbers of strings to replace. Now we manually walk the string and stop at each\n * placeholder we find, only then we replace it. Most of the time we will\n * replace all the placeholders in a single run, at times we will need a few\n * more runs when placeholders are replaced with strings that contain placeholders\n * themselves.\n *\n * @param {String} content The content in which string placeholders are to be found.\n * @param {Array} strings The strings to replace with.\n * @return {String} The treated content.\n */\n Renderer.prototype.treatStringsInContent = function(content, strings) {\n var pattern = /\\[\\[_s\\d+\\]\\]/,\n treated,\n index,\n strIndex,\n walker,\n char,\n strFinal;\n\n do {\n treated = '';\n index = content.search(pattern);\n while (index > -1) {\n\n // Copy the part prior to the placeholder to the treated string.\n treated += content.substring(0, index);\n content = content.substr(index);\n strIndex = '';\n walker = 4; // 4 is the length of '[[_s'.\n\n // Walk the characters to manually extract the index of the string from the placeholder.\n char = content.substr(walker, 1);\n do {\n strIndex += char;\n walker++;\n char = content.substr(walker, 1);\n } while (char != ']');\n\n // Get the string, add it to the treated result, and remove the placeholder from the content to treat.\n strFinal = strings[parseInt(strIndex, 10)];\n if (typeof strFinal === 'undefined') {\n Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');\n strFinal = '';\n }\n treated += strFinal;\n content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.\n\n // Find the next placeholder.\n index = content.search(pattern);\n }\n\n // The content becomes the treated part with the rest of the content.\n content = treated + content;\n\n // Check if we need to walk the content again, in case strings contained placeholders.\n index = content.search(pattern);\n\n } while (index > -1);\n\n return content;\n };\n\n /**\n * Treat strings in content.\n *\n * The purpose of this method is to replace the date placeholders found in the\n * content with the their respective translated dates.\n *\n * @param {String} content The content in which string placeholders are to be found.\n * @param {Array} strings The strings to replace with.\n * @return {String} The treated content.\n */\n Renderer.prototype.treatDatesInContent = function(content, dates) {\n dates.forEach(function(date, index) {\n var key = '\\\\[\\\\[_t_' + index + '\\\\]\\\\]';\n var re = new RegExp(key, 'g');\n content = content.replace(re, date);\n });\n\n return content;\n };\n\n /**\n * Render a template and then call the callback with the result.\n *\n * @method doRender\n * @private\n * @param {string} templateSource The mustache template to render.\n * @param {Object} context Simple types used as the context for the template.\n * @param {String} themeName Name of the current theme.\n * @return {Promise} object\n */\n Renderer.prototype.doRender = function(templateSource, context, themeName) {\n this.currentThemeName = themeName;\n var iconTemplate = iconSystem.getTemplateName();\n\n var pendingPromise = new Pending('core/templates:doRender');\n return this.getTemplate(iconTemplate).then(function() {\n this.addHelpers(context, themeName);\n var result = mustache.render(templateSource, context, this.partialHelper.bind(this));\n return $.Deferred().resolve(result.trim(), this.getJS()).promise();\n }.bind(this))\n .then(function(html, js) {\n if (this.requiredStrings.length > 0) {\n return str.get_strings(this.requiredStrings).then(function(strings) {\n\n // Make sure string substitutions are done for the userdate\n // values as well.\n this.requiredDates = this.requiredDates.map(function(date) {\n return {\n timestamp: this.treatStringsInContent(date.timestamp, strings),\n format: this.treatStringsInContent(date.format, strings)\n };\n }.bind(this));\n\n // Why do we not do another call the render here?\n //\n // Because that would expose DOS holes. E.g.\n // I create an assignment called \"{{fish\" which\n // would get inserted in the template in the first pass\n // and cause the template to die on the second pass (unbalanced).\n html = this.treatStringsInContent(html, strings);\n js = this.treatStringsInContent(js, strings);\n return $.Deferred().resolve(html, js).promise();\n }.bind(this));\n }\n\n return $.Deferred().resolve(html, js).promise();\n }.bind(this))\n .then(function(html, js) {\n // This has to happen after the strings replacement because you can\n // use the string helper in content for the user date helper.\n if (this.requiredDates.length > 0) {\n return UserDate.get(this.requiredDates).then(function(dates) {\n html = this.treatDatesInContent(html, dates);\n js = this.treatDatesInContent(js, dates);\n return $.Deferred().resolve(html, js).promise();\n }.bind(this));\n }\n\n return $.Deferred().resolve(html, js).promise();\n }.bind(this))\n .then(function(html, js) {\n pendingPromise.resolve();\n return $.Deferred().resolve(html, js).promise();\n });\n };\n\n /**\n * Execute a block of JS returned from a template.\n * Call this AFTER adding the template HTML into the DOM so the nodes can be found.\n *\n * @method runTemplateJS\n * @param {string} source - A block of javascript.\n */\n var runTemplateJS = function(source) {\n if (source.trim() !== '') {\n var newscript = $('