UNPKG

yoastseo

Version:

Yoast client-side content analysis

773 lines (719 loc) 26.6 kB
"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