diff --git a/inc/options/class-wpseo-option.php b/inc/options/class-wpseo-option.php
index d0fe27e870e..98d2ee8b356 100644
--- a/inc/options/class-wpseo-option.php
+++ b/inc/options/class-wpseo-option.php
@@ -6,7 +6,7 @@
*/
/**
- * This abstract class and it's concrete classes implement defaults and value validation for
+ * This abstract class and its concrete classes implement defaults and value validation for
* all WPSEO options and subkeys within options.
*
* Some guidelines:
@@ -22,9 +22,9 @@
*
* [Updating/Adding options]
* - For multisite site_options, please use the WPSEO_Options::update_site_option() method.
- * - For normal options, use the normal add/update_option() functions. As long a the classes here
+ * - For normal options, use the normal add/update_option() functions. As long as the classes here
* are instantiated, validation for all options and their subkeys will be automatic.
- * - On (succesfull) update of a couple of options, certain related actions will be run automatically.
+ * - On (successful) update of a couple of options, certain related actions will be run automatically.
* Some examples:
* - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly
* - on change of wpseo and wpseo_title, some caches will be cleared
@@ -41,7 +41,7 @@
* translate_defaults() method.
* - When you remove an array key from an option: if it's important that the option is really removed,
* add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run.
- * This will re-save the option and automatically remove the array key no longer in existance.
+ * This will re-save the option and automatically remove the array key no longer in existence.
* - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run.
* - When you change the default for an option sub-key, make sure you verify that the validation routine will
* still work the way it should.
@@ -74,8 +74,8 @@ abstract class WPSEO_Option {
* Option group name for use in settings forms.
*
* Will be set automagically if not set in concrete class (i.e.
- * if it confirm to the normal pattern 'yoast' . $option_name . 'options',
- * only set in conrete class if it doesn't).
+ * if it conforms to the normal pattern 'yoast' . $option_name . 'options',
+ * only set in concrete class if it doesn't).
*
* @var string
*/
@@ -107,7 +107,7 @@ abstract class WPSEO_Option {
protected $defaults;
/**
- * Array of variable option name patterns for the option - if any -.
+ * Array of variable option name patterns for the option - if any.
*
* Set this when the option contains array keys which vary based on post_type
* or taxonomy.
@@ -230,7 +230,7 @@ protected function __construct() {
* ```
* ---------------
*
- * Concrete classes *may* contain a enrich_defaults method to add additional defaults once
+ * Concrete classes *may* contain an enrich_defaults method to add additional defaults once
* all post_types and taxonomies have been registered.
*
* ```
@@ -497,7 +497,7 @@ public function get_option( $options = null ) {
return $filtered;
}
- /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */
+ /* *********** METHODS influencing add_option(), update_option() and saving from admin pages. *********** */
/**
* Register (whitelist) the option for the configuration pages.
@@ -523,7 +523,7 @@ public function register_setting() {
}
/**
- * Validate the option
+ * Validate the option.
*
* @param mixed $option_value The unvalidated new value for the option.
*
@@ -645,7 +645,7 @@ public function maybe_add_option() {
*
* @param mixed $value The new value for the option.
*
- * @return bool Whether the update was succesfull.
+ * @return bool Whether the update was successful.
*/
public function update_site_option( $value ) {
if ( $this->multisite_only === true && is_multisite() ) {
@@ -667,7 +667,7 @@ public function update_site_option( $value ) {
* @uses WPSEO_Option::import()
*
* @param string|null $current_version Optional. Version from which to upgrade, if not set,
- * version specific upgrades will be disregarded.
+ * version-specific upgrades will be disregarded.
*
* @return void
*/
@@ -692,7 +692,7 @@ public function clean( $current_version = null ) {
*
* @param array $option_value Option value to be imported.
* @param string|null $current_version Optional. Version from which to upgrade, if not set,
- * version specific upgrades will be disregarded.
+ * version-specific upgrades will be disregarded.
* @param array|null $all_old_option_values Optional. Only used when importing old options to
* have access to the real old values, in contrast to
* the saved ones.
diff --git a/inc/sitemaps/class-taxonomy-sitemap-provider.php b/inc/sitemaps/class-taxonomy-sitemap-provider.php
index c8980015e2f..02ebbb5ff9d 100644
--- a/inc/sitemaps/class-taxonomy-sitemap-provider.php
+++ b/inc/sitemaps/class-taxonomy-sitemap-provider.php
@@ -287,7 +287,7 @@ public function is_valid_taxonomy( $taxonomy_name ) {
return false;
}
- if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu' ], true ) ) {
+ if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu', 'wp_pattern_category' ], true ) ) {
return false;
}
diff --git a/package.json b/package.json
index baa13713706..d3db5033d78 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,7 @@
"typescript": "^4.2.4"
},
"yoast": {
- "pluginVersion": "21.4-RC4"
+ "pluginVersion": "21.4-RC6"
},
"version": "0.0.0"
}
diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js
index c0e07e04d44..c808b3af28c 100644
--- a/packages/js/src/components/WincherSEOPerformance.js
+++ b/packages/js/src/components/WincherSEOPerformance.js
@@ -382,6 +382,7 @@ const TableContent = ( props ) => {
return [];
}
return Object.values( trackedKeyphrases )
+ .filter( keyphrase => !! keyphrase?.position?.history )
.map( keyphrase => ( {
label: keyphrase.keyword,
data: keyphrase.position.history,
diff --git a/packages/js/src/decorator/helpers/getAnnotationsHelpers.js b/packages/js/src/decorator/helpers/getAnnotationsHelpers.js
index bc53672b285..7966d0c6fa0 100644
--- a/packages/js/src/decorator/helpers/getAnnotationsHelpers.js
+++ b/packages/js/src/decorator/helpers/getAnnotationsHelpers.js
@@ -183,7 +183,7 @@ export const getAnnotationsForFAQ = ( attributeWithAnnotationSupport, block, mar
*/
export const getAnnotationsForHowTo = ( attributeWithAnnotationSupport, block, marks ) => {
const annotatableTextsFromBlock = block.attributes[ attributeWithAnnotationSupport.key ];
- if ( annotatableTextsFromBlock.length === 0 ) {
+ if ( annotatableTextsFromBlock && annotatableTextsFromBlock.length === 0 ) {
return [];
}
const annotations = [];
diff --git a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js
index 9163801318b..ac2376338a3 100644
--- a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js
+++ b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js
@@ -4,8 +4,8 @@ import { helpers } from "yoastseo";
/**
* Regex to detect HTML tags.
* Please note that this regex will also detect non-HTML tags that are also wrapped in `<>`.
- * For example, in the following sentence, cats rabbit ,
- * we will match , and . This is an edge case though.
+ * For example, in the following sentence, `cats rabbit `,
+ * we will match ``, `` and ``. This is an edge case though.
* @type {RegExp}
*/
const htmlTagsRegex = /(<([a-z]|\/)[^<>]+>)/ig;
@@ -53,6 +53,27 @@ const adjustFirstSectionOffsets = ( blockStartOffset, blockEndOffset, blockName
return { blockStartOffset, blockEndOffset };
};
+/**
+ * Retrieves the length for HTML tags, adjusts the length for `
` tags.
+ * @param {[Object]} htmlTags Array of HTML tags.
+ * @returns {number} The length of the given HTML tags.
+ */
+const getTagsLength = ( htmlTags ) => {
+ let tagsLength = 0;
+ forEachRight( htmlTags, ( htmlTag ) => {
+ const [ tag ] = htmlTag;
+ let tagLength = tag.length;
+ // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength.
+ if ( /^<\/?br/.test( tag ) ) {
+ tagLength -= 1;
+ }
+
+ tagsLength += tagLength;
+ } );
+
+ return tagsLength;
+};
+
/**
* Adjusts the block start and end offsets of a given Mark when the block HTML contains HTML tags.
*
@@ -80,17 +101,11 @@ const adjustOffsetsForHtmlTags = ( slicedBlockHtmlToStartOffset, slicedBlockHtml
* - Text: This is a giant panda.
* - Range of "panda": 16 -21
*/
- let foundHtmlTags = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ];
- forEachRight( foundHtmlTags, ( foundHtmlTag ) => {
- const [ tag ] = foundHtmlTag;
- blockStartOffset -= tag.length;
- } );
+ const foundHtmlTagsToStartOffset = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ];
+ blockStartOffset -= getTagsLength( foundHtmlTagsToStartOffset );
- foundHtmlTags = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ];
- forEachRight( foundHtmlTags, ( foundHtmlTag ) => {
- const [ tag ] = foundHtmlTag;
- blockEndOffset -= tag.length;
- } );
+ const foundHtmlTagsToEndOffset = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ];
+ blockEndOffset -= getTagsLength( foundHtmlTagsToEndOffset );
return { blockStartOffset, blockEndOffset };
};
diff --git a/packages/js/src/settings/routes/breadcrumbs.js b/packages/js/src/settings/routes/breadcrumbs.js
index 3b36fa9c998..6aec54c0fe1 100644
--- a/packages/js/src/settings/routes/breadcrumbs.js
+++ b/packages/js/src/settings/routes/breadcrumbs.js
@@ -111,10 +111,8 @@ const Breadcrumbs = () => {
as={ SelectField }
name={ `wpseo_titles.post_types-${ postTypeName }-maintax` }
id={ `input-wpseo_titles-post_types-${ postTypeName }-maintax` }
- label={ <>
- { postType.label }
- { postTypeName }
- > }
+ label={ postType.label }
+ labelSuffix={ { postTypeName }
}
options={ postType.options }
className="yst-max-w-sm"
/> ) }
@@ -131,12 +129,10 @@ const Breadcrumbs = () => {
as={ SelectField }
name={ `wpseo_titles.taxonomy-${ taxonomy.name }-ptparent` }
id={ `input-wpseo_titles-taxonomy-${ taxonomy.name }-ptparent` }
- label={ <>
- { taxonomy.label }
- { taxonomy.name }
- > }
+ label={ taxonomy.label }
options={ taxonomy.options }
className="yst-max-w-sm"
+ labelSuffix={ { taxonomy.name }
}
/>
) ) }
diff --git a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js
index c67344bc7bb..31614969e6e 100644
--- a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js
+++ b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js
@@ -299,7 +299,7 @@ const testCases = [
attributeId: "",
clientId: "",
isFirstSection: false } } ),
- ],
+ ],
skip: false,
},
{
@@ -953,6 +953,41 @@ const testCasesWithSpecialCharacters = [
],
skip: false,
},
+ {
+ description: "can match keyphrases in texts that contain
tags",
+ paper: new Paper( "
This is a test.
This is
another
test.
", { keyword: "test" } ),
+ keyphraseForms: [ [ "test" ] ],
+ expectedCount: 2,
+ expectedMarkings: [
+ new Mark( {
+ original: "This is a test.",
+ marked: "This is a test.",
+ position: {
+ startOffset: 13,
+ endOffset: 17,
+ startOffsetBlock: 10,
+ endOffsetBlock: 14,
+ attributeId: "",
+ clientId: "",
+ isFirstSection: false,
+ } }
+ ),
+ new Mark( {
+ original: "\nThis is\nanother\ntest.",
+ marked: "\nThis is\nanother\ntest.",
+ position: {
+ startOffset: 46,
+ endOffset: 50,
+ startOffsetBlock: 43,
+ endOffsetBlock: 47,
+ attributeId: "",
+ clientId: "",
+ isFirstSection: false,
+ } }
+ ),
+ ],
+ skip: false,
+ },
{
description: "can match paragraph gutenberg block",
paper: new Paper(
@@ -1856,6 +1891,35 @@ describe( "Test for counting the keyphrase in a text for Japanese", () => {
original: "私の猫はかわいいです。" } ) ] );
} );
+ it( "counts the keyphrase occurrence inside an image caption.", function() {
+ const mockPaper = new Paper( "[caption id=\"attachment_157\" align=\"alignnone\" width=\"225\"] 一日一冊の本を読むのはできるかどうかやってみます。[/caption]
", {
+ locale: "ja",
+ keyphrase: "一冊の本を読む",
+ shortcodes: [
+ "wp_caption",
+ "caption",
+ "gallery",
+ "playlist" ],
+ } );
+ const keyphraseForms = [
+ [ "一冊" ],
+ [ "本" ],
+ [ "読む", "読み", "読ま", "読め", "読も", "読ん", "読める", "読ませ", "読ませる", "読まれ", "読まれる", "読もう" ],
+ ];
+ const researcher = buildJapaneseMockResearcher( keyphraseForms, wordsCountHelper, matchWordsHelper );
+ buildTree( mockPaper, researcher );
+
+
+ expect( getKeyphraseCount( mockPaper, researcher ).count ).toBe( 1 );
+ expect( getKeyphraseCount( mockPaper, researcher ).markings ).toEqual( [
+ new Mark( {
+ marked: "一日一冊の本を読むのはできるかどうかやってみます。",
+ original: "一日一冊の本を読むのはできるかどうかやってみます。" } ) ] );
+ } );
+
it( "counts/marks a string of text with multiple occurrences of the same keyphrase in it.", function() {
const mockPaper = new Paper( "私の猫はかわいい猫です。
", { locale: "ja", keyphrase: "猫" } );
const researcher = buildJapaneseMockResearcher( [ [ "猫" ] ], wordsCountHelper, matchWordsHelper );
diff --git a/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js b/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js
index 4fb3b9c6a29..c33399e13d1 100644
--- a/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js
+++ b/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js
@@ -141,6 +141,21 @@ describe( "A test for getting positions of sentences", () => {
expect( getTextElementPositions( node, sentences ) ).toEqual( sentencesWithPositions );
} );
+ it( "should determine the correct token positions when the sentence contains a br tag", function() {
+ // HTML: Hello
world!
.
+ const html = "Hello
world!
";
+ const tree = adapt( parseFragment( html, { sourceCodeLocationInfo: true } ) );
+ const paragraph = tree.childNodes[ 0 ];
+ const tokens = [ "Hello", "\n", "world", "!" ].map( string => new Token( string ) );
+
+ const [ hello, br, world, bang ] = getTextElementPositions( paragraph, tokens, 3 );
+
+ expect( hello.sourceCodeRange ).toEqual( { startOffset: 3, endOffset: 8 } );
+ expect( br.sourceCodeRange ).toEqual( { startOffset: 13, endOffset: 14 } );
+ expect( world.sourceCodeRange ).toEqual( { startOffset: 14, endOffset: 19 } );
+ expect( bang.sourceCodeRange ).toEqual( { startOffset: 19, endOffset: 20 } );
+ } );
+
it( "gets the sentence positions from a node that has a code child node", function() {
// HTML: Hello array.push( something )
code!
const node = new Paragraph( {}, [
diff --git a/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js b/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js
index 979f1355f0c..a00d2825961 100644
--- a/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js
+++ b/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js
@@ -477,6 +477,30 @@ describe( "A test for the tokenize function",
name: "#document-fragment",
} );
} );
+
+ it( "should correctly tokenize a paragraph with single br tags", function() {
+ const mockPaper = new Paper( "This is a sentence.
This is
another sentence.
" );
+ const mockResearcher = new EnglishResearcher( mockPaper );
+ const languageProcessor = new LanguageProcessor( mockResearcher );
+ buildTreeNoTokenize( mockPaper );
+ const result = tokenize( mockPaper.getTree(), languageProcessor );
+ const sentences = result.childNodes[ 0 ].sentences;
+ expect( sentences.length ).toEqual( 2 );
+ const firstSentence = sentences[ 0 ];
+ expect( firstSentence.text ).toEqual( "This is a sentence." );
+ expect( firstSentence.sourceCodeRange ).toEqual( { startOffset: 3, endOffset: 22 } );
+ expect( firstSentence.tokens.length ).toEqual( 8 );
+ const secondSentence = sentences[ 1 ];
+ expect( secondSentence.text ).toEqual( "\nThis is\nanother sentence." );
+ expect( secondSentence.sourceCodeRange ).toEqual( { startOffset: 27, endOffset: 56 } );
+ expect( secondSentence.tokens.length ).toEqual( 9 );
+ const [ br1, this1, , is1, br2, another1, , , ] = secondSentence.tokens;
+ expect( br1.sourceCodeRange ).toEqual( { startOffset: 27, endOffset: 28 } );
+ expect( this1.sourceCodeRange ).toEqual( { startOffset: 28, endOffset: 32 } );
+ expect( is1.sourceCodeRange ).toEqual( { startOffset: 33, endOffset: 35 } );
+ expect( br2.sourceCodeRange ).toEqual( { startOffset: 38, endOffset: 39 } );
+ expect( another1.sourceCodeRange ).toEqual( { startOffset: 39, endOffset: 46 } );
+ } );
} );
describe( "A test for tokenizing a Japanese sentence", function() {
diff --git a/packages/yoastseo/spec/parse/traverse/innerTextSpec.js b/packages/yoastseo/spec/parse/traverse/innerTextSpec.js
index 39e1504f105..4afc8e40d89 100644
--- a/packages/yoastseo/spec/parse/traverse/innerTextSpec.js
+++ b/packages/yoastseo/spec/parse/traverse/innerTextSpec.js
@@ -46,6 +46,17 @@ describe( "A test for innerText", () => {
expect( searchResult ).toEqual( expected );
} );
+ it( "should consider break tags to be line breaks", function() {
+ // Matsuo Bashō's "old pond".
+ paper._text = "old pond
frog leaps in
water's sound
";
+ const tree = build( paper, languageProcessor );
+
+ const searchResult = innerText( tree );
+ const expected = "old pond\nfrog leaps in\nwater's sound";
+
+ expect( searchResult ).toEqual( expected );
+ } );
+
it( "should return an empty string when presented an empty node", function() {
const tree = new Node( "" );
diff --git a/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js b/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js
index 4b774ada4e2..27e0a372a45 100644
--- a/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js
+++ b/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js
@@ -320,7 +320,7 @@ describe( "A test for marking the keyphrase", function() {
const keyphraseDensityAssessment = new KeyphraseDensityAssessment();
const paper = new Paper( " " +
- "A flamboyant cat with a toy
\n" +
+ "A flamboyant cat with a toy
" +
"
",
{ keyword: "cat toy" } );
const researcher = new EnglishResearcher( paper );
diff --git a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js
index db19d8189a8..f409563791e 100644
--- a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js
+++ b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js
@@ -1,3 +1,23 @@
+/**
+ * Retrieves the start offset for a given node.
+ * @param {Node} node The current node.
+ * @returns {number} The start offset.
+ */
+function getStartOffset( node ) {
+ return node.sourceCodeLocation &&
+ ( ( node.sourceCodeLocation.startTag && node.sourceCodeLocation.startTag.endOffset ) || node.sourceCodeLocation.startOffset ) || 0;
+}
+
+/**
+ * Retrieves the parent node for a given node.
+ * @param {Paper} paper The current paper.
+ * @param {Node} node The current node.
+ * @returns {Node} The parent node.
+ */
+function getParentNode( paper, node ) {
+ return paper.getTree().findAll( treeNode => treeNode.childNodes && treeNode.childNodes.includes( node ) )[ 0 ];
+}
+
/**
* Gets all the sentences from paragraph and heading nodes.
* These two node types are the nodes that should contain sentences for the analysis.
@@ -11,17 +31,23 @@ export default function( paper ) {
const tree = paper.getTree().findAll( treeNode => !! treeNode.sentences );
return tree.flatMap( node => node.sentences.map( sentence => {
+ let parentNode = node;
+
+ // For implicit paragraphs, base the details on the parent of this node.
+ if ( node.isImplicit ) {
+ parentNode = getParentNode( paper, node );
+ }
+
return {
...sentence,
// The parent node's start offset is the start offset of the parent node if it doesn't have a `startTag` property.
- parentStartOffset: node.sourceCodeLocation && ( ( node.sourceCodeLocation.startTag && node.sourceCodeLocation.startTag.endOffset ) ||
- node.sourceCodeLocation.startOffset ) || 0,
+ parentStartOffset: getStartOffset( parentNode ),
// The block client id of the parent node.
- parentClientId: node.clientId || "",
+ parentClientId: parentNode.clientId || "",
// The attribute id of the parent node, if available, otherwise an empty string.
- parentAttributeId: node.attributeId || "",
+ parentAttributeId: parentNode.attributeId || "",
// Whether the parent node is the first section of Yoast sub-blocks.
- isParentFirstSectionOfBlock: node.isFirstSection || false,
+ isParentFirstSectionOfBlock: parentNode.isFirstSection || false,
};
} ) );
}
diff --git a/packages/yoastseo/src/languageProcessing/researches/keywordCount.js b/packages/yoastseo/src/languageProcessing/researches/keywordCount.js
index e26b819b8ad..315f91fb7c5 100644
--- a/packages/yoastseo/src/languageProcessing/researches/keywordCount.js
+++ b/packages/yoastseo/src/languageProcessing/researches/keywordCount.js
@@ -6,6 +6,7 @@ import matchWordFormsWithSentence from "../helpers/match/matchWordFormsWithSente
import isDoubleQuoted from "../helpers/match/isDoubleQuoted";
import { markWordsInASentence } from "../helpers/word/markWordsInSentences";
import getSentences from "../helpers/sentence/getSentences";
+import { filterShortcodesFromHTML } from "../helpers";
/**
* Counts the occurrences of the keyphrase in the text and creates the Mark objects for the matches.
@@ -90,8 +91,12 @@ export default function getKeyphraseCount( paper, researcher ) {
const matchWordCustomHelper = researcher.getHelper( "matchWordCustomHelper" );
const customSentenceTokenizer = researcher.getHelper( "memoizedTokenizer" );
const locale = paper.getLocale();
+ const text = matchWordCustomHelper
+ ? filterShortcodesFromHTML( paper.getText(), paper._attributes && paper._attributes.shortcodes )
+ : paper.getText();
+
// When the custom helper is available, we're using the sentences retrieved from the text for the analysis.
- const sentences = matchWordCustomHelper ? getSentences( paper.getText(), customSentenceTokenizer ) : getSentencesFromTree( paper );
+ const sentences = matchWordCustomHelper ? getSentences( text, customSentenceTokenizer ) : getSentencesFromTree( paper );
// Exact matching is requested when the keyphrase is enclosed in double quotes.
const isExactMatchRequested = isDoubleQuoted( paper.getKeyword() );
diff --git a/packages/yoastseo/src/parse/build/private/getTextElementPositions.js b/packages/yoastseo/src/parse/build/private/getTextElementPositions.js
index fd13846d4bc..250dfe24a08 100644
--- a/packages/yoastseo/src/parse/build/private/getTextElementPositions.js
+++ b/packages/yoastseo/src/parse/build/private/getTextElementPositions.js
@@ -26,7 +26,18 @@ function getDescendantPositions( descendantNodes ) {
descendantTagPositions.push( node.sourceCodeLocation );
} else {
if ( node.sourceCodeLocation.startTag ) {
- descendantTagPositions.push( node.sourceCodeLocation.startTag );
+ const startRange = {
+ startOffset: node.sourceCodeLocation.startTag.startOffset,
+ endOffset: node.sourceCodeLocation.startTag.endOffset,
+ };
+ /*
+ * Here, we need to account for the fact that earlier (in innerText.js), we treated a
as a newline character.
+ * Therefore, we need to subtract 1 from the endOffset to not count it twice.
+ */
+ if ( node.name === "br" ) {
+ startRange.endOffset = startRange.endOffset - 1;
+ }
+ descendantTagPositions.push( startRange );
}
/*
* Check whether node has an end tag before adding it to the array.
diff --git a/packages/yoastseo/src/parse/traverse/innerText.js b/packages/yoastseo/src/parse/traverse/innerText.js
index ab454196eef..71df6b6dfce 100644
--- a/packages/yoastseo/src/parse/traverse/innerText.js
+++ b/packages/yoastseo/src/parse/traverse/innerText.js
@@ -14,6 +14,8 @@ export default function innerText( node ) {
node.childNodes.forEach( child => {
if ( child.name === "#text" ) {
text += child.value;
+ } else if ( child.name === "br" ) {
+ text += "\n";
} else {
text += innerText( child );
}
diff --git a/src/builders/indexable-hierarchy-builder.php b/src/builders/indexable-hierarchy-builder.php
index cdc663bd75c..e2e0f7392d8 100644
--- a/src/builders/indexable-hierarchy-builder.php
+++ b/src/builders/indexable-hierarchy-builder.php
@@ -132,21 +132,6 @@ public function build( Indexable $indexable ) {
* @return bool True when indexable has a built hierarchy.
*/
protected function hierarchy_is_built( Indexable $indexable ) {
- /**
- * Filters ignoring checking if the hierarchy is already built.
- *
- * Used when adding term with `wp_set_object_terms` together with `wp_insert_post`.
- *
- * @since 21.2
- *
- * @param bool $ignore_already_saved If the hierarchy already saved check should be ignored.
- * @return bool The filtered value of the `$ignore_already_saved` parameter.
- */
- $ignore_already_saved = apply_filters( 'wpseo_hierarchy_ignore_already_saved', false );
- if ( $ignore_already_saved ) {
- return false;
- }
-
if ( \in_array( $indexable->id, $this->saved_ancestors, true ) ) {
return true;
}
diff --git a/src/integrations/admin/indexables-exclude-taxonomy-integration.php b/src/integrations/admin/indexables-exclude-taxonomy-integration.php
index 09e76662e87..4d1592b8d8f 100644
--- a/src/integrations/admin/indexables-exclude-taxonomy-integration.php
+++ b/src/integrations/admin/indexables-exclude-taxonomy-integration.php
@@ -41,13 +41,15 @@ public function register_hooks() {
*
* @param array $excluded_taxonomies The excluded taxonomies.
*
- * @return array The excluded post types, including the specific post type.
+ * @return array The excluded taxonomies, including specific taxonomies.
*/
public function exclude_taxonomies_for_indexation( $excluded_taxonomies ) {
+ $taxonomies_to_exclude = \array_merge( $excluded_taxonomies, [ 'wp_pattern_category' ] );
+
if ( $this->options_helper->get( 'disable-post_format', false ) ) {
- return \array_merge( $excluded_taxonomies, [ 'post_format' ] );
+ return \array_merge( $taxonomies_to_exclude, [ 'post_format' ] );
}
- return $excluded_taxonomies;
+ return $taxonomies_to_exclude;
}
}
diff --git a/src/integrations/third-party/wordproof.php b/src/integrations/third-party/wordproof.php
index c9d63ba1d4b..2928bd1bdca 100644
--- a/src/integrations/third-party/wordproof.php
+++ b/src/integrations/third-party/wordproof.php
@@ -89,10 +89,19 @@ public function register_hooks() {
*/
\add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 10, 0 );
- /**
- * Add async to the wordproof scripts.
- */
- \add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 );
+ if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ) ) {
+ \add_action(
+ 'wp_enqueue_scripts',
+ function() {
+ \wp_script_add_data( WPSEO_Admin_Asset_Manager::PREFIX . 'wordproof-uikit', 'strategy', 'async' );
+ },
+ 11,
+ 0
+ );
+ }
+ else {
+ \add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 );
+ }
/**
* Removes the post meta timestamp key for the old privacy page.
diff --git a/src/integrations/watchers/indexable-ancestor-watcher.php b/src/integrations/watchers/indexable-ancestor-watcher.php
index 9c76a4529b1..fddf0c92d18 100644
--- a/src/integrations/watchers/indexable-ancestor-watcher.php
+++ b/src/integrations/watchers/indexable-ancestor-watcher.php
@@ -92,9 +92,6 @@ public function __construct(
*/
public function register_hooks() {
\add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 );
- if ( ! \check_ajax_referer( 'inlineeditnonce', '_inline_edit', false ) ) {
- \add_action( 'set_object_terms', [ $this, 'build_post_hierarchy' ], 10, 6 );
- }
}
/**
@@ -106,29 +103,6 @@ public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
- /**
- * Validates if the current primary category is still present. If not just remove the post meta for it.
- *
- * @param int $object_id Object ID.
- * @param array $terms Unused. An array of object terms.
- * @param array $tt_ids An array of term taxonomy IDs.
- * @param string $taxonomy Taxonomy slug.
- * @param bool $append Whether to append new terms to the old terms.
- * @param array $old_tt_ids Old array of term taxonomy IDs.
- */
- public function build_post_hierarchy( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
- $post = \get_post( $object_id );
- if ( $this->post_type_helper->is_excluded( $post->post_type ) ) {
- return;
- }
-
- $indexable = $this->indexable_repository->find_by_id_and_type( $post->ID, $post->post_type );
-
- if ( $indexable instanceof Indexable ) {
- $this->indexable_hierarchy_builder->build( $indexable );
- }
- }
-
/**
* If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink.
*
diff --git a/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php b/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php
index 0f171869e82..8731d7ef87e 100644
--- a/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php
+++ b/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php
@@ -9,6 +9,7 @@
* Class WPSEO_Taxonomy_Sitemap_Provider_Test.
*
* @group sitemaps
+ * @coversDefaultClass WPSEO_Taxonomy_Sitemap_Provider
*/
class WPSEO_Taxonomy_Sitemap_Provider_Test extends WPSEO_UnitTestCase {
@@ -31,7 +32,7 @@ public function set_up() {
/**
* Tests the retrieval of the index links.
*
- * @covers WPSEO_Taxonomy_Sitemap_Provider::get_index_links
+ * @covers ::get_index_links
*/
public function test_get_index_links() {
@@ -57,7 +58,7 @@ public function test_get_index_links() {
/**
* Tests retrieval of the sitemap links.
*
- * @covers WPSEO_Taxonomy_Sitemap_Provider::get_sitemap_links
+ * @covers ::get_sitemap_links
*/
public function test_get_sitemap_links() {
@@ -71,7 +72,7 @@ public function test_get_sitemap_links() {
/**
* Makes sure invalid sitemap pages return no contents (404).
*
- * @covers WPSEO_Taxonomy_Sitemap_Provider::get_index_links
+ * @covers ::get_index_links
*/
public function test_get_index_links_empty_sitemap() {
// Fetch the global sitemap.
@@ -87,4 +88,44 @@ public function test_get_index_links_empty_sitemap() {
// Expect an empty page (404) to be returned.
$this->expectOutputString( '' );
}
+
+ /**
+ * Data provider for is_valid_taxonomy test.
+ *
+ * @return array
+ */
+ public function data_provider_is_valis_taxonomy() {
+ return [
+ 'Pattern Categories' => [
+ 'taxonomy' => 'wp_pattern_category',
+ 'expected' => false,
+ ],
+ 'nav_menu' => [
+ 'taxonomy' => 'nav_menu',
+ 'expected' => false,
+ ],
+ 'link_category' => [
+ 'taxonomy' => 'link_category',
+ 'expected' => false,
+ ],
+ 'post_format' => [
+ 'taxonomy' => 'post_format',
+ 'expected' => false,
+ ],
+ ];
+ }
+
+ /**
+ * Tetst of is_valid_taxonomy.
+ *
+ * @covers ::is_valid_taxonomy
+ *
+ * @dataProvider data_provider_is_valis_taxonomy
+ *
+ * @param string $taxonomy Taxonomy name.
+ * @param bool $expected Expected result.
+ */
+ public function test_is_valid_taxonomy( $taxonomy, $expected ) {
+ $this->assertSame( $expected, self::$class_instance->is_valid_taxonomy( $taxonomy ) );
+ }
}
diff --git a/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php b/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php
index 1b55929e38d..04e2eed2e54 100644
--- a/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php
+++ b/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php
@@ -168,116 +168,15 @@ public function test_get_conditionals() {
);
}
- /**
- * Data provider for the register_hooks test.
- *
- * @return array The data.
- */
- public function data_provider_register_hooks() {
- return [
- 'When ancestor is changes inline edit' => [
- 'is_inline_edit' => true,
- 'set_object_terms_action' => false,
- ],
- 'When ancestor is changes not inline edit' => [
- 'is_inline_edit' => false,
- 'set_object_terms_action' => 10,
- ],
- ];
- }
-
/**
* Tests if the expected hooks are registered.
*
* @covers ::register_hooks
- *
- * @dataProvider data_provider_register_hooks
- *
- * @param bool $is_inline_edit Whether or not the request is an inline edit.
- * @param bool|int $set_object_terms_action The set_object_terms action return value.
*/
- public function test_register_hooks( $is_inline_edit, $set_object_terms_action ) {
-
- Functions\expect( 'check_ajax_referer' )
- ->once()
- ->with( 'inlineeditnonce', '_inline_edit', false )
- ->andReturn( $is_inline_edit );
-
+ public function test_register_hooks() {
$this->instance->register_hooks();
self::assertNotFalse( \has_action( 'wpseo_save_indexable', [ $this->instance, 'reset_children' ] ) );
- self::assertSame( $set_object_terms_action, \has_action( 'set_object_terms', [ $this->instance, 'build_post_hierarchy' ] ) );
- }
-
- /**
- * Data provider for the build_post_hierarchy test.
- *
- * @return array The data.
- */
- public static function data_provider_build_post_hierarchy() {
- $indexable = Mockery::mock( Indexable_Mock::class );
- return [
- 'Building hierarchy' => [
- 'is_excluded' => false,
- 'find_by_id_and_type_times' => 1,
- 'indexable' => $indexable,
- 'build_times' => 1,
- ],
- 'Not building hierarchy because no indexable' => [
- 'is_excluded' => false,
- 'find_by_id_and_type_times' => 1,
- 'indexable' => (object) [ 'ID' => 5 ],
- 'build_times' => 0,
- ],
- 'Not building because excluded post type' => [
- 'is_excluded' => true,
- 'find_by_id_and_type_times' => 0,
- 'indexable' => (object) [ 'ID' => 5 ],
- 'build_times' => 0,
- ],
- ];
- }
-
- /**
- * Tests the build_post_hierarchy.
- *
- * @covers ::build_post_hierarchy
- *
- * @dataProvider data_provider_build_post_hierarchy
- *
- * @param bool $is_excluded Whether or not the post type is excluded.
- * @param int $find_by_id_and_type_times The number of times the find_by_id_and_type method is called.
- * @param Indexable_Mock $indexable The indexable.
- * @param int $build_times The number of times the build method is called.
- */
- public function test_build_post_hierarchy( $is_excluded, $find_by_id_and_type_times, $indexable, $build_times ) {
- $post = Mockery::mock( \WP_Post::class );
- $post->post_type = 'post';
- $post->ID = 5;
-
- Functions\expect( 'get_post' )
- ->once()
- ->with( 5 )
- ->andReturn( $post );
-
- $this->post_type_helper
- ->expects( 'is_excluded' )
- ->once()
- ->with( $post->post_type )
- ->andReturn( $is_excluded );
-
- $this->indexable_repository
- ->expects( 'find_by_id_and_type' )
- ->times( $find_by_id_and_type_times )
- ->with( $post->ID, $post->post_type )
- ->andReturn( $indexable );
-
- $this->indexable_hierarchy_builder
- ->expects( 'build' )
- ->times( $build_times )
- ->with( $indexable );
-
- $this->instance->build_post_hierarchy( 5, [ 'test_term' ], [ 7 ], 'category', false, [] );
}
/**
diff --git a/wp-seo-main.php b/wp-seo-main.php
index 1f06571a89a..2f47796809c 100644
--- a/wp-seo-main.php
+++ b/wp-seo-main.php
@@ -15,7 +15,7 @@
* {@internal Nobody should be able to overrule the real version number as this can cause
* serious issues with the options, so no if ( ! defined() ).}}
*/
-define( 'WPSEO_VERSION', '21.4-RC4' );
+define( 'WPSEO_VERSION', '21.4-RC6' );
if ( ! defined( 'WPSEO_PATH' ) ) {
diff --git a/wp-seo.php b/wp-seo.php
index def76d6adf5..358a2b61c0e 100644
--- a/wp-seo.php
+++ b/wp-seo.php
@@ -8,7 +8,7 @@
*
* @wordpress-plugin
* Plugin Name: Yoast SEO
- * Version: 21.4-RC4
+ * Version: 21.4-RC6
* Plugin URI: https://yoa.st/1uj
* Description: The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more.
* Author: Team Yoast
@@ -20,7 +20,7 @@
* Requires PHP: 7.2.5
*
* WC requires at least: 7.1
- * WC tested up to: 8.1
+ * WC tested up to: 8.2
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by