Файловый менеджер - Редактировать - /home/harasnat/www/learning/lib/amd/src/local/templates/renderer.js
Назад
// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. import * as Log from 'core/log'; import * as Truncate from 'core/truncate'; import * as UserDate from 'core/user_date'; import Pending from 'core/pending'; import {getStrings} from 'core/str'; import IconSystem from 'core/icon_system'; import config from 'core/config'; import mustache from 'core/mustache'; import Loader from './loader'; import {getNormalisedComponent} from 'core/utils'; /** @var {string} The placeholder character used for standard strings (unclean) */ const placeholderString = 's'; /** @var {string} The placeholder character used for cleaned strings */ const placeholderCleanedString = 'c'; /** * Template Renderer Class. * * Note: This class is not intended to be instantiated directly. Instead, use the core/templates module. * * @module core/local/templates/renderer * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.3 */ export default class Renderer { /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */ requiredStrings = null; /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */ requiredDates = []; /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */ requiredJS = null; /** @var {String} themeName for the current render */ currentThemeName = ''; /** @var {Number} uniqInstances Count of times this constructor has been called. */ static uniqInstances = 0; /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */ static loadTemplateBuffer = []; /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */ static isLoadingTemplates = false; /** @var {Object} iconSystem - Object extending core/iconsystem */ iconSystem = null; /** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers */ static disallowedNestedHelpers = [ 'js', ]; /** @var {String[]} templateCache - Cache of already loaded template strings */ static templateCache = {}; /** * Cache of already loaded template promises. * * @type {Promise[]} * @static * @private */ static templatePromises = {}; /** * The loader used to fetch templates. * @type {Loader} * @static * @private */ static loader = Loader; /** * Constructor * * Each call to templates.render gets it's own instance of this class. */ constructor() { this.requiredStrings = []; this.requiredJS = []; this.requiredDates = []; this.currentThemeName = ''; } /** * Set the template loader to use for all Template renderers. * * @param {Loader} loader */ static setLoader(loader) { this.loader = loader; } /** * Get the Loader used to fetch templates. * * @returns {Loader} */ static getLoader() { return this.loader; } /** * Render a single image icon. * * @method renderIcon * @private * @param {string} key The icon key. * @param {string} component The component name. * @param {string} title The icon title * @returns {Promise} */ async renderIcon(key, component, title) { // Preload the module to do the icon rendering based on the theme iconsystem. component = getNormalisedComponent(component); await this.setupIconSystem(); const template = await Renderer.getLoader().getTemplate( this.iconSystem.getTemplateName(), this.currentThemeName, ); return this.iconSystem.renderIcon( key, component, title, template ); } /** * Helper to set up the icon system. */ async setupIconSystem() { if (!this.iconSystem) { this.iconSystem = await IconSystem.instance(); } return this.iconSystem; } /** * Render image icons. * * @method pixHelper * @private * @param {object} context The mustache context * @param {string} sectionText The text to parse arguments from. * @param {function} helper Used to render the alt attribute of the text. * @returns {string} */ pixHelper(context, sectionText, helper) { const parts = sectionText.split(','); let key = ''; let component = ''; let text = ''; if (parts.length > 0) { key = helper(parts.shift().trim(), context); } if (parts.length > 0) { component = helper(parts.shift().trim(), context); } if (parts.length > 0) { text = helper(parts.join(',').trim(), context); } // Note: We cannot use Promises in Mustache helpers. // We must fetch straight from the Loader cache. // The Loader cache is statically defined on the Loader class and should be used by all children. const Loader = Renderer.getLoader(); const templateName = this.iconSystem.getTemplateName(); const searchKey = Loader.getSearchKey(this.currentThemeName, templateName); const template = Loader.getTemplateFromCache(searchKey); component = getNormalisedComponent(component); // The key might have been escaped by the JS Mustache engine which // converts forward slashes to HTML entities. Let us undo that here. key = key.replace(///gi, '/'); return this.iconSystem.renderIcon( key, component, text, template ); } /** * Render blocks of javascript and save them in an array. * * @method jsHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to save as a js block. * @param {function} helper Used to render the block. * @returns {string} */ jsHelper(context, sectionText, helper) { this.requiredJS.push(helper(sectionText, context)); return ''; } /** * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}} * into a get_string call. * * @method stringHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to parse the arguments from. * @param {function} helper Used to render subsections of the text. * @returns {string} */ stringHelper(context, sectionText, helper) { // A string instruction is in the format: // key, component, params. let parts = sectionText.split(','); const key = parts.length > 0 ? parts.shift().trim() : ''; const component = parts.length > 0 ? getNormalisedComponent(parts.shift().trim()) : ''; let param = parts.length > 0 ? parts.join(',').trim() : ''; if (param !== '') { // Allow variable expansion in the param part only. param = helper(param, context); } if (param.match(/^{\s*"/gm)) { // If it can't be parsed then the string is not a JSON format. try { const parsedParam = JSON.parse(param); // Handle non-exception-throwing cases, e.g. null, integer, boolean. if (parsedParam && typeof parsedParam === "object") { param = parsedParam; } } catch (err) { // This was probably not JSON. // Keep the error message visible but do not promote it because it may not be an error. window.console.warn(err.message); } } const index = this.requiredStrings.length; this.requiredStrings.push({ key, component, param, }); // The placeholder must not use {{}} as those can be misinterpreted by the engine. return `[[_s${index}]]`; } /** * String helper to render {{#cleanstr}}abd component { a : 'fish'}{{/cleanstr}} * into a get_string following by an HTML escape. * * @method cleanStringHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to parse the arguments from. * @param {function} helper Used to render subsections of the text. * @returns {string} */ cleanStringHelper(context, sectionText, helper) { // We're going to use [[_cx]] format for clean strings, where x is a number. // Hence, replacing 's' with 'c' in the placeholder that stringHelper returns. return this .stringHelper(context, sectionText, helper) .replace(placeholderString, placeholderCleanedString); } /** * Quote helper used to wrap content in quotes, and escape all special JSON characters present in the content. * * @method quoteHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to parse the arguments from. * @param {function} helper Used to render subsections of the text. * @returns {string} */ quoteHelper(context, sectionText, helper) { let content = helper(sectionText.trim(), context); // Escape the {{ and JSON encode. // This involves wrapping {{, and }} in change delimeter tags. content = JSON.stringify(content); content = content.replace(/([{}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>'); return content; } /** * Shorten text helper to truncate text and append a trailing ellipsis. * * @method shortenTextHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to parse the arguments from. * @param {function} helper Used to render subsections of the text. * @returns {string} */ shortenTextHelper(context, sectionText, helper) { // Non-greedy split on comma to grab section text into the length and // text parts. const parts = sectionText.match(/(.*?),(.*)/); // The length is the part matched in the first set of parethesis. const length = parts[1].trim(); // The length is the part matched in the second set of parethesis. const text = parts[2].trim(); const content = helper(text, context); return Truncate.truncate(content, { length, words: true, ellipsis: '...' }); } /** * User date helper to render user dates from timestamps. * * @method userDateHelper * @private * @param {object} context The current mustache context. * @param {string} sectionText The text to parse the arguments from. * @param {function} helper Used to render subsections of the text. * @returns {string} */ userDateHelper(context, sectionText, helper) { // Non-greedy split on comma to grab the timestamp and format. const parts = sectionText.match(/(.*?),(.*)/); const timestamp = helper(parts[1].trim(), context); const format = helper(parts[2].trim(), context); const index = this.requiredDates.length; this.requiredDates.push({ timestamp: timestamp, format: format }); return `[[_t_${index}]]`; } /** * Return a helper function to be added to the context for rendering the a * template. * * This will parse the provided text before giving it to the helper function * in order to remove any disallowed nested helpers to prevent one helper * from calling another. * * In particular to prevent the JS helper from being called from within another * helper because it can lead to security issues when the JS portion is user * provided. * * @param {function} helperFunction The helper function to add * @param {object} context The template context for the helper function * @returns {Function} To be set in the context */ addHelperFunction(helperFunction, context) { return function() { return function(sectionText, helper) { // Override the disallowed helpers in the template context with // a function that returns an empty string for use when executing // other helpers. This is to prevent these helpers from being // executed as part of the rendering of another helper in order to // prevent any potential security issues. const originalHelpers = Renderer.disallowedNestedHelpers.reduce((carry, name) => { if (context.hasOwnProperty(name)) { carry[name] = context[name]; } return carry; }, {}); Renderer.disallowedNestedHelpers.forEach((helperName) => { context[helperName] = () => ''; }); // Execute the helper with the modified context that doesn't include // the disallowed nested helpers. This prevents the disallowed // helpers from being called from within other helpers. const result = helperFunction.apply(this, [context, sectionText, helper]); // Restore the original helper implementation in the context so that // any further rendering has access to them again. for (const name in originalHelpers) { context[name] = originalHelpers[name]; } return result; }.bind(this); }.bind(this); } /** * Add some common helper functions to all context objects passed to templates. * These helpers match exactly the helpers available in php. * * @method addHelpers * @private * @param {Object} context Simple types used as the context for the template. * @param {String} themeName We set this multiple times, because there are async calls. */ addHelpers(context, themeName) { this.currentThemeName = themeName; this.requiredStrings = []; this.requiredJS = []; context.uniqid = (Renderer.uniqInstances++); // Please note that these helpers _must_ not return a Promise. context.str = this.addHelperFunction(this.stringHelper, context); context.cleanstr = this.addHelperFunction(this.cleanStringHelper, context); context.pix = this.addHelperFunction(this.pixHelper, context); context.js = this.addHelperFunction(this.jsHelper, context); context.quote = this.addHelperFunction(this.quoteHelper, context); context.shortentext = this.addHelperFunction(this.shortenTextHelper, context); context.userdate = this.addHelperFunction(this.userDateHelper, context); context.globals = {config: config}; context.currentTheme = themeName; } /** * Get all the JS blocks from the last rendered template. * * @method getJS * @private * @returns {string} */ getJS() { return this.requiredJS.join(";\n"); } /** * Treat strings in content. * * The purpose of this method is to replace the placeholders found in a string * with the their respective translated strings. * * Previously we were relying on String.replace() but the complexity increased with * the numbers of strings to replace. Now we manually walk the string and stop at each * placeholder we find, only then we replace it. Most of the time we will * replace all the placeholders in a single run, at times we will need a few * more runs when placeholders are replaced with strings that contain placeholders * themselves. * * @param {String} content The content in which string placeholders are to be found. * @param {Map} stringMap The strings to replace with. * @returns {String} The treated content. */ treatStringsInContent(content, stringMap) { // Placeholders are in the for [[_sX]] or [[_cX]] where X is the string index. const stringPattern = /(?<placeholder>\[\[_(?<stringType>[cs])(?<stringIndex>\d+)\]\])/g; // A helpre to fetch the string for a given placeholder. const getUpdatedString = ({placeholder, stringType, stringIndex}) => { if (stringMap.has(placeholder)) { return stringMap.get(placeholder); } if (stringType === placeholderCleanedString) { // Attempt to find the unclean string and clean it. Store it for later use. const uncleanString = stringMap.get(`[[_s${stringIndex}]]`); if (uncleanString) { stringMap.set(placeholder, mustache.escape(uncleanString)); return stringMap.get(placeholder); } } Log.debug(`Could not find string for pattern ${placeholder}`); return ''; }; // Find all placeholders in the content and replace them with their respective strings. let match; while ((match = stringPattern.exec(content)) !== null) { let updatedContent = content.slice(0, match.index); updatedContent += getUpdatedString(match.groups); updatedContent += content.slice(match.index + match.groups.placeholder.length); content = updatedContent; } return content; } /** * Treat strings in content. * * The purpose of this method is to replace the date placeholders found in the * content with the their respective translated dates. * * @param {String} content The content in which string placeholders are to be found. * @param {Array} dates The dates to replace with. * @returns {String} The treated content. */ treatDatesInContent(content, dates) { dates.forEach((date, index) => { content = content.replace( new RegExp(`\\[\\[_t_${index}\\]\\]`, 'g'), date, ); }); return content; } /** * Render a template and then call the callback with the result. * * @method doRender * @private * @param {string|Promise} templateSourcePromise The mustache template to render. * @param {Object} context Simple types used as the context for the template. * @param {String} themeName Name of the current theme. * @returns {Promise<object<string, string>>} The rendered HTML and JS. */ async doRender(templateSourcePromise, context, themeName) { this.currentThemeName = themeName; const iconTemplate = this.iconSystem.getTemplateName(); const pendingPromise = new Pending('core/templates:doRender'); const [templateSource] = await Promise.all([ templateSourcePromise, Renderer.getLoader().getTemplate(iconTemplate, themeName), ]); this.addHelpers(context, themeName); // Render the template. const renderedContent = await mustache.render( templateSource, context, // Note: The third parameter is a function that will be called to process partials. (partialName) => Renderer.getLoader().partialHelper(partialName, themeName), ); const {html, js} = await this.processRenderedContent(renderedContent); pendingPromise.resolve(); return {html, js}; } /** * Process the rendered content, treating any strings and applying and helper strings, dates, etc. * @param {string} renderedContent * @returns {Promise<object<string, string>>} The rendered HTML and JS. */ async processRenderedContent(renderedContent) { let html = renderedContent.trim(); let js = this.getJS(); if (this.requiredStrings.length > 0) { // Fetch the strings into a new Map using the placeholder as an index. // Note: We only fetch the unclean version. Cleaning of strings happens lazily in treatStringsInContent. const stringMap = new Map( (await getStrings(this.requiredStrings)).map((string, index) => ( [`[[_s${index}]]`, string] )) ); // Make sure string substitutions are done for the userdate // values as well. this.requiredDates = this.requiredDates.map(function(date) { return { timestamp: this.treatStringsInContent(date.timestamp, stringMap), format: this.treatStringsInContent(date.format, stringMap) }; }.bind(this)); // Why do we not do another call the render here? // // Because that would expose DOS holes. E.g. // I create an assignment called "{{fish" which // would get inserted in the template in the first pass // and cause the template to die on the second pass (unbalanced). html = this.treatStringsInContent(html, stringMap); js = this.treatStringsInContent(js, stringMap); } // This has to happen after the strings replacement because you can // use the string helper in content for the user date helper. if (this.requiredDates.length > 0) { const dates = await UserDate.get(this.requiredDates); html = this.treatDatesInContent(html, dates); js = this.treatDatesInContent(js, dates); } return {html, js}; } /** * Load a template and call doRender on it. * * @method render * @private * @param {string} templateName - should consist of the component and the name of the template like this: * core/menu (lib/templates/menu.mustache) or * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache) * @param {Object} [context={}] - Could be array, string or simple value for the context of the template. * @param {string} [themeName] - Name of the current theme. * @returns {Promise<object>} Native promise object resolved when the template has been rendered.} */ async render( templateName, context = {}, themeName = config.theme, ) { this.currentThemeName = themeName; // Preload the module to do the icon rendering based on the theme iconsystem. await this.setupIconSystem(); const templateSource = Renderer.getLoader().cachePartials(templateName, themeName); return this.doRender(templateSource, context, themeName); } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка