UNPKG

yoastseo

Version:

Yoast client-side content analysis

398 lines (378 loc) 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.KeywordDensityAssessment = exports.KeyphraseDensityAssessment = void 0; var _i18n = require("@wordpress/i18n"); var _lodash = require("lodash"); var _recommendedKeywordCount = _interopRequireDefault(require("../../helpers/assessments/recommendedKeywordCount.js")); var _assessment = _interopRequireDefault(require("../assessment")); var _AssessmentResult = _interopRequireDefault(require("../../../values/AssessmentResult")); var _inRange = require("../../helpers/assessments/inRange"); var _helpers = require("../../../helpers"); var _keyphraseLengthFactor = _interopRequireDefault(require("../../helpers/assessments/keyphraseLengthFactor.js")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * @typedef {import("../../../languageProcessing/AbstractResearcher").default } Researcher * @typedef {import("../../../values/").Paper } Paper * @typedef {import("../../../values/Mark").default } Mark */ /** * @typedef {Object} KeyphraseDensityConfig * @property {Object} parameters The parameters to use. * If word forms are not available: * @property {Object} parameters.noWordForms The parameters to use when no morphological forms are available. * @property {number} parameters.noWordForms.overMaximum The percentage of keyphrase instances in the text that * is way over the maximum. * @property {number} parameters.noWordForms.maximum The maximum percentage of keyphrase instances in the text. * @property {number} parameters.noWordForms.minimum The minimum percentage of keyphrase instances in the text. * If word forms are available: * @property {Object} parameters.multipleWordForms The parameters to use when morphological forms are available. * @property {number} parameters.multipleWordForms.overMaximum The percentage of keyphrase instances in the text that * is way over the maximum. * @property {number} parameters.multipleWordForms.maximum The maximum percentage of keyphrase instances in the text. * @property {number} parameters.multipleWordForms.minimum The minimum percentage of keyphrase instances in the text. * @property {Object} scores The scores to use. * @property {number} scores.wayOverMaximum The score to return if there are way too many instances of keyphrase in the text. * @property {number} scores.overMaximum The score to return if there are too many instances of keyphrase in the text. * @property {number} scores.correctDensity The score to return if there is a good number of keyphrase instances in the text. * @property {number} scores.underMinimum The score to return if there are not enough keyphrase instances in the text. * @property {number} scores.noKeyphraseOrText The score to return if there is no text or no keyphrase set. * @property {string} urlTitle The URL to the Yoast article about keyphrase density. * @property {string} urlCallToAction The URL to the Yoast article about keyphrase density. */ /** * Represents the assessment that will assess if the keyphrase density is within the recommended range. */ class KeyphraseDensityAssessment extends _assessment.default { /** * Sets the identifier and the config. * @param {Object} [config={}] The configuration to use. */ constructor(config = {}) { super(); /** * The default configuration. * @type KeyphraseDensityConfig */ const defaultConfig = { parameters: { noWordForms: { overMaximum: 4, maximum: 3, minimum: 0.5 }, multipleWordForms: { overMaximum: 4, maximum: 3.5, minimum: 0.5 } }, scores: { wayOverMaximum: -50, overMaximum: -10, correctDensity: 9, underMinimum: 4, noKeyphraseOrText: -50 }, urlTitle: (0, _helpers.createAnchorOpeningTag)("https://yoa.st/33v"), urlCallToAction: (0, _helpers.createAnchorOpeningTag)("https://yoa.st/33w") }; this.identifier = "keyphraseDensity"; this._config = (0, _lodash.merge)(defaultConfig, config); } /** * Determines correct boundaries depending on the availability of morphological forms. * * @param {number} keyphraseLength The length of the keyphrase in words. * @param {number} textLength The length of the text in words. * @returns {void} */ setBoundaries(keyphraseLength, textLength) { this._boundaries = this._config.parameters.noWordForms; if (this._hasMorphologicalForms) { this._boundaries = this._config.parameters.multipleWordForms; } this._minRecommendedKeyphraseCount = (0, _recommendedKeywordCount.default)(keyphraseLength, this._boundaries.minimum, "min", textLength); this._maxRecommendedKeyphraseCount = (0, _recommendedKeywordCount.default)(keyphraseLength, this._boundaries.maximum, "max", textLength); } /** * Runs the keyphrase density module, based on this returns an assessment * result with a score. * * @param {Paper} paper The paper to use for the assessment. * @param {Researcher} researcher The researcher used for calling the research. * * @returns {AssessmentResult} The result of the assessment. */ getResult(paper, researcher) { const assessmentResult = new _AssessmentResult.default(); // Whether the paper has the data needed to return meaningful feedback (keyphrase and text). this._canAssess = paper.hasKeyword() && paper.hasText(); let calculatedScore; if (this._canAssess) { this._keyphraseCount = researcher.getResearch("getKeyphraseCount"); this._keyphraseDensityResult = researcher.getResearch("getKeyphraseDensity"); this._textLength = this._keyphraseDensityResult.textLength; assessmentResult.setHasMarks(this._keyphraseCount.count > 0); if (this._textLength < 100) { // Calculate the score for short texts. this._minRecommendedKeyphraseCount = 1; this._maxRecommendedKeyphraseCount = this._textLength > 50 ? 2 : 1; calculatedScore = this.calculateResultShortText(); } else { // Calculate the score for long texts. this._hasMorphologicalForms = researcher.getData("morphology") !== false; const keyphraseLength = this._keyphraseCount.keyphraseLength; this.setBoundaries(keyphraseLength, this._textLength); // Safe access with fallback const density = this._keyphraseDensityResult?.density ?? 0; this._keyphraseDensity = density * (0, _keyphraseLengthFactor.default)(keyphraseLength); calculatedScore = this.calculateResult(); } } else { // Calculate the score for papers with no keyphrase or text. calculatedScore = this.calculateResult(); } assessmentResult.setScore(calculatedScore.score); assessmentResult.setText(calculatedScore.resultText); // Only shows the AI button when the keyphrase hasn't been used enough times. // The button will handle its own disabled state and tooltip when there's no keyphrase or text. const shouldShowAIButton = calculatedScore.score === this._config.scores.underMinimum || calculatedScore.score === this._config.scores.noKeyphraseOrText && !this._canAssess; if (shouldShowAIButton) { assessmentResult.setHasAIFixes(true); } return assessmentResult; } /** * Checks whether there are no keyphrase matches in the text. * * @returns {boolean} Returns true if the keyphrase count is 0. */ hasNoMatches() { return this._keyphraseCount.count === 0; } /** * Checks whether there are too few keyphrase matches in the text. * * @returns {boolean} Returns true if the rounded keyphrase density is below the recommended minimum, * or if the keyphrase count is 1. */ hasTooFewMatches() { return (0, _inRange.inRangeStartInclusive)(this._keyphraseDensity, 0, this._boundaries.minimum) || this._keyphraseCount.count === 1; } /** * Checks whether there is a good number of keyphrase matches in the text. * * @returns {boolean} Returns true if the rounded keyphrase density is between the recommended minimum and maximum, * or if the keyphrase count is 2 and the recommended minimum is at most 2, * or if the text length is less than the short text limit and the keyphrase count is 1. */ hasGoodNumberOfMatches() { return (0, _inRange.inRangeStartEndInclusive)(this._keyphraseDensity, this._boundaries.minimum, this._boundaries.maximum) || this._keyphraseCount.count === 2 && this._minRecommendedKeyphraseCount <= 2; } /** * Checks whether the number of keyphrase matches in the text is between the * recommended maximum and the specified overMaximum value. * * @returns {boolean} Returns true if the rounded keyphrase density is between * the recommended maximum and the specified overMaximum * value. */ hasTooManyMatches() { return (0, _inRange.inRangeEndInclusive)(this._keyphraseDensity, this._boundaries.maximum, this._boundaries.overMaximum); } /** * Checks whether there is a good number of keyphrase matches in a short text (<= 100 words). * * @returns {boolean} Returns true if the number of keyphrase occurrences is between the minimum and maximum recommended count. */ hasGoodNumberOfMatchesShortText() { return (0, _inRange.inRangeStartEndInclusive)(this._keyphraseCount.count, this._minRecommendedKeyphraseCount, this._maxRecommendedKeyphraseCount); } /** * Checks whether the number of keyphrase matches in a short text (<= 100 words) is too high. * * @returns {boolean} Returns true if the number of keyphrase occurrences is one more than the maximum recommended count. */ hasTooManyMatchesShortText() { return this._keyphraseCount.count === this._maxRecommendedKeyphraseCount + 1; } /** * Creates a translation string for the first sentence of the feedback, which reports on the number of times * the keyphrase was found. * * @returns {string} The first sentence of the feedback. */ getFeedbackStringsFirstSentence() { return (0, _i18n.sprintf)( /* translators: %1$s expands to a link to Yoast.com, %2$s expands to the anchor end tag, %3$d expands to the number of times the keyphrase occurred in the text. */ (0, _i18n._n)("%1$sKeyphrase density%2$s: The keyphrase was found %3$d time.", "%1$sKeyphrase density%2$s: The keyphrase was found %3$d times.", this._keyphraseCount.count, "wordpress-seo"), this._config.urlTitle, "</a>", this._keyphraseCount.count); } /** * Returns the score for the keyphrase density. * * * @returns {{score: number, resultText: string}} result object with a score and translation text. */ calculateResultShortText() { if (this.hasNoMatches()) { return { score: this._config.scores.underMinimum, resultText: (0, _i18n.sprintf)( /* translators: %1$s and %4$s expand to links to Yoast.com, %2$s expands to the anchor end tag, %3$d expands to the recommended minimal number of times the keyphrase should occur in the text. */ (0, _i18n._n)("%1$sKeyphrase density%2$s: The keyphrase was found 0 times. That's less than the recommended minimum of %3$d time for a text of this length. %4$sFocus on your keyphrase%2$s!", "%1$sKeyphrase density%2$s: The keyphrase was found 0 times. That's less than the recommended minimum of %3$d times for a text of this length. %4$sFocus on your keyphrase%2$s!", this._minRecommendedKeyphraseCount, "wordpress-seo"), this._config.urlTitle, "</a>", this._minRecommendedKeyphraseCount, this._config.urlCallToAction) }; } if (this.hasGoodNumberOfMatchesShortText()) { return { score: this._config.scores.correctDensity, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to a link to Yoast.com, %2$s expands to the anchor end tag, %3$d expands to the number of times the keyphrase occurred in the text. */ (0, _i18n._n)("%1$sKeyphrase density%2$s: The keyphrase was found %3$d time. This is great!", "%1$sKeyphrase density%2$s: The keyphrase was found %3$d times. This is great!", this._keyphraseCount.count, "wordpress-seo"), this._config.urlTitle, "</a>", this._keyphraseCount.count) }; } const feedbackStringsFirstSentence = this.getFeedbackStringsFirstSentence(); if (this.hasTooManyMatchesShortText()) { return { score: this._config.scores.overMaximum, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to the sentence "Keyphrase density: The keyphrase was found X time(s).", %2$d expands to the recommended maximum number of times the keyphrase should occur in the text, %3$s expands to a link to Yoast.com., %4$s expands to the anchor end tag, */ (0, _i18n._n)("%1$s That's more than the recommended maximum of %2$d time for a text of this length. %3$sDon't overoptimize%4$s!", "%1$s That's more than the recommended maximum of %2$d times for a text of this length. %3$sDon't overoptimize%4$s!", this._maxRecommendedKeyphraseCount, "wordpress-seo"), feedbackStringsFirstSentence, this._maxRecommendedKeyphraseCount, this._config.urlCallToAction, "</a>") }; } // Implicitly returns this if the keyphrase count is higher than for any of the above conditions. return { score: this._config.scores.wayOverMaximum, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to the sentence "Keyphrase density: The keyphrase was found X time(s).", %2$d expands to the recommended maximum number of times the keyphrase should occur in the text, %3$s expands to a link to Yoast.com., %4$s expands to the anchor end tag, */ (0, _i18n._n)("%1$s That's way more than the recommended maximum of %2$d time for a text of this length. %3$sDon't overoptimize%4$s!", "%1$s That's way more than the recommended maximum of %2$d times for a text of this length. %3$sDon't overoptimize%4$s!", this._maxRecommendedKeyphraseCount, "wordpress-seo"), feedbackStringsFirstSentence, this._maxRecommendedKeyphraseCount, this._config.urlCallToAction, "</a>") }; } /** * Returns the score for the keyphrase density. * * * @returns {{score: number, resultText: string}} result object with a score and translation text. */ calculateResult() { if (!this._canAssess) { return { score: this._config.scores.noKeyphraseOrText, resultText: (0, _i18n.sprintf)(/* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag. */ (0, _i18n.__)("%1$sKeyphrase density%3$s: %2$sPlease add both a keyphrase and some text containing the keyphrase%3$s.", "wordpress-seo"), this._config.urlTitle, this._config.urlCallToAction, "</a>") }; } if (this.hasNoMatches()) { return { score: this._config.scores.underMinimum, resultText: (0, _i18n.sprintf)( /* translators: %1$s and %4$s expand to links to Yoast.com, %2$s expands to the anchor end tag, %3$d expands to the recommended minimal number of times the keyphrase should occur in the text. */ (0, _i18n._n)("%1$sKeyphrase density%2$s: The keyphrase was found 0 times. That's less than the recommended minimum of %3$d time for a text of this length. %4$sFocus on your keyphrase%2$s!", "%1$sKeyphrase density%2$s: The keyphrase was found 0 times. That's less than the recommended minimum of %3$d times for a text of this length. %4$sFocus on your keyphrase%2$s!", this._minRecommendedKeyphraseCount, "wordpress-seo"), this._config.urlTitle, "</a>", this._minRecommendedKeyphraseCount, this._config.urlCallToAction) }; } const feedbackStringsFirstSentence = this.getFeedbackStringsFirstSentence(); if (this.hasTooFewMatches()) { return { score: this._config.scores.underMinimum, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to the sentence "Keyphrase density: The keyphrase was found X time(s).", %2$d expands to the recommended minimum number of times the keyphrase should occur in the text, %3$s expands to a link to Yoast.com., %4$s expands to the anchor end tag, */ (0, _i18n._n)("%1$s That's less than the recommended minimum of %2$d time for a text of this length. %3$sFocus on your keyphrase%4$s!", "%1$s That's less than the recommended minimum of %2$d times for a text of this length. %3$sFocus on your keyphrase%4$s!", this._minRecommendedKeyphraseCount, "wordpress-seo"), feedbackStringsFirstSentence, this._minRecommendedKeyphraseCount, this._config.urlCallToAction, "</a>") }; } if (this.hasGoodNumberOfMatches()) { return { score: this._config.scores.correctDensity, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to a link to Yoast.com, %2$s expands to the anchor end tag, %3$d expands to the number of times the keyphrase occurred in the text. */ (0, _i18n._n)("%1$sKeyphrase density%2$s: The keyphrase was found %3$d time. This is great!", "%1$sKeyphrase density%2$s: The keyphrase was found %3$d times. This is great!", this._keyphraseCount.count, "wordpress-seo"), this._config.urlTitle, "</a>", this._keyphraseCount.count) }; } if (this.hasTooManyMatches()) { return { score: this._config.scores.overMaximum, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to the sentence "Keyphrase density: The keyphrase was found X time(s).", %2$d expands to the recommended maximum number of times the keyphrase should occur in the text, %3$s expands to a link to Yoast.com., %4$s expands to the anchor end tag, */ (0, _i18n._n)("%1$s That's more than the recommended maximum of %2$d time for a text of this length. %3$sDon't overoptimize%4$s!", "%1$s That's more than the recommended maximum of %2$d times for a text of this length. %3$sDon't overoptimize%4$s!", this._maxRecommendedKeyphraseCount, "wordpress-seo"), feedbackStringsFirstSentence, this._maxRecommendedKeyphraseCount, this._config.urlCallToAction, "</a>") }; } // Implicitly returns this if the rounded keyphrase density is higher than overMaximum. return { score: this._config.scores.wayOverMaximum, resultText: (0, _i18n.sprintf)( /* translators: %1$s expands to the sentence "Keyphrase density: The keyphrase was found X time(s).", %2$d expands to the recommended maximum number of times the keyphrase should occur in the text, %3$s expands to a link to Yoast.com., %4$s expands to the anchor end tag, */ (0, _i18n._n)("%1$s That's way more than the recommended maximum of %2$d time for a text of this length. %3$sDon't overoptimize%4$s!", "%1$s That's way more than the recommended maximum of %2$d times for a text of this length. %3$sDon't overoptimize%4$s!", this._maxRecommendedKeyphraseCount, "wordpress-seo"), feedbackStringsFirstSentence, this._maxRecommendedKeyphraseCount, this._config.urlCallToAction, "</a>") }; } /** * Marks the occurrences of keyphrase in the text for the keyphrase density assessment. * * @returns {Mark[]} Marks that should be applied. */ getMarks() { return this._keyphraseCount.markings; } } /** * This assessment checks if the keyphrase density is within the recommended range. * KeywordDensityAssessment was the previous name for KeyphraseDensityAssessment (hence the name of this file). * We keep (and expose) this assessment for backwards compatibility. * * @deprecated Use KeyphraseDensityAssessment instead. */ exports.KeyphraseDensityAssessment = KeyphraseDensityAssessment; class KeywordDensityAssessment extends KeyphraseDensityAssessment { /** * Sets the identifier and the config. * * @param {Object} config The configuration to use. */ constructor(config = {}) { super(config); this.identifier = "keywordDensity"; console.warn("This object is deprecated, use KeyphraseDensityAssessment instead."); } } exports.KeywordDensityAssessment = KeywordDensityAssessment; var _default = exports.default = KeyphraseDensityAssessment; //# sourceMappingURL=KeywordDensityAssessment.js.map