yoastseo
Version:
Yoast client-side content analysis
353 lines (332 loc) • 14.5 kB
JavaScript
"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