UNPKG

yoastseo

Version:

Yoast client-side content analysis

353 lines (332 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _lodash = require("lodash"); var _invalidType = _interopRequireDefault(require("./errors/invalidType")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * The plugins object takes care of plugin registrations, preloading and managing data modifications. * * Please note that there is a newer copy of this plugin in `packages/js/src/lib/Pluggable.js`. * For internal use, please use the newer copy for all interfaces except for registering assessments. * * A plugin for YoastSEO.js is basically a piece of JavaScript that hooks into YoastSEO.js by registering modifications. * In order to do so, it must first register itself as a plugin with YoastSEO.js. To keep our content analysis fast, we * don't allow asynchronous modifications. That's why we require plugins to preload all data they need in order to modify * the content. If plugins need to preload data, they can first register, then preload using AJAX and call `ready` once * preloaded. * * To minimize client side memory usage, we request plugins to preload as little data as possible. If you need to dynamically * fetch more data in the process of content creation, you can reload your data set and let YoastSEO.js know you've reloaded * by calling `reloaded`. */ class Pluggable { /** * Setup Pluggable and set its default values. * * @constructor * @param {App} app The App object to attach to. * @property {number} preloadThreshold The maximum time plugins are allowed to preload before we load our content analysis. * @property {object} plugins The plugins that have been registered. * @property {object} modifications The modifications that have been registered. Every modification contains an array with callables. * @property {Array} customTests All tests added by plugins. */ constructor(app) { this.app = app; this.loaded = false; this.preloadThreshold = 3000; this.plugins = {}; this.modifications = {}; this.customTests = []; // Allow plugins 1500 ms to register before we start polling their setTimeout(this._pollLoadingPlugins.bind(this), 1500); } // ***** DSL IMPLEMENTATION ***** // /** * Register a plugin with YoastSEO. A plugin can be declared "ready" right at registration or later using `this.ready`. * * @param {string} pluginName The name of the plugin to be registered. * @param {object} options The options passed by the plugin. * @param {string} options.status The status of the plugin being registered. Can either be "loading" or "ready". * @returns {boolean} Whether or not the plugin was successfully registered. */ _registerPlugin(pluginName, options) { if (typeof pluginName !== "string") { console.error("Failed to register plugin. Expected parameter `pluginName` to be a string."); return false; } if (!(0, _lodash.isUndefined)(options) && typeof options !== "object") { console.error("Failed to register plugin " + pluginName + ". Expected parameters `options` to be a object."); return false; } if (this._validateUniqueness(pluginName) === false) { console.error("Failed to register plugin. Plugin with name " + pluginName + " already exists"); return false; } this.plugins[pluginName] = options; return true; } /** * Declare a plugin "ready". Use this if you need to preload data with AJAX. * * @param {string} pluginName The name of the plugin to be declared as ready. * @returns {boolean} Whether or not the plugin was successfully declared ready. */ _ready(pluginName) { if (typeof pluginName !== "string") { console.error("Failed to modify status for plugin " + pluginName + ". Expected parameter `pluginName` to be a string."); return false; } if ((0, _lodash.isUndefined)(this.plugins[pluginName])) { console.error("Failed to modify status for plugin " + pluginName + ". The plugin was not properly registered."); return false; } this.plugins[pluginName].status = "ready"; return true; } /** * Used to declare a plugin has been reloaded. If an analysis is currently running. We will reset it to ensure running the latest modifications. * * @param {string} pluginName The name of the plugin to be declared as reloaded. * @returns {boolean} Whether or not the plugin was successfully declared as reloaded. */ _reloaded(pluginName) { if (typeof pluginName !== "string") { console.error("Failed to reload Content Analysis for " + pluginName + ". Expected parameter `pluginName` to be a string."); return false; } if ((0, _lodash.isUndefined)(this.plugins[pluginName])) { console.error("Failed to reload Content Analysis for plugin " + pluginName + ". The plugin was not properly registered."); return false; } this.app.refresh(); return true; } /** * Enables hooking a callable to a specific data filter supported by YoastSEO. Can only be performed for plugins that have finished loading. * * @param {string} modification The name of the filter * @param {function} callable The callable * @param {string} pluginName The plugin that is registering the modification. * @param {number} priority (optional) Used to specify the order in which the callables associated with a particular filter are called. * Lower numbers correspond with earlier execution. * @returns {boolean} Whether or not applying the hook was successful. */ _registerModification(modification, callable, pluginName, priority) { if (typeof modification !== "string") { console.error("Failed to register modification for plugin " + pluginName + ". Expected parameter `modification` to be a string."); return false; } if (typeof callable !== "function") { console.error("Failed to register modification for plugin " + pluginName + ". Expected parameter `callable` to be a function."); return false; } if (typeof pluginName !== "string") { console.error("Failed to register modification for plugin " + pluginName + ". Expected parameter `pluginName` to be a string."); return false; } // Validate origin if (this._validateOrigin(pluginName) === false) { console.error("Failed to register modification for plugin " + pluginName + ". The integration has not finished loading yet."); return false; } // Default priority to 10 const prio = typeof priority === "number" ? priority : 10; const callableObject = { callable: callable, origin: pluginName, priority: prio }; // Make sure modification is defined on modifications object if ((0, _lodash.isUndefined)(this.modifications[modification])) { this.modifications[modification] = []; } this.modifications[modification].push(callableObject); return true; } /** * Register test for a specific plugin * * @returns {void} * * @deprecated */ _registerTest() { console.error("This function is deprecated, please use _registerAssessment"); } /** * Register an assessment for a specific plugin * * @param {object} assessor The assessor object where the assessments needs to be added. * @param {string} name The name of the assessment. * @param {function} assessment The function to run as an assessment. * @param {string} pluginName The name of the plugin associated with the assessment. * @returns {boolean} Whether registering the assessment was successful. * @private */ _registerAssessment(assessor, name, assessment, pluginName) { if (!(0, _lodash.isString)(name)) { throw new _invalidType.default("Failed to register test for plugin " + pluginName + ". Expected parameter `name` to be a string."); } if (!(0, _lodash.isObject)(assessment)) { throw new _invalidType.default("Failed to register assessment for plugin " + pluginName + ". Expected parameter `assessment` to be a function."); } if (!(0, _lodash.isString)(pluginName)) { throw new _invalidType.default("Failed to register assessment for plugin " + pluginName + ". Expected parameter `pluginName` to be a string."); } // Prefix the name with the pluginName so the test name is always unique. name = pluginName + "-" + name; assessor.addAssessment(name, assessment); return true; } // ***** PRIVATE HANDLERS *****// /** * Poller to handle loading of plugins. Plugins can register with our app to let us know they are going to hook into our Javascript. They are allowed * 5 seconds of pre-loading time to fetch all the data they need to be able to perform their data modifications. We will only apply data modifications * from plugins that have declared ready within the pre-loading time in order to safeguard UX and data integrity. * * @param {number} pollTime (optional) The accumulated time to compare with the pre-load threshold. * @returns {void} * @private */ _pollLoadingPlugins(pollTime) { pollTime = (0, _lodash.isUndefined)(pollTime) ? 0 : pollTime; if (this._allReady() === true) { this.loaded = true; this.app.pluginsLoaded(); } else if (pollTime >= this.preloadThreshold) { this._pollTimeExceeded(); } else { pollTime += 50; setTimeout(this._pollLoadingPlugins.bind(this, pollTime), 50); } } /** * Checks if all registered plugins have finished loading * * @returns {boolean} Whether or not all registered plugins are loaded. * @private */ _allReady() { return (0, _lodash.reduce)(this.plugins, function (allReady, plugin) { return allReady && plugin.status === "ready"; }, true); } /** * Removes the plugins that were not loaded within time and calls `pluginsLoaded` on the app. * * @returns {void} * @private */ _pollTimeExceeded() { (0, _lodash.forEach)(this.plugins, function (plugin, pluginName) { if (!(0, _lodash.isUndefined)(plugin.options) && plugin.options.status !== "ready") { console.error("Error: Plugin " + pluginName + ". did not finish loading in time."); delete this.plugins[pluginName]; } }); this.loaded = true; this.app.pluginsLoaded(); } /** * Calls the callables added to a modification hook. See the YoastSEO.js Readme for a list of supported modification hooks. * * @param {string} modification The name of the filter * @param {*} data The data to filter * @param {*} context (optional) Object for passing context parameters to the callable. * @returns {*} The filtered data * @private */ _applyModifications(modification, data, context) { let callChain = this.modifications[modification]; if (callChain instanceof Array && callChain.length > 0) { callChain = this._stripIllegalModifications(callChain); callChain.sort(function (a, b) { return a.priority - b.priority; }); (0, _lodash.forEach)(callChain, function (callableObject) { const callable = callableObject.callable; const newData = callable(data, context); if (typeof newData === typeof data) { data = newData; } else { console.error("Modification with name " + modification + " performed by plugin with name " + callableObject.origin + " was ignored because the data that was returned by it was of a different type than the data we had passed it."); } }); } return data; } /** * Adds new tests to the analyzer and it's scoring object. * * @param {Object} analyzer The analyzer object to add the tests to * @returns {void} * @private */ _addPluginTests(analyzer) { this.customTests.map(function (customTest) { this._addPluginTest(analyzer, customTest); }, this); } /** * Adds one new test to the analyzer and it's scoring object. * * @param {Object} analyzer The analyzer that the test will be added to. * @param {Object} pluginTest The test to be added. * @param {string} pluginTest.name The name of the test. * @param {function} pluginTest.callable The function associated with the test. * @param {function} pluginTest.analysis The function associated with the analyzer. * @param {Object} pluginTest.scoring The scoring object to be used. * @returns {void} * @private */ _addPluginTest(analyzer, pluginTest) { analyzer.addAnalysis({ name: pluginTest.name, callable: pluginTest.analysis }); analyzer.analyzeScorer.addScoring({ name: pluginTest.name, scoring: pluginTest.scoring }); } /** * Strips modifications from a callChain if they were not added with a valid origin. * * @param {Array} callChain The callChain that contains items with possible invalid origins. * @returns {Array} callChain The stripped version of the callChain. * @private */ _stripIllegalModifications(callChain) { (0, _lodash.forEach)(callChain, function (callableObject, index) { if (this._validateOrigin(callableObject.origin) === false) { delete callChain[index]; } }.bind(this)); return callChain; } /** * Validates if origin of a modification has been registered and finished preloading. * * @param {string} pluginName The name of the plugin that needs to be validated. * @returns {boolean} Whether or not the origin is valid. * @private */ _validateOrigin(pluginName) { return this.plugins[pluginName].status === "ready"; } /** * Validates if registered plugin has a unique name. * * @param {string} pluginName The name of the plugin that needs to be validated for uniqueness. * @returns {boolean} Whether or not the plugin has a unique name. * @private */ _validateUniqueness(pluginName) { return (0, _lodash.isUndefined)(this.plugins[pluginName]); } } exports.default = Pluggable; //# sourceMappingURL=pluggable.js.map