AutorÃa | Ultima modificación | Ver Log |
// 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 $ from 'jquery';import ajax from 'core/ajax';import * as str from 'core/str';import * as config from 'core/config';import mustache from 'core/mustache';import storage from 'core/localstorage';import {getNormalisedComponent} from 'core/utils';/*** Template this.** @module core/local/templates/loader* @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 Loader {/** @var {String} themeName for the current render */currentThemeName = '';/** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */static loadTemplateBuffer = [];/** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */static isLoadingTemplates = false;/** @var {Map} templateCache - Cache of already loaded template strings */static templateCache = new Map();/** @var {Promise[]} templatePromises - Cache of already loaded template promises */static templatePromises = {};/** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */static cachePartialPromises = [];/*** A helper to get the search key** @param {string} theme* @param {string} templateName* @returns {string}*/static getSearchKey(theme, templateName) {return `${theme}/${templateName}`;}/*** Load a template.** @method getTemplate* @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 {string} [themeName=config.theme] - The theme to load the template from* @return {Promise} JQuery promise object resolved when the template has been fetched.*/static getTemplate(templateName, themeName = config.theme) {const searchKey = this.getSearchKey(themeName, templateName);// If we haven't already seen this template then buffer it.const cachedPromise = this.getTemplatePromiseFromCache(searchKey);if (cachedPromise) {return cachedPromise;}// Check the buffer to see if this template has already been added.const existingBufferRecords = this.loadTemplateBuffer.filter((record) => record.searchKey === searchKey);if (existingBufferRecords.length) {// This template is already in the buffer so just return the existing// promise. No need to add it to the buffer again.return existingBufferRecords[0].deferred.promise();}// This is the first time this has been requested so let's add it to the buffer// to be loaded.const parts = templateName.split('/');const component = getNormalisedComponent(parts.shift());const name = parts.join('/');const deferred = $.Deferred();// Add this template to the buffer to be loaded.this.loadTemplateBuffer.push({component,name,theme: themeName,searchKey,deferred,});// We know there is at least one thing in the buffer so kick off a processing run.this.processLoadTemplateBuffer();return deferred.promise();}/*** Store a template in the cache.** @param {string} searchKey* @param {string} templateSource*/static setTemplateInCache(searchKey, templateSource) {// Cache all of the dependent templates because we'll need them to render// the requested template.this.templateCache.set(searchKey, templateSource);}/*** Fetch a template from the cache.** @param {string} searchKey* @returns {string}*/static getTemplateFromCache(searchKey) {return this.templateCache.get(searchKey);}/*** Check whether a template is in the cache.** @param {string} searchKey* @returns {bool}*/static hasTemplateInCache(searchKey) {return this.templateCache.has(searchKey);}/*** Prefetch a set of templates without rendering them.** @param {Array} templateNames The list of templates to fetch* @param {string} themeName*/static prefetchTemplates(templateNames, themeName) {templateNames.forEach((templateName) => this.prefetchTemplate(templateName, themeName));}/*** Prefetech a sginle template without rendering it.** @param {string} templateName* @param {string} themeName*/static prefetchTemplate(templateName, themeName) {const searchKey = this.getSearchKey(themeName, templateName);// If we haven't already seen this template then buffer it.if (this.hasTemplateInCache(searchKey)) {return;}// Check the buffer to see if this template has already been added.const existingBufferRecords = this.loadTemplateBuffer.filter((record) => record.searchKey === searchKey);if (existingBufferRecords.length) {// This template is already in the buffer so just return the existing promise.// No need to add it to the buffer again.return;}// This is the first time this has been requested so let's add it to the buffer to be loaded.const parts = templateName.split('/');const component = getNormalisedComponent(parts.shift());const name = parts.join('/');// Add this template to the buffer to be loaded.this.loadTemplateBuffer.push({component,name,theme: themeName,searchKey,deferred: $.Deferred(),});this.processLoadTemplateBuffer();}/*** Load a partial from the cache or ajax.** @method partialHelper* @param {string} name The partial name to load.* @param {string} [themeName = config.theme] The theme to load the partial from.* @return {string}*/static partialHelper(name, themeName = config.theme) {const searchKey = this.getSearchKey(themeName, name);if (!this.hasTemplateInCache(searchKey)) {new Error(`Failed to pre-fetch the template: ${name}`);}return this.getTemplateFromCache(searchKey);}/*** Scan a template source for partial tags and return a list of the found partials.** @method scanForPartials* @param {string} templateSource - source template to scan.* @return {Array} List of partials.*/static scanForPartials(templateSource) {const tokens = mustache.parse(templateSource);const partials = [];const findPartial = (tokens, partials) => {let i;for (i = 0; i < tokens.length; i++) {const token = tokens[i];if (token[0] == '>' || token[0] == '<') {partials.push(token[1]);}if (token.length > 4) {findPartial(token[4], partials);}}};findPartial(tokens, partials);return partials;}/*** Load a template and scan it for partials. Recursively fetch the partials.** @method cachePartials* @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 {string} [themeName=config.theme]* @param {Array} parentage - A list of requested partials in this render chain.* @return {Promise} JQuery promise object resolved when all partials are in the cache.*/static cachePartials(templateName, themeName = config.theme, parentage = []) {const searchKey = this.getSearchKey(themeName, templateName);if (searchKey in this.cachePartialPromises) {return this.cachePartialPromises[searchKey];}// This promise will not be resolved until all child partials are also resolved and ready.// We create it here to allow us to check for recursive inclusion of templates.// Keep track of the requested partials in this chain.if (!parentage.length) {parentage.push(searchKey);}this.cachePartialPromises[searchKey] = $.Deferred();this._cachePartials(templateName, themeName, parentage).catch((error) => {this.cachePartialPromises[searchKey].reject(error);});return this.cachePartialPromises[searchKey];}/*** Cache the template partials for the specified template.** @param {string} templateName* @param {string} themeName* @param {array} parentage* @returns {promise<string>}*/static async _cachePartials(templateName, themeName, parentage) {const searchKey = this.getSearchKey(themeName, templateName);const templateSource = await this.getTemplate(templateName, themeName);const partials = this.scanForPartials(templateSource);const uniquePartials = partials.filter((partialName) => {// Check for recursion.if (parentage.indexOf(`${themeName}/${partialName}`) >= 0) {// Ignore templates which include a parent template already requested in the current chain.return false;}// Ignore templates that include themselves.return partialName !== templateName;});// Fetch any partial which has not already been fetched.const fetchThemAll = uniquePartials.map((partialName) => {parentage.push(`${themeName}/${partialName}`);return this.cachePartials(partialName, themeName, parentage);});await Promise.all(fetchThemAll);return this.cachePartialPromises[searchKey].resolve(templateSource);}/*** Take all of the templates waiting in the buffer and load them from the server* or from the cache.** All of the templates that need to be loaded from the server will be batched up* and sent in a single network request.*/static processLoadTemplateBuffer() {if (!this.loadTemplateBuffer.length) {return;}if (this.isLoadingTemplates) {return;}this.isLoadingTemplates = true;// Grab any templates waiting in the buffer.const templatesToLoad = this.loadTemplateBuffer.slice();// This will be resolved with the list of promises for the server request.const serverRequestsDeferred = $.Deferred();const requests = [];// Get a list of promises for each of the templates we need to load.const templatePromises = templatesToLoad.map((templateData) => {const component = getNormalisedComponent(templateData.component);const name = templateData.name;const searchKey = templateData.searchKey;const theme = templateData.theme;const templateDeferred = templateData.deferred;let promise = null;// Double check to see if this template happened to have landed in the// cache as a dependency of an earlier template.if (this.hasTemplateInCache(searchKey)) {// We've seen this template so immediately resolve the existing promise.promise = this.getTemplatePromiseFromCache(searchKey);} else {// We haven't seen this template yet so we need to request it from// the server.requests.push({methodname: 'core_output_load_template_with_dependencies',args: {component,template: name,themename: theme,lang: config.language,}});// Remember the index in the requests list for this template so that// we can get the appropriate promise back.const index = requests.length - 1;// The server deferred will be resolved with a list of all of the promises// that were sent in the order that they were added to the requests array.promise = serverRequestsDeferred.promise().then((promises) => {// The promise for this template will be the one that matches the index// for it's entry in the requests array.//// Make sure the promise is added to the promises cache for this template// search key so that we don't request it again.templatePromises[searchKey] = promises[index].then((response) => {// Process all of the template dependencies for this template and add// them to the caches so that we don't request them again later.response.templates.forEach((data) => {data.component = getNormalisedComponent(data.component);const tempSearchKey = this.getSearchKey(theme,[data.component, data.name].join('/'),);// Cache all of the dependent templates because we'll need them to render// the requested template.this.setTemplateInCache(tempSearchKey, data.value);if (config.templaterev > 0) {// The template cache is enabled - set the value there.storage.set(`core_template/${config.templaterev}:${tempSearchKey}`, data.value);}});if (response.strings.length) {// If we have strings that the template needs then warm the string cache// with them now so that we don't need to re-fetch them.str.cache_strings(response.strings.map(({component, name, value}) => ({component: getNormalisedComponent(component),key: name,value,})));}// Return the original template source that the user requested.if (this.hasTemplateInCache(searchKey)) {return this.getTemplateFromCache(searchKey);}return null;});return templatePromises[searchKey];});}return promise// When we've successfully loaded the template then resolve the deferred// in the buffer so that all of the calling code can proceed..then((source) => templateDeferred.resolve(source)).catch((error) => {// If there was an error loading the template then reject the deferred// in the buffer so that all of the calling code can proceed.templateDeferred.reject(error);// Rethrow for anyone else listening.throw error;});});if (requests.length) {// We have requests to send so resolve the deferred with the promises.serverRequestsDeferred.resolve(ajax.call(requests, true, false, false, 0, config.templaterev));} else {// Nothing to load so we can resolve our deferred.serverRequestsDeferred.resolve();}// Once we've finished loading all of the templates then recurse to process// any templates that may have been added to the buffer in the time that we// were fetching.$.when.apply(null, templatePromises).then(() => {// Remove the templates we've loaded from the buffer.this.loadTemplateBuffer.splice(0, templatesToLoad.length);this.isLoadingTemplates = false;this.processLoadTemplateBuffer();return;}).catch(() => {// Remove the templates we've loaded from the buffer.this.loadTemplateBuffer.splice(0, templatesToLoad.length);this.isLoadingTemplates = false;this.processLoadTemplateBuffer();});}/*** Search the various caches for a template promise for the given search key.* The search key should be in the format <theme>/<component>/<template> e.g. boost/core/modal.** If the template is found in any of the caches it will populate the other caches with* the same data as well.** @param {String} searchKey The template search key in the format <theme>/<component>/<template> e.g. boost/core/modal* @returns {Object|null} jQuery promise resolved with the template source*/static getTemplatePromiseFromCache(searchKey) {// First try the cache of promises.if (searchKey in this.templatePromises) {return this.templatePromises[searchKey];}// Check the module cache.if (this.hasTemplateInCache(searchKey)) {const templateSource = this.getTemplateFromCache(searchKey);// Add this to the promises cache for future.this.templatePromises[searchKey] = $.Deferred().resolve(templateSource).promise();return this.templatePromises[searchKey];}if (config.templaterev <= 0) {// Template caching is disabled. Do not store in persistent storage.return null;}// Now try local storage.const cached = storage.get(`core_template/${config.templaterev}:${searchKey}`);if (cached) {// Add this to the module cache for future.this.setTemplateInCache(searchKey, cached);// Add to the promises cache for future.this.templatePromises[searchKey] = $.Deferred().resolve(cached).promise();return this.templatePromises[searchKey];}return null;}}