e2c2f0535184ba1d01467a69730afb098614bb33
[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(['core/mustache',
27          'jquery',
28          'core/ajax',
29          'core/str',
30          'core/notification',
31          'core/url',
32          'core/config',
33          'core/localstorage',
34          'core/icon_system',
35          'core/event',
36          'core/yui',
37          'core/log',
38          'core/truncate',
39          'core/user_date'
40        ],
41        function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate) {
43     // Module variables.
44     /** @var {Number} uniqInstances Count of times this constructor has been called. */
45     var uniqInstances = 0;
47     /** @var {String[]} templateCache - Cache of already loaded template strings */
48     var templateCache = {};
50     /** @var {Promise[]} templatePromises - Cache of already loaded template promises */
51     var templatePromises = {};
53     /** @var {Object} iconSystem - Object extending core/iconsystem */
54     var iconSystem = {};
56     /**
57      * Constructor
58      *
59      * Each call to templates.render gets it's own instance of this class.
60      */
61     var Renderer = function() {
62         this.requiredStrings = [];
63         this.requiredJS = [];
64         this.requiredDates = [];
65         this.currentThemeName = '';
66     };
67     // Class variables and functions.
69     /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
70     Renderer.prototype.requiredStrings = null;
72     /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */
73     Renderer.prototype.requiredDates = [];
75     /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
76     Renderer.prototype.requiredJS = null;
78     /** @var {String} themeName for the current render */
79     Renderer.prototype.currentThemeName = '';
81     /**
82      * Load a template from the cache or local storage or ajax request.
83      *
84      * @method getTemplate
85      * @private
86      * @param {string} templateName - should consist of the component and the name of the template like this:
87      *                              core/menu (lib/templates/menu.mustache) or
88      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
89      * @return {Promise} JQuery promise object resolved when the template has been fetched.
90      */
91     Renderer.prototype.getTemplate = function(templateName) {
92         var parts = templateName.split('/');
93         var component = parts.shift();
94         var name = parts.shift();
96         var searchKey = this.currentThemeName + '/' + templateName;
98         // First try request variables.
99         if (searchKey in templatePromises) {
100             return templatePromises[searchKey];
101         }
103         // Now try local storage.
104         var cached = storage.get('core_template/' + searchKey);
106         if (cached) {
107             templateCache[searchKey] = cached;
108             templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
109             return templatePromises[searchKey];
110         }
112         // Oh well - load via ajax.
113         var promises = ajax.call([{
114             methodname: 'core_output_load_template',
115             args: {
116                 component: component,
117                 template: name,
118                 themename: this.currentThemeName
119             }
120         }], true, false);
122         templatePromises[searchKey] = promises[0].then(
123             function(templateSource) {
124                 templateCache[searchKey] = templateSource;
125                 storage.set('core_template/' + searchKey, templateSource);
126                 return templateSource;
127             }
128         );
129         return templatePromises[searchKey];
130     };
132     /**
133      * Load a partial from the cache or ajax.
134      *
135      * @method partialHelper
136      * @private
137      * @param {string} name The partial name to load.
138      * @return {string}
139      */
140     Renderer.prototype.partialHelper = function(name) {
142         var searchKey = this.currentThemeName + '/' + name;
144         if (!(searchKey in templateCache)) {
145             notification.exception(new Error('Failed to pre-fetch the template: ' + name));
146         }
148         return templateCache[searchKey];
149     };
151     /**
152      * Render a single image icon.
153      *
154      * @method renderIcon
155      * @private
156      * @param {string} key The icon key.
157      * @param {string} component The component name.
158      * @param {string} title The icon title
159      * @return {Promise}
160      */
161     Renderer.prototype.renderIcon = function(key, component, title) {
162         // Preload the module to do the icon rendering based on the theme iconsystem.
163         var modulename = config.iconsystemmodule;
165         // RequireJS does not return a promise.
166         var ready = $.Deferred();
167         require([modulename], function(System) {
168             var system = new System();
169             if (!(system instanceof IconSystem)) {
170                 ready.reject('Invalid icon system specified' + config.iconsystemmodule);
171             } else {
172                 iconSystem = system;
173                 system.init().then(ready.resolve);
174             }
175         });
177         return ready.then(function(iconSystem) {
178             return this.getTemplate(iconSystem.getTemplateName());
179         }.bind(this)).then(function(template) {
180             return iconSystem.renderIcon(key, component, title, template);
181         });
182     };
184     /**
185      * Render image icons.
186      *
187      * @method pixHelper
188      * @private
189      * @param {object} context The mustache context
190      * @param {string} sectionText The text to parse arguments from.
191      * @param {function} helper Used to render the alt attribute of the text.
192      * @return {string}
193      */
194     Renderer.prototype.pixHelper = function(context, sectionText, helper) {
195         var parts = sectionText.split(',');
196         var key = '';
197         var component = '';
198         var text = '';
200         if (parts.length > 0) {
201             key = helper(parts.shift().trim(), context);
202         }
203         if (parts.length > 0) {
204             component = helper(parts.shift().trim(), context);
205         }
206         if (parts.length > 0) {
207             text = helper(parts.join(',').trim(), context);
208         }
210         var templateName = iconSystem.getTemplateName();
212         var searchKey = this.currentThemeName + '/' + templateName;
213         var template = templateCache[searchKey];
215         // The key might have been escaped by the JS Mustache engine which
216         // converts forward slashes to HTML entities. Let us undo that here.
217         key = key.replace(/&#x2F;/gi, '/');
219         return iconSystem.renderIcon(key, component, text, template);
220     };
222     /**
223      * Render blocks of javascript and save them in an array.
224      *
225      * @method jsHelper
226      * @private
227      * @param {object} context The current mustache context.
228      * @param {string} sectionText The text to save as a js block.
229      * @param {function} helper Used to render the block.
230      * @return {string}
231      */
232     Renderer.prototype.jsHelper = function(context, sectionText, helper) {
233         this.requiredJS.push(helper(sectionText, context));
234         return '';
235     };
237     /**
238      * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
239      * into a get_string call.
240      *
241      * @method stringHelper
242      * @private
243      * @param {object} context The current mustache context.
244      * @param {string} sectionText The text to parse the arguments from.
245      * @param {function} helper Used to render subsections of the text.
246      * @return {string}
247      */
248     Renderer.prototype.stringHelper = function(context, sectionText, helper) {
249         var parts = sectionText.split(',');
250         var key = '';
251         var component = '';
252         var param = '';
253         if (parts.length > 0) {
254             key = parts.shift().trim();
255         }
256         if (parts.length > 0) {
257             component = parts.shift().trim();
258         }
259         if (parts.length > 0) {
260             param = parts.join(',').trim();
261         }
263         if (param !== '') {
264             // Allow variable expansion in the param part only.
265             param = helper(param, context);
266         }
267         // Allow json formatted $a arguments.
268         if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
269             param = JSON.parse(param);
270         }
272         var index = this.requiredStrings.length;
273         this.requiredStrings.push({key: key, component: component, param: param});
275         // The placeholder must not use {{}} as those can be misinterpreted by the engine.
276         return '[[_s' + index + ']]';
277     };
279     /**
280      * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
281      *
282      * @method quoteHelper
283      * @private
284      * @param {object} context The current mustache context.
285      * @param {string} sectionText The text to parse the arguments from.
286      * @param {function} helper Used to render subsections of the text.
287      * @return {string}
288      */
289     Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
290         var content = helper(sectionText.trim(), context);
292         // Escape the {{ and the ".
293         // This involves wrapping {{, and }} in change delimeter tags.
294         content = content
295             .replace('"', '\\"')
296             .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
297             ;
298         return '"' + content + '"';
299     };
301     /**
302      * Shorten text helper to truncate text and append a trailing ellipsis.
303      *
304      * @method shortenTextHelper
305      * @private
306      * @param {object} context The current mustache context.
307      * @param {string} sectionText The text to parse the arguments from.
308      * @param {function} helper Used to render subsections of the text.
309      * @return {string}
310      */
311     Renderer.prototype.shortenTextHelper = function(context, sectionText, helper) {
312         // Non-greedy split on comma to grab section text into the length and
313         // text parts.
314         var regex = /(.*?),(.*)/;
315         var parts = sectionText.match(regex);
316         // The length is the part matched in the first set of parethesis.
317         var length = parts[1].trim();
318         // The length is the part matched in the second set of parethesis.
319         var text = parts[2].trim();
320         var content = helper(text, context);
321         return Truncate.truncate(content, {
322             length: length,
323             words: true,
324             ellipsis: '...'
325         });
326     };
328     /**
329      * User date helper to render user dates from timestamps.
330      *
331      * @method userDateHelper
332      * @private
333      * @param {object} context The current mustache context.
334      * @param {string} sectionText The text to parse the arguments from.
335      * @param {function} helper Used to render subsections of the text.
336      * @return {string}
337      */
338     Renderer.prototype.userDateHelper = function(context, sectionText, helper) {
339         // Non-greedy split on comma to grab the timestamp and format.
340         var regex = /(.*?),(.*)/;
341         var parts = sectionText.match(regex);
342         var timestamp = helper(parts[1].trim(), context);
343         var format = helper(parts[2].trim(), context);
344         var index = this.requiredDates.length;
346         this.requiredDates.push({
347             timestamp: timestamp,
348             format: format
349         });
351         return '[[_t_' + index + ']]';
352     };
354     /**
355      * Add some common helper functions to all context objects passed to templates.
356      * These helpers match exactly the helpers available in php.
357      *
358      * @method addHelpers
359      * @private
360      * @param {Object} context Simple types used as the context for the template.
361      * @param {String} themeName We set this multiple times, because there are async calls.
362      */
363     Renderer.prototype.addHelpers = function(context, themeName) {
364         this.currentThemeName = themeName;
365         this.requiredStrings = [];
366         this.requiredJS = [];
367         context.uniqid = (uniqInstances++);
368         context.str = function() {
369           return this.stringHelper.bind(this, context);
370         }.bind(this);
371         context.pix = function() {
372           return this.pixHelper.bind(this, context);
373         }.bind(this);
374         context.js = function() {
375           return this.jsHelper.bind(this, context);
376         }.bind(this);
377         context.quote = function() {
378           return this.quoteHelper.bind(this, context);
379         }.bind(this);
380         context.shortentext = function() {
381           return this.shortenTextHelper.bind(this, context);
382         }.bind(this);
383         context.userdate = function() {
384           return this.userDateHelper.bind(this, context);
385         }.bind(this);
386         context.globals = {config: config};
387         context.currentTheme = themeName;
388     };
390     /**
391      * Get all the JS blocks from the last rendered template.
392      *
393      * @method getJS
394      * @private
395      * @return {string}
396      */
397     Renderer.prototype.getJS = function() {
398         var js = '';
399         if (this.requiredJS.length > 0) {
400             js = this.requiredJS.join(";\n");
401         }
403         return js;
404     };
406     /**
407      * Treat strings in content.
408      *
409      * The purpose of this method is to replace the placeholders found in a string
410      * with the their respective translated strings.
411      *
412      * Previously we were relying on String.replace() but the complexity increased with
413      * the numbers of strings to replace. Now we manually walk the string and stop at each
414      * placeholder we find, only then we replace it. Most of the time we will
415      * replace all the placeholders in a single run, at times we will need a few
416      * more runs when placeholders are replaced with strings that contain placeholders
417      * themselves.
418      *
419      * @param {String} content The content in which string placeholders are to be found.
420      * @param {Array} strings The strings to replace with.
421      * @return {String} The treated content.
422      */
423     Renderer.prototype.treatStringsInContent = function(content, strings) {
424         var pattern = /\[\[_s\d+\]\]/,
425             treated,
426             index,
427             strIndex,
428             walker,
429             char,
430             strFinal;
432         do {
433             treated = '';
434             index = content.search(pattern);
435             while (index > -1) {
437                 // Copy the part prior to the placeholder to the treated string.
438                 treated += content.substring(0, index);
439                 content = content.substr(index);
440                 strIndex = '';
441                 walker = 4;  // 4 is the length of '[[_s'.
443                 // Walk the characters to manually extract the index of the string from the placeholder.
444                 char = content.substr(walker, 1);
445                 do {
446                     strIndex += char;
447                     walker++;
448                     char = content.substr(walker, 1);
449                 } while (char != ']');
451                 // Get the string, add it to the treated result, and remove the placeholder from the content to treat.
452                 strFinal = strings[parseInt(strIndex, 10)];
453                 if (typeof strFinal === 'undefined') {
454                     Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
455                     strFinal = '';
456                 }
457                 treated += strFinal;
458                 content = content.substr(6 + strIndex.length);  // 6 is the length of the placeholder without the index: '[[_s]]'.
460                 // Find the next placeholder.
461                 index = content.search(pattern);
462             }
464             // The content becomes the treated part with the rest of the content.
465             content = treated + content;
467             // Check if we need to walk the content again, in case strings contained placeholders.
468             index = content.search(pattern);
470         } while (index > -1);
472         return content;
473     };
475     /**
476      * Treat strings in content.
477      *
478      * The purpose of this method is to replace the date placeholders found in the
479      * content with the their respective translated dates.
480      *
481      * @param {String} content The content in which string placeholders are to be found.
482      * @param {Array} strings The strings to replace with.
483      * @return {String} The treated content.
484      */
485     Renderer.prototype.treatDatesInContent = function(content, dates) {
486         dates.forEach(function(date, index) {
487             var key = '\\[\\[_t_' + index + '\\]\\]';
488             var re = new RegExp(key, 'g');
489             content = content.replace(re, date);
490         });
492         return content;
493     };
495     /**
496      * Render a template and then call the callback with the result.
497      *
498      * @method doRender
499      * @private
500      * @param {string} templateSource The mustache template to render.
501      * @param {Object} context Simple types used as the context for the template.
502      * @param {String} themeName Name of the current theme.
503      * @return {Promise} object
504      */
505     Renderer.prototype.doRender = function(templateSource, context, themeName) {
506         this.currentThemeName = themeName;
507         var iconTemplate = iconSystem.getTemplateName();
509         return this.getTemplate(iconTemplate).then(function() {
510             this.addHelpers(context, themeName);
511             var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
512             return $.Deferred().resolve(result.trim(), this.getJS()).promise();
513         }.bind(this))
514         .then(function(html, js) {
515             if (this.requiredStrings.length > 0) {
516                 return str.get_strings(this.requiredStrings).then(function(strings) {
518                     // Make sure string substitutions are done for the userdate
519                     // values as well.
520                     this.requiredDates = this.requiredDates.map(function(date) {
521                         return {
522                             timestamp: this.treatStringsInContent(date.timestamp, strings),
523                             format: this.treatStringsInContent(date.format, strings)
524                         };
525                     }.bind(this));
527                     // Why do we not do another call the render here?
528                     //
529                     // Because that would expose DOS holes. E.g.
530                     // I create an assignment called "{{fish" which
531                     // would get inserted in the template in the first pass
532                     // and cause the template to die on the second pass (unbalanced).
533                     html = this.treatStringsInContent(html, strings);
534                     js = this.treatStringsInContent(js, strings);
535                     return $.Deferred().resolve(html, js).promise();
536                 }.bind(this));
537             }
539             return $.Deferred().resolve(html, js).promise();
540         }.bind(this))
541         .then(function(html, js) {
542             // This has to happen after the strings replacement because you can
543             // use the string helper in content for the user date helper.
544             if (this.requiredDates.length > 0) {
545                 return UserDate.get(this.requiredDates).then(function(dates) {
546                     html = this.treatDatesInContent(html, dates);
547                     js = this.treatDatesInContent(js, dates);
548                     return $.Deferred().resolve(html, js).promise();
549                 }.bind(this));
550             }
552             return $.Deferred().resolve(html, js).promise();
553         }.bind(this));
554     };
556     /**
557      * Execute a block of JS returned from a template.
558      * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
559      *
560      * @method runTemplateJS
561      * @param {string} source - A block of javascript.
562      */
563     var runTemplateJS = function(source) {
564         if (source.trim() !== '') {
565             var newscript = $('<script>').attr('type', 'text/javascript').html(source);
566             $('head').append(newscript);
567         }
568     };
570     /**
571      * Do some DOM replacement and trigger correct events and fire javascript.
572      *
573      * @method domReplace
574      * @private
575      * @param {JQuery} element - Element or selector to replace.
576      * @param {String} newHTML - HTML to insert / replace.
577      * @param {String} newJS - Javascript to run after the insertion.
578      * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
579      */
580     var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
581         var replaceNode = $(element);
582         if (replaceNode.length) {
583             // First create the dom nodes so we have a reference to them.
584             var newNodes = $(newHTML);
585             var yuiNodes = null;
586             // Do the replacement in the page.
587             if (replaceChildNodes) {
588                 // Cleanup any YUI event listeners attached to any of these nodes.
589                 yuiNodes = new Y.NodeList(replaceNode.children().get());
590                 yuiNodes.destroy(true);
592                 // JQuery will cleanup after itself.
593                 replaceNode.empty();
594                 replaceNode.append(newNodes);
595             } else {
596                 // Cleanup any YUI event listeners attached to any of these nodes.
597                 yuiNodes = new Y.NodeList(replaceNode.get());
598                 yuiNodes.destroy(true);
600                 // JQuery will cleanup after itself.
601                 replaceNode.replaceWith(newNodes);
602             }
603             // Run any javascript associated with the new HTML.
604             runTemplateJS(newJS);
605             // Notify all filters about the new content.
606             event.notifyFilterContentUpdated(newNodes);
607         }
608     };
610     /**
611      * Scan a template source for partial tags and return a list of the found partials.
612      *
613      * @method scanForPartials
614      * @private
615      * @param {string} templateSource - source template to scan.
616      * @return {Array} List of partials.
617      */
618     Renderer.prototype.scanForPartials = function(templateSource) {
619         var tokens = mustache.parse(templateSource),
620             partials = [];
622         var findPartial = function(tokens, partials) {
623             var i, token;
624             for (i = 0; i < tokens.length; i++) {
625                 token = tokens[i];
626                 if (token[0] == '>' || token[0] == '<') {
627                     partials.push(token[1]);
628                 }
629                 if (token.length > 4) {
630                     findPartial(token[4], partials);
631                 }
632             }
633         };
635         findPartial(tokens, partials);
637         return partials;
638     };
640     /**
641      * Load a template and scan it for partials. Recursively fetch the partials.
642      *
643      * @method cachePartials
644      * @private
645      * @param {string} templateName - should consist of the component and the name of the template like this:
646      *                              core/menu (lib/templates/menu.mustache) or
647      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
648      * @return {Promise} JQuery promise object resolved when all partials are in the cache.
649      */
650     Renderer.prototype.cachePartials = function(templateName) {
651         return this.getTemplate(templateName).then(function(templateSource) {
652             var i;
653             var partials = this.scanForPartials(templateSource);
654             var fetchThemAll = [];
656             for (i = 0; i < partials.length; i++) {
657                 var searchKey = this.currentThemeName + '/' + partials[i];
658                 if (searchKey in templatePromises) {
659                     fetchThemAll.push(templatePromises[searchKey]);
660                 } else {
661                     fetchThemAll.push(this.cachePartials(partials[i]));
662                 }
663             }
665             return $.when.apply($, fetchThemAll).then(function() {
666                 return templateSource;
667             });
668         }.bind(this));
669     };
671     /**
672      * Load a template and call doRender on it.
673      *
674      * @method render
675      * @private
676      * @param {string} templateName - should consist of the component and the name of the template like this:
677      *                              core/menu (lib/templates/menu.mustache) or
678      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
679      * @param {Object} context - Could be array, string or simple value for the context of the template.
680      * @param {string} themeName - Name of the current theme.
681      * @return {Promise} JQuery promise object resolved when the template has been rendered.
682      */
683     Renderer.prototype.render = function(templateName, context, themeName) {
684         if (typeof (themeName) === "undefined") {
685             // System context by default.
686             themeName = config.theme;
687         }
689         this.currentThemeName = themeName;
691         // Preload the module to do the icon rendering based on the theme iconsystem.
692         var modulename = config.iconsystemmodule;
694         var ready = $.Deferred();
695         require([modulename], function(System) {
696             var system = new System();
697             if (!(system instanceof IconSystem)) {
698                 ready.reject('Invalid icon system specified' + config.iconsystem);
699             } else {
700                 iconSystem = system;
701                 system.init().then(ready.resolve);
702             }
703         });
705         return ready.then(function() {
706                 return this.cachePartials(templateName);
707             }.bind(this)).then(function(templateSource) {
708                 return this.doRender(templateSource, context, themeName);
709             }.bind(this));
710     };
712     /**
713      * Prepend some HTML to a node and trigger events and fire javascript.
714      *
715      * @method domPrepend
716      * @private
717      * @param {jQuery|String} element - Element or selector to prepend HTML to
718      * @param {String} html - HTML to prepend
719      * @param {String} js - Javascript to run after we prepend the html
720      */
721     var domPrepend = function(element, html, js) {
722         var node = $(element);
723         if (node.length) {
724             // Prepend the html.
725             node.prepend(html);
726             // Run any javascript associated with the new HTML.
727             runTemplateJS(js);
728             // Notify all filters about the new content.
729             event.notifyFilterContentUpdated(node);
730         }
731     };
733     /**
734      * Append some HTML to a node and trigger events and fire javascript.
735      *
736      * @method domAppend
737      * @private
738      * @param {jQuery|String} element - Element or selector to append HTML to
739      * @param {String} html - HTML to append
740      * @param {String} js - Javascript to run after we append the html
741      */
742     var domAppend = function(element, html, js) {
743         var node = $(element);
744         if (node.length) {
745             // Append the html.
746             node.append(html);
747             // Run any javascript associated with the new HTML.
748             runTemplateJS(js);
749             // Notify all filters about the new content.
750             event.notifyFilterContentUpdated(node);
751         }
752     };
754     return /** @alias module:core/templates */ {
755         // Public variables and functions.
756         /**
757          * Every call to render creates a new instance of the class and calls render on it. This
758          * means each render call has it's own class variables.
759          *
760          * @method render
761          * @private
762          * @param {string} templateName - should consist of the component and the name of the template like this:
763          *                              core/menu (lib/templates/menu.mustache) or
764          *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
765          * @param {Object} context - Could be array, string or simple value for the context of the template.
766          * @param {string} themeName - Name of the current theme.
767          * @return {Promise} JQuery promise object resolved when the template has been rendered.
768          */
769         render: function(templateName, context, themeName) {
770             var renderer = new Renderer();
771             return renderer.render(templateName, context, themeName);
772         },
774         /**
775          * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
776          * means each render call has it's own class variables.
777          *
778          * @method renderIcon
779          * @public
780          * @param {string} key - Icon key.
781          * @param {string} component - Icon component
782          * @param {string} title - Icon title
783          * @return {Promise} JQuery promise object resolved when the pix has been rendered.
784          */
785         renderPix: function(key, component, title) {
786             var renderer = new Renderer();
787             return renderer.renderIcon(key, component, title);
788         },
790         /**
791          * Execute a block of JS returned from a template.
792          * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
793          *
794          * @method runTemplateJS
795          * @param {string} source - A block of javascript.
796          */
797         runTemplateJS: runTemplateJS,
799         /**
800          * Replace a node in the page with some new HTML and run the JS.
801          *
802          * @method replaceNodeContents
803          * @param {JQuery} element - Element or selector to replace.
804          * @param {String} newHTML - HTML to insert / replace.
805          * @param {String} newJS - Javascript to run after the insertion.
806          */
807         replaceNodeContents: function(element, newHTML, newJS) {
808             domReplace(element, newHTML, newJS, true);
809         },
811         /**
812          * Insert a node in the page with some new HTML and run the JS.
813          *
814          * @method replaceNode
815          * @param {JQuery} element - Element or selector to replace.
816          * @param {String} newHTML - HTML to insert / replace.
817          * @param {String} newJS - Javascript to run after the insertion.
818          */
819         replaceNode: function(element, newHTML, newJS) {
820             domReplace(element, newHTML, newJS, false);
821         },
823         /**
824          * Prepend some HTML to a node and trigger events and fire javascript.
825          *
826          * @method prependNodeContents
827          * @param {jQuery|String} element - Element or selector to prepend HTML to
828          * @param {String} html - HTML to prepend
829          * @param {String} js - Javascript to run after we prepend the html
830          */
831         prependNodeContents: function(element, html, js) {
832             domPrepend(element, html, js);
833         },
835         /**
836          * Append some HTML to a node and trigger events and fire javascript.
837          *
838          * @method appendNodeContents
839          * @param {jQuery|String} element - Element or selector to append HTML to
840          * @param {String} html - HTML to append
841          * @param {String} js - Javascript to run after we append the html
842          */
843         appendNodeContents: function(element, html, js) {
844             domAppend(element, html, js);
845         }
846     };
847 });