yoastseo
Version:
Yoast client-side content analysis
1,259 lines (1,190 loc) • 48 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _featureFlag = require("@yoast/feature-flag");
var _i18n = require("@wordpress/i18n");
var _lodash = require("lodash");
var _loglevel = require("loglevel");
var _build = require("../parse/build");
var _shortlinker = require("../helpers/shortlinker");
var _invalidType = _interopRequireDefault(require("../errors/invalidType.js"));
var _includesAny = _interopRequireDefault(require("../helpers/includesAny.js"));
var _LanguageProcessor = _interopRequireDefault(require("../parse/language/LanguageProcessor.js"));
var _missingArgument = _interopRequireDefault(require("../errors/missingArgument.js"));
var _Paper = _interopRequireDefault(require("../values/Paper.js"));
var _scheduler = _interopRequireDefault(require("./scheduler"));
var _transporter = _interopRequireDefault(require("./transporter"));
var _wrapTryCatchAroundAction = _interopRequireDefault(require("./wrapTryCatchAroundAction.js"));
var _contentAssessor = _interopRequireDefault(require("../scoring/assessors/contentAssessor.js"));
var _contentAssessor2 = _interopRequireDefault(require("../scoring/assessors/cornerstone/contentAssessor.js"));
var _relatedKeywordAssessor = _interopRequireDefault(require("../scoring/assessors/cornerstone/relatedKeywordAssessor.js"));
var _seoAssessor = _interopRequireDefault(require("../scoring/assessors/cornerstone/seoAssessor.js"));
var _inclusiveLanguageAssessor = _interopRequireDefault(require("../scoring/assessors/inclusiveLanguageAssessor.js"));
var _relatedKeywordAssessor2 = _interopRequireDefault(require("../scoring/assessors/relatedKeywordAssessor.js"));
var _relatedKeywordTaxonomyAssessor = _interopRequireDefault(require("../scoring/assessors/relatedKeywordTaxonomyAssessor.js"));
var _seoAssessor2 = _interopRequireDefault(require("../scoring/assessors/seoAssessor.js"));
var _taxonomyAssessor = _interopRequireDefault(require("../scoring/assessors/taxonomyAssessor.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/* eslint-disable complexity */
// External dependencies.
// Internal dependencies.
// Assessor classes.
const logger = (0, _loglevel.getLogger)("yoast-analysis-worker");
logger.setDefaultLevel("error");
/**
* Analysis Web Worker.
*
* Worker API: https://developer.mozilla.org/en-US/docs/Web/API/Worker
* Webpack loader: https://github.com/webpack-contrib/worker-loader
*/
class AnalysisWebWorker {
/* eslint-disable max-statements */
/**
* Initializes the AnalysisWebWorker class.
*
* @param {Object} scope The scope for the messaging. Expected to have the
* `onmessage` event and the `postMessage` function.
* @param {Researcher} researcher The researcher to use.
*/
constructor(scope, researcher) {
this._scope = scope;
this._configuration = {
contentAnalysisActive: true,
keywordAnalysisActive: true,
inclusiveLanguageAnalysisActive: false,
useCornerstone: false,
useTaxonomy: false,
// The locale used for language-specific configurations in Flesch-reading ease and Sentence length assessments.
locale: "en_US",
customAnalysisType: ""
};
this._scheduler = new _scheduler.default();
this._paper = null;
this._relatedKeywords = {};
this._researcher = researcher;
this._contentAssessor = null;
this._seoAssessor = null;
this._relatedKeywordAssessor = null;
this.additionalAssessors = {};
this._inclusiveLanguageOptions = {};
/*
* The cached analysis results.
*
* A single result has the following structure:
* {AssessmentResult[]} readability.results An array of assessment results; in serialized format.
* {number} readability.score The overall score.
*
* The results have the following structure.
* {Object} readability Content assessor results.
* {Object} seo SEO assessor results, per keyword identifier or empty string for the main.
* {Object} seo[ "" ] The result of the paper analysis for the main keyword.
* {Object} seo[ key ] Same as above, but instead for a related keyword.
* {Object} inclusiveLanguage Inclusive language assessor results.
*/
this._results = {
readability: {
results: [],
score: 0
},
seo: {
"": {
results: [],
score: 0
}
},
inclusiveLanguage: {
results: [],
score: 0
}
};
this._registeredAssessments = [];
this._registeredMessageHandlers = {};
this._registeredParsers = [];
// Custom assessor classes.
this._CustomSEOAssessorClasses = {};
this._CustomCornerstoneSEOAssessorClasses = {};
this._CustomContentAssessorClasses = {};
this._CustomCornerstoneContentAssessorClasses = {};
this._CustomRelatedKeywordAssessorClasses = {};
this._CustomCornerstoneRelatedKeywordAssessorClasses = {};
// Custom assessor options.
this._CustomSEOAssessorOptions = {};
this._CustomCornerstoneSEOAssessorOptions = {};
this._CustomContentAssessorOptions = {};
this._CustomCornerstoneContentAssessorOptions = {};
this._CustomRelatedKeywordAssessorOptions = {};
this._CustomCornerstoneRelatedKeywordAssessorOptions = {};
this.bindActions();
this.assessRelatedKeywords = this.assessRelatedKeywords.bind(this);
// Bind register functions to this scope.
this.registerAssessment = this.registerAssessment.bind(this);
this.registerMessageHandler = this.registerMessageHandler.bind(this);
this.refreshAssessment = this.refreshAssessment.bind(this);
this.setCustomContentAssessorClass = this.setCustomContentAssessorClass.bind(this);
this.setCustomCornerstoneContentAssessorClass = this.setCustomCornerstoneContentAssessorClass.bind(this);
this.setCustomSEOAssessorClass = this.setCustomSEOAssessorClass.bind(this);
this.setCustomCornerstoneSEOAssessorClass = this.setCustomCornerstoneSEOAssessorClass.bind(this);
this.setCustomRelatedKeywordAssessorClass = this.setCustomRelatedKeywordAssessorClass.bind(this);
this.setCustomCornerstoneRelatedKeywordAssessorClass = this.setCustomCornerstoneRelatedKeywordAssessorClass.bind(this);
this.registerAssessor = this.registerAssessor.bind(this);
this.registerResearch = this.registerResearch.bind(this);
this.registerHelper = this.registerHelper.bind(this);
this.registerResearcherConfig = this.registerResearcherConfig.bind(this);
this.setInclusiveLanguageOptions = this.setInclusiveLanguageOptions.bind(this);
// Bind event handlers to this scope.
this.handleMessage = this.handleMessage.bind(this);
// Wrap try/catch around actions.
this.analyzeRelatedKeywords = (0, _wrapTryCatchAroundAction.default)(logger, this.analyze, "An error occurred while running the related keywords analysis.");
/*
* Overwrite this.analyze after we use it in this.analyzeRelatedKeywords so that this.analyzeRelatedKeywords
* doesn't use the overwritten version. Therefore, this order shouldn't be changed.
*/
this.analyze = (0, _wrapTryCatchAroundAction.default)(logger, this.analyze, "An error occurred while running the analysis.");
this.runResearch = (0, _wrapTryCatchAroundAction.default)(logger, this.runResearch, "An error occurred after running the '%%name%%' research.");
}
/* eslint-enable max-statements */
/**
* Binds actions to this scope.
*
* @returns {void}
*/
bindActions() {
// Bind actions to this scope.
this.analyze = this.analyze.bind(this);
this.analyzeDone = this.analyzeDone.bind(this);
this.analyzeRelatedKeywordsDone = this.analyzeRelatedKeywordsDone.bind(this);
this.loadScript = this.loadScript.bind(this);
this.loadScriptDone = this.loadScriptDone.bind(this);
this.customMessage = this.customMessage.bind(this);
this.customMessageDone = this.customMessageDone.bind(this);
this.clearCache = this.clearCache.bind(this);
this.runResearch = this.runResearch.bind(this);
this.runResearchDone = this.runResearchDone.bind(this);
}
/**
* Sets a custom content assessor class.
*
* @param {ContentAssessor} ContentAssessorClass A content assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomContentAssessorClass(ContentAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomContentAssessorClasses[customAnalysisType] = ContentAssessorClass;
this._CustomContentAssessorOptions[customAnalysisType] = customAssessorOptions;
this._contentAssessor = this.createContentAssessor();
}
/**
* Sets a custom cornerstone content assessor class.
*
* @param {CornerstoneContentAssessor} CornerstoneContentAssessorClass A cornerstone content assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomCornerstoneContentAssessorClass(CornerstoneContentAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomCornerstoneContentAssessorClasses[customAnalysisType] = CornerstoneContentAssessorClass;
this._CustomCornerstoneContentAssessorOptions[customAnalysisType] = customAssessorOptions;
this._contentAssessor = this.createContentAssessor();
}
/**
* Sets a custom SEO assessor class.
*
* @param {SEOAssessor} SEOAssessorClass An SEO assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomSEOAssessorClass(SEOAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomSEOAssessorClasses[customAnalysisType] = SEOAssessorClass;
this._CustomSEOAssessorOptions[customAnalysisType] = customAssessorOptions;
this._seoAssessor = this.createSEOAssessor();
}
/**
* Sets a custom cornerstone SEO assessor class.
*
* @param {CornerstoneSEOAssessor} CornerstoneSEOAssessorClass A cornerstone SEO assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomCornerstoneSEOAssessorClass(CornerstoneSEOAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomCornerstoneSEOAssessorClasses[customAnalysisType] = CornerstoneSEOAssessorClass;
this._CustomCornerstoneSEOAssessorOptions[customAnalysisType] = customAssessorOptions;
this._seoAssessor = this.createSEOAssessor();
}
/**
* Sets a custom related keyword assessor class.
*
* @param {RelatedKeywordAssessor} RelatedKeywordAssessorClass A related keyword assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomRelatedKeywordAssessorClass(RelatedKeywordAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomRelatedKeywordAssessorClasses[customAnalysisType] = RelatedKeywordAssessorClass;
this._CustomRelatedKeywordAssessorOptions[customAnalysisType] = customAssessorOptions;
this._relatedKeywordAssessor = this.createRelatedKeywordsAssessor();
}
/**
* Sets a custom cornerstone related keyword assessor class.
*
* @param {CornerstoneRelatedKeywordAssessor} CornerstoneRelatedKeywordAssessorClass A cornerstone related keyword assessor class.
* @param {string} customAnalysisType The type of analysis.
* @param {Object} customAssessorOptions The options to use.
*
* @returns {void}
*/
setCustomCornerstoneRelatedKeywordAssessorClass(CornerstoneRelatedKeywordAssessorClass, customAnalysisType, customAssessorOptions) {
this._CustomCornerstoneRelatedKeywordAssessorClasses[customAnalysisType] = CornerstoneRelatedKeywordAssessorClass;
this._CustomCornerstoneRelatedKeywordAssessorOptions[customAnalysisType] = customAssessorOptions;
this._relatedKeywordAssessor = this.createRelatedKeywordsAssessor();
}
/**
* Sets the options to use for the Inclusive language analysis.
*
* @param {{infoLinks: {}}} options The options to use.
*
* @returns {void}
*/
setInclusiveLanguageOptions(options) {
this._inclusiveLanguageOptions = options;
}
/**
* Registers this web worker with the scope passed to its constructor.
*
* @returns {void}
*/
register() {
this._scope.onmessage = this.handleMessage;
this._scope.analysisWorker = this;
}
/**
* Receives the post message and determines the action.
*
* See: https://developer.mozilla.org/en-US/docs/Web/API/Worker/onmessage
*
* @param {MessageEvent} event The post message event.
* @param {Object} event.data The data object.
* @param {string} event.data.type The action type.
* @param {string} event.data.id The request id.
* @param {string} event.data.payload The payload of the action.
*
* @returns {void}
*/
handleMessage({
data: {
type,
id,
payload
}
}) {
payload = _transporter.default.parse(payload);
logger.debug("AnalysisWebWorker incoming:", type, id, payload);
switch (type) {
case "initialize":
this.initialize(id, payload);
this._scheduler.startPolling();
break;
case "analyze":
this._scheduler.schedule({
id,
execute: this.analyze,
done: this.analyzeDone,
data: payload,
type: type
});
break;
case "analyzeRelatedKeywords":
this._scheduler.schedule({
id,
execute: this.analyzeRelatedKeywords,
done: this.analyzeRelatedKeywordsDone,
data: payload,
type: type
});
break;
case "loadScript":
this._scheduler.schedule({
id,
execute: this.loadScript,
done: this.loadScriptDone,
data: payload,
type: type
});
break;
case "runResearch":
this._scheduler.schedule({
id,
execute: this.runResearch,
done: this.runResearchDone,
data: payload
});
break;
case "customMessage":
{
const name = payload.name;
if (name && this._registeredMessageHandlers[name]) {
this._scheduler.schedule({
id,
execute: this.customMessage,
done: this.customMessageDone,
data: payload,
type: type
});
break;
}
this.customMessageDone(id, {
error: new Error("No message handler registered for messages with name: " + name)
});
break;
}
default:
console.warn("AnalysisWebWorker unrecognized action:", type);
}
}
/**
* Initializes the appropriate content assessor.
*
* @returns {ContentAssessor|null} The chosen content assessor.
*/
createContentAssessor() {
const {
contentAnalysisActive,
useCornerstone,
customAnalysisType
} = this._configuration;
if (contentAnalysisActive === false) {
return null;
}
let assessor;
if (useCornerstone === true) {
/*
* Use a custom cornerstone content assessor if available,
* otherwise set the default cornerstone content assessor.
*/
assessor = this._CustomCornerstoneContentAssessorClasses[customAnalysisType] ? new this._CustomCornerstoneContentAssessorClasses[customAnalysisType](this._researcher, this._CustomCornerstoneContentAssessorOptions[customAnalysisType]) : new _contentAssessor2.default(this._researcher);
// Add the readability assessment for cornerstone content to the cornerstone content assessor.
this._registeredAssessments.forEach(({
name,
assessment,
type
}) => {
if ((0, _lodash.isUndefined)(assessor.getAssessment(name)) && type === "cornerstoneReadability") {
assessor.addAssessment(name, assessment);
}
});
} else {
/*
* For non-cornerstone content, use a custom SEO assessor if available,
* otherwise use the default SEO assessor.
*/
assessor = this._CustomContentAssessorClasses[customAnalysisType] ? new this._CustomContentAssessorClasses[customAnalysisType](this._researcher, this._CustomContentAssessorOptions[customAnalysisType]) : new _contentAssessor.default(this._researcher);
// Add the readability assessment for regular content to the regular content assessor.
this._registeredAssessments.forEach(({
name,
assessment,
type
}) => {
if ((0, _lodash.isUndefined)(assessor.getAssessment(name)) && type === "readability") {
assessor.addAssessment(name, assessment);
}
});
}
return assessor;
}
/**
* Initializes the appropriate SEO assessor.
*
* @returns {SEOAssessor|null} The chosen SEO assessor.
*/
createSEOAssessor() {
const {
keywordAnalysisActive,
useCornerstone,
useTaxonomy,
customAnalysisType
} = this._configuration;
if (keywordAnalysisActive === false) {
return null;
}
let assessor;
if (useTaxonomy === true) {
assessor = new _taxonomyAssessor.default(this._researcher);
} else {
// Set cornerstone SEO assessor for cornerstone content.
if (useCornerstone === true) {
// Use a custom cornerstone SEO assessor if available, otherwise set the default cornerstone SEO assessor.
assessor = this._CustomCornerstoneSEOAssessorClasses[customAnalysisType] ? new this._CustomCornerstoneSEOAssessorClasses[customAnalysisType](this._researcher, this._CustomCornerstoneSEOAssessorOptions[customAnalysisType]) : new _seoAssessor.default(this._researcher);
} else {
/*
* For non-cornerstone content, use a custom SEO assessor if available,
* otherwise use the default SEO assessor.
*/
assessor = this._CustomSEOAssessorClasses[customAnalysisType] ? new this._CustomSEOAssessorClasses[customAnalysisType](this._researcher, this._CustomSEOAssessorOptions[customAnalysisType]) : new _seoAssessor2.default(this._researcher);
}
}
this._registeredAssessments.forEach(({
name,
assessment,
type
}) => {
if ((0, _lodash.isUndefined)(assessor.getAssessment(name)) && type === "seo") {
assessor.addAssessment(name, assessment);
}
});
return assessor;
}
/**
* Initializes the appropriate inclusive language assessor.
*
* @returns {InclusiveLanguageAssessor|null} The chosen inclusive language assessor.
*/
createInclusiveLanguageAssessor() {
const {
inclusiveLanguageAnalysisActive
} = this._configuration;
if (!inclusiveLanguageAnalysisActive) {
return null;
}
return new _inclusiveLanguageAssessor.default(this._researcher, this._inclusiveLanguageOptions);
}
/**
* Initializes the appropriate SEO assessor for related keywords.
*
* @returns {RelatedKeywordAssessor|null} The chosen related keyword assessor.
*/
createRelatedKeywordsAssessor() {
const {
keywordAnalysisActive,
useCornerstone,
useTaxonomy,
customAnalysisType
} = this._configuration;
if (keywordAnalysisActive === false) {
return null;
}
let assessor;
if (useTaxonomy === true) {
assessor = new _relatedKeywordTaxonomyAssessor.default(this._researcher);
} else {
// Set cornerstone related keyword assessor for cornerstone content.
if (useCornerstone === true) {
// Use a custom related keyword assessor if available, otherwise use the default related keyword assessor.
assessor = this._CustomCornerstoneRelatedKeywordAssessorClasses[customAnalysisType] ? new this._CustomCornerstoneRelatedKeywordAssessorClasses[customAnalysisType](this._researcher, this._CustomCornerstoneRelatedKeywordAssessorOptions[customAnalysisType]) : new _relatedKeywordAssessor.default(this._researcher);
} else {
/*
* For non-cornerstone content, use a custom related keyword assessor if available,
* otherwise use the default related keyword assessor.
*/
assessor = this._CustomRelatedKeywordAssessorClasses[customAnalysisType] ? new this._CustomRelatedKeywordAssessorClasses[customAnalysisType](this._researcher, this._CustomRelatedKeywordAssessorOptions[customAnalysisType]) : new _relatedKeywordAssessor2.default(this._researcher);
}
}
this._registeredAssessments.forEach(({
name,
assessment,
type
}) => {
if ((0, _lodash.isUndefined)(assessor.getAssessment(name)) && type === "relatedKeyphrase") {
assessor.addAssessment(name, assessment);
}
});
return assessor;
}
/**
* Sends a message.
*
* @param {string} type The message type.
* @param {number} id The request id.
* @param {Object} [payload] The payload to deliver.
*
* @returns {void}
*/
send(type, id, payload = {}) {
logger.debug("AnalysisWebWorker outgoing:", type, id, payload);
payload = _transporter.default.serialize(payload);
this._scope.postMessage({
type,
id,
payload
});
}
/**
* Checks which assessors should update giving a configuration.
*
* @param {Object} configuration The configuration to check.
* @param {ContentAssessor|null} [contentAssessor=null] The content assessor.
* @param {SEOAssessor|null} [seoAssessor=null] The SEO assessor.
* @param {InclusiveLanguageAssessor|null} [inclusiveLanguageAssessor=null] The inclusive language assessor.
*
* @returns {{seo: boolean, readability: boolean, inclusiveLanguage: boolean}} Whether each assessor should update.
*/
static shouldAssessorsUpdate(configuration, contentAssessor = null, seoAssessor = null, inclusiveLanguageAssessor = null) {
const readability = ["contentAnalysisActive", "useCornerstone", "locale", "translations", "customAnalysisType"];
const seo = ["keywordAnalysisActive", "useCornerstone", "useTaxonomy", "locale", "translations", "researchData", "customAnalysisType"];
const inclusiveLanguage = ["inclusiveLanguageAnalysisActive", "locale", "translations"];
const configurationKeys = Object.keys(configuration);
return {
readability: (0, _lodash.isNull)(contentAssessor) || (0, _includesAny.default)(configurationKeys, readability),
seo: (0, _lodash.isNull)(seoAssessor) || (0, _includesAny.default)(configurationKeys, seo),
inclusiveLanguage: (0, _lodash.isNull)(inclusiveLanguageAssessor) || (0, _includesAny.default)(configurationKeys, inclusiveLanguage)
};
}
/**
* Configures the analysis worker.
*
* @param {number} id The request id.
* @param {Object} configuration The configuration object.
* @param {boolean} [configuration.contentAnalysisActive] Whether the content analysis is active.
* @param {boolean} [configuration.keywordAnalysisActive] Whether the keyword analysis is active.
* @param {boolean} [configuration.useCornerstone] Whether the paper is cornerstone or not.
* @param {boolean} [configuration.useTaxonomy] Whether the taxonomy assessor should be used.
* @param {string} [configuration.locale] The locale used in the seo assessor.
* @param {Object} [configuration.translations] The translation strings.
* @param {Object} [configuration.researchData] Extra research data.
* @param {Object} [configuration.defaultQueryParams] The default query params for the Shortlinker.
* @param {string} [configuration.logLevel] Log level, see: https://github.com/pimterry/loglevel#documentation
* @param {string[]} [configuration.enabledFeatures] A list of feature name flags of the experimental features to enable.
*
* @returns {void}
*/
initialize(id, configuration) {
const update = AnalysisWebWorker.shouldAssessorsUpdate(configuration, this._contentAssessor, this._seoAssessor, this._inclusiveLanguageAssessor);
if ((0, _lodash.has)(configuration, "translations")) {
Object.values(configuration.translations).forEach(translation => {
// Don't proceed if translation object is null or otherwise falsy.
if (translation) {
const {
domain,
locale_data: localeData
} = translation;
(0, _i18n.setLocaleData)(localeData[domain], domain);
}
});
}
if ((0, _lodash.has)(configuration, "researchData")) {
(0, _lodash.forEach)(configuration.researchData, (data, research) => {
this._researcher.addResearchData(research, data);
});
delete configuration.researchData;
}
if ((0, _lodash.has)(configuration, "defaultQueryParams")) {
(0, _shortlinker.configureShortlinker)({
params: configuration.defaultQueryParams
});
delete configuration.defaultQueryParams;
}
if ((0, _lodash.has)(configuration, "logLevel")) {
logger.setLevel(configuration.logLevel, false);
delete configuration.logLevel;
}
if ((0, _lodash.has)(configuration, "enabledFeatures")) {
// Make feature flags available inside the web worker.
(0, _featureFlag.enableFeatures)(configuration.enabledFeatures);
delete configuration.enabledFeatures;
}
this._configuration = (0, _lodash.merge)(this._configuration, configuration);
if (update.readability) {
this._contentAssessor = this.createContentAssessor();
}
if (update.seo) {
this._seoAssessor = this.createSEOAssessor();
this._relatedKeywordAssessor = this.createRelatedKeywordsAssessor();
}
if (update.inclusiveLanguage) {
this._inclusiveLanguageAssessor = this.createInclusiveLanguageAssessor();
}
// Reset the paper in order to not use the cached results on analyze.
this.clearCache();
this.send("initialize:done", id);
}
/**
* Registers a custom assessor.
*
* @param {string} name The name of the assessor.
* @param {Function} AssessorClass The assessor class to instantiate.
* @param {Function} shouldUpdate Function that checks whether the assessor should update.
*
* @returns {void}
*/
registerAssessor(name, AssessorClass, shouldUpdate) {
const assessor = new AssessorClass(this._researcher);
this.additionalAssessors[name] = {
assessor,
shouldUpdate
};
}
/**
* Register an assessment for a specific plugin.
*
* @param {string} name The name of the assessment.
* @param {Assessment} assessment The assessment to add.
* @param {string} pluginName The name of the plugin associated with the assessment.
* @param {string} type The type of the assessment. The default type is "seo".
*
* @returns {boolean} Whether registering the assessment was successful.
*/
registerAssessment(name, assessment, pluginName, type = "seo") {
const {
useCornerstone
} = this._configuration;
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to register assessment 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.
const combinedName = pluginName + "-" + name;
if (this._seoAssessor !== null && type === "seo") {
this._seoAssessor.addAssessment(combinedName, assessment);
}
if (this._contentAssessor !== null && type === "readability") {
this._contentAssessor.addAssessment(combinedName, assessment);
}
if (this._contentAssessor !== null && type === "cornerstoneReadability" && useCornerstone) {
this._contentAssessor.addAssessment(combinedName, assessment);
}
if (this._relatedKeywordAssessor !== null && type === "relatedKeyphrase") {
this._relatedKeywordAssessor.addAssessment(combinedName, assessment);
}
this._registeredAssessments.push({
combinedName,
assessment,
type
});
this.refreshAssessment(name, pluginName);
return true;
}
/**
* Register a message handler for a specific plugin.
*
* @param {string} name The name of the message handler.
* @param {function} handler The function to run as a message handler.
* @param {string} pluginName The name of the plugin associated with the message handler.
*
* @returns {boolean} Whether registering the message handler was successful.
*/
registerMessageHandler(name, handler, pluginName) {
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to register handler for plugin " + pluginName + ". Expected parameter `name` to be a string.");
}
if (!(0, _lodash.isObject)(handler)) {
throw new _invalidType.default("Failed to register handler for plugin " + pluginName + ". Expected parameter `handler` to be a function.");
}
if (!(0, _lodash.isString)(pluginName)) {
throw new _invalidType.default("Failed to register handler 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;
this._registeredMessageHandlers[name] = handler;
return true;
}
/**
* Refreshes an assessment in the analysis.
*
* Custom assessments can use this to mark their assessment as needing a
* refresh.
*
* @param {string} name The name of the assessment.
* @param {string} pluginName The name of the plugin associated with the assessment.
*
* @returns {boolean} Whether refreshing the assessment was successful.
*/
refreshAssessment(name, pluginName) {
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to refresh assessment for plugin " + pluginName + ". Expected parameter `name` to be a string.");
}
if (!(0, _lodash.isString)(pluginName)) {
throw new _invalidType.default("Failed to refresh assessment for plugin " + pluginName + ". Expected parameter `pluginName` to be a string.");
}
this.clearCache();
return true;
}
/**
* Clears the worker cache to force a new analysis.
*
* @returns {void}
*/
clearCache() {
this._paper = null;
}
/**
* Changes the locale in the configuration.
*
* If the locale is different:
* - Update the configuration locale.
* - Create the content assessor.
*
* @param {string} locale The locale to set.
*
* @returns {void}
*/
setLocale(locale) {
if (this._configuration.locale === locale) {
return;
}
this._configuration.locale = locale;
this._contentAssessor = this.createContentAssessor();
}
/**
* Checks if the paper contains changes that are used for readability.
*
* @param {Paper} paper The paper to check against the cached paper.
*
* @returns {boolean} True if there are changes detected.
*/
shouldReadabilityUpdate(paper) {
if (this._paper === null) {
return true;
}
if (this._paper.getText() !== paper.getText()) {
return true;
}
if (this._paper.getKeyword() !== paper.getKeyword()) {
return true;
}
// Perform deep comparison between the list of Gutenberg blocks as we want to update the readability analysis
// if the client IDs of the blocks inside `wpBlocks` change.
if (!(0, _lodash.isEqual)(this._paper._attributes.wpBlocks, paper._attributes.wpBlocks)) {
return true;
}
return this._paper.getLocale() !== paper.getLocale();
}
/**
* Checks if the paper contains changes that are used for inclusive language analysis.
*
* @param {Paper} paper The paper to check against the cached paper.
*
* @returns {boolean} True if there are changes detected.
*/
shouldInclusiveLanguageUpdate(paper) {
if (this._paper === null) {
return true;
}
if (this._paper.getText() !== paper.getText()) {
return true;
}
if (this._paper.getTextTitle() !== paper.getTextTitle()) {
return true;
}
return this._paper.getLocale() !== paper.getLocale();
}
/**
* Updates the results for the inclusive language assessor.
*
* @param {boolean} shouldInclusiveLanguageUpdate Whether the results of the inclusive language assessor should be updated.
* @returns {void}
*/
updateInclusiveLanguageAssessor(shouldInclusiveLanguageUpdate) {
if (this._configuration.inclusiveLanguageAnalysisActive && this._inclusiveLanguageAssessor && shouldInclusiveLanguageUpdate) {
this._inclusiveLanguageAssessor.assess(this._paper);
this._results.inclusiveLanguage = {
results: this._inclusiveLanguageAssessor.results,
score: this._inclusiveLanguageAssessor.calculateOverallScore()
};
}
}
/**
* Checks if the related keyword contains changes that are used for seo.
*
* @param {string} key The identifier of the related keyword.
* @param {Object} relatedKeyword The related keyword object.
* @param {string} relatedKeyword.keyword The keyword.
* @param {string} relatedKeyword.synonyms The synonyms.
*
* @returns {boolean} True if there are changes detected.
*/
shouldSeoUpdate(key, {
keyword,
synonyms
}) {
if ((0, _lodash.isUndefined)(this._relatedKeywords[key])) {
return true;
}
if (this._relatedKeywords[key].keyword !== keyword) {
return true;
}
return this._relatedKeywords[key].synonyms !== synonyms;
}
/**
* Checks whether the additional assessor should be updated.
*
* @param {Paper} paper The paper to check.
* @returns {Object} An object containing the information whether each additional assessor needs to be updated.
*/
shouldAdditionalAssessorsUpdate(paper) {
const shouldCustomAssessorsUpdate = {};
Object.keys(this.additionalAssessors).forEach(assessorName => {
shouldCustomAssessorsUpdate[assessorName] = this.additionalAssessors[assessorName].shouldUpdate(this._paper, paper);
});
return shouldCustomAssessorsUpdate;
}
/**
* Updates the results for the additional assessor.
*
* @param {Object} shouldCustomAssessorsUpdate Whether the results of the additional assessor should be updated.
* @returns {void}
*/
updateAdditionalAssessors(shouldCustomAssessorsUpdate) {
Object.keys(this.additionalAssessors).forEach(assessorName => {
const {
assessor
} = this.additionalAssessors[assessorName];
if (!this._results[assessorName] || shouldCustomAssessorsUpdate[assessorName]) {
assessor.assess(this._paper);
this._results[assessorName] = {
results: assessor.results,
score: assessor.calculateOverallScore()
};
}
});
}
/**
* Runs analyses on a paper.
*
* The paper includes the keyword and synonyms data. However, this is
* possibly just one instance of these. From here we are going to split up
* this data and keep track of the different sets of keyword-synonyms and
* their results.
*
* @param {number} id The request id.
* @param {Object} payload The payload object.
* @param {Paper} payload.paper The paper to analyze.
* @param {Object} [payload.relatedKeywords] The related keywords.
*
* @returns {Object} The result, may not contain readability or seo.
*/
async analyze(id, {
paper,
relatedKeywords = {}
}) {
const paperHasChanges = this._paper === null || !this._paper.equals(paper);
const shouldReadabilityUpdate = this.shouldReadabilityUpdate(paper);
const shouldInclusiveLanguageUpdate = this.shouldInclusiveLanguageUpdate(paper);
const shouldCustomAssessorsUpdate = this.shouldAdditionalAssessorsUpdate(paper);
// Only set the paper and build the tree if the paper has any changes.
if (paperHasChanges) {
this._paper = paper;
this._researcher.setPaper(this._paper);
const languageProcessor = new _LanguageProcessor.default(this._researcher);
const shortcodes = this._paper._attributes && this._paper._attributes.shortcodes;
this._paper.setTree((0, _build.build)(this._paper, languageProcessor, shortcodes));
// Update the configuration locale to the paper locale.
this.setLocale(this._paper.getLocale());
}
if (this._configuration.keywordAnalysisActive && this._seoAssessor) {
// Only assess the focus keyphrase if the paper has any changes.
if (paperHasChanges) {
// Assess the SEO of the content regarding the main keyphrase.
this._results.seo[""] = await this.assess(this._paper, this._seoAssessor);
}
// Only assess the related keyphrases when they have been given.
if (!(0, _lodash.isEmpty)(relatedKeywords)) {
// Get the related keyphrase keys (one for each keyphrase).
const requestedRelatedKeywordKeys = Object.keys(relatedKeywords);
// Analyze the SEO for each related keyphrase and wait for the results.
const relatedKeyphraseResults = await this.assessRelatedKeywords(paper, relatedKeywords);
// Put the related keyphrase results on the SEO results, under the right key.
relatedKeyphraseResults.forEach(result => {
this._results.seo[result.key] = result.results;
});
// Clear the results of unrequested related keyphrases, but only if there are requested related keyphrases.
if (requestedRelatedKeywordKeys.length > 1) {
this._results.seo = (0, _lodash.pickBy)(this._results.seo, (relatedKeyword, key) => (0, _lodash.includes)(requestedRelatedKeywordKeys, key) || key === "");
}
}
}
if (this._configuration.contentAnalysisActive && this._contentAssessor && shouldReadabilityUpdate) {
// Set the locale (we are more lenient for languages that have full analysis support).
this._contentAssessor.getScoreAggregator().setLocale(this._configuration.locale);
this._results.readability = await this.assess(this._paper, this._contentAssessor);
}
this.updateInclusiveLanguageAssessor(shouldInclusiveLanguageUpdate);
this.updateAdditionalAssessors(shouldCustomAssessorsUpdate);
return this._results;
}
/**
* Assesses a given paper
* using an original Assessor (that works on a string representation of the text).
*
* The results of both analyses are combined using the given score aggregator.
*
* @param {Paper} paper The paper to analyze.
* @param {Assessor} assessor The original assessor.
*
* @returns {Promise<{score: number, results: AssessmentResult[]}>} The analysis results.
*/
async assess(paper, assessor) {
/*
* Assess the paper using the original assessor.
*/
assessor.assess(paper);
const results = assessor.results;
// Aggregate the results.
const score = assessor.getScoreAggregator().aggregate(results);
return {
results: results,
score: score
};
}
/**
* Assesses the SEO of a paper on the given related keyphrases and their synonyms.
*
* The old assessor is used and their results are combined.
*
* @param {Paper} paper The paper to analyze.
* @param {Object} relatedKeywords The related keyphrases to use in the analysis.
*
* @returns {Promise<[{results: {score: number, results: AssessmentResult[]}, key: string}]>} The results, one for each keyphrase.
*/
async assessRelatedKeywords(paper, relatedKeywords) {
const keywordKeys = Object.keys(relatedKeywords);
return await Promise.all(keywordKeys.map(key => {
this._relatedKeywords[key] = relatedKeywords[key];
const relatedPaper = _Paper.default.parse({
...paper.serialize(),
keyword: this._relatedKeywords[key].keyword,
synonyms: this._relatedKeywords[key].synonyms
});
// We need to remember the key, since the SEO results are stored in an object, not an array.
return this.assess(relatedPaper, this._relatedKeywordAssessor).then(results => ({
key: key,
results: results
}));
}));
}
/**
* Loads a new script from an external source.
*
* @param {number} id The request id.
* @param {string} url The url of the script to load;
*
* @returns {Object} An object containing whether the url was loaded, the url and possibly an error message.
*/
loadScript(id, {
url
}) {
if ((0, _lodash.isUndefined)(url)) {
return {
loaded: false,
url,
message: "Load Script was called without an URL."
};
}
try {
this._scope.importScripts(url);
} catch (error) {
return {
loaded: false,
url,
message: error.message
};
}
return {
loaded: true,
url
};
}
/**
* Sends the load script result back.
*
* @param {number} id The request id.
* @param {Object} result The result.
*
* @returns {void}
*/
loadScriptDone(id, result) {
if (!result.loaded) {
this.send("loadScript:failed", id, result);
return;
}
this.send("loadScript:done", id, result);
}
/**
* Sends the analyze result back.
*
* @param {number} id The request id.
* @param {Object} result The result.
*
* @returns {void}
*/
analyzeDone(id, result) {
if (result.error) {
this.send("analyze:failed", id, result);
return;
}
this.send("analyze:done", id, result);
}
/**
* Sends the analyze related keywords result back.
*
* @param {number} id The request id.
* @param {Object} result The result.
*
* @returns {void}
*/
analyzeRelatedKeywordsDone(id, result) {
if (result.error) {
this.send("analyzeRelatedKeywords:failed", id, result);
return;
}
this.send("analyzeRelatedKeywords:done", id, result);
}
/**
* Handle a custom message using the registered handler.
*
* @param {number} id The request id.
* @param {string} name The name of the message.
* @param {Object} data The data of the message.
*
* @returns {Object} An object containing either success and data or an error.
*/
customMessage(id, {
name,
data
}) {
try {
return {
success: true,
data: this._registeredMessageHandlers[name](data)
};
} catch (error) {
return {
error
};
}
}
/**
* Send the result of a custom message back.
*
* @param {number} id The request id.
* @param {Object} result The result.
*
* @returns {void}
*/
customMessageDone(id, result) {
if (result.success) {
this.send("customMessage:done", id, result.data);
return;
}
this.send("customMessage:failed", result.error);
}
/**
* Registers custom research to the researcher.
*
* @param {string} name The name of the research.
* @param {function} research The research function to add.
*
* @returns {void}
*/
registerResearch(name, research) {
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to register the custom research. Expected parameter `name` to be a string.");
}
if (!(0, _lodash.isObject)(research)) {
throw new _invalidType.default("Failed to register the custom research. Expected parameter `research` to be a function.");
}
const researcher = this._researcher;
if (!researcher.hasResearch(name)) {
researcher.addResearch(name, research);
}
}
/**
* Runs the specified research in the worker. Optionally pass a paper.
*
* @param {number} id The request id.
* @param {string} name The name of the research to run.
* @param {Paper} [paper] The paper to run the research on if it shouldn't
* be run on the latest paper.
*
* @returns {Object} The result of the research.
*/
runResearch(id, {
name,
paper = null
}) {
// Save morphology data if it is available in the current researcher.
const morphologyData = this._researcher.getData("morphology");
const researcher = this._researcher;
// When a specific paper is passed we create a temporary new researcher.
if (paper !== null) {
researcher.setPaper(paper);
researcher.addResearchData("morphology", morphologyData);
// Build and set the tree if it's not been set before.
if (paper.getTree() === null) {
const languageProcessor = new _LanguageProcessor.default(researcher);
const shortcodes = paper._attributes && paper._attributes.shortcodes;
paper.setTree((0, _build.build)(paper, languageProcessor, shortcodes));
}
}
return researcher.getResearch(name);
}
/**
* Send the result of a custom message back.
*
* @param {number} id The request id.
* @param {Object} result The result.
*
* @returns {void}
*/
runResearchDone(id, result) {
if (result.error) {
this.send("runResearch:failed", id, result);
return;
}
this.send("runResearch:done", id, result);
}
/**
* Registers a custom helper to the researcher.
*
* @param {string} name The name of the helper.
* @param {function} helper The helper function to add.
*
* @returns {void}
*/
registerHelper(name, helper) {
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to register the custom helper. Expected parameter `name` to be a string.");
}
if (!(0, _lodash.isObject)(helper)) {
throw new _invalidType.default("Failed to register the custom helper. Expected parameter `helper` to be a function.");
}
const researcher = this._researcher;
if (!researcher.hasHelper(name)) {
researcher.addHelper(name, helper);
}
}
/**
* Registers a configuration to the researcher.
*
* @param {string} name The name of the researcher configuration.
* @param {*} researcherConfig The researcher configuration to add.
*
* @returns {void}
*/
registerResearcherConfig(name, researcherConfig) {
if (!(0, _lodash.isString)(name)) {
throw new _invalidType.default("Failed to register the custom researcher config. Expected parameter `name` to be a string.");
}
if ((0, _lodash.isUndefined)(researcherConfig) || (0, _lodash.isEmpty)(researcherConfig)) {
throw new _missingArgument.default("Failed to register the custom researcher config. Expected parameter `researcherConfig` to be defined.");
}
const researcher = this._researcher;
if (!researcher.hasConfig(name)) {
researcher.addConfig(name, researcherConfig);
}
}
}
exports.default = AnalysisWebWorker;
//# sourceMappingURL=AnalysisWebWorker.js.map