From d314a1723d8cc82e635ed5754a4977bb66087419 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 31 Oct 2023 11:25:25 +0100 Subject: [PATCH 01/35] Add back the shortcode plugin --- .../src/analysis/plugins/shortcode-plugin.js | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 packages/js/src/analysis/plugins/shortcode-plugin.js diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js new file mode 100644 index 00000000000..252839610cc --- /dev/null +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -0,0 +1,338 @@ +/* global tinyMCE */ +/* global wpseoScriptData */ +/* global ajaxurl */ +/* global _ */ + +const shortcodeNameMatcher = "[^<>&/\\[\\]\x00-\x20=]+?"; +const shortcodeAttributesMatcher = "( [^\\]]+?)?"; + +const shortcodeStartRegex = new RegExp( "\\[" + shortcodeNameMatcher + shortcodeAttributesMatcher + "\\]", "g" ); +const shortcodeEndRegex = new RegExp( "\\[/" + shortcodeNameMatcher + "\\]", "g" ); + +/** + * The Yoast Shortcode plugin parses the shortcodes in a given piece of text. It analyzes multiple input fields for + * shortcodes which it will preload using AJAX. + */ +class YoastShortcodePlugin { + /** + * Constructs the YoastShortcodePlugin. + * + * @property {RegExp} keywordRegex Used to match a given string for valid shortcode keywords. + * @property {RegExp} closingTagRegex Used to match a given string for shortcode closing tags. + * @property {RegExp} nonCaptureRegex Used to match a given string for non-capturing shortcodes. + * @property {Array} parsedShortcodes Used to store parsed shortcodes. + * + * @param {Object} interface Object Formerly Known as App, but for backwards compatibility + * still passed here as one argument. + * @param {function} interface.registerPlugin Register a plugin with Yoast SEO. + * @param {function} interface.registerModification Register a modification with Yoast SEO. + * @param {function} interface.pluginReady Notify Yoast SEO that the plugin is ready. + * @param {function} interface.pluginReloaded Notify Yoast SEO that the plugin has been reloaded. + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. + * @returns {void} + */ + constructor( { registerPlugin, registerModification, pluginReady, pluginReloaded }, shortcodesToBeParsed ) { + this._registerModification = registerModification; + this._pluginReady = pluginReady; + this._pluginReloaded = pluginReloaded; + + registerPlugin( "YoastShortcodePlugin", { status: "loading" } ); + this.bindElementEvents(); + + const keywordRegexString = "(" + shortcodesToBeParsed.join( "|" ) + ")"; + + // The regex for matching shortcodes based on the available shortcode keywords. + this.keywordRegex = new RegExp( keywordRegexString, "g" ); + this.closingTagRegex = new RegExp( "\\[\\/" + keywordRegexString + "\\]", "g" ); + this.nonCaptureRegex = new RegExp( "\\[" + keywordRegexString + "[^\\]]*?\\]", "g" ); + + /** + * The array of parsedShortcode objects. + * + * @type {Object[]} + */ + this.parsedShortcodes = []; + + this.loadShortcodes( this.declareReady.bind( this ) ); + } + + /* YOAST SEO CLIENT */ + + /** + * Declares ready with YoastSEO. + * + * @returns {void} + */ + declareReady() { + this._pluginReady( "YoastShortcodePlugin" ); + this.registerModifications(); + } + + /** + * Declares reloaded with YoastSEO. + * + * @returns {void} + */ + declareReloaded() { + this._pluginReloaded( "YoastShortcodePlugin" ); + } + + /** + * Registers the modifications for the content in which we want to replace shortcodes. + * + * @returns {void} + */ + registerModifications() { + this._registerModification( "content", this.replaceShortcodes.bind( this ), "YoastShortcodePlugin" ); + } + + + /** + * Removes all unknown shortcodes. Not all plugins properly registered their shortcodes in the WordPress backend. + * Since we cannot use the data from these shortcodes they must be removed. + * + * @param {String} data The text to remove unknown shortcodes. + * @returns {String} The text with removed unknown shortcodes. + */ + removeUnknownShortCodes( data ) { + data = data.replace( shortcodeStartRegex, "" ); + data = data.replace( shortcodeEndRegex, "" ); + + return data; + } + + /** + * The callback used to replace the shortcodes. + * + * @param {String} data The text to replace the shortcodes in. + * + * @returns {String} The text with replaced shortcodes. + */ + replaceShortcodes( data ) { + const parsedShortcodes = this.parsedShortcodes; + + if ( typeof data === "string" && parsedShortcodes.length > 0 ) { + for ( let i = 0; i < parsedShortcodes.length; i++ ) { + data = data.replace( parsedShortcodes[ i ].shortcode, parsedShortcodes[ i ].output ); + } + } + + data = this.removeUnknownShortCodes( data ); + + return data; + } + + /* DATA SOURCING */ + + /** + * Get data from input fields and store them in an analyzerData object. This object will be used to fill + * the analyzer and the snippet preview. + * + * @param {function} callback To declare either ready or reloaded after parsing. + * + * @returns {void} + */ + loadShortcodes( callback ) { + const unparsedShortcodes = this.getUnparsedShortcodes( this.getShortcodes( this.getContentTinyMCE() ) ); + if ( unparsedShortcodes.length > 0 ) { + this.parseShortcodes( unparsedShortcodes, callback ); + } else { + return callback(); + } + } + + /** + * Bind elements to be able to reload the dataset if shortcodes get added. + * + * @returns {void} + */ + bindElementEvents() { + const contentElement = document.getElementById( "content" ) || false; + const callback = _.debounce( this.loadShortcodes.bind( this, this.declareReloaded.bind( this ) ), 500 ); + + if ( contentElement ) { + contentElement.addEventListener( "keyup", callback ); + contentElement.addEventListener( "change", callback ); + } + + if ( typeof tinyMCE !== "undefined" && typeof tinyMCE.on === "function" ) { + tinyMCE.on( "addEditor", function( e ) { + e.editor.on( "change", callback ); + e.editor.on( "keyup", callback ); + } ); + } + } + + /** + * Gets content from the content field, if tinyMCE is initialized, use the getContent function to + * get the data from tinyMCE. + * + * @returns {String} The content from tinyMCE. + */ + getContentTinyMCE() { + let content = document.getElementById( "content" ) ? document.getElementById( "content" ).value : ""; + if ( typeof tinyMCE !== "undefined" && typeof tinyMCE.editors !== "undefined" && tinyMCE.editors.length !== 0 ) { + content = tinyMCE.get( "content" ) ? tinyMCE.get( "content" ).getContent() : ""; + } + + return content; + } + + /* SHORTCODE PARSING */ + + /** + * Returns the unparsed shortcodes out of a collection of shortcodes. + * + * @param {Array} shortcodes The shortcodes to check. + * + * @returns {Boolean|Array} Array with unparsed shortcodes. + */ + getUnparsedShortcodes( shortcodes ) { + const unparsedShortcodes = []; + + if ( typeof shortcodes !== "object" ) { + console.error( "Failed to get unparsed shortcodes. Expected parameter to be an array, instead received " + typeof shortcodes ); + return false; + } + + for ( let i = 0; i < shortcodes.length; i++ ) { + const shortcode = shortcodes[ i ]; + if ( unparsedShortcodes.indexOf( shortcode ) === -1 && this.isUnparsedShortcode( shortcode ) ) { + unparsedShortcodes.push( shortcode ); + } + } + + return unparsedShortcodes; + } + + /** + * Checks if a given shortcode was already parsed. + * + * @param {String} shortcode The shortcode to check. + * + * @returns {Boolean} True when shortcode is not parsed yet. + */ + isUnparsedShortcode( shortcode ) { + let alreadyExists = false; + + for ( let i = 0; i < this.parsedShortcodes.length; i++ ) { + if ( this.parsedShortcodes[ i ].shortcode === shortcode ) { + alreadyExists = true; + } + } + + return alreadyExists === false; + } + + /** + * Gets the shortcodes from a given piece of text. + * + * @param {String} text Text to extract shortcodes from. + * + * @returns {Boolean|Array} The matched shortcodes. + */ + getShortcodes( text ) { + if ( typeof text !== "string" ) { + console.error( "Failed to get shortcodes. Expected parameter to be a string, instead received" + typeof text ); + return false; + } + + const captures = this.matchCapturingShortcodes( text ); + + // Remove the capturing shortcodes from the text before trying to match the capturing shortcodes. + for ( let i = 0; i < captures.length; i++ ) { + text = text.replace( captures[ i ], "" ); + } + + const nonCaptures = this.matchNonCapturingShortcodes( text ); + + return captures.concat( nonCaptures ); + } + + /** + * Matches the capturing shortcodes from a given piece of text. + * + * @param {String} text Text to get the capturing shortcodes from. + * + * @returns {Array} The capturing shortcodes. + */ + matchCapturingShortcodes( text ) { + let captures = []; + + // First identify which tags are being used in a capturing shortcode by looking for closing tags. + const captureKeywords = ( text.match( this.closingTagRegex ) || [] ).join( " " ).match( this.keywordRegex ) || []; + + // Fetch the capturing shortcodes and strip them from the text, so we can easily match the non-capturing shortcodes. + for ( let i = 0; i < captureKeywords.length; i++ ) { + const captureKeyword = captureKeywords[ i ]; + const captureRegex = "\\[" + captureKeyword + "[^\\]]*?\\].*?\\[\\/" + captureKeyword + "\\]"; + const matches = text.match( new RegExp( captureRegex, "g" ) ) || []; + + captures = captures.concat( matches ); + } + + return captures; + } + + /** + * Matches the non-capturing shortcodes from a given piece of text. + * + * @param {String} text Text to get the non-capturing shortcodes from. + * + * @returns {Array} The non-capturing shortcodes. + */ + matchNonCapturingShortcodes( text ) { + return text.match( this.nonCaptureRegex ) || []; + } + + /** + * Parses the unparsed shortcodes through AJAX and clears them. + * + * @param {Array} shortcodes shortcodes to be parsed. + * @param {function} callback function to be called in the context of the AJAX callback. + * + * @returns {void} + */ + parseShortcodes( shortcodes, callback ) { + if ( typeof callback !== "function" ) { + console.error( "Failed to parse shortcodes. Expected parameter to be a function, instead received " + typeof callback ); + return false; + } + + if ( typeof shortcodes === "object" && shortcodes.length > 0 ) { + jQuery.post( + ajaxurl, + { + action: "wpseo_filter_shortcodes", + _wpnonce: wpseoScriptData.analysis.plugins.shortcodes.wpseo_filter_shortcodes_nonce, + data: shortcodes, + }, + function( shortcodeResults ) { + this.saveParsedShortcodes( shortcodeResults, callback ); + }.bind( this ) + ); + } else { + return callback(); + } + } + + /** + * Saves the shortcodes that were parsed with AJAX to `this.parsedShortcodes` + * + * @param {Array} shortcodeResults Shortcodes that must be saved. + * @param {function} callback Callback to execute of saving shortcodes. + * + * @returns {void} + */ + saveParsedShortcodes( shortcodeResults, callback ) { + shortcodeResults = JSON.parse( shortcodeResults ); + for ( let i = 0; i < shortcodeResults.length; i++ ) { + this.parsedShortcodes.push( shortcodeResults[ i ] ); + } + + callback(); + } +} + + +export default YoastShortcodePlugin; From 04ef6cb6d6f22d2d74cf5779825baec3c7c241cc Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 31 Oct 2023 11:28:14 +0100 Subject: [PATCH 02/35] Parses the provided shortcodes --- packages/js/src/initializers/post-scraper.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index 80eb4102857..dbe06ec6bb5 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -8,10 +8,12 @@ import { } from "lodash"; import { isShallowEqualObjects } from "@wordpress/is-shallow-equal"; import { select, subscribe } from "@wordpress/data"; +import { applyFilters } from "@wordpress/hooks"; // Internal dependencies. import YoastReplaceVarPlugin from "../analysis/plugins/replacevar-plugin"; import YoastReusableBlocksPlugin from "../analysis/plugins/reusable-blocks-plugin"; +import YoastShortcodePlugin from "../analysis/plugins/shortcode-plugin"; import YoastMarkdownPlugin from "../analysis/plugins/markdown-plugin"; import * as tinyMCEHelper from "../lib/tinymce"; import CompatibilityHelper from "../compatibility/compatibilityHelper"; @@ -52,7 +54,6 @@ import { actions } from "@yoast/externals/redux"; // Helper dependencies. import isBlockEditor from "../helpers/isBlockEditor"; - const { setFocusKeyword, setMarkerStatus, @@ -65,6 +66,7 @@ const { // Plugin class prototypes (not the instances) are being used by other plugins from the window. window.YoastReplaceVarPlugin = YoastReplaceVarPlugin; +window.YoastShortcodePlugin = YoastShortcodePlugin; /** * @summary Initializes the post scraper script. @@ -495,6 +497,20 @@ export default function initPostScraper( $, store, editorData ) { window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); + const shortcodesToBeParsed = []; + + applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); + + // Parses the shortcodes when `shortcodesToBeParsed` is provided. + if ( shortcodesToBeParsed.length > 0 ) { + window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { + registerPlugin: app.registerPlugin, + registerModification: app.registerModification, + pluginReady: app.pluginReady, + pluginReloaded: app.pluginReloaded, + }, shortcodesToBeParsed ); + } + if ( isBlockEditor() ) { const reusableBlocksPlugin = new YoastReusableBlocksPlugin( app.registerPlugin, app.registerModification, window.YoastSEO.app.refresh ); reusableBlocksPlugin.register(); From 6f7abcf7f1b500bf1ff042fe3634cc6c05e71271 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 31 Oct 2023 11:28:39 +0100 Subject: [PATCH 03/35] Add back `wpseo_filter_shortcodes_nonce` --- admin/metabox/class-metabox.php | 3 ++- src/integrations/third-party/elementor.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/admin/metabox/class-metabox.php b/admin/metabox/class-metabox.php index 31fda910e33..b6013b9e887 100644 --- a/admin/metabox/class-metabox.php +++ b/admin/metabox/class-metabox.php @@ -893,7 +893,8 @@ public function enqueue() { 'has_taxonomies' => $this->current_post_type_has_taxonomies(), ], 'shortcodes' => [ - 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ]; diff --git a/src/integrations/third-party/elementor.php b/src/integrations/third-party/elementor.php index f5dd067a656..9b98b153cab 100644 --- a/src/integrations/third-party/elementor.php +++ b/src/integrations/third-party/elementor.php @@ -434,7 +434,8 @@ public function enqueue() { 'has_taxonomies' => $this->current_post_type_has_taxonomies(), ], 'shortcodes' => [ - 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ]; From 57240111b409dc5f2dbc302b2539b7ebf94ac648 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 31 Oct 2023 12:19:27 +0100 Subject: [PATCH 04/35] adapt code --- packages/js/src/initializers/post-scraper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index dbe06ec6bb5..9ac93768115 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -497,9 +497,9 @@ export default function initPostScraper( $, store, editorData ) { window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); - const shortcodesToBeParsed = []; + let shortcodesToBeParsed = []; - applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); + shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); // Parses the shortcodes when `shortcodesToBeParsed` is provided. if ( shortcodesToBeParsed.length > 0 ) { From 26b7c5ad14f41536f7b261fd4aa96b9b842f7fc3 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 1 Nov 2023 11:00:31 +0100 Subject: [PATCH 05/35] simplify code --- .../src/analysis/plugins/shortcode-plugin.js | 51 +++++++------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 252839610cc..4dec1402f3b 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -112,9 +112,9 @@ class YoastShortcodePlugin { const parsedShortcodes = this.parsedShortcodes; if ( typeof data === "string" && parsedShortcodes.length > 0 ) { - for ( let i = 0; i < parsedShortcodes.length; i++ ) { - data = data.replace( parsedShortcodes[ i ].shortcode, parsedShortcodes[ i ].output ); - } + parsedShortcodes.forEach( ( { shortcode, output } ) => { + data = data.replace( shortcode, output ); + } ); } data = this.removeUnknownShortCodes( data ); @@ -188,40 +188,23 @@ class YoastShortcodePlugin { * @returns {Boolean|Array} Array with unparsed shortcodes. */ getUnparsedShortcodes( shortcodes ) { - const unparsedShortcodes = []; - if ( typeof shortcodes !== "object" ) { console.error( "Failed to get unparsed shortcodes. Expected parameter to be an array, instead received " + typeof shortcodes ); return false; } - for ( let i = 0; i < shortcodes.length; i++ ) { - const shortcode = shortcodes[ i ]; - if ( unparsedShortcodes.indexOf( shortcode ) === -1 && this.isUnparsedShortcode( shortcode ) ) { - unparsedShortcodes.push( shortcode ); - } - } - - return unparsedShortcodes; + return shortcodes.filter( shortcode => this.isUnparsedShortcode( shortcode ) ); } /** * Checks if a given shortcode was already parsed. * - * @param {String} shortcode The shortcode to check. + * @param {String} shortcodeToCheck The shortcode to check. * * @returns {Boolean} True when shortcode is not parsed yet. */ - isUnparsedShortcode( shortcode ) { - let alreadyExists = false; - - for ( let i = 0; i < this.parsedShortcodes.length; i++ ) { - if ( this.parsedShortcodes[ i ].shortcode === shortcode ) { - alreadyExists = true; - } - } - - return alreadyExists === false; + isUnparsedShortcode( shortcodeToCheck ) { + return ! this.parsedShortcodes.some( ( { shortcode } ) => shortcode === shortcodeToCheck ); } /** @@ -239,10 +222,10 @@ class YoastShortcodePlugin { const captures = this.matchCapturingShortcodes( text ); - // Remove the capturing shortcodes from the text before trying to match the capturing shortcodes. - for ( let i = 0; i < captures.length; i++ ) { - text = text.replace( captures[ i ], "" ); - } + // Remove the capturing shortcodes from the text before trying to match the non-capturing shortcodes. + captures.forEach( capture => { + text = text.replace( capture, "" ); + } ); const nonCaptures = this.matchNonCapturingShortcodes( text ); @@ -263,13 +246,12 @@ class YoastShortcodePlugin { const captureKeywords = ( text.match( this.closingTagRegex ) || [] ).join( " " ).match( this.keywordRegex ) || []; // Fetch the capturing shortcodes and strip them from the text, so we can easily match the non-capturing shortcodes. - for ( let i = 0; i < captureKeywords.length; i++ ) { - const captureKeyword = captureKeywords[ i ]; + captureKeywords.forEach( captureKeyword => { const captureRegex = "\\[" + captureKeyword + "[^\\]]*?\\].*?\\[\\/" + captureKeyword + "\\]"; const matches = text.match( new RegExp( captureRegex, "g" ) ) || []; captures = captures.concat( matches ); - } + } ); return captures; } @@ -326,9 +308,10 @@ class YoastShortcodePlugin { */ saveParsedShortcodes( shortcodeResults, callback ) { shortcodeResults = JSON.parse( shortcodeResults ); - for ( let i = 0; i < shortcodeResults.length; i++ ) { - this.parsedShortcodes.push( shortcodeResults[ i ] ); - } + + shortcodeResults.forEach( result => { + this.parsedShortcodes.push( result ); + } ); callback(); } From ae91b2c46a660083ec4edbc896d6936eeb11cfd1 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Thu, 2 Nov 2023 12:38:52 +0100 Subject: [PATCH 06/35] Add initial unit tests --- .../analysis/plugins/shortcode-plugin.test.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/js/tests/analysis/plugins/shortcode-plugin.test.js diff --git a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js new file mode 100644 index 00000000000..be349a60649 --- /dev/null +++ b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js @@ -0,0 +1,30 @@ +import YoastShortcodePlugin from "../../../src/analysis/plugins/shortcode-plugin"; + +global._ = { + debounce: ( fn ) => { + fn.cancel = jest.fn(); + return fn; + }, +}; + +const mockRegisterPlugin = jest.fn(); +const mockRegisterModification = jest.fn(); +const mockPluginReady = jest.fn(); +const mockPluginReload = jest.fn(); +const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; + +const shortcodePlugin = new YoastShortcodePlugin( { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReload: mockPluginReload, +}, shortcodesToBeParsed ); + +describe( "A test for Yoast Shortcode Plugin", () => { + it( "returns true if the shortcode is unparsed", () => { + expect( shortcodePlugin.isUnparsedShortcode( "caption" ) ).toBeTruthy(); + } ); + it( "returns the unparsed shortcodes", () => { + expect( shortcodePlugin.getUnparsedShortcodes( [ "caption", "wpseo_breadcrumb" ] ) ).toEqual( [ "caption", "wpseo_breadcrumb" ] ); + } ); +} ); From a950aafaa0ab4a11858e2ea59e86c9d862a1edcd Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Thu, 2 Nov 2023 17:39:06 +0100 Subject: [PATCH 07/35] Add more unit tests --- .../analysis/plugins/shortcode-plugin.test.js | 131 ++++++++++++++---- 1 file changed, 106 insertions(+), 25 deletions(-) diff --git a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js index be349a60649..23c82fcc48f 100644 --- a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js +++ b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js @@ -1,30 +1,111 @@ import YoastShortcodePlugin from "../../../src/analysis/plugins/shortcode-plugin"; -global._ = { - debounce: ( fn ) => { - fn.cancel = jest.fn(); - return fn; - }, -}; - -const mockRegisterPlugin = jest.fn(); -const mockRegisterModification = jest.fn(); -const mockPluginReady = jest.fn(); -const mockPluginReload = jest.fn(); -const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; - -const shortcodePlugin = new YoastShortcodePlugin( { - registerPlugin: mockRegisterPlugin, - registerModification: mockRegisterModification, - pluginReady: mockPluginReady, - pluginReload: mockPluginReload, -}, shortcodesToBeParsed ); - -describe( "A test for Yoast Shortcode Plugin", () => { - it( "returns true if the shortcode is unparsed", () => { - expect( shortcodePlugin.isUnparsedShortcode( "caption" ) ).toBeTruthy(); +describe( "YoastShortcodePlugin", () => { + // Mock the functions and objects needed for the class + const mockRegisterPlugin = jest.fn(); + const mockRegisterModification = jest.fn(); + const mockPluginReady = jest.fn(); + const mockPluginReloaded = jest.fn(); + const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; + + // Mock the global objects and functions used in the class + global.tinyMCE = {}; + global.wpseoScriptData = { analysis: { plugins: { shortcodes: { wpseo_filter_shortcodes_nonce: "nonce" } } } }; + global.ajaxurl = "http://example.com/ajax"; + global._ = { debounce: jest.fn() }; + + beforeEach( () => { + mockRegisterPlugin.mockClear(); + mockRegisterModification.mockClear(); + mockPluginReady.mockClear(); + mockPluginReloaded.mockClear(); + global._.debounce.mockClear(); + } ); + + it( "should initialize YoastShortcodePlugin and register with Yoast SEO", () => { + const plugin = new YoastShortcodePlugin( + { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReloaded: mockPluginReloaded, + }, + shortcodesToBeParsed + ); + plugin.declareReady(); + + expect( mockRegisterPlugin ).toHaveBeenCalledWith( "YoastShortcodePlugin", { status: "loading" } ); + } ); + + it( "should declare ready with YoastSEO", () => { + const plugin = new YoastShortcodePlugin( + { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReloaded: mockPluginReloaded, + }, + shortcodesToBeParsed + ); + + plugin.declareReady(); + + expect( mockPluginReady ).toHaveBeenCalledWith( "YoastShortcodePlugin" ); } ); - it( "returns the unparsed shortcodes", () => { - expect( shortcodePlugin.getUnparsedShortcodes( [ "caption", "wpseo_breadcrumb" ] ) ).toEqual( [ "caption", "wpseo_breadcrumb" ] ); + + it( "should return true if the shortcode is unparsed", () => { + const plugin = new YoastShortcodePlugin( + { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReloaded: mockPluginReloaded, + }, + shortcodesToBeParsed + ); + expect( plugin.isUnparsedShortcode( "caption" ) ).toBeTruthy(); + } ); + + it( "should return the unparsed shortcodes", () => { + const plugin = new YoastShortcodePlugin( + { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReloaded: mockPluginReloaded, + }, + shortcodesToBeParsed + ); + expect( plugin.getUnparsedShortcodes( [ "caption", "wpseo_breadcrumb" ] ) ).toEqual( [ "caption", "wpseo_breadcrumb" ] ); + } ); + + it( "should extract shortcodes from a given piece of text", () => { + const plugin = new YoastShortcodePlugin( + { + registerPlugin: mockRegisterPlugin, + registerModification: mockRegisterModification, + pluginReady: mockPluginReady, + pluginReloaded: mockPluginReloaded, + }, + shortcodesToBeParsed + ); + + // Test input text with shortcodes + const inputText = "This is a sample [wpseo_breadcrumb] text with an image with caption [caption id=\"attachment_8\" align=\"alignnone\"" + + " width=\"230\"]A tortie cat, not wayang kulit" + + " A tortie cat, not a red panda.[/caption]."; + + // Call the getShortcodes method + const shortcodes = plugin.getShortcodes( inputText ); + + // Expect the extracted shortcodes + expect( shortcodes ).toEqual( [ + "[caption id=\"attachment_8\" align=\"alignnone\" " + + "width=\"230\"]A tortie cat, not wayang kulit" + + " A tortie cat, not a red panda.[/caption]", + "[wpseo_breadcrumb]" ] ); } ); } ); + From 4a1c30fdd08897d81043e42753d6d34a1937fc0c Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Fri, 3 Nov 2023 09:36:57 +0100 Subject: [PATCH 08/35] Add more unit tests --- .../analysis/plugins/shortcode-plugin.test.js | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js index 23c82fcc48f..e4bbe736564 100644 --- a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js +++ b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js @@ -1,19 +1,19 @@ import YoastShortcodePlugin from "../../../src/analysis/plugins/shortcode-plugin"; -describe( "YoastShortcodePlugin", () => { - // Mock the functions and objects needed for the class - const mockRegisterPlugin = jest.fn(); - const mockRegisterModification = jest.fn(); - const mockPluginReady = jest.fn(); - const mockPluginReloaded = jest.fn(); - const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; - - // Mock the global objects and functions used in the class - global.tinyMCE = {}; - global.wpseoScriptData = { analysis: { plugins: { shortcodes: { wpseo_filter_shortcodes_nonce: "nonce" } } } }; - global.ajaxurl = "http://example.com/ajax"; - global._ = { debounce: jest.fn() }; +// Mock the functions and objects needed for the class +const mockRegisterPlugin = jest.fn(); +const mockRegisterModification = jest.fn(); +const mockPluginReady = jest.fn(); +const mockPluginReloaded = jest.fn(); +const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; + +// Mock the global objects and functions used in the class +global.tinyMCE = {}; +global.wpseoScriptData = { analysis: { plugins: { shortcodes: { wpseo_filter_shortcodes_nonce: "nonce" } } } }; +global.ajaxurl = "http://example.com/ajax"; +global._ = { debounce: jest.fn() }; +describe( "YoastShortcodePlugin", () => { beforeEach( () => { mockRegisterPlugin.mockClear(); mockRegisterModification.mockClear(); @@ -90,16 +90,16 @@ describe( "YoastShortcodePlugin", () => { shortcodesToBeParsed ); - // Test input text with shortcodes + // Test input text with shortcodes. const inputText = "This is a sample [wpseo_breadcrumb] text with an image with caption [caption id=\"attachment_8\" align=\"alignnone\"" + " width=\"230\"]A tortie cat, not wayang kulit" + " A tortie cat, not a red panda.[/caption]."; - // Call the getShortcodes method + // Call the getShortcodes method. const shortcodes = plugin.getShortcodes( inputText ); - // Expect the extracted shortcodes + // Expect the extracted shortcodes. expect( shortcodes ).toEqual( [ "[caption id=\"attachment_8\" align=\"alignnone\" " + "width=\"230\"]A tortie cat, not wayang kulit" + + " A tortie cat, not a red panda.[/caption]."; + const capturingShortcodes = plugin.matchCapturingShortcodes( inputText ); + expect( capturingShortcodes ).toEqual( [ + "[caption id=\"attachment_8\" align=\"alignnone\" width=\"230\"]" + + "A tortie cat, " +
+			"not wayang kulit A tortie cat, not a red panda.[/caption]", + ] ); + } ); + + it( "should match and return non-capturing shortcodes from the content", () => { + const inputText = "This is a sample [wpseo_breadcrumb] text with an image with caption [caption id=\"attachment_8\" align=\"alignnone\"" + + " width=\"230\"]A tortie cat, not wayang kulit" + + " A tortie cat, not a red panda.[/caption]."; + const capturingShortcodes = plugin.matchNonCapturingShortcodes( inputText ); + expect( capturingShortcodes ).toEqual( [ + "[wpseo_breadcrumb]", + "[caption id=\"attachment_8\" align=\"alignnone\" width=\"230\"]", + ] ); + } ); + it( "should extract shortcodes from a given piece of text", () => { // Test input text with shortcodes. const inputText = "This is a sample [wpseo_breadcrumb] text with an image with caption [caption id=\"attachment_8\" align=\"alignnone\"" + @@ -75,7 +119,7 @@ describe( "YoastShortcodePlugin", () => { "[wpseo_breadcrumb]" ] ); } ); - it( "should parse shortcodes through AJAX and save parsed shortcodes", ( done ) => { + it( "should parse shortcodes through AJAX and save parsed shortcodes to `parsedShortcodes` array", ( done ) => { // Simulate an AJAX response with parsed shortcodes const shortcodeResults = JSON.stringify( [ { shortcode: "[shortcode1]", output: "Parsed Output 1" }, @@ -95,6 +139,11 @@ describe( "YoastShortcodePlugin", () => { plugin.parseShortcodes( [ "[shortcode1]", "[shortcode2]" ], () => { // Expect that saveParsedShortcodes was called with the correct parsed shortcodes. expect( saveParsedShortcodes ).toHaveBeenCalledWith( shortcodeResults, expect.any( Function ) ); + // Expect that shortcode results are saved in `parsedShortcodes` array. + expect( plugin.parsedShortcodes ).toEqual( [ + { shortcode: "[shortcode1]", output: "Parsed Output 1" }, + { shortcode: "[shortcode2]", output: "Parsed Output 2" }, + ] ); done(); } ); From 0c0cee9fdc7b139640c1b73bad7f7ae45f48b62e Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Mon, 6 Nov 2023 16:01:44 +0100 Subject: [PATCH 12/35] use global function --- admin/metabox/class-metabox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/metabox/class-metabox.php b/admin/metabox/class-metabox.php index 08498ce80c7..12cbfceb556 100644 --- a/admin/metabox/class-metabox.php +++ b/admin/metabox/class-metabox.php @@ -894,7 +894,7 @@ public function enqueue() { ], 'shortcodes' => [ 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), - 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ), + 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ]; From efe2df86bc846c12e8dc314ce6cf46c6587f3afa Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Mon, 6 Nov 2023 16:02:24 +0100 Subject: [PATCH 13/35] Pass the mark button status --- packages/js/src/containers/Results.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/js/src/containers/Results.js b/packages/js/src/containers/Results.js index 70f77b91265..ae80991eb25 100644 --- a/packages/js/src/containers/Results.js +++ b/packages/js/src/containers/Results.js @@ -7,11 +7,13 @@ export default compose( [ const { getActiveMarker, getIsPremium, + getMarksButtonStatus, } = select( "yoast-seo/editor" ); return { activeMarker: getActiveMarker(), isPremium: getIsPremium(), + marksButtonStatus: getMarksButtonStatus(), }; } ), withDispatch( dispatch => { From f65ab9b0d38f37b4b3955056874eb8404be41cc6 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Mon, 6 Nov 2023 16:05:17 +0100 Subject: [PATCH 14/35] Disable the mark button when the filter is used --- packages/js/src/initializers/post-scraper.js | 34 +++++++++++--------- packages/js/src/lib/tinymce.js | 6 ++-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index 9ac93768115..4d0a2799e3a 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -132,24 +132,25 @@ export default function initPostScraper( $, store, editorData ) { /** * Determines if markers should be shown. - * + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * @returns {boolean} True when markers should be shown. */ - function displayMarkers() { - return ! isBlockEditor() && wpseoScriptData.metabox.show_markers === "1"; + function displayMarkers( shortcodesToBeParsed ) { + return ! isBlockEditor() && wpseoScriptData.metabox.show_markers === "1" && shortcodesToBeParsed.length === 0; } /** * Updates the store to indicate if the markers should be hidden. * * @param {Object} store The store. + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * * @returns {void} */ - function updateMarkerStatus( store ) { + function updateMarkerStatus( store, shortcodesToBeParsed ) { // Only add markers when tinyMCE is loaded and show_markers is enabled (can be disabled by a WordPress hook). // Only check for the tinyMCE object because the actual editor isn't loaded at this moment yet. - if ( typeof window.tinyMCE === "undefined" || ! displayMarkers() ) { + if ( typeof window.tinyMCE === "undefined" || ! displayMarkers( shortcodesToBeParsed ) || shortcodesToBeParsed.length > 0 ) { store.dispatch( setMarkerStatus( "disabled" ) ); } } @@ -255,11 +256,12 @@ export default function initPostScraper( $, store, editorData ) { * Returns the arguments necessary to initialize the app. * * @param {Object} store The store. + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * * @returns {Object} The arguments to initialize the app */ - function getAppArgs( store ) { - updateMarkerStatus( store ); + function getAppArgs( store, shortcodesToBeParsed ) { + updateMarkerStatus( store, shortcodesToBeParsed ); const args = { // ID's of elements that need to trigger updating the analyzer. elementTarget: [ @@ -371,10 +373,10 @@ export default function initPostScraper( $, store, editorData ) { /** * Handles page builder compatibility, regarding the marker buttons. - * + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * @returns {void} */ - function handlePageBuilderCompatibility() { + function handlePageBuilderCompatibility( shortcodesToBeParsed ) { const compatibilityHelper = new CompatibilityHelper(); if ( compatibilityHelper.isClassicEditorHidden() ) { @@ -390,7 +392,7 @@ export default function initPostScraper( $, store, editorData ) { }, classicEditorShown: () => { if ( ! tinyMCEHelper.isTextViewActive() ) { - tinyMCEHelper.enableMarkerButtons(); + tinyMCEHelper.enableMarkerButtons( shortcodesToBeParsed ); } }, } ); @@ -434,7 +436,11 @@ export default function initPostScraper( $, store, editorData ) { tinyMCEHelper.setStore( store ); tinyMCEHelper.wpTextViewOnInitCheck(); - handlePageBuilderCompatibility(); + let shortcodesToBeParsed = []; + + shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); + + handlePageBuilderCompatibility( shortcodesToBeParsed ); // Avoid error when snippet metabox is not rendered. if ( metaboxContainer.length === 0 ) { @@ -444,7 +450,7 @@ export default function initPostScraper( $, store, editorData ) { postDataCollector = initializePostDataCollector( editorData ); publishBox.initialize(); - const appArgs = getAppArgs( store ); + const appArgs = getAppArgs( store, shortcodesToBeParsed ); app = new App( appArgs ); // Content analysis @@ -497,10 +503,6 @@ export default function initPostScraper( $, store, editorData ) { window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); - let shortcodesToBeParsed = []; - - shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); - // Parses the shortcodes when `shortcodesToBeParsed` is provided. if ( shortcodesToBeParsed.length > 0 ) { window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { diff --git a/packages/js/src/lib/tinymce.js b/packages/js/src/lib/tinymce.js index e593cf22010..86c413eda88 100644 --- a/packages/js/src/lib/tinymce.js +++ b/packages/js/src/lib/tinymce.js @@ -147,11 +147,11 @@ export function disableMarkerButtons() { /** * Calls the function in the YoastSEO.js app that enables the marker (eye)icons. - * + * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * @returns {void} */ -export function enableMarkerButtons() { - if ( ! isUndefined( store ) ) { +export function enableMarkerButtons( shortcodesToBeParsed ) { + if ( ! isUndefined( store ) && shortcodesToBeParsed.length === 0 ) { store.dispatch( actions.setMarkerStatus( "enabled" ) ); } } From 8020be7478e73e804daa20dfd6b5925d4183e8e3 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Mon, 6 Nov 2023 16:06:36 +0100 Subject: [PATCH 15/35] remove console.log --- packages/js/src/analysis/plugins/shortcode-plugin.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 9a992038c9f..4dec1402f3b 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -282,7 +282,6 @@ class YoastShortcodePlugin { } if ( typeof shortcodes === "object" && shortcodes.length > 0 ) { - console.log( "AJAX URL", ajaxurl ); jQuery.post( ajaxurl, { @@ -308,7 +307,6 @@ class YoastShortcodePlugin { * @returns {void} */ saveParsedShortcodes( shortcodeResults, callback ) { - console.log( "SHORTCODE RESULTS", shortcodeResults ); shortcodeResults = JSON.parse( shortcodeResults ); shortcodeResults.forEach( result => { From 23ddc3c8af83d3ddeac6ad6d3802621c9b0b4b80 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 8 Nov 2023 10:10:10 +0100 Subject: [PATCH 16/35] Add shortcodes for parsing to the store --- packages/js/src/redux/actions/analysis.js | 15 +++++++++++++++ packages/js/src/redux/reducers/analysisData.js | 8 +++++++- packages/js/src/redux/selectors/analysis.js | 8 ++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/js/src/redux/actions/analysis.js b/packages/js/src/redux/actions/analysis.js index 6ba63dc0aba..3665431bf9a 100644 --- a/packages/js/src/redux/actions/analysis.js +++ b/packages/js/src/redux/actions/analysis.js @@ -1,5 +1,6 @@ export const UPDATE_SNIPPET_DATA = "SNIPPET_EDITOR_UPDATE_ANALYSIS_DATA"; export const RUN_ANALYSIS = "RUN_ANALYSIS"; +export const UPDATE_SHORTCODES_FOR_PARSING = "UPDATE_SHORTCODES_FOR_PARSING"; /** * Updates the analysis data in redux. @@ -26,3 +27,17 @@ export function runAnalysis() { timestamp: Date.now(), }; } + +/** + * Updates the list of shortcodes for parsing. + * + * @param {Array} shortcodes The list of shortcodes for parsing. + * + * @returns {Object} An action to dispatch. + */ +export function updateShortcodesForParsing( shortcodes ) { + return { + type: UPDATE_SHORTCODES_FOR_PARSING, + shortcodesForParsing: shortcodes, + }; +} diff --git a/packages/js/src/redux/reducers/analysisData.js b/packages/js/src/redux/reducers/analysisData.js index f24dc98d83c..7de5b53bbea 100644 --- a/packages/js/src/redux/reducers/analysisData.js +++ b/packages/js/src/redux/reducers/analysisData.js @@ -1,8 +1,9 @@ -import { UPDATE_SNIPPET_DATA, RUN_ANALYSIS } from "../actions"; +import { UPDATE_SNIPPET_DATA, RUN_ANALYSIS, UPDATE_SHORTCODES_FOR_PARSING } from "../actions"; const INITIAL_STATE = { snippet: {}, timestamp: 0, + shortcodesForParsing: [], }; /** @@ -25,6 +26,11 @@ function analysisDataReducer( state = INITIAL_STATE, action ) { ...state, timestamp: action.timestamp, }; + case UPDATE_SHORTCODES_FOR_PARSING: + return { + ...state, + shortcodesForParsing: action.shortcodesForParsing, + }; } return state; diff --git a/packages/js/src/redux/selectors/analysis.js b/packages/js/src/redux/selectors/analysis.js index 72443b4a994..392f64747cc 100644 --- a/packages/js/src/redux/selectors/analysis.js +++ b/packages/js/src/redux/selectors/analysis.js @@ -42,6 +42,14 @@ export const getPermalink = ( state ) => get( state, "analysisData.snippet.url", */ export const getAnalysisTimestamp = ( state ) => parseInt( get( state, "analysisData.timestamp", 0 ), 10 ); +/** + * Gets the list of shortcodes that will be parsed. + * + * @param {Object} state The state. + * @returns {string[]} And array of shortcodes that will be parsed. + */ +export const getShortcodesForParsing = ( state ) => get( state, "analysisData.shortcodesForParsing", [] ); + /** * Gets the analysis data. * From 0f20e5dfc761a3f4fb4d8729b843f9e1bd3b8d94 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 8 Nov 2023 10:11:26 +0100 Subject: [PATCH 17/35] Disable the highlighting button when there are shortcodes to be parsed --- .../components/contentAnalysis/InclusiveLanguageAnalysis.js | 6 +++++- .../src/components/contentAnalysis/ReadabilityAnalysis.js | 6 +++++- packages/js/src/components/contentAnalysis/SeoAnalysis.js | 5 ++++- packages/js/src/containers/Results.js | 2 -- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js index 6b065bfc123..d4e5e360846 100644 --- a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js +++ b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js @@ -84,7 +84,7 @@ const InclusiveLanguageAnalysis = ( props ) => { 0 ? "disabled" : props.marksButtonStatus } resultCategoryLabels={ { problems: __( "Non-inclusive phrases", "wordpress-seo" ), improvements: __( "Potentially non-inclusive phrases", "wordpress-seo" ), @@ -232,21 +232,25 @@ InclusiveLanguageAnalysis.propTypes = { // eslint-disable-next-line react/no-unused-prop-types marksButtonStatus: PropTypes.oneOf( [ "enabled", "disabled", "hidden" ] ).isRequired, overallScore: PropTypes.number, + shortcodesForParsing: PropTypes.array, }; InclusiveLanguageAnalysis.defaultProps = { results: [], overallScore: null, + shortcodesForParsing: [], }; export default withSelect( select => { const { getInclusiveLanguageResults, getMarkButtonStatus, + getShortcodesForParsing, } = select( "yoast-seo/editor" ); return { ...getInclusiveLanguageResults(), marksButtonStatus: getMarkButtonStatus(), + shortcodesForParsing: getShortcodesForParsing(), }; } )( InclusiveLanguageAnalysis ); diff --git a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js index 0b44e9aa110..54179f87fbb 100644 --- a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js +++ b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js @@ -66,7 +66,7 @@ class ReadabilityAnalysis extends Component { results={ this.props.results } upsellResults={ upsellResults } marksButtonClassName="yoast-tooltip yoast-tooltip-w" - marksButtonStatus={ this.props.marksButtonStatus } + marksButtonStatus={ this.props.shortcodesForParsing.length > 0 ? "disabled" : this.props.marksButtonStatus } /> ); @@ -183,21 +183,25 @@ ReadabilityAnalysis.propTypes = { marksButtonStatus: PropTypes.string.isRequired, overallScore: PropTypes.number, shouldUpsell: PropTypes.bool, + shortcodesForParsing: PropTypes.array, }; ReadabilityAnalysis.defaultProps = { overallScore: null, shouldUpsell: false, + shortcodesForParsing: [], }; export default withSelect( select => { const { getReadabilityResults, getMarkButtonStatus, + getShortcodesForParsing, } = select( "yoast-seo/editor" ); return { ...getReadabilityResults(), marksButtonStatus: getMarkButtonStatus(), + shortcodesForParsing: getShortcodesForParsing(), }; } )( ReadabilityAnalysis ); diff --git a/packages/js/src/components/contentAnalysis/SeoAnalysis.js b/packages/js/src/components/contentAnalysis/SeoAnalysis.js index 3fdd17080c2..b5ee7dc6754 100644 --- a/packages/js/src/components/contentAnalysis/SeoAnalysis.js +++ b/packages/js/src/components/contentAnalysis/SeoAnalysis.js @@ -265,6 +265,7 @@ SeoAnalysis.propTypes = { shouldUpsell: PropTypes.bool, shouldUpsellWordFormRecognition: PropTypes.bool, overallScore: PropTypes.number, + shortcodesForParsing: PropTypes.array, }; SeoAnalysis.defaultProps = { @@ -274,6 +275,7 @@ SeoAnalysis.defaultProps = { shouldUpsell: false, shouldUpsellWordFormRecognition: false, overallScore: null, + shortcodesForParsing: [], }; export default withSelect( ( select, ownProps ) => { @@ -281,13 +283,14 @@ export default withSelect( ( select, ownProps ) => { getFocusKeyphrase, getMarksButtonStatus, getResultsForKeyword, + getShortcodesForParsing, } = select( "yoast-seo/editor" ); const keyword = getFocusKeyphrase(); return { ...getResultsForKeyword( keyword ), - marksButtonStatus: ownProps.hideMarksButtons ? "disabled" : getMarksButtonStatus(), + marksButtonStatus: ownProps.hideMarksButtons || getShortcodesForParsing().length > 0 ? "disabled" : getMarksButtonStatus(), keyword, }; } )( SeoAnalysis ); diff --git a/packages/js/src/containers/Results.js b/packages/js/src/containers/Results.js index ae80991eb25..70f77b91265 100644 --- a/packages/js/src/containers/Results.js +++ b/packages/js/src/containers/Results.js @@ -7,13 +7,11 @@ export default compose( [ const { getActiveMarker, getIsPremium, - getMarksButtonStatus, } = select( "yoast-seo/editor" ); return { activeMarker: getActiveMarker(), isPremium: getIsPremium(), - marksButtonStatus: getMarksButtonStatus(), }; } ), withDispatch( dispatch => { From 298ca1ac19b870575c8b5040ffdad75ce2b339b5 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 8 Nov 2023 10:24:03 +0100 Subject: [PATCH 18/35] Update the shortcodes for parsing in the store --- packages/js/src/initializers/post-scraper.js | 33 ++++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index 4d0a2799e3a..946aa87fa63 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -62,6 +62,7 @@ const { refreshSnippetEditor, setReadabilityResults, setSeoResultsForKeyword, + updateShortcodesForParsing, } = actions; // Plugin class prototypes (not the instances) are being used by other plugins from the window. @@ -132,25 +133,24 @@ export default function initPostScraper( $, store, editorData ) { /** * Determines if markers should be shown. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. + * * @returns {boolean} True when markers should be shown. */ - function displayMarkers( shortcodesToBeParsed ) { - return ! isBlockEditor() && wpseoScriptData.metabox.show_markers === "1" && shortcodesToBeParsed.length === 0; + function displayMarkers() { + return ! isBlockEditor() && wpseoScriptData.metabox.show_markers; } /** * Updates the store to indicate if the markers should be hidden. * * @param {Object} store The store. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * * @returns {void} */ - function updateMarkerStatus( store, shortcodesToBeParsed ) { + function updateMarkerStatus( store ) { // Only add markers when tinyMCE is loaded and show_markers is enabled (can be disabled by a WordPress hook). // Only check for the tinyMCE object because the actual editor isn't loaded at this moment yet. - if ( typeof window.tinyMCE === "undefined" || ! displayMarkers( shortcodesToBeParsed ) || shortcodesToBeParsed.length > 0 ) { + if ( typeof window.tinyMCE === "undefined" || ! displayMarkers() ) { store.dispatch( setMarkerStatus( "disabled" ) ); } } @@ -256,12 +256,11 @@ export default function initPostScraper( $, store, editorData ) { * Returns the arguments necessary to initialize the app. * * @param {Object} store The store. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * * @returns {Object} The arguments to initialize the app */ - function getAppArgs( store, shortcodesToBeParsed ) { - updateMarkerStatus( store, shortcodesToBeParsed ); + function getAppArgs( store ) { + updateMarkerStatus( store ); const args = { // ID's of elements that need to trigger updating the analyzer. elementTarget: [ @@ -373,10 +372,9 @@ export default function initPostScraper( $, store, editorData ) { /** * Handles page builder compatibility, regarding the marker buttons. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. * @returns {void} */ - function handlePageBuilderCompatibility( shortcodesToBeParsed ) { + function handlePageBuilderCompatibility() { const compatibilityHelper = new CompatibilityHelper(); if ( compatibilityHelper.isClassicEditorHidden() ) { @@ -392,7 +390,7 @@ export default function initPostScraper( $, store, editorData ) { }, classicEditorShown: () => { if ( ! tinyMCEHelper.isTextViewActive() ) { - tinyMCEHelper.enableMarkerButtons( shortcodesToBeParsed ); + tinyMCEHelper.enableMarkerButtons(); } }, } ); @@ -436,11 +434,8 @@ export default function initPostScraper( $, store, editorData ) { tinyMCEHelper.setStore( store ); tinyMCEHelper.wpTextViewOnInitCheck(); - let shortcodesToBeParsed = []; - - shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); - handlePageBuilderCompatibility( shortcodesToBeParsed ); + handlePageBuilderCompatibility(); // Avoid error when snippet metabox is not rendered. if ( metaboxContainer.length === 0 ) { @@ -450,7 +445,7 @@ export default function initPostScraper( $, store, editorData ) { postDataCollector = initializePostDataCollector( editorData ); publishBox.initialize(); - const appArgs = getAppArgs( store, shortcodesToBeParsed ); + const appArgs = getAppArgs( store ); app = new App( appArgs ); // Content analysis @@ -502,9 +497,13 @@ export default function initPostScraper( $, store, editorData ) { // Analysis plugins window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); + let shortcodesToBeParsed = []; + shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); // Parses the shortcodes when `shortcodesToBeParsed` is provided. if ( shortcodesToBeParsed.length > 0 ) { + store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); + window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { registerPlugin: app.registerPlugin, registerModification: app.registerModification, From 5fcf2a90c608eaebfa71629202d0b7bf102ec988 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 8 Nov 2023 10:27:00 +0100 Subject: [PATCH 19/35] Adapt code --- packages/js/src/lib/tinymce.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/js/src/lib/tinymce.js b/packages/js/src/lib/tinymce.js index 86c413eda88..013ede23d20 100644 --- a/packages/js/src/lib/tinymce.js +++ b/packages/js/src/lib/tinymce.js @@ -15,14 +15,14 @@ let store; * * @type {string} */ -export var tmceId = "content"; +export const tmceId = "content"; /** * The HTML 'id' attribute for the tinyMCE editor on the edit term page. * * @type {string} */ -export var termsTmceId = "description"; +export const termsTmceId = "description"; /** * Sets the store. @@ -46,7 +46,7 @@ function tinyMCEElementContent( contentID ) { } /** - * Returns whether or not the tinyMCE script is available on the page. + * Returns whether the tinyMCE script is available on the page. * * @returns {boolean} True when tinyMCE is loaded. */ @@ -73,7 +73,7 @@ function isTinyMCEBodyAvailable( editorID ) { } /** - * Returns whether or not a tinyMCE editor with the given ID is available. + * Returns whether a tinyMCE editor with the given ID is available. * * @param {string} editorID The ID of the tinyMCE editor. * @@ -84,7 +84,7 @@ export function isTinyMCEAvailable( editorID ) { return false; } - var editor = tinyMCE.get( editorID ); + const editor = tinyMCE.get( editorID ); return ( editor !== null && ! editor.isHidden() @@ -98,7 +98,7 @@ export function isTinyMCEAvailable( editorID ) { */ export function getContentTinyMce( contentID ) { // If no TinyMCE object available - var content = ""; + let content = ""; if ( isTinyMCEAvailable( contentID ) === false || isTinyMCEBodyAvailable( contentID ) === false ) { content = tinyMCEElementContent( contentID ); } else { @@ -122,7 +122,7 @@ export function addEventHandler( editorId, events, callback ) { } tinyMCE.on( "addEditor", function( evt ) { - var editor = evt.editor; + const editor = evt.editor; if ( editor.id !== editorId ) { return; @@ -147,11 +147,11 @@ export function disableMarkerButtons() { /** * Calls the function in the YoastSEO.js app that enables the marker (eye)icons. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. + * * @returns {void} */ -export function enableMarkerButtons( shortcodesToBeParsed ) { - if ( ! isUndefined( store ) && shortcodesToBeParsed.length === 0 ) { +export function enableMarkerButtons() { + if ( ! isUndefined( store ) ) { store.dispatch( actions.setMarkerStatus( "enabled" ) ); } } From 60c06302760bee4631acf55a1265b0da3297c2d1 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 8 Nov 2023 14:10:12 +0100 Subject: [PATCH 20/35] Add unit test --- .../js/tests/redux/reducers/analysisData.test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/js/tests/redux/reducers/analysisData.test.js b/packages/js/tests/redux/reducers/analysisData.test.js index 4767d6f4884..66747335de2 100644 --- a/packages/js/tests/redux/reducers/analysisData.test.js +++ b/packages/js/tests/redux/reducers/analysisData.test.js @@ -1,11 +1,11 @@ -import { updateAnalysisData } from "../../../src/redux/actions/analysis"; +import { updateAnalysisData, updateShortcodesForParsing } from "../../../src/redux/actions/analysis"; import analysisDataReducer from "../../../src/redux/reducers/analysisData"; describe( "Analysis data reducer", () => { it( "has a default state", () => { const result = analysisDataReducer( undefined, { type: "undefined" } ); - expect( result ).toEqual( { snippet: {}, timestamp: 0 } ); + expect( result ).toEqual( { snippet: {}, timestamp: 0, shortcodesForParsing: [] } ); } ); it( "ignores unrecognized actions", () => { @@ -24,4 +24,13 @@ describe( "Analysis data reducer", () => { expect( result ).toEqual( expected ); } ); + + it( "updates the shortcodes for parsing", () => { + const action = updateShortcodesForParsing( [ "wpseo_breadcrumb" ] ); + const expected = { shortcodesForParsing: [ "wpseo_breadcrumb" ] }; + + const result = analysisDataReducer( {}, action ); + + expect( result ).toEqual( expected ); + } ); } ); From faebc781685a0dcbb89427a2287295a582a55b5e Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Thu, 9 Nov 2023 11:53:36 +0100 Subject: [PATCH 21/35] Adjust code comments --- packages/js/src/analysis/plugins/shortcode-plugin.js | 9 ++++++--- .../js/tests/analysis/plugins/shortcode-plugin.test.js | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 4dec1402f3b..127ab777425 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -102,6 +102,7 @@ class YoastShortcodePlugin { } /** + * Replaces the unparsed shortcodes with the parsed ones. * The callback used to replace the shortcodes. * * @param {String} data The text to replace the shortcodes in. @@ -125,7 +126,7 @@ class YoastShortcodePlugin { /* DATA SOURCING */ /** - * Get data from input fields and store them in an analyzerData object. This object will be used to fill + * Gets data from input fields and store them in an analyzerData object. This object will be used to fill * the analyzer and the snippet preview. * * @param {function} callback To declare either ready or reloaded after parsing. @@ -142,7 +143,7 @@ class YoastShortcodePlugin { } /** - * Bind elements to be able to reload the dataset if shortcodes get added. + * Binds elements to be able to reload the dataset if shortcodes get added. * * @returns {void} */ @@ -301,14 +302,16 @@ class YoastShortcodePlugin { /** * Saves the shortcodes that were parsed with AJAX to `this.parsedShortcodes` * - * @param {Array} shortcodeResults Shortcodes that must be saved. + * @param {String} shortcodeResults Shortcodes that must be saved. This is the response from the jQuery. * @param {function} callback Callback to execute of saving shortcodes. * * @returns {void} */ saveParsedShortcodes( shortcodeResults, callback ) { + // Parse the stringified shortcode results to an array. shortcodeResults = JSON.parse( shortcodeResults ); + // Push each shortcode result to the array of parsed shortcodes. shortcodeResults.forEach( result => { this.parsedShortcodes.push( result ); } ); diff --git a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js index 0f1432a5e4e..417de7657bd 100644 --- a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js +++ b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js @@ -2,14 +2,13 @@ import YoastShortcodePlugin from "../../../src/analysis/plugins/shortcode-plugin describe( "YoastShortcodePlugin", () => { let plugin; - // Mock the functions and objects needed for the class + // Mock the functions and objects needed for the class. const mockRegisterPlugin = jest.fn(); const mockRegisterModification = jest.fn(); const mockPluginReady = jest.fn(); const mockPluginReloaded = jest.fn(); const shortcodesToBeParsed = [ "caption", "wpseo_breadcrumb" ]; - // Mock the global objects and functions used in the class global.tinyMCE = {}; // eslint-disable-next-line camelcase global.wpseoScriptData = { analysis: { plugins: { shortcodes: { wpseo_filter_shortcodes_nonce: "nonce" } } } }; From afc7ced817010b8eb785edecabe7946103c63e1e Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Thu, 9 Nov 2023 13:43:49 +0100 Subject: [PATCH 22/35] Only include valid shortcodes --- packages/js/src/initializers/post-scraper.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index 946aa87fa63..c2601d3bf56 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -500,6 +500,12 @@ export default function initPostScraper( $, store, editorData ) { let shortcodesToBeParsed = []; shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); + + // Make sure the added shortcodes are valid. They are valid if they are included in `wpseo_shortcode_tags`. + shortcodesToBeParsed = shortcodesToBeParsed.filter( shortcode => { + return wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags.includes( shortcode ); + } ); + // Parses the shortcodes when `shortcodesToBeParsed` is provided. if ( shortcodesToBeParsed.length > 0 ) { store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); From 0b2f853729d380f19f18b4309a746087527c52bb Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Thu, 9 Nov 2023 17:19:08 +0100 Subject: [PATCH 23/35] Use the right Pluggable --- packages/js/src/elementor/initializers/pluggable.js | 7 +++++-- packages/js/src/initializers/post-scraper.js | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/js/src/elementor/initializers/pluggable.js b/packages/js/src/elementor/initializers/pluggable.js index b5a947b34f8..c4467de3c92 100644 --- a/packages/js/src/elementor/initializers/pluggable.js +++ b/packages/js/src/elementor/initializers/pluggable.js @@ -11,8 +11,11 @@ let pluggable = null; */ const getPluggable = () => { if ( pluggable === null ) { - const refresh = dispatch( "yoast-seo/editor" ).runAnalysis; - pluggable = new Pluggable( refresh ); + // const refresh = dispatch( "yoast-seo/editor" ).runAnalysis; + // pluggable = new Pluggable( refresh ); + // Instead of initializing a new Pluggable plugin, here we need to use the initialized in post scrapper. + // I think this way we can get the registered modifications. + pluggable = window.YoastSEO.app.pluggable; } return pluggable; diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index c2601d3bf56..b5cbd0392e4 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -458,7 +458,7 @@ export default function initPostScraper( $, store, editorData ) { editorData, store, customAnalysisData, - app.pluggable, + window.YoastSEO.app.pluggable, select( "core/block-editor" ) ); window.YoastSEO.analysis.applyMarks = ( paper, marks ) => getApplyMarks()( paper, marks ); @@ -511,10 +511,10 @@ export default function initPostScraper( $, store, editorData ) { store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { - registerPlugin: app.registerPlugin, - registerModification: app.registerModification, - pluginReady: app.pluginReady, - pluginReloaded: app.pluginReloaded, + registerPlugin: window.YoastSEO.app.registerPlugin, + registerModification: window.YoastSEO.app.registerModification, + pluginReady: window.YoastSEO.app.pluginReady, + pluginReloaded: window.YoastSEO.app.pluginReloaded, }, shortcodesToBeParsed ); } From 6d8debaaf5fda1905620eee924ea6eb0d63facef Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Fri, 10 Nov 2023 10:48:33 +0100 Subject: [PATCH 24/35] Improve comments --- packages/js/src/elementor/initializers/pluggable.js | 10 ++-------- packages/js/src/initializers/post-scraper.js | 10 +++++----- packages/yoastseo/src/pluggable.js | 3 +++ 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/js/src/elementor/initializers/pluggable.js b/packages/js/src/elementor/initializers/pluggable.js index c4467de3c92..26b16565773 100644 --- a/packages/js/src/elementor/initializers/pluggable.js +++ b/packages/js/src/elementor/initializers/pluggable.js @@ -1,6 +1,3 @@ -import { dispatch } from "@wordpress/data"; -import Pluggable from "../../lib/Pluggable"; - // Holds the singleton used in getPluggable. let pluggable = null; @@ -11,10 +8,7 @@ let pluggable = null; */ const getPluggable = () => { if ( pluggable === null ) { - // const refresh = dispatch( "yoast-seo/editor" ).runAnalysis; - // pluggable = new Pluggable( refresh ); - // Instead of initializing a new Pluggable plugin, here we need to use the initialized in post scrapper. - // I think this way we can get the registered modifications. + // Use the initialized pluggable plugin in post-scrapper. pluggable = window.YoastSEO.app.pluggable; } @@ -63,7 +57,7 @@ export const registerModification = ( modification, callable, pluginName, priori }; /** - * Register a plugin with YoastSEO. + * Registers a plugin with YoastSEO. * * A plugin can be declared "ready" right at registration or later using `this.ready`. * diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index b5cbd0392e4..c2601d3bf56 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -458,7 +458,7 @@ export default function initPostScraper( $, store, editorData ) { editorData, store, customAnalysisData, - window.YoastSEO.app.pluggable, + app.pluggable, select( "core/block-editor" ) ); window.YoastSEO.analysis.applyMarks = ( paper, marks ) => getApplyMarks()( paper, marks ); @@ -511,10 +511,10 @@ export default function initPostScraper( $, store, editorData ) { store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { - registerPlugin: window.YoastSEO.app.registerPlugin, - registerModification: window.YoastSEO.app.registerModification, - pluginReady: window.YoastSEO.app.pluginReady, - pluginReloaded: window.YoastSEO.app.pluginReloaded, + registerPlugin: app.registerPlugin, + registerModification: app.registerModification, + pluginReady: app.pluginReady, + pluginReloaded: app.pluginReloaded, }, shortcodesToBeParsed ); } diff --git a/packages/yoastseo/src/pluggable.js b/packages/yoastseo/src/pluggable.js index f953327980f..fcb32fd0c2d 100644 --- a/packages/yoastseo/src/pluggable.js +++ b/packages/yoastseo/src/pluggable.js @@ -4,6 +4,9 @@ import InvalidTypeError from "./errors/invalidType"; /** * The plugins object takes care of plugin registrations, preloading and managing data modifications. * + * Please note that there is a newer copy of this plugin in `packages/js/src/lib/Pluggable.js`. + * For internal use, please use the newer copy for all interfaces except for registering assessments. + * * A plugin for YoastSEO.js is basically a piece of JavaScript that hooks into YoastSEO.js by registering modifications. * In order to do so, it must first register itself as a plugin with YoastSEO.js. To keep our content analysis fast, we * don't allow asynchronous modifications. That's why we require plugins to preload all data they need in order to modify From eb3d0877403191e95f683b10f885d85e1cb231de Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Fri, 10 Nov 2023 13:57:51 +0100 Subject: [PATCH 25/35] Use the initialized pluggable plugin in post-scrapper if it's available. Otherwise, initiate a new Pluggable plugin. This is because in Elementor, pluggable is not available in the window --- packages/js/src/elementor/initializers/pluggable.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/js/src/elementor/initializers/pluggable.js b/packages/js/src/elementor/initializers/pluggable.js index 26b16565773..76a53c79f0f 100644 --- a/packages/js/src/elementor/initializers/pluggable.js +++ b/packages/js/src/elementor/initializers/pluggable.js @@ -1,3 +1,5 @@ +import { dispatch } from "@wordpress/data"; +import Pluggable from "../../lib/Pluggable"; // Holds the singleton used in getPluggable. let pluggable = null; @@ -8,8 +10,12 @@ let pluggable = null; */ const getPluggable = () => { if ( pluggable === null ) { - // Use the initialized pluggable plugin in post-scrapper. - pluggable = window.YoastSEO.app.pluggable; + // Use the initialized pluggable plugin in post-scrapper if it's available. + // Otherwise, initiate a new Pluggable plugin. + const refresh = dispatch( "yoast-seo/editor" ).runAnalysis; + pluggable = window.YoastSEO.app && window.YoastSEO.app.pluggable + ? window.YoastSEO.app.pluggable + : new Pluggable( refresh ); } return pluggable; From 1861ab5c85365d8d501bde5bb31fee61bbb966f7 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Fri, 10 Nov 2023 17:07:04 +0100 Subject: [PATCH 26/35] Watch the content including the applied modifications instead of just watching the content retrieved from the main text editor --- packages/js/src/insights/initializer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/src/insights/initializer.js b/packages/js/src/insights/initializer.js index 31fc7c37c7c..9c8b21169aa 100644 --- a/packages/js/src/insights/initializer.js +++ b/packages/js/src/insights/initializer.js @@ -44,8 +44,8 @@ const createUpdater = () => { * @returns {function} The subscriber. */ const createSubscriber = () => { - const { getEditorDataContent, getContentLocale } = select( "yoast-seo/editor" ); - const collector = createCollector( getEditorDataContent, getContentLocale ); + const { getContentLocale } = select( "yoast-seo/editor" ); + const collector = createCollector( getContentLocale, collectData ); const updater = createUpdater(); // Force an initial update after 1.5 seconds. From 6c3e012ae4a66a425d93bb1a713d5d420e12dd72 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Mon, 13 Nov 2023 17:49:13 +0100 Subject: [PATCH 27/35] Improve naming --- .../src/analysis/plugins/shortcode-plugin.js | 32 +++++++++---------- packages/js/src/initializers/post-scraper.js | 5 ++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 127ab777425..1b22d090dab 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -3,11 +3,11 @@ /* global ajaxurl */ /* global _ */ -const shortcodeNameMatcher = "[^<>&/\\[\\]\x00-\x20=]+?"; -const shortcodeAttributesMatcher = "( [^\\]]+?)?"; +const SHORTCODE_NAME_MATCHER = "[^<>&/\\[\\]\x00-\x20=]+?"; +const SHORTCODE_ATTRIBUTES_MATCHER = "( [^\\]]+?)?"; -const shortcodeStartRegex = new RegExp( "\\[" + shortcodeNameMatcher + shortcodeAttributesMatcher + "\\]", "g" ); -const shortcodeEndRegex = new RegExp( "\\[/" + shortcodeNameMatcher + "\\]", "g" ); +const SHORTCODE_START_REGEX = new RegExp( "\\[" + SHORTCODE_NAME_MATCHER + SHORTCODE_ATTRIBUTES_MATCHER + "\\]", "g" ); +const SHORTCODE_END_REGEX = new RegExp( "\\[/" + SHORTCODE_NAME_MATCHER + "\\]", "g" ); /** * The Yoast Shortcode plugin parses the shortcodes in a given piece of text. It analyzes multiple input fields for @@ -17,7 +17,7 @@ class YoastShortcodePlugin { /** * Constructs the YoastShortcodePlugin. * - * @property {RegExp} keywordRegex Used to match a given string for valid shortcode keywords. + * @property {RegExp} shortcodesRegex Used to match a given string for a valid shortcode. * @property {RegExp} closingTagRegex Used to match a given string for shortcode closing tags. * @property {RegExp} nonCaptureRegex Used to match a given string for non-capturing shortcodes. * @property {Array} parsedShortcodes Used to store parsed shortcodes. @@ -39,12 +39,12 @@ class YoastShortcodePlugin { registerPlugin( "YoastShortcodePlugin", { status: "loading" } ); this.bindElementEvents(); - const keywordRegexString = "(" + shortcodesToBeParsed.join( "|" ) + ")"; + const shortcodesRegexString = "(" + shortcodesToBeParsed.join( "|" ) + ")"; - // The regex for matching shortcodes based on the available shortcode keywords. - this.keywordRegex = new RegExp( keywordRegexString, "g" ); - this.closingTagRegex = new RegExp( "\\[\\/" + keywordRegexString + "\\]", "g" ); - this.nonCaptureRegex = new RegExp( "\\[" + keywordRegexString + "[^\\]]*?\\]", "g" ); + // The regex for matching shortcodes based on the list of shortcodes. + this.shortcodesRegex = new RegExp( shortcodesRegexString, "g" ); + this.closingTagRegex = new RegExp( "\\[\\/" + shortcodesRegexString + "\\]", "g" ); + this.nonCaptureRegex = new RegExp( "\\[" + shortcodesRegexString + "[^\\]]*?\\]", "g" ); /** * The array of parsedShortcode objects. @@ -95,8 +95,8 @@ class YoastShortcodePlugin { * @returns {String} The text with removed unknown shortcodes. */ removeUnknownShortCodes( data ) { - data = data.replace( shortcodeStartRegex, "" ); - data = data.replace( shortcodeEndRegex, "" ); + data = data.replace( SHORTCODE_START_REGEX, "" ); + data = data.replace( SHORTCODE_END_REGEX, "" ); return data; } @@ -244,11 +244,11 @@ class YoastShortcodePlugin { let captures = []; // First identify which tags are being used in a capturing shortcode by looking for closing tags. - const captureKeywords = ( text.match( this.closingTagRegex ) || [] ).join( " " ).match( this.keywordRegex ) || []; + const captureShortcodes = ( text.match( this.closingTagRegex ) || [] ).join( " " ).match( this.shortcodesRegex ) || []; - // Fetch the capturing shortcodes and strip them from the text, so we can easily match the non-capturing shortcodes. - captureKeywords.forEach( captureKeyword => { - const captureRegex = "\\[" + captureKeyword + "[^\\]]*?\\].*?\\[\\/" + captureKeyword + "\\]"; + // Fetch the capturing shortcodes. + captureShortcodes.forEach( captureShortcode => { + const captureRegex = "\\[" + captureShortcode + "[^\\]]*?\\].*?\\[\\/" + captureShortcode + "\\]"; const matches = text.match( new RegExp( captureRegex, "g" ) ) || []; captures = captures.concat( matches ); diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index c2601d3bf56..189d4ab81a8 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -497,14 +497,13 @@ export default function initPostScraper( $, store, editorData ) { // Analysis plugins window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); + const validShortcodes = wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags; let shortcodesToBeParsed = []; shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); // Make sure the added shortcodes are valid. They are valid if they are included in `wpseo_shortcode_tags`. - shortcodesToBeParsed = shortcodesToBeParsed.filter( shortcode => { - return wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags.includes( shortcode ); - } ); + shortcodesToBeParsed = shortcodesToBeParsed.filter( shortcode => validShortcodes.includes( shortcode ) ); // Parses the shortcodes when `shortcodesToBeParsed` is provided. if ( shortcodesToBeParsed.length > 0 ) { From 18ce522195202100883cfa6bc385e8213a4a1e8f Mon Sep 17 00:00:00 2001 From: Aida Marfuaty <48715883+FAMarfuaty@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:22:21 +0100 Subject: [PATCH 28/35] Update packages/js/src/elementor/initializers/pluggable.js Co-authored-by: Martijn van der Klis --- packages/js/src/elementor/initializers/pluggable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/elementor/initializers/pluggable.js b/packages/js/src/elementor/initializers/pluggable.js index 76a53c79f0f..f5e864c4ab2 100644 --- a/packages/js/src/elementor/initializers/pluggable.js +++ b/packages/js/src/elementor/initializers/pluggable.js @@ -10,7 +10,7 @@ let pluggable = null; */ const getPluggable = () => { if ( pluggable === null ) { - // Use the initialized pluggable plugin in post-scrapper if it's available. + // Use the initialized pluggable plugin in `post-scraper.js` if it's available. // Otherwise, initiate a new Pluggable plugin. const refresh = dispatch( "yoast-seo/editor" ).runAnalysis; pluggable = window.YoastSEO.app && window.YoastSEO.app.pluggable From acb4b61ad82a1e72aa1e624f80881b94afb3fd4a Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 14 Nov 2023 10:03:40 +0100 Subject: [PATCH 29/35] Improve JSDoc --- .../src/analysis/plugins/shortcode-plugin.js | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 1b22d090dab..3e1b22f63f3 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -9,6 +9,13 @@ const SHORTCODE_ATTRIBUTES_MATCHER = "( [^\\]]+?)?"; const SHORTCODE_START_REGEX = new RegExp( "\\[" + SHORTCODE_NAME_MATCHER + SHORTCODE_ATTRIBUTES_MATCHER + "\\]", "g" ); const SHORTCODE_END_REGEX = new RegExp( "\\[/" + SHORTCODE_NAME_MATCHER + "\\]", "g" ); +/** + * @typedef ParsedShortcode + * @type {object} + * @property {string} shortcode The unparsed shortcode. + * @property {string} output The parsed shortcode. + */ + /** * The Yoast Shortcode plugin parses the shortcodes in a given piece of text. It analyzes multiple input fields for * shortcodes which it will preload using AJAX. @@ -47,9 +54,8 @@ class YoastShortcodePlugin { this.nonCaptureRegex = new RegExp( "\\[" + shortcodesRegexString + "[^\\]]*?\\]", "g" ); /** - * The array of parsedShortcode objects. - * - * @type {Object[]} + * The array of parsed shortcode objects. + * @type {ParsedShortcode[]} */ this.parsedShortcodes = []; @@ -91,8 +97,8 @@ class YoastShortcodePlugin { * Removes all unknown shortcodes. Not all plugins properly registered their shortcodes in the WordPress backend. * Since we cannot use the data from these shortcodes they must be removed. * - * @param {String} data The text to remove unknown shortcodes. - * @returns {String} The text with removed unknown shortcodes. + * @param {string} data The text to remove unknown shortcodes. + * @returns {string} The text with removed unknown shortcodes. */ removeUnknownShortCodes( data ) { data = data.replace( SHORTCODE_START_REGEX, "" ); @@ -105,9 +111,9 @@ class YoastShortcodePlugin { * Replaces the unparsed shortcodes with the parsed ones. * The callback used to replace the shortcodes. * - * @param {String} data The text to replace the shortcodes in. + * @param {string} data The text to replace the shortcodes in. * - * @returns {String} The text with replaced shortcodes. + * @returns {string} The text with replaced shortcodes. */ replaceShortcodes( data ) { const parsedShortcodes = this.parsedShortcodes; @@ -149,7 +155,7 @@ class YoastShortcodePlugin { */ bindElementEvents() { const contentElement = document.getElementById( "content" ) || false; - const callback = _.debounce( this.loadShortcodes.bind( this, this.declareReloaded.bind( this ) ), 500 ); + const callback = debounce( this.loadShortcodes.bind( this, this.declareReloaded.bind( this ) ), 500 ); if ( contentElement ) { contentElement.addEventListener( "keyup", callback ); @@ -168,7 +174,7 @@ class YoastShortcodePlugin { * Gets content from the content field, if tinyMCE is initialized, use the getContent function to * get the data from tinyMCE. * - * @returns {String} The content from tinyMCE. + * @returns {string} The content from tinyMCE. */ getContentTinyMCE() { let content = document.getElementById( "content" ) ? document.getElementById( "content" ).value : ""; @@ -186,7 +192,7 @@ class YoastShortcodePlugin { * * @param {Array} shortcodes The shortcodes to check. * - * @returns {Boolean|Array} Array with unparsed shortcodes. + * @returns {boolean|Array} Array with unparsed shortcodes. */ getUnparsedShortcodes( shortcodes ) { if ( typeof shortcodes !== "object" ) { @@ -200,9 +206,9 @@ class YoastShortcodePlugin { /** * Checks if a given shortcode was already parsed. * - * @param {String} shortcodeToCheck The shortcode to check. + * @param {string} shortcodeToCheck The shortcode to check. * - * @returns {Boolean} True when shortcode is not parsed yet. + * @returns {boolean} True when shortcode is not parsed yet. */ isUnparsedShortcode( shortcodeToCheck ) { return ! this.parsedShortcodes.some( ( { shortcode } ) => shortcode === shortcodeToCheck ); @@ -211,9 +217,9 @@ class YoastShortcodePlugin { /** * Gets the shortcodes from a given piece of text. * - * @param {String} text Text to extract shortcodes from. + * @param {string} text Text to extract shortcodes from. * - * @returns {Boolean|Array} The matched shortcodes. + * @returns {boolean|Array} The matched shortcodes. */ getShortcodes( text ) { if ( typeof text !== "string" ) { @@ -236,7 +242,7 @@ class YoastShortcodePlugin { /** * Matches the capturing shortcodes from a given piece of text. * - * @param {String} text Text to get the capturing shortcodes from. + * @param {string} text Text to get the capturing shortcodes from. * * @returns {Array} The capturing shortcodes. */ @@ -260,7 +266,7 @@ class YoastShortcodePlugin { /** * Matches the non-capturing shortcodes from a given piece of text. * - * @param {String} text Text to get the non-capturing shortcodes from. + * @param {string} text Text to get the non-capturing shortcodes from. * * @returns {Array} The non-capturing shortcodes. */ @@ -302,7 +308,7 @@ class YoastShortcodePlugin { /** * Saves the shortcodes that were parsed with AJAX to `this.parsedShortcodes` * - * @param {String} shortcodeResults Shortcodes that must be saved. This is the response from the jQuery. + * @param {string} shortcodeResults Shortcodes that must be saved. This is the response from the jQuery. * @param {function} callback Callback to execute of saving shortcodes. * * @returns {void} From d4ff692da11b1d099fcee6a8c68e43ca65b3ea77 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 14 Nov 2023 10:03:54 +0100 Subject: [PATCH 30/35] Improve code readability --- packages/js/src/analysis/plugins/shortcode-plugin.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 3e1b22f63f3..42138c947a0 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -1,7 +1,7 @@ /* global tinyMCE */ /* global wpseoScriptData */ /* global ajaxurl */ -/* global _ */ +import { debounce } from "lodash"; const SHORTCODE_NAME_MATCHER = "[^<>&/\\[\\]\x00-\x20=]+?"; const SHORTCODE_ATTRIBUTES_MATCHER = "( [^\\]]+?)?"; @@ -116,10 +116,8 @@ class YoastShortcodePlugin { * @returns {string} The text with replaced shortcodes. */ replaceShortcodes( data ) { - const parsedShortcodes = this.parsedShortcodes; - - if ( typeof data === "string" && parsedShortcodes.length > 0 ) { - parsedShortcodes.forEach( ( { shortcode, output } ) => { + if ( typeof data === "string" ) { + this.parsedShortcodes.forEach( ( { shortcode, output } ) => { data = data.replace( shortcode, output ); } ); } From c3f6a1121b4f5cf832329900f157f7091b82baa3 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 14 Nov 2023 10:48:44 +0100 Subject: [PATCH 31/35] Move the check for disabling the eye marker when there are available shortcodes for parsing to `Result.js` --- .../components/contentAnalysis/InclusiveLanguageAnalysis.js | 6 +----- .../src/components/contentAnalysis/ReadabilityAnalysis.js | 6 +----- packages/js/src/components/contentAnalysis/Results.js | 4 +++- packages/js/src/components/contentAnalysis/SeoAnalysis.js | 5 +---- packages/js/src/containers/Results.js | 2 ++ 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js index d4e5e360846..6b065bfc123 100644 --- a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js +++ b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js @@ -84,7 +84,7 @@ const InclusiveLanguageAnalysis = ( props ) => { 0 ? "disabled" : props.marksButtonStatus } + marksButtonStatus={ props.marksButtonStatus } resultCategoryLabels={ { problems: __( "Non-inclusive phrases", "wordpress-seo" ), improvements: __( "Potentially non-inclusive phrases", "wordpress-seo" ), @@ -232,25 +232,21 @@ InclusiveLanguageAnalysis.propTypes = { // eslint-disable-next-line react/no-unused-prop-types marksButtonStatus: PropTypes.oneOf( [ "enabled", "disabled", "hidden" ] ).isRequired, overallScore: PropTypes.number, - shortcodesForParsing: PropTypes.array, }; InclusiveLanguageAnalysis.defaultProps = { results: [], overallScore: null, - shortcodesForParsing: [], }; export default withSelect( select => { const { getInclusiveLanguageResults, getMarkButtonStatus, - getShortcodesForParsing, } = select( "yoast-seo/editor" ); return { ...getInclusiveLanguageResults(), marksButtonStatus: getMarkButtonStatus(), - shortcodesForParsing: getShortcodesForParsing(), }; } )( InclusiveLanguageAnalysis ); diff --git a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js index 54179f87fbb..0b44e9aa110 100644 --- a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js +++ b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js @@ -66,7 +66,7 @@ class ReadabilityAnalysis extends Component { results={ this.props.results } upsellResults={ upsellResults } marksButtonClassName="yoast-tooltip yoast-tooltip-w" - marksButtonStatus={ this.props.shortcodesForParsing.length > 0 ? "disabled" : this.props.marksButtonStatus } + marksButtonStatus={ this.props.marksButtonStatus } /> ); @@ -183,25 +183,21 @@ ReadabilityAnalysis.propTypes = { marksButtonStatus: PropTypes.string.isRequired, overallScore: PropTypes.number, shouldUpsell: PropTypes.bool, - shortcodesForParsing: PropTypes.array, }; ReadabilityAnalysis.defaultProps = { overallScore: null, shouldUpsell: false, - shortcodesForParsing: [], }; export default withSelect( select => { const { getReadabilityResults, getMarkButtonStatus, - getShortcodesForParsing, } = select( "yoast-seo/editor" ); return { ...getReadabilityResults(), marksButtonStatus: getMarkButtonStatus(), - shortcodesForParsing: getShortcodesForParsing(), }; } )( ReadabilityAnalysis ); diff --git a/packages/js/src/components/contentAnalysis/Results.js b/packages/js/src/components/contentAnalysis/Results.js index c010d4c07d7..fe00d628d78 100644 --- a/packages/js/src/components/contentAnalysis/Results.js +++ b/packages/js/src/components/contentAnalysis/Results.js @@ -277,7 +277,7 @@ class Results extends Component { onEditButtonClick={ this.handleEditButtonClick } marksButtonClassName={ this.props.marksButtonClassName } editButtonClassName={ this.props.editButtonClassName } - marksButtonStatus={ this.props.marksButtonStatus } + marksButtonStatus={ this.props.shortcodesForParsing.length > 0 ? "disabled" : this.props.marksButtonStatus } headingLevel={ 3 } keywordKey={ this.props.keywordKey } isPremium={ this.props.isPremium } @@ -308,6 +308,7 @@ Results.propTypes = { considerations: PropTypes.string, goodResults: PropTypes.string, } ), + shortcodesForParsing: PropTypes.array, }; Results.defaultProps = { @@ -321,6 +322,7 @@ Results.defaultProps = { location: "", isPremium: false, resultCategoryLabels: {}, + shortcodesForParsing: [], }; export default Results; diff --git a/packages/js/src/components/contentAnalysis/SeoAnalysis.js b/packages/js/src/components/contentAnalysis/SeoAnalysis.js index b5ee7dc6754..3fdd17080c2 100644 --- a/packages/js/src/components/contentAnalysis/SeoAnalysis.js +++ b/packages/js/src/components/contentAnalysis/SeoAnalysis.js @@ -265,7 +265,6 @@ SeoAnalysis.propTypes = { shouldUpsell: PropTypes.bool, shouldUpsellWordFormRecognition: PropTypes.bool, overallScore: PropTypes.number, - shortcodesForParsing: PropTypes.array, }; SeoAnalysis.defaultProps = { @@ -275,7 +274,6 @@ SeoAnalysis.defaultProps = { shouldUpsell: false, shouldUpsellWordFormRecognition: false, overallScore: null, - shortcodesForParsing: [], }; export default withSelect( ( select, ownProps ) => { @@ -283,14 +281,13 @@ export default withSelect( ( select, ownProps ) => { getFocusKeyphrase, getMarksButtonStatus, getResultsForKeyword, - getShortcodesForParsing, } = select( "yoast-seo/editor" ); const keyword = getFocusKeyphrase(); return { ...getResultsForKeyword( keyword ), - marksButtonStatus: ownProps.hideMarksButtons || getShortcodesForParsing().length > 0 ? "disabled" : getMarksButtonStatus(), + marksButtonStatus: ownProps.hideMarksButtons ? "disabled" : getMarksButtonStatus(), keyword, }; } )( SeoAnalysis ); diff --git a/packages/js/src/containers/Results.js b/packages/js/src/containers/Results.js index 70f77b91265..0c01a6b82a1 100644 --- a/packages/js/src/containers/Results.js +++ b/packages/js/src/containers/Results.js @@ -7,11 +7,13 @@ export default compose( [ const { getActiveMarker, getIsPremium, + getShortcodesForParsing, } = select( "yoast-seo/editor" ); return { activeMarker: getActiveMarker(), isPremium: getIsPremium(), + shortcodesForParsing: getShortcodesForParsing(), }; } ), withDispatch( dispatch => { From d5af3d05c8db13d1c7a1b27ca5a1c8e584587a40 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Tue, 14 Nov 2023 10:58:22 +0100 Subject: [PATCH 32/35] Simplify code --- .../js/src/analysis/plugins/shortcode-plugin.js | 15 ++++----------- .../analysis/plugins/shortcode-plugin.test.js | 6 ++++++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 42138c947a0..5db59faf5a1 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -1,7 +1,7 @@ /* global tinyMCE */ /* global wpseoScriptData */ /* global ajaxurl */ -import { debounce } from "lodash"; +import { debounce, flatten } from "lodash"; const SHORTCODE_NAME_MATCHER = "[^<>&/\\[\\]\x00-\x20=]+?"; const SHORTCODE_ATTRIBUTES_MATCHER = "( [^\\]]+?)?"; @@ -245,20 +245,13 @@ class YoastShortcodePlugin { * @returns {Array} The capturing shortcodes. */ matchCapturingShortcodes( text ) { - let captures = []; - // First identify which tags are being used in a capturing shortcode by looking for closing tags. const captureShortcodes = ( text.match( this.closingTagRegex ) || [] ).join( " " ).match( this.shortcodesRegex ) || []; - // Fetch the capturing shortcodes. - captureShortcodes.forEach( captureShortcode => { + return flatten( captureShortcodes.map( captureShortcode => { const captureRegex = "\\[" + captureShortcode + "[^\\]]*?\\].*?\\[\\/" + captureShortcode + "\\]"; - const matches = text.match( new RegExp( captureRegex, "g" ) ) || []; - - captures = captures.concat( matches ); - } ); - - return captures; + return text.match( new RegExp( captureRegex, "g" ) ) || []; + } ) ); } /** diff --git a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js index 417de7657bd..c10256296db 100644 --- a/packages/js/tests/analysis/plugins/shortcode-plugin.test.js +++ b/packages/js/tests/analysis/plugins/shortcode-plugin.test.js @@ -87,6 +87,12 @@ describe( "YoastShortcodePlugin", () => { ] ); } ); + it( "should return an empty array if no match is found for the capturing shortcodes", () => { + const inputText = "This is a sample text without shortcodes"; + const capturingShortcodes = plugin.matchCapturingShortcodes( inputText ); + expect( capturingShortcodes ).toEqual( [] ); + } ); + it( "should match and return non-capturing shortcodes from the content", () => { const inputText = "This is a sample [wpseo_breadcrumb] text with an image with caption [caption id=\"attachment_8\" align=\"alignnone\"" + " width=\"230\"] $this->determine_scope(), ], 'shortcodes' => [ - 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), + 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ], 'worker' => [ diff --git a/packages/js/src/analysis/plugins/shortcode-plugin.js b/packages/js/src/analysis/plugins/shortcode-plugin.js index 5db59faf5a1..37578c59c23 100644 --- a/packages/js/src/analysis/plugins/shortcode-plugin.js +++ b/packages/js/src/analysis/plugins/shortcode-plugin.js @@ -2,6 +2,8 @@ /* global wpseoScriptData */ /* global ajaxurl */ import { debounce, flatten } from "lodash"; +import { applyFilters } from "@wordpress/hooks"; +import { actions } from "@yoast/externals/redux"; const SHORTCODE_NAME_MATCHER = "[^<>&/\\[\\]\x00-\x20=]+?"; const SHORTCODE_ATTRIBUTES_MATCHER = "( [^\\]]+?)?"; @@ -27,7 +29,7 @@ class YoastShortcodePlugin { * @property {RegExp} shortcodesRegex Used to match a given string for a valid shortcode. * @property {RegExp} closingTagRegex Used to match a given string for shortcode closing tags. * @property {RegExp} nonCaptureRegex Used to match a given string for non-capturing shortcodes. - * @property {Array} parsedShortcodes Used to store parsed shortcodes. + * @property {ParsedShortcode[]} parsedShortcodes Used to store parsed shortcodes. * * @param {Object} interface Object Formerly Known as App, but for backwards compatibility * still passed here as one argument. @@ -35,7 +37,7 @@ class YoastShortcodePlugin { * @param {function} interface.registerModification Register a modification with Yoast SEO. * @param {function} interface.pluginReady Notify Yoast SEO that the plugin is ready. * @param {function} interface.pluginReloaded Notify Yoast SEO that the plugin has been reloaded. - * @param {Array} shortcodesToBeParsed The array of shortcodes to be parsed. + * @param {string[]} shortcodesToBeParsed The array of shortcodes to be parsed. * @returns {void} */ constructor( { registerPlugin, registerModification, pluginReady, pluginReloaded }, shortcodesToBeParsed ) { @@ -92,7 +94,6 @@ class YoastShortcodePlugin { this._registerModification( "content", this.replaceShortcodes.bind( this ), "YoastShortcodePlugin" ); } - /** * Removes all unknown shortcodes. Not all plugins properly registered their shortcodes in the WordPress backend. * Since we cannot use the data from these shortcodes they must be removed. @@ -135,7 +136,7 @@ class YoastShortcodePlugin { * * @param {function} callback To declare either ready or reloaded after parsing. * - * @returns {void} + * @returns {function} The callback function. */ loadShortcodes( callback ) { const unparsedShortcodes = this.getUnparsedShortcodes( this.getShortcodes( this.getContentTinyMCE() ) ); @@ -152,8 +153,8 @@ class YoastShortcodePlugin { * @returns {void} */ bindElementEvents() { - const contentElement = document.getElementById( "content" ) || false; - const callback = debounce( this.loadShortcodes.bind( this, this.declareReloaded.bind( this ) ), 500 ); + const contentElement = document.querySelector( ".wp-editor-area" ); + const callback = debounce( this.loadShortcodes.bind( this, this.declareReloaded.bind( this ) ), 500 ); if ( contentElement ) { contentElement.addEventListener( "keyup", callback ); @@ -175,7 +176,7 @@ class YoastShortcodePlugin { * @returns {string} The content from tinyMCE. */ getContentTinyMCE() { - let content = document.getElementById( "content" ) ? document.getElementById( "content" ).value : ""; + let content = document.querySelector( ".wp-editor-area" ) ? document.querySelector( ".wp-editor-area" ).value : ""; if ( typeof tinyMCE !== "undefined" && typeof tinyMCE.editors !== "undefined" && tinyMCE.editors.length !== 0 ) { content = tinyMCE.get( "content" ) ? tinyMCE.get( "content" ).getContent() : ""; } @@ -271,7 +272,7 @@ class YoastShortcodePlugin { * @param {Array} shortcodes shortcodes to be parsed. * @param {function} callback function to be called in the context of the AJAX callback. * - * @returns {void} + * @returns {boolean|function} The callback function or false if no function has been supplied. */ parseShortcodes( shortcodes, callback ) { if ( typeof callback !== "function" ) { @@ -297,25 +298,55 @@ class YoastShortcodePlugin { } /** - * Saves the shortcodes that were parsed with AJAX to `this.parsedShortcodes` + * Saves the shortcodes that were parsed to `this.parsedShortcodes`, and then call the callback function. * - * @param {string} shortcodeResults Shortcodes that must be saved. This is the response from the jQuery. - * @param {function} callback Callback to execute of saving shortcodes. + * @param {string} shortcodeResults Shortcodes that must be saved. This is the response from the AJAX request. + * @param {function} callback Callback to execute after saving shortcodes. * * @returns {void} */ saveParsedShortcodes( shortcodeResults, callback ) { // Parse the stringified shortcode results to an array. - shortcodeResults = JSON.parse( shortcodeResults ); + const shortcodes = JSON.parse( shortcodeResults ); - // Push each shortcode result to the array of parsed shortcodes. - shortcodeResults.forEach( result => { - this.parsedShortcodes.push( result ); - } ); + // Push the shortcodes to the array of parsed shortcodes. + this.parsedShortcodes.push( ...shortcodes ); callback(); } } - export default YoastShortcodePlugin; + +const { + updateShortcodesForParsing, +} = actions; + +/** + * Initializes the shortcode plugin. + * + * @param {App} app The app object. + * @param {Object} store The redux store. + * + * @returns {void} + */ +export function initShortcodePlugin( app, store ) { + let shortcodesToBeParsed = []; + shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); + + // Make sure the added shortcodes are valid. They are valid if they are included in `wpseo_shortcode_tags`. + const validShortcodes = wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags; + shortcodesToBeParsed = shortcodesToBeParsed.filter( shortcode => validShortcodes.includes( shortcode ) ); + + // Parses the shortcodes when `shortcodesToBeParsed` is provided. + if ( shortcodesToBeParsed.length > 0 ) { + store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); + + window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { + registerPlugin: app.registerPlugin, + registerModification: app.registerModification, + pluginReady: app.pluginReady, + pluginReloaded: app.pluginReloaded, + }, shortcodesToBeParsed ); + } +} diff --git a/packages/js/src/initializers/post-scraper.js b/packages/js/src/initializers/post-scraper.js index 189d4ab81a8..eb625f613b7 100644 --- a/packages/js/src/initializers/post-scraper.js +++ b/packages/js/src/initializers/post-scraper.js @@ -2,18 +2,14 @@ // External dependencies. import { App } from "yoastseo"; -import { - isUndefined, - debounce, -} from "lodash"; +import { debounce, isUndefined } from "lodash"; import { isShallowEqualObjects } from "@wordpress/is-shallow-equal"; import { select, subscribe } from "@wordpress/data"; -import { applyFilters } from "@wordpress/hooks"; // Internal dependencies. import YoastReplaceVarPlugin from "../analysis/plugins/replacevar-plugin"; import YoastReusableBlocksPlugin from "../analysis/plugins/reusable-blocks-plugin"; -import YoastShortcodePlugin from "../analysis/plugins/shortcode-plugin"; +import YoastShortcodePlugin, { initShortcodePlugin } from "../analysis/plugins/shortcode-plugin"; import YoastMarkdownPlugin from "../analysis/plugins/markdown-plugin"; import * as tinyMCEHelper from "../lib/tinymce"; import CompatibilityHelper from "../compatibility/compatibilityHelper"; @@ -62,7 +58,6 @@ const { refreshSnippetEditor, setReadabilityResults, setSeoResultsForKeyword, - updateShortcodesForParsing, } = actions; // Plugin class prototypes (not the instances) are being used by other plugins from the window. @@ -88,7 +83,6 @@ export default function initPostScraper( $, store, editorData ) { let postDataCollector; const customAnalysisData = new CustomAnalysisData(); - /** * Retrieves either a generated slug or the page title as slug for the preview. * @@ -497,25 +491,7 @@ export default function initPostScraper( $, store, editorData ) { // Analysis plugins window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); - const validShortcodes = wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags; - let shortcodesToBeParsed = []; - - shortcodesToBeParsed = applyFilters( "yoast.analysis.shortcodes", shortcodesToBeParsed ); - - // Make sure the added shortcodes are valid. They are valid if they are included in `wpseo_shortcode_tags`. - shortcodesToBeParsed = shortcodesToBeParsed.filter( shortcode => validShortcodes.includes( shortcode ) ); - - // Parses the shortcodes when `shortcodesToBeParsed` is provided. - if ( shortcodesToBeParsed.length > 0 ) { - store.dispatch( updateShortcodesForParsing( shortcodesToBeParsed ) ); - - window.YoastSEO.wp.shortcodePlugin = new YoastShortcodePlugin( { - registerPlugin: app.registerPlugin, - registerModification: app.registerModification, - pluginReady: app.pluginReady, - pluginReloaded: app.pluginReloaded, - }, shortcodesToBeParsed ); - } + initShortcodePlugin( app, store ); if ( isBlockEditor() ) { const reusableBlocksPlugin = new YoastReusableBlocksPlugin( app.registerPlugin, app.registerModification, window.YoastSEO.app.refresh ); diff --git a/packages/js/src/initializers/term-scraper.js b/packages/js/src/initializers/term-scraper.js index 425ae758753..00478ca506e 100644 --- a/packages/js/src/initializers/term-scraper.js +++ b/packages/js/src/initializers/term-scraper.js @@ -13,6 +13,7 @@ import { termsTmceId } from "../lib/tinymce"; import Pluggable from "../lib/Pluggable"; import requestWordsToHighlight from "../analysis/requestWordsToHighlight.js"; import YoastReplaceVarPlugin from "../analysis/plugins/replacevar-plugin"; +import YoastShortcodePlugin, { initShortcodePlugin } from "../analysis/plugins/shortcode-plugin"; // UI dependencies. import { update as updateTrafficLight } from "../ui/trafficLight"; @@ -56,6 +57,7 @@ window.yoastHideMarkers = true; // Plugin class prototypes (not the instances) are being used by other plugins from the window. window.YoastReplaceVarPlugin = YoastReplaceVarPlugin; +window.YoastShortcodePlugin = YoastShortcodePlugin; /** * @summary Initializes the term scraper script. @@ -372,6 +374,7 @@ export default function initTermScraper( $, store, editorData ) { // Init Plugins. window.YoastSEO.wp = {}; window.YoastSEO.wp.replaceVarsPlugin = new YoastReplaceVarPlugin( app, store ); + initShortcodePlugin( app, store ); // For backwards compatibility. window.YoastSEO.analyzerArgs = args; From fe8e603e717933dcda0ee5466ddbe756613a727e Mon Sep 17 00:00:00 2001 From: Martijn van der Klis Date: Mon, 20 Nov 2023 09:45:46 +0100 Subject: [PATCH 35/35] Don't set the markers to 'disabled' when they are 'hidden' --- packages/js/src/components/contentAnalysis/Results.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/js/src/components/contentAnalysis/Results.js b/packages/js/src/components/contentAnalysis/Results.js index fe00d628d78..c92a0d90ce2 100644 --- a/packages/js/src/components/contentAnalysis/Results.js +++ b/packages/js/src/components/contentAnalysis/Results.js @@ -263,6 +263,12 @@ class Results extends Component { const labels = Object.assign( defaultLabels, resultCategoryLabels ); + let marksButtonStatus = this.props.marksButtonStatus; + // If the marks are enabled, but we are also parsing shortcodes, disable the markers. + if ( marksButtonStatus === "enabled" && this.props.shortcodesForParsing.length > 0 ) { + marksButtonStatus = "disabled"; + } + return ( 0 ? "disabled" : this.props.marksButtonStatus } + marksButtonStatus={ marksButtonStatus } headingLevel={ 3 } keywordKey={ this.props.keywordKey } isPremium={ this.props.isPremium } @@ -294,7 +300,7 @@ Results.propTypes = { upsellResults: PropTypes.array, marksButtonClassName: PropTypes.string, editButtonClassName: PropTypes.string, - marksButtonStatus: PropTypes.string, + marksButtonStatus: PropTypes.oneOf( [ "enabled", "disabled", "hidden" ] ), setActiveMarker: PropTypes.func.isRequired, setMarkerPauseStatus: PropTypes.func.isRequired, activeMarker: PropTypes.string,