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 different cat with toy " + - "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