{"version":3,"file":"renderer.min.js","sources":["https:\/\/courses.fincert.org\/lib\/amd\/src\/local\/templates\/renderer.js"],"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\nimport * as Log from 'core\/log';\nimport * as Truncate from 'core\/truncate';\nimport * as UserDate from 'core\/user_date';\nimport Pending from 'core\/pending';\nimport {getStrings} from 'core\/str';\nimport IconSystem from 'core\/icon_system';\nimport config from 'core\/config';\nimport mustache from 'core\/mustache';\nimport Loader from '.\/loader';\nimport {getNormalisedComponent} from 'core\/utils';\n\n\/** @var {string} The placeholder character used for standard strings (unclean) *\/\nconst placeholderString = 's';\n\n\/** @var {string} The placeholder character used for cleaned strings *\/\nconst placeholderCleanedString = 'c';\n\n\/**\n * Template Renderer Class.\n *\n * Note: This class is not intended to be instantiated directly. Instead, use the core\/templates module.\n *\n * @module core\/local\/templates\/renderer\n * @copyright 2023 Andrew Lyons \n * @license http:\/\/www.gnu.org\/copyleft\/gpl.html GNU GPL v3 or later\n * @since 4.3\n *\/\nexport default class Renderer {\n \/** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template *\/\n requiredStrings = null;\n\n \/** @var {object[]} requiredDates - Collection of dates found during the rendering of one template *\/\n requiredDates = [];\n\n \/** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template *\/\n requiredJS = null;\n\n \/** @var {String} themeName for the current render *\/\n currentThemeName = '';\n\n \/** @var {Number} uniqInstances Count of times this constructor has been called. *\/\n static uniqInstances = 0;\n\n \/** @var {Object[]} loadTemplateBuffer - List of templates to be loaded *\/\n static loadTemplateBuffer = [];\n\n \/** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded *\/\n static isLoadingTemplates = false;\n\n \/** @var {Object} iconSystem - Object extending core\/iconsystem *\/\n iconSystem = null;\n\n \/** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers *\/\n static disallowedNestedHelpers = [\n 'js',\n ];\n\n \/** @var {String[]} templateCache - Cache of already loaded template strings *\/\n static templateCache = {};\n\n \/**\n * Cache of already loaded template promises.\n *\n * @type {Promise[]}\n * @static\n * @private\n *\/\n static templatePromises = {};\n\n \/**\n * The loader used to fetch templates.\n * @type {Loader}\n * @static\n * @private\n *\/\n static loader = Loader;\n\n \/**\n * Constructor\n *\n * Each call to templates.render gets it's own instance of this class.\n *\/\n constructor() {\n this.requiredStrings = [];\n this.requiredJS = [];\n this.requiredDates = [];\n this.currentThemeName = '';\n }\n\n \/**\n * Set the template loader to use for all Template renderers.\n *\n * @param {Loader} loader\n *\/\n static setLoader(loader) {\n this.loader = loader;\n }\n\n \/**\n * Get the Loader used to fetch templates.\n *\n * @returns {Loader}\n *\/\n static getLoader() {\n return this.loader;\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 * @returns {Promise}\n *\/\n async renderIcon(key, component, title) {\n \/\/ Preload the module to do the icon rendering based on the theme iconsystem.\n component = getNormalisedComponent(component);\n\n await this.setupIconSystem();\n const template = await Renderer.getLoader().getTemplate(\n this.iconSystem.getTemplateName(),\n this.currentThemeName,\n );\n\n return this.iconSystem.renderIcon(\n key,\n component,\n title,\n template\n );\n }\n\n \/**\n * Helper to set up the icon system.\n *\/\n async setupIconSystem() {\n if (!this.iconSystem) {\n this.iconSystem = await IconSystem.instance();\n }\n\n return this.iconSystem;\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 * @returns {string}\n *\/\n pixHelper(context, sectionText, helper) {\n const parts = sectionText.split(',');\n let key = '';\n let component = '';\n let 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 \/\/ Note: We cannot use Promises in Mustache helpers.\n \/\/ We must fetch straight from the Loader cache.\n \/\/ The Loader cache is statically defined on the Loader class and should be used by all children.\n const Loader = Renderer.getLoader();\n const templateName = this.iconSystem.getTemplateName();\n const searchKey = Loader.getSearchKey(this.currentThemeName, templateName);\n const template = Loader.getTemplateFromCache(searchKey);\n\n component = getNormalisedComponent(component);\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 this.iconSystem.renderIcon(\n key,\n component,\n text,\n template\n );\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 * @returns {string}\n *\/\n jsHelper(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 * @returns {string}\n *\/\n stringHelper(context, sectionText, helper) {\n \/\/ A string instruction is in the format:\n \/\/ key, component, params.\n\n let parts = sectionText.split(',');\n\n const key = parts.length > 0 ? parts.shift().trim() : '';\n const component = parts.length > 0 ? getNormalisedComponent(parts.shift().trim()) : '';\n let param = parts.length > 0 ? parts.join(',').trim() : '';\n\n if (param !== '') {\n \/\/ Allow variable expansion in the param part only.\n param = helper(param, context);\n }\n\n if (param.match(\/^{\\s*\"\/gm)) {\n \/\/ If it can't be parsed then the string is not a JSON format.\n try {\n const parsedParam = JSON.parse(param);\n \/\/ Handle non-exception-throwing cases, e.g. null, integer, boolean.\n if (parsedParam && typeof parsedParam === \"object\") {\n param = parsedParam;\n }\n } catch (err) {\n \/\/ This was probably not JSON.\n \/\/ Keep the error message visible but do not promote it because it may not be an error.\n window.console.warn(err.message);\n }\n }\n\n const index = this.requiredStrings.length;\n this.requiredStrings.push({\n key,\n component,\n param,\n });\n\n \/\/ The placeholder must not use {{}} as those can be misinterpreted by the engine.\n return `[[_s${index}]]`;\n }\n\n \/**\n * String helper to render {{#cleanstr}}abd component { a : 'fish'}{{\/cleanstr}}\n * into a get_string following by an HTML escape.\n *\n * @method cleanStringHelper\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 * @returns {string}\n *\/\n cleanStringHelper(context, sectionText, helper) {\n \/\/ We're going to use [[_cx]] format for clean strings, where x is a number.\n \/\/ Hence, replacing 's' with 'c' in the placeholder that stringHelper returns.\n return this\n .stringHelper(context, sectionText, helper)\n .replace(placeholderString, placeholderCleanedString);\n }\n\n \/**\n * Quote helper used to wrap content in quotes, and escape all special JSON characters 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 * @returns {string}\n *\/\n quoteHelper(context, sectionText, helper) {\n let content = helper(sectionText.trim(), context);\n\n \/\/ Escape the {{ and JSON encode.\n \/\/ This involves wrapping {{, and }} in change delimeter tags.\n content = JSON.stringify(content);\n content = content.replace(\/([{}]{2,3})\/g, '{{=<% %>=}}$1<%={{ }}=%>');\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 * @returns {string}\n *\/\n shortenTextHelper(context, sectionText, helper) {\n \/\/ Non-greedy split on comma to grab section text into the length and\n \/\/ text parts.\n const parts = sectionText.match(\/(.*?),(.*)\/);\n\n \/\/ The length is the part matched in the first set of parethesis.\n const length = parts[1].trim();\n \/\/ The length is the part matched in the second set of parethesis.\n const text = parts[2].trim();\n const content = helper(text, context);\n return Truncate.truncate(content, {\n 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 * @returns {string}\n *\/\n userDateHelper(context, sectionText, helper) {\n \/\/ Non-greedy split on comma to grab the timestamp and format.\n const parts = sectionText.match(\/(.*?),(.*)\/);\n\n const timestamp = helper(parts[1].trim(), context);\n const format = helper(parts[2].trim(), context);\n const 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 * Return a helper function to be added to the context for rendering the a\n * template.\n *\n * This will parse the provided text before giving it to the helper function\n * in order to remove any disallowed nested helpers to prevent one helper\n * from calling another.\n *\n * In particular to prevent the JS helper from being called from within another\n * helper because it can lead to security issues when the JS portion is user\n * provided.\n *\n * @param {function} helperFunction The helper function to add\n * @param {object} context The template context for the helper function\n * @returns {Function} To be set in the context\n *\/\n addHelperFunction(helperFunction, context) {\n return function() {\n return function(sectionText, helper) {\n \/\/ Override the disallowed helpers in the template context with\n \/\/ a function that returns an empty string for use when executing\n \/\/ other helpers. This is to prevent these helpers from being\n \/\/ executed as part of the rendering of another helper in order to\n \/\/ prevent any potential security issues.\n const originalHelpers = Renderer.disallowedNestedHelpers.reduce((carry, name) => {\n if (context.hasOwnProperty(name)) {\n carry[name] = context[name];\n }\n\n return carry;\n }, {});\n\n Renderer.disallowedNestedHelpers.forEach((helperName) => {\n context[helperName] = () => '';\n });\n\n \/\/ Execute the helper with the modified context that doesn't include\n \/\/ the disallowed nested helpers. This prevents the disallowed\n \/\/ helpers from being called from within other helpers.\n const result = helperFunction.apply(this, [context, sectionText, helper]);\n\n \/\/ Restore the original helper implementation in the context so that\n \/\/ any further rendering has access to them again.\n for (const name in originalHelpers) {\n context[name] = originalHelpers[name];\n }\n\n return result;\n }.bind(this);\n }.bind(this);\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 addHelpers(context, themeName) {\n this.currentThemeName = themeName;\n this.requiredStrings = [];\n this.requiredJS = [];\n context.uniqid = (Renderer.uniqInstances++);\n\n \/\/ Please note that these helpers _must_ not return a Promise.\n context.str = this.addHelperFunction(this.stringHelper, context);\n context.cleanstr = this.addHelperFunction(this.cleanStringHelper, context);\n context.pix = this.addHelperFunction(this.pixHelper, context);\n context.js = this.addHelperFunction(this.jsHelper, context);\n context.quote = this.addHelperFunction(this.quoteHelper, context);\n context.shortentext = this.addHelperFunction(this.shortenTextHelper, context);\n context.userdate = this.addHelperFunction(this.userDateHelper, context);\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 * @returns {string}\n *\/\n getJS() {\n return this.requiredJS.join(\";\\n\");\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 {Map} stringMap The strings to replace with.\n * @returns {String} The treated content.\n *\/\n treatStringsInContent(content, stringMap) {\n \/\/ Placeholders are in the for [[_sX]] or [[_cX]] where X is the string index.\n const stringPattern = \/(?\\[\\[_(?[cs])(?\\d+)\\]\\])\/g;\n\n \/\/ A helpre to fetch the string for a given placeholder.\n const getUpdatedString = ({placeholder, stringType, stringIndex}) => {\n if (stringMap.has(placeholder)) {\n return stringMap.get(placeholder);\n }\n\n if (stringType === placeholderCleanedString) {\n \/\/ Attempt to find the unclean string and clean it. Store it for later use.\n const uncleanString = stringMap.get(`[[_s${stringIndex}]]`);\n if (uncleanString) {\n stringMap.set(placeholder, mustache.escape(uncleanString));\n return stringMap.get(placeholder);\n }\n }\n\n Log.debug(`Could not find string for pattern ${placeholder}`);\n return '';\n };\n\n \/\/ Find all placeholders in the content and replace them with their respective strings.\n let match;\n while ((match = stringPattern.exec(content)) !== null) {\n let updatedContent = content.slice(0, match.index);\n updatedContent += getUpdatedString(match.groups);\n updatedContent += content.slice(match.index + match.groups.placeholder.length);\n\n content = updatedContent;\n }\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} dates The dates to replace with.\n * @returns {String} The treated content.\n *\/\n treatDatesInContent(content, dates) {\n dates.forEach((date, index) => {\n content = content.replace(\n new RegExp(`\\\\[\\\\[_t_${index}\\\\]\\\\]`, 'g'),\n date,\n );\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|Promise} templateSourcePromise 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 * @returns {Promise