yoastseo
Version:
Yoast client-side content analysis
773 lines (719 loc) • 26.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _i18n = require("@wordpress/i18n");
var _lodash = require("lodash");
var _AssessorPresenter = _interopRequireDefault(require("./scoring/renderers/AssessorPresenter.js"));
var _helpers = require("./helpers");
var _missingArgument = _interopRequireDefault(require("./errors/missingArgument.js"));
var _Paper = _interopRequireDefault(require("./values/Paper.js"));
var _pluggable = _interopRequireDefault(require("./pluggable.js"));
var _htmlParser = _interopRequireDefault(require("./languageProcessing/helpers/html/htmlParser.js"));
var _contentAssessor = _interopRequireDefault(require("./scoring/assessors/contentAssessor.js"));
var _contentAssessor2 = _interopRequireDefault(require("./scoring/assessors/cornerstone/contentAssessor.js"));
var _seoAssessor = _interopRequireDefault(require("./scoring/assessors/cornerstone/seoAssessor.js"));
var _seoAssessor2 = _interopRequireDefault(require("./scoring/assessors/seoAssessor.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
// Assessors.
const inputDebounceDelay = 800;
/**
* Default config for YoastSEO.js
*
* @type {Object}
*/
const defaults = {
callbacks: {
bindElementEvents: _lodash.noop,
updateSnippetValues: _lodash.noop,
saveScores: _lodash.noop,
saveContentScore: _lodash.noop,
updatedContentResults: _lodash.noop,
updatedKeywordsResults: _lodash.noop
},
sampleText: {
baseUrl: "example.org/",
snippetCite: "example-post/",
title: "",
keyword: "Choose a focus keyword",
meta: "",
text: "Start writing your text!"
},
queue: ["wordCount", "keywordDensity", "subHeadings", "stopwords", "fleschReading", "linkCount", "imageCount", "slugKeyword", "urlLength", "metaDescription", "pageTitleKeyword", "pageTitleWidth", "firstParagraph", "'keywordDoubles"],
typeDelay: 3000,
typeDelayStep: 1500,
maxTypeDelay: 5000,
dynamicDelay: true,
locale: "en_US",
translations: {
domain: "wordpress-seo",
// eslint-disable-next-line camelcase
locale_data: {
"wordpress-seo": {
"": {}
}
}
},
replaceTarget: [],
resetTarget: [],
elementTarget: [],
marker: _lodash.noop,
keywordAnalysisActive: true,
contentAnalysisActive: true,
debounceRefresh: true
};
/**
* Check arguments passed to the App to check if all necessary arguments are set.
*
* @private
* @param {Object} args The arguments object passed to the App.
* @returns {void}
*/
function verifyArguments(args) {
if (!(0, _lodash.isObject)(args.callbacks.getData)) {
throw new _missingArgument.default("The app requires an object with a getdata callback.");
}
if (!(0, _lodash.isObject)(args.targets)) {
throw new _missingArgument.default("`targets` is a required App argument, `targets` is not an object.");
}
}
/**
* This should return an object with the given properties.
*
* @callback YoastSEO.App~getData
* @returns {Object} data. The data object containing the following properties: keyword, meta, text, metaTitle, title, url, excerpt.
* @returns {String} data.keyword The keyword that should be used.
* @returns {String} data.meta The meta description to analyze.
* @returns {String} data.text The text to analyze.
* @returns {String} data.metaTitle The text in the HTML title tag.
* @returns {String} data.title The title to analyze.
* @returns {String} data.url The URL for the given page
* @returns {String} data.excerpt Excerpt for the pages
*/
/**
* @callback YoastSEO.App~getAnalyzerInput
*
* @returns {Array} An array containing the analyzer queue.
*/
/**
* @callback YoastSEO.App~bindElementEvents
*
* @param {YoastSEO.App} app A reference to the YoastSEO.App from where this is called.
*/
/**
* @callback YoastSEO.App~updateSnippetValues
*
* @param {Object} ev The event emitted from the DOM.
*/
/**
* @callback YoastSEO.App~saveScores
*
* @param {int} score The overall keyword score as determined by the assessor.
* @param {AssessorPresenter} assessorPresenter The assessor presenter that will be used to render the keyword score.
*/
/**
* @callback YoastSEO.App~saveContentScore
*
* @param {int} score The overall content score as determined by the assessor.
* @param {AssessorPresenter} assessorPresenter The assessor presenter that will be used to render the content score.
*/
/**
* @callback YoastSEO.App~updatedContentResults
*
* @param {Object[]} result The updated content analysis results.
* @param {number} result[].score The SEO score.
* @param {string} result[].rating String representation of the SEO score.
* @param {string} result[].text Textual explanation of the score.
* @param {number} overallContentScore The overall content SEO score.
*/
/**
* @callback YoastSEO.App~updatedKeywordsResults
*
* @param {Object[]} result The updated keywords analysis results.
* @param {number} result[].score The SEO score.
* @param {string} result[].rating String representation of the SEO score.
* @param {string} result[].text Textual explanation of the score.
* @param {number} overallContentScore The overall keywords SEO score.
*/
/**
* Represents the main YoastSEO App.
*/
class App {
/**
* Loader for the analyzer, loads the eventbinder and the elementdefiner
*
* @param {Object} args The arguments passed to the loader.
* @param {Object} args.translations Jed compatible translations.
* @param {Object} args.targets Targets to retrieve or set on.
* @param {String} args.targets.snippet ID for the snippet preview element.
* @param {String} args.targets.output ID for the element to put the output of the analyzer in.
* @param {int} args.typeDelay Number of milliseconds to wait between typing to refresh the analyzer output.
* @param {boolean} args.dynamicDelay Whether to enable dynamic delay, will ignore type delay if the analyzer takes a long time. Applicable on slow devices.
* @param {int} args.maxTypeDelay The maximum amount of type delay even if dynamic delay is on.
* @param {int} args.typeDelayStep The amount with which to increase the typeDelay on each step when dynamic delay is enabled.
* @param {Object} args.callbacks The callbacks that the app requires.
* @param {Object} args.assessor The Assessor to use instead of the default assessor.
* @param {YoastSEO.App~getData} args.callbacks.getData Called to retrieve input data
* @param {YoastSEO.App~getAnalyzerInput} args.callbacks.getAnalyzerInput Called to retrieve input for the analyzer.
* @param {YoastSEO.App~bindElementEvents} args.callbacks.bindElementEvents Called to bind events to the DOM elements.
* @param {YoastSEO.App~updateSnippetValues} args.callbacks.updateSnippetValues Called when the snippet values need to be updated.
* @param {YoastSEO.App~saveScores} args.callbacks.saveScores Called when the score has been determined by the analyzer.
* @param {YoastSEO.App~saveContentScore} args.callback.saveContentScore Called when the content score has been determined by the assessor.
* @param {YoastSEO.App~updatedContentResults} args.callbacks.updatedContentResults Called when the score has been determined by the analyzer.
* @param {YoastSEO.App~updatedKeywordsResults} args.callback.updatedKeywordsResults Called when the content score has been determined by the assessor.
* @param {Function} args.callbacks.saveSnippetData Function called when the snippet data is changed.
* @param {Function} args.marker The marker to use to apply the list of marks retrieved from an assessment.
*
* @param {boolean} [args.debouncedRefresh=true] Whether or not to debounce the refresh function. Defaults to true.
* @param {Researcher} args.researcher The Researcher object to be used.
*
* @constructor
*/
constructor(args) {
if (!(0, _lodash.isObject)(args)) {
args = {};
}
(0, _lodash.defaultsDeep)(args, defaults);
verifyArguments(args);
this.config = args;
if (args.debouncedRefresh === true) {
this.refresh = (0, _lodash.debounce)(this.refresh.bind(this), inputDebounceDelay);
}
this._pureRefresh = (0, _lodash.throttle)(this._pureRefresh.bind(this), this.config.typeDelay);
this.callbacks = this.config.callbacks;
this.researcher = this.config.researcher;
(0, _i18n.setLocaleData)(this.config.translations.locale_data["wordpress-seo"], "wordpress-seo");
this.initializeAssessors(args);
this.pluggable = new _pluggable.default(this);
this.getData();
this.defaultOutputElement = this.getDefaultOutputElement(args);
if (this.defaultOutputElement !== "") {
this.showLoadingDialog();
}
this._assessorOptions = {
useCornerStone: false
};
this.initAssessorPresenters();
}
/**
* Returns the default output element based on which analyses are active.
*
* @param {Object} args The arguments passed to the App.
* @returns {string} The ID of the target that is active.
*/
getDefaultOutputElement(args) {
if (args.keywordAnalysisActive) {
return args.targets.output;
}
if (args.contentAnalysisActive) {
return args.targets.contentOutput;
}
return "";
}
/**
* Sets the assessors based on the assessor options and refreshes them.
*
* @param {Object} assessorOptions The specific options.
* @returns {void}
*/
changeAssessorOptions(assessorOptions) {
this._assessorOptions = (0, _lodash.merge)(this._assessorOptions, assessorOptions);
// Set the assessors based on the new assessor options.
this.seoAssessor = this.getSeoAssessor();
this.contentAssessor = this.getContentAssessor();
// Refresh everything so the user sees the changes.
this.initAssessorPresenters();
this.refresh();
}
/**
* Returns an instance of the seo assessor to use.
*
* @returns {Assessor} The assessor instance.
*/
getSeoAssessor() {
const {
useCornerStone
} = this._assessorOptions;
return useCornerStone ? this.cornerStoneSeoAssessor : this.defaultSeoAssessor;
}
/**
* Returns an instance of the content assessor to use.
*
* @returns {Assessor} The assessor instance.
*/
getContentAssessor() {
const {
useCornerStone
} = this._assessorOptions;
return useCornerStone ? this.cornerStoneContentAssessor : this.defaultContentAssessor;
}
/**
* Initializes assessors based on whether the respective analysis is active.
*
* @param {Object} args The arguments passed to the App.
* @returns {void}
*/
initializeAssessors(args) {
this.initializeSEOAssessor(args);
this.initializeContentAssessor(args);
}
/**
* Initializes the SEO assessor.
*
* @param {Object} args The arguments passed to the App.
* @returns {void}
*/
initializeSEOAssessor(args) {
if (!args.keywordAnalysisActive) {
return;
}
this.defaultSeoAssessor = new _seoAssessor2.default(this.researcher, {
marker: this.config.marker
});
this.cornerStoneSeoAssessor = new _seoAssessor.default(this.researcher, {
marker: this.config.marker
});
// Set the assessor
if ((0, _lodash.isUndefined)(args.seoAssessor)) {
this.seoAssessor = this.defaultSeoAssessor;
} else {
this.seoAssessor = args.seoAssessor;
}
}
/**
* Initializes the content assessor.
*
* @param {Object} args The arguments passed to the App.
* @returns {void}
*/
initializeContentAssessor(args) {
if (!args.contentAnalysisActive) {
return;
}
this.defaultContentAssessor = new _contentAssessor.default(this.researcher, {
marker: this.config.marker,
locale: this.config.locale
});
this.cornerStoneContentAssessor = new _contentAssessor2.default(this.researcher, {
marker: this.config.marker,
locale: this.config.locale
});
// Set the content assessor
if ((0, _lodash.isUndefined)(args._contentAssessor)) {
this.contentAssessor = this.defaultContentAssessor;
} else {
this.contentAssessor = args._contentAssessor;
}
}
/**
* Extends the config with defaults.
*
* @param {Object} args The arguments to be extended.
*
* @returns {Object} The extended arguments.
*/
extendConfig(args) {
args.sampleText = this.extendSampleText(args.sampleText);
args.locale = args.locale || "en_US";
return args;
}
/**
* Extends sample text config with defaults.
*
* @param {Object} sampleText The sample text to be extended.
* @returns {Object} The extended sample text.
*/
extendSampleText(sampleText) {
const defaultSampleText = defaults.sampleText;
if ((0, _lodash.isUndefined)(sampleText)) {
return defaultSampleText;
}
for (const key in sampleText) {
if ((0, _lodash.isUndefined)(sampleText[key])) {
sampleText[key] = defaultSampleText[key];
}
}
return sampleText;
}
/**
* Registers a custom data callback.
*
* @param {Function} callback The callback to register.
*
* @returns {void}
*/
registerCustomDataCallback(callback) {
if (!this.callbacks.custom) {
this.callbacks.custom = [];
}
if ((0, _lodash.isFunction)(callback)) {
this.callbacks.custom.push(callback);
}
}
/**
* Retrieves data from the callbacks.getData and applies modification to store these in this.rawData.
*
* @returns {void}
*/
getData() {
this.rawData = this.callbacks.getData();
// Add the custom data to the raw data.
if ((0, _lodash.isArray)(this.callbacks.custom)) {
this.callbacks.custom.forEach(customCallback => {
const customData = customCallback();
this.rawData = (0, _lodash.merge)(this.rawData, customData);
});
}
if (this.pluggable.loaded) {
this.rawData.metaTitle = this.pluggable._applyModifications("data_page_title", this.rawData.metaTitle);
this.rawData.meta = this.pluggable._applyModifications("data_meta_desc", this.rawData.meta);
}
this.rawData.titleWidth = (0, _helpers.measureTextWidth)(this.rawData.metaTitle);
this.rawData.locale = this.config.locale;
}
/**
* Refreshes the analyzer and output of the analyzer, is debounced for a better experience.
*
* @returns {void}
*/
refresh() {
// Until all plugins are loaded, do not trigger a refresh.
if (!this.pluggable.loaded) {
return;
}
this._pureRefresh();
}
/**
* Refreshes the analyzer and output of the analyzer, is throttled to prevent performance issues.
*
* @returns {void}
*
* @private
*/
_pureRefresh() {
this.getData();
this.runAnalyzer();
}
/**
* Initializes the assessor presenters for content and SEO analysis.
*
* @returns {void}
*/
initAssessorPresenters() {
// Pass the assessor result through to the formatter.
if (!(0, _lodash.isUndefined)(this.config.targets.output)) {
this.seoAssessorPresenter = new _AssessorPresenter.default({
targets: {
output: this.config.targets.output
},
assessor: this.seoAssessor
});
}
if (!(0, _lodash.isUndefined)(this.config.targets.contentOutput)) {
// Pass the assessor result through to the formatter.
this.contentAssessorPresenter = new _AssessorPresenter.default({
targets: {
output: this.config.targets.contentOutput
},
assessor: this.contentAssessor
});
}
}
/**
* Sets the startTime timestamp.
*
* @returns {void}
*/
startTime() {
this.startTimestamp = new Date().getTime();
}
/**
* Sets the endTime timestamp and compares with startTime to determine typeDelayincrease.
*
* @returns {void}
*/
endTime() {
this.endTimestamp = new Date().getTime();
if (this.endTimestamp - this.startTimestamp > this.config.typeDelay) {
if (this.config.typeDelay < this.config.maxTypeDelay - this.config.typeDelayStep) {
this.config.typeDelay += this.config.typeDelayStep;
}
}
}
/**
* Inits a new pageAnalyzer with the inputs from the getInput function and calls the scoreFormatter to format outputs.
*
* @returns {void}
*/
runAnalyzer() {
if (this.pluggable.loaded === false) {
return;
}
if (this.config.dynamicDelay) {
this.startTime();
}
this.analyzerData = this.modifyData(this.rawData);
let text = this.analyzerData.text;
// Insert HTML stripping code.
text = (0, _htmlParser.default)(text);
const titleWidth = this.analyzerData.titleWidth;
// Create a paper object for the Researcher.
this.paper = new _Paper.default(text, {
keyword: this.analyzerData.keyword,
synonyms: this.analyzerData.synonyms,
description: this.analyzerData.meta,
slug: this.analyzerData.slug,
title: this.analyzerData.metaTitle,
titleWidth: titleWidth,
locale: this.config.locale,
permalink: this.analyzerData.permalink
});
this.config.researcher.setPaper(this.paper);
this.runKeywordAnalysis();
this.runContentAnalysis();
this._renderAnalysisResults();
if (this.config.dynamicDelay) {
this.endTime();
}
}
/**
* Runs the keyword analysis and calls the appropriate callbacks.
*
* @returns {void}
*/
runKeywordAnalysis() {
if (this.config.keywordAnalysisActive) {
this.seoAssessor.assess(this.paper);
const overallSeoScore = this.seoAssessor.calculateOverallScore();
if (!(0, _lodash.isUndefined)(this.callbacks.updatedKeywordsResults)) {
this.callbacks.updatedKeywordsResults(this.seoAssessor.results, overallSeoScore);
}
if (!(0, _lodash.isUndefined)(this.callbacks.saveScores)) {
this.callbacks.saveScores(overallSeoScore, this.seoAssessorPresenter);
}
}
}
/**
* Runs the content analysis and calls the appropriate callbacks.
*
* @returns {void}
*/
runContentAnalysis() {
if (this.config.contentAnalysisActive) {
this.contentAssessor.assess(this.paper);
const overallContentScore = this.contentAssessor.calculateOverallScore();
if (!(0, _lodash.isUndefined)(this.callbacks.updatedContentResults)) {
this.callbacks.updatedContentResults(this.contentAssessor.results, overallContentScore);
}
if (!(0, _lodash.isUndefined)(this.callbacks.saveContentScore)) {
this.callbacks.saveContentScore(overallContentScore, this.contentAssessorPresenter);
}
}
}
/**
* Modifies the data with plugins before it is sent to the analyzer.
*
* @param {Object} data The data to be modified.
* @returns {Object} The data with the applied modifications.
*/
modifyData(data) {
// Copy rawdata to lose object reference.
data = JSON.parse(JSON.stringify(data));
data.text = this.pluggable._applyModifications("content", data.text);
data.metaTitle = this.pluggable._applyModifications("title", data.metaTitle);
return data;
}
/**
* Removes the loading dialog and fires the analyzer when all plugins are loaded.
*
* @returns {void}
*/
pluginsLoaded() {
this.removeLoadingDialog();
this.refresh();
}
/**
* Shows the loading dialog which shows the loading of the plugins.
*
* @returns {void}
*/
showLoadingDialog() {
const outputElement = document.getElementById(this.defaultOutputElement);
if (this.defaultOutputElement !== "" && !(0, _lodash.isEmpty)(outputElement)) {
const dialogDiv = document.createElement("div");
dialogDiv.className = "YoastSEO_msg";
dialogDiv.id = "YoastSEO-plugin-loading";
document.getElementById(this.defaultOutputElement).appendChild(dialogDiv);
}
}
/**
* Updates the loading plugins. Uses the plugins as arguments to show which plugins are loading.
*
* @param {Object} plugins The plugins to be parsed into the dialog.
* @returns {void}
*/
updateLoadingDialog(plugins) {
const outputElement = document.getElementById(this.defaultOutputElement);
if (this.defaultOutputElement === "" || (0, _lodash.isEmpty)(outputElement)) {
return;
}
const dialog = document.getElementById("YoastSEO-plugin-loading");
dialog.textContent = "";
(0, _lodash.forEach)(plugins, function (plugin, pluginName) {
dialog.innerHTML += "<span class=left>" + pluginName + "</span><span class=right " + plugin.status + ">" + plugin.status + "</span><br />";
});
dialog.innerHTML += "<span class=bufferbar></span>";
}
/**
* Removes the plugin load dialog.
*
* @returns {void}
*/
removeLoadingDialog() {
const outputElement = document.getElementById(this.defaultOutputElement);
const loadingDialog = document.getElementById("YoastSEO-plugin-loading");
if (this.defaultOutputElement !== "" && !(0, _lodash.isEmpty)(outputElement) && !(0, _lodash.isEmpty)(loadingDialog)) {
document.getElementById(this.defaultOutputElement).removeChild(document.getElementById("YoastSEO-plugin-loading"));
}
}
// ***** PLUGGABLE PUBLIC DSL ***** //
/**
* Delegates to `YoastSEO.app.pluggable.registerPlugin`
*
* @param {string} pluginName The name of the plugin to be registered.
* @param {object} options The options object.
* @param {string} options.status The status of the plugin being registered. Can either be "loading" or "ready".
* @returns {boolean} Whether or not it was successfully registered.
*/
registerPlugin(pluginName, options) {
return this.pluggable._registerPlugin(pluginName, options);
}
/**
* Delegates to `YoastSEO.app.pluggable.ready`
*
* @param {string} pluginName The name of the plugin to check.
* @returns {boolean} Whether or not the plugin is ready.
*/
pluginReady(pluginName) {
return this.pluggable._ready(pluginName);
}
/**
* Delegates to `YoastSEO.app.pluggable.reloaded`
*
* @param {string} pluginName The name of the plugin to reload
* @returns {boolean} Whether or not the plugin was reloaded.
*/
pluginReloaded(pluginName) {
return this.pluggable._reloaded(pluginName);
}
/**
* Delegates to `YoastSEO.app.pluggable.registerModification`.
*
* @param {string} modification The name of the filter.
* @param {function} callable The callable function.
* @param {string} pluginName The plugin that is registering the modification.
* @param {number} [priority] 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 the modification was successfully registered.
*/
registerModification(modification, callable, pluginName, priority) {
return this.pluggable._registerModification(modification, callable, pluginName, priority);
}
/**
* Registers a custom assessment for use in the analyzer, this will result in a new line in the analyzer results.
* The function needs to use the assessment result to return a result based on the contents of the page/posts.
*
* Score 0 results in a grey circle if it is not explicitly set by using setscore
* Scores 0, 1, 2, 3 and 4 result in a red circle
* Scores 6 and 7 result in a yellow circle
* Scores 8, 9 and 10 result in a green circle
*
* @param {string} name Name of the test.
* @param {function} assessment The assessment to run.
* @param {string} pluginName The plugin that is registering the test.
* @returns {boolean} Whether or not the test was successfully registered.
*/
registerAssessment(name, assessment, pluginName) {
if (!(0, _lodash.isUndefined)(this.seoAssessor)) {
return this.pluggable._registerAssessment(this.defaultSeoAssessor, name, assessment, pluginName) && this.pluggable._registerAssessment(this.cornerStoneSeoAssessor, name, assessment, pluginName);
}
}
/**
* Disables markers visually in the UI.
*
* @returns {void}
*/
disableMarkers() {
if (!(0, _lodash.isUndefined)(this.seoAssessorPresenter)) {
this.seoAssessorPresenter.disableMarker();
}
if (!(0, _lodash.isUndefined)(this.contentAssessorPresenter)) {
this.contentAssessorPresenter.disableMarker();
}
}
/**
* Renders the content and keyword analysis results.
*
* @returns {void}
*/
_renderAnalysisResults() {
if (this.config.contentAnalysisActive && !(0, _lodash.isUndefined)(this.contentAssessorPresenter)) {
this.contentAssessorPresenter.renderIndividualRatings();
}
if (this.config.keywordAnalysisActive && !(0, _lodash.isUndefined)(this.seoAssessorPresenter)) {
this.seoAssessorPresenter.setKeyword(this.paper.getKeyword());
this.seoAssessorPresenter.render();
}
}
// Deprecated functions
/**
* The analyzeTimer calls the checkInputs function with a delay, so the function won't be executed
* at every keystroke checks the reference object, so this function can be called from anywhere,
* without problems with different scopes.
*
* @deprecated: 1.3 - Use this.refresh() instead.
*
* @returns {void}
*/
analyzeTimer() {
this.refresh();
}
/**
* Registers a custom test for use in the analyzer, this will result in a new line in the analyzer results. The function
* has to return a result based on the contents of the page/posts.
*
* The scoring object is a special object with definitions about how to translate a result from your analysis function
* to a SEO score.
*
* Negative scores result in a red circle
* Scores 1, 2, 3, 4 and 5 result in an orange circle
* Scores 6 and 7 result in a yellow circle
* Scores 8, 9 and 10 result in a red circle
*
* @returns {void}
*
* @deprecated since version 1.2
*/
registerTest() {
console.error("This function is deprecated, please use registerAssessment");
}
/**
* Switches between the cornerstone and default assessors.
*
* @deprecated 1.35.0 - Use changeAssessorOption instead.
*
* @param {boolean} useCornerStone True when cornerstone should be used.
*
* @returns {void}
*/
switchAssessors(useCornerStone) {
console.warn("Switch assessor is deprecated since YoastSEO.js version 1.35.0");
this.changeAssessorOptions({
useCornerStone
});
}
}
var _default = exports.default = App;
//# sourceMappingURL=app.js.map