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