Merge branch 'MDL-57636_master' of https://github.com/dasistwas/moodle
[moodle.git] / lib / amd / src / templates.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Template renderer for Moodle. Load and render Moodle templates with Mustache.
18  *
19  * @module     core/templates
20  * @package    core
21  * @class      templates
22  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      2.9
25  */
26 define([
27         'core/mustache',
28         'jquery',
29         'core/ajax',
30         'core/str',
31         'core/notification',
32         'core/url',
33         'core/config',
34         'core/localstorage',
35         'core/icon_system',
36         'core/event',
37         'core/yui',
38         'core/log',
39         'core/truncate',
40         'core/user_date',
41         'core/pending',
42     ],
43     function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate,
44         Pending) {
46     // Module variables.
47     /** @var {Number} uniqInstances Count of times this constructor has been called. */
48     var uniqInstances = 0;
50     /** @var {String[]} templateCache - Cache of already loaded template strings */
51     var templateCache = {};
53     /** @var {Promise[]} templatePromises - Cache of already loaded template promises */
54     var templatePromises = {};
56     /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */
57     var cachePartialPromises = {};
59     /** @var {Object} iconSystem - Object extending core/iconsystem */
60     var iconSystem = {};
62     /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */
63     var loadTemplateBuffer = [];
65     /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */
66     var isLoadingTemplates = false;
68     /**
69      * Search the various caches for a template promise for the given search key.
70      * The search key should be in the format <theme>/<component>/<template> e.g. boost/core/modal.
71      *
72      * If the template is found in any of the caches it will populate the other caches with
73      * the same data as well.
74      *
75      * @param {String} searchKey The template search key in the format <theme>/<component>/<template> e.g. boost/core/modal
76      * @return {Object} jQuery promise resolved with the template source
77      */
78     var getTemplatePromiseFromCache = function(searchKey) {
79         // First try the cache of promises.
80         if (searchKey in templatePromises) {
81             return templatePromises[searchKey];
82         }
84         // Check the module cache.
85         if (searchKey in templateCache) {
86             // Add this to the promises cache for future.
87             templatePromises[searchKey] = $.Deferred().resolve(templateCache[searchKey]).promise();
88             return templatePromises[searchKey];
89         }
91         // Now try local storage.
92         var cached = storage.get('core_template/' + searchKey);
93         if (cached) {
94             // Add this to the module cache for future.
95             templateCache[searchKey] = cached;
96             // Add this to the promises cache for future.
97             templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
98             return templatePromises[searchKey];
99         }
101         return null;
102     };
104     /**
105      * Take all of the templates waiting in the buffer and load them from the server
106      * or from the cache.
107      *
108      * All of the templates that need to be loaded from the server will be batched up
109      * and sent in a single network request.
110      */
111     var processLoadTemplateBuffer = function() {
112         if (!loadTemplateBuffer.length) {
113             return;
114         }
116         if (isLoadingTemplates) {
117             return;
118         }
120         isLoadingTemplates = true;
121         // Grab any templates waiting in the buffer.
122         var templatesToLoad = loadTemplateBuffer.slice();
123         // This will be resolved with the list of promises for the server request.
124         var serverRequestsDeferred = $.Deferred();
125         var requests = [];
126         // Get a list of promises for each of the templates we need to load.
127         var templatePromises = templatesToLoad.map(function(templateData) {
128             var component = templateData.component;
129             var name = templateData.name;
130             var searchKey = templateData.searchKey;
131             var theme = templateData.theme;
132             var templateDeferred = templateData.deferred;
133             var promise = null;
135             // Double check to see if this template happened to have landed in the
136             // cache as a dependency of an earlier template.
137             var cachedPromise = getTemplatePromiseFromCache(searchKey);
138             if (cachedPromise) {
139                 // We've seen this template so immediately resolve the existing promise.
140                 promise = cachedPromise;
141             } else {
142                 // We haven't seen this template yet so we need to request it from
143                 // the server.
144                 requests.push({
145                     methodname: 'core_output_load_template_with_dependencies',
146                     args: {
147                         component: component,
148                         template: name,
149                         themename: theme
150                     }
151                 });
152                 // Remember the index in the requests list for this template so that
153                 // we can get the appropriate promise back.
154                 var index = requests.length - 1;
156                 // The server deferred will be resolved with a list of all of the promises
157                 // that were sent in the order that they were added to the requests array.
158                 promise = serverRequestsDeferred.promise()
159                     .then(function(promises) {
160                         // The promise for this template will be the one that matches the index
161                         // for it's entry in the requests array.
162                         //
163                         // Make sure the promise is added to the promises cache for this template
164                         // search key so that we don't request it again.
165                         templatePromises[searchKey] = promises[index].then(function(response) {
166                             var templateSource = null;
168                             // Process all of the template dependencies for this template and add
169                             // them to the caches so that we don't request them again later.
170                             response.templates.forEach(function(data) {
171                                 // Generate the search key for this template in the response so that we
172                                 // can add it to the caches.
173                                 var tempSearchKey = [theme, data.component, data.name].join('/');
174                                 // Cache all of the dependent templates because we'll need them to render
175                                 // the requested template.
176                                 templateCache[tempSearchKey] = data.value;
177                                 storage.set('core_template/' + tempSearchKey, data.value);
179                                 if (data.component == component && data.name == name) {
180                                     // This is the original template that was requested so remember it to return.
181                                     templateSource = data.value;
182                                 }
183                             });
185                             if (response.strings.length) {
186                                 // If we have strings that the template needs then warm the string cache
187                                 // with them now so that we don't need to re-fetch them.
188                                 str.cache_strings(response.strings.map(function(data) {
189                                     return {
190                                         component: data.component,
191                                         key: data.name,
192                                         value: data.value
193                                     };
194                                 }));
195                             }
197                             // Return the original template source that the user requested.
198                             return templateSource;
199                         });
201                         return templatePromises[searchKey];
202                     });
203             }
205             return promise
206                 .then(function(source) {
207                     // When we've successfully loaded the template then resolve the deferred
208                     // in the buffer so that all of the calling code can proceed.
209                     return templateDeferred.resolve(source);
210                 })
211                 .catch(function(error) {
212                     // If there was an error loading the template then reject the deferred
213                     // in the buffer so that all of the calling code can proceed.
214                     templateDeferred.reject(error);
215                     // Rethrow for anyone else listening.
216                     throw error;
217                 });
218         });
220         if (requests.length) {
221             // We have requests to send so resolve the deferred with the promises.
222             serverRequestsDeferred.resolve(ajax.call(requests, true, false));
223         } else {
224             // Nothing to load so we can resolve our deferred.
225             serverRequestsDeferred.resolve();
226         }
228         // Once we've finished loading all of the templates then recurse to process
229         // any templates that may have been added to the buffer in the time that we
230         // were fetching.
231         $.when.apply(null, templatePromises)
232             .then(function() {
233                 // Remove the templates we've loaded from the buffer.
234                 loadTemplateBuffer.splice(0, templatesToLoad.length);
235                 isLoadingTemplates = false;
236                 processLoadTemplateBuffer();
237                 return;
238             })
239             .catch(function() {
240                 // Remove the templates we've loaded from the buffer.
241                 loadTemplateBuffer.splice(0, templatesToLoad.length);
242                 isLoadingTemplates = false;
243                 processLoadTemplateBuffer();
244             });
245     };
247     /**
248      * Constructor
249      *
250      * Each call to templates.render gets it's own instance of this class.
251      */
252     var Renderer = function() {
253         this.requiredStrings = [];
254         this.requiredJS = [];
255         this.requiredDates = [];
256         this.currentThemeName = '';
257     };
258     // Class variables and functions.
260     /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
261     Renderer.prototype.requiredStrings = null;
263     /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */
264     Renderer.prototype.requiredDates = [];
266     /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
267     Renderer.prototype.requiredJS = null;
269     /** @var {String} themeName for the current render */
270     Renderer.prototype.currentThemeName = '';
272     /**
273      * Load a template.
274      *
275      * @method getTemplate
276      * @private
277      * @param {string} templateName - should consist of the component and the name of the template like this:
278      *                              core/menu (lib/templates/menu.mustache) or
279      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
280      * @return {Promise} JQuery promise object resolved when the template has been fetched.
281      */
282     Renderer.prototype.getTemplate = function(templateName) {
283         var currentTheme = this.currentThemeName;
284         var searchKey = currentTheme + '/' + templateName;
286         // If we haven't already seen this template then buffer it.
287         var cachedPromise = getTemplatePromiseFromCache(searchKey);
288         if (cachedPromise) {
289             return cachedPromise;
290         }
292         // Check the buffer to seee if this template has already been added.
293         var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
294             return record.searchKey == searchKey;
295         });
296         if (existingBufferRecords.length) {
297             // This template is already in the buffer so just return the existing
298             // promise. No need to add it to the buffer again.
299             return existingBufferRecords[0].deferred.promise();
300         }
302         // This is the first time this has been requested so let's add it to the buffer
303         // to be loaded.
304         var parts = templateName.split('/');
305         var component = parts.shift();
306         var name = parts.shift();
307         var deferred = $.Deferred();
309         // Add this template to the buffer to be loaded.
310         loadTemplateBuffer.push({
311             component: component,
312             name: name,
313             theme: currentTheme,
314             searchKey: searchKey,
315             deferred: deferred
316         });
318         // We know there is at least one thing in the buffer so kick off a processing run.
319         processLoadTemplateBuffer();
320         return deferred.promise();
321     };
323     /**
324      * Load a partial from the cache or ajax.
325      *
326      * @method partialHelper
327      * @private
328      * @param {string} name The partial name to load.
329      * @return {string}
330      */
331     Renderer.prototype.partialHelper = function(name) {
333         var searchKey = this.currentThemeName + '/' + name;
335         if (!(searchKey in templateCache)) {
336             notification.exception(new Error('Failed to pre-fetch the template: ' + name));
337         }
339         return templateCache[searchKey];
340     };
342     /**
343      * Render a single image icon.
344      *
345      * @method renderIcon
346      * @private
347      * @param {string} key The icon key.
348      * @param {string} component The component name.
349      * @param {string} title The icon title
350      * @return {Promise}
351      */
352     Renderer.prototype.renderIcon = function(key, component, title) {
353         // Preload the module to do the icon rendering based on the theme iconsystem.
354         var modulename = config.iconsystemmodule;
356         // RequireJS does not return a promise.
357         var ready = $.Deferred();
358         require([modulename], function(System) {
359             var system = new System();
360             if (!(system instanceof IconSystem)) {
361                 ready.reject('Invalid icon system specified' + config.iconsystemmodule);
362             } else {
363                 iconSystem = system;
364                 system.init().then(ready.resolve).catch(notification.exception);
365             }
366         });
368         return ready.then(function(iconSystem) {
369             return this.getTemplate(iconSystem.getTemplateName());
370         }.bind(this)).then(function(template) {
371             return iconSystem.renderIcon(key, component, title, template);
372         });
373     };
375     /**
376      * Render image icons.
377      *
378      * @method pixHelper
379      * @private
380      * @param {object} context The mustache context
381      * @param {string} sectionText The text to parse arguments from.
382      * @param {function} helper Used to render the alt attribute of the text.
383      * @return {string}
384      */
385     Renderer.prototype.pixHelper = function(context, sectionText, helper) {
386         var parts = sectionText.split(',');
387         var key = '';
388         var component = '';
389         var text = '';
391         if (parts.length > 0) {
392             key = helper(parts.shift().trim(), context);
393         }
394         if (parts.length > 0) {
395             component = helper(parts.shift().trim(), context);
396         }
397         if (parts.length > 0) {
398             text = helper(parts.join(',').trim(), context);
399         }
401         var templateName = iconSystem.getTemplateName();
403         var searchKey = this.currentThemeName + '/' + templateName;
404         var template = templateCache[searchKey];
406         // The key might have been escaped by the JS Mustache engine which
407         // converts forward slashes to HTML entities. Let us undo that here.
408         key = key.replace(/&#x2F;/gi, '/');
410         return iconSystem.renderIcon(key, component, text, template);
411     };
413     /**
414      * Render blocks of javascript and save them in an array.
415      *
416      * @method jsHelper
417      * @private
418      * @param {object} context The current mustache context.
419      * @param {string} sectionText The text to save as a js block.
420      * @param {function} helper Used to render the block.
421      * @return {string}
422      */
423     Renderer.prototype.jsHelper = function(context, sectionText, helper) {
424         this.requiredJS.push(helper(sectionText, context));
425         return '';
426     };
428     /**
429      * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
430      * into a get_string call.
431      *
432      * @method stringHelper
433      * @private
434      * @param {object} context The current mustache context.
435      * @param {string} sectionText The text to parse the arguments from.
436      * @param {function} helper Used to render subsections of the text.
437      * @return {string}
438      */
439     Renderer.prototype.stringHelper = function(context, sectionText, helper) {
440         var parts = sectionText.split(',');
441         var key = '';
442         var component = '';
443         var param = '';
444         if (parts.length > 0) {
445             key = parts.shift().trim();
446         }
447         if (parts.length > 0) {
448             component = parts.shift().trim();
449         }
450         if (parts.length > 0) {
451             param = parts.join(',').trim();
452         }
454         if (param !== '') {
455             // Allow variable expansion in the param part only.
456             param = helper(param, context);
457         }
458         // Allow json formatted $a arguments.
459         if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
460             param = JSON.parse(param);
461         }
463         var index = this.requiredStrings.length;
464         this.requiredStrings.push({key: key, component: component, param: param});
466         // The placeholder must not use {{}} as those can be misinterpreted by the engine.
467         return '[[_s' + index + ']]';
468     };
470     /**
471      * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
472      *
473      * @method quoteHelper
474      * @private
475      * @param {object} context The current mustache context.
476      * @param {string} sectionText The text to parse the arguments from.
477      * @param {function} helper Used to render subsections of the text.
478      * @return {string}
479      */
480     Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
481         var content = helper(sectionText.trim(), context);
483         // Escape the {{ and the ".
484         // This involves wrapping {{, and }} in change delimeter tags.
485         content = content
486             .replace('"', '\\"')
487             .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
488             ;
489         return '"' + content + '"';
490     };
492     /**
493      * Shorten text helper to truncate text and append a trailing ellipsis.
494      *
495      * @method shortenTextHelper
496      * @private
497      * @param {object} context The current mustache context.
498      * @param {string} sectionText The text to parse the arguments from.
499      * @param {function} helper Used to render subsections of the text.
500      * @return {string}
501      */
502     Renderer.prototype.shortenTextHelper = function(context, sectionText, helper) {
503         // Non-greedy split on comma to grab section text into the length and
504         // text parts.
505         var regex = /(.*?),(.*)/;
506         var parts = sectionText.match(regex);
507         // The length is the part matched in the first set of parethesis.
508         var length = parts[1].trim();
509         // The length is the part matched in the second set of parethesis.
510         var text = parts[2].trim();
511         var content = helper(text, context);
512         return Truncate.truncate(content, {
513             length: length,
514             words: true,
515             ellipsis: '...'
516         });
517     };
519     /**
520      * User date helper to render user dates from timestamps.
521      *
522      * @method userDateHelper
523      * @private
524      * @param {object} context The current mustache context.
525      * @param {string} sectionText The text to parse the arguments from.
526      * @param {function} helper Used to render subsections of the text.
527      * @return {string}
528      */
529     Renderer.prototype.userDateHelper = function(context, sectionText, helper) {
530         // Non-greedy split on comma to grab the timestamp and format.
531         var regex = /(.*?),(.*)/;
532         var parts = sectionText.match(regex);
533         var timestamp = helper(parts[1].trim(), context);
534         var format = helper(parts[2].trim(), context);
535         var index = this.requiredDates.length;
537         this.requiredDates.push({
538             timestamp: timestamp,
539             format: format
540         });
542         return '[[_t_' + index + ']]';
543     };
545     /**
546      * Add some common helper functions to all context objects passed to templates.
547      * These helpers match exactly the helpers available in php.
548      *
549      * @method addHelpers
550      * @private
551      * @param {Object} context Simple types used as the context for the template.
552      * @param {String} themeName We set this multiple times, because there are async calls.
553      */
554     Renderer.prototype.addHelpers = function(context, themeName) {
555         this.currentThemeName = themeName;
556         this.requiredStrings = [];
557         this.requiredJS = [];
558         context.uniqid = (uniqInstances++);
559         context.str = function() {
560           return this.stringHelper.bind(this, context);
561         }.bind(this);
562         context.pix = function() {
563           return this.pixHelper.bind(this, context);
564         }.bind(this);
565         context.js = function() {
566           return this.jsHelper.bind(this, context);
567         }.bind(this);
568         context.quote = function() {
569           return this.quoteHelper.bind(this, context);
570         }.bind(this);
571         context.shortentext = function() {
572           return this.shortenTextHelper.bind(this, context);
573         }.bind(this);
574         context.userdate = function() {
575           return this.userDateHelper.bind(this, context);
576         }.bind(this);
577         context.globals = {config: config};
578         context.currentTheme = themeName;
579     };
581     /**
582      * Get all the JS blocks from the last rendered template.
583      *
584      * @method getJS
585      * @private
586      * @return {string}
587      */
588     Renderer.prototype.getJS = function() {
589         var js = '';
590         if (this.requiredJS.length > 0) {
591             js = this.requiredJS.join(";\n");
592         }
594         return js;
595     };
597     /**
598      * Treat strings in content.
599      *
600      * The purpose of this method is to replace the placeholders found in a string
601      * with the their respective translated strings.
602      *
603      * Previously we were relying on String.replace() but the complexity increased with
604      * the numbers of strings to replace. Now we manually walk the string and stop at each
605      * placeholder we find, only then we replace it. Most of the time we will
606      * replace all the placeholders in a single run, at times we will need a few
607      * more runs when placeholders are replaced with strings that contain placeholders
608      * themselves.
609      *
610      * @param {String} content The content in which string placeholders are to be found.
611      * @param {Array} strings The strings to replace with.
612      * @return {String} The treated content.
613      */
614     Renderer.prototype.treatStringsInContent = function(content, strings) {
615         var pattern = /\[\[_s\d+\]\]/,
616             treated,
617             index,
618             strIndex,
619             walker,
620             char,
621             strFinal;
623         do {
624             treated = '';
625             index = content.search(pattern);
626             while (index > -1) {
628                 // Copy the part prior to the placeholder to the treated string.
629                 treated += content.substring(0, index);
630                 content = content.substr(index);
631                 strIndex = '';
632                 walker = 4; // 4 is the length of '[[_s'.
634                 // Walk the characters to manually extract the index of the string from the placeholder.
635                 char = content.substr(walker, 1);
636                 do {
637                     strIndex += char;
638                     walker++;
639                     char = content.substr(walker, 1);
640                 } while (char != ']');
642                 // Get the string, add it to the treated result, and remove the placeholder from the content to treat.
643                 strFinal = strings[parseInt(strIndex, 10)];
644                 if (typeof strFinal === 'undefined') {
645                     Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
646                     strFinal = '';
647                 }
648                 treated += strFinal;
649                 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
651                 // Find the next placeholder.
652                 index = content.search(pattern);
653             }
655             // The content becomes the treated part with the rest of the content.
656             content = treated + content;
658             // Check if we need to walk the content again, in case strings contained placeholders.
659             index = content.search(pattern);
661         } while (index > -1);
663         return content;
664     };
666     /**
667      * Treat strings in content.
668      *
669      * The purpose of this method is to replace the date placeholders found in the
670      * content with the their respective translated dates.
671      *
672      * @param {String} content The content in which string placeholders are to be found.
673      * @param {Array} strings The strings to replace with.
674      * @return {String} The treated content.
675      */
676     Renderer.prototype.treatDatesInContent = function(content, dates) {
677         dates.forEach(function(date, index) {
678             var key = '\\[\\[_t_' + index + '\\]\\]';
679             var re = new RegExp(key, 'g');
680             content = content.replace(re, date);
681         });
683         return content;
684     };
686     /**
687      * Render a template and then call the callback with the result.
688      *
689      * @method doRender
690      * @private
691      * @param {string} templateSource The mustache template to render.
692      * @param {Object} context Simple types used as the context for the template.
693      * @param {String} themeName Name of the current theme.
694      * @return {Promise} object
695      */
696     Renderer.prototype.doRender = function(templateSource, context, themeName) {
697         this.currentThemeName = themeName;
698         var iconTemplate = iconSystem.getTemplateName();
700         var pendingPromise = new Pending('core/templates:doRender');
701         return this.getTemplate(iconTemplate).then(function() {
702             this.addHelpers(context, themeName);
703             var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
704             return $.Deferred().resolve(result.trim(), this.getJS()).promise();
705         }.bind(this))
706         .then(function(html, js) {
707             if (this.requiredStrings.length > 0) {
708                 return str.get_strings(this.requiredStrings).then(function(strings) {
710                     // Make sure string substitutions are done for the userdate
711                     // values as well.
712                     this.requiredDates = this.requiredDates.map(function(date) {
713                         return {
714                             timestamp: this.treatStringsInContent(date.timestamp, strings),
715                             format: this.treatStringsInContent(date.format, strings)
716                         };
717                     }.bind(this));
719                     // Why do we not do another call the render here?
720                     //
721                     // Because that would expose DOS holes. E.g.
722                     // I create an assignment called "{{fish" which
723                     // would get inserted in the template in the first pass
724                     // and cause the template to die on the second pass (unbalanced).
725                     html = this.treatStringsInContent(html, strings);
726                     js = this.treatStringsInContent(js, strings);
727                     return $.Deferred().resolve(html, js).promise();
728                 }.bind(this));
729             }
731             return $.Deferred().resolve(html, js).promise();
732         }.bind(this))
733         .then(function(html, js) {
734             // This has to happen after the strings replacement because you can
735             // use the string helper in content for the user date helper.
736             if (this.requiredDates.length > 0) {
737                 return UserDate.get(this.requiredDates).then(function(dates) {
738                     html = this.treatDatesInContent(html, dates);
739                     js = this.treatDatesInContent(js, dates);
740                     return $.Deferred().resolve(html, js).promise();
741                 }.bind(this));
742             }
744             return $.Deferred().resolve(html, js).promise();
745         }.bind(this))
746         .then(function(html, js) {
747             pendingPromise.resolve();
748             return $.Deferred().resolve(html, js).promise();
749         });
750     };
752     /**
753      * Execute a block of JS returned from a template.
754      * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
755      *
756      * @method runTemplateJS
757      * @param {string} source - A block of javascript.
758      */
759     var runTemplateJS = function(source) {
760         if (source.trim() !== '') {
761             var newscript = $('<script>').attr('type', 'text/javascript').html(source);
762             $('head').append(newscript);
763         }
764     };
766     /**
767      * Do some DOM replacement and trigger correct events and fire javascript.
768      *
769      * @method domReplace
770      * @private
771      * @param {JQuery} element - Element or selector to replace.
772      * @param {String} newHTML - HTML to insert / replace.
773      * @param {String} newJS - Javascript to run after the insertion.
774      * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
775      */
776     var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
777         var replaceNode = $(element);
778         if (replaceNode.length) {
779             // First create the dom nodes so we have a reference to them.
780             var newNodes = $(newHTML);
781             var yuiNodes = null;
782             // Do the replacement in the page.
783             if (replaceChildNodes) {
784                 // Cleanup any YUI event listeners attached to any of these nodes.
785                 yuiNodes = new Y.NodeList(replaceNode.children().get());
786                 yuiNodes.destroy(true);
788                 // JQuery will cleanup after itself.
789                 replaceNode.empty();
790                 replaceNode.append(newNodes);
791             } else {
792                 // Cleanup any YUI event listeners attached to any of these nodes.
793                 yuiNodes = new Y.NodeList(replaceNode.get());
794                 yuiNodes.destroy(true);
796                 // JQuery will cleanup after itself.
797                 replaceNode.replaceWith(newNodes);
798             }
799             // Run any javascript associated with the new HTML.
800             runTemplateJS(newJS);
801             // Notify all filters about the new content.
802             event.notifyFilterContentUpdated(newNodes);
803         }
804     };
806     /**
807      * Scan a template source for partial tags and return a list of the found partials.
808      *
809      * @method scanForPartials
810      * @private
811      * @param {string} templateSource - source template to scan.
812      * @return {Array} List of partials.
813      */
814     Renderer.prototype.scanForPartials = function(templateSource) {
815         var tokens = mustache.parse(templateSource),
816             partials = [];
818         var findPartial = function(tokens, partials) {
819             var i, token;
820             for (i = 0; i < tokens.length; i++) {
821                 token = tokens[i];
822                 if (token[0] == '>' || token[0] == '<') {
823                     partials.push(token[1]);
824                 }
825                 if (token.length > 4) {
826                     findPartial(token[4], partials);
827                 }
828             }
829         };
831         findPartial(tokens, partials);
833         return partials;
834     };
836     /**
837      * Load a template and scan it for partials. Recursively fetch the partials.
838      *
839      * @method cachePartials
840      * @private
841      * @param {string} templateName - should consist of the component and the name of the template like this:
842      *                              core/menu (lib/templates/menu.mustache) or
843      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
844      * @param {Array} parentage - A list of requested partials in this render chain.
845      * @return {Promise} JQuery promise object resolved when all partials are in the cache.
846      */
847     Renderer.prototype.cachePartials = function(templateName, parentage) {
848         var searchKey = this.currentThemeName + '/' + templateName;
850         if (searchKey in cachePartialPromises) {
851             return cachePartialPromises[searchKey];
852         }
854         // This promise will not be resolved until all child partials are also resolved and ready.
855         // We create it here to allow us to check for recursive inclusion of templates.
856         // Keep track of the requested partials in this chain.
857         parentage = parentage || [searchKey];
859         cachePartialPromises[searchKey] = $.Deferred();
861         this.getTemplate(templateName)
862         .then(function(templateSource) {
863             var partials = this.scanForPartials(templateSource);
864             var uniquePartials = partials.filter(function(partialName) {
865                 // Check for recursion.
867                 if (parentage.indexOf(this.currentThemeName + '/' + partialName) >= 0) {
868                     // Ignore templates which include a parent template already requested in the current chain.
869                     return false;
870                 }
872                 // Ignore templates that include themselves.
873                 return partialName != templateName;
874             }.bind(this));
876             // Fetch any partial which has not already been fetched.
877             var fetchThemAll = uniquePartials.map(function(partialName) {
878                 parentage.push(this.currentThemeName + '/' + partialName);
879                 return this.cachePartials(partialName, parentage);
880             }.bind(this));
882             // Resolve the templateName promise when all of the children are resolved.
883             return $.when.apply($, fetchThemAll)
884             .then(function() {
885                 return cachePartialPromises[searchKey].resolve(templateSource);
886             });
887         }.bind(this))
888         .catch(cachePartialPromises[searchKey].reject);
890         return cachePartialPromises[searchKey];
891     };
893     /**
894      * Load a template and call doRender on it.
895      *
896      * @method render
897      * @private
898      * @param {string} templateName - should consist of the component and the name of the template like this:
899      *                              core/menu (lib/templates/menu.mustache) or
900      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
901      * @param {Object} context - Could be array, string or simple value for the context of the template.
902      * @param {string} themeName - Name of the current theme.
903      * @return {Promise} JQuery promise object resolved when the template has been rendered.
904      */
905     Renderer.prototype.render = function(templateName, context, themeName) {
906         if (typeof (themeName) === "undefined") {
907             // System context by default.
908             themeName = config.theme;
909         }
911         this.currentThemeName = themeName;
913         // Preload the module to do the icon rendering based on the theme iconsystem.
914         var modulename = config.iconsystemmodule;
916         var ready = $.Deferred();
917         require([modulename], function(System) {
918             var system = new System();
919             if (!(system instanceof IconSystem)) {
920                 ready.reject('Invalid icon system specified' + config.iconsystem);
921             } else {
922                 iconSystem = system;
923                 system.init().then(ready.resolve).catch(notification.exception);
924             }
925         });
927         return ready.then(function() {
928                 return this.cachePartials(templateName);
929             }.bind(this)).then(function(templateSource) {
930                 return this.doRender(templateSource, context, themeName);
931             }.bind(this));
932     };
934     /**
935      * Prepend some HTML to a node and trigger events and fire javascript.
936      *
937      * @method domPrepend
938      * @private
939      * @param {jQuery|String} element - Element or selector to prepend HTML to
940      * @param {String} html - HTML to prepend
941      * @param {String} js - Javascript to run after we prepend the html
942      */
943     var domPrepend = function(element, html, js) {
944         var node = $(element);
945         if (node.length) {
946             // Prepend the html.
947             node.prepend(html);
948             // Run any javascript associated with the new HTML.
949             runTemplateJS(js);
950             // Notify all filters about the new content.
951             event.notifyFilterContentUpdated(node);
952         }
953     };
955     /**
956      * Append some HTML to a node and trigger events and fire javascript.
957      *
958      * @method domAppend
959      * @private
960      * @param {jQuery|String} element - Element or selector to append HTML to
961      * @param {String} html - HTML to append
962      * @param {String} js - Javascript to run after we append the html
963      */
964     var domAppend = function(element, html, js) {
965         var node = $(element);
966         if (node.length) {
967             // Append the html.
968             node.append(html);
969             // Run any javascript associated with the new HTML.
970             runTemplateJS(js);
971             // Notify all filters about the new content.
972             event.notifyFilterContentUpdated(node);
973         }
974     };
976     return /** @alias module:core/templates */ {
977         // Public variables and functions.
978         /**
979          * Every call to render creates a new instance of the class and calls render on it. This
980          * means each render call has it's own class variables.
981          *
982          * @method render
983          * @private
984          * @param {string} templateName - should consist of the component and the name of the template like this:
985          *                              core/menu (lib/templates/menu.mustache) or
986          *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
987          * @param {Object} context - Could be array, string or simple value for the context of the template.
988          * @param {string} themeName - Name of the current theme.
989          * @return {Promise} JQuery promise object resolved when the template has been rendered.
990          */
991         render: function(templateName, context, themeName) {
992             var renderer = new Renderer();
993             return renderer.render(templateName, context, themeName);
994         },
996         /**
997          * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
998          * means each render call has it's own class variables.
999          *
1000          * @method renderIcon
1001          * @public
1002          * @param {string} key - Icon key.
1003          * @param {string} component - Icon component
1004          * @param {string} title - Icon title
1005          * @return {Promise} JQuery promise object resolved when the pix has been rendered.
1006          */
1007         renderPix: function(key, component, title) {
1008             var renderer = new Renderer();
1009             return renderer.renderIcon(key, component, title);
1010         },
1012         /**
1013          * Execute a block of JS returned from a template.
1014          * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
1015          *
1016          * @method runTemplateJS
1017          * @param {string} source - A block of javascript.
1018          */
1019         runTemplateJS: runTemplateJS,
1021         /**
1022          * Replace a node in the page with some new HTML and run the JS.
1023          *
1024          * @method replaceNodeContents
1025          * @param {JQuery} element - Element or selector to replace.
1026          * @param {String} newHTML - HTML to insert / replace.
1027          * @param {String} newJS - Javascript to run after the insertion.
1028          */
1029         replaceNodeContents: function(element, newHTML, newJS) {
1030             domReplace(element, newHTML, newJS, true);
1031         },
1033         /**
1034          * Insert a node in the page with some new HTML and run the JS.
1035          *
1036          * @method replaceNode
1037          * @param {JQuery} element - Element or selector to replace.
1038          * @param {String} newHTML - HTML to insert / replace.
1039          * @param {String} newJS - Javascript to run after the insertion.
1040          */
1041         replaceNode: function(element, newHTML, newJS) {
1042             domReplace(element, newHTML, newJS, false);
1043         },
1045         /**
1046          * Prepend some HTML to a node and trigger events and fire javascript.
1047          *
1048          * @method prependNodeContents
1049          * @param {jQuery|String} element - Element or selector to prepend HTML to
1050          * @param {String} html - HTML to prepend
1051          * @param {String} js - Javascript to run after we prepend the html
1052          */
1053         prependNodeContents: function(element, html, js) {
1054             domPrepend(element, html, js);
1055         },
1057         /**
1058          * Append some HTML to a node and trigger events and fire javascript.
1059          *
1060          * @method appendNodeContents
1061          * @param {jQuery|String} element - Element or selector to append HTML to
1062          * @param {String} html - HTML to append
1063          * @param {String} js - Javascript to run after we append the html
1064          */
1065         appendNodeContents: function(element, html, js) {
1066             domAppend(element, html, js);
1067         }
1068     };
1069 });