diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 578b36c7403..f58a901652e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -136,7 +136,7 @@ jobs: matrix: include: - php_version: "7.2" - wp_version: "6.4" + wp_version: "6.5" multisite: true coverage: true @@ -151,7 +151,7 @@ jobs: coverage: false - php_version: "8.0" - wp_version: "6.4" + wp_version: "6.5" multisite: false coverage: false @@ -161,7 +161,7 @@ jobs: coverage: false - php_version: '8.2' - wp_version: '6.4' + wp_version: '6.5' multisite: true coverage: false diff --git a/admin/class-admin-asset-manager.php b/admin/class-admin-asset-manager.php index ec0df47a1a3..a05adf4e1de 100644 --- a/admin/class-admin-asset-manager.php +++ b/admin/class-admin-asset-manager.php @@ -313,6 +313,9 @@ protected function scripts_to_be_registered() { self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], + 'new-dashboard' => [ + self::PREFIX . 'api-client', + ], ]; $plugin_scripts = $this->load_generated_asset_file( @@ -660,6 +663,11 @@ protected function styles_to_be_registered() { 'src' => 'academy-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], + [ + 'name' => 'new-dashboard', + 'src' => 'new-dashboard-' . $flat_version, + 'deps' => [ self::PREFIX . 'tailwind' ], + ], [ 'name' => 'support', 'src' => 'support-' . $flat_version, diff --git a/admin/class-admin.php b/admin/class-admin.php index a5d09b29689..1b4649c4dc3 100644 --- a/admin/class-admin.php +++ b/admin/class-admin.php @@ -5,6 +5,7 @@ * @package WPSEO\Admin */ +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Integrations\Settings_Integration; /** @@ -241,7 +242,8 @@ public function add_action_link( $links, $file ) { $configuration_title = ( ! $first_time_configuration_notice_helper->should_show_alternate_message() ) ? 'first-time configuration' : 'SEO configuration'; /* translators: CTA to finish the first time configuration. %s: Either first-time SEO configuration or SEO configuration. */ $message = sprintf( __( 'Finish your %s', 'wordpress-seo' ), $configuration_title ); - $ftc_link = '' . $message . ''; + $ftc_page = ( ( new New_Dashboard_Ui_Conditional() )->is_met() ) ? 'admin.php?page=wpseo_dashboard#/first-time-configuration' : 'admin.php?page=wpseo_dashboard#top#first-time-configuration'; + $ftc_link = '' . $message . ''; array_unshift( $links, $ftc_link ); } diff --git a/admin/class-config.php b/admin/class-config.php index 58e037db822..ad865b2bbcd 100644 --- a/admin/class-config.php +++ b/admin/class-config.php @@ -6,7 +6,8 @@ */ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; -use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; +use Yoast\WP\SEO\Dashboard\User_Interface\New_Dashboard_Page_Integration; use Yoast\WP\SEO\Integrations\Academy_Integration; use Yoast\WP\SEO\Integrations\Settings_Integration; use Yoast\WP\SEO\Integrations\Support_Integration; @@ -50,11 +51,17 @@ public function __construct() { public function init() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; - if ( in_array( $page, [ Settings_Integration::PAGE, Academy_Integration::PAGE, Support_Integration::PAGE ], true ) ) { + + // Don't load the scripts for the following pages. + $page_exceptions = in_array( $page, [ Settings_Integration::PAGE, Academy_Integration::PAGE, Support_Integration::PAGE ], true ); + // Don't load the scripts for the new dashboard page, but only if the feature flag is enabled. + $new_dashboard_conditional = new New_Dashboard_Ui_Conditional(); + $new_dashboard_page = ( $page === New_Dashboard_Page_Integration::PAGE && $new_dashboard_conditional->is_met() ); + + if ( $page_exceptions || $new_dashboard_page ) { // Bail, this is managed in the applicable integration. return; } - add_action( 'admin_enqueue_scripts', [ $this, 'config_page_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'config_page_styles' ] ); } @@ -89,16 +96,13 @@ public function config_page_scripts() { wp_enqueue_script( 'dashboard' ); wp_enqueue_script( 'thickbox' ); - $alert_dismissal_action = YoastSEO()->classes->get( Alert_Dismissal_Action::class ); - $dismissed_alerts = $alert_dismissal_action->all_dismissed(); - $woocommerce_conditional = new WooCommerce_Conditional(); + $alert_dismissal_action = YoastSEO()->classes->get( Alert_Dismissal_Action::class ); + $dismissed_alerts = $alert_dismissal_action->all_dismissed(); $script_data = [ - 'userLanguageCode' => WPSEO_Language_Utils::get_language( get_user_locale() ), 'dismissedAlerts' => $dismissed_alerts, 'isRtl' => is_rtl(), 'isPremium' => YoastSEO()->helpers->product->is_premium(), - 'isWooCommerceActive' => $woocommerce_conditional->is_met(), 'currentPromotions' => YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(), 'webinarIntroFirstTimeConfigUrl' => $this->get_webinar_shortlink(), 'linkParams' => WPSEO_Shortlinker::get_query_params(), diff --git a/admin/class-gutenberg-compatibility.php b/admin/class-gutenberg-compatibility.php index 74b55b1b6ff..cb2cf3aac02 100644 --- a/admin/class-gutenberg-compatibility.php +++ b/admin/class-gutenberg-compatibility.php @@ -15,14 +15,14 @@ class WPSEO_Gutenberg_Compatibility { * * @var string */ - public const CURRENT_RELEASE = '19.1.0'; + public const CURRENT_RELEASE = '19.3.0'; /** * The minimally supported version of Gutenberg by the plugin. * * @var string */ - public const MINIMUM_SUPPORTED = '19.1.0'; + public const MINIMUM_SUPPORTED = '19.3.0'; /** * Holds the current version. diff --git a/admin/class-premium-upsell-admin-block.php b/admin/class-premium-upsell-admin-block.php index 2912d3684b5..157fa71211c 100644 --- a/admin/class-premium-upsell-admin-block.php +++ b/admin/class-premium-upsell-admin-block.php @@ -112,9 +112,10 @@ public function render() { echo '
'; if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) { - $bf_label = esc_html__( '30% OFF | Code: BF2024', 'wordpress-seo' ); + $bf_label = esc_html__( 'BLACK FRIDAY', 'wordpress-seo' ); + $sale_label = esc_html__( '30% OFF', 'wordpress-seo' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped above. - echo "
$bf_label
"; + echo "
$sale_label $bf_label
"; } echo '
'; diff --git a/admin/class-yoast-notification-center.php b/admin/class-yoast-notification-center.php index fcbc734d097..c9531284117 100644 --- a/admin/class-yoast-notification-center.php +++ b/admin/class-yoast-notification-center.php @@ -600,6 +600,12 @@ private function split_on_user_id( $notifications ) { * @return void */ public function update_storage() { + /** + * Plugins might exit on the plugins_loaded hook. + * This prevents the pluggable.php file from loading, as it's loaded after the plugins_loaded hook. + * As we need functions defined in pluggable.php, make sure it's loaded. + */ + require_once ABSPATH . WPINC . '/pluggable.php'; $notifications = $this->notifications; diff --git a/admin/class-yoast-notification.php b/admin/class-yoast-notification.php index 3191827b11e..dd01300cf85 100644 --- a/admin/class-yoast-notification.php +++ b/admin/class-yoast-notification.php @@ -355,6 +355,15 @@ public function render() { return '
' . $message . '
' . PHP_EOL; } + /** + * Get the message for the notification. + * + * @return string The message. + */ + public function get_message() { + return wpautop( $this->message ); + } + /** * Wraps the message with a Yoast SEO icon. * diff --git a/admin/formatter/class-metabox-formatter.php b/admin/formatter/class-metabox-formatter.php index 687361c79ff..02deb3c8eb4 100644 --- a/admin/formatter/class-metabox-formatter.php +++ b/admin/formatter/class-metabox-formatter.php @@ -52,14 +52,9 @@ private function get_defaults() { $defaults = [ 'author_name' => get_the_author_meta( 'display_name' ), - 'sitewide_social_image' => WPSEO_Options::get( 'og_default_image' ), 'keyword_usage' => [], 'title_template' => '', 'metadesc_template' => '', - 'showSocial' => [ - 'facebook' => WPSEO_Options::get( 'opengraph', false ), - 'twitter' => WPSEO_Options::get( 'twitter', false ), - ], 'schema' => [ 'displayFooter' => WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ), 'pageTypeOptions' => $schema_types->get_page_type_options(), diff --git a/admin/menu/class-admin-menu.php b/admin/menu/class-admin-menu.php index 5c5dce746e6..e8f5ba2a018 100644 --- a/admin/menu/class-admin-menu.php +++ b/admin/menu/class-admin-menu.php @@ -5,6 +5,7 @@ * @package WPSEO\Admin\Menu */ +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; /** * Registers the admin menu on the left of the admin area. */ @@ -88,7 +89,6 @@ public function get_submenu_pages() { // Submenu pages. $submenu_pages = [ - $this->get_submenu_page( __( 'General', 'wordpress-seo' ), $this->get_page_identifier() ), $this->get_submenu_page( __( 'Search Console', 'wordpress-seo' ), 'wpseo_search_console', @@ -98,6 +98,10 @@ public function get_submenu_pages() { $this->get_submenu_page( $this->get_license_page_title(), 'wpseo_licenses' ), ]; + if ( ! ( new New_Dashboard_Ui_Conditional() )->is_met() ) { + array_unshift( $submenu_pages, $this->get_submenu_page( __( 'General', 'wordpress-seo' ), $this->get_page_identifier() ) ); + } + /** * Filter: 'wpseo_submenu_pages' - Collects all submenus that need to be shown. * diff --git a/admin/metabox/class-metabox.php b/admin/metabox/class-metabox.php index 3f264dded33..e9e43f968fd 100644 --- a/admin/metabox/class-metabox.php +++ b/admin/metabox/class-metabox.php @@ -5,9 +5,6 @@ * @package WPSEO\Admin */ -use Yoast\WP\SEO\Conditionals\Third_Party\Jetpack_Boost_Active_Conditional; -use Yoast\WP\SEO\Conditionals\Third_Party\Jetpack_Boost_Not_Premium_Conditional; -use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository; use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter; use Yoast\WP\SEO\Presenters\Admin\Meta_Fields_Presenter; @@ -882,14 +879,12 @@ public function enqueue() { 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(), ]; - $woocommerce_conditional = new WooCommerce_Conditional(); - $woocommerce_active = $woocommerce_conditional->is_met(); - $addon_manager = new WPSEO_Addon_Manager(); - $woocommerce_seo_active = is_plugin_active( $addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ); + $page_on_front = (int) get_option( 'page_on_front' ); + $homepage_is_page = get_option( 'show_on_front' ) === 'page'; + $is_front_page = $homepage_is_page && $page_on_front === (int) $post_id; $script_data = [ 'metabox' => $this->get_metabox_script_data(), - 'userLanguageCode' => WPSEO_Language_Utils::get_language( get_user_locale() ), 'isPost' => true, 'isBlockEditor' => $is_block_editor, 'postId' => $post_id, @@ -900,10 +895,7 @@ public function enqueue() { 'plugins' => $plugins_script_data, 'worker' => $worker_script_data, ], - 'isJetpackBoostActive' => ( $is_block_editor ) ? YoastSEO()->classes->get( Jetpack_Boost_Active_Conditional::class )->is_met() : false, - 'isJetpackBoostNotPremium' => ( $is_block_editor ) ? YoastSEO()->classes->get( Jetpack_Boost_Not_Premium_Conditional::class )->is_met() : false, - 'isWooCommerceSeoActive' => $woocommerce_seo_active, - 'isWooCommerceActive' => $woocommerce_active, + 'isFrontPage' => $is_front_page, ]; /** diff --git a/admin/taxonomy/class-taxonomy.php b/admin/taxonomy/class-taxonomy.php index 2ace8a9b849..62b0bd2e766 100644 --- a/admin/taxonomy/class-taxonomy.php +++ b/admin/taxonomy/class-taxonomy.php @@ -182,7 +182,6 @@ public function admin_enqueue_scripts() { ], ], 'metabox' => $this->localize_term_scraper_script( $tag_id ), - 'userLanguageCode' => WPSEO_Language_Utils::get_language( get_user_locale() ), 'isTerm' => true, 'postId' => $tag_id, 'termType' => $this->get_taxonomy(), diff --git a/admin/views/licenses.php b/admin/views/licenses.php index 376e068c049..40df5e0742d 100644 --- a/admin/views/licenses.php +++ b/admin/views/licenses.php @@ -135,7 +135,7 @@ if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) { /* translators: %1$s expands to opening span, %2$s expands to closing span */ - $sale_badge_span = sprintf( esc_html__( '%1$s30%% OFF | Use code: BF2024%2$s', 'wordpress-seo' ), '', '' ); + $sale_badge_span = sprintf( esc_html__( '%1$s30%% OFF%2$s', 'wordpress-seo' ), '', '' ); $sale_badge = '
' . $sale_badge_span . '
'; diff --git a/admin/views/tabs/tool/import-seo.php b/admin/views/tabs/tool/import-seo.php index 9cac6366f82..c1501c97ec9 100644 --- a/admin/views/tabs/tool/import-seo.php +++ b/admin/views/tabs/tool/import-seo.php @@ -5,6 +5,8 @@ * @package WPSEO\Admin\Views */ +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; + if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); @@ -100,10 +102,11 @@ function wpseo_import_external_select( $name, $plugins ) {

is_met() ) ? 'admin.php?page=wpseo_dashboard#/first-time-configuration' : 'admin.php?page=wpseo_dashboard#top#first-time-configuration'; printf( /* translators: 1: Link start tag to the First time configuration tab in the General page, 2: Link closing tag. */ esc_html__( 'You should finish the %1$sfirst time configuration%2$s to make sure your SEO data has been optimized and you’ve set the essential Yoast SEO settings for your site.', 'wordpress-seo' ), - '', + '', '' ); ?> diff --git a/composer.json b/composer.json index dd95768d495..bddc56eb708 100644 --- a/composer.json +++ b/composer.json @@ -91,7 +91,7 @@ "Yoast\\WP\\SEO\\Composer\\Actions::check_coding_standards" ], "check-cs-thresholds": [ - "@putenv YOASTCS_THRESHOLD_ERRORS=2482", + "@putenv YOASTCS_THRESHOLD_ERRORS=2479", "@putenv YOASTCS_THRESHOLD_WARNINGS=252", "Yoast\\WP\\SEO\\Composer\\Actions::check_cs_thresholds" ], diff --git a/config/dependency-injection/deprecated-classes.php b/config/dependency-injection/deprecated-classes.php index d67526967ec..7c059890c3e 100644 --- a/config/dependency-injection/deprecated-classes.php +++ b/config/dependency-injection/deprecated-classes.php @@ -23,6 +23,7 @@ use Yoast\WP\SEO\Conditionals\Third_Party\Wordproof_Plugin_Inactive_Conditional; use Yoast\WP\SEO\Config\Wordproof_App_Config; use Yoast\WP\SEO\Config\Wordproof_Translations; +use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Helpers\Wordproof_Helper; use Yoast\WP\SEO\Integrations\Admin\Disable_Concatenate_Scripts_Integration; use Yoast\WP\SEO\Integrations\Admin\Old_Premium_Integration; @@ -45,6 +46,7 @@ Ai_Generate_Titles_And_Descriptions_Introduction_Upsell::class => '23.2', Disable_Concatenate_Scripts_Integration::class => '23.2', Duplicate_Post_Integration::class => '23.4', + Request_Helper::class => '23.6', ]; foreach ( $deprecated_classes as $original_class => $version ) { diff --git a/config/webpack/paths.js b/config/webpack/paths.js index 28132d9f0bd..8633e3f9496 100644 --- a/config/webpack/paths.js +++ b/config/webpack/paths.js @@ -44,6 +44,7 @@ const getEntries = ( sourceDirectory = "./packages/js/src" ) => ( { settings: `${ sourceDirectory }/settings.js`, "new-settings": `${ sourceDirectory }/settings/initialize.js`, academy: `${ sourceDirectory }/academy/initialize.js`, + "new-dashboard": `${ sourceDirectory }/dashboard/initialize.js`, support: `${ sourceDirectory }/support/initialize.js`, "how-to-block": `${ sourceDirectory }/structured-data-blocks/how-to/block.js`, "faq-block": `${ sourceDirectory }/structured-data-blocks/faq/block.js`, diff --git a/css/src/admin-global.css b/css/src/admin-global.css index 0bda03369d5..f2deec79f5c 100644 --- a/css/src/admin-global.css +++ b/css/src/admin-global.css @@ -817,6 +817,10 @@ body.folded .wpseo-admin-submit-fixed { color: #d63638; } +.yoast-notice-migrated-header { + margin-top: 10px; +} + .privacy-settings .notice-yoast { margin: 0 20px; } diff --git a/css/src/first-time-configuration.css b/css/src/first-time-configuration.css index e1303b5889c..3e5eedf25f4 100644 --- a/css/src/first-time-configuration.css +++ b/css/src/first-time-configuration.css @@ -1,5 +1,4 @@ -#wpseo-first-time-configuration { - .yst-root { +#yoast-configuration { /* Override aggressive WordPress inputs styling */ .yst-input { @@ -60,5 +59,4 @@ .yst-checkbox__input { @apply before:yst-content-none !important; } - } } diff --git a/css/src/new-dashboard.css b/css/src/new-dashboard.css new file mode 100644 index 00000000000..96fdd31ae69 --- /dev/null +++ b/css/src/new-dashboard.css @@ -0,0 +1,256 @@ +body.toplevel_page_wpseo_dashboard { + .notice, .yoast-migrated-notice { + display:none; + } + + .yoast-new-dashboard-notices:last-child{ + margin-bottom: 2rem; + } + + .yoast-dashboard-notice, .yoast-webinar-dashboard{ + background: white; + padding: 0.75rem; + border-radius: 0.375rem; + + @apply + yst-border + yst-border-primary-200 + yst-shadow-md + } + + .yoast-dashboard-notice .yoast-icon { + background-color: var(--yoast-color-primary); + mask-image: var(--yoast-svg-icon-yoast); + -webkit-mask-image: var(--yoast-svg-icon-yoast); + mask-size: 100% 100%; + -webkit-mask-size: 100% 100%; + display: inline-block; + width: 17px; + height: 17px; + margin-right: 12px; + } + + .yoast-dashboard-notice .notice-dismiss { + position: relative; + @apply yst-text-slate-400 + hover:yst-text-slate-500 + } + + .yoast-dashboard-notice .notice-dismiss:before{ + display: none; + } + + .yoast-dashboard-notice .notice-yoast img{ + display: none; + } + + .yoast-dashboard-notice a{ + font-weight: 500; + } + + .yoast-webinar-dashboard { + margin-bottom: 0.8rem; + margin-top: 0; + } + + .yoast-webinar-dashboard .notice-dismiss:before{ + /* yst-text-slate-400 */ + background: url("data:image/svg+xml,"); + content: ''; + } + + .yoast-webinar-dashboard .notice-dismiss:hover:before{ + /* yst-text-slate-500 */ + background: url("data:image/svg+xml,"); + content: ''; + } + + .yoast-webinar-dashboard p{ + padding-left: 1.8rem; + } + + .yoast-webinar-dashboard .yoast-icon{ + width: 17px; + height: 17px; + margin-right: 12px; + } + + .yoast-notice-migrated-header{ + font-weight: 500; + font-size: .8125rem; + color: #1E293B; + line-height: 19px; + } + + .yoast-webinar-dashboard a{ + font-weight: 500; + } + + .yoast-webinar-dashboard .notice-yoast__container{ + padding: 0; + } + + .yoast-webinar-dashboard .notice-dismiss { + @apply yst-p-3 + } + + .notice-yoast__header { + margin-bottom: 0.25rem; + } + + @apply yst-bg-slate-100; + + /* Move WP footer behind our content. */ + z-index: -1; + + #wpcontent { + padding-left: 0 !important; + } + + #wpfooter { + @apply yst-pr-4; + + @media (min-width: 768px) { + @apply yst-pr-8; + + padding-left: 17rem; + } + } + + .wp-responsive-open #wpbody { + @media screen and (max-width: 782px) { + right: -190px; /* Override to not leave space between the mobile menu and the content. */ + } + } + + #modal-search .yst-modal__close { + margin-top: -0.25rem; + } + + &.sticky-menu { + .yst-root .yst-notifications--bottom-left { + @media (min-width: 783px) and (max-width: 962px) { + left: calc(160px + 2rem); /* Next to admin menu. 160px is the admin menu width. */ + } + } + + &.folded, &.auto-fold { + .yst-root .yst-notifications--bottom-left { + @media (min-width: 783px) and (max-width: 963px) { + left: calc(32px + 2rem); /* Next to admin menu. 32px is the collapsed admin menu width. */ + } + } + } + + &.folded { + .yst-root .yst-notifications--bottom-left { + @media (min-width: 962px) { + left: calc(32px + 2rem); /* Next to admin menu. 32px is the collapsed admin menu width. */ + } + } + } + } + + &:not(.sticky-menu) .wp-responsive-open .yst-root { + .yst-notifications--bottom-left { + @media (max-width: 783px) { + left: calc(190px + 2rem); /* Next to admin menu. 190px is the responsive admin menu width. */ + } + } + } + + .yst-root { + + .yst-mobile-navigation__top { + @media (min-width: 601px) and (max-width: 768px) { + top: 46px; + } + + @media (min-width: 783px) { + @apply yst-hidden; + } + } + + .yst-mobile-navigation__dialog { + z-index: 99999; + } + + .yst-paper { + .button { + /* .yst-button */ + @apply + yst-inline-flex + yst-items-center + yst-py-2 + yst-px-3 + yst-border + yst-shadow-sm + yst-no-underline + yst-cursor-pointer + yst-rounded-md + yst-text-sm + yst-font-medium + yst-leading-4 + + focus:yst-outline-none + focus:yst-ring-2 + focus:yst-ring-offset-2 + focus:yst-ring-primary-500 + + disabled:yst-cursor-not-allowed + disabled:yst-pointer-events-none + disabled:yst-opacity-50; + + /* .yst-button--secondary */ + @apply + yst-text-slate-800 + yst-bg-white + yst-border-slate-300 + hover:yst-bg-slate-50; + + /* .yst-button--small */ + @apply + yst-text-xs + yst-leading-4 + yst-px-2.5 + yst-py-1.5 + + /* custom for general page */ + yst-mt-3; + } + + .button-link { + @apply + yst-text-indigo-600 + yst-cursor-pointer + yst-underline + + hover:yst-text-indigo-500 + focus:yst-text-indigo-500 + focus:yst-outline-none + focus:yst-ring-1 + focus:yst-ring-offset-1 + focus:yst-ring-offset-transparent + focus:yst-ring-indigo-600 + focus:yst-rounded-sm + visited:yst-text-primary-500 + visited:hover:yst-text-primary-400; + } + + .ul-disc{ + @apply + yst-list-disc + yst-pl-3 + yst-ml-3 + yst-mt-1.5; + } + } + } + /* RTL */ + + &.rtl { + .yst-root .yst-replacevar .emoji-select-popover { + @apply yst-left-0 yst-right-auto; + } + } +} diff --git a/inc/class-wpseo-admin-bar-menu.php b/inc/class-wpseo-admin-bar-menu.php index 340531d4c72..f2ce1149d25 100644 --- a/inc/class-wpseo-admin-bar-menu.php +++ b/inc/class-wpseo-admin-bar-menu.php @@ -590,7 +590,7 @@ protected function add_premium_link( WP_Admin_Bar $wp_admin_bar ) { if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) { $sale_percentage = sprintf( '%1$s', - esc_html__( '30% OFF | Code: BF2024', 'wordpress-seo' ) + esc_html__( '30% OFF', 'wordpress-seo' ) ); } $wp_admin_bar->add_menu( diff --git a/inc/class-wpseo-content-images.php b/inc/class-wpseo-content-images.php index 6218c004e09..bae91bc6360 100644 --- a/inc/class-wpseo-content-images.php +++ b/inc/class-wpseo-content-images.php @@ -35,11 +35,11 @@ public function get_images_from_content( $content ) { } $content_images = $this->get_img_tags_from_content( $content ); - $images = array_map( [ $this, 'get_img_tag_source' ], $content_images ); - $images = array_filter( $images ); - $images = array_unique( $images ); - $images = array_values( $images ); // Reset the array keys. + $images = array_map( [ $this, 'get_img_tag_source' ], $content_images ); + $images = array_filter( $images ); + $images = array_unique( $images ); + $images = array_values( $images ); // Reset the array keys. return $images; } diff --git a/inc/class-wpseo-utils.php b/inc/class-wpseo-utils.php index 37a8d5a17db..beeeafd0f61 100644 --- a/inc/class-wpseo-utils.php +++ b/inc/class-wpseo-utils.php @@ -863,10 +863,6 @@ public static function get_admin_l10n() { 'postTypeNamePlural' => ( $page_type === 'post' ) ? $label_object->label : $label_object->name, 'postTypeNameSingular' => ( $page_type === 'post' ) ? $label_object->labels->singular_name : $label_object->singular_name, 'isBreadcrumbsDisabled' => WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ), - // phpcs:ignore Generic.ControlStructures.DisallowYodaConditions -- Bug: squizlabs/PHP_CodeSniffer#2962. - 'isPrivateBlog' => ( (string) get_option( 'blog_public' ) ) === '0', - 'news_seo_is_active' => ( defined( 'WPSEO_NEWS_FILE' ) ), - 'isAiFeatureActive' => (bool) WPSEO_Options::get( 'enable_ai_generator' ), ]; $additional_entries = apply_filters( 'wpseo_admin_l10n', [] ); diff --git a/package.json b/package.json index 361156a266a..07a93fadc6c 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "webpack-bundle-analyzer": "^4.9.1" }, "yoast": { - "pluginVersion": "23.5-RC1" + "pluginVersion": "23.6-RC4" }, "version": "0.0.0" } diff --git a/packages/js/images/jetpack-boost-integration-logo.svg b/packages/js/images/jetpack-boost-integration-logo.svg deleted file mode 100644 index b00a8b2bd4c..00000000000 --- a/packages/js/images/jetpack-boost-integration-logo.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - Group - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/js/images/jetpack-boost-logo.svg b/packages/js/images/jetpack-boost-logo.svg deleted file mode 100644 index d670bec7659..00000000000 --- a/packages/js/images/jetpack-boost-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/js/images/yoast-seo-simple-logo.svg b/packages/js/images/yoast-seo-simple-logo.svg deleted file mode 100644 index 5fa9051d0a5..00000000000 --- a/packages/js/images/yoast-seo-simple-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/js/package.json b/packages/js/package.json index 1ccb5955d1c..788c14007aa 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "cd ../.. && wp-scripts build --config config/webpack/webpack.config.js", "test": "jest", - "lint": "eslint src tests --max-warnings=69" + "lint": "eslint src tests --max-warnings=59" }, "dependencies": { "@draft-js-plugins/mention": "^5.0.0", @@ -27,7 +27,7 @@ "@wordpress/i18n": "^4.14.0", "@wordpress/is-shallow-equal": "^3.1.1", "@wordpress/plugins": "^3.1.2", - "@wordpress/rich-text": "^3.25.3", + "@wordpress/rich-text": "^7.0.2", "@wordpress/server-side-render": "^1.21.1", "@wordpress/url": "^3.17.0", "@yoast/analysis-report": "^1.21.0", diff --git a/packages/js/src/ai-generator/components/modal-content.js b/packages/js/src/ai-generator/components/modal-content.js index 466ed95c89a..087b620d1fe 100644 --- a/packages/js/src/ai-generator/components/modal-content.js +++ b/packages/js/src/ai-generator/components/modal-content.js @@ -14,51 +14,58 @@ export const ModalContent = () => { const upsellLinkPremium = useSelect( select => select( STORE ).selectLink( "https://yoa.st/ai-generator-upsell" ), [] ); const upsellLinkWooPremiumBundle = useSelect( select => select( STORE ).selectLink( "https://yoa.st/ai-generator-upsell-woo-seo-premium-bundle" ), [] ); const upsellLinkWoo = useSelect( select => select( STORE ).selectLink( "https://yoa.st/ai-generator-upsell-woo-seo" ), [] ); - const isPremium = useSelect( select => select( STORE ).getIsPremium(), [] ); - const isWooSeoUpsell = useSelect( select => select( STORE ).getIsWooSeoUpsell(), [] ); - const isProduct = useSelect( select => select( STORE ).getIsProduct(), [] ); - const wooSeoNoPremium = isProduct && ! isWooSeoUpsell && ! isPremium; - const isProductCopy = !! ( isWooSeoUpsell || wooSeoNoPremium ); - const postModalprops = { - isProductCopy, + + const isPremiumActive = useSelect( select => select( STORE ).getIsPremium(), [] ); + const isWooSeoActive = useSelect( select => select( STORE ).getIsWooSeoActive(), [] ); + const isWooCommerceActive = useSelect( select => select( STORE ).getIsWooCommerceActive(), [] ); + + const isProductPost = useSelect( select => select( STORE ).getIsProduct(), [] ); + const isProductTerm = useSelect( select => select( STORE ).getIsProductTerm(), [] ); + + const upsellProps = { upsellLink: upsellLinkPremium, }; - if ( isProductCopy ) { + // Use specific copy for product posts. + if ( isWooCommerceActive && isProductPost ) { + upsellProps.title = __( "Generate product titles & descriptions with AI!", "wordpress-seo" ); + upsellProps.isProductCopy = true; + } + + // Use specific copy for product posts and terms, otherwise revert to the defaults. + if ( isWooCommerceActive && ( isProductPost || isProductTerm ) ) { const upsellPremiumWooLabel = sprintf( /* translators: %1$s expands to Yoast SEO Premium, %2$s expands to Yoast WooCommerce SEO. */ __( "%1$s + %2$s", "wordpress-seo" ), "Yoast SEO Premium", "Yoast WooCommerce SEO" ); - postModalprops.newToText = sprintf( + upsellProps.newToText = sprintf( /* translators: %1$s expands to Yoast SEO Premium and Yoast WooCommerce SEO. */ __( "New in %1$s", "wordpress-seo" ), upsellPremiumWooLabel ); - postModalprops.title = __( "Generate product titles & descriptions with AI!", "wordpress-seo" ); - if ( ! isPremium && isWooSeoUpsell ) { - postModalprops.upsellLabel = `${ sprintf( + + if ( isPremiumActive ) { + upsellProps.upsellLabel = sprintf( + /* translators: %1$s expands to Yoast WooCommerce SEO. */ + __( "Unlock with %1$s", "wordpress-seo" ), + "Yoast WooCommerce SEO" + ); + upsellProps.upsellLink = upsellLinkWoo; + } else if ( ! isWooSeoActive ) { + upsellProps.upsellLabel = `${sprintf( /* translators: %1$s expands to Woo Premium bundle. */ __( "Unlock with the %1$s", "wordpress-seo" ), "Woo Premium bundle" )}*`; - postModalprops.bundleNote =

+ upsellProps.bundleNote =
{ `*${upsellPremiumWooLabel}` }
; - postModalprops.upsellLink = upsellLinkWooPremiumBundle; - } - if ( isPremium ) { - postModalprops.upsellLabel = sprintf( - /* translators: %1$s expands to Yoast WooCommerce SEO. */ - __( "Unlock with %1$s", "wordpress-seo" ), - "Yoast WooCommerce SEO" - ); - postModalprops.upsellLink = upsellLinkWoo; + upsellProps.upsellLink = upsellLinkWooPremiumBundle; } } - const imageLink = useSelect( select => select( STORE ).selectImageLink( "ai-generator-preview.png" ), [] ); const thumbnail = useMemo( () => ( { src: imageLink, @@ -71,13 +78,12 @@ export const ModalContent = () => { const { setWistiaEmbedPermission: set } = useDispatch( STORE ); const wistiaEmbedPermission = useMemo( () => ( { value, status, set } ), [ value, status, set ] ); - return ( ); }; diff --git a/packages/js/src/ai-generator/initialize.js b/packages/js/src/ai-generator/initialize.js index 98868210f5b..a5960b3ee80 100644 --- a/packages/js/src/ai-generator/initialize.js +++ b/packages/js/src/ai-generator/initialize.js @@ -53,10 +53,9 @@ const STORE = "yoast-seo/editor"; */ const initializeAiGenerator = () => { const isPremium = select( STORE ).getIsPremium(); - const isWooSeoUpsell = select( STORE ).getIsWooSeoUpsell(); - const isProduct = select( STORE ).getIsProduct(); - - const shouldShowAiGeneratorUpsell = ( isProduct ) ? ! isPremium || isWooSeoUpsell : ! isPremium; + const isWooSeoUpsellPost = select( STORE ).getIsWooSeoUpsell(); + const isWooSeoUpsellTerm = select( STORE ).getIsWooSeoUpsellTerm(); + const shouldShowAiGeneratorUpsell = ! isPremium || isWooSeoUpsellPost || isWooSeoUpsellTerm; addFilter( "yoast.replacementVariableEditor.additionalButtons", @@ -69,7 +68,6 @@ const initializeAiGenerator = () => { ); } - return buttons; } ); diff --git a/packages/js/src/analysis/collectAnalysisData.js b/packages/js/src/analysis/collectAnalysisData.js index 0cd64657b08..59b8ea1bdd5 100644 --- a/packages/js/src/analysis/collectAnalysisData.js +++ b/packages/js/src/analysis/collectAnalysisData.js @@ -2,6 +2,7 @@ import { applyFilters } from "@wordpress/hooks"; import { cloneDeep, merge, + get, } from "lodash"; import { serialize } from "@wordpress/blocks"; @@ -96,6 +97,7 @@ export default function collectAnalysisData( editorData, store, customAnalysisDa data.shortcodes = window.wpseoScriptData.analysis.plugins.shortcodes ? window.wpseoScriptData.analysis.plugins.shortcodes.wpseo_shortcode_tags : []; + data.isFrontPage = get( window, "wpseoScriptData.isFrontPage", "0" ) === "1"; return Paper.parse( applyFilters( "yoast.analysis.data", data ) ); } diff --git a/packages/js/src/components/BlackFridayPromotion.js b/packages/js/src/components/BlackFridayPromotion.js index b9603ee5567..e58d2a49e90 100644 --- a/packages/js/src/components/BlackFridayPromotion.js +++ b/packages/js/src/components/BlackFridayPromotion.js @@ -23,14 +23,14 @@ export const BlackFridayPromotion = ( { const linkParams = useSelect( select => select( store ).selectLinkParams(), [ store ] ); const title = location === "sidebar" ? sprintf( - /* translators: %1$s expands to YOAST SEO PREMIUM */ - __( "30%% OFF %1$s | Use code: BF2024", "wordpress-seo" ), + /* translators: %1$s expands to Yoast SEO Premium */ + __( "Now with 30%% OFF: %1$s", "wordpress-seo" ), "Yoast SEO Premium" ) : createInterpolateElement( sprintf( - /* translators: %1$s expands to YOAST SEO PREMIUM, %2$s expands to a link on yoast.com, %3$s expands to the anchor end tag. */ - __( "30%% OFF %1$s | Use code: BF2024 %2$sBuy now!%3$s", "wordpress-seo" ), + /* translators: %1$s expands to Yoast SEO Premium, %2$s expands to a link on yoast.com, %3$s expands to the anchor end tag. */ + __( "Now with 30%% OFF: %1$s %2$sBuy now!%3$s", "wordpress-seo" ), "Yoast SEO Premium", "", "" @@ -49,7 +49,7 @@ export const BlackFridayPromotion = ( { title={ title } { ...props } > - { __( "Black Friday", "wordpress-seo" ) } + { __( "BLACK FRIDAY SALE", "wordpress-seo" ) } { location === "sidebar" && { - return sprintf( - isJetpackBoostActive - /* translators: 1: Yoast, 2: Jetpack Boost, 3: Boost (short for Jetpack Boost). */ - ? __( "%1$s recommends using %2$s for automated Critical CSS generation. Whenever you change your site, %3$s will automatically generate optimal CSS and performance scores. Upgrade %3$s, speed up your site, and improve its ranking!", "wordpress-seo" ) - /* translators: 1: Yoast, 2: Jetpack Boost, 3: Boost (short for Jetpack Boost). */ - : __( "%1$s recommends using %2$s to speed up your site. Optimize CSS, defer non-essential Javascript, and lazy load images. Install %3$s, speed up your site, and improve its ranking!", "wordpress-seo" ), - "Yoast", - "Jetpack Boost", - "Boost" - ); -}; - -/* eslint-disable complexity */ -/** - * @param {string} store The redux store key. - * @param {boolean} isAlertDismissed Whether the "alert" is dismissed. - * @param {function} onDismissed Function that will dismiss the "alert". - * @returns {JSX.Element} The JetpackBoost element (which is null when dismissed). -*/ -const JetpackBoost = ( { store, isAlertDismissed, onDismissed } ) => { - const host = window.location.host; - const isJetpackBoostActive = get( window, "wpseoScriptData.isJetpackBoostActive", "" ) === "1"; - const isJetpackBoostNotPremium = get( window, "wpseoScriptData.isJetpackBoostNotPremium", "" ) === "1"; - const getJetpackBoostPrePublishLink = useSelect( select => select( store ).selectLink( `https://yoa.st/jetpack-boost-get-prepublish?domain=${host}`, [] ) ); - const upgradeJetpackBoostPrePublishLink = useSelect( select => select( store ).selectLink( `https://yoa.st/jetpack-boost-upgrade-prepublish?domain=${host}`, [] ) ); - - const isPremium = useSelect( select => select( store ).getIsPremium() ); - if ( isPremium || isAlertDismissed || ! isJetpackBoostNotPremium ) { - return null; - } - - return ( - - { /* If this UI library Root is available up the chain, there is no need for it here anymore. */ } - -
-
- - - -
- - { sprintf( - /* translators: 1: Yoast SEO; 2: Jetpack Boost. */ - __( "Speed up your website with %1$s and %2$s", "wordpress-seo" ), - "Yoast SEO", - "Jetpack Boost" - ) } - -

- { getDescription( isJetpackBoostActive ) } -

- - - - { sprintf( - isJetpackBoostActive - /* translators: Jetpack Boost. */ - ? __( "Upgrade %s", "wordpress-seo" ) - /* translators: Jetpack Boost. */ - : __( "Get %s", "wordpress-seo" ), - "Jetpack Boost" - ) } - - - - { - /* translators: Hidden accessibility text. */ - __( "(Opens in a new browser tab)", "wordpress-seo" ) - } - - - - - ); -}; -/* eslint-enable complexity */ - -JetpackBoost.propTypes = { - store: PropTypes.string, - // eslint-disable-next-line react/no-unused-prop-types -- Used in the `withPersistentDismiss` HOC. - alertKey: PropTypes.string, - isAlertDismissed: PropTypes.bool.isRequired, - onDismissed: PropTypes.func.isRequired, -}; - -JetpackBoost.defaultProps = { - store: "yoast-seo/editor", - alertKey: "get-jetpack-boost-pre-publish-notification", -}; - -export default withPersistentDismiss( JetpackBoost ); diff --git a/packages/js/src/components/PrimaryTaxonomyPicker.js b/packages/js/src/components/PrimaryTaxonomyPicker.js index cbf46617f95..ab5be3801d4 100644 --- a/packages/js/src/components/PrimaryTaxonomyPicker.js +++ b/packages/js/src/components/PrimaryTaxonomyPicker.js @@ -244,7 +244,7 @@ class PrimaryTaxonomyPicker extends Component { id={ fieldId } terms={ this.state.selectedTerms } /> - + { __( "Learn more", "wordpress-seo" ) } { __( "Learn more about the primary category.", "wordpress-seo" ) } diff --git a/packages/js/src/components/TaxonomyPicker.js b/packages/js/src/components/TaxonomyPicker.js index 2dd803fd966..1b0f4f8afdd 100644 --- a/packages/js/src/components/TaxonomyPicker.js +++ b/packages/js/src/components/TaxonomyPicker.js @@ -21,6 +21,7 @@ const TaxonomyPicker = ( { id, value, terms, label, onChange } ) => { return ( { isBlackFriday &&
-
{ __( "BLACK FRIDAY | 30% OFF", "wordpress-seo" ) }
+
{ __( "30% OFF - BLACK FRIDAY", "wordpress-seo" ) }
} { this.props.title } diff --git a/packages/js/src/components/contentAnalysis/SeoAnalysis.js b/packages/js/src/components/contentAnalysis/SeoAnalysis.js index 184283991ba..bd9f7a16fcd 100644 --- a/packages/js/src/components/contentAnalysis/SeoAnalysis.js +++ b/packages/js/src/components/contentAnalysis/SeoAnalysis.js @@ -209,7 +209,7 @@ class SeoAnalysis extends Component { const isPremium = getL10nObject().isPremium; // Don't show the button if the AI feature is not enabled for Yoast SEO Premium users. - if ( isPremium && ! this.props.isAiFeatureEnabled ) { + if ( ! this.props.isAiFeatureEnabled ) { return; } @@ -324,7 +324,6 @@ export default withSelect( ( select, ownProps ) => { getMarksButtonStatus, getResultsForKeyword, getIsElementorEditor, - getPreference, } = select( "yoast-seo/editor" ); const keyword = getFocusKeyphrase(); @@ -334,6 +333,6 @@ export default withSelect( ( select, ownProps ) => { marksButtonStatus: ownProps.hideMarksButtons ? "disabled" : getMarksButtonStatus(), keyword, isElementor: getIsElementorEditor(), - isAiFeatureEnabled: getPreference( "isAiFeatureActive", false ), + isAiFeatureEnabled: select( "yoast-seo-premium/editor" )?.getIsAiFeatureEnabled(), }; } )( SeoAnalysis ); diff --git a/packages/js/src/components/fills/MetaboxFill.js b/packages/js/src/components/fills/MetaboxFill.js index 6f8def95c95..95197220fae 100644 --- a/packages/js/src/components/fills/MetaboxFill.js +++ b/packages/js/src/components/fills/MetaboxFill.js @@ -23,7 +23,6 @@ import PremiumSEOAnalysisModal from "../modals/PremiumSEOAnalysisModal"; import KeywordUpsell from "../modals/KeywordUpsell"; import { BlackFridayProductEditorChecklistPromotion } from "../BlackFridayProductEditorChecklistPromotion"; import { BlackFridayPromotion } from "../BlackFridayPromotion"; -import { isWooCommerceActive } from "../../helpers/isWooCommerceActive"; import { withMetaboxWarningsCheck } from "../higherorder/withMetaboxWarningsCheck"; const BlackFridayProductEditorChecklistPromotionWithMetaboxWarningsCheck = withMetaboxWarningsCheck( BlackFridayProductEditorChecklistPromotion ); @@ -40,8 +39,9 @@ const BlackFridayPromotionWithMetaboxWarningsCheck = withMetaboxWarningsCheck( B export default function MetaboxFill( { settings } ) { const isTerm = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsTerm(), [] ); const isProduct = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsProduct(), [] ); + const isWooCommerceActive = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsWooCommerceActive(), [] ); - const shouldShowWooCommerceChecklistPromo = isProduct && isWooCommerceActive(); + const shouldShowWooCommerceChecklistPromo = isProduct && isWooCommerceActive; return ( <> diff --git a/packages/js/src/containers/PersistentDismissableNotification.js b/packages/js/src/containers/PersistentDismissableNotification.js index ae489886247..ee33a37115c 100644 --- a/packages/js/src/containers/PersistentDismissableNotification.js +++ b/packages/js/src/containers/PersistentDismissableNotification.js @@ -22,14 +22,16 @@ export const PersistentDismissableNotification = ( { onDismissed, } ) => { return isAlertDismissed ? null : ( -
+
{ hasIcon && } -

{ title }

+

{ title }

+
+
+

{ children }

-

{ children }

{ Image && }
diff --git a/packages/js/src/dashboard/app.js b/packages/js/src/dashboard/app.js new file mode 100644 index 00000000000..acd480fd0c3 --- /dev/null +++ b/packages/js/src/dashboard/app.js @@ -0,0 +1,142 @@ +/* eslint-disable complexity */ + +import { Transition } from "@headlessui/react"; +import { AdjustmentsIcon, BellIcon } from "@heroicons/react/outline"; +import { __ } from "@wordpress/i18n"; +import { useEffect, useMemo } from "@wordpress/element"; +import { select } from "@wordpress/data"; +import { addQueryArgs } from "@wordpress/url"; +import { SidebarNavigation, useSvgAria } from "@yoast/ui-library"; +import PropTypes from "prop-types"; +import { Link, Route, Routes, useLocation } from "react-router-dom"; +import { MenuItemLink, YoastLogo } from "../shared-admin/components"; +import { useSelectDashboard } from "./hooks"; +import { STORE_NAME } from "./constants"; +import { FirstTimeConfiguration, AlertCenter } from "./routes"; +import { getMigratingNoticeInfo, deleteMigratingNotices } from "../helpers/migrateNotices"; +import Notice from "./components/notice"; +import WebinarPromoNotification from "../components/WebinarPromoNotification"; +import { shouldShowWebinarPromotionNotificationInDashboard } from "../helpers/shouldShowWebinarPromotionNotification"; + +/** + * @param {string} [idSuffix] Extra id suffix. Can prevent double IDs on the page. + * @returns {JSX.Element} The menu element. + */ +const Menu = ( { idSuffix = "" } ) => { + const svgAriaProps = useSvgAria(); + const isPremium = useSelectDashboard( "selectPreference", [], "isPremium" ); + + return <> +
+ + + +
+
+
    + + + { __( "Alert center", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> + + + { __( "First-time configuration", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> +
+
+ ; +}; +Menu.propTypes = { + idSuffix: PropTypes.string, +}; + +/** + * @returns {JSX.Element} The app component. + */ +const App = () => { + const notices = useMemo( getMigratingNoticeInfo, [] ); + useEffect( () => { + deleteMigratingNotices( notices ); + }, [ notices ] ); + + const { pathname } = useLocation(); + const linkParams = select( STORE_NAME ).selectLinkParams(); + const webinarIntroSettingsUrl = addQueryArgs( "https://yoa.st/webinar-intro-settings", linkParams ); + + return ( + +
diff --git a/packages/js/src/settings/components/index.js b/packages/js/src/settings/components/index.js index 9d880ba08ca..48464eadc79 100644 --- a/packages/js/src/settings/components/index.js +++ b/packages/js/src/settings/components/index.js @@ -14,7 +14,5 @@ export { default as NewsSeoAlert } from "./news-seo-alert"; export { default as OpenGraphDisabledAlert } from "./open-graph-disabled-alert"; export { default as RouteLayout } from "./route-layout"; export { default as Search } from "./search"; -export { default as SidebarNavigation } from "./sidebar-navigation"; export { default as SidebarRecommendations } from "./sidebar-recommendations"; export { default as ErrorFallback } from "./error-fallback"; -export { ReactComponent as YoastLogo } from "./yoast-logo.svg"; diff --git a/packages/js/src/shared-admin/components/index.js b/packages/js/src/shared-admin/components/index.js index efc31492597..4cbaeb72147 100644 --- a/packages/js/src/shared-admin/components/index.js +++ b/packages/js/src/shared-admin/components/index.js @@ -2,8 +2,10 @@ export { AcademyUpsellCard } from "./academy-upsell-card"; export { AiGenerateTitlesAndDescriptionsUpsell } from "./ai-generate-titles-and-descriptions-upsell"; export { FieldsetLayout } from "./fieldset-layout"; export { ReactComponent as G2LogoWhite } from "./g2-logo-white.svg"; +export { MenuItemLink } from "./menu-item-link"; export { OutboundLink } from "./outbound-link"; export { PremiumUpsellCard } from "./premium-upsell-card"; export { RecommendationsSidebar } from "./recommendations-sidebar"; export { VideoFlow } from "./video-flow"; export { AiFixAssessmentsUpsell } from "./ai-fix-assessments-upsell"; +export { ReactComponent as YoastLogo } from "./yoast-logo.svg"; diff --git a/packages/js/src/settings/components/sidebar-navigation.js b/packages/js/src/shared-admin/components/menu-item-link.js similarity index 64% rename from packages/js/src/settings/components/sidebar-navigation.js rename to packages/js/src/shared-admin/components/menu-item-link.js index 6c98ccedf61..6ebe3b87c21 100644 --- a/packages/js/src/settings/components/sidebar-navigation.js +++ b/packages/js/src/shared-admin/components/menu-item-link.js @@ -4,24 +4,18 @@ import { replace } from "lodash"; import PropTypes from "prop-types"; import { Link } from "react-router-dom"; -const PureSubmenuItem = SidebarNavigation.SubmenuItem; - /** * @param {Object} props The props. * @param {string} to The path to link to. * @param {string} [idSuffix] Extra id suffix. Can prevent double IDs on the page. * @returns {JSX.Element} The submenu item element. */ -const SubmenuItem = ( { to, idSuffix = "", ...props } ) => { +export const MenuItemLink = ( { to, idSuffix = "", ...props } ) => { const id = useMemo( () => replace( replace( `link-${ to }`, "/", "-" ), "--", "-" ), [ to ] ); - return ; + return ; }; -SubmenuItem.propTypes = { +MenuItemLink.propTypes = { to: PropTypes.string.isRequired, idSuffix: PropTypes.string, }; - -SidebarNavigation.SubmenuItem = SubmenuItem; - -export default SidebarNavigation; diff --git a/packages/js/src/shared-admin/components/premium-upsell-card.js b/packages/js/src/shared-admin/components/premium-upsell-card.js index 36acefd164c..b38823070da 100644 --- a/packages/js/src/shared-admin/components/premium-upsell-card.js +++ b/packages/js/src/shared-admin/components/premium-upsell-card.js @@ -54,7 +54,7 @@ export const PremiumUpsellCard = ( { link, linkProps, promotions } ) => { { isBlackFriday &&
- { __( "30% OFF | Code: BF2024", "wordpress-seo" ) } + { __( "30% OFF - BLACK FRIDAY", "wordpress-seo" ) }
} @@ -74,8 +74,10 @@ export const PremiumUpsellCard = ( { link, linkProps, promotions } ) => { <ArrowNarrowRightIcon className="yst-w-4 yst-h-4 yst-icon-rtl" /> </Button> <p className="yst-text-center yst-text-xs yst-mx-2 yst-font-light yst-leading-5 yst-mt-2"> - { __( "Only $/€/£99 per year (ex VAT)", "wordpress-seo" ) } - <br /> + { ! isBlackFriday && <> + { __( "Only $/€/£99 per year (ex VAT)", "wordpress-seo" ) } + <br /> + </> } { __( "30-day money back guarantee.", "wordpress-seo" ) } </p> <hr className="yst-border-t yst-border-primary-300 yst-my-4" /> diff --git a/packages/js/src/shared-admin/components/premium-upsell-list.js b/packages/js/src/shared-admin/components/premium-upsell-list.js new file mode 100644 index 00000000000..1fe2f9f8052 --- /dev/null +++ b/packages/js/src/shared-admin/components/premium-upsell-list.js @@ -0,0 +1,70 @@ +import { ArrowNarrowRightIcon } from "@heroicons/react/outline"; +import { createInterpolateElement } from "@wordpress/element"; +import { __, sprintf } from "@wordpress/i18n"; +import { Button, Title, Paper } from "@yoast/ui-library"; +import PropTypes from "prop-types"; +import { getPremiumBenefits } from "../../helpers/get-premium-benefits"; + +/** + * @param {string} premiumLink The premium link. + * @param {Object} [premiumUpsellConfig] The premium upsell configuration data. + * @param {array} [promotions] Promotions. + * @returns {JSX.Element} The premium upsell card. + */ +export const PremiumUpsellList = ( { premiumLink, premiumUpsellConfig, promotions } ) => { + const isBlackFriday = promotions.includes( "black-friday-2024-promotion" ); + return ( + <Paper as="div" className="xl:yst-max-w-3xl"> + { isBlackFriday && <div + className="yst-rounded-t-lg yst-h-9 yst-flex yst-justify-between yst-items-center yst-bg-black yst-text-amber-300 yst-px-4 yst-text-lg yst-border-b yst-border-amber-300 yst-border-solid yst-font-semibold" + > + <div>{ __( "30% OFF", "wordpress-seo" ) }</div> + <div>{ __( "BLACK FRIDAY", "wordpress-seo" ) }</div> + </div> } + <div className="yst-p-6 yst-flex yst-flex-col"> + <Title as="h2" size="4" className="yst-text-xl yst-text-primary-500"> + { sprintf( + /* translators: %s expands to "Yoast SEO" Premium */ + __( "Upgrade to %s", "wordpress-seo" ), + "Yoast SEO Premium" + ) } + +
    + { getPremiumBenefits().map( ( benefit, index ) => ( +
  • + { createInterpolateElement( benefit, { strong: } ) } +
  • + ) ) } +
+ +
+ + ); +}; + +PremiumUpsellList.propTypes = { + premiumLink: PropTypes.string.isRequired, + premiumUpsellConfig: PropTypes.object, + promotions: PropTypes.array, +}; + +PremiumUpsellList.defaultProps = { + premiumUpsellConfig: {}, + promotions: [], +}; diff --git a/packages/js/src/settings/components/yoast-logo.svg b/packages/js/src/shared-admin/components/yoast-logo.svg similarity index 100% rename from packages/js/src/settings/components/yoast-logo.svg rename to packages/js/src/shared-admin/components/yoast-logo.svg diff --git a/packages/js/src/term-edit.js b/packages/js/src/term-edit.js index bb52f732005..385a0439f26 100644 --- a/packages/js/src/term-edit.js +++ b/packages/js/src/term-edit.js @@ -33,11 +33,6 @@ domReady( () => { // Initialize the insights. initializeInsights(); - // Don't initialize the AI generator for WooCommerce categories and tags. - const AI_IGNORED_TAXONOMIES = [ "product_cat", "product_tag" ]; - - if ( window.wpseoScriptData.termType && ! AI_IGNORED_TAXONOMIES.includes( window.wpseoScriptData.termType ) ) { - // Initialize the AI Generator upsell. - initializeAiGenerator(); - } + // Initialize the AI Generator upsell. + initializeAiGenerator(); } ); diff --git a/packages/js/tests/dashboard/components/__snapshots__/alerts-list.test.js.snap b/packages/js/tests/dashboard/components/__snapshots__/alerts-list.test.js.snap new file mode 100644 index 00000000000..2f725cdd0a2 --- /dev/null +++ b/packages/js/tests/dashboard/components/__snapshots__/alerts-list.test.js.snap @@ -0,0 +1,212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsList should match snapshot 1`] = ` +
+
    +
  • +
    +
    + + + +
    +
    + You've added a new type of content. We recommend that you review the corresponding Search appearance settings. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well! +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + Huge SEO Issue: You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute. +
    + +
    +
  • +
+
+`; diff --git a/packages/js/tests/dashboard/components/__snapshots__/alerts-title.test.js.snap b/packages/js/tests/dashboard/components/__snapshots__/alerts-title.test.js.snap new file mode 100644 index 00000000000..b7a2388f49f --- /dev/null +++ b/packages/js/tests/dashboard/components/__snapshots__/alerts-title.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsTitle should match snapshot 1`] = ` +
+
+
+ +

+ Test Title + + (5) +

+
+
+
+`; diff --git a/packages/js/tests/dashboard/components/__snapshots__/notifications.test.js.snap b/packages/js/tests/dashboard/components/__snapshots__/notifications.test.js.snap new file mode 100644 index 00000000000..0c92339309a --- /dev/null +++ b/packages/js/tests/dashboard/components/__snapshots__/notifications.test.js.snap @@ -0,0 +1,290 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsList should match snapshot 1`] = ` +
+
+
+
+
+ +

+ Notifications + + (4) +

+
+
+
    +
  • +
    +
    + + + +
    +
    + You've added a new type of content. We recommend that you review the corresponding Search appearance settings. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well! +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + Huge SEO Issue: You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute. +
    + +
    +
  • +
+
+ +
+
+
+
+`; diff --git a/packages/js/tests/dashboard/components/__snapshots__/problems.test.js.snap b/packages/js/tests/dashboard/components/__snapshots__/problems.test.js.snap new file mode 100644 index 00000000000..5af109bd2ab --- /dev/null +++ b/packages/js/tests/dashboard/components/__snapshots__/problems.test.js.snap @@ -0,0 +1,298 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsList should match snapshot 1`] = ` +
+
+
+
+
+ +

+ Problems + + (4) +

+
+

+ We have detected the following issues that affect the SEO of your site. +

+
+
    +
  • +
    +
    + + + +
    +
    + You've added a new type of content. We recommend that you review the corresponding Search appearance settings. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well! +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + + Huge SEO Issue: + + You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results. +
    + +
    +
  • +
  • +
    +
    + + + +
    +
    + We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute. +
    + +
    +
  • +
+
+ +
+
+
+
+`; diff --git a/packages/js/tests/dashboard/components/__snapshots__/route-layout.test.js.snap b/packages/js/tests/dashboard/components/__snapshots__/route-layout.test.js.snap new file mode 100644 index 00000000000..2a8707c8899 --- /dev/null +++ b/packages/js/tests/dashboard/components/__snapshots__/route-layout.test.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RouteLayout renders the component correctly for custom props 1`] = ` +
+
+
+

+ Custom title +

+

+ custom description +

+
+
+ empty component +
+
+
+
+ Custom title Dashboard - Yoast SEO +
+
+
+
+`; + +exports[`RouteLayout renders the component correctly for default props 1`] = ` +
+
+
+

+ Title +

+

+ Description +

+
+
+ empty component +
+
+
+
+ Title Dashboard - Yoast SEO +
+
+
+
+`; diff --git a/packages/js/tests/dashboard/components/alerts-list.test.js b/packages/js/tests/dashboard/components/alerts-list.test.js new file mode 100644 index 00000000000..bb67ee5f424 --- /dev/null +++ b/packages/js/tests/dashboard/components/alerts-list.test.js @@ -0,0 +1,46 @@ +import { render } from "../../test-utils"; +import { AlertsList } from "../../../src/dashboard/components/alerts-list"; +import { AlertsContext } from "../../../src/dashboard/contexts/alerts-context"; + +const notificationsTheme = { + bulletClass: "yst-fill-blue-500", +}; + +jest.mock( "@wordpress/data", () => ( { + useDispatch: jest.fn( () => ( { toggleAlertStatus: jest.fn() } ) ), +} ) ); + +describe( "AlertsList", () => { + it( "should match snapshot", () => { + const items = [ + { + id: "test-id-1", + message: "You've added a new type of content. We recommend that you review the corresponding Search appearance settings.", + type: "warning", + dismissed: false, + }, + { + id: "test-id-2", + message: "We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well!", + type: "warning", + dismissed: false, + }, + { + id: "test-id-3", + message: "Huge SEO Issue: You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results.", + type: "warning", + dismissed: true, + }, + { + id: "test-id-4", + message: "We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute.", + type: "warning", + dismissed: true, + }, + ]; + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/js/tests/dashboard/components/alerts-title.test.js b/packages/js/tests/dashboard/components/alerts-title.test.js new file mode 100644 index 00000000000..8bfd494af74 --- /dev/null +++ b/packages/js/tests/dashboard/components/alerts-title.test.js @@ -0,0 +1,36 @@ +import { render } from "../../test-utils"; +import { AlertsTitle } from "../../../src/dashboard/components/alerts-title"; +import { AlertsContext } from "../../../src/dashboard/contexts/alerts-context"; +import { BellIcon } from "@heroicons/react/outline"; + +const notificationsTheme = { + Icon: BellIcon, + bulletClass: "yst-fill-blue-500", + iconClass: "yst-text-blue-500", +}; + +describe( "AlertsTitle", () => { + it( "should match snapshot", () => { + const title = "Test Title"; + const counts = 5; + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); + + it( "renders the children correctly", () => { + const children =
Test Children
; + + const { getByText } = render( + + + { children } + + + ); + + const childrenElement = getByText( "Test Children" ); + expect( childrenElement ).toBeInTheDocument(); + } ); +} ); diff --git a/packages/js/tests/dashboard/components/collapsible.test.js b/packages/js/tests/dashboard/components/collapsible.test.js new file mode 100644 index 00000000000..c7814ae5588 --- /dev/null +++ b/packages/js/tests/dashboard/components/collapsible.test.js @@ -0,0 +1,24 @@ +import { render, fireEvent } from "../../test-utils"; +import { Collapsible } from "../../../src/dashboard/components/collapsible"; + +describe( "Collapsible", () => { + it( "should hide children when collapsible is closed", () => { + const { queryByText } = render( +
Test Children
+
); + + const childrenElement = queryByText( "Test Children" ); + expect( childrenElement ).not.toBeInTheDocument(); + } ); + it( "should show children when collapsible is open", () => { + const { queryByText, getByRole } = render( +
Test Children
+
); + + const buttonElement = getByRole( "button" ); + fireEvent.click( buttonElement ); + + const childrenElement = queryByText( "Test Children" ); + expect( childrenElement ).toBeInTheDocument(); + } ); +} ); diff --git a/packages/js/tests/dashboard/components/notifications.test.js b/packages/js/tests/dashboard/components/notifications.test.js new file mode 100644 index 00000000000..765288ac2d4 --- /dev/null +++ b/packages/js/tests/dashboard/components/notifications.test.js @@ -0,0 +1,41 @@ +import { render } from "../../test-utils"; +import { Notifications } from "../../../src/dashboard/components/notifications"; + +const items = [ + { + id: "test-id-1", + message: "You've added a new type of content. We recommend that you review the corresponding Search appearance settings.", + type: "warning", + dismissed: false, + }, + { + id: "test-id-2", + message: "We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well!", + type: "warning", + dismissed: false, + }, + { + id: "test-id-3", + message: "Huge SEO Issue: You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results.", + type: "warning", + dismissed: false, + }, + { + id: "test-id-4", + message: "We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute.", + type: "warning", + dismissed: false, + }, +]; + +jest.mock( "@wordpress/data", () => ( { + useSelect: jest.fn( () => items ), + useDispatch: jest.fn( () => ( { toggleAlertStatus: jest.fn() } ) ), +} ) ); + +describe( "AlertsList", () => { + it( "should match snapshot", () => { + const { container } = render( ); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/js/tests/dashboard/components/problems.test.js b/packages/js/tests/dashboard/components/problems.test.js new file mode 100644 index 00000000000..595531334bc --- /dev/null +++ b/packages/js/tests/dashboard/components/problems.test.js @@ -0,0 +1,41 @@ +import { render } from "../../test-utils"; +import { Problems } from "../../../src/dashboard/components/problems"; + +const items = [ + { + id: "test-id-1", + message: "You've added a new type of content. We recommend that you review the corresponding Search appearance settings.", + type: "error", + dismissed: false, + }, + { + id: "test-id-2", + message: "We notice that you have installed WPML. To make sure your canonical URLs are set correctly, install and activate the WPML SEO add-on as well!", + type: "error", + dismissed: false, + }, + { + id: "test-id-3", + message: "Huge SEO Issue: You're blocking access to robots. If you want search engines to show this site in their results, you must go to your Reading Settings and uncheck the box for Search Engine Visibility. I don't want this site to show in the search results.", + type: "error", + dismissed: false, + }, + { + id: "test-id-4", + message: "We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. We estimate this will take less than a minute.", + type: "error", + dismissed: false, + }, +]; + +jest.mock( "@wordpress/data", () => ( { + useSelect: jest.fn( () => items ), + useDispatch: jest.fn( () => ( { toggleAlertStatus: jest.fn() } ) ), +} ) ); + +describe( "AlertsList", () => { + it( "should match snapshot", () => { + const { container } = render( ); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/js/tests/dashboard/components/route-layout.test.js b/packages/js/tests/dashboard/components/route-layout.test.js new file mode 100644 index 00000000000..626a874c1a1 --- /dev/null +++ b/packages/js/tests/dashboard/components/route-layout.test.js @@ -0,0 +1,24 @@ +import { render } from "../../test-utils"; +import { RouteLayout } from "../../../src/dashboard/components/route-layout"; + +describe( "RouteLayout", () => { + const props = { + children: <>empty component , + title: "Title", + description: "Description", + }; + + it( "renders the component correctly for default props", () => { + const { container } = render( ); + expect( container ).toMatchSnapshot(); + } ); + + it( "renders the component correctly for custom props", () => { + const { container } = render( ); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/js/tests/dashboard/store/alert-center.test.js b/packages/js/tests/dashboard/store/alert-center.test.js new file mode 100644 index 00000000000..4f8f9285fdc --- /dev/null +++ b/packages/js/tests/dashboard/store/alert-center.test.js @@ -0,0 +1,59 @@ +import { toggleAlertStatus, alertCenterActions, alertCenterSelectors } from "../../../src/dashboard/store/alert-center"; + +describe( "toggleAlertStatus", () => { + it( "should dispatch the request action", () => { + const id = "alertId"; + const nonce = "alertNonce"; + const hidden = false; + + const generator = toggleAlertStatus( id, nonce, hidden ); + + expect( generator.next().value ).toEqual( { + type: "toggleAlertVisibility/request", + } ); + } ); + + it( "should dispatch the success action with the correct payload", () => { + const id = "alertId"; + const nonce = "alertNonce"; + const hidden = false; + + const generator = toggleAlertStatus( id, nonce, hidden ); + + generator.next(); + + const successAction = generator.next().value; + + expect( successAction.type ).toEqual( "toggleAlertVisibility" ); + expect( successAction.payload.id ).toEqual( id ); + expect( successAction.payload.nonce ).toEqual( nonce ); + expect( successAction.payload.hidden ).toEqual( hidden ); + } ); +} ); + +describe( "alertCenterActions", () => { + it( "should have the toggleAlert action", () => { + expect( alertCenterActions.toggleAlert ).toBeDefined(); + expect( alertCenterActions.toggleAlertStatus ).toBeDefined(); + } ); +} ); + +describe( "alertCenterSelectors", () => { + it( "should have the selectActiveNotifications selector", () => { + expect( alertCenterSelectors.selectActiveNotifications ).toBeDefined(); + } ); + + it( "should have the selectDismissedNotifications selector", () => { + expect( alertCenterSelectors.selectDismissedNotifications ).toBeDefined(); + } ); + + it( "should have the selectActiveProblems selector", () => { + expect( alertCenterSelectors.selectActiveProblems ).toBeDefined(); + } ); + + it( "should have the selectDismissedProblems selector", () => { + expect( alertCenterSelectors.selectDismissedProblems ).toBeDefined(); + } ); +} ); + + diff --git a/packages/ui-library/src/components/sidebar-navigation/collapsible.js b/packages/ui-library/src/components/sidebar-navigation/collapsible.js new file mode 100644 index 00000000000..6fb055b7840 --- /dev/null +++ b/packages/ui-library/src/components/sidebar-navigation/collapsible.js @@ -0,0 +1,51 @@ +import { ChevronDownIcon } from "@heroicons/react/outline"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; +import { useToggleState } from "../../hooks"; +import { Icon } from "./icon"; + +/** + * @param {JSX.ElementClass} [as="div"] The component. + * @param {string} label The label. + * @param {JSX.ElementClass} [icon] Optional icon to put before the label. + * @param {JSX.node} [children] The content. + * @param {boolean} [defaultOpen] Whether the sub menu starts opened. + * @param {Object} [props] Extra props. + * @returns {JSX.Element} The element. + */ +export const Collapsible = ( { as: Component = "div", label, icon, children, defaultOpen = true, ...props } ) => { + const [ isOpen, toggleOpen ] = useToggleState( defaultOpen ); + + return ( + + + { isOpen && children } + + ); +}; + +Collapsible.displayName = "SidebarNavigation.Collapsible"; +Collapsible.propTypes = { + as: PropTypes.elementType, + icon: PropTypes.elementType, + label: PropTypes.string.isRequired, + defaultOpen: PropTypes.bool, + children: PropTypes.node, +}; diff --git a/packages/ui-library/src/components/sidebar-navigation/icon.js b/packages/ui-library/src/components/sidebar-navigation/icon.js new file mode 100644 index 00000000000..feab6d9736d --- /dev/null +++ b/packages/ui-library/src/components/sidebar-navigation/icon.js @@ -0,0 +1,28 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; + +/** + * @param {JSX.ElementClass} [as="span"] The component. + * @param {JSX.node} [children] The content. + * @param {string} [className] The classname. + * @param {Object} [props] Extra props. + * @returns {JSX.Element} The element. + */ +export const Icon = ( { as: Component = "span", children, className, ...props } ) => { + return ( + + { children } + + ); +}; + +Icon.displayName = "SidebarNavigation.Icon"; +Icon.propTypes = { + as: PropTypes.elementType, + children: PropTypes.node, + className: PropTypes.string, +}; diff --git a/packages/ui-library/src/components/sidebar-navigation/index.js b/packages/ui-library/src/components/sidebar-navigation/index.js index 749fb7cc709..05f3c2d1ba0 100644 --- a/packages/ui-library/src/components/sidebar-navigation/index.js +++ b/packages/ui-library/src/components/sidebar-navigation/index.js @@ -1,6 +1,11 @@ import { noop } from "lodash"; import PropTypes from "prop-types"; import React, { createContext, useContext, useState } from "react"; +import { Collapsible } from "./collapsible"; +import { Icon } from "./icon"; +import { Item } from "./item"; +import { Link } from "./link"; +import { List } from "./list"; import MenuItem from "./menu-item"; import Mobile from "./mobile"; import Sidebar from "./sidebar"; @@ -37,13 +42,23 @@ SidebarNavigation.propTypes = { children: PropTypes.node.isRequired, }; +// Different types of navigation. SidebarNavigation.Sidebar = Sidebar; SidebarNavigation.Sidebar.displayName = "SidebarNavigation.Sidebar"; SidebarNavigation.Mobile = Mobile; SidebarNavigation.Mobile.displayName = "SidebarNavigation.Mobile"; + +// "V1" components. SidebarNavigation.MenuItem = MenuItem; SidebarNavigation.MenuItem.displayName = "SidebarNavigation.MenuItem"; SidebarNavigation.SubmenuItem = SubmenuItem; SidebarNavigation.SubmenuItem.displayName = "SidebarNavigation.SubmenuItem"; +// "V2" building blocks. +SidebarNavigation.List = List; +SidebarNavigation.Item = Item; +SidebarNavigation.Collapsible = Collapsible; +SidebarNavigation.Link = Link; +SidebarNavigation.Icon = Icon; + export default SidebarNavigation; diff --git a/packages/ui-library/src/components/sidebar-navigation/item.js b/packages/ui-library/src/components/sidebar-navigation/item.js new file mode 100644 index 00000000000..8a6b254d7f7 --- /dev/null +++ b/packages/ui-library/src/components/sidebar-navigation/item.js @@ -0,0 +1,30 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; + +/** + * Represents a Menu Item. + * + * @param {JSX.ElementClass} [as="li"] The component. + * @param {JSX.node} [children] The content. + * @param {string} [className] The classname. + * @param {Object} [props] Extra props. + * @returns {JSX.Element} The menu. + */ +export const Item = ( { as: Component = "li", children, className, ...props } ) => { + return ( + + { children } + + ); +}; + +Item.displayName = "SidebarNavigation.Item"; +Item.propTypes = { + as: PropTypes.elementType, + children: PropTypes.node, + className: PropTypes.string, +}; diff --git a/packages/ui-library/src/components/sidebar-navigation/link.js b/packages/ui-library/src/components/sidebar-navigation/link.js new file mode 100644 index 00000000000..3aa68194f94 --- /dev/null +++ b/packages/ui-library/src/components/sidebar-navigation/link.js @@ -0,0 +1,46 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React, { useCallback } from "react"; +import { useNavigationContext } from "./index"; + +/** + * @param {JSX.ElementClass} [as="a"] The component. + * @param {string} [pathProp="href"] The key of the path in the props. + * @param {JSX.node} [children] The content. + * @param {string} [className] The classname. + * @param {Function} [onClick] The click handler. We wrap this to close the mobile menu on click. + * @param {Object} [props] Extra props. + * @returns {JSX.Element} The element. + */ +export const Link = ( { as: Component = "a", pathProp = "href", children, className, onClick, ...props } ) => { + const { activePath, setMobileMenuOpen } = useNavigationContext(); + + const handleClick = useCallback( () => { + setMobileMenuOpen( false ); + onClick?.(); + }, [ setMobileMenuOpen ] ); + + return ( + + { children } + + ); +}; + +Link.displayName = "SidebarNavigation.Link"; +Link.propTypes = { + as: PropTypes.elementType, + pathProp: PropTypes.string, + children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func, +}; diff --git a/packages/ui-library/src/components/sidebar-navigation/list.js b/packages/ui-library/src/components/sidebar-navigation/list.js new file mode 100644 index 00000000000..8412a8e138f --- /dev/null +++ b/packages/ui-library/src/components/sidebar-navigation/list.js @@ -0,0 +1,35 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; + +/** + * @param {JSX.ElementClass} [as="ul"] The component. + * @param {JSX.node} [children] The content. + * @param {boolean} [isIndented=false] Whether the list is indented. + * @param {string} [className] The classname. + * @param {Object} [props] Extra props. + * @returns {JSX.Element} The element. + */ +export const List = ( { as: Component = "ul", children, isIndented = false, className, ...props } ) => { + return ( + + { children } + + ); +}; + +List.displayName = "SidebarNavigation.List"; +List.propTypes = { + as: PropTypes.elementType, + children: PropTypes.node, + isIndented: PropTypes.bool, + className: PropTypes.string, +}; diff --git a/packages/ui-library/src/components/sidebar-navigation/menu-item.js b/packages/ui-library/src/components/sidebar-navigation/menu-item.js index a2aeadba321..066563bc749 100644 --- a/packages/ui-library/src/components/sidebar-navigation/menu-item.js +++ b/packages/ui-library/src/components/sidebar-navigation/menu-item.js @@ -1,7 +1,7 @@ -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/outline"; import PropTypes from "prop-types"; import React from "react"; -import { useToggleState } from "../../hooks"; +import { Collapsible } from "./collapsible"; +import { List } from "./list"; /** * @param {string} label The label. @@ -9,33 +9,15 @@ import { useToggleState } from "../../hooks"; * @param {JSX.node} [children] Optional sub menu. * @param {boolean} [defaultOpen] Whether the sub menu starts opened. * @param {Object} [props] Extra props. - * @returns {JSX.Element} The menu item element. + * @returns {JSX.Element} The element. */ const MenuItem = ( { label, icon: Icon = null, children = null, defaultOpen = true, ...props } ) => { - const [ isOpen, toggleOpen ] = useToggleState( defaultOpen ); - const ChevronIcon = isOpen ? ChevronUpIcon : ChevronDownIcon; - return ( -
- - { isOpen && children &&
    + + { children } -
} -
+ + ); }; diff --git a/packages/ui-library/src/components/sidebar-navigation/sidebar.js b/packages/ui-library/src/components/sidebar-navigation/sidebar.js index a3260ef3d55..3f45318f46c 100644 --- a/packages/ui-library/src/components/sidebar-navigation/sidebar.js +++ b/packages/ui-library/src/components/sidebar-navigation/sidebar.js @@ -1,3 +1,4 @@ +import classNames from "classnames"; import PropTypes from "prop-types"; import React from "react"; @@ -7,7 +8,7 @@ import React from "react"; * @returns {JSX.Element} The sidebar element. */ const Sidebar = ( { children, className = "" } ) => ( - + ); Sidebar.propTypes = { diff --git a/packages/ui-library/src/components/sidebar-navigation/stories.js b/packages/ui-library/src/components/sidebar-navigation/stories.js index 37849481424..bbbb0352f9c 100644 --- a/packages/ui-library/src/components/sidebar-navigation/stories.js +++ b/packages/ui-library/src/components/sidebar-navigation/stories.js @@ -1,52 +1,68 @@ import { AdjustmentsIcon, ColorSwatchIcon, DesktopComputerIcon, NewspaperIcon } from "@heroicons/react/outline"; -import React from "react"; +import { useArgs } from "@storybook/preview-api"; +import React, { useCallback, useEffect } from "react"; import SidebarNavigation from "."; import { InteractiveDocsPage } from "../../../.storybook/interactive-docs-page"; import Table from "../../elements/table"; -const Template = ( { children, activePath } ) => { - return ( - - { children } - ); +const Template = ( args ) => { + const [ , updateArgs ] = useArgs(); + + const hashChangeHandler = useCallback( () => updateArgs( { activePath: document.location.hash } ), [ updateArgs ] ); + + useEffect( () => { + window.addEventListener( "hashchange", hashChangeHandler ); + hashChangeHandler(); + + return () => window.removeEventListener( "hashchange", hashChangeHandler ); + }, [ hashChangeHandler ] ); + + return ; }; export const Factory = { render: Template.bind( {} ), args: { children: - - - - - - - - - - - - - - - - - - +
    + + + + + + + + + + + + + + + + + + Item 1 label + } + className="yst-gap-3" + /> +
, }, }; @@ -117,7 +133,6 @@ export const Sidebar = { }, }; - export const Mobile = { render: Template.bind( {} ), parameters: { @@ -133,17 +148,36 @@ export const Mobile = { closeButtonScreenReaderText="Close sidebar" >
Mobile menu
- - - - - - +
    + + + + + + + + + + + + + Item 1 label + } + className="yst-gap-3" + /> +
), }, }; @@ -188,6 +222,96 @@ export const NavigationContext = { }, }; +export const UsingBuildingBlocks = { + render: Template.bind( {} ), + name: "Using the smaller building blocks", + parameters: { + docs: { + description: { + story: "To allow for more flexibility, we provide `SidebarNavigation.List`, `SidebarNavigation.Item`, `SidebarNavigation.Collapsible`, `SidebarNavigation.Link` and `SidebarNavigation.Icon` to create your own combinations.", + }, + }, + }, + args: { + children: ( + + + + + + Link 1 + + + Link 2 + + + Link 3 + + + + + + + Link 4 + + + Link 5 + + + Link 6 + + + + + + + Link 7 + + + + + ), + }, +}; + +export const NotUsingBuildingBlocks = { + render: Template.bind( {} ), + name: "Not using the smaller building blocks", + parameters: { + docs: { + description: { + story: "Here is the same example as the previous one, but without using the smaller building blocks. The `MenuItem` is a `Collapsible` with a `List`. The `SubmenuItem` is an `Item` with a `Link`.", + }, + }, + }, + args: { + children: ( + +
    + + + + + + + + + + + + + Link 7 + } + className="yst-flex yst-gap-x-3" + /> +
+
+ ), + }, +}; + export default { title: "2) Components/Sidebar navigation", component: SidebarNavigation, @@ -217,7 +341,7 @@ export default { }, icon: { control: "object", - description: "Available for `MenuItem`", + description: "Available for `MenuItem` and `SubmenuItem`", table: { type: { summary: "JSX Element" }, }, @@ -250,7 +374,16 @@ export default { description: { component: "A sidebar navigation component. Contains the subcomponents `Sidebar`, `Mobile`, `MenuItem` and `SubmenuItem` and contains the hook `useNavigationContext`.", }, - page: () => , + page: () => , }, }, }; diff --git a/packages/ui-library/src/components/sidebar-navigation/style.css b/packages/ui-library/src/components/sidebar-navigation/style.css index 9f8a7d082d9..14fba087243 100644 --- a/packages/ui-library/src/components/sidebar-navigation/style.css +++ b/packages/ui-library/src/components/sidebar-navigation/style.css @@ -7,5 +7,107 @@ .yst-mobile-navigation__dialog { @apply yst-fixed yst-inset-0 yst-flex yst-z-50; } + + .yst-sidebar-navigation__sidebar, .yst-mobile-navigation__dialog { + /* Initializing the variable here instead of on the list, as one of these should always exist. */ + --yst-menu-text-color: theme('colors.slate.800'); + } + + .yst-sidebar-navigation__list { + /* Due to the ring falling outside of the size, we need some spacing. */ + @apply yst-space-y-0.5; + + } + + .yst-sidebar-navigation__list--indented { + @apply yst-ml-8; + --yst-menu-text-color: theme('colors.slate.600'); + } + + .yst-sidebar-navigation__item--active { + &.yst-sidebar-navigation__link, &.yst-sidebar-navigation__collapsible-button { + @apply yst-text-slate-900 yst-bg-slate-200 !important; + } + + .yst-sidebar-navigation__icon { + @apply yst-text-slate-500; + } + } + + .yst-sidebar-navigation__item { + @apply yst-list-none; + + /* Due to the ring falling outside of the size, we need some spacing. */ + @apply yst-space-y-0.5; + + &:first-child { + @apply yst-mt-0.5; + } + } + + .yst-sidebar-navigation__collapsible ~ .yst-sidebar-navigation__collapsible { + /* Due to the ring falling outside of the size, we need some spacing. This is the fallback for if not nested in a list. */ + @apply yst-mt-1; + } + + .yst-sidebar-navigation__collapsible-button { + @apply + yst-flex + yst-w-full + yst-justify-center + yst-items-center + yst-gap-x-3 + yst-py-2 + yst-px-3 + yst-no-underline + yst-cursor-pointer + yst-rounded-md + yst-text-sm + yst-font-medium + yst-text-[var(--yst-menu-text-color)] + + hover:yst-text-slate-900 + hover:yst-bg-slate-50 + + focus:yst-outline-none + focus:yst-ring-2 + focus:yst-ring-primary-500 + ; + } + + .yst-sidebar-navigation__link { + @apply + yst-flex + yst-py-2 + yst-px-3 + yst-items-center + yst-no-underline + yst-text-sm + yst-font-medium + yst-rounded-md + yst-text-[var(--yst-menu-text-color)] + + hover:yst-text-slate-900 + hover:yst-bg-slate-50 + + focus:yst-outline-none + focus:yst-ring-1 + focus:yst-ring-offset-1 + focus:yst-ring-offset-transparent + focus:yst-ring-primary-500; + } + + a.yst-sidebar-navigation__link { + /* Override anchor defaults that conflict. */ + @apply + focus:yst-text-[var(--yst-menu-text-color)] + focus:yst-rounded-md + visited:yst-text-[var(--yst-menu-text-color)] + visited:hover:yst-text-slate-900; + } + + .yst-sidebar-navigation__icon { + @apply yst-flex-shrink-0 yst-text-slate-400 group-hover:yst-text-slate-500; + } } -} \ No newline at end of file +} diff --git a/packages/ui-library/src/components/sidebar-navigation/submenu-item.js b/packages/ui-library/src/components/sidebar-navigation/submenu-item.js index 94ae0692b6c..6a349c255d1 100644 --- a/packages/ui-library/src/components/sidebar-navigation/submenu-item.js +++ b/packages/ui-library/src/components/sidebar-navigation/submenu-item.js @@ -1,7 +1,7 @@ -import classNames from "classnames"; import PropTypes from "prop-types"; -import React, { useCallback } from "react"; -import { useNavigationContext } from "./index"; +import React from "react"; +import { Item } from "./item"; +import { Link } from "./link"; /** * @param {JSX.node} label The label. @@ -10,29 +10,13 @@ import { useNavigationContext } from "./index"; * @param {Object} [props] Extra props. * @returns {JSX.Element} The submenu item element. */ -const SubmenuItem = ( { as: Component = "a", pathProp = "href", label, ...props } ) => { - const { activePath, setMobileMenuOpen } = useNavigationContext(); - - const handleClick = useCallback( () => setMobileMenuOpen( false ), [ setMobileMenuOpen ] ); - - return ( -
  • - - { label } - -
  • - ); -}; +const SubmenuItem = ( { as = "a", pathProp = "href", label, ...props } ) => ( + + + { label } + + +); SubmenuItem.propTypes = { as: PropTypes.elementType, diff --git a/packages/yoastseo/spec/appSpec.js b/packages/yoastseo/spec/appSpec.js index ea9f8f4aad8..8aa5cc91d7c 100644 --- a/packages/yoastseo/spec/appSpec.js +++ b/packages/yoastseo/spec/appSpec.js @@ -1,5 +1,6 @@ import MissingArgument from "../src/errors/missingArgument.js"; import App from "../src/app.js"; +import Factory from "../src/helpers/factory"; // Mock these function to prevent us from needing an actual DOM in the tests. App.prototype.showLoadingDialog = function() {}; @@ -8,7 +9,7 @@ App.prototype.removeLoadingDialog = function() {}; App.prototype.runAnalyzer = function() {}; // Makes lodash think this is a valid HTML element -var mockElement = []; +const mockElement = []; mockElement.nodeType = 1; global.document = {}; @@ -16,15 +17,15 @@ document.getElementById = function() { return mockElement; }; +const researcher = Factory.buildMockResearcher( {}, true, false ); + describe( "Creating an App", function() { it( "throws an error when no args are given", function() { - expect( App ).toThrowError( MissingArgument ); + expect( () => new App() ).toThrowError( MissingArgument ); } ); it( "throws on an empty args object", function() { - expect( function() { - new App( {} ); - } ).toThrowError( MissingArgument ); + expect( () => new App( {} ) ).toThrowError( MissingArgument ); } ); it( "throws on an invalid targets argument", function() { @@ -50,6 +51,17 @@ describe( "Creating an App", function() { } ).toThrowError( MissingArgument ); } ); + it( "throws on a missing researcher argument", function() { + expect( function() { + new App( { + targets: { + snippet: "snippetID", + output: "outputID", + }, + } ); + } ).toThrowError( MissingArgument ); + } ); + it( "should work without an output ID", function() { new App( { targets: { @@ -60,6 +72,7 @@ describe( "Creating an App", function() { return {}; }, }, + researcher: researcher, } ); } ); @@ -74,6 +87,7 @@ describe( "Creating an App", function() { return {}; }, }, + researcher: researcher, } ); } ); } ); diff --git a/packages/yoastseo/spec/fullTextTests/testTexts/en/englishPaperForPerformanceTest.js b/packages/yoastseo/spec/fullTextTests/testTexts/en/englishPaperForPerformanceTest.js index fdd6606787d..ba97a5f9981 100644 --- a/packages/yoastseo/spec/fullTextTests/testTexts/en/englishPaperForPerformanceTest.js +++ b/packages/yoastseo/spec/fullTextTests/testTexts/en/englishPaperForPerformanceTest.js @@ -127,7 +127,7 @@ const expectedResults = { textTransitionWords: { isApplicable: true, score: 3, - resultText: "
    Transition words: Only 5.5% of the sentences contain transition words, " + + resultText: "Transition words: Only 6.8% of the sentences contain transition words, " + "which is not enough. Use more of them.", }, passiveVoice: { diff --git a/packages/yoastseo/spec/languageProcessing/researches/findTransitionWordsSpec.js b/packages/yoastseo/spec/languageProcessing/researches/findTransitionWordsSpec.js index 02a74743d29..69234009b9f 100644 --- a/packages/yoastseo/spec/languageProcessing/researches/findTransitionWordsSpec.js +++ b/packages/yoastseo/spec/languageProcessing/researches/findTransitionWordsSpec.js @@ -4,6 +4,20 @@ import Paper from "../../../src/values/Paper.js"; import EnglishResearcher from "../../../src/languageProcessing/languages/en/Researcher"; import FrenchResearcher from "../../../src/languageProcessing/languages/fr/Researcher"; import JapaneseResearcher from "../../../src/languageProcessing/languages/ja/Researcher"; +import GermanResearcher from "../../../src/languageProcessing/languages/de/Researcher"; +import DutchResearcher from "../../../src/languageProcessing/languages/nl/Researcher"; +import SpanishResearcher from "../../../src/languageProcessing/languages/es/Researcher"; +import ItalianResearcher from "../../../src/languageProcessing/languages/it/Researcher"; +import PortugueseResearcher from "../../../src/languageProcessing/languages/pt/Researcher"; +import CatalanResearcher from "../../../src/languageProcessing/languages/ca/Researcher"; +import PolishResearcher from "../../../src/languageProcessing/languages/pl/Researcher"; +import HungarianResearcher from "../../../src/languageProcessing/languages/hu/Researcher"; +import SwedishResearcher from "../../../src/languageProcessing/languages/sv/Researcher"; +import IndonesianResearcher from "../../../src/languageProcessing/languages/id/Researcher"; +import TurkishResearcher from "../../../src/languageProcessing/languages/tr/Researcher"; +import RussianResearcher from "../../../src/languageProcessing/languages/ru/Researcher"; +import HebrewResearcher from "../../../src/languageProcessing/languages/he/Researcher"; +import ArabicResearcher from "../../../src/languageProcessing/languages/ar/Researcher"; // eslint-disable-next-line max-statements describe( "a test for finding transition words from a string", function() { @@ -41,7 +55,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 1 ); } ); - it( "returns 1 when a two-part transition word is found in a sentence (English)", function() { + it( "returns 1 when a two-part transition word is found in a sentence (English)", function() { // Transition word: either...or. mockPaper = new Paper( "I will either tell you a story, or read you a novel.", { locale: "en_US" } ); result = transitionWordsResearch( mockPaper, new EnglishResearcher( mockPaper ) ); @@ -49,7 +63,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 1 ); } ); - it( "returns 1 when a two-part transition word is found in a sentence, and no transition word in another sentence. (English)", function() { + it( "returns 1 when a two-part transition word is found in a sentence, and no transition word in another sentence. (English)", function() { // Transition word: either...or. mockPaper = new Paper( "I will either tell you a story, or read you a novel. Okay?", { locale: "en_US" } ); result = transitionWordsResearch( mockPaper, new EnglishResearcher( mockPaper ) ); @@ -57,7 +71,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 1 ); } ); - it( "returns 2 when a two-part transition word is found in a sentence, and a transition word in another sentence. (English)", function() { + it( "returns 2 when a two-part transition word is found in a sentence, and a transition word in another sentence. (English)", function() { // Transition words: either...or, unless. mockPaper = new Paper( "I will either tell you a story, or read you a novel. Unless it is about a boy.", { locale: "en_US" } ); result = transitionWordsResearch( mockPaper, new EnglishResearcher( mockPaper ) ); @@ -66,8 +80,8 @@ describe( "a test for finding transition words from a string", function() { } ); it( "returns 2 when a two-part transition word is found in two sentences. (English)", function() { - // Transition words: either...or, if...then. - mockPaper = new Paper( "I will either tell you a story, or read you a novel. If you want, then I will.", { locale: "en_US" } ); + // Transition words: either...or, both...and. + mockPaper = new Paper( "I will either tell you a story, or read you a novel. She was both furious and disappointed.", { locale: "en_US" } ); result = transitionWordsResearch( mockPaper, new EnglishResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 2 ); expect( result.transitionWordSentences ).toBe( 2 ); @@ -75,9 +89,9 @@ describe( "a test for finding transition words from a string", function() { it( "returns 2 when a two-part transition word is found in two sentences, " + "and an additional transition word is found in one of them. (English)", function() { - // Transition words: either...or, if ...then, as soon as. + // Transition words: either...or, both...and, as soon as. mockPaper = new Paper( "I will either tell you a story about a boy, or read you a novel. " + - "If you want, then I will start as soon as you're ready.", { locale: "en_US" } ); + "I can read it to both you and her as soon as you're ready.", { locale: "en_US" } ); result = transitionWordsResearch( mockPaper, new EnglishResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 2 ); expect( result.transitionWordSentences ).toBe( 2 ); @@ -159,7 +173,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 0 ); } ); - /*it( "returns 1 when a transition word is found in a sentence (German)", function() { + it( "returns 1 when a transition word is found in a sentence (German)", function() { // Transition word: zuerst. mockPaper = new Paper( "Zuerst werde ich versuchen zu verstehen, warum er so denkt.", { locale: "de_DE" } ); result = transitionWordsResearch( mockPaper, new GermanResearcher( mockPaper ) ); @@ -190,8 +204,8 @@ describe( "a test for finding transition words from a string", function() { expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 0 ); } ); -*/ - /* it( "returns 1 when a transition word is found in a sentence (French)", function() { + + it( "returns 1 when a transition word is found in a sentence (French)", function() { // Transition word: deuxièmement. mockPaper = new Paper( "Deuxièmement, il convient de reconnaître la complexité des tâches à entreprendre.", { locale: "fr_FR" } ); result = transitionWordsResearch( mockPaper, new FrenchResearcher( mockPaper ) ); @@ -205,7 +219,7 @@ describe( "a test for finding transition words from a string", function() { result = transitionWordsResearch( mockPaper, new FrenchResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 1 ); - } );*/ + } ); it( "returns 1 when a transition word with an apostrophe is found in a sentence (French)", function() { // Transition word: quoi qu’il en soit. @@ -215,7 +229,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 1 ); } ); - /*it( "returns 0 when no transition words are present in a sentence (French)", function() { + it( "returns 0 when no transition words are present in a sentence (French)", function() { mockPaper = new Paper( "Une, deux, trois.", { locale: "fr_FR" } ); result = transitionWordsResearch( mockPaper, new FrenchResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 1 ); @@ -353,8 +367,8 @@ describe( "a test for finding transition words from a string", function() { expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 1 ); } ); -*/ - /*it( "returns 1 when a transition word with a punt volat (·) is found in a sentence (Catalan)", function() { + + it( "returns 1 when a transition word with a punt volat (·) is found in a sentence (Catalan)", function() { // Transition word: per il·lustrar. mockPaper = new Paper( "Roma proposa un concurs de curtmetratges per il·lustrar com ha de ser la ciutat ideal", { locale: "ca_ES" } ); result = transitionWordsResearch( mockPaper, new CatalanResearcher( mockPaper ) ); @@ -442,8 +456,9 @@ describe( "a test for finding transition words from a string", function() { result = transitionWordsResearch( mockPaper, new HungarianResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 1 ); - } );*/ - /*it( "returns 1 when a three-part transition word is found in a sentence (Hungarian)", function() { + } ); + + it( "returns 1 when a three-part transition word is found in a sentence (Hungarian)", function() { // Transition word: nemcsak, hanem, is mockPaper = new Paper( "Nemcsak a csokoládét szeretem, hanem a süteményt is.", { locale: "hu_HU" } ); result = transitionWordsResearch( mockPaper, new HungarianResearcher( mockPaper ) ); @@ -487,7 +502,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 0 ); } ); - it( "returns 1 when a transition word is found in a sentence (Turkish)", function() { + it( "returns 1 when a single word transition word is found in a sentence (Turkish)", function() { // Transition word: ama. mockPaper = new Paper( "Ama durum bu olmayabilir.", { locale: "tr_TR" } ); result = transitionWordsResearch( mockPaper, new TurkishResearcher( mockPaper ) ); @@ -495,9 +510,17 @@ describe( "a test for finding transition words from a string", function() { expect( result.transitionWordSentences ).toBe( 1 ); } ); + it( "returns 1 when two multiple word transition words are found in a sentence (Turkish)", function() { + // Transition words: bir yandan, diğer yandan. + mockPaper = new Paper( "Bir yandan bunu hissediyor, diğer yandan içimden geçenlerin hepsinin boş birer hayal olduğunu düşünüyordum.", { locale: "tr_TR" } ); + result = transitionWordsResearch( mockPaper, new TurkishResearcher( mockPaper ) ); + expect( result.totalSentences ).toBe( 1 ); + expect( result.transitionWordSentences ).toBe( 1 ); + } ); + it( "returns 1 when a two-part transition word is found in a sentence (Turkish)", function() { - // Transition word: hem, hem de. - mockPaper = new Paper( "Hem şapka hem de ceket dolapta.", { locale: "tr_TR" } ); + // Transition word: olsun, olmasın. + mockPaper = new Paper( "Тaşıt koltuklarına takılmış olsun veya olmasın baş yastıkların onayı.", { locale: "tr_TR" } ); result = transitionWordsResearch( mockPaper, new TurkishResearcher( mockPaper ) ); expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 1 ); @@ -571,7 +594,7 @@ describe( "a test for finding transition words from a string", function() { expect( result.totalSentences ).toBe( 1 ); expect( result.transitionWordSentences ).toBe( 1 ); } ); -*/ + it( "returns 1 when a (multiple) transition word is found in a language that uses a custom" + " match transition word helper (Japanese)", function() { // Transition word: ゆえに (tokenized: [ "ゆえ", "に" ]) diff --git a/packages/yoastseo/spec/languageProcessing/values/ClauseSpec.js b/packages/yoastseo/spec/languageProcessing/values/ClauseSpec.js index 96487b4dc36..436e286a7f5 100644 --- a/packages/yoastseo/spec/languageProcessing/values/ClauseSpec.js +++ b/packages/yoastseo/spec/languageProcessing/values/ClauseSpec.js @@ -40,11 +40,5 @@ describe( "a test for serializing and parsing a Clause class instance", function isPassive: false, participles: [], } ); - expect( mockClause.parse( mockClause.serialize() ) ).toEqual( { - _clauseText: "The cat is loved.", - _auxiliaries: [ "is" ], - _isPassive: false, - _participles: [], - } ); } ); } ); diff --git a/packages/yoastseo/spec/languageProcessing/values/SentenceSpec.js b/packages/yoastseo/spec/languageProcessing/values/SentenceSpec.js index 80bf8ad6d62..20689cd8bf4 100644 --- a/packages/yoastseo/spec/languageProcessing/values/SentenceSpec.js +++ b/packages/yoastseo/spec/languageProcessing/values/SentenceSpec.js @@ -64,15 +64,5 @@ describe( "Creates a sentence object", function() { isPassive: true, sentenceText: "Cats are adored.", } ); - expect( sentence.parse( sentence.serialize() ) ).toEqual( { - _clauses: [ - { _auxiliaries: [ "are" ], - _clauseText: "Cats are adored", - _isPassive: true, - _participles: [], - } ], - _isPassive: true, - _sentenceText: "Cats are adored.", - } ); } ); } ); diff --git a/packages/yoastseo/spec/scoring/assessments/readability/SubheadingDistributionTooLongAssessmentSpec.js b/packages/yoastseo/spec/scoring/assessments/readability/SubheadingDistributionTooLongAssessmentSpec.js index aee869dcfd9..0139b6569f1 100644 --- a/packages/yoastseo/spec/scoring/assessments/readability/SubheadingDistributionTooLongAssessmentSpec.js +++ b/packages/yoastseo/spec/scoring/assessments/readability/SubheadingDistributionTooLongAssessmentSpec.js @@ -3,7 +3,7 @@ import SubheadingDistributionTooLong from "../../../../src/scoring/assessments/r import Paper from "../../../../src/values/Paper.js"; import Factory from "../../../../src/helpers/factory.js"; import Mark from "../../../../src/values/Mark.js"; -import CornerStoneContentAssessor from "../../../../src/scoring/assessors/cornerstone/contentAssessor.js"; +import CornerstoneContentAssessor from "../../../../src/scoring/assessors/cornerstone/contentAssessor.js"; import ProductCornerstoneContentAssessor from "../../../../src/scoring/assessors/productPages/cornerstone/contentAssessor.js"; import DefaultResearcher from "../../../../src/languageProcessing/languages/_default/Researcher.js"; import EnglishResearcher from "../../../../src/languageProcessing/languages/en/Researcher.js"; @@ -583,7 +583,7 @@ describe( "Language-specific configuration for specific types of content is used expect( assessment._config.farTooMany ).toEqual( japaneseConfig.defaultParameters.farTooMany ); } ); - let cornerStoneContentAssessor = new CornerStoneContentAssessor( englishResearcher ); + let cornerStoneContentAssessor = new CornerstoneContentAssessor( englishResearcher ); let productCornerstoneContentAssessor = new ProductCornerstoneContentAssessor( englishResearcher, mockOptions ); [ cornerStoneContentAssessor, productCornerstoneContentAssessor ].forEach( assessor => { @@ -617,7 +617,7 @@ describe( "Language-specific configuration for specific types of content is used } ); } ); - cornerStoneContentAssessor = new CornerStoneContentAssessor( japaneseResearcher ); + cornerStoneContentAssessor = new CornerstoneContentAssessor( japaneseResearcher ); productCornerstoneContentAssessor = new ProductCornerstoneContentAssessor( japaneseResearcher, mockOptions ); [ cornerStoneContentAssessor, productCornerstoneContentAssessor ].forEach( assessor => { diff --git a/packages/yoastseo/spec/scoring/assessments/seo/UrlKeywordAssessmentSpec.js b/packages/yoastseo/spec/scoring/assessments/seo/UrlKeywordAssessmentSpec.js index 5503e2fa84a..7337b899194 100644 --- a/packages/yoastseo/spec/scoring/assessments/seo/UrlKeywordAssessmentSpec.js +++ b/packages/yoastseo/spec/scoring/assessments/seo/UrlKeywordAssessmentSpec.js @@ -93,6 +93,10 @@ describe( "A keyword in slug count assessment", function() { } ); describe( "tests for the assessment applicability.", function() { + afterEach( () => { + window.wpseoScriptData = { }; + } ); + it( "returns false when there is no keyword and slug found.", function() { const paper = new Paper( "sample keyword" ); const researcher = new DefaultResearcher( paper ); @@ -122,6 +126,17 @@ describe( "tests for the assessment applicability.", function() { expect( keywordCountInSlug.isApplicable( paper, researcher ) ).toBe( false ); } ); + it( "returns false when the page is the front page.", function() { + const paper = new Paper( "sample keyword", { + slug: "sample-with-keyword", + keyword: "keyword", + isFrontPage: true, + } ); + + const researcher = new DefaultResearcher( paper ); + expect( keywordCountInSlug.isApplicable( paper, researcher ) ).toBe( false ); + } ); + it( "returns true when the researcher has the keywordCountInSlug research.", function() { const paper = new Paper( "sample keyword", { slug: "sample-with-keyword", @@ -130,7 +145,6 @@ describe( "tests for the assessment applicability.", function() { // The default researcher has the keywordCountInSlug research. const researcher = new DefaultResearcher( paper ); - expect( keywordCountInSlug.isApplicable( paper, researcher ) ).toBe( true ); } ); } ); diff --git a/packages/yoastseo/spec/scoring/assessors/collectionPages/fullTextTests/testTexts/en/englishPaper1.js b/packages/yoastseo/spec/scoring/assessors/collectionPages/fullTextTests/testTexts/en/englishPaper1.js index 4a4046106d3..6d2ad1c18ac 100644 --- a/packages/yoastseo/spec/scoring/assessors/collectionPages/fullTextTests/testTexts/en/englishPaper1.js +++ b/packages/yoastseo/spec/scoring/assessors/collectionPages/fullTextTests/testTexts/en/englishPaper1.js @@ -99,9 +99,8 @@ const expectedResults = { }, textTransitionWords: { isApplicable: true, - score: 6, - resultText: "Transition words: Only 25% of the sentences contain " + - "transition words, which is not enough. Use more of them.", + score: 9, + resultText: "Transition words: Well done!", }, passiveVoice: { isApplicable: true, diff --git a/packages/yoastseo/spec/scoring/assessors/storePostsAndPages/cornerstone/contentAssessorSpec.js b/packages/yoastseo/spec/scoring/assessors/storePostsAndPages/cornerstone/contentAssessorSpec.js index 71a089a1937..16d708b86cc 100644 --- a/packages/yoastseo/spec/scoring/assessors/storePostsAndPages/cornerstone/contentAssessorSpec.js +++ b/packages/yoastseo/spec/scoring/assessors/storePostsAndPages/cornerstone/contentAssessorSpec.js @@ -185,13 +185,13 @@ describe( "A test for content assessor for English", function() { expect( assessments.length ).toBe( expected ); expect( assessments.map( ( { identifier } ) => identifier ) ).toEqual( [ - "subheadingsTooLong", "textParagraphTooLong", - "textSentenceLength", "textTransitionWords", "passiveVoice", "textPresence", "sentenceBeginnings", + "subheadingsTooLong", + "textSentenceLength", "wordComplexity", ] ); @@ -323,10 +323,10 @@ describe( "calculateOverallScore for non-English that uses Default researcher", expect( assessments.length ).toBe( expected ); expect( assessments.map( ( { identifier } ) => identifier ) ).toEqual( [ - "subheadingsTooLong", "textParagraphTooLong", - "textSentenceLength", "textPresence", + "subheadingsTooLong", + "textSentenceLength", ] ); } ); diff --git a/packages/yoastseo/spec/worker/transporter/serializeSpec.js b/packages/yoastseo/spec/worker/transporter/serializeSpec.js index f1895b971b4..7c5711f8eb5 100644 --- a/packages/yoastseo/spec/worker/transporter/serializeSpec.js +++ b/packages/yoastseo/spec/worker/transporter/serializeSpec.js @@ -102,6 +102,7 @@ describe( "serialize", () => { textTitle: "The title of the text", writingDirection: "LTR", wpBlocks: [], + isFrontPage: false, } ); } ); diff --git a/packages/yoastseo/src/app.js b/packages/yoastseo/src/app.js index 8555025751f..81aedbbf1d5 100644 --- a/packages/yoastseo/src/app.js +++ b/packages/yoastseo/src/app.js @@ -14,14 +14,14 @@ import CornerstoneContentAssessor from "./scoring/assessors/cornerstone/contentA import CornerstoneSEOAssessor from "./scoring/assessors/cornerstone/seoAssessor.js"; import SEOAssessor from "./scoring/assessors/seoAssessor.js"; -var inputDebounceDelay = 800; +const inputDebounceDelay = 800; /** * Default config for YoastSEO.js * * @type {Object} */ -var defaults = { +const defaults = { callbacks: { bindElementEvents: noop, updateSnippetValues: noop, @@ -79,7 +79,7 @@ var defaults = { * Check arguments passed to the App to check if all necessary arguments are set. * * @private - * @param {Object} args The arguments object passed to the App. + * @param {Object} args The arguments object passed to the App. * @returns {void} */ function verifyArguments( args ) { @@ -93,15 +93,15 @@ function verifyArguments( args ) { } /** - * This should return an object with the given properties + * This should return an object with the given properties. * * @callback YoastSEO.App~getData - * @returns {Object} data - * @returns {String} data.keyword The keyword that should be used - * @returns {String} data.meta - * @returns {String} data.text The text to analyze - * @returns {String} data.metaTitle The text in the HTML title tag - * @returns {String} data.title The title to analyze + * @returns {Object} data. The data object containing the following properties: keyword, meta, text, metaTitle, title, url, excerpt. + * @returns {String} data.keyword The keyword that should be used. + * @returns {String} data.meta The meta description to analyze. + * @returns {String} data.text The text to analyze. + * @returns {String} data.metaTitle The text in the HTML title tag. + * @returns {String} data.title The title to analyze. * @returns {String} data.url The URL for the given page * @returns {String} data.excerpt Excerpt for the pages */ @@ -109,7 +109,7 @@ function verifyArguments( args ) { /** * @callback YoastSEO.App~getAnalyzerInput * - * @returns {Array} An array containing the analyzer queue + * @returns {Array} An array containing the analyzer queue. */ /** @@ -121,7 +121,7 @@ function verifyArguments( args ) { /** * @callback YoastSEO.App~updateSnippetValues * - * @param {Object} ev The event emitted from the DOM + * @param {Object} ev The event emitted from the DOM. */ /** @@ -159,664 +159,669 @@ function verifyArguments( args ) { */ /** - * Loader for the analyzer, loads the eventbinder and the elementdefiner - * - * @param {Object} args The arguments passed to the loader. - * @param {Object} args.translations Jed compatible translations. - * @param {Object} args.targets Targets to retrieve or set on. - * @param {String} args.targets.snippet ID for the snippet preview element. - * @param {String} args.targets.output ID for the element to put the output of the analyzer in. - * @param {int} args.typeDelay Number of milliseconds to wait between typing to refresh the analyzer output. - * @param {boolean} args.dynamicDelay Whether to enable dynamic delay, will ignore type delay if the analyzer takes a long time. - * Applicable on slow devices. - * @param {int} args.maxTypeDelay The maximum amount of type delay even if dynamic delay is on. - * @param {int} args.typeDelayStep The amount with which to increase the typeDelay on each step when dynamic delay is enabled. - * @param {Object} args.callbacks The callbacks that the app requires. - * @param {Object} args.assessor The Assessor to use instead of the default assessor. - * @param {YoastSEO.App~getData} args.callbacks.getData Called to retrieve input data - * @param {YoastSEO.App~getAnalyzerInput} args.callbacks.getAnalyzerInput Called to retrieve input for the analyzer. - * @param {YoastSEO.App~bindElementEvents} args.callbacks.bindElementEvents Called to bind events to the DOM elements. - * @param {YoastSEO.App~updateSnippetValues} args.callbacks.updateSnippetValues Called when the snippet values need to be updated. - * @param {YoastSEO.App~saveScores} args.callbacks.saveScores Called when the score has been determined by the analyzer. - * @param {YoastSEO.App~saveContentScore} args.callback.saveContentScore Called when the content score has been - * determined by the assessor. - * @param {YoastSEO.App~updatedContentResults} args.callbacks.updatedContentResults Called when the score has been determined - * by the analyzer. - * @param {YoastSEO.App~updatedKeywordsResults} args.callback.updatedKeywordsResults Called when the content score has been - * determined by the assessor. - * @param {Function} args.callbacks.saveSnippetData Function called when the snippet data is changed. - * @param {Function} args.marker The marker to use to apply the list of marks retrieved from an assessment. - * - * @param {boolean} [args.debouncedRefresh] Whether or not to debounce the - * refresh function. Defaults to true. - * @param {Researcher} args.researcher The Researcher object to be used. - * - * @constructor - */ -var App = function( args ) { - if ( ! isObject( args ) ) { - args = {}; - } - - defaultsDeep( args, defaults ); + * Represents the main YoastSEO App. + */ +class App { + /** + * Loader for the analyzer, loads the eventbinder and the elementdefiner + * + * @param {Object} args The arguments passed to the loader. + * @param {Object} args.translations Jed compatible translations. + * @param {Object} args.targets Targets to retrieve or set on. + * @param {String} args.targets.snippet ID for the snippet preview element. + * @param {String} args.targets.output ID for the element to put the output of the analyzer in. + * @param {int} args.typeDelay Number of milliseconds to wait between typing to refresh the analyzer output. + * @param {boolean} args.dynamicDelay Whether to enable dynamic delay, will ignore type delay if the analyzer takes a long time. Applicable on slow devices. + * @param {int} args.maxTypeDelay The maximum amount of type delay even if dynamic delay is on. + * @param {int} args.typeDelayStep The amount with which to increase the typeDelay on each step when dynamic delay is enabled. + * @param {Object} args.callbacks The callbacks that the app requires. + * @param {Object} args.assessor The Assessor to use instead of the default assessor. + * @param {YoastSEO.App~getData} args.callbacks.getData Called to retrieve input data + * @param {YoastSEO.App~getAnalyzerInput} args.callbacks.getAnalyzerInput Called to retrieve input for the analyzer. + * @param {YoastSEO.App~bindElementEvents} args.callbacks.bindElementEvents Called to bind events to the DOM elements. + * @param {YoastSEO.App~updateSnippetValues} args.callbacks.updateSnippetValues Called when the snippet values need to be updated. + * @param {YoastSEO.App~saveScores} args.callbacks.saveScores Called when the score has been determined by the analyzer. + * @param {YoastSEO.App~saveContentScore} args.callback.saveContentScore Called when the content score has been determined by the assessor. + * @param {YoastSEO.App~updatedContentResults} args.callbacks.updatedContentResults Called when the score has been determined by the analyzer. + * @param {YoastSEO.App~updatedKeywordsResults} args.callback.updatedKeywordsResults Called when the content score has been determined by the assessor. + * @param {Function} args.callbacks.saveSnippetData Function called when the snippet data is changed. + * @param {Function} args.marker The marker to use to apply the list of marks retrieved from an assessment. + * + * @param {boolean} [args.debouncedRefresh=true] Whether or not to debounce the refresh function. Defaults to true. + * @param {Researcher} args.researcher The Researcher object to be used. + * + * @constructor + */ + constructor( args ) { + if ( ! isObject( args ) ) { + args = {}; + } - verifyArguments( args ); + defaultsDeep( args, defaults ); - this.config = args; + verifyArguments( args ); - if ( args.debouncedRefresh === true ) { - this.refresh = debounce( this.refresh.bind( this ), inputDebounceDelay ); - } - this._pureRefresh = throttle( this._pureRefresh.bind( this ), this.config.typeDelay ); + this.config = args; - this.callbacks = this.config.callbacks; + if ( args.debouncedRefresh === true ) { + this.refresh = debounce( this.refresh.bind( this ), inputDebounceDelay ); + } + this._pureRefresh = throttle( this._pureRefresh.bind( this ), this.config.typeDelay ); - setLocaleData( this.config.translations.locale_data[ "wordpress-seo" ], "wordpress-seo" ); + this.callbacks = this.config.callbacks; - this.initializeAssessors( args ); + this.researcher = this.config.researcher; - this.pluggable = new Pluggable( this ); + setLocaleData( this.config.translations.locale_data[ "wordpress-seo" ], "wordpress-seo" ); - this.getData(); + this.initializeAssessors( args ); - this.defaultOutputElement = this.getDefaultOutputElement( args ); + this.pluggable = new Pluggable( this ); - if ( this.defaultOutputElement !== "" ) { - this.showLoadingDialog(); - } + this.getData(); - this._assessorOptions = { - useCornerStone: false, - }; + this.defaultOutputElement = this.getDefaultOutputElement( args ); - this.initAssessorPresenters(); -}; + if ( this.defaultOutputElement !== "" ) { + this.showLoadingDialog(); + } -/** - * Returns the default output element based on which analyses are active. - * - * @param {Object} args The arguments passed to the App. - * @returns {string} The ID of the target that is active. - */ -App.prototype.getDefaultOutputElement = function( args ) { - if ( args.keywordAnalysisActive ) { - return args.targets.output; - } + this._assessorOptions = { + useCornerStone: false, + }; - if ( args.contentAnalysisActive ) { - return args.targets.contentOutput; + this.initAssessorPresenters(); } - return ""; -}; - -/** - * Sets the assessors based on the assessor options and refreshes them. - * - * @param {Object} assessorOptions The specific options. - * @returns {void} - */ -App.prototype.changeAssessorOptions = function( assessorOptions ) { - this._assessorOptions = merge( this._assessorOptions, assessorOptions ); - - // Set the assessors based on the new assessor options. - this.seoAssessor = this.getSeoAssessor(); - this.contentAssessor = this.getContentAssessor(); - - // Refresh everything so the user sees the changes. - this.initAssessorPresenters(); - this.refresh(); -}; + /** + * Returns the default output element based on which analyses are active. + * + * @param {Object} args The arguments passed to the App. + * @returns {string} The ID of the target that is active. + */ + getDefaultOutputElement( args ) { + if ( args.keywordAnalysisActive ) { + return args.targets.output; + } -/** - * Returns an instance of the seo assessor to use. - * - * @returns {Assessor} The assessor instance. - */ -App.prototype.getSeoAssessor = function() { - const { useCornerStone } = this._assessorOptions; - return useCornerStone ? this.cornerStoneSeoAssessor : this.defaultSeoAssessor; -}; + if ( args.contentAnalysisActive ) { + return args.targets.contentOutput; + } -/** - * Returns an instance of the content assessor to use. - * - * @returns {Assessor} The assessor instance. - */ -App.prototype.getContentAssessor = function() { - const { useCornerStone } = this._assessorOptions; - return useCornerStone ? this.cornerStoneContentAssessor : this.defaultContentAssessor; -}; + return ""; + } + + /** + * Sets the assessors based on the assessor options and refreshes them. + * + * @param {Object} assessorOptions The specific options. + * @returns {void} + */ + changeAssessorOptions( assessorOptions ) { + this._assessorOptions = merge( this._assessorOptions, assessorOptions ); + + // Set the assessors based on the new assessor options. + this.seoAssessor = this.getSeoAssessor(); + this.contentAssessor = this.getContentAssessor(); + + // Refresh everything so the user sees the changes. + this.initAssessorPresenters(); + this.refresh(); + } + + /** + * Returns an instance of the seo assessor to use. + * + * @returns {Assessor} The assessor instance. + */ + getSeoAssessor() { + const { useCornerStone } = this._assessorOptions; + return useCornerStone ? this.cornerStoneSeoAssessor : this.defaultSeoAssessor; + } + + /** + * Returns an instance of the content assessor to use. + * + * @returns {Assessor} The assessor instance. + */ + getContentAssessor() { + const { useCornerStone } = this._assessorOptions; + return useCornerStone ? this.cornerStoneContentAssessor : this.defaultContentAssessor; + } + + /** + * Initializes assessors based on whether the respective analysis is active. + * + * @param {Object} args The arguments passed to the App. + * @returns {void} + */ + initializeAssessors( args ) { + this.initializeSEOAssessor( args ); + this.initializeContentAssessor( args ); + } + + /** + * Initializes the SEO assessor. + * + * @param {Object} args The arguments passed to the App. + * @returns {void} + */ + initializeSEOAssessor( args ) { + if ( ! args.keywordAnalysisActive ) { + return; + } -/** - * Initializes assessors based on if the respective analysis is active. - * - * @param {Object} args The arguments passed to the App. - * @returns {void} - */ -App.prototype.initializeAssessors = function( args ) { - this.initializeSEOAssessor( args ); - this.initializeContentAssessor( args ); -}; + this.defaultSeoAssessor = new SEOAssessor( this.researcher, { marker: this.config.marker } ); + this.cornerStoneSeoAssessor = new CornerstoneSEOAssessor( this.researcher, { marker: this.config.marker } ); -/** - * Initializes the SEO assessor. - * - * @param {Object} args The arguments passed to the App. - * @returns {void} - */ -App.prototype.initializeSEOAssessor = function( args ) { - if ( ! args.keywordAnalysisActive ) { - return; + // Set the assessor + if ( isUndefined( args.seoAssessor ) ) { + this.seoAssessor = this.defaultSeoAssessor; + } else { + this.seoAssessor = args.seoAssessor; + } } - this.defaultSeoAssessor = new SEOAssessor( { marker: this.config.marker } ); - this.cornerStoneSeoAssessor = new CornerstoneSEOAssessor( { marker: this.config.marker } ); + /** + * Initializes the content assessor. + * + * @param {Object} args The arguments passed to the App. + * @returns {void} + */ + initializeContentAssessor( args ) { + if ( ! args.contentAnalysisActive ) { + return; + } - // Set the assessor - if ( isUndefined( args.seoAssessor ) ) { - this.seoAssessor = this.defaultSeoAssessor; - } else { - this.seoAssessor = args.seoAssessor; - } -}; + this.defaultContentAssessor = new ContentAssessor( this.researcher, { marker: this.config.marker, locale: this.config.locale } ); + this.cornerStoneContentAssessor = new CornerstoneContentAssessor( this.researcher, + { marker: this.config.marker, locale: this.config.locale } + ); -/** - * Initializes the content assessor. - * - * @param {Object} args The arguments passed to the App. - * @returns {void} - */ -App.prototype.initializeContentAssessor = function( args ) { - if ( ! args.contentAnalysisActive ) { - return; + // Set the content assessor + if ( isUndefined( args._contentAssessor ) ) { + this.contentAssessor = this.defaultContentAssessor; + } else { + this.contentAssessor = args._contentAssessor; + } } - this.defaultContentAssessor = new ContentAssessor( { marker: this.config.marker, locale: this.config.locale } ); - this.cornerStoneContentAssessor = new CornerstoneContentAssessor( { marker: this.config.marker, locale: this.config.locale } ); + /** + * Extends the config with defaults. + * + * @param {Object} args The arguments to be extended. + * + * @returns {Object} The extended arguments. + */ + extendConfig( args ) { + args.sampleText = this.extendSampleText( args.sampleText ); + args.locale = args.locale || "en_US"; - // Set the content assessor - if ( isUndefined( args._contentAssessor ) ) { - this.contentAssessor = this.defaultContentAssessor; - } else { - this.contentAssessor = args._contentAssessor; + return args; } -}; -/** - * Extend the config with defaults. - * - * @param {Object} args The arguments to be extended. - * @returns {Object} args The extended arguments. - */ -App.prototype.extendConfig = function( args ) { - args.sampleText = this.extendSampleText( args.sampleText ); - args.locale = args.locale || "en_US"; + /** + * Extends sample text config with defaults. + * + * @param {Object} sampleText The sample text to be extended. + * @returns {Object} The extended sample text. + */ + extendSampleText( sampleText ) { + const defaultSampleText = defaults.sampleText; - return args; -}; + if ( isUndefined( sampleText ) ) { + return defaultSampleText; + } -/** - * Extend sample text config with defaults. - * - * @param {Object} sampleText The sample text to be extended. - * @returns {Object} sampleText The extended sample text. - */ -App.prototype.extendSampleText = function( sampleText ) { - var defaultSampleText = defaults.sampleText; + for ( const key in sampleText ) { + if ( isUndefined( sampleText[ key ] ) ) { + sampleText[ key ] = defaultSampleText[ key ]; + } + } - if ( isUndefined( sampleText ) ) { - return defaultSampleText; + return sampleText; } - for ( var key in sampleText ) { - if ( isUndefined( sampleText[ key ] ) ) { - sampleText[ key ] = defaultSampleText[ key ]; + /** + * Registers a custom data callback. + * + * @param {Function} callback The callback to register. + * + * @returns {void} + */ + registerCustomDataCallback( callback ) { + if ( ! this.callbacks.custom ) { + this.callbacks.custom = []; } - } - - return sampleText; -}; -/** - * Registers a custom data callback. - * - * @param {Function} callback The callback to register. - * - * @returns {void} - */ -App.prototype.registerCustomDataCallback = function( callback ) { - if ( ! this.callbacks.custom ) { - this.callbacks.custom = []; + if ( isFunction( callback ) ) { + this.callbacks.custom.push( callback ); + } } - if ( isFunction( callback ) ) { - this.callbacks.custom.push( callback ); - } -}; + /** + * Retrieves data from the callbacks.getData and applies modification to store these in this.rawData. + * + * @returns {void} + */ + getData() { + this.rawData = this.callbacks.getData(); -/** - * Retrieves data from the callbacks.getData and applies modification to store these in this.rawData. - * - * @returns {void} - */ -App.prototype.getData = function() { - this.rawData = this.callbacks.getData(); + // Add the custom data to the raw data. + if ( isArray( this.callbacks.custom ) ) { + this.callbacks.custom.forEach( ( customCallback ) => { + const customData = customCallback(); - // Add the custom data to the raw data. - if ( isArray( this.callbacks.custom ) ) { - this.callbacks.custom.forEach( ( customCallback ) => { - const customData = customCallback(); - - this.rawData = merge( this.rawData, customData ); - } ); - } - - if ( this.pluggable.loaded ) { - this.rawData.metaTitle = this.pluggable._applyModifications( "data_page_title", this.rawData.metaTitle ); - this.rawData.meta = this.pluggable._applyModifications( "data_meta_desc", this.rawData.meta ); - } + this.rawData = merge( this.rawData, customData ); + } ); + } - this.rawData.titleWidth = measureTextWidth( this.rawData.metaTitle ); + if ( this.pluggable.loaded ) { + this.rawData.metaTitle = this.pluggable._applyModifications( "data_page_title", this.rawData.metaTitle ); + this.rawData.meta = this.pluggable._applyModifications( "data_meta_desc", this.rawData.meta ); + } - this.rawData.locale = this.config.locale; -}; + this.rawData.titleWidth = measureTextWidth( this.rawData.metaTitle ); -/** - * Refreshes the analyzer and output of the analyzer, is debounced for a better experience. - * - * @returns {void} - */ -App.prototype.refresh = function() { - // Until all plugins are loaded, do not trigger a refresh. - if ( ! this.pluggable.loaded ) { - return; + this.rawData.locale = this.config.locale; } - this._pureRefresh(); -}; + /** + * Refreshes the analyzer and output of the analyzer, is debounced for a better experience. + * + * @returns {void} + */ + refresh() { + // Until all plugins are loaded, do not trigger a refresh. + if ( ! this.pluggable.loaded ) { + return; + } -/** - * Refreshes the analyzer and output of the analyzer, is throttled to prevent performance issues. - * - * @returns {void} - * - * @private - */ -App.prototype._pureRefresh = function() { - this.getData(); - this.runAnalyzer(); -}; + this._pureRefresh(); + } + + /** + * Refreshes the analyzer and output of the analyzer, is throttled to prevent performance issues. + * + * @returns {void} + * + * @private + */ + _pureRefresh() { + this.getData(); + this.runAnalyzer(); + } + + /** + * Initializes the assessor presenters for content and SEO analysis. + * + * @returns {void} + */ + initAssessorPresenters() { + // Pass the assessor result through to the formatter. + if ( ! isUndefined( this.config.targets.output ) ) { + this.seoAssessorPresenter = new AssessorPresenter( { + targets: { + output: this.config.targets.output, + }, + assessor: this.seoAssessor, + } ); + } -/** - * Initializes the assessor presenters for content and SEO. - * - * @returns {void} - */ -App.prototype.initAssessorPresenters = function() { - // Pass the assessor result through to the formatter - if ( ! isUndefined( this.config.targets.output ) ) { - this.seoAssessorPresenter = new AssessorPresenter( { - targets: { - output: this.config.targets.output, - }, - assessor: this.seoAssessor, - } ); + if ( ! isUndefined( this.config.targets.contentOutput ) ) { + // Pass the assessor result through to the formatter. + this.contentAssessorPresenter = new AssessorPresenter( { + targets: { + output: this.config.targets.contentOutput, + }, + assessor: this.contentAssessor, + } ); + } } - if ( ! isUndefined( this.config.targets.contentOutput ) ) { - // Pass the assessor result through to the formatter - this.contentAssessorPresenter = new AssessorPresenter( { - targets: { - output: this.config.targets.contentOutput, - }, - assessor: this.contentAssessor, - } ); + /** + * Sets the startTime timestamp. + * + * @returns {void} + */ + startTime() { + this.startTimestamp = new Date().getTime(); + } + + /** + * Sets the endTime timestamp and compares with startTime to determine typeDelayincrease. + * + * @returns {void} + */ + endTime() { + this.endTimestamp = new Date().getTime(); + if ( this.endTimestamp - this.startTimestamp > this.config.typeDelay ) { + if ( this.config.typeDelay < ( this.config.maxTypeDelay - this.config.typeDelayStep ) ) { + this.config.typeDelay += this.config.typeDelayStep; + } + } } -}; - -/** - * Sets the startTime timestamp. - * - * @returns {void} - */ -App.prototype.startTime = function() { - this.startTimestamp = new Date().getTime(); -}; -/** - * Sets the endTime timestamp and compares with startTime to determine typeDelayincrease. - * - * @returns {void} - */ -App.prototype.endTime = function() { - this.endTimestamp = new Date().getTime(); - if ( this.endTimestamp - this.startTimestamp > this.config.typeDelay ) { - if ( this.config.typeDelay < ( this.config.maxTypeDelay - this.config.typeDelayStep ) ) { - this.config.typeDelay += this.config.typeDelayStep; + /** + * Inits a new pageAnalyzer with the inputs from the getInput function and calls the scoreFormatter to format outputs. + * + * @returns {void} + */ + runAnalyzer() { + if ( this.pluggable.loaded === false ) { + return; } - } -}; -/** - * Inits a new pageAnalyzer with the inputs from the getInput function and calls the scoreFormatter - * to format outputs. - * - * @returns {void} - */ -App.prototype.runAnalyzer = function() { - if ( this.pluggable.loaded === false ) { - return; - } + if ( this.config.dynamicDelay ) { + this.startTime(); + } - if ( this.config.dynamicDelay ) { - this.startTime(); - } + this.analyzerData = this.modifyData( this.rawData ); - this.analyzerData = this.modifyData( this.rawData ); - let text = this.analyzerData.text; + let text = this.analyzerData.text; - // Insert HTML stripping code - text = removeHtmlBlocks( text ); + // Insert HTML stripping code. + text = removeHtmlBlocks( text ); - const titleWidth = this.analyzerData.titleWidth; + const titleWidth = this.analyzerData.titleWidth; - // Create a paper object for the Researcher - this.paper = new Paper( text, { - keyword: this.analyzerData.keyword, - synonyms: this.analyzerData.synonyms, - description: this.analyzerData.meta, - slug: this.analyzerData.slug, - title: this.analyzerData.metaTitle, - titleWidth: titleWidth, - locale: this.config.locale, - permalink: this.analyzerData.permalink, - } ); + // Create a paper object for the Researcher. + this.paper = new Paper( text, { + keyword: this.analyzerData.keyword, + synonyms: this.analyzerData.synonyms, + description: this.analyzerData.meta, + slug: this.analyzerData.slug, + title: this.analyzerData.metaTitle, + titleWidth: titleWidth, + locale: this.config.locale, + permalink: this.analyzerData.permalink, + } ); - this.config.researcher.setPaper( this.paper ); + this.config.researcher.setPaper( this.paper ); - this.runKeywordAnalysis(); + this.runKeywordAnalysis(); - this.runContentAnalysis(); + this.runContentAnalysis(); - this._renderAnalysisResults(); + this._renderAnalysisResults(); - if ( this.config.dynamicDelay ) { - this.endTime(); + if ( this.config.dynamicDelay ) { + this.endTime(); + } } -}; -/** - * Runs the keyword analysis and calls the appropriate callbacks. - * - * @returns {void} - */ -App.prototype.runKeywordAnalysis = function() { - if ( this.config.keywordAnalysisActive ) { - this.seoAssessor.assess( this.paper ); - const overallSeoScore = this.seoAssessor.calculateOverallScore(); + /** + * Runs the keyword analysis and calls the appropriate callbacks. + * + * @returns {void} + */ + runKeywordAnalysis() { + if ( this.config.keywordAnalysisActive ) { + this.seoAssessor.assess( this.paper ); + const overallSeoScore = this.seoAssessor.calculateOverallScore(); - if ( ! isUndefined( this.callbacks.updatedKeywordsResults ) ) { - this.callbacks.updatedKeywordsResults( this.seoAssessor.results, overallSeoScore ); - } + if ( ! isUndefined( this.callbacks.updatedKeywordsResults ) ) { + this.callbacks.updatedKeywordsResults( this.seoAssessor.results, overallSeoScore ); + } - if ( ! isUndefined( this.callbacks.saveScores ) ) { - this.callbacks.saveScores( overallSeoScore, this.seoAssessorPresenter ); + if ( ! isUndefined( this.callbacks.saveScores ) ) { + this.callbacks.saveScores( overallSeoScore, this.seoAssessorPresenter ); + } } } -}; -/** - * Runs the content analysis and calls the appropriate callbacks. - * - * @returns {void} - */ -App.prototype.runContentAnalysis = function() { - if ( this.config.contentAnalysisActive ) { - this.contentAssessor.assess( this.paper ); - const overallContentScore = this.contentAssessor.calculateOverallScore(); + /** + * Runs the content analysis and calls the appropriate callbacks. + * + * @returns {void} + */ + runContentAnalysis() { + if ( this.config.contentAnalysisActive ) { + this.contentAssessor.assess( this.paper ); + const overallContentScore = this.contentAssessor.calculateOverallScore(); - if ( ! isUndefined( this.callbacks.updatedContentResults ) ) { - this.callbacks.updatedContentResults( this.contentAssessor.results, overallContentScore ); - } + if ( ! isUndefined( this.callbacks.updatedContentResults ) ) { + this.callbacks.updatedContentResults( this.contentAssessor.results, overallContentScore ); + } - if ( ! isUndefined( this.callbacks.saveContentScore ) ) { - this.callbacks.saveContentScore( overallContentScore, this.contentAssessorPresenter ); + if ( ! isUndefined( this.callbacks.saveContentScore ) ) { + this.callbacks.saveContentScore( overallContentScore, this.contentAssessorPresenter ); + } } } -}; -/** - * Modifies the data with plugins before it is sent to the analyzer. - * - * @param {Object} data The data to be modified. - * @returns {Object} The data with the applied modifications. - */ -App.prototype.modifyData = function( data ) { - // Copy rawdata to lose object reference. - data = JSON.parse( JSON.stringify( data ) ); + /** + * Modifies the data with plugins before it is sent to the analyzer. + * + * @param {Object} data The data to be modified. + * @returns {Object} The data with the applied modifications. + */ + modifyData( data ) { + // Copy rawdata to lose object reference. + data = JSON.parse( JSON.stringify( data ) ); + + data.text = this.pluggable._applyModifications( "content", data.text ); + data.metaTitle = this.pluggable._applyModifications( "title", data.metaTitle ); + + return data; + } + + /** + * Removes the loading dialog and fires the analyzer when all plugins are loaded. + * + * @returns {void} + */ + pluginsLoaded() { + this.removeLoadingDialog(); + this.refresh(); + } + + /** + * Shows the loading dialog which shows the loading of the plugins. + * + * @returns {void} + */ + showLoadingDialog() { + const outputElement = document.getElementById( this.defaultOutputElement ); + + if ( this.defaultOutputElement !== "" && ! isEmpty( outputElement ) ) { + const dialogDiv = document.createElement( "div" ); + dialogDiv.className = "YoastSEO_msg"; + dialogDiv.id = "YoastSEO-plugin-loading"; + document.getElementById( this.defaultOutputElement ).appendChild( dialogDiv ); + } + } - data.text = this.pluggable._applyModifications( "content", data.text ); - data.metaTitle = this.pluggable._applyModifications( "title", data.metaTitle ); + /** + * Updates the loading plugins. Uses the plugins as arguments to show which plugins are loading. + * + * @param {Object} plugins The plugins to be parsed into the dialog. + * @returns {void} + */ + updateLoadingDialog( plugins ) { + const outputElement = document.getElementById( this.defaultOutputElement ); - return data; -}; + if ( this.defaultOutputElement === "" || isEmpty( outputElement ) ) { + return; + } -/** - * Function to fire the analyzer when all plugins are loaded, removes the loading dialog. - * - * @returns {void} - */ -App.prototype.pluginsLoaded = function() { - this.removeLoadingDialog(); - this.refresh(); -}; + const dialog = document.getElementById( "YoastSEO-plugin-loading" ); + dialog.textContent = ""; -/** - * Shows the loading dialog which shows the loading of the plugins. - * - * @returns {void} - */ -App.prototype.showLoadingDialog = function() { - var outputElement = document.getElementById( this.defaultOutputElement ); + forEach( plugins, function( plugin, pluginName ) { + dialog.innerHTML += "" + pluginName + "" + plugin.status + "
    "; + } ); - if ( this.defaultOutputElement !== "" && ! isEmpty( outputElement ) ) { - var dialogDiv = document.createElement( "div" ); - dialogDiv.className = "YoastSEO_msg"; - dialogDiv.id = "YoastSEO-plugin-loading"; - document.getElementById( this.defaultOutputElement ).appendChild( dialogDiv ); + dialog.innerHTML += ""; } -}; -/** - * Updates the loading plugins. Uses the plugins as arguments to show which plugins are loading. - * - * @param {Object} plugins The plugins to be parsed into the dialog. - * @returns {void} - */ -App.prototype.updateLoadingDialog = function( plugins ) { - var outputElement = document.getElementById( this.defaultOutputElement ); + /** + * Removes the plugin load dialog. + * + * @returns {void} + */ + removeLoadingDialog() { + const outputElement = document.getElementById( this.defaultOutputElement ); + const loadingDialog = document.getElementById( "YoastSEO-plugin-loading" ); - if ( this.defaultOutputElement === "" || isEmpty( outputElement ) ) { - return; + if ( ( this.defaultOutputElement !== "" && ! isEmpty( outputElement ) ) && ! isEmpty( loadingDialog ) ) { + document.getElementById( this.defaultOutputElement ).removeChild( document.getElementById( "YoastSEO-plugin-loading" ) ); + } } - var dialog = document.getElementById( "YoastSEO-plugin-loading" ); - dialog.textContent = ""; - - forEach( plugins, function( plugin, pluginName ) { - dialog.innerHTML += "" + pluginName + "" + plugin.status + "
    "; - } ); - - dialog.innerHTML += ""; -}; - -/** - * Removes the pluging load dialog. - * - * @returns {void} - */ -App.prototype.removeLoadingDialog = function() { - var outputElement = document.getElementById( this.defaultOutputElement ); - var loadingDialog = document.getElementById( "YoastSEO-plugin-loading" ); - - if ( ( this.defaultOutputElement !== "" && ! isEmpty( outputElement ) ) && ! isEmpty( loadingDialog ) ) { - document.getElementById( this.defaultOutputElement ).removeChild( document.getElementById( "YoastSEO-plugin-loading" ) ); + // ***** PLUGGABLE PUBLIC DSL ***** // + /** + * Delegates to `YoastSEO.app.pluggable.registerPlugin` + * + * @param {string} pluginName The name of the plugin to be registered. + * @param {object} options The options object. + * @param {string} options.status The status of the plugin being registered. Can either be "loading" or "ready". + * @returns {boolean} Whether or not it was successfully registered. + */ + registerPlugin( pluginName, options ) { + return this.pluggable._registerPlugin( pluginName, options ); + } + + /** + * Delegates to `YoastSEO.app.pluggable.ready` + * + * @param {string} pluginName The name of the plugin to check. + * @returns {boolean} Whether or not the plugin is ready. + */ + pluginReady( pluginName ) { + return this.pluggable._ready( pluginName ); + } + + /** + * Delegates to `YoastSEO.app.pluggable.reloaded` + * + * @param {string} pluginName The name of the plugin to reload + * @returns {boolean} Whether or not the plugin was reloaded. + */ + pluginReloaded( pluginName ) { + return this.pluggable._reloaded( pluginName ); + } + + /** + * Delegates to `YoastSEO.app.pluggable.registerModification`. + * + * @param {string} modification The name of the filter. + * @param {function} callable The callable function. + * @param {string} pluginName The plugin that is registering the modification. + * @param {number} [priority] Used to specify the order in which the callables associated with a particular filter are called. + * Lower numbers correspond with earlier execution. + * + * @returns {boolean} Whether or not the modification was successfully registered. + */ + registerModification( modification, callable, pluginName, priority ) { + return this.pluggable._registerModification( modification, callable, pluginName, priority ); + } + + /** + * Registers a custom assessment for use in the analyzer, this will result in a new line in the analyzer results. + * The function needs to use the assessment result to return a result based on the contents of the page/posts. + * + * Score 0 results in a grey circle if it is not explicitly set by using setscore + * Scores 0, 1, 2, 3 and 4 result in a red circle + * Scores 6 and 7 result in a yellow circle + * Scores 8, 9 and 10 result in a green circle + * + * @param {string} name Name of the test. + * @param {function} assessment The assessment to run. + * @param {string} pluginName The plugin that is registering the test. + * @returns {boolean} Whether or not the test was successfully registered. + */ + registerAssessment( name, assessment, pluginName ) { + if ( ! isUndefined( this.seoAssessor ) ) { + return this.pluggable._registerAssessment( this.defaultSeoAssessor, name, assessment, pluginName ) && + this.pluggable._registerAssessment( this.cornerStoneSeoAssessor, name, assessment, pluginName ); + } } -}; - -// ***** PLUGGABLE PUBLIC DSL ***** // - -/** - * Delegates to `YoastSEO.app.pluggable.registerPlugin` - * - * @param {string} pluginName The name of the plugin to be registered. - * @param {object} options The options object. - * @param {string} options.status The status of the plugin being registered. Can either be "loading" or "ready". - * @returns {boolean} Whether or not it was successfully registered. - */ -App.prototype.registerPlugin = function( pluginName, options ) { - return this.pluggable._registerPlugin( pluginName, options ); -}; - -/** - * Delegates to `YoastSEO.app.pluggable.ready` - * - * @param {string} pluginName The name of the plugin to check. - * @returns {boolean} Whether or not the plugin is ready. - */ -App.prototype.pluginReady = function( pluginName ) { - return this.pluggable._ready( pluginName ); -}; - -/** - * Delegates to `YoastSEO.app.pluggable.reloaded` - * - * @param {string} pluginName The name of the plugin to reload - * @returns {boolean} Whether or not the plugin was reloaded. - */ -App.prototype.pluginReloaded = function( pluginName ) { - return this.pluggable._reloaded( pluginName ); -}; - -/** - * Delegates to `YoastSEO.app.pluggable.registerModification`. - * - * @param {string} modification The name of the filter - * @param {function} callable The callable function - * @param {string} pluginName The plugin that is registering the modification. - * @param {number} [priority] Used to specify the order in which the callables associated with a particular filter are called. - * Lower numbers correspond with earlier execution. - * - * @returns {boolean} Whether or not the modification was successfully registered. - */ -App.prototype.registerModification = function( modification, callable, pluginName, priority ) { - return this.pluggable._registerModification( modification, callable, pluginName, priority ); -}; -/** - * Registers a custom assessment for use in the analyzer, this will result in a new line in the analyzer results. - * The function needs to use the assessmentresult to return an result based on the contents of the page/posts. - * - * Score 0 results in a grey circle if it is not explicitly set by using setscore - * Scores 0, 1, 2, 3 and 4 result in a red circle - * Scores 6 and 7 result in a yellow circle - * Scores 8, 9 and 10 result in a green circle - * - * @param {string} name Name of the test. - * @param {function} assessment The assessment to run - * @param {string} pluginName The plugin that is registering the test. - * @returns {boolean} Whether or not the test was successfully registered. - */ -App.prototype.registerAssessment = function( name, assessment, pluginName ) { - if ( ! isUndefined( this.seoAssessor ) ) { - return this.pluggable._registerAssessment( this.defaultSeoAssessor, name, assessment, pluginName ) && - this.pluggable._registerAssessment( this.cornerStoneSeoAssessor, name, assessment, pluginName ); - } -}; + /** + * Disables markers visually in the UI. + * + * @returns {void} + */ + disableMarkers() { + if ( ! isUndefined( this.seoAssessorPresenter ) ) { + this.seoAssessorPresenter.disableMarker(); + } -/** - * Disables markers visually in the UI. - * - * @returns {void} - */ -App.prototype.disableMarkers = function() { - if ( ! isUndefined( this.seoAssessorPresenter ) ) { - this.seoAssessorPresenter.disableMarker(); + if ( ! isUndefined( this.contentAssessorPresenter ) ) { + this.contentAssessorPresenter.disableMarker(); + } } - if ( ! isUndefined( this.contentAssessorPresenter ) ) { - this.contentAssessorPresenter.disableMarker(); + /** + * Renders the content and keyword analysis results. + * + * @returns {void} + */ + _renderAnalysisResults() { + if ( this.config.contentAnalysisActive && ! isUndefined( this.contentAssessorPresenter ) ) { + this.contentAssessorPresenter.renderIndividualRatings(); + } + if ( this.config.keywordAnalysisActive && ! isUndefined( this.seoAssessorPresenter ) ) { + this.seoAssessorPresenter.setKeyword( this.paper.getKeyword() ); + this.seoAssessorPresenter.render(); + } } -}; -/** - * Renders the content and keyword analysis results. - * - * @returns {void} - */ -App.prototype._renderAnalysisResults = function() { - if ( this.config.contentAnalysisActive && ! isUndefined( this.contentAssessorPresenter ) ) { - this.contentAssessorPresenter.renderIndividualRatings(); - } - if ( this.config.keywordAnalysisActive && ! isUndefined( this.seoAssessorPresenter ) ) { - this.seoAssessorPresenter.setKeyword( this.paper.getKeyword() ); - this.seoAssessorPresenter.render(); + // Deprecated functions + /** + * The analyzeTimer calls the checkInputs function with a delay, so the function won't be executed + * at every keystroke checks the reference object, so this function can be called from anywhere, + * without problems with different scopes. + * + * @deprecated: 1.3 - Use this.refresh() instead. + * + * @returns {void} + */ + analyzeTimer() { + this.refresh(); + } + + /** + * Registers a custom test for use in the analyzer, this will result in a new line in the analyzer results. The function + * has to return a result based on the contents of the page/posts. + * + * The scoring object is a special object with definitions about how to translate a result from your analysis function + * to a SEO score. + * + * Negative scores result in a red circle + * Scores 1, 2, 3, 4 and 5 result in an orange circle + * Scores 6 and 7 result in a yellow circle + * Scores 8, 9 and 10 result in a red circle + * + * @returns {void} + * + * @deprecated since version 1.2 + */ + registerTest() { + console.error( "This function is deprecated, please use registerAssessment" ); + } + + /** + * Switches between the cornerstone and default assessors. + * + * @deprecated 1.35.0 - Use changeAssessorOption instead. + * + * @param {boolean} useCornerStone True when cornerstone should be used. + * + * @returns {void} + */ + switchAssessors( useCornerStone ) { + // eslint-disable-next-line no-console + console.warn( "Switch assessor is deprecated since YoastSEO.js version 1.35.0" ); + + this.changeAssessorOptions( { + useCornerStone, + } ); } -}; - -// Deprecated functions -/** - * The analyzeTimer calls the checkInputs function with a delay, so the function won't be executed - * at every keystroke checks the reference object, so this function can be called from anywhere, - * without problems with different scopes. - * - * @deprecated: 1.3 - Use this.refresh() instead. - * - * @returns {void} - */ -App.prototype.analyzeTimer = function() { - this.refresh(); -}; - -/** - * Registers a custom test for use in the analyzer, this will result in a new line in the analyzer results. The function - * has to return a result based on the contents of the page/posts. - * - * The scoring object is a special object with definitions about how to translate a result from your analysis function - * to a SEO score. - * - * Negative scores result in a red circle - * Scores 1, 2, 3, 4 and 5 result in a orange circle - * Scores 6 and 7 result in a yellow circle - * Scores 8, 9 and 10 result in a red circle - * - * @returns {void} - * - * @deprecated since version 1.2 - */ -App.prototype.registerTest = function() { - console.error( "This function is deprecated, please use registerAssessment" ); -}; - -/** - * Switches between the cornerstone and default assessors. - * - * @deprecated 1.35.0 - Use changeAssessorOption instead. - * - * @param {boolean} useCornerStone True when cornerstone should be used. - * - * @returns {void} - */ -App.prototype.switchAssessors = function( useCornerStone ) { - // eslint-disable-next-line no-console - console.warn( "Switch assessor is deprecated since YoastSEO.js version 1.35.0" ); +} - this.changeAssessorOptions( { - useCornerStone, - } ); -}; export default App; diff --git a/packages/yoastseo/src/bundledPlugins/previouslyUsedKeywords.js b/packages/yoastseo/src/bundledPlugins/previouslyUsedKeywords.js index d74a2e594d0..f0dfb3ca13d 100644 --- a/packages/yoastseo/src/bundledPlugins/previouslyUsedKeywords.js +++ b/packages/yoastseo/src/bundledPlugins/previouslyUsedKeywords.js @@ -2,186 +2,191 @@ import { __, sprintf } from "@wordpress/i18n"; import { isUndefined } from "lodash"; import MissingArgument from "../errors/missingArgument"; -import { createAnchorOpeningTag } from "../helpers/shortlinker"; +import { createAnchorOpeningTag } from "../helpers"; import AssessmentResult from "../values/AssessmentResult.js"; - /** - * @param {object} app The app - * @param {object} args An arguments object with usedKeywords, searchUrl, postUrl, - * @param {object} args.usedKeywords An object with keywords and ids where they are used. - * @param {object} args.usedKeywordsPostTypes An object with the post types of the post ids from usedKeywords. - * @param {string} args.searchUrl The url used to link to a search page when multiple usages of the keyword are found. - * @param {string} args.postUrl The url used to link to a post when 1 usage of the keyword is found. - * @constructor + * The PreviouslyUsedKeyword plugin allows to check for previously used keywords. */ -var PreviouslyUsedKeyword = function( app, args ) { - if ( isUndefined( app ) ) { - throw new MissingArgument( "The previously keyword plugin requires the YoastSEO app" ); - } +export default class PreviouslyUsedKeyword { + /** + * Constructs a new PreviouslyUsedKeyword plugin. + * + * @param {object} app The app. + * @param {object} args An arguments object. + * @param {object} args.usedKeywords An object with keywords and ids where they are used. + * @param {object} args.usedKeywordsPostTypes An object with the post types of the post ids from usedKeywords. + * @param {string} args.searchUrl The url used to link to a search page when multiple usages of the keyword are found. + * @param {string} args.postUrl The url used to link to a post when 1 usage of the keyword is found. + * + * @constructor + */ + constructor( app, args ) { + if ( isUndefined( app ) ) { + throw new MissingArgument( "The previously keyword plugin requires the YoastSEO app" ); + } - if ( isUndefined( args ) ) { - args = { - usedKeywords: {}, - usedKeywordsPostTypes: {}, - searchUrl: "", - postUrl: "", - }; - } + if ( isUndefined( args ) ) { + args = { + usedKeywords: {}, + usedKeywordsPostTypes: {}, + searchUrl: "", + postUrl: "", + }; + } - this.app = app; - this.usedKeywords = args.usedKeywords; - this.usedKeywordsPostTypes = args.usedKeywordsPostTypes; - this.searchUrl = args.searchUrl; - this.postUrl = args.postUrl; - this.urlTitle = createAnchorOpeningTag( "https://yoa.st/33x" ); - this.urlCallToAction = createAnchorOpeningTag( "https://yoa.st/33y" ); -}; + this.app = app; + this.usedKeywords = args.usedKeywords; + this.usedKeywordsPostTypes = args.usedKeywordsPostTypes; + this.searchUrl = args.searchUrl; + this.postUrl = args.postUrl; + this.urlTitle = createAnchorOpeningTag( "https://yoa.st/33x" ); + this.urlCallToAction = createAnchorOpeningTag( "https://yoa.st/33y" ); + } -/** - * Registers the assessment with the assessor. - * - * @returns {void} - */ -PreviouslyUsedKeyword.prototype.registerPlugin = function() { - this.app.registerAssessment( "usedKeywords", { - getResult: this.assess.bind( this ), - /** - * Checks if the paper has a keyphrase, which is a prerequisite for the assessment to run. - * - * @param {Paper} paper The paper. - * - * @returns {boolean} Whether the paper has a keyphrase. - */ - isApplicable: function( paper ) { - return paper.hasKeyword(); - }, - }, "previouslyUsedKeywords" ); -}; + /** + * Registers the assessment with the assessor. + * + * @returns {void} + */ + registerPlugin() { + this.app.registerAssessment( "usedKeywords", { + getResult: this.assess.bind( this ), + /** + * Checks if the paper has a keyphrase, which is a prerequisite for the assessment to run. + * + * @param {Paper} paper The paper. + * + * @returns {boolean} Whether the paper has a keyphrase. + */ + isApplicable: function( paper ) { + return paper.hasKeyword(); + }, + }, "previouslyUsedKeywords" ); + } -/** - * Updates the usedKeywords. - * - * @param {object} usedKeywords An object with keywords and ids where they are used. - * @param {object} usedKeywordsPostTypes An object with keywords and in which post types they are used. - * The post types correspond with the ids in the usedKeywords parameter. - * @returns {void} - */ -PreviouslyUsedKeyword.prototype.updateKeywordUsage = function( usedKeywords, usedKeywordsPostTypes ) { - this.usedKeywords = usedKeywords; - this.usedKeywordsPostTypes = usedKeywordsPostTypes; -}; + /** + * Updates the usedKeywords. + * + * @param {object} usedKeywords An object with keywords and ids where they are used. + * @param {object} usedKeywordsPostTypes An object with keywords and in which post types they are used. + * The post types correspond with the ids in the usedKeywords parameter. + * @returns {void} + */ + updateKeywordUsage( usedKeywords, usedKeywordsPostTypes ) { + this.usedKeywords = usedKeywords; + this.usedKeywordsPostTypes = usedKeywordsPostTypes; + } -/** - * Scores the previously used keyword assessment based on the count. - * - * @param {object} previouslyUsedKeywords The result of the previously used keywords research - * @param {Paper} paper The paper object to research. - * @returns {object} the scoreobject with text and score. - */ -PreviouslyUsedKeyword.prototype.scoreAssessment = function( previouslyUsedKeywords, paper ) { - const count = previouslyUsedKeywords.count; - const id = previouslyUsedKeywords.id; - const postTypeToDisplay = previouslyUsedKeywords.postTypeToDisplay; - let url; + /** + * Scores the previously used keyword assessment based on the count. + * + * @param {object} previouslyUsedKeywords The result of the previously used keywords research + * @param {Paper} paper The paper object to research. + * @returns {{text: string, score: number}} The result object with a feedback text and a score. + */ + scoreAssessment( previouslyUsedKeywords, paper ) { + const count = previouslyUsedKeywords.count; + const id = previouslyUsedKeywords.id; + const postTypeToDisplay = previouslyUsedKeywords.postTypeToDisplay; + let url; + + if ( count === 0 ) { + return { + text: sprintf( + /* translators: + %1$s expands to a link to an article on yoast.com, + %2$s expands to an anchor end tag. */ + __( "%1$sPreviously used keyphrase%2$s: You've not used this keyphrase before, very good.", "wordpress-seo" ), + this.urlTitle, + "" + ), + score: 9, + }; + } + if ( count === 1 ) { + url = ``; + return { + /* translators: %1$s expands to an admin link where the keyphrase is already used, + %2$s expands to the anchor end tag, %3$s and %4$s expand to links on yoast.com. */ + text: sprintf( __( + "%3$sPreviously used keyphrase%2$s: You've used this keyphrase %1$sonce before%2$s. %4$sDo not use your keyphrase more than once%2$s.", + "wordpress-seo" + ), + url, + "", + this.urlTitle, + this.urlCallToAction + ), + score: 6, + }; + } - if ( count === 0 ) { - return { - text: sprintf( - /* translators: - %1$s expands to a link to an article on yoast.com, - %2$s expands to an anchor end tag. */ - __( "%1$sPreviously used keyphrase%2$s: You've not used this keyphrase before, very good.", "wordpress-seo" ), + if ( count > 1 ) { + if ( postTypeToDisplay ) { + url = ``; + } else { + url = ``; + } + + return { + /* translators: %1$s expands to a link to the admin search page for the keyphrase, + %2$s expands to the anchor end tag, %3$s and %4$s expand to links to yoast.com */ + text: sprintf( __( + "%3$sPreviously used keyphrase%2$s: You've used this keyphrase %1$smultiple times before%2$s. %4$sDo not use your keyphrase more than once%2$s.", + "wordpress-seo" + ), + url, + "", this.urlTitle, - "" - ), - score: 9, - }; - } - if ( count === 1 ) { - url = ``; - return { - /* translators: %1$s expands to an admin link where the keyphrase is already used, - %2$s expands to the anchor end tag, %3$s and %4$s expand to links on yoast.com. */ - text: sprintf( __( - "%3$sPreviously used keyphrase%2$s: You've used this keyphrase %1$sonce before%2$s. %4$sDo not use your keyphrase more than once%2$s.", - "wordpress-seo" - ), - url, - "", - this.urlTitle, - this.urlCallToAction - ), - score: 6, - }; + this.urlCallToAction + ), + score: 1, + }; + } } - if ( count > 1 ) { - if ( postTypeToDisplay ) { - url = ``; - } else { - url = ``; + /** + * Researches the previously used keywords, based on the used keywords and the keyword in the paper. + * + * @param {Paper} paper The paper object to research. + * @returns {{id: number, count: number}} The object with the count and the id of the previously used keyword + */ + researchPreviouslyUsedKeywords( paper ) { + const keyword = paper.getKeyword(); + let count = 0; + let postTypeToDisplay = ""; + let id = 0; + + if ( ! isUndefined( this.usedKeywords[ keyword ] ) && this.usedKeywords[ keyword ].length > 0 ) { + count = this.usedKeywords[ keyword ].length; + if ( keyword in this.usedKeywordsPostTypes ) { + postTypeToDisplay = this.usedKeywordsPostTypes[ keyword ][ 0 ]; + } + id = this.usedKeywords[ keyword ][ 0 ]; } return { - /* translators: %1$s expands to a link to the admin search page for the keyphrase, - %2$s expands to the anchor end tag, %3$s and %4$s expand to links to yoast.com */ - text: sprintf( __( - "%3$sPreviously used keyphrase%2$s: You've used this keyphrase %1$smultiple times before%2$s. %4$sDo not use your keyphrase more than once%2$s.", - "wordpress-seo" - ), - url, - "", - this.urlTitle, - this.urlCallToAction - ), - score: 1, + id: id, + count: count, + postTypeToDisplay: postTypeToDisplay, }; } -}; -/** - * Researches the previously used keywords, based on the used keywords and the keyword in the paper. - * - * @param {Paper} paper The paper object to research. - * @returns {{id: number, count: number}} The object with the count and the id of the previously used keyword - */ -PreviouslyUsedKeyword.prototype.researchPreviouslyUsedKeywords = function( paper ) { - const keyword = paper.getKeyword(); - let count = 0; - let postTypeToDisplay = ""; - let id = 0; - - if ( ! isUndefined( this.usedKeywords[ keyword ] ) && this.usedKeywords[ keyword ].length > 0 ) { - count = this.usedKeywords[ keyword ].length; - if ( keyword in this.usedKeywordsPostTypes ) { - postTypeToDisplay = this.usedKeywordsPostTypes[ keyword ][ 0 ]; - } - id = this.usedKeywords[ keyword ][ 0 ]; + /** + * Executes the assessment that checks whether a text uses previously used keywords. + * + * @param {Paper} paper The Paper object to assess. + * @returns {AssessmentResult} The assessment result containing both a score and a descriptive text. + */ + assess( paper ) { + const previouslyUsedKeywords = this.researchPreviouslyUsedKeywords( paper ); + const previouslyUsedKeywordsResult = this.scoreAssessment( previouslyUsedKeywords, paper ); + + const assessmentResult = new AssessmentResult(); + assessmentResult.setScore( previouslyUsedKeywordsResult.score ); + assessmentResult.setText( previouslyUsedKeywordsResult.text ); + + return assessmentResult; } - - return { - id: id, - count: count, - postTypeToDisplay: postTypeToDisplay, - }; -}; - -/** - * The assessment for the previously used keywords. - * - * @param {Paper} paper The Paper object to assess. - * @returns {AssessmentResult} The assessment result of the assessment - */ -PreviouslyUsedKeyword.prototype.assess = function( paper ) { - var previouslyUsedKeywords = this.researchPreviouslyUsedKeywords( paper ); - var previouslyUsedKeywordsResult = this.scoreAssessment( previouslyUsedKeywords, paper ); - - var assessmentResult = new AssessmentResult(); - assessmentResult.setScore( previouslyUsedKeywordsResult.score ); - assessmentResult.setText( previouslyUsedKeywordsResult.text ); - - return assessmentResult; -}; - -export default PreviouslyUsedKeyword; +} diff --git a/packages/yoastseo/src/errors/invalidType.js b/packages/yoastseo/src/errors/invalidType.js index 425fafc0228..fd48e45e100 100644 --- a/packages/yoastseo/src/errors/invalidType.js +++ b/packages/yoastseo/src/errors/invalidType.js @@ -1,18 +1,14 @@ -import util from "util"; - /** - * Throws an invalid type error - * - * @param {string} message The message to show when the error is thrown - * - * @returns {void} + * The InvalidTypeError is thrown when an invalid type is passed as an argument. */ -function InvalidTypeError( message ) { - Error.captureStackTrace( this, this.constructor ); - this.name = this.constructor.name; - this.message = message; +export default class InvalidTypeError extends Error { + /** + * Constructs an InvalidTypeError. + * @param {string} message The message to show when the error is thrown. + * @constructor + */ + constructor( message ) { + super( message ); + this.name = "InvalidTypeError"; + } } - -util.inherits( InvalidTypeError, Error ); - -export default InvalidTypeError; diff --git a/packages/yoastseo/src/errors/missingArgument.js b/packages/yoastseo/src/errors/missingArgument.js index 4cecb3a4e79..7346740e07b 100644 --- a/packages/yoastseo/src/errors/missingArgument.js +++ b/packages/yoastseo/src/errors/missingArgument.js @@ -1,20 +1,14 @@ -import util from "util"; - /** - * Error that means that an argument should be passed that wasn't passed. - * - * @constructor - * - * @param {string} message The message for this error. - * - * @returns {void} + * The MissingArgumentError is thrown when a required argument is not passed. */ -function MissingArgumentError( message ) { - Error.captureStackTrace( this, this.constructor ); - this.name = this.constructor.name; - this.message = message; +export default class MissingArgumentError extends Error { + /** + * Constructs a MissingArgumentError. + * @param {string} message The message to show when the error is thrown. + * @constructor + */ + constructor( message ) { + super( message ); + this.name = "MissingArgumentError"; + } } - -util.inherits( MissingArgumentError, Error ); - -export default MissingArgumentError; diff --git a/packages/yoastseo/src/helpers/factory.js b/packages/yoastseo/src/helpers/factory.js index af1439897b1..7ec193aa082 100644 --- a/packages/yoastseo/src/helpers/factory.js +++ b/packages/yoastseo/src/helpers/factory.js @@ -1,73 +1,151 @@ -/** - * A mock factory function. - * - * @returns {void} - */ import { isUndefined } from "lodash"; /** - * Factory prototype. + * FactoryProto is a mock factory function. * - * @constructor */ -const FactoryProto = function() {}; - -/** - * Returns a mock element that lodash accepts as an element - * - * @returns {object} Mock HTML element. - */ -FactoryProto.prototype.buildMockElement = function() { - const mockElement = []; - mockElement.nodeType = 1; - - return mockElement; -}; +export default class FactoryProto { + /** + * Returns a mock element that lodash accepts as an element. + * + * @returns {object} Mock HTML element. + */ + static buildMockElement() { + const mockElement = []; + mockElement.nodeType = 1; + + return mockElement; + } -/** - * Returns a mock researcher - * - * @param {object} expectedValue The expected value or values. - * @param {boolean} multiValue True if multiple values are expected. - * @param {boolean} hasMorphologyData True if the researcher has access to morphology data. - * @param {Object|boolean} config Optional config to be used for an assessment. - * @param {Object|boolean} helpers Optional helpers to be used for an assessment. - * - * @returns {Researcher} Mock researcher. - */ -FactoryProto.prototype.buildMockResearcher = function( expectedValue, multiValue = false, hasMorphologyData = false, - config = false, helpers = false ) { - if ( multiValue && ( typeof expectedValue === "object" || typeof helpers === "object" || typeof config === "object" ) ) { + /** + * Returns a mock researcher. + * + * @param {Object} expectedValue Expected value. + * @param {boolean} [multiValue=false] Whether the researcher has multiple values. + * @param {boolean} [hasMorphologyData=false] Whether the researcher has morphology data. + * @param {Object|boolean} [config=false] Optional config to be used for an assessment. + * @param {Object|boolean} [helpers=false] Optional helpers to be used for an assessment. + * + * @returns {Object} Mock researcher. + */ + static buildMockResearcher( expectedValue, multiValue = false, hasMorphologyData = false, + config = false, helpers = false ) { + if ( multiValue && ( typeof expectedValue === "object" || typeof helpers === "object" || typeof config === "object" ) ) { + return { + /** + * Returns research results by research name for multi-value mock researches. + * + * @param {string} research The name of the research. + * + * @returns {Object} The results of the research. + */ + getResearch: function( research ) { + return expectedValue[ research ]; + }, + + /** + * Returns whether the worker has the specified research. + * @param {string} research The name of the research. + * @returns {boolean} Whether the worker has the research. + */ + hasResearch: function( research ) { + return ! isUndefined( expectedValue[ research ] ); + }, + + /** + * Adds a research. + * @param {string} name The name of the research. + * @param {Object} research The research to register. + * + * @returns {void} + */ + addResearch: function( name, research ) { + expectedValue[ name ] = research; + }, + + /** + * Checks whether morphology data is available. + * + * @returns {boolean} True if the researcher has access to morphology data. + */ + getData: function() { + return hasMorphologyData; + }, + + /** + * Returns the helper to be used for the assessment. + * @param {string} name The name of the helper. + * + * @returns {function} The helper for the assessment. + */ + getHelper: function( name ) { + return helpers[ name ]; + }, + + /** + * Checks whether a helper with the given name exists. + * @param {string} name The name to check. + * + * @returns {boolean} True if the helper exists. + */ + hasHelper: function( name ) { + return ! isUndefined( helpers[ name ] ); + }, + + /** + * Adds a helper under the given name. + * @param {string} name The name. + * @param {function} helper The helper. + * + * @returns {void} + */ + addHelper: function( name, helper ) { + if ( ! helpers ) { + helpers = {}; + } + helpers[ name ] = helper; + }, + + /** + * Returns the config to be used for the assessment. + * @param {string} name The name of the config. + * + * @returns {function} The config for the assessment. + */ + getConfig: function( name ) { + return config[ name ]; + }, + + /** + * Checks if the config exists. + * @param {string} name The name of the config + * + * @returns {boolean} Whether the config exists. + */ + hasConfig: function( name ) { + return ! isUndefined( config[ name ] ); + }, + + /** + * Adds a configuration. + * @param {string} name The name of the config. + * @param {Object} researchConfig The config. + * + * @returns {void} + */ + addConfig: function( name, researchConfig ) { + config[ name ] = researchConfig; + }, + }; + } return { /** - * Return research results by research name for multi-value mock researchers. - * - * @param {string} research The name of the research. + * Returns research results. * * @returns {Object} The results of the research. */ - getResearch: function( research ) { - return expectedValue[ research ]; - }, - - /** - * Return whether the worker has the research. - * @param {string} research The name of the research. - * @returns {boolean} Whether the worker has the research. - */ - hasResearch: function( research ) { - return ! isUndefined( expectedValue[ research ] ); - }, - - /** - * Adds a research. - * @param {string} name The name of the research. - * @param {Object} research The research to register. - * - * @returns {void} - */ - addResearch: function( name, research ) { - expectedValue[ name ] = research; + getResearch: function() { + return expectedValue; }, /** @@ -80,149 +158,62 @@ FactoryProto.prototype.buildMockResearcher = function( expectedValue, multiValue }, /** - * Return the helper to be used for the assessment. - * @param {string} name The name of the helper. + * Returns the helpers to be used for the assessment. * - * @returns {function} The helper for the assessment. + * @returns {Object} The helpers for the assessment. */ - getHelper: function( name ) { - return helpers[ name ]; + getHelper: function() { + return helpers; }, /** * Checks whether a helper with the given name exists. * @param {string} name The name to check. * - * @returns {boolean} Trye if the helper exists. + * @returns {boolean} True if the helper exists. */ hasHelper: function( name ) { return ! isUndefined( helpers[ name ] ); }, /** - * Adds a helper under the given name. - * @param {string} name The name. - * @param {function} helper The helper. + * Returns the config to be used for the assessment. * - * @returns {void} + * @returns {Object} The config for the assessment results. */ - addHelper: function( name, helper ) { - if ( ! helpers ) { - helpers = {}; - } - helpers[ name ] = helper; + getConfig: function() { + return config; }, /** - * Return the config to be used for the assessment. + * Returns whether the worker has the specified config. * @param {string} name The name of the config. - * - * @returns {function} The config for the assessment. - */ - getConfig: function( name ) { - return config[ name ]; - }, - - /** - * Checks if the config exists. - * @param {string} name The name of the config - * - * @returns {boolean} Whether the config exists. + * @returns {boolean} Whether the worker has the specified config. */ hasConfig: function( name ) { - return ! isUndefined( config[ name ] ); - }, - - /** - * Adds a configuration. - * @param {string} name The name of the config. - * @param {Object} researchConfig The config. - * - * @returns {void} - */ - addConfig: function( name, researchConfig ) { - config[ name ] = researchConfig; + return ! isUndefined( expectedValue[ name ] ); }, }; } - return { - /** - * Return research results. - * - * @returns {Object} The results of the research. - */ - getResearch: function() { - return expectedValue; - }, - - /** - * Check whether morphology data is available. - * - * @returns {boolean} True if the researcher has access to morphology data. - */ - getData: function() { - return hasMorphologyData; - }, - - /** - * Return the helpers to be used for the assessment. - * - * @returns {Object} The helpers for the assessment. - */ - getHelper: function() { - return helpers; - }, - - /** - * Return whether the worker has the helper. - * - * @returns {boolean} Whether the worker has the helper. - */ - hasHelper: function() { - return expectedValue; - }, - - /** - * Return the config to be used for the assessment. - * - * @returns {Object} The config for the assessment results. - */ - getConfig: function() { - return config; - }, - - /** - * Return whether the worker has the config. - * @param {string} research The name of the config. - * @returns {boolean} Whether the worker has the research. - */ - hasConfig: function( research ) { - return ! isUndefined( expectedValue[ research ] ); - }, - }; -}; -/** - * This method repeats a string and returns a new string based on the string and the amount of repetitions. - * - * @param {string} string String to repeat. - * @param {int} repetitions Number of repetitions. - * - * @returns {string} The result. - */ -FactoryProto.prototype.buildMockString = function( string, repetitions ) { - let resultString = ""; - - string = string || "Test "; - repetitions = repetitions || 1; - - for ( let i = 0; i < repetitions; i++ ) { - resultString += string; + /** + * This method repeats a string and returns a new string based on the string and the amount of repetitions. + * + * @param {string} string String to repeat. + * @param {int} repetitions Number of repetitions. + * + * @returns {string} The result. + */ + static buildMockString( string, repetitions ) { + let resultString = ""; + + string = string || "Test "; + repetitions = repetitions || 1; + + for ( let i = 0; i < repetitions; i++ ) { + resultString += string; + } + + return resultString; } - - return resultString; -}; - -const Factory = new FactoryProto(); - -export default Factory; +} diff --git a/packages/yoastseo/src/languageProcessing/helpers/morphology/buildTopicStems.js b/packages/yoastseo/src/languageProcessing/helpers/morphology/buildTopicStems.js index cf846eb1e8c..72387354617 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/morphology/buildTopicStems.js +++ b/packages/yoastseo/src/languageProcessing/helpers/morphology/buildTopicStems.js @@ -6,31 +6,36 @@ import { isUndefined, escapeRegExp, memoize } from "lodash"; import isDoubleQuoted from "../match/isDoubleQuoted"; /** - * A topic phrase (i.e., a keyphrase or synonym) with stem-original pairs for the words in the topic phrase. - * - * @param {StemOriginalPair[]} stemOriginalPairs The stem-original pairs for the words in the topic phrase. - * @param {boolean} exactMatch Whether the topic phrase is an exact match. - * - * @constructor - */ -function TopicPhrase( stemOriginalPairs = [], exactMatch = false ) { - this.stemOriginalPairs = stemOriginalPairs; - this.exactMatch = exactMatch; -} - -/** - * Returns all stems in the topic phrase. - * - * @returns {string[]|[]} The stems in the topic phrase or empty array if the topic phrase is exact match. + * A TopicPhrase (i.e., a keyphrase or synonym) with stem-original pairs for the words in the topic phrase. */ -TopicPhrase.prototype.getStems = function() { - // An exact match keyphrase doesn't have stems. - if ( this.exactMatch ) { - return []; +class TopicPhrase { + /** + * Constructs a new TopicPhrase. + * + * @param {StemOriginalPair[]} stemOriginalPairs The stem-original pairs for the words in the topic phrase. + * @param {boolean} exactMatch Whether the topic phrase is an exact match. + * + * @constructor + */ + constructor( stemOriginalPairs = [], exactMatch = false ) { + this.stemOriginalPairs = stemOriginalPairs; + this.exactMatch = exactMatch; } - return this.stemOriginalPairs.map( stemOriginalPair => stemOriginalPair.stem ); -}; + /** + * Returns all stems in the topic phrase. + * + * @returns {string[]|[]} The stems in the topic phrase or empty array if the topic phrase is exact match. + */ + getStems() { + // An exact match keyphrase doesn't have stems. + if ( this.exactMatch ) { + return []; + } + + return this.stemOriginalPairs.map( stemOriginalPair => stemOriginalPair.stem ); + } +} /** * A stem-original pair ƒor a word in a topic phrase. @@ -64,7 +69,7 @@ const buildStems = function( keyphrase, stemmer, functionWords, areHyphensWordBo return new TopicPhrase(); } - // If the keyphrase is embedded in double quotation marks, return the keyprhase itself, without the outermost quotation marks. + // If the keyphrase is embedded in double quotation marks, return the keyphrase itself, without the outermost quotation marks. if ( isDoubleQuoted( keyphrase ) ) { keyphrase = keyphrase.substring( 1, keyphrase.length - 1 ); return new TopicPhrase( diff --git a/packages/yoastseo/src/languageProcessing/helpers/syllables/DeviationFragment.js b/packages/yoastseo/src/languageProcessing/helpers/syllables/DeviationFragment.js index c7f77ae6165..1997f19edd3 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/syllables/DeviationFragment.js +++ b/packages/yoastseo/src/languageProcessing/helpers/syllables/DeviationFragment.js @@ -1,109 +1,112 @@ import { isUndefined, pick } from "lodash"; /** - * Represents a partial deviation when counting syllables - * - * @param {Object} options Extra options about how to match this fragment. - * @param {string} options.location The location in the word where this deviation can occur. - * @param {string} options.word The actual string that should be counted differently. - * @param {number} options.syllables The amount of syllables this fragment has. - * @param {string[]} [options.notFollowedBy] A list of characters that this fragment shouldn't be followed with. - * @param {string[]} [options.alsoFollowedBy] A list of characters that this fragment could be followed with. - * - * @constructor + * A DeviationFragment represents a partial deviation when counting syllables. */ -function DeviationFragment( options ) { - this._location = options.location; - this._fragment = options.word; - this._syllables = options.syllables; - this._regex = null; +export default class DeviationFragment { + /** + * Constructs a new DeviationFragment. + * + * @param {Object} options Extra options that are used to match this deviation fragment. + * @param {string} options.location The location in the word where this deviation can occur. + * @param {string} options.word The actual string that should be counted differently. + * @param {number} options.syllables The amount of syllables this fragment has. + * @param {string[]} [options.notFollowedBy] A list of characters that this fragment shouldn't be followed with. + * @param {string[]} [options.alsoFollowedBy] A list of characters that this fragment could be followed with. + * + * @constructor + */ + constructor( options ) { + this._location = options.location; + this._fragment = options.word; + this._syllables = options.syllables; + this._regex = null; + + this._options = pick( options, [ "notFollowedBy", "alsoFollowedBy" ] ); + } - this._options = pick( options, [ "notFollowedBy", "alsoFollowedBy" ] ); -} + /** + * Creates a regex that matches this fragment inside a word. + * + * @returns {void} + */ + createRegex() { + const options = this._options; -/** - * Creates a regex that matches this fragment inside a word. - * - * @returns {void} - */ -DeviationFragment.prototype.createRegex = function() { - let regexString = ""; - const options = this._options; + let fragment = this._fragment; - let fragment = this._fragment; + if ( ! isUndefined( options.notFollowedBy ) ) { + fragment += "(?![" + options.notFollowedBy.join( "" ) + "])"; + } - if ( ! isUndefined( options.notFollowedBy ) ) { - fragment += "(?![" + options.notFollowedBy.join( "" ) + "])"; - } + if ( ! isUndefined( options.alsoFollowedBy ) ) { + fragment += "[" + options.alsoFollowedBy.join( "" ) + "]?"; + } - if ( ! isUndefined( options.alsoFollowedBy ) ) { - fragment += "[" + options.alsoFollowedBy.join( "" ) + "]?"; - } + let regexString; + switch ( this._location ) { + case "atBeginning": + regexString = "^" + fragment; + break; - switch ( this._location ) { - case "atBeginning": - regexString = "^" + fragment; - break; + case "atEnd": + regexString = fragment + "$"; + break; - case "atEnd": - regexString = fragment + "$"; - break; + case "atBeginningOrEnd": + regexString = "(^" + fragment + ")|(" + fragment + "$)"; + break; - case "atBeginningOrEnd": - regexString = "(^" + fragment + ")|(" + fragment + "$)"; - break; + default: + regexString = fragment; + break; + } - default: - regexString = fragment; - break; + this._regex = new RegExp( regexString ); } - this._regex = new RegExp( regexString ); -}; - -/** - * Returns the regex that matches this fragment inside a word. - * - * @returns {RegExp} The regexp that matches this fragment. - */ -DeviationFragment.prototype.getRegex = function() { - if ( null === this._regex ) { - this.createRegex(); + /** + * Returns the regex that matches this fragment inside a word. + * + * @returns {RegExp} The regex that matches this fragment. + */ + getRegex() { + if ( null === this._regex ) { + this.createRegex(); + } + + return this._regex; } - return this._regex; -}; + /** + * Returns whether this fragment occurs in a word. + * + * @param {string} word The word to match the fragment in. + * @returns {boolean} Whether or not this fragment occurs in a word. + */ + occursIn( word ) { + const regex = this.getRegex(); -/** - * Returns whether or not this fragment occurs in a word. - * - * @param {string} word The word to match the fragment in. - * @returns {boolean} Whether or not this fragment occurs in a word. - */ -DeviationFragment.prototype.occursIn = function( word ) { - const regex = this.getRegex(); - - return regex.test( word ); -}; - -/** - * Removes this fragment from the given word. - * - * @param {string} word The word to remove this fragment from. - * @returns {string} The modified word. - */ -DeviationFragment.prototype.removeFrom = function( word ) { - // Replace by a space to keep the remaining parts separated. - return word.replace( this._fragment, " " ); -}; + return regex.test( word ); + } -/** - * Returns the amount of syllables for this fragment. - * - * @returns {number} The amount of syllables for this fragment. - */ -DeviationFragment.prototype.getSyllables = function() { - return this._syllables; -}; + /** + * Removes this fragment from the given word. + * + * @param {string} word The word to remove this fragment from. + * @returns {string} The modified word. + */ + removeFrom( word ) { + // Replace by a space to keep the remaining parts separated. + return word.replace( this._fragment, " " ); + } -export default DeviationFragment; + /** + * Returns the amount of syllables for this fragment. + * + * @returns {number} The amount of syllables for this fragment. + */ + getSyllables() { + return this._syllables; + } +} diff --git a/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountIterator.js b/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountIterator.js index 7a8853aaecf..143045fdcb2 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountIterator.js +++ b/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountIterator.js @@ -3,51 +3,54 @@ import SyllableCountStep from "./syllableCountStep.js"; import { forEach, isUndefined } from "lodash"; /** - * Creates a syllable count iterator. - * - * @param {object} config The config object containing an array with syllable exclusions. - * @constructor + * A SyllableCountIterator contains individual SyllableCountSteps. */ -const SyllableCountIterator = function( config ) { - this.countSteps = []; - if ( ! isUndefined( config ) ) { - this.createSyllableCountSteps( config.deviations.vowels ); +export default class SyllableCountIterator { + /** + * Creates a syllable count iterator. + * + * @param {object} config The config object containing an array with syllable exclusions. + * @constructor + */ + constructor( config ) { + this.countSteps = []; + if ( ! isUndefined( config ) ) { + this.createSyllableCountSteps( config.deviations.vowels ); + } } -}; -/** - * Creates a syllable count step object for each exclusion. - * - * @param {object} syllableCounts The object containing all exclusion syllables including the multipliers. - * @returns {void} - */ -SyllableCountIterator.prototype.createSyllableCountSteps = function( syllableCounts ) { - forEach( syllableCounts, function( syllableCountStep ) { - this.countSteps.push( new SyllableCountStep( syllableCountStep ) ); - }.bind( this ) ); -}; - -/** - * Returns all available count steps. - * - * @returns {Array} All available count steps. - */ -SyllableCountIterator.prototype.getAvailableSyllableCountSteps = function() { - return this.countSteps; -}; + /** + * Creates a syllable count step object for each exclusion. + * + * @param {object} syllableCounts The object containing all exclusion syllables including the multipliers. + * @returns {void} + */ + createSyllableCountSteps( syllableCounts ) { + forEach( syllableCounts, function( syllableCountStep ) { + this.countSteps.push( new SyllableCountStep( syllableCountStep ) ); + }.bind( this ) ); + } -/** - * Counts the syllables for all the steps and returns the total syllable count. - * - * @param {String} word The word to count syllables in. - * @returns {number} The number of syllables found based on exclusions. - */ -SyllableCountIterator.prototype.countSyllables = function( word ) { - let syllableCount = 0; - forEach( this.countSteps, function( step ) { - syllableCount += step.countSyllables( word ); - } ); - return syllableCount; -}; + /** + * Returns all available count steps. + * + * @returns {Array} All available count steps. + */ + getAvailableSyllableCountSteps() { + return this.countSteps; + } -export default SyllableCountIterator; + /** + * Counts the syllables for all the steps and returns the total syllable count. + * + * @param {String} word The word to count syllables in. + * @returns {number} The number of syllables found based on exclusions. + */ + countSyllables( word ) { + let syllableCount = 0; + forEach( this.countSteps, function( step ) { + syllableCount += step.countSyllables( word ); + } ); + return syllableCount; + } +} diff --git a/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountStep.js b/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountStep.js index e6a7197a94b..c7bbf6f5c9f 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountStep.js +++ b/packages/yoastseo/src/languageProcessing/helpers/syllables/syllableCountStep.js @@ -3,63 +3,66 @@ import { isUndefined } from "lodash"; import arrayToRegex from "../regex/createRegexFromArray.js"; /** - * Constructs a language syllable regex that contains a regex for matching syllable exclusion. - * - * @param {object} syllableRegex The object containing the syllable exclusions. - * @constructor + * A SyllableCountStep is an individual step in a SyllableCountIterator. */ -const SyllableCountStep = function( syllableRegex ) { - this._hasRegex = false; - this._regex = ""; - this._multiplier = ""; - this.createRegex( syllableRegex ); -}; - -/** - * Returns if a valid regex has been set. - * - * @returns {boolean} True if a regex has been set, false if not. - */ -SyllableCountStep.prototype.hasRegex = function() { - return this._hasRegex; -}; +export default class SyllableCountStep { + /** + * Constructs a language syllable regex that contains a regex for matching syllable exclusion. + * + * @param {object} syllableRegex The object containing the syllable exclusions. + * @constructor + */ + constructor( syllableRegex ) { + this._hasRegex = false; + this._regex = ""; + this._multiplier = ""; + this.createRegex( syllableRegex ); + } -/** - * Creates a regex based on the given syllable exclusions, and sets the multiplier to use. - * - * @param {object} syllableRegex The object containing the syllable exclusions and multiplier. - * @returns {void} - */ -SyllableCountStep.prototype.createRegex = function( syllableRegex ) { - if ( ! isUndefined( syllableRegex ) && ! isUndefined( syllableRegex.fragments ) ) { - this._hasRegex = true; - this._regex = arrayToRegex( syllableRegex.fragments, true ); - this._multiplier = syllableRegex.countModifier; + /** + * Checks whether a valid regex has been set. + * + * @returns {boolean} True if a regex has been set, false if not. + */ + hasRegex() { + return this._hasRegex; } -}; -/** - * Returns the stored regular expression. - * - * @returns {RegExp} The stored regular expression. - */ -SyllableCountStep.prototype.getRegex = function() { - return this._regex; -}; + /** + * Creates a regex based on the given syllable exclusions, and sets the multiplier to use. + * + * @param {object} syllableRegex The object containing the syllable exclusions and multiplier. + * @returns {void} + */ + createRegex( syllableRegex ) { + if ( ! isUndefined( syllableRegex ) && ! isUndefined( syllableRegex.fragments ) ) { + this._hasRegex = true; + this._regex = arrayToRegex( syllableRegex.fragments, true ); + this._multiplier = syllableRegex.countModifier; + } + } -/** - * Matches syllable exclusions in a given word and the returns the number found multiplied with the - * given multiplier. - * - * @param {String} word The word to match for syllable exclusions. - * @returns {number} The amount of syllables found. - */ -SyllableCountStep.prototype.countSyllables = function( word ) { - if ( this._hasRegex ) { - const match = word.match( this._regex ) || []; - return match.length * this._multiplier; + /** + * Returns the stored regular expression. + * + * @returns {RegExp} The stored regular expression. + */ + getRegex() { + return this._regex; } - return 0; -}; -export default SyllableCountStep; + /** + * Matches syllable exclusions in a given word and returns the number found multiplied by the + * given multiplier. The result of this multiplication is the syllable count. + * + * @param {String} word The word to match for syllable exclusions. + * @returns {number} The amount of syllables found. + */ + countSyllables( word ) { + if ( this._hasRegex ) { + const match = word.match( this._regex ) || []; + return match.length * this._multiplier; + } + return 0; + } +} diff --git a/packages/yoastseo/src/languageProcessing/languages/en/config/transitionWords.js b/packages/yoastseo/src/languageProcessing/languages/en/config/transitionWords.js index afbd9bb76a7..7d6b1bbf2ef 100644 --- a/packages/yoastseo/src/languageProcessing/languages/en/config/transitionWords.js +++ b/packages/yoastseo/src/languageProcessing/languages/en/config/transitionWords.js @@ -11,7 +11,7 @@ export const singleWords = [ "accordingly", "additionally", "afterward", "afterw "shortly", "significantly", "similarly", "simultaneously", "since", "so", "soon", "specifically", "still", "straightaway", "subsequently", "surely", "surprisingly", "than", "then", "thereafter", "therefore", "thereupon", "thirdly", "though", "thus", "till", "undeniably", "undoubtedly", "unless", "unlike", "unquestionably", "until", "when", "whenever", - "whereas", "while" ]; + "whereas", "while", "whether", "if", "actually" ]; export const multipleWords = [ "above all", "after all", "after that", "all in all", "all of a sudden", "all things considered", "analogous to", "although this may be true", "analogous to", "another key point", "as a matter of fact", "as a result", "as an illustration", "as can be seen", "as has been noted", "as I have noted", "as I have said", "as I have shown", @@ -40,7 +40,7 @@ export const multipleWords = [ "above all", "after all", "after that", "all in a "to summarize", "to that end", "to the end that", "to this end", "together with", "under those circumstances", "until now", "up against", "up to the present time", "vis a vis", "what's more", "while it may be true", "while this may be true", "with attention to", "with the result that", "with this in mind", "with this intention", "with this purpose in mind", - "without a doubt", "without delay", "without doubt", "without reservation" ]; + "without a doubt", "without delay", "without doubt", "without reservation", "according to", "no sooner" ]; export const allWords = singleWords.concat( multipleWords ); diff --git a/packages/yoastseo/src/languageProcessing/languages/en/config/twoPartTransitionWords.js b/packages/yoastseo/src/languageProcessing/languages/en/config/twoPartTransitionWords.js index 404d175b06c..99e6235c90f 100644 --- a/packages/yoastseo/src/languageProcessing/languages/en/config/twoPartTransitionWords.js +++ b/packages/yoastseo/src/languageProcessing/languages/en/config/twoPartTransitionWords.js @@ -4,5 +4,4 @@ * Returns an array with two-part transition words to be used by the assessments. * @type {Array} The array filled with two-part transition words. */ -export default [ [ "both", "and" ], [ "if", "then" ], [ "not only", "but also" ], [ "neither", "nor" ], [ "either", "or" ], [ "not", "but" ], - [ "whether", "or" ], [ "no sooner", "than" ] ]; +export default [ [ "both", "and" ], [ "not only", "but also" ], [ "neither", "nor" ], [ "either", "or" ], [ "not", "but" ] ]; diff --git a/packages/yoastseo/src/languageProcessing/languages/tr/config/transitionWords.js b/packages/yoastseo/src/languageProcessing/languages/tr/config/transitionWords.js index a8cc460d5bd..73551b2f81c 100644 --- a/packages/yoastseo/src/languageProcessing/languages/tr/config/transitionWords.js +++ b/packages/yoastseo/src/languageProcessing/languages/tr/config/transitionWords.js @@ -8,7 +8,8 @@ export const singleWords = [ "fakat", "halbuki", "hatta", "üstelik", "ancak", " "genelde", "dolayısıyla", "gelgelelim", "aslında", "doğrusu", "mamafih", "binaenaleyh", "evvelce", "önceden", "şöylelikle", "örneğin", "mesela", "nitekim", "mademki", "şimdi", "halihazırda", "i̇laveten", "aynen", "nazaran", "nedeniyle", "yüzünden", "umumiyetle", "ekseriye", "amacıyla", "gayesiyle", "velhasıl", "ezcümle", "özetlersek", "etraflıca", "tafsilatlı", "genişçe", "bilfiil", "filhakika", "evvela", "i̇lkin", "en önce", - "birincisi", "i̇kincisi", "üçüncüsü", "sonuncusu", "tıpkı", "topyekun", "hem", "ne", "kah", "ister", "ya", "gerek", "ha" ]; + "birincisi", "i̇kincisi", "üçüncüsü", "sonuncusu", "tıpkı", "topyekun", "hem", "kah", "ister", "ya", "gerekse", "sayesinde", "sebebiyle", + "üzere", "göre", "uyarınca", "halen", "gerçekten", "madem", "yoksa" ]; export const multipleWords = [ "o halde", "bundan böyle", "demek ki", "ne yazık ki", "görüldüğü gibi", "i̇lk olarak", "son olarak", "ne var ki", "buna rağmen", "yine de", "başka bir deyişle", "açıklamak gerekirse", "özetlemek gerekirse", "kısaca söylemek gerekirse", "görüldüğü gibi", @@ -34,7 +35,7 @@ export const multipleWords = [ "o halde", "bundan böyle", "demek ki", "ne yazı "sözün özü", "en nihayetinde", "uzun uzadıya", "her iki durumda da", "özü itibariyle", "amacı ile", "olması için", "başka bir ifadeyle", "diğer bir deyişle", "i̇lk önce", "bir yandan", "bir taraftan", "hatırlatmak gerekirse", "bu bağlamda", "gel gelelim", "her şey hesaba katılırsa", "bütüne bakıldığında", "belirtildiği gibi", "bir başka ifadeyle", "lafı toparlamak gerekirse", "bu düşünceyle", "bu maksatla", "bu doğrultuda", - "bu niyetle", "hem de", "ne de", "ya da", "gerekse de" ]; + "bu niyetle", "ne de", "ya da", "aksi durumda", "bu durum", "olup olmadığı", "diğer yandan", "öte yandan", "ne olursa olsun" ]; export const allWords = singleWords.concat( multipleWords ); diff --git a/packages/yoastseo/src/languageProcessing/languages/tr/config/twoPartTransitionWords.js b/packages/yoastseo/src/languageProcessing/languages/tr/config/twoPartTransitionWords.js index 826accc203e..c43a0719f05 100644 --- a/packages/yoastseo/src/languageProcessing/languages/tr/config/twoPartTransitionWords.js +++ b/packages/yoastseo/src/languageProcessing/languages/tr/config/twoPartTransitionWords.js @@ -4,4 +4,4 @@ * Returns an array with two-part transition words to be used by the assessments. * @returns {Array} The array filled with two-part transition words. */ -export default [ [ "hem", "hem de" ], [ "ne", "ne de" ], [ "ya", "ya da" ], [ "gerek", "gerekse de" ] ]; +export default [ [ "ne", "ne" ], [ "gerek", "gerek" ], [ "olsun", "olmasın" ] ]; diff --git a/packages/yoastseo/src/languageProcessing/values/Clause.js b/packages/yoastseo/src/languageProcessing/values/Clause.js index b25fbc4ba90..42510f37ade 100644 --- a/packages/yoastseo/src/languageProcessing/values/Clause.js +++ b/packages/yoastseo/src/languageProcessing/values/Clause.js @@ -97,7 +97,7 @@ class Clause { * * @returns {Clause} The parsed Clause. */ - parse( serialized ) { + static parse( serialized ) { const clause = new Clause( serialized.clauseText, serialized.auxiliaries ); clause.setPassive( serialized.isPassive ); diff --git a/packages/yoastseo/src/languageProcessing/values/ProminentWord.js b/packages/yoastseo/src/languageProcessing/values/ProminentWord.js index 49a994eb188..fc3cb373129 100644 --- a/packages/yoastseo/src/languageProcessing/values/ProminentWord.js +++ b/packages/yoastseo/src/languageProcessing/values/ProminentWord.js @@ -1,90 +1,95 @@ /** * Represents a prominent word in the context of relevant words. - * - * @constructor - * - * @param {string} word The word. - * @param {string} [stem] The stem / base form of the word, defaults to the word. - * @param {number} [occurrences] The number of occurrences, defaults to 0. - */ -function ProminentWord( word, stem, occurrences ) { - this._word = word; - this._stem = stem ? stem : word; - this._occurrences = occurrences || 0; -} +*/ +class ProminentWord { + /** + * Constructs Prominent word object. + * + * @constructor + * + * @param {string} word The word. + * @param {string} [stem] The stem / base form of the word, defaults to the word. + * @param {number} [occurrences] The number of occurrences, defaults to 0. + */ + constructor( word, stem, occurrences ) { + this._word = word; + this._stem = stem ? stem : word; + this._occurrences = occurrences || 0; + } -/** - * Sets the word. - * - * @param {string} word The word to set. - * - * @returns {void}. - */ -ProminentWord.prototype.setWord = function( word ) { - this._word = word; -}; + /** + * Sets the word. + * + * @param {string} word The word to set. + * + * @returns {void}. + */ + setWord( word ) { + this._word = word; + } -/** - * Returns the word. - * - * @returns {string} The word. - */ -ProminentWord.prototype.getWord = function() { - return this._word; -}; + /** + * Returns the word. + * + * @returns {string} The word. + */ + getWord() { + return this._word; + } -/** - * Returns the stem of the word. - * - * @returns {string} The stem. - */ -ProminentWord.prototype.getStem = function() { - return this._stem; -}; + /** + * Returns the stem of the word. + * + * @returns {string} The stem. + */ + getStem() { + return this._stem; + } -/** - * Sets the number of occurrences to the word. - * - * @param {int} numberOfOccurrences The number of occurrences to set. - * - * @returns {void}. - */ -ProminentWord.prototype.setOccurrences = function( numberOfOccurrences ) { - this._occurrences = numberOfOccurrences; -}; + /** + * Sets the number of occurrences to the word. + * + * @param {int} numberOfOccurrences The number of occurrences to set. + * + * @returns {void}. + */ + setOccurrences( numberOfOccurrences ) { + this._occurrences = numberOfOccurrences; + } -/** - * Returns the amount of occurrences of this word. - * - * @returns {number} The number of occurrences. - */ -ProminentWord.prototype.getOccurrences = function() { - return this._occurrences; -}; + /** + * Returns the amount of occurrences of this word. + * + * @returns {number} The number of occurrences. + */ + getOccurrences() { + return this._occurrences; + } -/** - * Serializes the ProminentWord instance to an object. - * - * @returns {Object} The serialized ProminentWord. - */ -ProminentWord.prototype.serialize = function() { - return { - _parseClass: "ProminentWord", - word: this._word, - stem: this._stem, - occurrences: this._occurrences, - }; -}; + /** + * Serializes the ProminentWord instance to an object. + * + * @returns {Object} The serialized ProminentWord. + */ + serialize() { + return { + _parseClass: "ProminentWord", + word: this._word, + stem: this._stem, + occurrences: this._occurrences, + }; + } -/** - * Parses the object to a ProminentWord. - * - * @param {Object} serialized The serialized object. - * - * @returns {ProminentWord} The parsed ProminentWord. - */ -ProminentWord.parse = function( serialized ) { - return new ProminentWord( serialized.word, serialized.stem, serialized.occurrences ); -}; + /** + * Parses the object to a ProminentWord. + * + * @param {Object} serialized The serialized object. + * + * @returns {ProminentWord} The parsed ProminentWord. + */ + static parse( serialized ) { + return new ProminentWord( serialized.word, serialized.stem, serialized.occurrences ); + } +} export default ProminentWord; diff --git a/packages/yoastseo/src/languageProcessing/values/Sentence.js b/packages/yoastseo/src/languageProcessing/values/Sentence.js index af164af4399..948fc5354f7 100644 --- a/packages/yoastseo/src/languageProcessing/values/Sentence.js +++ b/packages/yoastseo/src/languageProcessing/values/Sentence.js @@ -99,7 +99,7 @@ class Sentence { * * @returns {Sentence} The parsed Sentence. */ - parse( serialized ) { + static parse( serialized ) { const sentence = new Sentence( serialized.sentenceText ); sentence.setClauses( serialized.clauses ); sentence.setPassive( serialized.isPassive ); diff --git a/packages/yoastseo/src/scoring/assessments/seo/UrlKeywordAssessment.js b/packages/yoastseo/src/scoring/assessments/seo/UrlKeywordAssessment.js index 890a076e622..1c6dd81386f 100644 --- a/packages/yoastseo/src/scoring/assessments/seo/UrlKeywordAssessment.js +++ b/packages/yoastseo/src/scoring/assessments/seo/UrlKeywordAssessment.js @@ -63,7 +63,7 @@ class SlugKeywordAssessment extends Assessment { * @returns {boolean} True if the paper contains a keyword and a slug, and if the keywordCountInSlug research is available on the researcher. */ isApplicable( paper, researcher ) { - return paper.hasKeyword() && paper.hasSlug() && researcher.hasResearch( "keywordCountInSlug" ); + return ! paper.isFrontPage() && paper.hasKeyword() && paper.hasSlug() && researcher.hasResearch( "keywordCountInSlug" ); } /** @@ -78,7 +78,7 @@ class SlugKeywordAssessment extends Assessment { return { score: this._config.scores.good, resultText: sprintf( - /* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag */ + /* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag */ __( "%1$sKeyphrase in slug%2$s: Great work!", "wordpress-seo" @@ -92,7 +92,7 @@ class SlugKeywordAssessment extends Assessment { return { score: this._config.scores.okay, resultText: sprintf( - /* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ + /* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ __( "%1$sKeyphrase in slug%3$s: (Part of) your keyphrase does not appear in the slug. %2$sChange that%3$s!", "wordpress-seo" @@ -108,7 +108,7 @@ class SlugKeywordAssessment extends Assessment { return { score: this._config.scores.good, resultText: sprintf( - /* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag */ + /* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag */ __( "%1$sKeyphrase in slug%2$s: More than half of your keyphrase appears in the slug. That's great!", "wordpress-seo" @@ -121,7 +121,7 @@ class SlugKeywordAssessment extends Assessment { return { score: this._config.scores.okay, resultText: sprintf( - /* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ + /* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ __( "%1$sKeyphrase in slug%3$s: (Part of) your keyphrase does not appear in the slug. %2$sChange that%3$s!", "wordpress-seo" @@ -139,7 +139,7 @@ class SlugKeywordAssessment extends Assessment { * UrlKeywordAssessment was the previous name for SlugKeywordAssessment (hence the name of this file). * We keep (and expose) this assessment for backwards compatibility. * - * @deprecated Since version 18.8 Use SlugKeywordAssessment instead. + * @deprecated Since version 1.19.1. Use SlugKeywordAssessment instead. */ class UrlKeywordAssessment extends SlugKeywordAssessment { /** diff --git a/packages/yoastseo/src/scoring/assessors/assessor.js b/packages/yoastseo/src/scoring/assessors/assessor.js index e2da5390968..9ac6df9a097 100644 --- a/packages/yoastseo/src/scoring/assessors/assessor.js +++ b/packages/yoastseo/src/scoring/assessors/assessor.js @@ -14,292 +14,291 @@ import { showTrace } from "../../helpers/errors.js"; const ScoreRating = 9; /** - * Creates the Assessor. - * - * @param {Researcher} researcher The researcher to use in the assessor. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The Assessor is a base class for all assessors. */ -const Assessor = function( researcher, options ) { - this.type = "assessor"; - this.setResearcher( researcher ); - this._assessments = []; - - this._options = options || {}; -}; - -/** - * Checks if the researcher is defined and sets it. - * - * @param {Researcher} researcher The researcher to use in the assessor. - * - * @throws {MissingArgument} Parameter needs to be a valid researcher object. - * @returns {void} - */ -Assessor.prototype.setResearcher = function( researcher ) { - if ( isUndefined( researcher ) ) { - throw new MissingArgument( "The assessor requires a researcher." ); +class Assessor { + /** + * Creates a new Assessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + this.type = "assessor"; + this.setResearcher( researcher ); + this._assessments = []; + + this._options = options || {}; } - this._researcher = researcher; -}; -/** - * Gets all available assessments. - * @returns {object} assessment - */ -Assessor.prototype.getAvailableAssessments = function() { - return this._assessments; -}; - -/** - * Checks whether the Assessment is applicable. - * - * @param {Object} assessment The Assessment object that needs to be checked. - * @param {Paper} paper The Paper object to check against. - * @param {Researcher} [researcher] The Researcher object containing additional information. - * @returns {boolean} Whether or not the Assessment is applicable. - */ -Assessor.prototype.isApplicable = function( assessment, paper, researcher ) { - if ( assessment.hasOwnProperty( "isApplicable" ) || typeof assessment.isApplicable === "function" ) { - return assessment.isApplicable( paper, researcher ); + /** + * Checks if the researcher is defined and sets it. + * + * @param {Researcher} researcher The researcher to use in the assessor. + * + * @throws {MissingArgument} Parameter needs to be a valid researcher object. + * @returns {void} + */ + setResearcher( researcher ) { + if ( isUndefined( researcher ) ) { + throw new MissingArgument( "The assessor requires a researcher." ); + } + this._researcher = researcher; } - return true; -}; - -/** - * Determines whether an assessment has a marker. - * - * @param {Object} assessment The assessment to check for. - * @returns {boolean} Whether or not the assessment has a marker. - */ -Assessor.prototype.hasMarker = function( assessment ) { - return isFunction( this._options.marker ) && ( assessment.hasOwnProperty( "getMarks" ) || typeof assessment.getMarks === "function" ); -}; - -/** - * Returns the specific marker for this assessor. - * - * @returns {Function} The specific marker for this assessor. - */ -Assessor.prototype.getSpecificMarker = function() { - return this._options.marker; -}; - -/** - * Returns the paper that was most recently assessed. - * - * @returns {Paper} The paper that was most recently assessed. - */ -Assessor.prototype.getPaper = function() { - return this._lastPaper; -}; - -/** - * Returns the marker for a given assessment, composes the specific marker with the assessment getMarks function. - * - * @param {Object} assessment The assessment for which we are retrieving the composed marker. - * @param {Paper} paper The paper to retrieve the marker for. - * @param {Researcher} researcher The researcher for the paper. - * @returns {Function} A function that can mark the given paper according to the given assessment. - */ -Assessor.prototype.getMarker = function( assessment, paper, researcher ) { - const specificMarker = this._options.marker; + /** + * Gets all available assessments. + * @returns {Assessment[]} assessment + */ + getAvailableAssessments() { + return this._assessments; + } - return function() { - let marks = assessment.getMarks( paper, researcher ); - marks = removeDuplicateMarks( marks ); + /** + * Checks whether the Assessment is applicable. + * + * @param {Assessment} assessment The Assessment object that needs to be checked. + * @param {Paper} paper The Paper object to check against. + * @param {Researcher} [researcher] The Researcher object containing additional information. + * @returns {boolean} Whether or not the Assessment is applicable. + */ + isApplicable( assessment, paper, researcher ) { + if ( assessment.hasOwnProperty( "isApplicable" ) || typeof assessment.isApplicable === "function" ) { + return assessment.isApplicable( paper, researcher ); + } - specificMarker( paper, marks ); - }; -}; + return true; + } -/** - * Runs the researches defined in the task list or the default researches. - * - * @param {Paper} paper The paper to run assessments on. - * @returns {void} - */ -Assessor.prototype.assess = function( paper ) { - this._researcher.setPaper( paper ); + /** + * Determines whether an assessment has a marker. + * + * @param {Assessment} assessment The assessment to check for. + * @returns {boolean} Whether or not the assessment has a marker. + */ + hasMarker( assessment ) { + return isFunction( this._options.marker ) && ( assessment.hasOwnProperty( "getMarks" ) || typeof assessment.getMarks === "function" ); + } - const languageProcessor = new LanguageProcessor( this._researcher ); - const shortcodes = paper._attributes && paper._attributes.shortcodes; - paper.setTree( build( paper, languageProcessor, shortcodes ) ); + /** + * Returns the specific marker for this assessor. + * + * @returns {Function} The specific marker for this assessor. + */ + getSpecificMarker() { + return this._options.marker; + } - let assessments = this.getAvailableAssessments(); - this.results = []; + /** + * Returns the paper that was most recently assessed. + * + * @returns {Paper} The paper that was most recently assessed. + */ + getPaper() { + return this._lastPaper; + } - assessments = filter( assessments, function( assessment ) { - return this.isApplicable( assessment, paper, this._researcher ); - }.bind( this ) ); + /** + * Returns the marker for a given assessment, composes the specific marker with the assessment getMarks function. + * + * @param {Assessment} assessment The assessment for which we are retrieving the composed marker. + * @param {Paper} paper The paper to retrieve the marker for. + * @param {Researcher} researcher The researcher for the paper. + * @returns {Function} A function that can mark the given paper according to the given assessment. + */ + getMarker( assessment, paper, researcher ) { + const specificMarker = this._options.marker; + + return function() { + let marks = assessment.getMarks( paper, researcher ); + marks = removeDuplicateMarks( marks ); + + specificMarker( paper, marks ); + }; + } - this.setHasMarkers( false ); - this.results = map( assessments, this.executeAssessment.bind( this, paper, this._researcher ) ); + /** + * Runs the researches defined in the task list or the default researches. + * + * @param {Paper} paper The paper to run assessments on. + * @returns {void} + */ + assess( paper ) { + this._researcher.setPaper( paper ); - this._lastPaper = paper; -}; + const languageProcessor = new LanguageProcessor( this._researcher ); + const shortcodes = paper._attributes && paper._attributes.shortcodes; + paper.setTree( build( paper, languageProcessor, shortcodes ) ); -/** - * Sets the value of has markers with a boolean to determine if there are markers. - * - * @param {boolean} hasMarkers True when there are markers, otherwise it is false. - * @returns {void} - */ -Assessor.prototype.setHasMarkers = function( hasMarkers ) { - this._hasMarkers = hasMarkers; -}; + let assessments = this.getAvailableAssessments(); + this.results = []; -/** - * Returns true when there are markers. - * - * @returns {boolean} Are there markers - */ -Assessor.prototype.hasMarkers = function() { - return this._hasMarkers; -}; + assessments = filter( assessments, function( assessment ) { + return this.isApplicable( assessment, paper, this._researcher ); + }.bind( this ) ); -/** - * Executes an assessment and returns the AssessmentResult. - * - * @param {Paper} paper The paper to pass to the assessment. - * @param {Researcher} researcher The researcher to pass to the assessment. - * @param {Object} assessment The assessment to execute. - * @returns {AssessmentResult} The result of the assessment. - */ -Assessor.prototype.executeAssessment = function( paper, researcher, assessment ) { - let result; + this.setHasMarkers( false ); + this.results = map( assessments, this.executeAssessment.bind( this, paper, this._researcher ) ); - try { - result = assessment.getResult( paper, researcher ); - result.setIdentifier( assessment.identifier ); + this._lastPaper = paper; + } - if ( result.hasMarks() ) { - result.marks = assessment.getMarks( paper, researcher ); - result.marks = removeDuplicateMarks( result.marks ); - } + /** + * Sets the value of has markers with a boolean to determine if there are markers. + * + * @param {boolean} hasMarkers True when there are markers, otherwise it is false. + * @returns {void} + */ + setHasMarkers( hasMarkers ) { + this._hasMarkers = hasMarkers; + } - if ( result.hasMarks() && this.hasMarker( assessment ) ) { - this.setHasMarkers( true ); + /** + * Returns true when there are markers. + * + * @returns {boolean} Are there markers + */ + hasMarkers() { + return this._hasMarkers; + } - result.setMarker( this.getMarker( assessment, paper, researcher ) ); + /** + * Executes an assessment and returns the AssessmentResult. + * + * @param {Paper} paper The paper to pass to the assessment. + * @param {Researcher} researcher The researcher to pass to the assessment. + * @param {Assessment} assessment The assessment to execute. + * @returns {AssessmentResult} The result of the assessment. + */ + executeAssessment( paper, researcher, assessment ) { + let result; + + try { + result = assessment.getResult( paper, researcher ); + result.setIdentifier( assessment.identifier ); + + if ( result.hasMarks() ) { + result.marks = assessment.getMarks( paper, researcher ); + result.marks = removeDuplicateMarks( result.marks ); + } + + if ( result.hasMarks() && this.hasMarker( assessment ) ) { + this.setHasMarkers( true ); + + result.setMarker( this.getMarker( assessment, paper, researcher ) ); + } + } catch ( assessmentError ) { + showTrace( assessmentError ); + + result = new AssessmentResult(); + + result.setScore( -1 ); + result.setText( sprintf( + /* translators: %1$s expands to the name of the assessment. */ + __( "An error occurred in the '%1$s' assessment", "wordpress-seo" ), + assessment.identifier, + assessmentError + ) ); } - } catch ( assessmentError ) { - showTrace( assessmentError ); - - result = new AssessmentResult(); - - result.setScore( -1 ); - result.setText( sprintf( - /* translators: %1$s expands to the name of the assessment. */ - __( "An error occurred in the '%1$s' assessment", "wordpress-seo" ), - assessment.identifier, - assessmentError - ) ); + return result; } - return result; -}; -/** - * Filters out all assessment results that have no score and no text. - * - * @returns {Array} The array with all the valid assessments. - */ -Assessor.prototype.getValidResults = function() { - return filter( this.results, function( result ) { - return this.isValidResult( result ); - }.bind( this ) ); -}; - -/** - * Returns if an assessmentResult is valid. - * - * @param {object} assessmentResult The assessmentResult to validate. - * @returns {boolean} whether or not the result is valid. - */ -Assessor.prototype.isValidResult = function( assessmentResult ) { - return assessmentResult.hasScore() && assessmentResult.hasText(); -}; + /** + * Filters out all assessment results that have no score and no text. + * + * @returns {AssessmentResult[]} The array with all the valid assessments. + */ + getValidResults() { + return filter( this.results, function( result ) { + return this.isValidResult( result ); + }.bind( this ) ); + } -/** - * Returns the overall score. Calculates the total score by adding all scores and dividing these - * by the number of results times the ScoreRating. - * - * @returns {number} The overall score. - */ -Assessor.prototype.calculateOverallScore = function() { - const results = this.getValidResults(); + /** + * Returns if an assessmentResult is valid. + * + * @param {AssessmentResult} assessmentResult The assessmentResult to validate. + * @returns {boolean} whether or not the result is valid. + */ + isValidResult( assessmentResult ) { + return assessmentResult.hasScore() && assessmentResult.hasText(); + } - const totalScore = results.reduce( ( total, assessmentResult ) => total + assessmentResult.getScore(), 0 ); + /** + * Returns the overall score. Calculates the total score by adding all scores and dividing these + * by the number of results times the ScoreRating. + * + * @returns {number} The overall score. + */ + calculateOverallScore() { + const results = this.getValidResults(); - return Math.round( totalScore / ( results.length * ScoreRating ) * 100 ) || 0; -}; + const totalScore = results.reduce( ( total, assessmentResult ) => total + assessmentResult.getScore(), 0 ); -/** - * Register an assessment to add it to the internal assessments object. - * - * @param {string} name The name of the assessment. - * @param {object} assessment The object containing function to run as an assessment and it's requirements. - * @returns {boolean} Whether registering the assessment was successful. - * @private - */ -Assessor.prototype.addAssessment = function( name, assessment ) { - if ( ! assessment.hasOwnProperty( "identifier" ) ) { - assessment.identifier = name; + return Math.round( totalScore / ( results.length * ScoreRating ) * 100 ) || 0; } - // If the assessor already has the same assessment, remove it and replace it with the new assessment with the same identifier. - if ( this.getAssessment( assessment.identifier ) ) { - this.removeAssessment( assessment.identifier ); - } - - this._assessments.push( assessment ); - return true; -}; -/** - * Remove a specific Assessment from the list of Assessments. - * - * @param {string} name The Assessment to remove from the list of assessments. - * @returns {void} - */ -Assessor.prototype.removeAssessment = function( name ) { - const toDelete = findIndex( this._assessments, function( assessment ) { - return assessment.hasOwnProperty( "identifier" ) && name === assessment.identifier; - } ); + /** + * Registers an assessment and adds it to the internal assessments object. + * + * @param {string} name The name of the assessment. + * @param {Assessment} assessment The object containing function to run as an assessment and it's requirements. + * @returns {boolean} Whether registering the assessment was successful. + */ + addAssessment( name, assessment ) { + if ( ! assessment.hasOwnProperty( "identifier" ) ) { + assessment.identifier = name; + } + // If the assessor already has the same assessment, remove it and replace it with the new assessment with the same identifier. + if ( this.getAssessment( assessment.identifier ) ) { + this.removeAssessment( assessment.identifier ); + } - if ( -1 !== toDelete ) { - this._assessments.splice( toDelete, 1 ); + this._assessments.push( assessment ); + return true; } -}; -/** - * Returns an assessment by identifier - * - * @param {string} identifier The identifier of the assessment. - * @returns {undefined|Assessment} The object if found, otherwise undefined. - */ -Assessor.prototype.getAssessment = function( identifier ) { - return find( this._assessments, function( assessment ) { - return assessment.hasOwnProperty( "identifier" ) && identifier === assessment.identifier; - } ); -}; + /** + * Removes a specific Assessment from the list of Assessments. + * + * @param {string} name The Assessment to remove from the list of assessments. + * @returns {void} + */ + removeAssessment( name ) { + const toDelete = findIndex( this._assessments, function( assessment ) { + return assessment.hasOwnProperty( "identifier" ) && name === assessment.identifier; + } ); + + if ( -1 !== toDelete ) { + this._assessments.splice( toDelete, 1 ); + } + } -/** - * Checks which of the available assessments are applicable and returns an array with applicable assessments. - * - * @returns {Array} The array with applicable assessments. - */ -Assessor.prototype.getApplicableAssessments = function() { - const availableAssessments = this.getAvailableAssessments(); - return filter( - availableAssessments, - function( availableAssessment ) { - return this.isApplicable( availableAssessment, this.getPaper(), this._researcher ); - }.bind( this ) - ); -}; + /** + * Returns an assessment by identifier + * + * @param {string} identifier The identifier of the assessment. + * @returns {undefined|Assessment} The object if found, otherwise undefined. + */ + getAssessment( identifier ) { + return find( this._assessments, function( assessment ) { + return assessment.hasOwnProperty( "identifier" ) && identifier === assessment.identifier; + } ); + } + /** + * Checks which of the available assessments are applicable and returns an array with applicable assessments. + * + * @returns {Assessment[]} The array with applicable assessments. + */ + getApplicableAssessments() { + const availableAssessments = this.getAvailableAssessments(); + return filter( + availableAssessments, + function( availableAssessment ) { + return this.isApplicable( availableAssessment, this.getPaper(), this._researcher ); + }.bind( this ) + ); + } +} export default Assessor; diff --git a/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/relatedKeywordAssessor.js index 5d2942049d6..c23beca0adc 100644 --- a/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/relatedKeywordAssessor.js @@ -1,53 +1,24 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; +import CollectionRelatedKeywordAssessor from "../relatedKeywordAssessor.js"; import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor used for collection pages. - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The CollectionCornerstoneRelatedKeywordAssessor class is used for the related keyword analysis for cornerstone collections. */ -const CollectionCornerstoneRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "collectionCornerstoneRelatedKeywordAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( - { parameters: { recommendedMinimum: 1 }, - scores: { good: 9, bad: 3 }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } - ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - ]; -}; - -inherits( CollectionCornerstoneRelatedKeywordAssessor, Assessor ); +export default class CollectionCornerstoneRelatedKeywordAssessor extends CollectionRelatedKeywordAssessor { + /** + * Creates a new CollectionCornerstoneRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "collectionRelatedKeywordAssessor"; -export default CollectionCornerstoneRelatedKeywordAssessor; + this.addAssessment( "metaDescriptionKeyword", new MetaDescriptionKeywordAssessment( { + parameters: { recommendedMinimum: 1 }, scores: { good: 9, bad: 3 }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/seoAssessor.js index 674c7fd97f4..e613306ed56 100644 --- a/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/collectionPages/cornerstone/seoAssessor.js @@ -1,57 +1,21 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import MetaDescriptionLengthAssessment from "../../../assessments/seo/MetaDescriptionLengthAssessment.js"; +import CollectionSEOAssessor from "../seoAssessor"; import TextLengthAssessment from "../../../assessments/seo/TextLengthAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../../../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import PageTitleWidthAssessment from "../../../assessments/seo/PageTitleWidthAssessment.js"; -import SlugKeywordAssessment from "../../../assessments/seo/UrlKeywordAssessment.js"; -import SingleH1Assessment from "../../../assessments/seo/SingleH1Assessment.js"; - import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor used for collection pages. - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @constructor + * The CollectionCornerstoneSEOAssessor class is used for the SEO analysis for cornerstone collections. */ -const CollectionCornerstoneSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "collectionCornerstoneSEOAssessor"; +export default class CollectionCornerstoneSEOAssessor extends CollectionSEOAssessor { + /** + * Creates a new CollectionCornerstoneSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "collectionCornerstoneSEOAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( - { - parameters: { recommendedMinimum: 1 }, - scores: { good: 9, bad: 3 }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } - ), - new MetaDescriptionLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new TextLengthAssessment( { + this.addAssessment( "textLength", new TextLengthAssessment( { recommendedMinimum: 30, slightlyBelowMinimum: 10, veryFarBelowMinimum: 1, @@ -59,42 +23,6 @@ const CollectionCornerstoneSEOAssessor = function( researcher, options ) { urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), cornerstoneContent: true, customContentType: this.type, - } ), - new KeyphraseInSEOTitleAssessment( - { - parameters: { - recommendedPosition: 0, - }, - scores: { - good: 9, - bad: 2, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } - ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), - } ), - ]; -}; - -inherits( CollectionCornerstoneSEOAssessor, Assessor ); - -export default CollectionCornerstoneSEOAssessor; + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/collectionPages/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/collectionPages/relatedKeywordAssessor.js index 106fa61673a..136325fd924 100644 --- a/packages/yoastseo/src/scoring/assessors/collectionPages/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/collectionPages/relatedKeywordAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor"; +import RelatedKeywordAssessor from "../relatedKeywordAssessor"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -9,42 +7,40 @@ import FunctionWordsInKeyphraseAssessment from "../../assessments/seo/FunctionWo import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor used for collection pages. - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The CollectionRelatedKeywordAssessor class is used for the related keyword analysis for collections. */ -const CollectionRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "collectionRelatedKeywordAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - ]; -}; - -inherits( CollectionRelatedKeywordAssessor, Assessor ); +export default class CollectionRelatedKeywordAssessor extends RelatedKeywordAssessor { + /** + * Creates a new CollectionRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "collectionRelatedKeywordAssessor"; -export default CollectionRelatedKeywordAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), + } ), + new KeyphraseLengthAssessment( { + isRelatedKeyphrase: true, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), + } ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/collectionPages/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/collectionPages/seoAssessor.js index cba89aa451e..0bedf72546a 100644 --- a/packages/yoastseo/src/scoring/assessors/collectionPages/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/collectionPages/seoAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; +import SEOAssessor from "../seoAssessor.js"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -12,75 +10,73 @@ import KeyphraseInSEOTitleAssessment from "../../assessments/seo/KeyphraseInSEOT import PageTitleWidthAssessment from "../../assessments/seo/PageTitleWidthAssessment.js"; import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment.js"; import SingleH1Assessment from "../../assessments/seo/SingleH1Assessment.js"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor used for collection pages. - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @constructor + * The CollectionSEOAssessor class is used for the SEO analysis for collections. */ -const CollectionSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "collectionSEOAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new MetaDescriptionLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new TextLengthAssessment( { - recommendedMinimum: 30, - slightlyBelowMinimum: 10, - veryFarBelowMinimum: 1, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify58" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), - customContentType: this.type, - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), - } ), - ]; -}; - -inherits( CollectionSEOAssessor, Assessor ); +export default class CollectionSEOAssessor extends SEOAssessor { + /** + * Creates a new CollectionSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "collectionSEOAssessor"; -export default CollectionSEOAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), + } ), + new KeyphraseLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), + } ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ), + new MetaDescriptionLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), + } ), + new TextLengthAssessment( { + recommendedMinimum: 30, + slightlyBelowMinimum: 10, + veryFarBelowMinimum: 1, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify58" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), + customContentType: this.type, + } ), + new KeyphraseInSEOTitleAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), + } ), + new PageTitleWidthAssessment( { + scores: { + widthTooShort: 9, + }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), + }, true ), + new SlugKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), + } ), + new SingleH1Assessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/contentAssessor.js index 00d86b7e0d4..e42802f79b2 100644 --- a/packages/yoastseo/src/scoring/assessors/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/contentAssessor.js @@ -9,164 +9,154 @@ import SentenceBeginnings from "../assessments/readability/SentenceBeginningsAss import TextPresence from "../assessments/readability/TextPresenceAssessment.js"; import scoreToRating from "../interpreters/scoreToRating.js"; - -/* - Temporarily disabled: - var sentenceLengthInDescription = require( "./assessments/sentenceLengthInDescriptionAssessment.js" ); - */ - /** - * Creates the Assessor - * - * @param {object} researcher The researcher to use for the analysis. - * @param {Object} options The options for this assessor. - * @param {Object} options.marker The marker to pass the list of marks to. - * - * @constructor + * The ContentAssessor class is used for the readability analysis. */ -const ContentAssessor = function( researcher, options = {} ) { - Assessor.call( this, researcher, options ); - this.type = "contentAssessor"; - this._assessments = [ - new SubheadingDistributionTooLong(), - new ParagraphTooLong(), - new SentenceLengthInText(), - new TransitionWords(), - new PassiveVoice(), - new TextPresence(), - new SentenceBeginnings(), - // Temporarily disabled: wordComplexity, - ]; -}; - -require( "util" ).inherits( ContentAssessor, Assessor ); +export default class ContentAssessor extends Assessor { + /** + * Creates a new ContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "contentAssessor"; + + this._assessments = [ + new SubheadingDistributionTooLong(), + new ParagraphTooLong(), + new SentenceLengthInText(), + new TransitionWords(), + new PassiveVoice(), + new TextPresence(), + new SentenceBeginnings(), + ]; + } -/** - * Calculates the weighted rating for languages that have all assessments based on a given rating. - * - * @param {number} rating The rating to be weighted. - * @returns {number} The weighted rating. - */ -ContentAssessor.prototype.calculatePenaltyPointsFullSupport = function( rating ) { - switch ( rating ) { - case "bad": - return 3; - case "ok": - return 2; - default: - case "good": - return 0; + /** + * Calculates the weighted rating for languages that have all assessments based on a given rating. + * + * @param {string} rating The rating to be weighted. + * @returns {number} The weighted rating. + */ + calculatePenaltyPointsFullSupport( rating ) { + switch ( rating ) { + case "bad": + return 3; + case "ok": + return 2; + default: + case "good": + return 0; + } } -}; -/** - * Calculates the weighted rating for languages that don't have all assessments based on a given rating. - * - * @param {number} rating The rating to be weighted. - * @returns {number} The weighted rating. - */ -ContentAssessor.prototype.calculatePenaltyPointsPartialSupport = function( rating ) { - switch ( rating ) { - case "bad": - return 4; - case "ok": - return 2; - default: - case "good": - return 0; + /** + * Calculates the weighted rating for languages that don't have all assessments based on a given rating. + * + * @param {string} rating The rating to be weighted. + * @returns {number} The weighted rating. + */ + calculatePenaltyPointsPartialSupport( rating ) { + switch ( rating ) { + case "bad": + return 4; + case "ok": + return 2; + default: + case "good": + return 0; + } } -}; -/** - * Determines whether a language is fully supported. If a language supports 8 content assessments - * it is fully supported - * - * @returns {boolean} True if fully supported. - */ -ContentAssessor.prototype._allAssessmentsSupported = function() { - const numberOfAssessments = this._assessments.length; - const applicableAssessments = this.getApplicableAssessments(); - return applicableAssessments.length === numberOfAssessments; -}; + /** + * Determines whether a language is fully supported. If a language supports 7 content assessments, + * it is fully supported + * + * @returns {boolean} True if fully supported. + */ + _allAssessmentsSupported() { + const numberOfAssessments = this._assessments.length; + const applicableAssessments = this.getApplicableAssessments(); + return applicableAssessments.length === numberOfAssessments; + } -/** - * Calculates the penalty points based on the assessment results. - * - * @returns {number} The total penalty points for the results. - */ -ContentAssessor.prototype.calculatePenaltyPoints = function() { - const results = this.getValidResults(); + /** + * Calculates the penalty points based on the assessment results. + * + * @returns {number} The total penalty points for the results. + */ + calculatePenaltyPoints() { + const results = this.getValidResults(); - const penaltyPoints = map( results, function( result ) { - const rating = scoreToRating( result.getScore() ); + const penaltyPoints = map( results, function( result ) { + const rating = scoreToRating( result.getScore() ); - if ( this._allAssessmentsSupported() ) { - return this.calculatePenaltyPointsFullSupport( rating ); - } + if ( this._allAssessmentsSupported() ) { + return this.calculatePenaltyPointsFullSupport( rating ); + } - return this.calculatePenaltyPointsPartialSupport( rating ); - }.bind( this ) ); + return this.calculatePenaltyPointsPartialSupport( rating ); + }.bind( this ) ); - return sum( penaltyPoints ); -}; - -/** - * Rates the penalty points - * - * @param {number} totalPenaltyPoints The amount of penalty points. - * @returns {number} The score based on the amount of penalty points. - * - * @private - */ -ContentAssessor.prototype._ratePenaltyPoints = function( totalPenaltyPoints ) { - if ( this.getValidResults().length === 1 ) { - // If we have only 1 result, we only have a "no content" result - return 30; + return sum( penaltyPoints ); } - if ( this._allAssessmentsSupported() ) { - // Determine the total score based on the total penalty points. - if ( totalPenaltyPoints > 6 ) { - // A red indicator. - return 30; - } - - if ( totalPenaltyPoints > 4 ) { - // An orange indicator. - return 60; - } - } else { - if ( totalPenaltyPoints > 4 ) { - // A red indicator. + /** + * Rates the penalty points + * + * @param {number} totalPenaltyPoints The amount of penalty points. + * @returns {number} The score based on the amount of penalty points. + * + * @private + */ + _ratePenaltyPoints( totalPenaltyPoints ) { + if ( this.getValidResults().length === 1 ) { + // If we have only 1 result, we only have a "no content" result return 30; } - if ( totalPenaltyPoints > 2 ) { - // An orange indicator. - return 60; + if ( this._allAssessmentsSupported() ) { + // Determine the total score based on the total penalty points. + if ( totalPenaltyPoints > 6 ) { + // A red indicator. + return 30; + } + + if ( totalPenaltyPoints > 4 ) { + // An orange indicator. + return 60; + } + } else { + if ( totalPenaltyPoints > 4 ) { + // A red indicator. + return 30; + } + + if ( totalPenaltyPoints > 2 ) { + // An orange indicator. + return 60; + } } + // A green indicator. + return 90; } - // A green indicator. - return 90; -}; -/** - * Calculates the overall score based on the assessment results. - * - * @returns {number} The overall score. - */ -ContentAssessor.prototype.calculateOverallScore = function() { - const results = this.getValidResults(); - - // If you have no content, you have a red indicator. - if ( results.length === 0 ) { - return 30; - } + /** + * Calculates the overall score based on the assessment results. + * + * @returns {number} The overall score. + */ + calculateOverallScore() { + const results = this.getValidResults(); - const totalPenaltyPoints = this.calculatePenaltyPoints(); - - return this._ratePenaltyPoints( totalPenaltyPoints ); -}; + // If you have no content, you have a red indicator. + if ( results.length === 0 ) { + return 30; + } -export default ContentAssessor; + const totalPenaltyPoints = this.calculatePenaltyPoints(); + return this._ratePenaltyPoints( totalPenaltyPoints ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/cornerstone/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/cornerstone/contentAssessor.js index 1319ae0e1f7..aa52faa1bae 100644 --- a/packages/yoastseo/src/scoring/assessors/cornerstone/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/cornerstone/contentAssessor.js @@ -1,57 +1,27 @@ -import Assessor from "../assessor.js"; import ContentAssessor from "../contentAssessor"; -import ParagraphTooLong from "../../assessments/readability/ParagraphTooLongAssessment.js"; import SentenceLengthInText from "../../assessments/readability/SentenceLengthInTextAssessment.js"; import SubheadingDistributionTooLong from "../../assessments/readability/SubheadingDistributionTooLongAssessment.js"; -import TransitionWords from "../../assessments/readability/TransitionWordsAssessment.js"; -import PassiveVoice from "../../assessments/readability/PassiveVoiceAssessment.js"; -import SentenceBeginnings from "../../assessments/readability/SentenceBeginningsAssessment.js"; -import TextPresence from "../../assessments/readability/TextPresenceAssessment.js"; - -/* - Temporarily disabled: - - var sentenceLengthInDescription = require( "./assessments/readability/sentenceLengthInDescriptionAssessment.js" ); - */ /** - * Creates the Assessor - * - * @param {object} researcher The researcher used for the analysis. - * @param {Object} options The options for this assessor. - * @param {Object} options.marker The marker to pass the list of marks to. - * - * @constructor + * The CornerStoneContentAssessor class is used for the readability analysis on cornerstone content. */ -const CornerStoneContentAssessor = function( researcher, options = {} ) { - Assessor.call( this, researcher, options ); - this.type = "cornerstoneContentAssessor"; - - this._assessments = [ - new SubheadingDistributionTooLong( { - parameters: { - slightlyTooMany: 250, - farTooMany: 300, - recommendedMaximumLength: 250, - }, +export default class CornerstoneContentAssessor extends ContentAssessor { + /** + * Creates a new CornerStoneContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "cornerstoneContentAssessor"; + + this.addAssessment( "subheadingsTooLong", new SubheadingDistributionTooLong( { + parameters: { slightlyTooMany: 250, farTooMany: 300, recommendedMaximumLength: 250 }, applicableIfTextLongerThan: 250, cornerstoneContent: true, - } ), - new ParagraphTooLong(), - new SentenceLengthInText( { - slightlyTooMany: 20, - farTooMany: 25, - }, true ), - new TransitionWords(), - new PassiveVoice(), - new TextPresence(), - new SentenceBeginnings(), - // Temporarily disabled: wordComplexity, - ]; -}; - -require( "util" ).inherits( CornerStoneContentAssessor, ContentAssessor ); - - -export default CornerStoneContentAssessor; - + } ) ); + this.addAssessment( "textSentenceLength", new SentenceLengthInText( { + slightlyTooMany: 20, farTooMany: 25 }, true ) + ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/cornerstone/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/cornerstone/relatedKeywordAssessor.js index ca80e2dbf46..9f0a2e8fe67 100644 --- a/packages/yoastseo/src/scoring/assessors/cornerstone/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/cornerstone/relatedKeywordAssessor.js @@ -1,43 +1,21 @@ -import { inherits } from "util"; -import Assessor from "../assessor.js"; -import IntroductionKeyword from "../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLength from "../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeyword from "../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import TextCompetingLinks from "../../assessments/seo/TextCompetingLinksAssessment.js"; -import FunctionWordsInKeyphrase from "../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import ImageKeyphrase from "../../assessments/seo/KeyphraseInImageTextAssessment.js"; +import RelatedKeywordAssessor from "../relatedKeywordAssessor"; +import KeyphraseInImagesAssessment from "../../assessments/seo/KeyphraseInImageTextAssessment"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The CornerstoneRelatedKeywordAssessor class is used for the related keyword analysis for cornerstone content. */ -const relatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "cornerstoneRelatedKeywordAssessor"; +export default class CornerstoneRelatedKeywordAssessor extends RelatedKeywordAssessor { + /** + * Creates a new CornerstoneRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "cornerstoneRelatedKeywordAssessor"; - this._assessments = [ - new IntroductionKeyword(), - new KeyphraseLength( { isRelatedKeyphrase: true } ), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeyword(), - new TextCompetingLinks(), - new FunctionWordsInKeyphrase(), - new ImageKeyphrase( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, - } ), - ]; -}; - -inherits( relatedKeywordAssessor, Assessor ); - -export default relatedKeywordAssessor; + this.addAssessment( "imageKeyphrase", new KeyphraseInImagesAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/cornerstone/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/cornerstone/seoAssessor.js index e8d6eade6a1..86e78ba123b 100644 --- a/packages/yoastseo/src/scoring/assessors/cornerstone/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/cornerstone/seoAssessor.js @@ -1,97 +1,45 @@ -import { inherits } from "util"; -import Assessor from "../assessor.js"; -import SEOAssessor from "../seoAssessor.js"; -import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../../assessments/seo/TextCompetingLinksAssessment.js"; -import InternalLinksAssessment from "../../assessments/seo/InternalLinksAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment.js"; -import MetaDescriptionLength from "../../assessments/seo/MetaDescriptionLengthAssessment.js"; -import SubheadingsKeyword from "../../assessments/seo/SubHeadingsKeywordAssessment.js"; -import ImageKeyphrase from "../../assessments/seo/KeyphraseInImageTextAssessment.js"; -import ImageCount from "../../assessments/seo/ImageCountAssessment.js"; -import TextLength from "../../assessments/seo/TextLengthAssessment.js"; -import OutboundLinks from "../../assessments/seo/OutboundLinksAssessment.js"; -import TitleWidth from "../../assessments/seo/PageTitleWidthAssessment.js"; -import FunctionWordsInKeyphrase from "../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import SingleH1Assessment from "../../assessments/seo/SingleH1Assessment.js"; +import SEOAssessor from "../seoAssessor"; +import MetaDescriptionLengthAssessment from "../../assessments/seo/MetaDescriptionLengthAssessment"; +import KeyphraseInImagesAssessment from "../../assessments/seo/KeyphraseInImageTextAssessment"; +import TextLengthAssessment from "../../assessments/seo/TextLengthAssessment"; +import OutboundLinksAssessment from "../../assessments/seo/OutboundLinksAssessment"; +import PageTitleWidthAssessment from "../../assessments/seo/PageTitleWidthAssessment"; +import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The CornerstoneSEOAssessor class is used for the SEO analysis for cornerstone content. */ -const CornerstoneSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "cornerstoneSEOAssessor"; +export default class CornerstoneSEOAssessor extends SEOAssessor { + /** + * Creates a new CornerstoneSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "cornerstoneSEOAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment(), - new KeyphraseLengthAssessment(), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeywordAssessment(), - new MetaDescriptionLength( { - scores: { - tooLong: 3, - tooShort: 3, - }, - } ), - new SubheadingsKeyword(), - new TextCompetingLinksAssessment(), - new ImageKeyphrase( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, - } ), - new ImageCount(), - new TextLength( { + this.addAssessment( "metaDescriptionLength", new MetaDescriptionLengthAssessment( { + scores: { tooLong: 3, tooShort: 3 }, + } ) ); + this.addAssessment( "imageKeyphrase", new KeyphraseInImagesAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, + } ) ); + this.addAssessment( "textLength", new TextLengthAssessment( { recommendedMinimum: 900, slightlyBelowMinimum: 400, belowMinimum: 300, - - scores: { - belowMinimum: -20, - farBelowMinimum: -20, - }, - + scores: { belowMinimum: -20, farBelowMinimum: -20 }, cornerstoneContent: true, - } ), - new OutboundLinks( { - scores: { - noLinks: 3, - }, - } ), - new KeyphraseInSEOTitleAssessment(), - new InternalLinksAssessment(), - new TitleWidth( - { - scores: { - widthTooShort: 9, - }, - }, - true - ), - new SlugKeywordAssessment( - { - scores: { - okay: 3, - }, - } - ), - new FunctionWordsInKeyphrase(), - new SingleH1Assessment(), - ]; -}; - -inherits( CornerstoneSEOAssessor, SEOAssessor ); - -export default CornerstoneSEOAssessor; + } ) ); + this.addAssessment( "externalLinks", new OutboundLinksAssessment( { + scores: { noLinks: 3 }, + } ) ); + this.addAssessment( "titleWidth", new PageTitleWidthAssessment( { + scores: { widthTooShort: 9 }, + }, true ) ); + this.addAssessment( "slugKeyword", new SlugKeywordAssessment( { + scores: { okay: 3 }, + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/inclusiveLanguageAssessor.js b/packages/yoastseo/src/scoring/assessors/inclusiveLanguageAssessor.js index 7c2b170eb7c..7bdee7e6ebe 100644 --- a/packages/yoastseo/src/scoring/assessors/inclusiveLanguageAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/inclusiveLanguageAssessor.js @@ -13,14 +13,13 @@ const defaultOptions = { }; /** - * An assessor that assesses a paper for potentially non-inclusive language. + * The InclusiveLanguageAssessor assesses a paper for potentially non-inclusive language. */ -class InclusiveLanguageAssessor extends Assessor { +export default class InclusiveLanguageAssessor extends Assessor { /** - * Creates a new inclusive language assessor. - * - * @param {Researcher} researcher The researcher to use. - * @param {Object} [options] The assessor options. + * Creates a new InclusiveLanguageAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. */ constructor( researcher, options = {} ) { super( researcher, options ); @@ -63,5 +62,3 @@ class InclusiveLanguageAssessor extends Assessor { return 90; } } - -export default InclusiveLanguageAssessor; diff --git a/packages/yoastseo/src/scoring/assessors/productPages/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/contentAssessor.js index c507566e654..e9449786aba 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/contentAssessor.js @@ -1,6 +1,3 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; import ContentAssessor from "../contentAssessor.js"; import SubheadingDistributionTooLongAssessment from "../../assessments/readability/SubheadingDistributionTooLongAssessment.js"; import ParagraphTooLongAssessment from "../../assessments/readability/ParagraphTooLongAssessment.js"; @@ -8,58 +5,53 @@ import SentenceLengthInTextAssessment from "../../assessments/readability/Senten import TransitionWordsAssessment from "../../assessments/readability/TransitionWordsAssessment.js"; import PassiveVoiceAssessment from "../../assessments/readability/PassiveVoiceAssessment.js"; import TextPresenceAssessment from "../../assessments/readability/TextPresenceAssessment.js"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {object} researcher The researcher to use for the analysis. - * @param {Object} options The options for this assessor. - * - * @constructor + * The ProductContentAssessor class is used for the readability analysis for products. */ -const ProductContentAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productContentAssessor"; - - this._assessments = [ - new SubheadingDistributionTooLongAssessment( { - shouldNotAppearInShortText: true, - urlTitle: createAnchorOpeningTag( options.subheadingUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.subheadingCTAUrl ), - } ), - new ParagraphTooLongAssessment( { - parameters: { - recommendedLength: 70, - maximumRecommendedLength: 100, - }, - urlTitle: createAnchorOpeningTag( options.paragraphUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.paragraphCTAUrl ), - }, true ), - new SentenceLengthInTextAssessment( { - slightlyTooMany: 20, - farTooMany: 25, - urlTitle: createAnchorOpeningTag( options.sentenceLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.sentenceLengthCTAUrl ), - }, false, true ), - new TransitionWordsAssessment( { - urlTitle: createAnchorOpeningTag( options.transitionWordsUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.transitionWordsCTAUrl ), - } ), - new PassiveVoiceAssessment( { - urlTitle: createAnchorOpeningTag( options.passiveVoiceUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.passiveVoiceCTAUrl ), - } ), - new TextPresenceAssessment( { - urlTitle: createAnchorOpeningTag( options.textPresenceUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textPresenceCTAUrl ), - } ), - ]; -}; - -inherits( ProductContentAssessor, ContentAssessor ); - - -export default ProductContentAssessor; - +export default class ProductContentAssessor extends ContentAssessor { + /** + * Creates a new ProductContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productContentAssessor"; + + this._assessments = [ + new SubheadingDistributionTooLongAssessment( { + shouldNotAppearInShortText: true, + urlTitle: createAnchorOpeningTag( options.subheadingUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.subheadingCTAUrl ), + } ), + new ParagraphTooLongAssessment( { + parameters: { + recommendedLength: 70, + maximumRecommendedLength: 100, + }, + urlTitle: createAnchorOpeningTag( options.paragraphUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.paragraphCTAUrl ), + }, true ), + new SentenceLengthInTextAssessment( { + slightlyTooMany: 20, + farTooMany: 25, + urlTitle: createAnchorOpeningTag( options.sentenceLengthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.sentenceLengthCTAUrl ), + }, false, true ), + new TransitionWordsAssessment( { + urlTitle: createAnchorOpeningTag( options.transitionWordsUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.transitionWordsCTAUrl ), + } ), + new PassiveVoiceAssessment( { + urlTitle: createAnchorOpeningTag( options.passiveVoiceUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.passiveVoiceCTAUrl ), + } ), + new TextPresenceAssessment( { + urlTitle: createAnchorOpeningTag( options.textPresenceUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.textPresenceCTAUrl ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/contentAssessor.js index 63effdf0cff..9014991a227 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/contentAssessor.js @@ -1,72 +1,34 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import ContentAssessor from "../../contentAssessor.js"; +import ProductContentAssessor from "../contentAssessor.js"; import SubheadingDistributionTooLongAssessment from "../../../assessments/readability/SubheadingDistributionTooLongAssessment.js"; -import ParagraphTooLongAssessment from "../../../assessments/readability/ParagraphTooLongAssessment.js"; import SentenceLengthInTextAssessment from "../../../assessments/readability/SentenceLengthInTextAssessment.js"; -import TransitionWordsAssessment from "../../../assessments/readability/TransitionWordsAssessment.js"; -import PassiveVoiceAssessment from "../../../assessments/readability/PassiveVoiceAssessment.js"; -import TextPresenceAssessment from "../../../assessments/readability/TextPresenceAssessment.js"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {object} researcher The researcher to use for the analysis. - * @param {Object} options The options for this assessor. - * - * @constructor + * The ProductContentAssessor class is used for the readability analysis for cornerstone products. */ -const ProductCornerstoneContentAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productCornerstoneContentAssessor"; - - this._assessments = [ - new SubheadingDistributionTooLongAssessment( { - parameters: { - slightlyTooMany: 250, - farTooMany: 300, - recommendedMaximumLength: 250, - }, +export default class ProductCornerstoneContentAssessor extends ProductContentAssessor { + /** + * Creates a new ProductContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productCornerstoneContentAssessor"; + + this.addAssessment( "subheadingsTooLong", new SubheadingDistributionTooLongAssessment( { + parameters: { slightlyTooMany: 250, farTooMany: 300, recommendedMaximumLength: 250 }, applicableIfTextLongerThan: 250, shouldNotAppearInShortText: true, urlTitle: createAnchorOpeningTag( options.subheadingUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.subheadingCTAUrl ), cornerstoneContent: true, - } ), - new ParagraphTooLongAssessment( { - parameters: { - recommendedLength: 70, - maximumRecommendedLength: 100, - }, - urlTitle: createAnchorOpeningTag( options.paragraphUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.paragraphCTAUrl ), - }, true ), - new SentenceLengthInTextAssessment( { - slightlyTooMany: 15, - farTooMany: 20, + } ) ); + this.addAssessment( "textSentenceLength", new SentenceLengthInTextAssessment( { + slightlyTooMany: 15, farTooMany: 20, urlTitle: createAnchorOpeningTag( options.sentenceLengthUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.sentenceLengthCTAUrl ), - }, true, true ), - new TransitionWordsAssessment( { - urlTitle: createAnchorOpeningTag( options.transitionWordsUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.transitionWordsCTAUrl ), - } ), - new PassiveVoiceAssessment( { - urlTitle: createAnchorOpeningTag( options.passiveVoiceUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.passiveVoiceCTAUrl ), - } ), - new TextPresenceAssessment( { - urlTitle: createAnchorOpeningTag( options.textPresenceUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textPresenceCTAUrl ), - } ), - ]; -}; - -inherits( ProductCornerstoneContentAssessor, ContentAssessor ); - - -export default ProductCornerstoneContentAssessor; - + }, true, true ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/relatedKeywordAssessor.js index 156f336939f..41115a88431 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/relatedKeywordAssessor.js @@ -1,72 +1,24 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../../../assessments/seo/TextCompetingLinksAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; +import ProductRelatedKeywordAssessor from "../relatedKeywordAssessor.js"; import ImageKeyphraseAssessment from "../../../assessments/seo/KeyphraseInImageTextAssessment.js"; - import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The CollectionCornerstoneRelatedKeywordAssessor class is used for the related keyword analysis for cornerstone products. */ -const ProductCornerStoneRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productPageCornerstoneRelatedKeywordAssessor"; +export default class ProductCornerstoneRelatedKeywordAssessor extends ProductRelatedKeywordAssessor { + /** + * Creates a new ProductCornerstoneRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productPageCornerstoneRelatedKeywordAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), - } ), - new KeyphraseLengthAssessment( { - parameters: { - recommendedMinimum: 4, - recommendedMaximum: 6, - acceptableMaximum: 8, - acceptableMinimum: 2, - }, - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), - }, true ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), - } ), - new ImageKeyphraseAssessment( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, + this.addAssessment( "imageKeyphrase", new ImageKeyphraseAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), - } ), - ]; -}; - -inherits( ProductCornerStoneRelatedKeywordAssessor, Assessor ); - -export default ProductCornerStoneRelatedKeywordAssessor; + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/seoAssessor.js index f8715336c84..40f9d72c03a 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/cornerstone/seoAssessor.js @@ -1,138 +1,48 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import SeoAssessor from "../../seoAssessor.js"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; +import ProductSEOAssessor from "../seoAssessor.js"; import MetaDescriptionLengthAssessment from "../../../assessments/seo/MetaDescriptionLengthAssessment.js"; -import SubheadingsKeywordAssessment from "../../../assessments/seo/SubHeadingsKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../../../assessments/seo/TextCompetingLinksAssessment.js"; import TextLengthAssessment from "../../../assessments/seo/TextLengthAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../../../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import PageTitleWidthAssessment from "../../../assessments/seo/PageTitleWidthAssessment.js"; import SlugKeywordAssessment from "../../../assessments/seo/UrlKeywordAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import SingleH1Assessment from "../../../assessments/seo/SingleH1Assessment.js"; -import ImageCountAssessment from "../../../assessments/seo/ImageCountAssessment.js"; import ImageKeyphraseAssessment from "../../../assessments/seo/KeyphraseInImageTextAssessment.js"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The ProductCornerstoneSEOAssessor class is used for the SEO analysis for cornerstone products. */ -const ProductCornerstoneSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productCornerstoneSEOAssessor"; +export default class ProductCornerstoneSEOAssessor extends ProductSEOAssessor { + /** + * Creates a new ProductCornerstoneSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productCornerstoneSEOAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), - } ), - new KeyphraseLengthAssessment( { - parameters: { - recommendedMinimum: 4, - recommendedMaximum: 6, - acceptableMaximum: 8, - acceptableMinimum: 2, - }, - urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), - }, true ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), - } ), - new MetaDescriptionLengthAssessment( { - scores: { - tooLong: 3, - tooShort: 3, - }, + this.addAssessment( "metaDescriptionLength", new MetaDescriptionLengthAssessment( { + scores: { tooLong: 3, tooShort: 3 }, urlTitle: createAnchorOpeningTag( options.metaDescriptionLengthUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.metaDescriptionLengthCTAUrl ), - } ), - new SubheadingsKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.subheadingsKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.subheadingsKeyphraseCTAUrl ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), - } ), - new TextLengthAssessment( { + } ) ); + this.addAssessment( "textLength", new TextLengthAssessment( { recommendedMinimum: 400, slightlyBelowMinimum: 300, belowMinimum: 200, - - scores: { - belowMinimum: -20, - farBelowMinimum: -20, - }, + scores: { belowMinimum: -20, farBelowMinimum: -20 }, urlTitle: createAnchorOpeningTag( options.textLengthUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.textLengthCTAUrl ), cornerstoneContent: true, customContentType: this.type, - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( options.titleKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.titleKeyphraseCTAUrl ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( options.titleWidthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.titleWidthCTAUrl ), - }, true ), - new SlugKeywordAssessment( - { - scores: { - okay: 3, - }, - urlTitle: createAnchorOpeningTag( options.urlKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.urlKeyphraseCTAUrl ), - } - ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( options.singleH1UrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.singleH1CTAUrl ), - } ), - new ImageCountAssessment( { - scores: { - okay: 6, - }, - recommendedCount: 4, - urlTitle: createAnchorOpeningTag( options.imageCountUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.imageCountCTAUrl ), - }, options.countVideos ), - new ImageKeyphraseAssessment( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, + } ) ); + this.addAssessment( "slugKeyword", new SlugKeywordAssessment( { + scores: { okay: 3 }, + urlTitle: createAnchorOpeningTag( options.urlKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.urlKeyphraseCTAUrl ), + } ) ); + this.addAssessment( "imageKeyphrase", new ImageKeyphraseAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), - } ), - ]; -}; - -inherits( ProductCornerstoneSEOAssessor, SeoAssessor ); - -export default ProductCornerstoneSEOAssessor; + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/productPages/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/relatedKeywordAssessor.js index c0fc7081c5f..5f3da6c4c11 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/relatedKeywordAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; +import RelatedKeywordAssessor from "../relatedKeywordAssessor.js"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -8,60 +6,57 @@ import MetaDescriptionKeywordAssessment from "../../assessments/seo/MetaDescript import TextCompetingLinksAssessment from "../../assessments/seo/TextCompetingLinksAssessment.js"; import FunctionWordsInKeyphraseAssessment from "../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; import ImageKeyphraseAssessment from "../../assessments/seo/KeyphraseInImageTextAssessment.js"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The ProductRelatedKeywordAssessor class is used for the related keyword analysis for products. */ -const ProductRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productPageRelatedKeywordAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), - } ), - new KeyphraseLengthAssessment( { - parameters: { - recommendedMinimum: 4, - recommendedMaximum: 6, - acceptableMaximum: 8, - acceptableMinimum: 2, - }, - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), - }, true ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), - } ), - new ImageKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), - } ), - ]; -}; - -inherits( ProductRelatedKeywordAssessor, Assessor ); +export default class ProductRelatedKeywordAssessor extends RelatedKeywordAssessor { + /** + * Creates a new ProductRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productPageRelatedKeywordAssessor"; -export default ProductRelatedKeywordAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), + } ), + new KeyphraseLengthAssessment( { + parameters: { + recommendedMinimum: 4, + recommendedMaximum: 6, + acceptableMaximum: 8, + acceptableMinimum: 2, + }, + isRelatedKeyphrase: true, + urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), + }, true ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), + } ), + new TextCompetingLinksAssessment( { + urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), + } ), + new ImageKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/productPages/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/productPages/seoAssessor.js index 90d2dc35005..836873c1229 100644 --- a/packages/yoastseo/src/scoring/assessors/productPages/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/productPages/seoAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; +import SEOAssessor from "../seoAssessor"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -16,103 +14,100 @@ import PageTitleWidthAssessment from "../../assessments/seo/PageTitleWidthAssess import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment.js"; import SingleH1Assessment from "../../assessments/seo/SingleH1Assessment.js"; import ImageCountAssessment from "../../assessments/seo/ImageCountAssessment.js"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The ProductSEOAssessor class is used for the SEO analysis for products. */ -const ProductSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "productSEOAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), - } ), - new KeyphraseLengthAssessment( { - parameters: { - recommendedMinimum: 4, - recommendedMaximum: 6, - acceptableMaximum: 8, - acceptableMinimum: 2, - }, - urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), - }, true ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), - } ), - new MetaDescriptionLengthAssessment( { - urlTitle: createAnchorOpeningTag( options.metaDescriptionLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.metaDescriptionLengthCTAUrl ), - } ), - new SubheadingsKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.subheadingsKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.subheadingsKeyphraseCTAUrl ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), - } ), - new TextLengthAssessment( { - recommendedMinimum: 200, - slightlyBelowMinimum: 150, - belowMinimum: 100, - veryFarBelowMinimum: 50, - urlTitle: createAnchorOpeningTag( options.textLengthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.textLengthCTAUrl ), - customContentType: this.type, - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( options.titleKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.titleKeyphraseCTAUrl ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( options.titleWidthUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.titleWidthCTAUrl ), - }, true ), - new SlugKeywordAssessment( { - urlTitle: createAnchorOpeningTag( options.urlKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.urlKeyphraseCTAUrl ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( options.singleH1UrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.singleH1CTAUrl ), - } ), - new ImageCountAssessment( { - scores: { - okay: 6, - }, - recommendedCount: 4, - urlTitle: createAnchorOpeningTag( options.imageCountUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.imageCountCTAUrl ), - }, options.countVideos ), - new ImageKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), - urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), - } ), - ]; -}; - -inherits( ProductSEOAssessor, Assessor ); +export default class ProductSEOAssessor extends SEOAssessor { + /** + * Creates a new ProductSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "productSEOAssessor"; -export default ProductSEOAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.introductionKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.introductionKeyphraseCTAUrl ), + } ), + new KeyphraseLengthAssessment( { + parameters: { + recommendedMinimum: 4, + recommendedMaximum: 6, + acceptableMaximum: 8, + acceptableMinimum: 2, + }, + urlTitle: createAnchorOpeningTag( options.keyphraseLengthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.keyphraseLengthCTAUrl ), + }, true ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( options.keyphraseDensityUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.keyphraseDensityCTAUrl ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.metaDescriptionKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.metaDescriptionKeyphraseCTAUrl ), + } ), + new MetaDescriptionLengthAssessment( { + urlTitle: createAnchorOpeningTag( options.metaDescriptionLengthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.metaDescriptionLengthCTAUrl ), + } ), + new SubheadingsKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.subheadingsKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.subheadingsKeyphraseCTAUrl ), + } ), + new TextCompetingLinksAssessment( { + urlTitle: createAnchorOpeningTag( options.textCompetingLinksUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.textCompetingLinksCTAUrl ), + } ), + new TextLengthAssessment( { + recommendedMinimum: 200, + slightlyBelowMinimum: 150, + belowMinimum: 100, + veryFarBelowMinimum: 50, + urlTitle: createAnchorOpeningTag( options.textLengthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.textLengthCTAUrl ), + customContentType: this.type, + } ), + new KeyphraseInSEOTitleAssessment( { + urlTitle: createAnchorOpeningTag( options.titleKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.titleKeyphraseCTAUrl ), + } ), + new PageTitleWidthAssessment( { + scores: { + widthTooShort: 9, + }, + urlTitle: createAnchorOpeningTag( options.titleWidthUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.titleWidthCTAUrl ), + }, true ), + new SlugKeywordAssessment( { + urlTitle: createAnchorOpeningTag( options.urlKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.urlKeyphraseCTAUrl ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( options.functionWordsInKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.functionWordsInKeyphraseCTAUrl ), + } ), + new SingleH1Assessment( { + urlTitle: createAnchorOpeningTag( options.singleH1UrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.singleH1CTAUrl ), + } ), + new ImageCountAssessment( { + scores: { + okay: 6, + }, + recommendedCount: 4, + urlTitle: createAnchorOpeningTag( options.imageCountUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.imageCountCTAUrl ), + }, options.countVideos ), + new ImageKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( options.imageKeyphraseUrlTitle ), + urlCallToAction: createAnchorOpeningTag( options.imageKeyphraseCTAUrl ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/relatedKeywordAssessor.js index 3c15af85a2e..2a3c84048a3 100644 --- a/packages/yoastseo/src/scoring/assessors/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/relatedKeywordAssessor.js @@ -1,38 +1,33 @@ -import { inherits } from "util"; - import Assessor from "./assessor.js"; import IntroductionKeyword from "../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLength from "../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../assessments/seo/KeywordDensityAssessment.js"; import MetaDescriptionKeyword from "../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import ImageKeyphrase from "../assessments/seo/KeyphraseInImageTextAssessment"; import TextCompetingLinks from "../assessments/seo/TextCompetingLinksAssessment.js"; -import FunctionWordsInKeyphrase from "../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; +import FunctionWordsInKeyphrase from "../assessments/seo/FunctionWordsInKeyphraseAssessment"; +import ImageKeyphrase from "../assessments/seo/KeyphraseInImageTextAssessment"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The relatedKeywordAssessor class is used for the related keyword analysis. */ -const relatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "relatedKeywordAssessor"; - - this._assessments = [ - new IntroductionKeyword(), - new KeyphraseLength( { isRelatedKeyphrase: true } ), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeyword(), - new TextCompetingLinks(), - new FunctionWordsInKeyphrase(), - new ImageKeyphrase(), - ]; -}; - -inherits( relatedKeywordAssessor, Assessor ); +export default class RelatedKeywordAssessor extends Assessor { + /** + * Creates a new RelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "relatedKeywordAssessor"; -export default relatedKeywordAssessor; + this._assessments = [ + new IntroductionKeyword(), + new KeyphraseLength( { isRelatedKeyphrase: true } ), + new KeyphraseDensityAssessment(), + new MetaDescriptionKeyword(), + new TextCompetingLinks(), + new FunctionWordsInKeyphrase(), + new ImageKeyphrase(), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/relatedKeywordTaxonomyAssessor.js b/packages/yoastseo/src/scoring/assessors/relatedKeywordTaxonomyAssessor.js index c7da6e9a6ee..5356129466e 100644 --- a/packages/yoastseo/src/scoring/assessors/relatedKeywordTaxonomyAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/relatedKeywordTaxonomyAssessor.js @@ -1,33 +1,19 @@ -import { inherits } from "util"; -import Assessor from "./assessor.js"; -import IntroductionKeywordAssessment from "../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import FunctionWordsInKeyphrase from "../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; +import RelatedKeywordAssessor from "./relatedKeywordAssessor.js"; /** - * Creates the Assessor used for taxonomy pages. - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * - * @constructor + * The RelatedKeywordTaxonomyAssessor class is used for the related keyword analysis on terms. */ -const RelatedKeywordTaxonomyAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "relatedKeywordsTaxonomyAssessor"; +export default class RelatedKeywordTaxonomyAssessor extends RelatedKeywordAssessor { + /** + * Creates a new RelatedKeywordTaxonomyAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "relatedKeywordsTaxonomyAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment(), - new KeyphraseLengthAssessment( { isRelatedKeyphrase: true } ), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeywordAssessment(), - // Text Images assessment here. - new FunctionWordsInKeyphrase(), - ]; -}; - -inherits( RelatedKeywordTaxonomyAssessor, Assessor ); - -export default RelatedKeywordTaxonomyAssessor; + this.removeAssessment( "textCompetingLinks" ); + this.removeAssessment( "imageKeyphrase" ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/seoAssessor.js index 47448306e2a..05ef9ba4d1e 100644 --- a/packages/yoastseo/src/scoring/assessors/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/seoAssessor.js @@ -1,61 +1,57 @@ -import { inherits } from "util"; -import Assessor from "./assessor.js"; -import IntroductionKeywordAssessment from "../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../assessments/seo/TextCompetingLinksAssessment.js"; -import InternalLinksAssessment from "../assessments/seo/InternalLinksAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import SlugKeywordAssessment from "../assessments/seo/UrlKeywordAssessment.js"; -import MetaDescriptionLength from "../assessments/seo/MetaDescriptionLengthAssessment.js"; -import SubheadingsKeyword from "../assessments/seo/SubHeadingsKeywordAssessment.js"; -import ImageKeyphrase from "../assessments/seo/KeyphraseInImageTextAssessment.js"; -import ImageCount from "../assessments/seo/ImageCountAssessment.js"; -import TextLength from "../assessments/seo/TextLengthAssessment.js"; -import OutboundLinks from "../assessments/seo/OutboundLinksAssessment.js"; -import TitleWidth from "../assessments/seo/PageTitleWidthAssessment.js"; -import FunctionWordsInKeyphrase from "../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import SingleH1Assessment from "../assessments/seo/SingleH1Assessment.js"; +import Assessor from "./assessor"; +import IntroductionKeywordAssessment from "../assessments/seo/IntroductionKeywordAssessment"; +import KeyphraseLengthAssessment from "../assessments/seo/KeyphraseLengthAssessment"; +import KeyphraseDensityAssessment from "../assessments/seo/KeywordDensityAssessment"; +import MetaDescriptionKeywordAssessment from "../assessments/seo/MetaDescriptionKeywordAssessment"; +import TextCompetingLinksAssessment from "../assessments/seo/TextCompetingLinksAssessment"; +import InternalLinksAssessment from "../assessments/seo/InternalLinksAssessment"; +import KeyphraseInSEOTitleAssessment from "../assessments/seo/KeyphraseInSEOTitleAssessment"; +import SlugKeywordAssessment from "../assessments/seo/UrlKeywordAssessment"; +import MetaDescriptionLength from "../assessments/seo/MetaDescriptionLengthAssessment"; +import SubheadingsKeyword from "../assessments/seo/SubHeadingsKeywordAssessment"; +import ImageKeyphrase from "../assessments/seo/KeyphraseInImageTextAssessment"; +import ImageCount from "../assessments/seo/ImageCountAssessment"; +import TextLength from "../assessments/seo/TextLengthAssessment"; +import OutboundLinks from "../assessments/seo/OutboundLinksAssessment"; +import TitleWidth from "../assessments/seo/PageTitleWidthAssessment"; +import FunctionWordsInKeyphrase from "../assessments/seo/FunctionWordsInKeyphraseAssessment"; +import SingleH1Assessment from "../assessments/seo/SingleH1Assessment"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The SEOAssessor class is used for the general SEO analysis. */ -const SEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "SEOAssessor"; +export default class SEOAssessor extends Assessor { + /** + * Creates a new SEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "SEOAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment(), - new KeyphraseLengthAssessment(), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeywordAssessment(), - new MetaDescriptionLength(), - new SubheadingsKeyword(), - new TextCompetingLinksAssessment(), - new ImageKeyphrase(), - new ImageCount(), - new TextLength(), - new OutboundLinks(), - new KeyphraseInSEOTitleAssessment(), - new InternalLinksAssessment(), - new TitleWidth( { - scores: { - widthTooShort: 9, - }, - }, true ), - new SlugKeywordAssessment(), - new FunctionWordsInKeyphrase(), - new SingleH1Assessment(), - ]; -}; - -inherits( SEOAssessor, Assessor ); - -export default SEOAssessor; + this._assessments = [ + new IntroductionKeywordAssessment(), + new KeyphraseLengthAssessment(), + new KeyphraseDensityAssessment(), + new MetaDescriptionKeywordAssessment(), + new MetaDescriptionLength(), + new SubheadingsKeyword(), + new TextCompetingLinksAssessment(), + new ImageKeyphrase(), + new ImageCount(), + new TextLength(), + new OutboundLinks(), + new KeyphraseInSEOTitleAssessment(), + new InternalLinksAssessment(), + new TitleWidth( { + scores: { + widthTooShort: 9, + }, + }, true ), + new SlugKeywordAssessment(), + new FunctionWordsInKeyphrase(), + new SingleH1Assessment(), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storeBlog/cornerstone/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/storeBlog/cornerstone/seoAssessor.js index e4655fd2f22..79f1c1a0425 100644 --- a/packages/yoastseo/src/scoring/assessors/storeBlog/cornerstone/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storeBlog/cornerstone/seoAssessor.js @@ -1,74 +1,31 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import SeoAssessor from "../../seoAssessor.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; +import StoreBlogSEOAssessor from "../seoAssessor.js"; import MetaDescriptionLengthAssessment from "../../../assessments/seo/MetaDescriptionLengthAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../../../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import PageTitleWidthAssessment from "../../../assessments/seo/PageTitleWidthAssessment.js"; import SlugKeywordAssessment from "../../../assessments/seo/UrlKeywordAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; - import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StoreBlogCornerstoneSEOAssessor class is used for the SEO analysis for cornerstone store blogs. */ -const StoreBlogCornerstoneSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storeBlogCornerstoneSEOAssessor"; +export default class StoreBlogCornerstoneSEOAssessor extends StoreBlogSEOAssessor { + /** + * Creates a new StoreBlogCornerstoneSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storeBlogCornerstoneSEOAssessor"; - this._assessments = [ - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new MetaDescriptionLengthAssessment( { - scores: { - tooLong: 3, - tooShort: 3, - }, + this.addAssessment( "metaDescriptionLength", new MetaDescriptionLengthAssessment( { + scores: { tooLong: 3, tooShort: 3 }, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( - { - scores: { - okay: 3, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } - ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - ]; -}; - -inherits( StoreBlogCornerstoneSEOAssessor, SeoAssessor ); + } ) ); -export default StoreBlogCornerstoneSEOAssessor; + this.addAssessment( "slugKeyword", new SlugKeywordAssessment( { + scores: { okay: 3 }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storeBlog/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/storeBlog/seoAssessor.js index b47691c56d0..4426fdfc449 100644 --- a/packages/yoastseo/src/scoring/assessors/storeBlog/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storeBlog/seoAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor"; +import SEOAssessor from "../seoAssessor"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment"; import MetaDescriptionKeywordAssessment from "../../assessments/seo/MetaDescriptionKeywordAssessment"; import MetaDescriptionLengthAssessment from "../../assessments/seo/MetaDescriptionLengthAssessment"; @@ -8,57 +6,53 @@ import KeyphraseInSEOTitleAssessment from "../../assessments/seo/KeyphraseInSEOT import PageTitleWidthAssessment from "../../assessments/seo/PageTitleWidthAssessment"; import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment"; import FunctionWordsInKeyphraseAssessment from "../../assessments/seo/FunctionWordsInKeyphraseAssessment"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StoreBlogSEOAssessor class is used for the SEO analysis for store blogs. */ -const StoreBlogSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storeBlogSEOAssessor"; - - this._assessments = [ - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new MetaDescriptionLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - ]; -}; - -inherits( StoreBlogSEOAssessor, Assessor ); +export default class StoreBlogSEOAssessor extends SEOAssessor { + /** + * Creates a new StoreBlogSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storeBlogSEOAssessor"; -export default StoreBlogSEOAssessor; + this._assessments = [ + new KeyphraseLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ), + new MetaDescriptionLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), + } ), + new KeyphraseInSEOTitleAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), + } ), + new PageTitleWidthAssessment( { + scores: { + widthTooShort: 9, + }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), + }, true ), + new SlugKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/contentAssessor.js index 030d116f015..85a7567dffa 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/contentAssessor.js @@ -1,6 +1,3 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; import ContentAssessor from "../contentAssessor.js"; import SubheadingDistributionTooLongAssessment from "../../assessments/readability/SubheadingDistributionTooLongAssessment.js"; import ParagraphTooLongAssessment from "../../assessments/readability/ParagraphTooLongAssessment.js"; @@ -12,50 +9,47 @@ import SentenceBeginningsAssessment from "../../assessments/readability/Sentence import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor for e-commerce posts and pages content types. - * - * @param {object} researcher The researcher to use for the analysis. - * @param {Object} options The options for this assessor. - * - * @constructor + * The StorePostsAndPagesContentAssessor class is used for the readability analysis for store posts and pages. */ -const StorePostsAndPagesContentAssessor = function( researcher, options = {} ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesContentAssessor"; - this._assessments = [ - - new SubheadingDistributionTooLongAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify68" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify69" ), - } ), - new ParagraphTooLongAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify66" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify67" ), - } ), - new SentenceLengthInTextAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify48" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify49" ), - } ), - new TransitionWordsAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify44" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify45" ), - } ), - new PassiveVoiceAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify42" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify43" ), - } ), - new TextPresenceAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify56" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify57" ), - } ), - new SentenceBeginningsAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify5" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify65" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesContentAssessor, ContentAssessor ); - -export default StorePostsAndPagesContentAssessor; +export default class StorePostsAndPagesContentAssessor extends ContentAssessor { + /** + * Creates a new StorePostsAndPagesContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesContentAssessor"; + this._assessments = [ + new SubheadingDistributionTooLongAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify68" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify69" ), + } ), + new ParagraphTooLongAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify66" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify67" ), + } ), + new SentenceLengthInTextAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify48" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify49" ), + } ), + new TransitionWordsAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify44" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify45" ), + } ), + new PassiveVoiceAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify42" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify43" ), + } ), + new TextPresenceAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify56" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify57" ), + } ), + new SentenceBeginningsAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify5" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify65" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/contentAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/contentAssessor.js index 04b6a933fe8..3914201ba0d 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/contentAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/contentAssessor.js @@ -1,72 +1,31 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import ContentAssessor from "../../contentAssessor.js"; +import ContentAssessor from "../contentAssessor.js"; import SubheadingDistributionTooLongAssessment from "../../../assessments/readability/SubheadingDistributionTooLongAssessment.js"; -import ParagraphTooLongAssessment from "../../../assessments/readability/ParagraphTooLongAssessment.js"; import SentenceLengthInTextAssessment from "../../../assessments/readability/SentenceLengthInTextAssessment.js"; -import TransitionWordsAssessment from "../../../assessments/readability/TransitionWordsAssessment.js"; -import PassiveVoiceAssessment from "../../../assessments/readability/PassiveVoiceAssessment.js"; -import TextPresenceAssessment from "../../../assessments/readability/TextPresenceAssessment.js"; -import SentenceBeginningsAssessment from "../../../assessments/readability/SentenceBeginningsAssessment.js"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {object} researcher The researcher used for the analysis. - * @param {Object} options The options for this assessor. - * @param {Object} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StorePostsAndPagesCornerstoneContentAssessor class is used for the readability analysis for cornerstone store posts and pages. */ -const StorePostsAndPagesCornerstoneContentAssessor = function( researcher, options = {} ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesCornerstoneContentAssessor"; - - this._assessments = [ - - new SubheadingDistributionTooLongAssessment( { - parameters: { - slightlyTooMany: 250, - farTooMany: 300, - recommendedMaximumLength: 250, - }, +export default class StorePostsAndPagesCornerstoneContentAssessor extends ContentAssessor { + /** + * Creates a new StorePostsAndPagesCornerstoneContentAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesCornerstoneContentAssessor"; + + this.addAssessment( "subheadingsTooLong", new SubheadingDistributionTooLongAssessment( { + parameters: { slightlyTooMany: 250, farTooMany: 300, recommendedMaximumLength: 250 }, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify68" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify69" ), cornerstoneContent: true, - } ), - new ParagraphTooLongAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify66" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify67" ), - } ), - new SentenceLengthInTextAssessment( { - slightlyTooMany: 20, - farTooMany: 25, + } ) ); + this.addAssessment( "textSentenceLength", new SentenceLengthInTextAssessment( { + slightlyTooMany: 20, farTooMany: 25, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify48" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify49" ), - }, true ), - new TransitionWordsAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify44" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify45" ), - } ), - new PassiveVoiceAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify42" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify43" ), - } ), - new TextPresenceAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify56" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify57" ), - } ), - new SentenceBeginningsAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify5" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify65" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesCornerstoneContentAssessor, ContentAssessor ); - - -export default StorePostsAndPagesCornerstoneContentAssessor; - + }, true ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/relatedKeywordAssessor.js index 38bb3dc68ca..07906391634 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/relatedKeywordAssessor.js @@ -1,66 +1,24 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../../../assessments/seo/TextCompetingLinksAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; +import StorePostsAndPagesRelatedKeywordAssessor from "../relatedKeywordAssessor"; import ImageKeyphraseAssessment from "../../../assessments/seo/KeyphraseInImageTextAssessment.js"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StorePostsAndPagesCornerstoneRelatedKeywordAssessor class is used for the related keyword analysis for cornerstone posts and pages. */ -const StorePostsAndPagesCornerstoneRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesCornerstoneRelatedKeywordAssessor"; +export default class StorePostsAndPagesCornerstoneRelatedKeywordAssessor extends StorePostsAndPagesRelatedKeywordAssessor { + /** + * Creates a new StorePostsAndPagesCornerstoneRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesCornerstoneRelatedKeywordAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new ImageKeyphraseAssessment( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, + this.addAssessment( "imageKeyphrase", new ImageKeyphraseAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesCornerstoneRelatedKeywordAssessor, Assessor ); - -export default StorePostsAndPagesCornerstoneRelatedKeywordAssessor; + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/seoAssessor.js index 3346a81ae78..0bec567734d 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/cornerstone/seoAssessor.js @@ -1,141 +1,46 @@ -import { inherits } from "util"; - -import Assessor from "../../assessor.js"; -import SeoAssessor from "../../seoAssessor.js"; -import IntroductionKeywordAssessment from "../../../assessments/seo/IntroductionKeywordAssessment.js"; -import KeyphraseLengthAssessment from "../../../assessments/seo/KeyphraseLengthAssessment.js"; -import KeyphraseDensityAssessment from "../../../assessments/seo/KeywordDensityAssessment.js"; -import MetaDescriptionKeywordAssessment from "../../../assessments/seo/MetaDescriptionKeywordAssessment.js"; +import StorePostsAndPagesSEOAssessor from "../seoAssessor.js"; import MetaDescriptionLengthAssessment from "../../../assessments/seo/MetaDescriptionLengthAssessment.js"; -import SubheadingsKeywordAssessment from "../../../assessments/seo/SubHeadingsKeywordAssessment.js"; -import TextCompetingLinksAssessment from "../../../assessments/seo/TextCompetingLinksAssessment.js"; -import ImageKeyphraseAssessment from "../../../assessments/seo/KeyphraseInImageTextAssessment.js"; -import ImageCountAssessment from "../../../assessments/seo/ImageCountAssessment.js"; import TextLengthAssessment from "../../../assessments/seo/TextLengthAssessment.js"; -import OutboundLinksAssessment from "../../../assessments/seo/OutboundLinksAssessment.js"; -import KeyphraseInSEOTitleAssessment from "../../../assessments/seo/KeyphraseInSEOTitleAssessment.js"; -import InternalLinksAssessment from "../../../assessments/seo/InternalLinksAssessment.js"; -import PageTitleWidthAssessment from "../../../assessments/seo/PageTitleWidthAssessment.js"; import SlugKeywordAssessment from "../../../assessments/seo/UrlKeywordAssessment.js"; -import FunctionWordsInKeyphraseAssessment from "../../../assessments/seo/FunctionWordsInKeyphraseAssessment.js"; -import SingleH1Assessment from "../../../assessments/seo/SingleH1Assessment.js"; - +import ImageKeyphraseAssessment from "../../../assessments/seo/KeyphraseInImageTextAssessment"; import { createAnchorOpeningTag } from "../../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StorePostsAndPagesCornerstoneSEOAssessor class is used for the SEO analysis for cornerstone products. */ -const StorePostsAndPagesCornerstoneSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesCornerstoneSEOAssessor"; +export default class StorePostsAndPagesCornerstoneSEOAssessor extends StorePostsAndPagesSEOAssessor { + /** + * Creates a new StorePostsAndPagesCornerstoneSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesCornerstoneSEOAssessor"; - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new MetaDescriptionLengthAssessment( { - scores: { - tooLong: 3, - tooShort: 3, - }, + this.addAssessment( "metaDescriptionLength", new MetaDescriptionLengthAssessment( { + scores: { tooLong: 3, tooShort: 3 }, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new SubheadingsKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify16" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify17" ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), - } ), - new ImageKeyphraseAssessment( { - scores: { - withAltNonKeyword: 3, - withAlt: 3, - noAlt: 3, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), - } ), - new ImageCountAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify20" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify21" ), - } ), - new TextLengthAssessment( { + } ) ); + this.addAssessment( "textLength", new TextLengthAssessment( { recommendedMinimum: 900, slightlyBelowMinimum: 400, belowMinimum: 300, - - scores: { - belowMinimum: -20, - farBelowMinimum: -20, - }, + scores: { belowMinimum: -20, farBelowMinimum: -20 }, urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify58" ), urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), cornerstoneContent: true, - } ), - new OutboundLinksAssessment( { - scores: { - noLinks: 3, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify62" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify63" ), - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } ), - new InternalLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify60" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify61" ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( - { - scores: { - okay: 3, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } - ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesCornerstoneSEOAssessor, SeoAssessor ); - -export default StorePostsAndPagesCornerstoneSEOAssessor; + } ) ); + this.addAssessment( "slugKeyword", new SlugKeywordAssessment( { + scores: { okay: 3 }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), + } ) ); + this.addAssessment( "imageKeyphrase", new ImageKeyphraseAssessment( { + scores: { withAltNonKeyword: 3, withAlt: 3, noAlt: 3 }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), + } ) ); + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/relatedKeywordAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/relatedKeywordAssessor.js index 158b692c839..2f7696eac41 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/relatedKeywordAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/relatedKeywordAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; +import RelatedKeywordAssessor from "../relatedKeywordAssessor.js"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -11,51 +9,48 @@ import ImageKeyphraseAssessment from "../../assessments/seo/KeyphraseInImageText import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StorePostsAndPagesRelatedKeywordAssessor class is used for the related keyword analysis for store posts and pages. */ -const StorePostsAndPagesRelatedKeywordAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesRelatedKeywordAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - isRelatedKeyphrase: true, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new ImageKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesRelatedKeywordAssessor, Assessor ); +export default class StorePostsAndPagesRelatedKeywordAssessor extends RelatedKeywordAssessor { + /** + * Creates a new StorePostsAndPagesRelatedKeywordAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesRelatedKeywordAssessor"; -export default StorePostsAndPagesRelatedKeywordAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), + } ), + new KeyphraseLengthAssessment( { + isRelatedKeyphrase: true, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), + } ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ), + new TextCompetingLinksAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), + } ), + new ImageKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/seoAssessor.js b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/seoAssessor.js index d5eeef151b0..f2ab1eb5066 100644 --- a/packages/yoastseo/src/scoring/assessors/storePostsAndPages/seoAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/storePostsAndPages/seoAssessor.js @@ -1,6 +1,4 @@ -import { inherits } from "util"; - -import Assessor from "../assessor.js"; +import SEOAssessor from "../seoAssessor.js"; import IntroductionKeywordAssessment from "../../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../../assessments/seo/KeyphraseLengthAssessment.js"; import KeyphraseDensityAssessment from "../../assessments/seo/KeywordDensityAssessment.js"; @@ -18,97 +16,93 @@ import InternalLinksAssessment from "../../assessments/seo/InternalLinksAssessme import PageTitleWidthAssessment from "../../assessments/seo/PageTitleWidthAssessment.js"; import SlugKeywordAssessment from "../../assessments/seo/UrlKeywordAssessment.js"; import SingleH1Assessment from "../../assessments/seo/SingleH1Assessment.js"; - import { createAnchorOpeningTag } from "../../../helpers"; /** - * Creates the Assessor - * - * @param {Researcher} researcher The researcher to use for the analysis. - * @param {Object?} options The options for this assessor. - * @param {Function} options.marker The marker to pass the list of marks to. - * - * @constructor + * The StorePostsAndPagesSEOAssessor class is used for the SEO analysis for store posts and pages. */ -const StorePostsAndPagesSEOAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "storePostsAndPagesSEOAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), - } ), - new KeyphraseLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), - } ), - new KeyphraseDensityAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), - } ), - new MetaDescriptionKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), - } ), - new MetaDescriptionLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), - } ), - new SubheadingsKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify16" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify17" ), - } ), - new TextCompetingLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), - } ), - new ImageKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), - } ), - new ImageCountAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify20" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify21" ), - } ), - new TextLengthAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify58" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), - } ), - new OutboundLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify62" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify63" ), - } ), - new KeyphraseInSEOTitleAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), - } ), - new InternalLinksAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify60" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify61" ), - } ), - new PageTitleWidthAssessment( { - scores: { - widthTooShort: 9, - }, - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), - }, true ), - new SlugKeywordAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), - } ), - new FunctionWordsInKeyphraseAssessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), - } ), - new SingleH1Assessment( { - urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), - urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), - } ), - ]; -}; - -inherits( StorePostsAndPagesSEOAssessor, Assessor ); +export default class StorePostsAndPagesSEOAssessor extends SEOAssessor { + /** + * Creates a new StorePostsAndPagesSEOAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "storePostsAndPagesSEOAssessor"; -export default StorePostsAndPagesSEOAssessor; + this._assessments = [ + new IntroductionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify8" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify9" ), + } ), + new KeyphraseLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify10" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify11" ), + } ), + new KeyphraseDensityAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify12" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify13" ), + } ), + new MetaDescriptionKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify14" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify15" ), + } ), + new MetaDescriptionLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify46" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify47" ), + } ), + new SubheadingsKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify16" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify17" ), + } ), + new TextCompetingLinksAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify18" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify19" ), + } ), + new ImageKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify22" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify23" ), + } ), + new ImageCountAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify20" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify21" ), + } ), + new TextLengthAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify58" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify59" ), + } ), + new OutboundLinksAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify62" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify63" ), + } ), + new KeyphraseInSEOTitleAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify24" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify25" ), + } ), + new InternalLinksAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify60" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify61" ), + } ), + new PageTitleWidthAssessment( { + scores: { + widthTooShort: 9, + }, + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify52" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify53" ), + }, true ), + new SlugKeywordAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify26" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify27" ), + } ), + new FunctionWordsInKeyphraseAssessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify50" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify51" ), + } ), + new SingleH1Assessment( { + urlTitle: createAnchorOpeningTag( "https://yoa.st/shopify54" ), + urlCallToAction: createAnchorOpeningTag( "https://yoa.st/shopify55" ), + } ), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/assessors/taxonomyAssessor.js b/packages/yoastseo/src/scoring/assessors/taxonomyAssessor.js index c8632445e3f..5ae239318c7 100644 --- a/packages/yoastseo/src/scoring/assessors/taxonomyAssessor.js +++ b/packages/yoastseo/src/scoring/assessors/taxonomyAssessor.js @@ -1,4 +1,3 @@ -import { inherits } from "util"; import Assessor from "./assessor.js"; import IntroductionKeywordAssessment from "../assessments/seo/IntroductionKeywordAssessment.js"; import KeyphraseLengthAssessment from "../assessments/seo/KeyphraseLengthAssessment.js"; @@ -18,7 +17,7 @@ import { createAnchorOpeningTag } from "../../helpers"; * * @returns {TextLengthAssessment} The text length assessment (with taxonomy configuration) to use. */ -export const getTextLengthAssessment = function() { +export const getTextLengthAssessment = () => { // Export so it can be used in tests. return new TextLengthAssessment( { recommendedMinimum: 30, @@ -31,37 +30,36 @@ export const getTextLengthAssessment = function() { }; /** - * Creates the Assessor used for taxonomy pages. - * - * @param {Researcher} researcher The researcher used for the analysis. - * @param {Object?} options The options for this assessor. - * @constructor + * The TaxonomyAssessor is used for the assessment of terms. */ -const TaxonomyAssessor = function( researcher, options ) { - Assessor.call( this, researcher, options ); - this.type = "taxonomyAssessor"; - - this._assessments = [ - new IntroductionKeywordAssessment(), - new KeyphraseLengthAssessment(), - new KeyphraseDensityAssessment(), - new MetaDescriptionKeywordAssessment(), - new MetaDescriptionLengthAssessment(), - getTextLengthAssessment(), - new KeyphraseInSEOTitleAssessment(), - new PageTitleWidthAssessment( - { - scores: { - widthTooShort: 9, - }, - }, true - ), - new SlugKeywordAssessment(), - new FunctionWordsInKeyphrase(), - new SingleH1Assessment(), - ]; -}; - -inherits( TaxonomyAssessor, Assessor ); +export default class TaxonomyAssessor extends Assessor { + /** + * Creates a new TaxonomyAssessor instance. + * @param {Researcher} researcher The researcher to use. + * @param {Object} [options] The assessor options. + */ + constructor( researcher, options ) { + super( researcher, options ); + this.type = "taxonomyAssessor"; -export default TaxonomyAssessor; + this._assessments = [ + new IntroductionKeywordAssessment(), + new KeyphraseLengthAssessment(), + new KeyphraseDensityAssessment(), + new MetaDescriptionKeywordAssessment(), + new MetaDescriptionLengthAssessment(), + getTextLengthAssessment(), + new KeyphraseInSEOTitleAssessment(), + new PageTitleWidthAssessment( + { + scores: { + widthTooShort: 9, + }, + }, true + ), + new SlugKeywordAssessment(), + new FunctionWordsInKeyphrase(), + new SingleH1Assessment(), + ]; + } +} diff --git a/packages/yoastseo/src/scoring/renderers/AssessorPresenter.js b/packages/yoastseo/src/scoring/renderers/AssessorPresenter.js index ecc022c7d2b..8afbb46282b 100644 --- a/packages/yoastseo/src/scoring/renderers/AssessorPresenter.js +++ b/packages/yoastseo/src/scoring/renderers/AssessorPresenter.js @@ -3,353 +3,358 @@ import scoreToRating from "../interpreters/scoreToRating.js"; import createConfig from "../../config/presenter.js"; /** - * Constructs the AssessorPresenter. - * - * @param {Object} args A list of arguments to use in the presenter. - * @param {object} args.targets The HTML elements to render the output to. - * @param {string} args.targets.output The HTML element to render the individual ratings out to. - * @param {string} args.targets.overall The HTML element to render the overall rating out to. - * @param {string} args.keyword The keyword to use for checking, when calculating the overall rating. - * @param {SEOAssessor} args.assessor The Assessor object to retrieve assessment results from. - * - * @constructor + * Represents the AssessorPresenter. */ -var AssessorPresenter = function( args ) { - this.keyword = args.keyword; - this.assessor = args.assessor; - this.output = args.targets.output; - this.overall = args.targets.overall || "overallScore"; - this.presenterConfig = createConfig(); +class AssessorPresenter { + /** + * Constructs the AssessorPresenter. + * + * @param {Object} args A list of arguments to use in the presenter. + * @param {object} args.targets The HTML elements to render the output to. + * @param {string} args.targets.output The HTML element to render the individual ratings out to. + * @param {string} args.targets.overall The HTML element to render the overall rating out to. + * @param {string} args.keyword The keyword to use for checking, when calculating the overall rating. + * @param {SEOAssessor} args.assessor The Assessor object to retrieve assessment results from. + * + * @constructor + */ + constructor( args ) { + this.keyword = args.keyword; + this.assessor = args.assessor; + this.output = args.targets.output; + this.overall = args.targets.overall || "overallScore"; + this.presenterConfig = createConfig(); + + this._disableMarkerButtons = false; - this._disableMarkerButtons = false; - - this._activeMarker = false; -}; - -/** - * Sets the keyword. - * - * @param {string} keyword The keyword to use. - * @returns {void} - */ -AssessorPresenter.prototype.setKeyword = function( keyword ) { - this.keyword = keyword; -}; - -/** - * Checks whether a specific property exists in the presenter configuration. - * - * @param {string} property The property name to search for. - * @returns {boolean} Whether or not the property exists. - */ -AssessorPresenter.prototype.configHasProperty = function( property ) { - return this.presenterConfig.hasOwnProperty( property ); -}; - -/** - * Gets a fully formatted indicator object that can be used. - * - * @param {string} rating The rating to use. - * @returns {Object} An object containing the class, the screen reader text, and the full text. - */ -AssessorPresenter.prototype.getIndicator = function( rating ) { - return { - className: this.getIndicatorColorClass( rating ), - screenReaderText: this.getIndicatorScreenReaderText( rating ), - fullText: this.getIndicatorFullText( rating ), - screenReaderReadabilityText: this.getIndicatorScreenReaderReadabilityText( rating ), - }; -}; + this._activeMarker = false; + } -/** - * Gets the indicator color class from the presenter configuration, if it exists. - * - * @param {string} rating The rating to check against the config. - * @returns {string} String containing the CSS class to be used. - */ -AssessorPresenter.prototype.getIndicatorColorClass = function( rating ) { - if ( ! this.configHasProperty( rating ) ) { - return ""; + /** + * Sets the keyword. + * + * @param {string} keyword The keyword to use. + * @returns {void} + */ + setKeyword( keyword ) { + this.keyword = keyword; } - return this.presenterConfig[ rating ].className; -}; + /** + * Checks whether a specific property exists in the presenter configuration. + * + * @param {string} property The property name to search for. + * @returns {boolean} Whether or not the property exists. + */ + configHasProperty( property ) { + return this.presenterConfig.hasOwnProperty( property ); + } -/** - * Get the indicator screen reader text from the presenter configuration, if it exists. - * - * @param {string} rating The rating to check against the config. - * @returns {string} Translated string containing the screen reader text to be used. - */ -AssessorPresenter.prototype.getIndicatorScreenReaderText = function( rating ) { - if ( ! this.configHasProperty( rating ) ) { - return ""; + /** + * Gets a fully formatted indicator object that can be used. + * + * @param {string} rating The rating to use. + * @returns {Object} An object containing the class, the screen reader text, and the full text. + */ + getIndicator( rating ) { + return { + className: this.getIndicatorColorClass( rating ), + screenReaderText: this.getIndicatorScreenReaderText( rating ), + fullText: this.getIndicatorFullText( rating ), + screenReaderReadabilityText: this.getIndicatorScreenReaderReadabilityText( rating ), + }; } - return this.presenterConfig[ rating ].screenReaderText; -}; + /** + * Gets the indicator color class from the presenter configuration, if it exists. + * + * @param {string} rating The rating to check against the config. + * @returns {string} String containing the CSS class to be used. + */ + getIndicatorColorClass( rating ) { + if ( ! this.configHasProperty( rating ) ) { + return ""; + } -/** - * Get the indicator screen reader readability text from the presenter configuration, if it exists. - * - * @param {string} rating The rating to check against the config. - * @returns {string} Translated string containing the screen reader readability text to be used. - */ -AssessorPresenter.prototype.getIndicatorScreenReaderReadabilityText = function( rating ) { - if ( ! this.configHasProperty( rating ) ) { - return ""; + return this.presenterConfig[ rating ].className; } - return this.presenterConfig[ rating ].screenReaderReadabilityText; -}; + /** + * Gets the indicator screen reader text from the presenter configuration, if it exists. + * + * @param {string} rating The rating to check against the config. + * @returns {string} Translated string containing the screen reader text to be used. + */ + getIndicatorScreenReaderText( rating ) { + if ( ! this.configHasProperty( rating ) ) { + return ""; + } -/** - * Get the indicator full text from the presenter configuration, if it exists. - * - * @param {string} rating The rating to check against the config. - * @returns {string} Translated string containing the full text to be used. - */ -AssessorPresenter.prototype.getIndicatorFullText = function( rating ) { - if ( ! this.configHasProperty( rating ) ) { - return ""; + return this.presenterConfig[ rating ].screenReaderText; } - return this.presenterConfig[ rating ].fullText; -}; + /** + * Gets the indicator screen reader readability text from the presenter configuration, if it exists. + * + * @param {string} rating The rating to check against the config. + * @returns {string} Translated string containing the screen reader readability text to be used. + */ + getIndicatorScreenReaderReadabilityText( rating ) { + if ( ! this.configHasProperty( rating ) ) { + return ""; + } -/** - * Adds a rating based on the numeric score. - * - * @param {Object} result Object based on the Assessment result. Requires a score property to work. - * @returns {Object} The Assessment result object with the rating added. - */ -AssessorPresenter.prototype.resultToRating = function( result ) { - if ( ! isObject( result ) ) { - return ""; + return this.presenterConfig[ rating ].screenReaderReadabilityText; } - result.rating = scoreToRating( result.score ); - - return result; -}; + /** + * Gets the indicator full text from the presenter configuration, if it exists. + * + * @param {string} rating The rating to check against the config. + * @returns {string} Translated string containing the full text to be used. + */ + getIndicatorFullText( rating ) { + if ( ! this.configHasProperty( rating ) ) { + return ""; + } -/** - * Takes the individual assessment results, sorts and rates them. - * - * @returns {Object} Object containing all the individual ratings. - */ -AssessorPresenter.prototype.getIndividualRatings = function() { - var ratings = {}; - var validResults = this.sort( this.assessor.getValidResults() ); - var mappedResults = validResults.map( this.resultToRating ); + return this.presenterConfig[ rating ].fullText; + } - forEach( mappedResults, function( item, key ) { - ratings[ key ] = this.addRating( item ); - }.bind( this ) ); + /** + * Adds a rating based on the numeric score. + * + * @param {Object} result Object based on the Assessment result. Requires a score property to work. + * @returns {Object} The Assessment result object with the rating added. + */ + resultToRating( result ) { + if ( ! isObject( result ) ) { + return ""; + } - return ratings; -}; + result.rating = scoreToRating( result.score ); -/** - * Excludes items from the results that are present in the exclude array. - * - * @param {Array} results Array containing the items to filter through. - * @param {Array} exclude Array of results to exclude. - * @returns {Array} Array containing items that remain after exclusion. - */ -AssessorPresenter.prototype.excludeFromResults = function( results, exclude ) { - return difference( results, exclude ); -}; + return result; + } -/** - * Sorts results based on their score property and always places items considered to be unsortable, at the top. - * - * @param {Array} results Array containing the results that need to be sorted. - * @returns {Array} Array containing the sorted results. - */ -AssessorPresenter.prototype.sort = function( results ) { - var unsortables = this.getUndefinedScores( results ); - var sortables = this.excludeFromResults( results, unsortables ); + /** + * Takes the individual assessment results, sorts and rates them. + * + * @returns {Object} Object containing all the individual ratings. + */ + getIndividualRatings() { + const ratings = {}; + const validResults = this.sort( this.assessor.getValidResults() ); + const mappedResults = validResults.map( this.resultToRating ); + + forEach( mappedResults, function( item, key ) { + ratings[ key ] = this.addRating( item ); + }.bind( this ) ); + + return ratings; + } - sortables.sort( function( a, b ) { - return a.score - b.score; - } ); + /** + * Excludes items from the results that are present in the `exclude` array. + * + * @param {Array} results Array containing the items to filter through. + * @param {Array} exclude Array of results to exclude. + * @returns {Array} Array containing items that remain after exclusion. + */ + excludeFromResults( results, exclude ) { + return difference( results, exclude ); + } - return unsortables.concat( sortables ); -}; + /** + * Sorts results based on their score property and always places items considered to be non-sortable, at the top. + * + * @param {Array} results Array containing the results that need to be sorted. + * @returns {Array} Array containing the sorted results. + */ + sort( results ) { + const nonSortables = this.getUndefinedScores( results ); + const sortables = this.excludeFromResults( results, nonSortables ); + + sortables.sort( function( a, b ) { + return a.score - b.score; + } ); + + return nonSortables.concat( sortables ); + } -/** - * Returns a subset of results that have an undefined score or a score set to zero. - * - * @param {Array} results The results to filter through. - * @returns {Array} A subset of results containing items with an undefined score or where the score is zero. - */ -AssessorPresenter.prototype.getUndefinedScores = function( results ) { - return results.filter( function( result ) { - return isUndefined( result.score ) || result.score === 0; - } ); -}; + /** + * Returns a subset of results that have an undefined score or a score set to zero. + * + * @param {Array} results The results to filter through. + * @returns {Array} A subset of results containing items with an undefined score or where the score is zero. + */ + getUndefinedScores( results ) { + return results.filter( function( result ) { + return isUndefined( result.score ) || result.score === 0; + } ); + } -/** - * Creates a rating object based on the item that is being passed. - * - * @param {AssessmentResult} item The item to check and create a rating object from. - * @returns {Object} Object containing a parsed item, including a colored indicator. - */ -AssessorPresenter.prototype.addRating = function( item ) { - var indicator = this.getIndicator( item.rating ); - indicator.text = item.text; - indicator.identifier = item.getIdentifier(); + /** + * Creates a rating object based on the item that is being passed. + * + * @param {Object} item The item to check and create a rating object from. + * @returns {Object} Object containing a parsed item, including a colored indicator. + */ + addRating( item ) { + const indicator = this.getIndicator( item.rating ); + indicator.text = item.text; + indicator.identifier = item.getIdentifier(); + + if ( item.hasMarker() ) { + indicator.marker = item.getMarker(); + } - if ( item.hasMarker() ) { - indicator.marker = item.getMarker(); + return indicator; } - return indicator; -}; + /** + * Calculates the overall rating score based on the overall score. + * + * @param {Number} overallScore The overall score to use in the calculation. + * @returns {Object} The rating based on the score. + */ + getOverallRating( overallScore ) { + let rating = 0; + + if ( this.keyword === "" ) { + return this.resultToRating( { score: rating } ); + } -/** - * Calculates the overall rating score based on the overall score. - * - * @param {Number} overallScore The overall score to use in the calculation. - * @returns {Object} The rating based on the score. - */ -AssessorPresenter.prototype.getOverallRating = function( overallScore ) { - var rating = 0; + if ( isNumber( overallScore ) ) { + rating = ( overallScore / 10 ); + } - if ( this.keyword === "" ) { return this.resultToRating( { score: rating } ); } - if ( isNumber( overallScore ) ) { - rating = ( overallScore / 10 ); - } + /** + * Marks with a given marker. This will set the active marker to the correct value. + * + * @param {string} identifier The identifier for the assessment/marker. + * @param {Function} marker The marker function. + * @returns {void} + */ + markAssessment( identifier, marker ) { + if ( this._activeMarker === identifier ) { + this.removeAllMarks(); + this._activeMarker = false; + } else { + marker(); + this._activeMarker = identifier; + } - return this.resultToRating( { score: rating } ); -}; + this.render(); + } -/** - * Mark with a given marker. This will set the active marker to the correct value. - * - * @param {string} identifier The identifier for the assessment/marker. - * @param {Function} marker The marker function. - * @returns {void} - */ -AssessorPresenter.prototype.markAssessment = function( identifier, marker ) { - if ( this._activeMarker === identifier ) { - this.removeAllMarks(); + /** + * Disables the currently active marker in the UI. + * + * @returns {void} + */ + disableMarker() { this._activeMarker = false; - } else { - marker(); - this._activeMarker = identifier; + this.render(); } - this.render(); -}; + /** + * Disables the marker buttons. + * + * @returns {void} + */ + disableMarkerButtons() { + this._disableMarkerButtons = true; + this.render(); + } -/** - * Disables the currently active marker in the UI. - * - * @returns {void} - */ -AssessorPresenter.prototype.disableMarker = function() { - this._activeMarker = false; - this.render(); -}; + /** + * Enables the marker buttons. + * + * @returns {void} + */ + enableMarkerButtons() { + this._disableMarkerButtons = false; + this.render(); + } -/** - * Disables the marker buttons. - * - * @returns {void} - */ -AssessorPresenter.prototype.disableMarkerButtons = function() { - this._disableMarkerButtons = true; - this.render(); -}; + /** + * Adds an event listener for the marker button + * + * @param {string} identifier The identifier for the assessment the marker belongs to. + * @param {Function} marker The marker function that can mark the assessment in the text. + * @returns {void} + */ + addMarkerEventHandler( identifier, marker ) { + const container = document.getElementById( this.output ); + const markButton = container.getElementsByClassName( "js-assessment-results__mark-" + identifier )[ 0 ]; + + markButton.addEventListener( "click", this.markAssessment.bind( this, identifier, marker ) ); + } -/** - * Enables the marker buttons. - * - * @returns {void} - */ -AssessorPresenter.prototype.enableMarkerButtons = function() { - this._disableMarkerButtons = false; - this.render(); -}; + /** + * Renders out both the individual and the overall ratings. + * + * @returns {void} + */ + render() { + this.renderIndividualRatings(); + this.renderOverallRating(); + } -/** - * Adds an event listener for the marker button - * - * @param {string} identifier The identifier for the assessment the marker belongs to. - * @param {Function} marker The marker function that can mark the assessment in the text. - * @returns {void} - */ -AssessorPresenter.prototype.addMarkerEventHandler = function( identifier, marker ) { - var container = document.getElementById( this.output ); - var markButton = container.getElementsByClassName( "js-assessment-results__mark-" + identifier )[ 0 ]; + /** + * Adds event handlers to the mark buttons. + * + * @param {Object} scores The list of rendered scores. + * + * @returns {void} + */ + bindMarkButtons( scores ) { + // Make sure the button works for every score with a marker. + forEach( scores, function( score ) { + if ( score.hasOwnProperty( "marker" ) ) { + this.addMarkerEventHandler( score.identifier, score.marker ); + } + }.bind( this ) ); + } - markButton.addEventListener( "click", this.markAssessment.bind( this, identifier, marker ) ); -}; + /** + * Removes all marks currently on the text. + * + * @returns {void} + */ + removeAllMarks() { + const marker = this.assessor.getSpecificMarker(); -/** - * Renders out both the individual and the overall ratings. - * - * @returns {void} - */ -AssessorPresenter.prototype.render = function() { - this.renderIndividualRatings(); - this.renderOverallRating(); -}; + marker( this.assessor.getPaper(), [] ); + } -/** - * Adds event handlers to the mark buttons - * - * @param {Array} scores The list of rendered scores. - * - * @returns {void} - */ -AssessorPresenter.prototype.bindMarkButtons = function( scores ) { - // Make sure the button works for every score with a marker. - forEach( scores, function( score ) { - if ( score.hasOwnProperty( "marker" ) ) { - this.addMarkerEventHandler( score.identifier, score.marker ); + /** + * Renders out the individual ratings. + * Here, this method is set to noop. In `post-scraper.js` and `term-scraper.js` where this method is called, it is overridden with noop as well. + * + * @returns {void} + */ + renderIndividualRatings() {} + + /** + * Renders out the overall rating. + * + * @returns {void} + */ + renderOverallRating() { + const overallRating = this.getOverallRating( this.assessor.calculateOverallScore() ); + const overallRatingElement = document.getElementById( this.overall ); + + if ( ! overallRatingElement ) { + return; } - }.bind( this ) ); -}; -/** - * Removes all marks currently on the text - * - * @returns {void} - */ -AssessorPresenter.prototype.removeAllMarks = function() { - var marker = this.assessor.getSpecificMarker(); - - marker( this.assessor.getPaper(), [] ); -}; - -/** - * Renders out the individual ratings. - * Here, this method is set to noop. In `post-scraper.js` and `term-scraper.js` where this method is called, it is overridden with noop as well. - * - * @returns {void} - */ -AssessorPresenter.prototype.renderIndividualRatings = function() {}; - -/** - * Renders out the overall rating. - * - * @returns {void} - */ -AssessorPresenter.prototype.renderOverallRating = function() { - var overallRating = this.getOverallRating( this.assessor.calculateOverallScore() ); - var overallRatingElement = document.getElementById( this.overall ); - - if ( ! overallRatingElement ) { - return; + overallRatingElement.className = "overallScore " + this.getIndicatorColorClass( overallRating.rating ); } - - overallRatingElement.className = "overallScore " + this.getIndicatorColorClass( overallRating.rating ); -}; +} export default AssessorPresenter; diff --git a/packages/yoastseo/src/values/AssessmentResult.js b/packages/yoastseo/src/values/AssessmentResult.js index 9a0530552a2..8a250e04c9d 100644 --- a/packages/yoastseo/src/values/AssessmentResult.js +++ b/packages/yoastseo/src/values/AssessmentResult.js @@ -7,334 +7,343 @@ import Mark from "./Mark"; * * @returns {Array} A list of empty marks. */ -var emptyMarker = function() { - return []; -}; +const emptyMarker = () => []; /** - * Construct the AssessmentResult value object. - * - * @param {Object} [values] The values for this assessment result. - * - * @constructor + * Represents the assessment result. */ -var AssessmentResult = function( values ) { - this._hasScore = false; - this._identifier = ""; - this._hasAIFixes = false; - this._hasMarks = false; - this._hasJumps = false; - this._hasEditFieldName = false; - this._marker = emptyMarker; - this._hasBetaBadge = false; - this.score = 0; - this.text = ""; - this.marks = []; - this.editFieldName = ""; - - if ( isUndefined( values ) ) { - values = {}; +class AssessmentResult { + /** + * Constructs the AssessmentResult value object. + * + * @param {Object} [values] The values for this assessment result. + * @param {number} [values.score] The score for this assessment result. + * @param {string} [values.text] The text for this assessment result. This is the text that can be used as a feedback message associated with the score. + * @param {array} [values.marks] The marks for this assessment result. + * @param {boolean} [values._hasBetaBadge] Whether this result has a beta badge. + * @param {boolean} [values._hasJumps] Whether this result causes a jump to a different field. + * @param {string} [values.editFieldName] The edit field name for this assessment result. + * @param {boolean} [values._hasAIFixes] Whether this result has AI fixes. + * @constructor + * @returns {void} + */ + constructor( values ) { + this._hasScore = false; + this._identifier = ""; + this._hasAIFixes = false; + this._hasMarks = false; + this._hasJumps = false; + this._hasEditFieldName = false; + this._marker = emptyMarker; + this._hasBetaBadge = false; + this.score = 0; + this.text = ""; + this.marks = []; + this.editFieldName = ""; + + if ( isUndefined( values ) ) { + values = {}; + } + + if ( ! isUndefined( values.score ) ) { + this.setScore( values.score ); + } + + if ( ! isUndefined( values.text ) ) { + this.setText( values.text ); + } + + if ( ! isUndefined( values.marks ) ) { + this.setMarks( values.marks ); + } + + if ( ! isUndefined( values._hasBetaBadge ) ) { + this.setHasBetaBadge( values._hasBetaBadge ); + } + + if ( ! isUndefined( values._hasJumps ) ) { + this.setHasJumps( values._hasJumps ); + } + + if ( ! isUndefined( values.editFieldName ) ) { + this.setEditFieldName( values.editFieldName ); + } + + if ( ! isUndefined( values._hasAIFixes ) ) { + this.setHasAIFixes( values._hasAIFixes ); + } } - if ( ! isUndefined( values.score ) ) { - this.setScore( values.score ); + /** + * Checks if a score is available. + * @returns {boolean} Whether or not a score is available. + */ + hasScore() { + return this._hasScore; } - if ( ! isUndefined( values.text ) ) { - this.setText( values.text ); + /** + * Gets the available score. + * @returns {number} The score associated with the AssessmentResult. + */ + getScore() { + return this.score; } - if ( ! isUndefined( values.marks ) ) { - this.setMarks( values.marks ); + /** + * Sets the score for the assessment. + * @param {number} score The score to be used for the score property. + * @returns {void} + */ + setScore( score ) { + if ( isNumber( score ) ) { + this.score = score; + this._hasScore = true; + } } - if ( ! isUndefined( values._hasBetaBadge ) ) { - this.setHasBetaBadge( values._hasBetaBadge ); + /** + * Checks if a text for the assessment result is available. + * @returns {boolean} Whether or not a text is available. + */ + hasText() { + return this.text !== ""; } - if ( ! isUndefined( values._hasJumps ) ) { - this.setHasJumps( values._hasJumps ); + /** + * Gets the available text for the assessment result. + * @returns {string} The text associated with the AssessmentResult. + */ + getText() { + return this.text; } - if ( ! isUndefined( values.editFieldName ) ) { - this.setEditFieldName( values.editFieldName ); + /** + * Sets the text for the assessment. + * @param {string} text The text to be used for the text property. + * @returns {void} + */ + setText( text ) { + if ( isUndefined( text ) ) { + text = ""; + } + + this.text = text; } - if ( ! isUndefined( values._hasAIFixes ) ) { - this.setHasAIFixes( values._hasAIFixes ); + /** + * Gets the available marks. + * + * @returns {array} The marks associated with the AssessmentResult. + */ + getMarks() { + return this.marks; } -}; - -/** - * Check if a score is available. - * @returns {boolean} Whether or not a score is available. - */ -AssessmentResult.prototype.hasScore = function() { - return this._hasScore; -}; - -/** - * Get the available score - * @returns {number} The score associated with the AssessmentResult. - */ -AssessmentResult.prototype.getScore = function() { - return this.score; -}; -/** - * Set the score for the assessment. - * @param {number} score The score to be used for the score property - * @returns {void} - */ -AssessmentResult.prototype.setScore = function( score ) { - if ( isNumber( score ) ) { - this.score = score; - this._hasScore = true; + /** + * Sets the marks for the assessment. + * + * @param {array} marks The marks to be used for the marks property. + * + * @returns {void} + */ + setMarks( marks ) { + if ( isArray( marks ) ) { + this.marks = marks; + this._hasMarks = marks.length > 0; + } } -}; - -/** - * Check if a text is available. - * @returns {boolean} Whether or not a text is available. - */ -AssessmentResult.prototype.hasText = function() { - return this.text !== ""; -}; - -/** - * Get the available text - * @returns {string} The text associated with the AssessmentResult. - */ -AssessmentResult.prototype.getText = function() { - return this.text; -}; -/** - * Set the text for the assessment. - * @param {string} text The text to be used for the text property - * @returns {void} - */ -AssessmentResult.prototype.setText = function( text ) { - if ( isUndefined( text ) ) { - text = ""; + /** + * Sets the identifier. + * + * @param {string} identifier An alphanumeric identifier for this result. + * @returns {void} + */ + setIdentifier( identifier ) { + this._identifier = identifier; } - this.text = text; -}; - -/** - * Gets the available marks. - * - * @returns {array} The marks associated with the AssessmentResult. - */ -AssessmentResult.prototype.getMarks = function() { - return this.marks; -}; - -/** - * Sets the marks for the assessment. - * - * @param {array} marks The marks to be used for the marks property - * - * @returns {void} - */ -AssessmentResult.prototype.setMarks = function( marks ) { - if ( isArray( marks ) ) { - this.marks = marks; - this._hasMarks = marks.length > 0; + /** + * Gets the identifier. + * + * @returns {string} An alphanumeric identifier for this result. + */ + getIdentifier() { + return this._identifier; } -}; - -/** - * Sets the identifier - * - * @param {string} identifier An alphanumeric identifier for this result. - * @returns {void} - */ -AssessmentResult.prototype.setIdentifier = function( identifier ) { - this._identifier = identifier; -}; - -/** - * Gets the identifier - * - * @returns {string} An alphanumeric identifier for this result. - */ -AssessmentResult.prototype.getIdentifier = function() { - return this._identifier; -}; -/** - * Sets the marker, a pure function that can return the marks for a given Paper - * - * @param {Function} marker The marker to set. - * @returns {void} - */ -AssessmentResult.prototype.setMarker = function( marker ) { - this._marker = marker; -}; + /** + * Sets the marker, a pure function that can return the marks for a given Paper. + * + * @param {Function} marker The marker to set. + * @returns {void} + */ + setMarker( marker ) { + this._marker = marker; + } -/** - * Returns whether or not this result has a marker that can be used to mark for a given Paper - * - * @returns {boolean} Whether or this result has a marker. - */ -AssessmentResult.prototype.hasMarker = function() { - return this._hasMarks && this._marker !== this.emptyMarker; -}; + /** + * Returns whether this result has a marker that can be used to mark for a given Paper. + * + * @returns {boolean} Whether this result has a marker. + */ + hasMarker() { + return this._hasMarks && this._marker !== emptyMarker; + } -/** - * Gets the marker, a pure function that can return the marks for a given Paper - * - * @returns {Function} The marker. - */ -AssessmentResult.prototype.getMarker = function() { - return this._marker; -}; + /** + * Gets the marker, a pure function that can return the marks for a given Paper. + * + * @returns {Function} The marker. + */ + getMarker() { + return this._marker; + } -/** - * Sets the value of _hasMarks to determine if there is something to mark. - * - * @param {boolean} hasMarks Is there something to mark. - * @returns {void} - */ -AssessmentResult.prototype.setHasMarks = function( hasMarks ) { - this._hasMarks = hasMarks; -}; + /** + * Sets the value of _hasMarks to determine if there is something to mark. + * + * @param {boolean} hasMarks Is there something to mark. + * @returns {void} + */ + setHasMarks( hasMarks ) { + this._hasMarks = hasMarks; + } -/** - * Returns the value of _hasMarks to determine if there is something to mark. - * - * @returns {boolean} Is there something to mark. - */ -AssessmentResult.prototype.hasMarks = function() { - return this._hasMarks; -}; + /** + * Returns the value of _hasMarks to determine if there is something to mark. + * + * @returns {boolean} Is there something to mark. + */ + hasMarks() { + return this._hasMarks; + } -/** - * Sets the value of _hasBetaBadge to determine if the result has a beta badge. - * - * @param {boolean} hasBetaBadge Whether this result has a beta badge. - * @returns {void} - */ -AssessmentResult.prototype.setHasBetaBadge = function( hasBetaBadge ) { - this._hasBetaBadge = hasBetaBadge; -}; + /** + * Sets the value of _hasBetaBadge to determine if the result has a beta badge. + * + * @param {boolean} hasBetaBadge Whether this result has a beta badge. + * @returns {void} + */ + setHasBetaBadge( hasBetaBadge ) { + this._hasBetaBadge = hasBetaBadge; + } -/** - * Returns the value of _hasBetaBadge to determine if the result has a beta badge. - * - * @returns {bool} Whether this result has a beta badge. - */ -AssessmentResult.prototype.hasBetaBadge = function() { - return this._hasBetaBadge; -}; + /** + * Returns the value of _hasBetaBadge to determine if the result has a beta badge. + * + * @returns {bool} Whether this result has a beta badge. + */ + hasBetaBadge() { + return this._hasBetaBadge; + } -/** - * Sets the value of _hasJumps to determine whether it's needed to jump to a different field. - * - * @param {boolean} hasJumps Whether this result causes a jump to a different field. - * @returns {void} - */ -AssessmentResult.prototype.setHasJumps = function( hasJumps ) { - this._hasJumps = hasJumps; -}; + /** + * Sets the value of _hasJumps to determine whether it's needed to jump to a different field. + * + * @param {boolean} hasJumps Whether this result causes a jump to a different field. + * @returns {void} + */ + setHasJumps( hasJumps ) { + this._hasJumps = hasJumps; + } -/** - * Returns the value of _hasJumps to determine whether it's needed to jump to a different field. - * - * @returns {bool} Whether this result causes a jump to a different field. - */ -AssessmentResult.prototype.hasJumps = function() { - return this._hasJumps; -}; + /** + * Returns the value of _hasJumps to determine whether it's needed to jump to a different field. + * + * @returns {bool} Whether this result causes a jump to a different field. + */ + hasJumps() { + return this._hasJumps; + } -/** - * Check if an edit field name is available. - * @returns {boolean} Whether or not an edit field name is available. - */ -AssessmentResult.prototype.hasEditFieldName = function() { - return this._hasEditFieldName; -}; + /** + * Check if an edit field name is available. + * @returns {boolean} Whether or not an edit field name is available. + */ + hasEditFieldName() { + return this._hasEditFieldName; + } -/** - * Get the edit field name. - * @returns {string} The edit field name associated with the AssessmentResult. - */ -AssessmentResult.prototype.getEditFieldName = function() { - return this.editFieldName; -}; + /** + * Gets the edit field name. + * @returns {string} The edit field name associated with the AssessmentResult. + */ + getEditFieldName() { + return this.editFieldName; + } -/** - * Set the edit field name to be used to create the aria label for an edit button. - * @param {string} editFieldName The string to be used for the string property - * @returns {void} - */ -AssessmentResult.prototype.setEditFieldName = function( editFieldName ) { - if ( editFieldName !== "" ) { - this.editFieldName = editFieldName; - this._hasEditFieldName = true; + /** + * Sets the edit field name to be used to create the aria label for an edit button. + * @param {string} editFieldName The string to be used for the string property + * @returns {void} + */ + setEditFieldName( editFieldName ) { + if ( editFieldName !== "" ) { + this.editFieldName = editFieldName; + this._hasEditFieldName = true; + } } -}; -/** - * Sets the value of _hasAIFixes to determine if the result has AI fixes. - * - * @param {boolean} hasAIFixes Whether this result has AI fixes. - * @returns {void} - */ -AssessmentResult.prototype.setHasAIFixes = function( hasAIFixes ) { - this._hasAIFixes = hasAIFixes; -}; + /** + * Sets the value of _hasAIFixes to determine if the result has AI fixes. + * + * @param {boolean} hasAIFixes Whether this result has AI fixes. + * @returns {void} + */ + setHasAIFixes( hasAIFixes ) { + this._hasAIFixes = hasAIFixes; + } -/** - * Returns the value of _hasAIFixes to determine if the result has AI fixes. - * - * @returns {bool} Whether this result has AI fixes. - */ -AssessmentResult.prototype.hasAIFixes = function() { - return this._hasAIFixes; -}; + /** + * Returns the value of _hasAIFixes to determine if the result has AI fixes. + * + * @returns {bool} Whether this result has AI fixes. + */ + hasAIFixes() { + return this._hasAIFixes; + } -/** - * Serializes the AssessmentResult instance to an object. - * - * @returns {Object} The serialized AssessmentResult. - */ -AssessmentResult.prototype.serialize = function() { - return { - _parseClass: "AssessmentResult", - identifier: this._identifier, - score: this.score, - text: this.text, - marks: this.marks.map( mark => mark.serialize() ), - _hasBetaBadge: this._hasBetaBadge, - _hasJumps: this._hasJumps, - _hasAIFixes: this._hasAIFixes, - editFieldName: this.editFieldName, - }; -}; + /** + * Serializes the AssessmentResult instance to an object. + * + * @returns {Object} The serialized AssessmentResult. + */ + serialize() { + return { + _parseClass: "AssessmentResult", + identifier: this._identifier, + score: this.score, + text: this.text, + marks: this.marks.map( mark => mark.serialize() ), + _hasBetaBadge: this._hasBetaBadge, + _hasJumps: this._hasJumps, + _hasAIFixes: this._hasAIFixes, + editFieldName: this.editFieldName, + }; + } -/** - * Parses the object to an AssessmentResult. - * - * @param {Object} serialized The serialized object. - * - * @returns {AssessmentResult} The parsed AssessmentResult. - */ -AssessmentResult.parse = function( serialized ) { - const result = new AssessmentResult( { - text: serialized.text, - score: serialized.score, - marks: serialized.marks.map( mark => Mark.parse( mark ) ), - _hasBetaBadge: serialized._hasBetaBadge, - _hasJumps: serialized._hasJumps, - _hasAIFixes: serialized._hasAIFixes, - editFieldName: serialized.editFieldName, - } ); - result.setIdentifier( serialized.identifier ); - - return result; -}; + /** + * Parses the object to an AssessmentResult. + * + * @param {Object} serialized The serialized object. + * + * @returns {AssessmentResult} The parsed AssessmentResult. + */ + static parse( serialized ) { + const result = new AssessmentResult( { + text: serialized.text, + score: serialized.score, + marks: serialized.marks.map( mark => Mark.parse( mark ) ), + _hasBetaBadge: serialized._hasBetaBadge, + _hasJumps: serialized._hasJumps, + _hasAIFixes: serialized._hasAIFixes, editFieldName: serialized.editFieldName, + } ); + result.setIdentifier( serialized.identifier ); + + return result; + } +} export default AssessmentResult; diff --git a/packages/yoastseo/src/values/Mark.js b/packages/yoastseo/src/values/Mark.js index 332ec3393a4..7a47717f4d0 100644 --- a/packages/yoastseo/src/values/Mark.js +++ b/packages/yoastseo/src/values/Mark.js @@ -1,264 +1,271 @@ import { defaults, isUndefined } from "lodash"; -/** - * Represents a place where highlighting should be applied. - * We allow both replacement-based highlighting (through providing `original`, `marked`, and potentially `fieldsToMark`) and - * position-based highlighting (through providing a `position`). - * - * @param {Object} properties The properties of this Mark. - * - * @param {string?} properties.original The original text that should be marked. - * @param {string?} properties.marked The new text including marks. - * @param {array?} properties.fieldsToMark The array that specifies which text section(s) to mark. - * - * @param {SourceCodeRange?} properties.position The position object: a range in the source code. - * - * @constructor - */ -function Mark( properties ) { - defaults( properties, { original: "", marked: "", fieldsToMark: [] } ); - this._properties = properties; - this.isValid(); -} +const defaultProperties = { original: "", marked: "", fieldsToMark: [] }; /** - * Returns the original text. - * - * @returns {string} The original text. - */ -Mark.prototype.getOriginal = function() { - return this._properties.original; -}; - -/** - * Returns the marked text. - * - * @returns {string} The replaced text. + * Represents a place where highlighting should be applied. */ -Mark.prototype.getMarked = function() { - return this._properties.marked; -}; +class Mark { + /** + * Represents a place where highlighting should be applied. + * We allow both replacement-based highlighting (through providing `original`, `marked`, and potentially `fieldsToMark`) and + * position-based highlighting (through providing a `position`). + * + * @param {Object} [properties] The properties of this Mark. + * + * @param {string} [properties.original] The original text that should be marked. + * @param {string} [properties.marked] The new text including marks. + * @param {array} [properties.fieldsToMark] The array that specifies which text section(s) to mark, e.g. "heading". + * + * @param {SourceCodeRange} [properties.position] The position object: a range in the source code. + */ + constructor( properties ) { + properties = properties || {}; + defaults( properties, defaultProperties ); + this._properties = properties; + this.isValid(); + } -/** - * Returns the fields to mark. - * - * @returns {array} The fields to mark. - */ -Mark.prototype.getFieldsToMark = function() { - return this._properties.fieldsToMark; -}; + /** + * Returns the original text. + * + * @returns {string} The original text. + */ + getOriginal() { + return this._properties.original; + } -/** - * Returns the position information. - * - * @returns {number} The position information. - */ -Mark.prototype.getPosition = function() { - return this._properties.position; -}; + /** + * Returns the marked text. + * + * @returns {string} The replaced text. + */ + getMarked() { + return this._properties.marked; + } -/** - * Returns the start position. - * - * @returns {number} The start position. - */ -Mark.prototype.getPositionStart = function() { - return this._properties.position && this._properties.position.startOffset; -}; + /** + * Returns the fields to mark. + * + * @returns {array} The fields to mark. + */ + getFieldsToMark() { + return this._properties.fieldsToMark; + } -/** - * Returns the end position. - * - * @returns {number} The end position. - */ -Mark.prototype.getPositionEnd = function() { - return this._properties.position && this._properties.position.endOffset; -}; + /** + * Returns the position information. + * + * @returns {Object} The position information. + */ + getPosition() { + return this._properties.position; + } -/** - * Sets the start position. - * - * @param {number} positionStart The new start position. - * - * @returns {void} - */ -Mark.prototype.setPositionStart = function( positionStart ) { - this._properties.position.startOffset = positionStart; -}; + /** + * Returns the start position. + * + * @returns {number} The start position. + */ + getPositionStart() { + return this._properties.position && this._properties.position.startOffset; + } -/** - * Sets the end position. - * - * @param {number} positionEnd The new end position. - * - * @returns {void} - */ -Mark.prototype.setPositionEnd = function( positionEnd ) { - this._properties.position.endOffset = positionEnd; -}; + /** + * Returns the end position. + * + * @returns {number} The end position. + */ + getPositionEnd() { + return this._properties.position && this._properties.position.endOffset; + } -/** - * Returns the start position of a block. - * - * @param {number} startOffsetBlock The block start offset. - * - * @returns {number} The start position of a block. - */ -Mark.prototype.setBlockPositionStart = function( startOffsetBlock ) { - this._properties.position.startOffsetBlock = startOffsetBlock; -}; + /** + * Sets the start position. + * + * @param {number} positionStart The new start position. + * + * @returns {void} + */ + setPositionStart( positionStart ) { + this._properties.position.startOffset = positionStart; + } -/** - * Returns the end position of a block. - * - * @param {number} endOffsetBlock The block end offset. - * - * @returns {number} The end position of a block. - */ -Mark.prototype.setBlockPositionEnd = function( endOffsetBlock ) { - this._properties.position.endOffsetBlock = endOffsetBlock; -}; + /** + * Sets the end position. + * + * @param {number} positionEnd The new end position. + * + * @returns {void} + */ + setPositionEnd( positionEnd ) { + this._properties.position.endOffset = positionEnd; + } -/** - * Gets the block client id. - * - * @returns {string} The block client id. - */ -Mark.prototype.getBlockClientId = function() { - return this._properties.position && this._properties.position.clientId; -}; + /** + * Returns the start position of a block. + * + * @param {number} startOffsetBlock The block start offset. + * + * @returns {number} The start position of a block. + */ + setBlockPositionStart( startOffsetBlock ) { + this._properties.position.startOffsetBlock = startOffsetBlock; + } -/** - * Gets the block attribute id. - * - * @returns {string} The block attribute id. - */ -Mark.prototype.getBlockAttributeId = function() { - return this._properties.position && this._properties.position.attributeId; -}; + /** + * Returns the end position of a block. + * + * @param {number} endOffsetBlock The block end offset. + * + * @returns {number} The end position of a block. + */ + setBlockPositionEnd( endOffsetBlock ) { + this._properties.position.endOffsetBlock = endOffsetBlock; + } + /** + * Gets the block client id. + * + * @returns {string} The block client id. + */ + getBlockClientId() { + return this._properties.position && this._properties.position.clientId; + } -/** - * Checks if the mark object is intended for the first section of a Yoast sub-block. - * This method will be used only for Yoast blocks where each block consists of sub-blocks - * with two sections. - * - * @returns {boolean} Whether the mark object is intended for the first section of a Yoast sub-block. - */ -Mark.prototype.isMarkForFirstBlockSection = function() { - return this._properties.position && this._properties.position.isFirstSection; -}; + /** + * Gets the block attribute id. + * + * @returns {string} The block attribute id. + */ + getBlockAttributeId() { + return this._properties.position && this._properties.position.attributeId; + } -/** - * Returns the start position inside block. - * - * @returns {number} The start position inside block if the mark position information, undefined otherwise. - */ -Mark.prototype.getBlockPositionStart = function() { - return this._properties.position && this._properties.position.startOffsetBlock; -}; -/** - * Returns the end position inside block if the mark has position information, undefined otherwise. - * - * @returns {number} The end position inside block. - */ -Mark.prototype.getBlockPositionEnd = function() { - return this._properties.position && this._properties.position.endOffsetBlock; -}; + /** + * Checks if the mark object is intended for the first section of a Yoast sub-block. + * This method will be used only for Yoast blocks where each block consists of sub-blocks + * with two sections. + * + * @returns {boolean} Whether the mark object is intended for the first section of a Yoast sub-block. + */ + isMarkForFirstBlockSection() { + return this._properties.position && this._properties.position.isFirstSection; + } -/** - * Applies this mark to the given text with replacement-based highlighting. - * - * @param {string} text The original text without the mark applied. - * @returns {string} A new text with the mark applied to it. - */ -Mark.prototype.applyWithReplace = function( text ) { - // (=^ ◡ ^=) Cute method to replace everything in a string without using regex. - return text.split( this._properties.original ).join( this._properties.marked ); -}; + /** + * Returns the start position inside block. + * + * @returns {number} The start position inside the block if the mark has position information, undefined otherwise. + */ + getBlockPositionStart() { + return this._properties.position && this._properties.position.startOffsetBlock; + } -/** - * Applies this mark to the given text with position-based highlighting. - * - * @param {string} text The original text without the mark applied. - * @returns {string} A new text with the mark applied to it. - */ -Mark.prototype.applyWithPosition = function( text ) { - const markStart = ""; - const markEnd = ""; + /** + * Returns the end position inside block if the mark has position information, undefined otherwise. + * + * @returns {number} The end position inside block. + */ + getBlockPositionEnd() { + return this._properties.position && this._properties.position.endOffsetBlock; + } - const newPositionEnd = this.getPositionEnd() + markStart.length; + /** + * Applies this mark to the given text with replacement-based highlighting. + * + * @param {string} text The original text without the mark applied. + * @returns {string} A new text with the mark applied to it. + */ + applyWithReplace( text ) { + // (=^ ◡ ^=) Cute method to replace everything in a string without using regex. + return text.split( this._properties.original ).join( this._properties.marked ); + } - text = text.substring( 0, this.getPositionStart() ) + markStart + text.substring( this.getPositionStart() ); - text = text.substring( 0, newPositionEnd ) + markEnd + text.substring( newPositionEnd ); + /** + * Applies this mark to the given text with position-based highlighting. + * + * @param {string} text The original text without the mark applied. + * @returns {string} A new text with the mark applied to it. + */ + applyWithPosition( text ) { + const markStart = ""; + const markEnd = ""; - return text; -}; + const newPositionEnd = this.getPositionEnd() + markStart.length; -/** - * Serializes the Mark instance to an object. - * - * @returns {Object} The serialized Mark. - */ -Mark.prototype.serialize = function() { - return { - _parseClass: "Mark", - ...this._properties, - }; -}; + text = text.substring( 0, this.getPositionStart() ) + markStart + text.substring( this.getPositionStart() ); + text = text.substring( 0, newPositionEnd ) + markEnd + text.substring( newPositionEnd ); -/** - * Checks if the mark object is valid for position-based highlighting. - * @returns {void} - */ -// eslint-disable-next-line complexity -Mark.prototype.isValid = function() { - if ( ! isUndefined( this.getPositionStart() ) && this.getPositionStart() < 0 ) { - throw new RangeError( "positionStart should be larger or equal than 0." ); + return text; } - if ( ! isUndefined( this.getPositionEnd() ) && this.getPositionEnd() <= 0 ) { - throw new RangeError( "positionEnd should be larger than 0." ); - } - if ( ! isUndefined( this.getPositionStart() ) && ! isUndefined( this.getPositionEnd() ) && - this.getPositionStart() >= this.getPositionEnd() ) { - throw new RangeError( "The positionStart should be smaller than the positionEnd." ); + + /** + * Serializes the Mark instance to an object. + * + * @returns {Object} The serialized Mark. + */ + serialize() { + return { + _parseClass: "Mark", + ...this._properties, + }; } - if ( isUndefined( this.getPositionStart() ) && ! isUndefined( this.getPositionEnd() ) || - isUndefined( this.getPositionEnd() ) && ! isUndefined( this.getPositionStart() ) ) { - throw new Error( "A mark object should either have start and end defined or start and end undefined." ); + + /* eslint-disable complexity */ + /** + * Checks if the mark object is valid for position-based highlighting. + * @returns {void} + */ + isValid() { + if ( ! isUndefined( this.getPositionStart() ) && this.getPositionStart() < 0 ) { + throw new RangeError( "positionStart should be larger or equal than 0." ); + } + if ( ! isUndefined( this.getPositionEnd() ) && this.getPositionEnd() <= 0 ) { + throw new RangeError( "positionEnd should be larger than 0." ); + } + if ( ! isUndefined( this.getPositionStart() ) && ! isUndefined( this.getPositionEnd() ) && + this.getPositionStart() >= this.getPositionEnd() ) { + throw new RangeError( "The positionStart should be smaller than the positionEnd." ); + } + if ( isUndefined( this.getPositionStart() ) && ! isUndefined( this.getPositionEnd() ) || + isUndefined( this.getPositionEnd() ) && ! isUndefined( this.getPositionStart() ) ) { + throw new Error( "A mark object should either have start and end defined or start and end undefined." ); + } } -}; + /* eslint-enable complexity */ -/** - * Checks if a mark has position information available. - * @returns {boolean} Returns true if the Mark object has position information, false otherwise. - */ -Mark.prototype.hasPosition = function() { - return ! isUndefined( this.getPositionStart() ); -}; + /** + * Checks if a mark has position information available. + * @returns {boolean} Returns true if the Mark object has position information, false otherwise. + */ + hasPosition() { + return ! isUndefined( this.getPositionStart() ); + } -/** - * Checks if a mark has block position information available. - * A block has position information if the block start offset is available. - * - * @returns {boolean} Returns true if the Mark object has block position information, false otherwise. - */ -Mark.prototype.hasBlockPosition = function() { - return ! isUndefined( this.getBlockPositionStart() ); -}; + /** + * Checks if a mark has block position information available. + * A block has position information if the block start offset is available. + * + * @returns {boolean} Returns true if the Mark object has block position information, false otherwise. + */ + hasBlockPosition() { + return ! isUndefined( this.getBlockPositionStart() ); + } -/** - * Parses the object to a Mark. - * - * @param {Object} serialized The serialized object. - * - * @returns {Mark} The parsed Mark. - */ -Mark.parse = function( serialized ) { - delete serialized._parseClass; - return new Mark( serialized ); -}; + /** + * Parses the object to a Mark. + * + * @param {Object} serialized The serialized object. + * + * @returns {Mark} The parsed Mark. + */ + static parse( serialized ) { + delete serialized._parseClass; + return new Mark( serialized ); + } +} export default Mark; diff --git a/packages/yoastseo/src/values/Paper.js b/packages/yoastseo/src/values/Paper.js index 3a72398258e..e9ccc906e63 100644 --- a/packages/yoastseo/src/values/Paper.js +++ b/packages/yoastseo/src/values/Paper.js @@ -3,7 +3,8 @@ import { defaults, isEmpty, isEqual, isNil } from "lodash"; /** * Default attributes to be used by the Paper if they are left undefined. * @type {{keyword: string, synonyms: string, description: string, title: string, titleWidth: number, - * slug: string, locale: string, permalink: string, date: string, customData: object, textTitle: string, writingDirection: "LTR" }} + * slug: string, locale: string, permalink: string, date: string, customData: object, textTitle: string, + * writingDirection: "LTR", isFrontPage: boolean }} */ const defaultAttributes = { keyword: "", @@ -19,341 +20,352 @@ const defaultAttributes = { textTitle: "", writingDirection: "LTR", wpBlocks: [], -}; - -/** - * Constructs the Paper object and sets the keyword property. - * - * @param {string} text The text to use in the analysis. - * @param {object} [attributes] The object containing all attributes. - * @param {string} [attributes.keyword] The main keyword. - * @param {string} [attributes.synonyms] The main keyword's synonyms. - * @param {string} [attributes.description] The SEO description. - * @param {string} [attributes.title] The SEO title. - * @param {number} [attributes.titleWidth] The width of the title in pixels. - * @param {string} [attributes.slug] The slug. - * @param {string} [attributes.locale] The locale. - * @param {string} [attributes.permalink] The base url + slug. - * @param {string} [attributes.date] The date. - * @param {Object} [attributes.wpBlocks] The text, encoded in WordPress block editor blocks. - * @param {Object} [attributes.customData] Custom data. - * @param {string} [attributes.textTitle] The title of the text. - * @param {string} [attributes.writingDirection] The writing direction of the paper. Defaults to left to right (LTR). - * - * @constructor - */ -function Paper( text, attributes ) { - this._text = text || ""; - - this._tree = null; + isFrontPage: false, +}; + +/** + * Represents an object where the analysis data is stored. + */ +export default class Paper { + /** + * Constructs the Paper object and sets its attributes. + * + * @param {string} text The text to use in the analysis. + * @param {object} [attributes] The object containing all attributes. + * @param {string} [attributes.keyword] The main keyword or keyphrase of the text. + * @param {string} [attributes.synonyms] The synonyms of the main keyword or keyphrase. It should be separated by commas if multiple synonyms are added. + * @param {string} [attributes.description] The SEO meta description. + * @param {string} [attributes.title] The SEO title. + * @param {number} [attributes.titleWidth=0] The width of the title in pixels. + * @param {string} [attributes.slug] The slug. + * @param {string} [attributes.locale=en_US] The locale. + * @param {string} [attributes.permalink] The full URL for any given post, page, or other pieces of content on a site. + * @param {string} [attributes.date] The date. + * @param {Object[]} [attributes.wpBlocks] The array of texts, encoded in WordPress block editor blocks. + * @param {Object} [attributes.customData] Custom data. + * @param {string} [attributes.textTitle] The title of the text. + * @param {string} [attributes.writingDirection=LTR] The writing direction of the paper. Defaults to left to right (LTR). + * @param {boolean} [attributes.isFrontPage=false] Whether the current page is the front page of the site. Defaults to false. + */ + constructor( text, attributes ) { + this._text = text || ""; + + this._tree = null; + + attributes = attributes || {}; + defaults( attributes, defaultAttributes ); + + if ( attributes.locale === "" ) { + attributes.locale = defaultAttributes.locale; + } + + if ( attributes.hasOwnProperty( "url" ) ) { + // The 'url' attribute has been deprecated since version 1.19.1, refer to hasUrl and getUrl below. + console.warn( "The 'url' attribute is deprecated, use 'slug' instead." ); + attributes.slug = attributes.url || attributes.slug; + } + + const onlyLetters = attributes.keyword.replace( /[‘’“”"'.?!:;,¿¡«»&*@#±^%|~`[\](){}⟨⟩<>/\\–\-\u2014\u00d7\u002b\s]/g, "" ); + + if ( isEmpty( onlyLetters ) ) { + attributes.keyword = defaultAttributes.keyword; + } + + this._attributes = attributes; + } - attributes = attributes || {}; - defaults( attributes, defaultAttributes ); - if ( attributes.locale === "" ) { - attributes.locale = defaultAttributes.locale; + /** + * Checks whether a keyword is available. + * @returns {boolean} Returns true if the Paper has a keyword. + */ + hasKeyword() { + return this._attributes.keyword !== ""; } - if ( attributes.hasOwnProperty( "url" ) ) { - // The 'url' attribute has been deprecated since version 18.8, refer to hasUrl and getUrl below. - console.warn( "The 'url' attribute is deprecated, use 'slug' instead." ); - attributes.slug = attributes.url || attributes.slug; + /** + * Returns the associated keyword or an empty string if no keyword is available. + * @returns {string} Returns Keyword + */ + getKeyword() { + return this._attributes.keyword; } - const onlyLetters = attributes.keyword.replace( /[‘’“”"'.?!:;,¿¡«»&*@#±^%|~`[\](){}⟨⟩<>/\\–\-\u2014\u00d7\u002b\u0026\s]/g, "" ); - - if ( isEmpty( onlyLetters ) ) { - attributes.keyword = defaultAttributes.keyword; + /** + * Checks whether synonyms are available. + * @returns {boolean} Returns true if the Paper has synonyms. + */ + hasSynonyms() { + return this._attributes.synonyms !== ""; } - this._attributes = attributes; -} - - -/** - * Checks whether a keyword is available. - * @returns {boolean} Returns true if the Paper has a keyword. - */ -Paper.prototype.hasKeyword = function() { - return this._attributes.keyword !== ""; -}; - -/** - * Returns the associated keyword or an empty string if no keyword is available. - * @returns {string} Returns Keyword - */ -Paper.prototype.getKeyword = function() { - return this._attributes.keyword; -}; - -/** - * Checks whether synonyms are available. - * @returns {boolean} Returns true if the Paper has synonyms. - */ -Paper.prototype.hasSynonyms = function() { - return this._attributes.synonyms !== ""; -}; - -/** - * Returns the associated synonyms or an empty string if no synonyms is available. - * @returns {string} Returns synonyms. - */ -Paper.prototype.getSynonyms = function() { - return this._attributes.synonyms; -}; - -/** - * Checks whether the text is available. - * @returns {boolean} Returns true if the paper has a text. - */ -Paper.prototype.hasText = function() { - return this._text !== ""; -}; - -/** - * Returns the associated text or am empty string if no text is available. - * @returns {string} Returns text - */ -Paper.prototype.getText = function() { - return this._text; -}; + /** + * Returns the associated synonyms or an empty string if no synonyms is available. + * @returns {string} Returns synonyms. + */ + getSynonyms() { + return this._attributes.synonyms; + } -/** - * Sets the tree. - * - * @param {Node} tree The tree to set. - * - * @returns {void} - */ -Paper.prototype.setTree = function( tree ) { - this._tree = tree; -}; + /** + * Checks whether the text is available. + * @returns {boolean} Returns true if the paper has a text. + */ + hasText() { + return this._text !== ""; + } -/** - * Returns the tree. - * - * @returns {Node} The tree. - */ -Paper.prototype.getTree = function() { - return this._tree; -}; + /** + * Returns the associated text or an empty string if no text is available. + * @returns {string} Returns the text. + */ + getText() { + return this._text; + } -/** - * Checks whether a description is available. - * @returns {boolean} Returns true if the paper has a description. - */ -Paper.prototype.hasDescription = function() { - return this._attributes.description !== ""; -}; + /** + * Sets the tree. + * + * @param {Node} tree The tree to set. + * + * @returns {void} + */ + setTree( tree ) { + this._tree = tree; + } -/** - * Returns the description or an empty string if no description is available. - * @returns {string} Returns the description. - */ -Paper.prototype.getDescription = function() { - return this._attributes.description; -}; + /** + * Returns the tree. + * + * @returns {Node} The tree. + */ + getTree() { + return this._tree; + } -/** - * Checks whether a title is available - * @returns {boolean} Returns true if the Paper has a title. - */ -Paper.prototype.hasTitle = function() { - return this._attributes.title !== ""; -}; + /** + * Checks whether a description is available. + * @returns {boolean} Returns true if the paper has a description. + */ + hasDescription() { + return this._attributes.description !== ""; + } -/** - * Returns the title, or an empty string of no title is available. - * @returns {string} Returns the title - */ -Paper.prototype.getTitle = function() { - return this._attributes.title; -}; + /** + * Returns the description or an empty string if no description is available. + * @returns {string} Returns the description. + */ + getDescription() { + return this._attributes.description; + } -/** - * Checks whether a title width in pixels is available - * @returns {boolean} Returns true if the Paper has a title. - */ -Paper.prototype.hasTitleWidth = function() { - return this._attributes.titleWidth !== 0; -}; + /** + * Checks whether an SEO title is available + * @returns {boolean} Returns true if the Paper has an SEO title. + */ + hasTitle() { + return this._attributes.title !== ""; + } -/** - * Returns the title width in pixels, or an empty string of no title width in pixels is available. - * @returns {string} Returns the title - */ -Paper.prototype.getTitleWidth = function() { - return this._attributes.titleWidth; -}; + /** + * Returns the SEO title, or an empty string if no title is available. + * @returns {string} Returns the SEO title. + */ + getTitle() { + return this._attributes.title; + } -/** - * Checks whether a slug is available - * @returns {boolean} Returns true if the Paper has a slug. - */ -Paper.prototype.hasSlug = function() { - return this._attributes.slug !== ""; -}; + /** + * Checks whether an SEO title width in pixels is available. + * @returns {boolean} Returns true if the Paper's SEO title is wider than 0 pixels. + */ + hasTitleWidth() { + return this._attributes.titleWidth !== 0; + } -/** - * Returns the slug, or an empty string of no slug is available. - * @returns {string} Returns the url - */ -Paper.prototype.getSlug = function() { - return this._attributes.slug; -}; + /** + * Gets the SEO title width in pixels, or an empty string of no title width in pixels is available. + * @returns {number} Returns the SEO title width in pixels. + */ + getTitleWidth() { + return this._attributes.titleWidth; + } -/** - * Checks whether an url is available - * @deprecated Since version 18.7. Use hasSlug instead. - * @returns {boolean} Returns true if the Paper has a slug. - */ -Paper.prototype.hasUrl = function() { - console.warn( "This function is deprecated, use hasSlug instead" ); - return this.hasSlug(); -}; + /** + * Checks whether a slug is available. + * @returns {boolean} Returns true if the Paper has a slug. + */ + hasSlug() { + return this._attributes.slug !== ""; + } -/** - * Returns the url, or an empty string if no url is available. - * @deprecated Since version 18.8. Use getSlug instead. - * @returns {string} Returns the url - */ -Paper.prototype.getUrl = function() { - console.warn( "This function is deprecated, use getSlug instead" ); - return this.getSlug(); -}; + /** + * Gets the paper's slug, or an empty string if no slug is available. + * @returns {string} Returns the slug. + */ + getSlug() { + return this._attributes.slug; + } -/** - * Checks whether a locale is available - * @returns {boolean} Returns true if the paper has a locale - */ -Paper.prototype.hasLocale = function() { - return this._attributes.locale !== ""; -}; + /** + * Checks if currently edited page is a front page. + * @returns {boolean} Returns true if the current page is a front page. + */ + isFrontPage() { + return this._attributes.isFrontPage; + } -/** - * Returns the locale or an empty string if no locale is available - * @returns {string} Returns the locale - */ -Paper.prototype.getLocale = function() { - return this._attributes.locale; -}; + /** + * Checks whether an url is available + * @deprecated Since version 1.19.1. Use hasSlug instead. + * @returns {boolean} Returns true if the Paper has a slug. + */ + hasUrl() { + console.warn( "This function is deprecated, use hasSlug instead" ); + return this.hasSlug(); + } -/** - * Gets the information of the writing direction of the paper. - * It returns "LTR" (left to right) if this attribute is not provided. - * - * @returns {string} Returns the information of the writing direction of the paper. - */ -Paper.prototype.getWritingDirection = function() { - return this._attributes.writingDirection; -}; + /** + * Returns the url, or an empty string if no url is available. + * @deprecated Since version 1.19.1. Use getSlug instead. + * @returns {string} Returns the url + */ + getUrl() { + console.warn( "This function is deprecated, use getSlug instead" ); + return this.getSlug(); + } -/** - * Checks whether a permalink is available - * @returns {boolean} Returns true if the Paper has a permalink. - */ -Paper.prototype.hasPermalink = function() { - return this._attributes.permalink !== ""; -}; + /** + * Checks whether a locale is available. + * @returns {boolean} Returns true if the paper has a locale. + */ + hasLocale() { + return this._attributes.locale !== ""; + } -/** - * Returns the permalink, or an empty string if no permalink is available. - * @returns {string} Returns the permalink. - */ -Paper.prototype.getPermalink = function() { - return this._attributes.permalink; -}; + /** + * Returns the locale or an empty string if no locale is available + * @returns {string} Returns the locale. + */ + getLocale() { + return this._attributes.locale; + } -/** - * Checks whether a date is available. - * @returns {boolean} Returns true if the Paper has a date. - */ -Paper.prototype.hasDate = function() { - return this._attributes.date !== ""; -}; + /** + * Gets the information of the writing direction of the paper. + * It returns "LTR" (left to right) if this attribute is not provided. + * + * @returns {string} Returns the information of the writing direction of the paper. + */ + getWritingDirection() { + return this._attributes.writingDirection; + } -/** - * Returns the date, or an empty string if no date is available. - * @returns {string} Returns the date. - */ -Paper.prototype.getDate = function() { - return this._attributes.date; -}; + /** + * Checks whether a permalink is available. + * @returns {boolean} Returns true if the Paper has a permalink. + */ + hasPermalink() { + return this._attributes.permalink !== ""; + } -/** - * Checks whether custom data is available. - * @returns {boolean} Returns true if the Paper has custom data. - */ -Paper.prototype.hasCustomData = function() { - return ! isEmpty( this._attributes.customData ); -}; + /** + * Returns the permalink, or an empty string if no permalink is available. + * @returns {string} Returns the permalink. + */ + getPermalink() { + return this._attributes.permalink; + } -/** - * Returns the custom data, or an empty object if no data is available. - * @returns {Object} Returns the custom data. - */ -Paper.prototype.getCustomData = function() { - return this._attributes.customData; -}; + /** + * Checks whether a date is available. + * @returns {boolean} Returns true if the Paper has a date. + */ + hasDate() { + return this._attributes.date !== ""; + } -/** - * Checks whether a text title is available. - * @returns {boolean} Returns true if the Paper has a text title. - */ -Paper.prototype.hasTextTitle = function() { - return this._attributes.textTitle !== "" && ! isNil( this._attributes.textTitle ); -}; + /** + * Returns the date, or an empty string if no date is available. + * @returns {string} Returns the date. + */ + getDate() { + return this._attributes.date; + } -/** - * Returns the text title, or an empty string if no data is available. - * @returns {string} Returns the text title. - */ -Paper.prototype.getTextTitle = function() { - return this._attributes.textTitle; -}; + /** + * Checks whether custom data is available. + * @returns {boolean} Returns true if the Paper has custom data. + */ + hasCustomData() { + return ! isEmpty( this._attributes.customData ); + } -/** - * Serializes the Paper instance to an object. - * - * @returns {Object} The serialized Paper. - */ -Paper.prototype.serialize = function() { - return { - _parseClass: "Paper", - text: this._text, - ...this._attributes, - }; -}; + /** + * Returns the custom data, or an empty object if no data is available. + * @returns {Object} Returns the custom data. + */ + getCustomData() { + return this._attributes.customData; + } -/** - * Checks whether the given paper has the same properties as this instance. - * - * @param {Paper} paper The paper to compare to. - * - * @returns {boolean} Whether the given paper is identical or not. - */ -Paper.prototype.equals = function( paper ) { - return this._text === paper.getText() && isEqual( this._attributes, paper._attributes ); -}; + /** + * Checks whether a text title is available. + * @returns {boolean} Returns true if the Paper has a text title. + */ + hasTextTitle() { + return this._attributes.textTitle !== "" && ! isNil( this._attributes.textTitle ); + } -/** - * Parses the object to a Paper. - * - * @param {Object|Paper} serialized The serialized object or Paper instance. - * - * @returns {Paper} The parsed Paper. - */ -Paper.parse = function( serialized ) { - // For ease of use, check if it is not already a Paper instance. - if ( serialized instanceof Paper ) { - return serialized; + /** + * Returns the text title, or an empty string if no data is available. + * @returns {string} Returns the text title. + */ + getTextTitle() { + return this._attributes.textTitle; } - // _parseClass is taken here, so it doesn't end up in the attributes. - // eslint-disable-next-line no-unused-vars - const { text, _parseClass, ...attributes } = serialized; + /** + * Serializes the Paper instance to an object. + * + * @returns {Object} The serialized Paper. + */ + serialize() { + return { + _parseClass: "Paper", + text: this._text, + ...this._attributes, + }; + } - return new Paper( text, attributes ); -}; + /** + * Checks whether the given paper has the same properties as this instance. + * + * @param {Paper} paper The paper to compare to. + * + * @returns {boolean} Whether the given paper is identical or not. + */ + equals( paper ) { + return this._text === paper.getText() && isEqual( this._attributes, paper._attributes ); + } -export default Paper; + /** + * Parses the object to a Paper. + * + * @param {Object|Paper} serialized The serialized object or Paper instance. + * + * @returns {Paper} The parsed Paper. + */ + static parse( serialized ) { + // For ease of use, check if it is not already a Paper instance. + if ( serialized instanceof Paper ) { + return serialized; + } + + // _parseClass is taken here, so it doesn't end up in the attributes. + // eslint-disable-next-line no-unused-vars + const { text, _parseClass, ...attributes } = serialized; + + return new Paper( text, attributes ); + } +} diff --git a/packages/yoastseo/src/worker/transporter/parse.js b/packages/yoastseo/src/worker/transporter/parse.js index 7df330b8dc5..001b42576c3 100644 --- a/packages/yoastseo/src/worker/transporter/parse.js +++ b/packages/yoastseo/src/worker/transporter/parse.js @@ -31,9 +31,7 @@ export default function parse( thing ) { const thingIsObject = isObject( thing ); if ( thingIsObject && thing._parseClass && PARSE_CLASSES[ thing._parseClass ] ) { - return thing._parseClass === "Sentence" || thing._parseClass === "Clause" - ? PARSE_CLASSES[ thing._parseClass ].prototype.parse( thing ) - : PARSE_CLASSES[ thing._parseClass ].parse( thing ); + return PARSE_CLASSES[ thing._parseClass ].parse( thing ); } if ( thingIsObject ) { diff --git a/readme.txt b/readme.txt index 40ab72083d4..5cd0398f470 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ License: GPLv3 License URI: http://www.gnu.org/licenses/gpl.html Tags: SEO, XML sitemap, Content analysis, Readability, Schema Tested up to: 6.6 -Stable tag: 23.4 +Stable tag: 23.5 Requires PHP: 7.2.5 Improve your WordPress SEO: Write better content and have a fully optimized WordPress site using the Yoast SEO plugin. @@ -30,9 +30,9 @@ Empower search engines to fully understand your website using our Schema.org str Yoast SEO offers comprehensive analysis tools that help elevate your content's SEO and readability. Get powerful insights and actionable recommendations to craft helpful content that resonates with readers and search engines. -**Premium Yoast AI features** Get suggestions for your titles and descriptions at the click of a button. The [Yoast AI features] (https://yoa.st/51c) save you time and optimize for higher click-through-rates. +**Premium Yoast AI features** Get suggestions for your titles and descriptions at the click of a button. The [Yoast AI features](https://yoa.st/51c) save you time and optimize for higher click-through-rates. -* Yoast AI Generate enables users to generate meta descriptions and titles for your pages, blog posts and social posts. Great! Even better, when you also have [Yoast WooCommerce SEO](https://yoa.st/3rh), you can recieve suggestions for product SEO titles and descriptions too! The best part, if you don't like the 5 suggestions, you can generate five more at a click. +* Yoast AI Generate enables users to generate meta descriptions and titles for your pages, blog posts and social posts. Great! Even better, when you also have [Yoast WooCommerce SEO](https://yoa.st/3rh), you can receive suggestions for product SEO titles and descriptions too! The best part, if you don't like the 5 suggestions, you can generate five more at a click. * Yoast AI Optimize helps you optimize existing content for search engines. Optimize three of the assessments in the Yoast SEO Analysis; Keyphrase in introduction, Keyphrase distribution and Keyphrase density, with easy dismiss or apply options. @@ -132,7 +132,7 @@ If you're looking for a structured learning path, our [Yoast SEO academy](https: Not only do you get many additional benefits by upgrading to [Yoast SEO Premium](https://yoa.st/1v8), but you'll also get 24/7 personalized support that takes away your worry. -* Unlock our [AI features] (https://yoa.st/51c); Yoast AI Optimize and Yoast AI Generate. Perfect for marketing professionals, freelance writers, and content strategists, Yoast AI features enable customers of all technical levels to apply SEO best practice to their content at the click of a button. +* Unlock our [AI features](https://yoa.st/51c); Yoast AI Optimize and Yoast AI Generate. Perfect for marketing professionals, freelance writers, and content strategists, Yoast AI features enable customers of all technical levels to apply SEO best practice to their content at the click of a button. * Optimize for up to five keyword synonyms by adding variants. Add up to four related synonyms of your keyword to expand your possibilities. You get the full SEO analysis for each. @@ -274,47 +274,40 @@ Your question has most likely been answered on our help center: [yoast.com/help/ == Changelog == -= 23.5 = += 23.6 = -Release date: 2024-09-17 +Release date: 2024-10-08 #### Enhancements -* Improves our integration with cache plugins by preventing flushing their cache when not needed. -* Improves analysis score feedback labels in the publish sections. +* Adds a filter to modify the sitemap's URL. Props to @ashujangra. +* Improves the _transition words_ assessment for Turkish and English by expanding the relevant lists of transitions words. Props to [abulu](https://wordpress.org/support/users/abulu/). +* Uses the full-sized counterpart when a resized first content image is used for Open Graph and X images. #### Bugfixes -* Fixes a bug where Arabic keyphrases containing certain function words were not correctly matched. +* Fixes a bug where the content analysis would error when removing an image caption in the default editor. +* Fixes a bug where the link popover would be hidden when editing a post in tablet/mobile view. Props to [stokesman](https://github.com/stokesman). +* Fixes a visual inconsistency where the descriptions of the disabled Premium policy settings would look enabled, when they are not enabled. -= 23.4 = +#### Other -Release date: 2024-09-03 +* Sets the minimum supported WordPress version to 6.5. -Yoast SEO 23.4 brings more enhancements and bugfixes. [Find more information about our software releases and updates here](https://yoa.st/release-3-9-24). += 23.5 = -#### Enhancements +Release date: 2024-09-24 -* Adds support for discarding the changes when switching to a post, using the Top bar feature in Elementor. -* Adds _так_ to the words recognized by the _transition words_ assessment in Russian. Props to @pavelmai83. -* Improves the schema output by following the specification for the _SearchAction_ more strictly. -* Re-enables the script concatenation that was disabled to prevent a bug with WordPress 5.5. +Yoast SEO 23.5 brings more enhancements and bugfixes. [Find more information about our software releases and updates here](https://yoa.st/release-24-9-24). -#### Bugfixes +#### Enhancements -* Fixes a bug where an image caption inside a classic block would be considered the introduction when using the _keyphrase in introduction_ assessment in the default editor. -* Fixes a bug where the first tag instead of the primary tag would be shown in a permalink when adding a link in the Classic editor. -* Fixes a bug where the Yoast tab would disappear when opening and closing the Site Settings in Elementor. -* Fixes a bug where the Yoast user settings would be wiped out after a user profile update when the respective global settings were disabled. -* Fixes a bug where two admin links would not be resolvable when using a custom admin URL. +* Improves analysis score feedback labels in the publish sections. +* Improves our integration with cache plugins by preventing flushing their cache when not needed. -#### Other +#### Bugfixes -* Adds a learn more link to the primary category picker. -* Deprecates some functions in the `Yoast_Input_Validation` class. -* Deprecates the `Disable_Concatenate_Scripts_Integration` class. -* Deprecates the `Duplicate_Post_Integration` class. -* Deprecates the `WPSEO_Admin_User_Profile::user_profile()` method and the `admin/views/user-profile.php` file. +* Fixes a bug where Arabic keyphrases containing certain function words were not correctly matched. = Earlier versions = For the changelog of earlier versions, please refer to [the changelog on yoast.com](https://yoa.st/yoast-seo-changelog). diff --git a/src/actions/indexing/abstract-indexing-action.php b/src/actions/indexing/abstract-indexing-action.php index d43bd36cd77..87cba3e44c5 100644 --- a/src/actions/indexing/abstract-indexing-action.php +++ b/src/actions/indexing/abstract-indexing-action.php @@ -57,7 +57,7 @@ public function get_limited_unindexed_count( $limit ) { $query = $this->get_select_query( $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. - $unindexed_object_ids = $this->wpdb->get_col( $query ); + $unindexed_object_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $count = (int) \count( $unindexed_object_ids ); \set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) ); @@ -83,7 +83,7 @@ public function get_total_unindexed() { $query = $this->get_count_query(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. - $count = $this->wpdb->get_var( $query ); + $count = ( $query === '' ) ? 0 : $this->wpdb->get_var( $query ); if ( \is_null( $count ) ) { return false; diff --git a/src/actions/indexing/indexable-term-indexation-action.php b/src/actions/indexing/indexable-term-indexation-action.php index f7115ac07be..b1dbfd356a8 100644 --- a/src/actions/indexing/indexable-term-indexation-action.php +++ b/src/actions/indexing/indexable-term-indexation-action.php @@ -83,7 +83,7 @@ public function index() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. - $term_ids = $this->wpdb->get_col( $query ); + $term_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $indexables = []; foreach ( $term_ids as $term_id ) { @@ -128,6 +128,10 @@ protected function get_count_query() { $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); + if ( empty( $public_taxonomies ) ) { + return ''; + } + $taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; @@ -159,7 +163,12 @@ protected function get_select_query( $limit = false ) { $indexable_table = Model::get_table_name( 'Indexable' ); $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); - $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); + + if ( empty( $public_taxonomies ) ) { + return ''; + } + + $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; \array_push( $replacements, ...$public_taxonomies ); diff --git a/src/actions/indexing/term-link-indexing-action.php b/src/actions/indexing/term-link-indexing-action.php index 4ef79077f54..3e6a4bc3a31 100644 --- a/src/actions/indexing/term-link-indexing-action.php +++ b/src/actions/indexing/term-link-indexing-action.php @@ -52,6 +52,10 @@ public function set_helper( Taxonomy_Helper $taxonomy_helper ) { protected function get_objects() { $query = $this->get_select_query( $this->get_limit() ); + if ( $query === '' ) { + return []; + } + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $terms = $this->wpdb->get_results( $query ); @@ -74,8 +78,13 @@ static function ( $term ) { */ protected function get_count_query() { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); - $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); - $indexable_table = Model::get_table_name( 'Indexable' ); + + if ( empty( $public_taxonomies ) ) { + return ''; + } + + $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); + $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, makes sure to update the query in get_select_query as well. return $this->wpdb->prepare( @@ -102,6 +111,10 @@ protected function get_count_query() { protected function get_select_query( $limit = false ) { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); + if ( empty( $public_taxonomies ) ) { + return ''; + } + $indexable_table = Model::get_table_name( 'Indexable' ); $replacements = $public_taxonomies; diff --git a/src/builders/indexable-link-builder.php b/src/builders/indexable-link-builder.php index ca703c2cda7..476600a8899 100644 --- a/src/builders/indexable-link-builder.php +++ b/src/builders/indexable-link-builder.php @@ -2,14 +2,13 @@ namespace Yoast\WP\SEO\Builders; -use DOMDocument; -use WP_HTML_Tag_Processor; use WPSEO_Image_Utils; use Yoast\WP\SEO\Helpers\Image_Helper; use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Post_Helper; use Yoast\WP\SEO\Helpers\Url_Helper; +use Yoast\WP\SEO\Images\Application\Image_Content_Extractor; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Models\SEO_Links; use Yoast\WP\SEO\Repositories\Indexable_Repository; @@ -69,6 +68,13 @@ class Indexable_Link_Builder { */ protected $indexable_repository; + /** + * Class that finds all images in a content string and extracts them. + * + * @var Image_Content_Extractor + */ + private $image_content_extractor; + /** * Indexable_Link_Builder constructor. * @@ -83,13 +89,15 @@ public function __construct( Url_Helper $url_helper, Post_Helper $post_helper, Options_Helper $options_helper, - Indexable_Helper $indexable_helper + Indexable_Helper $indexable_helper, + Image_Content_Extractor $image_content_extractor ) { - $this->seo_links_repository = $seo_links_repository; - $this->url_helper = $url_helper; - $this->post_helper = $post_helper; - $this->options_helper = $options_helper; - $this->indexable_helper = $indexable_helper; + $this->seo_links_repository = $seo_links_repository; + $this->url_helper = $url_helper; + $this->post_helper = $post_helper; + $this->options_helper = $options_helper; + $this->indexable_helper = $indexable_helper; + $this->image_content_extractor = $image_content_extractor; } /** @@ -137,7 +145,7 @@ public function build( $indexable, $content ) { $content = \str_replace( ']]>', ']]>', $content ); $links = $this->gather_links( $content ); - $images = $this->gather_images( $content ); + $images = $this->image_content_extractor->gather_images( $content ); if ( empty( $links ) && empty( $images ) ) { $indexable->link_count = 0; @@ -146,6 +154,10 @@ public function build( $indexable, $content ) { return []; } + if ( ! empty( $images ) && ( $indexable->open_graph_image_source === 'first-content-image' || $indexable->twitter_image_source === 'first-content-image' ) ) { + $this->update_first_content_image( $indexable, $images ); + } + $links = $this->create_links( $indexable, $links, $images ); $this->update_related_indexables( $indexable, $links ); @@ -229,164 +241,6 @@ protected function gather_links( $content ) { return $links; } - /** - * Gathers all images from content with WP's WP_HTML_Tag_Processor() and returns them along with their IDs, if - * possible. - * - * @param string $content The content. - * - * @return int[] An associated array of image IDs, keyed by their URL. - */ - protected function gather_images_wp( $content ) { - $processor = new WP_HTML_Tag_Processor( $content ); - $images = []; - - $query = [ - 'tag_name' => 'img', - ]; - - /** - * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. - * - * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. - * - * @api string The attribute to be used to extract image IDs from. - */ - $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); - - while ( $processor->next_tag( $query ) ) { - $src = \htmlentities( $processor->get_attribute( 'src' ), ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), \get_bloginfo( 'charset' ) ); - $classes = $processor->get_attribute( $attribute ); - $id = $this->extract_id_of_classes( $classes ); - - $images[ $src ] = $id; - } - - return $images; - } - - /** - * Gathers all images from content with DOMDocument() and returns them along with their IDs, if possible. - * - * @param string $content The content. - * - * @return int[] An associated array of image IDs, keyed by their URL. - */ - protected function gather_images_domdocument( $content ) { - $images = []; - $charset = \get_bloginfo( 'charset' ); - - /** - * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. - * - * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. - * - * @api string The attribute to be used to extract image IDs from. - */ - $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); - - \libxml_use_internal_errors( true ); - $post_dom = new DOMDocument(); - $post_dom->loadHTML( '' . $content ); - \libxml_clear_errors(); - - foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { - $src = \htmlentities( $img->getAttribute( 'src' ), ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), $charset ); - $classes = $img->getAttribute( $attribute ); - $id = $this->extract_id_of_classes( $classes ); - - $images[ $src ] = $id; - } - - return $images; - } - - /** - * Extracts image ID out of the image's classes. - * - * @param string $classes The classes assigned to the image. - * - * @return int The ID that's extracted from the classes. - */ - protected function extract_id_of_classes( $classes ) { - if ( ! $classes ) { - return 0; - } - - /** - * Filter 'wpseo_extract_id_pattern' - Allows filtering the regex patern to be used to extract image IDs from class/attribute names. - * - * Defaults to the pattern that extracts image IDs from core's `wp-image-` native format in image classes. - * - * @api string The regex pattern to be used to extract image IDs from class names. Empty string if the whole class/attribute should be returned. - */ - $pattern = \apply_filters( 'wpseo_extract_id_pattern', '/(?gather_images_wp( $content ); - } - - if ( ! $should_not_parse_content && \class_exists( DOMDocument::class ) ) { - return $this->gather_images_DOMDocument( $content ); - } - - if ( \strpos( $content, 'src' ) === false ) { - // Nothing to do. - return []; - } - - $images = []; - $regexp = ']*src=("??)([^" >]*?)\\1[^>]*>'; - // Used modifiers iU to match case insensitive and make greedy quantifiers lazy. - if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) { - foreach ( $matches as $match ) { - $images[ $match[2] ] = 0; - } - } - - return $images; - } - /** * Creates link models from lists of URLs and image sources. * @@ -729,4 +583,27 @@ protected function update_incoming_links_for_related_indexables( $related_indexa $this->indexable_repository->update_incoming_link_count( $count['target_indexable_id'], $count['incoming'] ); } } + + /** + * Updates the image ids when the indexable images are marked as first content image. + * + * @param Indexable $indexable The indexable to change. + * @param array $images The image array. + * + * @return void + */ + public function update_first_content_image( Indexable $indexable, array $images ): void { + $current_open_graph_image = $indexable->open_graph_image; + $current_twitter_image = $indexable->twitter_image; + + $first_content_image_url = \key( $images ); + $first_content_image_id = \current( $images ); + + if ( $indexable->open_graph_image_source === 'first-content-image' && $current_open_graph_image === $first_content_image_url && ! empty( $first_content_image_id ) ) { + $indexable->open_graph_image_id = $first_content_image_id; + } + if ( $indexable->twitter_image_source === 'first-content-image' && $current_twitter_image === $first_content_image_url && ! empty( $first_content_image_id ) ) { + $indexable->twitter_image_id = $first_content_image_id; + } + } } diff --git a/src/conditionals/new-dashboard-ui-conditional.php b/src/conditionals/new-dashboard-ui-conditional.php new file mode 100644 index 00000000000..f8397bf3411 --- /dev/null +++ b/src/conditionals/new-dashboard-ui-conditional.php @@ -0,0 +1,18 @@ +is_premium(); - } - - /** - * Retrieves, if available, if Jetpack Boost has priority feature available. - * - * @return bool Whether Jetpack Boost is premium. - */ - private function is_premium() { - if ( \class_exists( '\Automattic\Jetpack_Boost\Lib\Premium_Features', false ) ) { - return Premium_Features::has_feature( - Premium_Features::PRIORITY_SUPPORT - ); - } - - return false; - } -} diff --git a/src/context/meta-tags-context.php b/src/context/meta-tags-context.php index f5b95acdf06..3eaa76df01b 100644 --- a/src/context/meta-tags-context.php +++ b/src/context/meta-tags-context.php @@ -11,7 +11,6 @@ use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Permalink_Helper; -use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Helpers\Schema\ID_Helper; use Yoast\WP\SEO\Helpers\Site_Helper; use Yoast\WP\SEO\Helpers\Url_Helper; @@ -122,13 +121,6 @@ class Meta_Tags_Context extends Abstract_Presentation { */ private $id_helper; - /** - * The request helper. - * - * @var Request_Helper - */ - private $request_helper; - /** * The WPSEO Replace Vars object. * @@ -184,7 +176,6 @@ class Meta_Tags_Context extends Abstract_Presentation { * @param Permalink_Helper $permalink_helper The permalink helper. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Indexable_Repository $indexable_repository The indexable repository. - * @param Request_Helper $request_helper The request helper. */ public function __construct( Options_Helper $options, @@ -196,8 +187,7 @@ public function __construct( User_Helper $user, Permalink_Helper $permalink_helper, Indexable_Helper $indexable_helper, - Indexable_Repository $indexable_repository, - Request_Helper $request_helper + Indexable_Repository $indexable_repository ) { $this->options = $options; $this->url = $url; @@ -209,7 +199,6 @@ public function __construct( $this->permalink_helper = $permalink_helper; $this->indexable_helper = $indexable_helper; $this->indexable_repository = $indexable_repository; - $this->request_helper = $request_helper; } /** @@ -625,7 +614,7 @@ public function generate_main_image_url() { return $this->image->get_attachment_image_url( $this->main_image_id, 'full' ); } - if ( $this->request_helper->is_rest_request() ) { + if ( \wp_is_serving_rest_request() ) { return $this->get_main_image_url_for_rest_request(); } @@ -647,7 +636,7 @@ public function generate_main_image_url() { * @return int|null The main image ID. */ public function generate_main_image_id() { - if ( $this->request_helper->is_rest_request() ) { + if ( \wp_is_serving_rest_request() ) { return $this->get_main_image_id_for_rest_request(); } diff --git a/src/dashboard/user-interface/new-dashboard-page-integration.php b/src/dashboard/user-interface/new-dashboard-page-integration.php new file mode 100644 index 00000000000..2775ad107a6 --- /dev/null +++ b/src/dashboard/user-interface/new-dashboard-page-integration.php @@ -0,0 +1,203 @@ +asset_manager = $asset_manager; + $this->current_page_helper = $current_page_helper; + $this->product_helper = $product_helper; + $this->shortlink_helper = $shortlink_helper; + $this->notification_helper = $notification_helper; + $this->alert_dismissal_action = $alert_dismissal_action; + $this->promotion_manager = $promotion_manager; + } + + /** + * Returns the conditionals based on which this loadable should be active. + * + * @return array + */ + public static function get_conditionals() { + return [ Admin_Conditional::class, New_Dashboard_Ui_Conditional::class ]; + } + + /** + * Initializes the integration. + * + * This is the place to register hooks and filters. + * + * @return void + */ + public function register_hooks() { + // Add page. + \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] ); + + // Are we on the dashboard page? + if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { + \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); + } + } + + /** + * Adds the page. + * + * @param array> $pages The pages. + * + * @return array> The pages. + */ + public function add_page( $pages ) { + \array_splice( + $pages, + 0, + 0, + [ + [ + self::PAGE, + '', + \__( 'General', 'wordpress-seo' ), + 'wpseo_manage_options', + self::PAGE, + [ $this, 'display_page' ], + ], + ] + ); + + return $pages; + } + + /** + * Displays the page. + * + * @return void + */ + public function display_page() { + echo '
    '; + } + + /** + * Enqueues the assets. + * + * @return void + */ + public function enqueue_assets() { + // Remove the emoji script as it is incompatible with both React and any contenteditable fields. + \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); + \wp_enqueue_media(); + $this->asset_manager->enqueue_script( 'new-dashboard' ); + $this->asset_manager->enqueue_style( 'new-dashboard' ); + $this->asset_manager->localize_script( 'new-dashboard', 'wpseoScriptData', $this->get_script_data() ); + } + + /** + * Creates the script data. + * + * @return array>> The script data. + */ + private function get_script_data() { + return [ + 'preferences' => [ + 'isPremium' => $this->product_helper->is_premium(), + 'isRtl' => \is_rtl(), + 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), + 'upsellSettings' => [ + 'actionId' => 'load-nfd-ctb', + 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2', + ], + ], + 'linkParams' => $this->shortlink_helper->get_query_params(), + 'userEditUrl' => \add_query_arg( 'user_id', '{user_id}', \admin_url( 'user-edit.php' ) ), + 'alerts' => $this->notification_helper->get_alerts(), + 'currentPromotions' => $this->promotion_manager->get_current_promotions(), + 'dismissedAlerts' => $this->alert_dismissal_action->all_dismissed(), + ]; + } +} diff --git a/src/helpers/request-helper.php b/src/deprecated/src/helpers/request-helper.php similarity index 65% rename from src/helpers/request-helper.php rename to src/deprecated/src/helpers/request-helper.php index c4a80b3e1c5..8a57165b7c6 100644 --- a/src/helpers/request-helper.php +++ b/src/deprecated/src/helpers/request-helper.php @@ -4,6 +4,8 @@ /** * A helper object for the request state. + * + * @codeCoverageIgnore Because of deprecation. */ class Request_Helper { @@ -11,8 +13,13 @@ class Request_Helper { * Checks if the current request is a REST request. * * @return bool True when the current request is a REST request. + * + * @deprecated 23.6 + * @codeCoverageIgnore */ public function is_rest_request() { + \_deprecated_function( __METHOD__, 'Yoast SEO 23.6', 'wp_is_serving_rest_request' ); + return \defined( 'REST_REQUEST' ) && \REST_REQUEST === true; } } diff --git a/src/editors/framework/integrations/multilingual.php b/src/editors/framework/integrations/multilingual.php index 4f643dbca4a..1d78a5dd66a 100644 --- a/src/editors/framework/integrations/multilingual.php +++ b/src/editors/framework/integrations/multilingual.php @@ -61,7 +61,7 @@ public function is_enabled(): bool { * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { - return [ 'multilingualPluginActive' => $this->is_enabled() ]; + return [ 'isMultilingualActive' => $this->is_enabled() ]; } /** diff --git a/src/editors/framework/integrations/news-seo.php b/src/editors/framework/integrations/news-seo.php new file mode 100644 index 00000000000..d7059dc8511 --- /dev/null +++ b/src/editors/framework/integrations/news-seo.php @@ -0,0 +1,55 @@ +addon_manager = $addon_manager; + } + + /** + * If the plugin is activated. + * + * @return bool If the plugin is activated. + */ + public function is_enabled(): bool { + return \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::NEWS_SLUG ) ); + } + + /** + * Return this object represented by a key value array. + * + * @return array Returns the name and if the feature is enabled. + */ + public function to_array(): array { + return [ 'isNewsSeoActive' => $this->is_enabled() ]; + } + + /** + * Returns this object represented by a key value structure that is compliant with the script data array. + * + * @return array Returns the legacy key and if the feature is enabled. + */ + public function to_legacy_array(): array { + return [ 'isNewsSeoActive' => $this->is_enabled() ]; + } +} diff --git a/src/editors/framework/integrations/woocommerce-seo.php b/src/editors/framework/integrations/woocommerce-seo.php new file mode 100644 index 00000000000..4af3e2f735f --- /dev/null +++ b/src/editors/framework/integrations/woocommerce-seo.php @@ -0,0 +1,55 @@ +addon_manager = $addon_manager; + } + + /** + * If the plugin is activated. + * + * @return bool If the plugin is activated. + */ + public function is_enabled(): bool { + return \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ); + } + + /** + * Return this object represented by a key value array. + * + * @return array Returns the name and if the addon is enabled. + */ + public function to_array(): array { + return [ 'isWooCommerceSeoActive' => $this->is_enabled() ]; + } + + /** + * Returns this object represented by a key value structure that is compliant with the script data array. + * + * @return array Returns the legacy key and if the feature is enabled. + */ + public function to_legacy_array(): array { + return [ 'isWooCommerceSeoActive' => $this->is_enabled() ]; + } +} diff --git a/src/editors/framework/integrations/woocommerce.php b/src/editors/framework/integrations/woocommerce.php new file mode 100644 index 00000000000..2258a4d5ff0 --- /dev/null +++ b/src/editors/framework/integrations/woocommerce.php @@ -0,0 +1,55 @@ +woocommerce_conditional = $woocommerce_conditional; + } + + /** + * If the plugin is activated. + * + * @return bool If the plugin is activated. + */ + public function is_enabled(): bool { + return $this->woocommerce_conditional->is_met(); + } + + /** + * Return this object represented by a key value array. + * + * @return array Returns the name and if the feature is enabled. + */ + public function to_array(): array { + return [ 'isWooCommerceActive' => $this->is_enabled() ]; + } + + /** + * Returns this object represented by a key value structure that is compliant with the script data array. + * + * @return array Returns the legacy key and if the feature is enabled. + */ + public function to_legacy_array(): array { + return [ 'isWooCommerceActive' => $this->is_enabled() ]; + } +} diff --git a/src/editors/framework/site/base-site-information.php b/src/editors/framework/site/base-site-information.php index f62b8181eba..35b217f6cf0 100644 --- a/src/editors/framework/site/base-site-information.php +++ b/src/editors/framework/site/base-site-information.php @@ -3,9 +3,11 @@ namespace Yoast\WP\SEO\Editors\Framework\Site; use Exception; +use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; +use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; use Yoast\WP\SEO\Surfaces\Meta_Surface; /** @@ -41,6 +43,20 @@ abstract class Base_Site_Information { */ protected $product_helper; + /** + * The options helper. + * + * @var Options_Helper $options_helper + */ + protected $options_helper; + + /** + * The promotion manager. + * + * @var Promotion_Manager $promotion_manager + */ + protected $promotion_manager; + /** * The constructor. * @@ -49,17 +65,23 @@ abstract class Base_Site_Information { * repository. * @param Meta_Surface $meta The meta surface. * @param Product_Helper $product_helper The product helper. + * @param Options_Helper $options_helper The options helper. + * @param Promotion_Manager $promotion_manager The promotion manager. */ public function __construct( Short_Link_Helper $short_link_helper, Wistia_Embed_Permission_Repository $wistia_embed_permission_repository, Meta_Surface $meta, - Product_Helper $product_helper + Product_Helper $product_helper, + Options_Helper $options_helper, + Promotion_Manager $promotion_manager ) { $this->short_link_helper = $short_link_helper; $this->wistia_embed_permission_repository = $wistia_embed_permission_repository; $this->meta = $meta; $this->product_helper = $product_helper; + $this->options_helper = $options_helper; + $this->promotion_manager = $promotion_manager; } /** @@ -70,16 +92,25 @@ public function __construct( */ public function get_site_information(): array { return [ - 'adminUrl' => \admin_url( 'admin.php' ), - 'linkParams' => $this->short_link_helper->get_query_params(), - 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), - 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), - 'site_name' => $this->meta->for_current_page()->site_name, - 'contentLocale' => \get_locale(), - 'userLocale' => \get_user_locale(), - 'isRtl' => \is_rtl(), - 'isPremium' => $this->product_helper->is_premium(), - 'siteIconUrl' => \get_site_icon_url(), + 'adminUrl' => \admin_url( 'admin.php' ), + 'linkParams' => $this->short_link_helper->get_query_params(), + 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), + 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), + 'site_name' => $this->meta->for_current_page()->site_name, + 'contentLocale' => \get_locale(), + 'userLocale' => \get_user_locale(), + 'isRtl' => \is_rtl(), + 'isPremium' => $this->product_helper->is_premium(), + 'siteIconUrl' => \get_site_icon_url(), + 'showSocial' => [ + 'facebook' => $this->options_helper->get( 'opengraph', false ), + 'twitter' => $this->options_helper->get( 'twitter', false ), + ], + 'sitewideSocialImage' => $this->options_helper->get( 'og_default_image' ), + // phpcs:ignore Generic.ControlStructures.DisallowYodaConditions -- Bug: squizlabs/PHP_CodeSniffer#2962. + 'isPrivateBlog' => ( (string) \get_option( 'blog_public' ) ) === '0', + 'currentPromotions' => $this->promotion_manager->get_current_promotions(), + 'blackFridayBlockEditorUrl' => ( $this->promotion_manager->is( 'black-friday-2023-checklist' ) ) ? $this->short_link_helper->get( 'https://yoa.st/black-friday-checklist' ) : '', ]; } @@ -91,17 +122,26 @@ public function get_site_information(): array { */ public function get_legacy_site_information(): array { return [ - 'adminUrl' => \admin_url( 'admin.php' ), - 'linkParams' => $this->short_link_helper->get_query_params(), - 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), - 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), - 'metabox' => [ + 'adminUrl' => \admin_url( 'admin.php' ), + 'linkParams' => $this->short_link_helper->get_query_params(), + 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), + 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), + 'sitewideSocialImage' => $this->options_helper->get( 'og_default_image' ), + // phpcs:ignore Generic.ControlStructures.DisallowYodaConditions -- Bug: squizlabs/PHP_CodeSniffer#2962. + 'isPrivateBlog' => ( (string) \get_option( 'blog_public' ) ) === '0', + 'currentPromotions' => $this->promotion_manager->get_current_promotions(), + 'blackFridayBlockEditorUrl' => ( $this->promotion_manager->is( 'black-friday-2023-checklist' ) ) ? $this->short_link_helper->get( 'https://yoa.st/black-friday-checklist' ) : '', + 'metabox' => [ 'site_name' => $this->meta->for_current_page()->site_name, 'contentLocale' => \get_locale(), 'userLocale' => \get_user_locale(), 'isRtl' => \is_rtl(), 'isPremium' => $this->product_helper->is_premium(), 'siteIconUrl' => \get_site_icon_url(), + 'showSocial' => [ + 'facebook' => $this->options_helper->get( 'opengraph', false ), + 'twitter' => $this->options_helper->get( 'twitter', false ), + ], ], ]; } diff --git a/src/editors/framework/site/post-site-information.php b/src/editors/framework/site/post-site-information.php index 4882642aa7a..33f2abbd53a 100644 --- a/src/editors/framework/site/post-site-information.php +++ b/src/editors/framework/site/post-site-information.php @@ -3,6 +3,7 @@ namespace Yoast\WP\SEO\Editors\Framework\Site; use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; +use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; @@ -28,36 +29,30 @@ class Post_Site_Information extends Base_Site_Information { */ private $alert_dismissal_action; - /** - * The promotion manager. - * - * @var Promotion_Manager $promotion_manager - */ - private $promotion_manager; - /** * Constructs the class. * - * @param Promotion_Manager $promotion_manager The promotion manager. * @param Short_Link_Helper $short_link_helper The short link helper. * @param Wistia_Embed_Permission_Repository $wistia_embed_permission_repository The wistia embed permission * repository. * @param Meta_Surface $meta The meta surface. * @param Product_Helper $product_helper The product helper. * @param Alert_Dismissal_Action $alert_dismissal_action The alert dismissal action. + * @param Options_Helper $options_helper The options helper. + * @param Promotion_Manager $promotion_manager The promotion manager. * * @return void */ public function __construct( - Promotion_Manager $promotion_manager, Short_Link_Helper $short_link_helper, Wistia_Embed_Permission_Repository $wistia_embed_permission_repository, Meta_Surface $meta, Product_Helper $product_helper, - Alert_Dismissal_Action $alert_dismissal_action + Alert_Dismissal_Action $alert_dismissal_action, + Options_Helper $options_helper, + Promotion_Manager $promotion_manager ) { - parent::__construct( $short_link_helper, $wistia_embed_permission_repository, $meta, $product_helper ); - $this->promotion_manager = $promotion_manager; + parent::__construct( $short_link_helper, $wistia_embed_permission_repository, $meta, $product_helper, $options_helper, $promotion_manager ); $this->alert_dismissal_action = $alert_dismissal_action; } @@ -82,9 +77,7 @@ public function get_legacy_site_information(): array { $data = [ 'dismissedAlerts' => $dismissed_alerts, - 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), - 'blackFridayBlockEditorUrl' => ( $this->promotion_manager->is( 'black-friday-2023-checklist' ) ) ? $this->short_link_helper->get( 'https://yoa.st/black-friday-checklist' ) : '', 'metabox' => [ 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), @@ -105,9 +98,7 @@ public function get_site_information(): array { $data = [ 'dismissedAlerts' => $dismissed_alerts, - 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), - 'blackFridayBlockEditorUrl' => ( $this->promotion_manager->is( 'black-friday-2023-checklist' ) ) ? $this->short_link_helper->get( 'https://yoa.st/black-friday-checklist' ) : '', 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), 'base_url' => $this->base_url_for_js(), diff --git a/src/editors/framework/site/term-site-information.php b/src/editors/framework/site/term-site-information.php index 90c212f2a5a..532de69aa35 100644 --- a/src/editors/framework/site/term-site-information.php +++ b/src/editors/framework/site/term-site-information.php @@ -4,24 +4,12 @@ use WP_Taxonomy; use WP_Term; -use Yoast\WP\SEO\Helpers\Options_Helper; -use Yoast\WP\SEO\Helpers\Product_Helper; -use Yoast\WP\SEO\Helpers\Short_Link_Helper; -use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; -use Yoast\WP\SEO\Surfaces\Meta_Surface; /** * The Term_Site_Information class. */ class Term_Site_Information extends Base_Site_Information { - /** - * The options helper. - * - * @var Options_Helper - */ - private $options_helper; - /** * The taxonomy. * @@ -36,27 +24,6 @@ class Term_Site_Information extends Base_Site_Information { */ private $term; - /** - * The constructor. - * - * @param Options_Helper $options_helper The options helper. - * @param Short_Link_Helper $short_link_helper The short link helper. - * @param Wistia_Embed_Permission_Repository $wistia_embed_permission_repository The wistia embed permission - * repository. - * @param Meta_Surface $meta The meta surface. - * @param Product_Helper $product_helper The product helper. - */ - public function __construct( - Options_Helper $options_helper, - Short_Link_Helper $short_link_helper, - Wistia_Embed_Permission_Repository $wistia_embed_permission_repository, - Meta_Surface $meta, - Product_Helper $product_helper - ) { - parent::__construct( $short_link_helper, $wistia_embed_permission_repository, $meta, $product_helper ); - $this->options_helper = $options_helper; - } - /** * Sets the term for the information object and retrieves its taxonomy. * diff --git a/src/helpers/image-helper.php b/src/helpers/image-helper.php index 560c7eb9b62..2f6dc77c7b3 100644 --- a/src/helpers/image-helper.php +++ b/src/helpers/image-helper.php @@ -366,7 +366,8 @@ public function get_attachment_id_from_settings( $setting ) { } /** - * Based on and image ID return array with the best variation of that image. If it's not saved to the DB, save it to an option. + * Based on and image ID return array with the best variation of that image. If it's not saved to the DB, save it + * to an option. * * @param string $setting The setting name. Should be company or person. * diff --git a/src/helpers/notification-helper.php b/src/helpers/notification-helper.php index b84d388b8af..a01e5b7bfdd 100644 --- a/src/helpers/notification-helper.php +++ b/src/helpers/notification-helper.php @@ -22,4 +22,53 @@ class Notification_Helper { public function restore_notification( Yoast_Notification $notification ) { return Yoast_Notification_Center::restore_notification( $notification ); } + + /** + * Return the notifications sorted on type and priority. (wrapper function) + * + * @codeCoverageIgnore + * + * @return array|Yoast_Notification[] Sorted Notifications + */ + public function get_sorted_notifications() { + $notification_center = Yoast_Notification_Center::get(); + + return $notification_center->get_sorted_notifications(); + } + + /** + * Check if the user has dismissed a notification. (wrapper function) + * + * @param Yoast_Notification $notification The notification to check for dismissal. + * @param int|null $user_id User ID to check on. + * + * @codeCoverageIgnore + * + * @return bool + */ + private function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) { + return Yoast_Notification_Center::is_notification_dismissed( $notification, $user_id ); + } + + /** + * Parses all the notifications to an array with just id, message, nonce, type and dismissed. + * + * @return array + */ + public function get_alerts(): array { + $all_notifications = $this->get_sorted_notifications(); + + return \array_map( + function ( $notification ) { + return [ + 'id' => $notification->get_id(), + 'message' => $notification->get_message(), + 'nonce' => $notification->get_nonce(), + 'type' => $notification->get_type(), + 'dismissed' => $this->is_notification_dismissed( $notification ), + ]; + }, + $all_notifications + ); + } } diff --git a/src/images/Application/image-content-extractor.php b/src/images/Application/image-content-extractor.php new file mode 100644 index 00000000000..156df9d71a5 --- /dev/null +++ b/src/images/Application/image-content-extractor.php @@ -0,0 +1,172 @@ +gather_images_wp( $content ); + } + + if ( ! $should_not_parse_content && \class_exists( DOMDocument::class ) ) { + + return $this->gather_images_DOMDocument( $content ); + } + + if ( \strpos( $content, 'src' ) === false ) { + // Nothing to do. + return []; + } + + $images = []; + $regexp = ']*src=("??)([^" >]*?)\\1[^>]*>'; + // Used modifiers iU to match case insensitive and make greedy quantifiers lazy. + if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) { + foreach ( $matches as $match ) { + $images[ $match[2] ] = 0; + } + } + + return $images; + } + + /** + * Gathers all images from content with WP's WP_HTML_Tag_Processor() and returns them along with their IDs, if + * possible. + * + * @param string $content The content. + * + * @return int[] An associated array of image IDs, keyed by their URL. + */ + protected function gather_images_wp( $content ) { + $processor = new WP_HTML_Tag_Processor( $content ); + $images = []; + + $query = [ + 'tag_name' => 'img', + ]; + + /** + * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. + * + * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. + * + * @api string The attribute to be used to extract image IDs from. + */ + $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); + while ( $processor->next_tag( $query ) ) { + $src = \htmlentities( $processor->get_attribute( 'src' ), ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), \get_bloginfo( 'charset' ) ); + $classes = $processor->get_attribute( $attribute ); + $id = $this->extract_id_of_classes( $classes ); + + $images[ $src ] = $id; + } + + return $images; + } + + /** + * Gathers all images from content with DOMDocument() and returns them along with their IDs, if possible. + * + * @param string $content The content. + * + * @return int[] An associated array of image IDs, keyed by their URL. + */ + protected function gather_images_domdocument( $content ) { + $images = []; + $charset = \get_bloginfo( 'charset' ); + + /** + * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. + * + * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. + * + * @api string The attribute to be used to extract image IDs from. + */ + $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); + + \libxml_use_internal_errors( true ); + $post_dom = new DOMDocument(); + $post_dom->loadHTML( '' . $content ); + \libxml_clear_errors(); + + foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { + $src = \htmlentities( $img->getAttribute( 'src' ), ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), $charset ); + $classes = $img->getAttribute( $attribute ); + + $id = $this->extract_id_of_classes( $classes ); + + $images[ $src ] = $id; + } + + return $images; + } + + /** + * Extracts image ID out of the image's classes. + * + * @param string $classes The classes assigned to the image. + * + * @return int The ID that's extracted from the classes. + */ + protected function extract_id_of_classes( $classes ) { + if ( ! $classes ) { + return 0; + } + + /** + * Filter 'wpseo_extract_id_pattern' - Allows filtering the regex patern to be used to extract image IDs from class/attribute names. + * + * Defaults to the pattern that extracts image IDs from core's `wp-image-` native format in image classes. + * + * @api string The regex pattern to be used to extract image IDs from class names. Empty string if the whole class/attribute should be returned. + */ + $pattern = \apply_filters( 'wpseo_extract_id_pattern', '/(?options_helper = $options_helper; $this->admin_asset_manager = $admin_asset_manager; $this->first_time_configuration_notice_helper = $first_time_configuration_notice_helper; + $this->new_dashboard_ui_conditional = $new_dashboard_ui_conditional; } /** @@ -102,7 +113,7 @@ public function first_time_configuration_notice() { $this->admin_asset_manager->enqueue_style( 'monorepo' ); $title = $this->first_time_configuration_notice_helper->get_first_time_configuration_title(); - $link_url = \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ); + $link_url = ( $this->new_dashboard_ui_conditional->is_met() ) ? \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ) : \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ); if ( ! $this->first_time_configuration_notice_helper->should_show_alternate_message() ) { $content = \sprintf( @@ -138,13 +149,12 @@ public function first_time_configuration_notice() { echo ''; diff --git a/src/integrations/admin/installation-success-integration.php b/src/integrations/admin/installation-success-integration.php index 9c232ca437b..6f763ce213d 100644 --- a/src/integrations/admin/installation-success-integration.php +++ b/src/integrations/admin/installation-success-integration.php @@ -4,6 +4,7 @@ use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; @@ -27,6 +28,13 @@ class Installation_Success_Integration implements Integration_Interface { */ protected $product_helper; + /** + * The New Dashboard UI conditional. + * + * @var New_Dashboard_Ui_Conditional + */ + private $new_dashboard_ui_conditional; + /** * {@inheritDoc} */ @@ -37,12 +45,18 @@ public static function get_conditionals() { /** * Installation_Success_Integration constructor. * - * @param Options_Helper $options_helper The options helper. - * @param Product_Helper $product_helper The product helper. + * @param Options_Helper $options_helper The options helper. + * @param Product_Helper $product_helper The product helper. + * @param New_Dashboard_Ui_Conditional $new_dashboard_ui_conditional The new dashboard UI conditional. */ - public function __construct( Options_Helper $options_helper, Product_Helper $product_helper ) { - $this->options_helper = $options_helper; - $this->product_helper = $product_helper; + public function __construct( + Options_Helper $options_helper, + Product_Helper $product_helper, + New_Dashboard_Ui_Conditional $new_dashboard_ui_conditional + ) { + $this->options_helper = $options_helper; + $this->product_helper = $product_helper; + $this->new_dashboard_ui_conditional = $new_dashboard_ui_conditional; } /** @@ -127,12 +141,14 @@ public function enqueue_assets() { $asset_manager->enqueue_style( 'tailwind' ); $asset_manager->enqueue_style( 'monorepo' ); + $ftc_url = ( $this->new_dashboard_ui_conditional->is_met() ) ? \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ) : \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ); + $asset_manager->localize_script( 'installation-success', 'wpseoInstallationSuccess', [ 'pluginUrl' => \esc_url( \plugins_url( '', \WPSEO_FILE ) ), - 'firstTimeConfigurationUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ), + 'firstTimeConfigurationUrl' => $ftc_url, 'dashboardUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard' ) ), ] ); diff --git a/src/integrations/admin/integrations-page.php b/src/integrations/admin/integrations-page.php index 5fec0c8a03a..4a950f68e46 100644 --- a/src/integrations/admin/integrations-page.php +++ b/src/integrations/admin/integrations-page.php @@ -8,12 +8,11 @@ use WP_Recipe_Maker; use WPSEO_Addon_Manager; use WPSEO_Admin_Asset_Manager; -use WPSEO_Shortlinker; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\Jetpack_Conditional; use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional; -use Yoast\WP\SEO\Conditionals\Third_Party\Jetpack_Boost_Active_Conditional; -use Yoast\WP\SEO\Conditionals\Third_Party\Jetpack_Boost_Not_Premium_Conditional; + + use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Woocommerce_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; @@ -115,10 +114,8 @@ public function enqueue_assets() { $this->admin_asset_manager->enqueue_script( 'integrations-page' ); - $elementor_conditional = new Elementor_Activated_Conditional(); - $jetpack_conditional = new Jetpack_Conditional(); - $jetpack_boost_active_conditional = new Jetpack_Boost_Active_Conditional(); - $jetpack_boost_not_premium_conditional = new Jetpack_Boost_Not_Premium_Conditional(); + $elementor_conditional = new Elementor_Activated_Conditional(); + $jetpack_conditional = new Jetpack_Conditional(); $woocommerce_seo_file = 'wpseo-woocommerce/wpseo-woocommerce.php'; $acf_seo_file = 'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php'; @@ -126,7 +123,6 @@ public function enqueue_assets() { $algolia_file = 'wp-search-with-algolia/algolia.php'; $old_algolia_file = 'search-by-algolia-instant-relevant-results/algolia.php'; - $host = \YoastSEO()->helpers->url->get_url_host( \get_site_url() ); $addon_manager = new WPSEO_Addon_Manager(); $woocommerce_seo_installed = $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ); @@ -139,8 +135,6 @@ public function enqueue_assets() { $acf_active = \class_exists( 'acf' ); $algolia_active = \is_plugin_active( $algolia_file ); $edd_active = \class_exists( Easy_Digital_Downloads::class ); - $jetpack_boost_active = $jetpack_boost_active_conditional->is_met(); - $jetpack_boost_premium = ( ! $jetpack_boost_not_premium_conditional->is_met() ); $old_algolia_active = \is_plugin_active( $old_algolia_file ); $tec_active = \class_exists( Events_Schema::class ); $ssp_active = \class_exists( PodcastEpisode::class ); @@ -199,12 +193,6 @@ public function enqueue_assets() { 'mastodon_active' => $mastodon_active, 'is_multisite' => \is_multisite(), 'plugin_url' => \plugins_url( '', \WPSEO_FILE ), - 'jetpack-boost_active' => $jetpack_boost_active, - 'jetpack-boost_premium' => $jetpack_boost_premium, - 'jetpack-boost_logo_link' => WPSEO_Shortlinker::get( 'https://yoa.st/integrations-logo-jetpack-boost' ), - 'jetpack-boost_get_link' => WPSEO_Shortlinker::get( 'https://yoa.st/integrations-get-jetpack-boost?domain=' . $host ), - 'jetpack-boost_upgrade_link' => WPSEO_Shortlinker::get( 'https://yoa.st/integrations-upgrade-jetpack-boost?domain=' . $host ), - 'jetpack-boost_learn_more_link' => \admin_url( 'admin.php?page=jetpack-boost' ), ] ); } diff --git a/src/integrations/admin/old-configuration-integration.php b/src/integrations/admin/old-configuration-integration.php index 095a20defda..77889174c7f 100644 --- a/src/integrations/admin/old-configuration-integration.php +++ b/src/integrations/admin/old-configuration-integration.php @@ -3,6 +3,7 @@ namespace Yoast\WP\SEO\Integrations\Admin; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** @@ -64,7 +65,8 @@ public function redirect_to_new_configuration() { if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_configurator' ) { return; } - \wp_safe_redirect( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ), 302, 'Yoast SEO' ); + $redirect_url = ( ( new New_Dashboard_Ui_Conditional() )->is_met() ) ? 'admin.php?page=wpseo_dashboard#/first-time-configuration' : 'admin.php?page=wpseo_dashboard#top#first-time-configuration'; + \wp_safe_redirect( \admin_url( $redirect_url ), 302, 'Yoast SEO' ); exit; } } diff --git a/src/integrations/admin/workouts-integration.php b/src/integrations/admin/workouts-integration.php index fe3b0fb25f2..9a9290284b8 100644 --- a/src/integrations/admin/workouts-integration.php +++ b/src/integrations/admin/workouts-integration.php @@ -5,6 +5,7 @@ use WPSEO_Addon_Manager; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; @@ -43,6 +44,13 @@ class Workouts_Integration implements Integration_Interface { */ private $product_helper; + /** + * The New Dashboard UI conditional. + * + * @var New_Dashboard_Ui_Conditional + */ + private $new_dashboard_ui_conditional; + /** * {@inheritDoc} */ @@ -53,21 +61,24 @@ public static function get_conditionals() { /** * Workouts_Integration constructor. * - * @param WPSEO_Addon_Manager $addon_manager The addon manager. - * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. - * @param Options_Helper $options_helper The options helper. - * @param Product_Helper $product_helper The product helper. + * @param WPSEO_Addon_Manager $addon_manager The addon manager. + * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. + * @param Options_Helper $options_helper The options helper. + * @param Product_Helper $product_helper The product helper. + * @param New_Dashboard_Ui_Conditional $new_dashboard_ui_conditional The new dashboard UI conditional. */ public function __construct( WPSEO_Addon_Manager $addon_manager, WPSEO_Admin_Asset_Manager $admin_asset_manager, Options_Helper $options_helper, - Product_Helper $product_helper + Product_Helper $product_helper, + New_Dashboard_Ui_Conditional $new_dashboard_ui_conditional ) { - $this->addon_manager = $addon_manager; - $this->admin_asset_manager = $admin_asset_manager; - $this->options_helper = $options_helper; - $this->product_helper = $product_helper; + $this->addon_manager = $addon_manager; + $this->admin_asset_manager = $admin_asset_manager; + $this->options_helper = $options_helper; + $this->product_helper = $product_helper; + $this->new_dashboard_ui_conditional = $new_dashboard_ui_conditional; } /** @@ -144,6 +155,7 @@ public function enqueue_assets() { $this->admin_asset_manager->enqueue_style( 'workouts' ); $workouts_option = $this->get_workouts_option(); + $ftc_url = ( $this->new_dashboard_ui_conditional->is_met() ) ? \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ) : \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ); $this->admin_asset_manager->enqueue_script( 'workouts' ); $this->admin_asset_manager->localize_script( @@ -155,7 +167,7 @@ public function enqueue_assets() { 'pluginUrl' => \esc_url( \plugins_url( '', \WPSEO_FILE ) ), 'toolsPageUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_tools' ) ), 'usersPageUrl' => \esc_url( \admin_url( 'users.php' ) ), - 'firstTimeConfigurationUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ), + 'firstTimeConfigurationUrl' => $ftc_url, 'isPremium' => $this->product_helper->is_premium(), 'upsellText' => $this->get_upsell_text(), 'upsellLink' => $this->get_upsell_link(), diff --git a/src/integrations/alerts/jetpack-boost-pre-publish.php b/src/integrations/alerts/jetpack-boost-pre-publish.php deleted file mode 100644 index 9709a8b0b9c..00000000000 --- a/src/integrations/alerts/jetpack-boost-pre-publish.php +++ /dev/null @@ -1,26 +0,0 @@ -context_memoizer = $context_memoizer; $this->replace_vars = $replace_vars; $this->helpers = $helpers; $this->indexable_repository = $indexable_repository; - $this->request_helper = $request_helper; $this->base_path = \WPSEO_PATH . 'blocks/dynamic-blocks/'; } @@ -99,7 +88,7 @@ public function present( $attributes ) { $presenter = new Breadcrumbs_Presenter(); // $this->context_memoizer->for_current_page only works on the frontend. To render the right breadcrumb in the // editor, we need the repository. - if ( $this->request_helper->is_rest_request() || \is_admin() ) { + if ( \wp_is_serving_rest_request() || \is_admin() ) { $post_id = \get_the_ID(); if ( $post_id ) { $indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); diff --git a/src/integrations/front-end-integration.php b/src/integrations/front-end-integration.php index 1d895366ada..fc739c9d0d3 100644 --- a/src/integrations/front-end-integration.php +++ b/src/integrations/front-end-integration.php @@ -7,7 +7,6 @@ use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Context\Meta_Tags_Context; use Yoast\WP\SEO\Helpers\Options_Helper; -use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter; use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter; @@ -42,13 +41,6 @@ class Front_End_Integration implements Integration_Interface { */ protected $options; - /** - * Represents the request helper. - * - * @var Request_Helper - */ - protected $request; - /** * The helpers surface. * @@ -207,7 +199,6 @@ public static function get_conditionals() { * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. * @param ContainerInterface $service_container The DI container. * @param Options_Helper $options The options helper. - * @param Request_Helper $request The request helper. * @param Helpers_Surface $helpers The helpers surface. * @param WPSEO_Replace_Vars $replace_vars The replace vars helper. */ @@ -215,14 +206,12 @@ public function __construct( Meta_Tags_Context_Memoizer $context_memoizer, ContainerInterface $service_container, Options_Helper $options, - Request_Helper $request, Helpers_Surface $helpers, WPSEO_Replace_Vars $replace_vars ) { $this->container = $service_container; $this->context_memoizer = $context_memoizer; $this->options = $options; - $this->request = $request; $this->helpers = $helpers; $this->replace_vars = $replace_vars; } @@ -353,7 +342,7 @@ public function filter_robots_presenter( $presenters ) { return $presenters; } - if ( $this->request->is_rest_request() ) { + if ( \wp_is_serving_rest_request() ) { return $presenters; } @@ -559,7 +548,7 @@ public function should_title_presenter_be_removed() { */ private function maybe_remove_title_presenter( $presenters ) { // Do not remove the title if we're on a REST request. - if ( $this->request->is_rest_request() ) { + if ( \wp_is_serving_rest_request() ) { return $presenters; } diff --git a/src/integrations/third-party/elementor.php b/src/integrations/third-party/elementor.php index 18cbc1d5ccc..20cfaf2dc84 100644 --- a/src/integrations/third-party/elementor.php +++ b/src/integrations/third-party/elementor.php @@ -6,7 +6,6 @@ use WP_Screen; use WPSEO_Admin_Asset_Manager; use WPSEO_Admin_Recommended_Replace_Vars; -use WPSEO_Language_Utils; use WPSEO_Meta; use WPSEO_Metabox_Analysis_Inclusive_Language; use WPSEO_Metabox_Analysis_Readability; @@ -16,7 +15,6 @@ use WPSEO_Replace_Vars; use WPSEO_Utils; use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional; -use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository; use Yoast\WP\SEO\Elementor\Infrastructure\Request_Post; use Yoast\WP\SEO\Helpers\Capability_Helper; @@ -408,16 +406,16 @@ public function enqueue() { 'enabled_features' => WPSEO_Utils::retrieve_enabled_features(), ]; - $woocommerce_conditional = new WooCommerce_Conditional(); - $permalink = $this->get_permalink(); + $permalink = $this->get_permalink(); + $page_on_front = (int) \get_option( 'page_on_front' ); + $homepage_is_page = \get_option( 'show_on_front' ) === 'page'; + $is_front_page = $homepage_is_page && $page_on_front === $post_id; $script_data = [ 'metabox' => $this->get_metabox_script_data( $permalink ), - 'userLanguageCode' => WPSEO_Language_Utils::get_language( \get_user_locale() ), 'isPost' => true, 'isBlockEditor' => WP_Screen::get()->is_block_editor(), 'isElementorEditor' => true, - 'isWooCommerceActive' => $woocommerce_conditional->is_met(), 'postStatus' => \get_post_status( $post_id ), 'postType' => \get_post_type( $post_id ), 'analysis' => [ @@ -425,6 +423,7 @@ public function enqueue() { 'worker' => $worker_script_data, ], 'usedKeywordsNonce' => \wp_create_nonce( 'wpseo-keyword-usage-and-post-types' ), + 'isFrontPage' => $is_front_page, ]; /** diff --git a/src/integrations/third-party/wincher-publish.php b/src/integrations/third-party/wincher-publish.php index ba378fa50de..11c4b922e81 100644 --- a/src/integrations/third-party/wincher-publish.php +++ b/src/integrations/third-party/wincher-publish.php @@ -105,8 +105,12 @@ public static function get_conditionals() { * Determines whether the current request is a REST request. * * @return bool Whether the request is a REST request. + * + * @deprecated 23.6 + * @codeCoverageIgnore */ public function is_rest_request() { + \_deprecated_function( __METHOD__, 'Yoast SEO 23.6', 'wp_is_serving_rest_request' ); return \defined( 'REST_REQUEST' ) && \REST_REQUEST; } @@ -153,7 +157,7 @@ public function track_after_rest_api_request( $post ) { * @return void */ public function track_after_post_request( $post_id, $post ) { - if ( $this->is_rest_request() ) { + if ( \wp_is_serving_rest_request() ) { return; } diff --git a/src/integrations/watchers/search-engines-discouraged-watcher.php b/src/integrations/watchers/search-engines-discouraged-watcher.php index 36f27583fcc..0ba2a5563b2 100644 --- a/src/integrations/watchers/search-engines-discouraged-watcher.php +++ b/src/integrations/watchers/search-engines-discouraged-watcher.php @@ -101,6 +101,8 @@ public function __construct( public function register_hooks() { \add_action( 'admin_init', [ $this, 'manage_search_engines_discouraged_notification' ] ); + \add_action( 'update_option_blog_public', [ $this, 'restore_ignore_option' ] ); + /* * The `admin_notices` hook fires on single site admin pages vs. * `network_admin_notices` which fires on multisite admin pages and @@ -230,4 +232,15 @@ protected function notification() { ] ); } + + /** + * Should restore the ignore option for the search engines discouraged notice. + * + * @return void + */ + public function restore_ignore_option() { + if ( ! $this->search_engines_are_discouraged() ) { + $this->options_helper->set( 'ignore_search_engines_discouraged_notice', false ); + } + } } diff --git a/src/presenters/admin/indexing-notification-presenter.php b/src/presenters/admin/indexing-notification-presenter.php index bb86853833f..f57b343a971 100644 --- a/src/presenters/admin/indexing-notification-presenter.php +++ b/src/presenters/admin/indexing-notification-presenter.php @@ -53,8 +53,8 @@ public function __construct( $short_link_helper, $total_unindexed, $reason ) { * @return string The HTML string representation of the notification. */ public function present() { - $notification_text = '

    ' . $this->get_message( $this->reason ) . '

    '; - $notification_text .= '

    ' . $this->get_time_estimate( $this->total_unindexed ) . '

    '; + $notification_text = '

    ' . $this->get_message( $this->reason ); + $notification_text .= $this->get_time_estimate( $this->total_unindexed ) . '

    '; $notification_text .= ''; $notification_text .= \esc_html__( 'Start SEO data optimization', 'wordpress-seo' ); $notification_text .= ''; @@ -84,16 +84,16 @@ protected function get_message( $reason ) { $text = \esc_html__( 'Because of a change in your tag base setting, some of your SEO data needs to be reprocessed.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_POST_TYPE_MADE_PUBLIC: - $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your post types. Please help us do that by running the SEO data optimization. ', 'wordpress-seo' ); + $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your post types. Please help us do that by running the SEO data optimization.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_TAXONOMY_MADE_PUBLIC: - $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization. ', 'wordpress-seo' ); + $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_ATTACHMENTS_MADE_ENABLED: - $text = \esc_html__( 'It looks like you\'ve enabled media pages. We recommend that you help us to re-analyze your site by running the SEO data optimization. ', 'wordpress-seo' ); + $text = \esc_html__( 'It looks like you\'ve enabled media pages. We recommend that you help us to re-analyze your site by running the SEO data optimization.', 'wordpress-seo' ); break; default: - $text = \esc_html__( 'You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. ', 'wordpress-seo' ); + $text = \esc_html__( 'You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.', 'wordpress-seo' ); } /** @@ -114,14 +114,14 @@ protected function get_message( $reason ) { */ protected function get_time_estimate( $total_unindexed ) { if ( $total_unindexed < 400 ) { - return \esc_html__( 'We estimate this will take less than a minute.', 'wordpress-seo' ); + return \esc_html__( ' We estimate this will take less than a minute.', 'wordpress-seo' ); } if ( $total_unindexed < 2500 ) { - return \esc_html__( 'We estimate this will take a couple of minutes.', 'wordpress-seo' ); + return \esc_html__( ' We estimate this will take a couple of minutes.', 'wordpress-seo' ); } - $estimate = \esc_html__( 'We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:', 'wordpress-seo' ); + $estimate = \esc_html__( ' We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:', 'wordpress-seo' ); $estimate .= '
      '; $estimate .= '
    • '; $estimate .= \sprintf( diff --git a/src/presenters/admin/migration-error-presenter.php b/src/presenters/admin/migration-error-presenter.php index 9ef789fbb55..b8eb52e31b6 100644 --- a/src/presenters/admin/migration-error-presenter.php +++ b/src/presenters/admin/migration-error-presenter.php @@ -37,6 +37,11 @@ public function __construct( $migration_error ) { * @return string The error HTML. */ public function present() { + $header = \sprintf( + /* translators: %s: Yoast SEO. */ + \esc_html__( '%s is unable to create database tables', 'wordpress-seo' ), + 'Yoast SEO' + ); $message = \sprintf( /* translators: %s: Yoast SEO. */ \esc_html__( '%s had problems creating the database tables needed to speed up your site.', 'wordpress-seo' ), @@ -61,7 +66,8 @@ public function present() { ); return \sprintf( - '

      %1$s

      %2$s

      %3$s

      %4$s
      ', + '

      %1$s

      %2$s

      %3$s

      %4$s

      %5$s
      ', + $header, $message, $support, $reassurance, diff --git a/src/presenters/admin/notice-presenter.php b/src/presenters/admin/notice-presenter.php index 1b2b0f23790..1135c42090f 100644 --- a/src/presenters/admin/notice-presenter.php +++ b/src/presenters/admin/notice-presenter.php @@ -104,15 +104,17 @@ public function present() { $out .= '
      '; $out .= ''; $out .= \sprintf( - '

      %s

      ', + '

      %s

      ', \esc_html( $this->title ) ); $out .= '
      '; + $out .= '
      '; $out .= '

      ' . $this->content . '

      '; if ( ! \is_null( $this->button ) ) { $out .= '

      ' . $this->button . '

      '; } $out .= '
      '; + $out .= '
    '; if ( ! \is_null( $this->image_filename ) ) { $out .= ''; diff --git a/src/presenters/admin/sidebar-presenter.php b/src/presenters/admin/sidebar-presenter.php index 8958bc19b9a..876ecdaf3dd 100644 --- a/src/presenters/admin/sidebar-presenter.php +++ b/src/presenters/admin/sidebar-presenter.php @@ -17,7 +17,7 @@ class Sidebar_Presenter extends Abstract_Presenter { * @return string The sidebar HTML. */ public function present() { - $title = \__( '30% OFF | Code: BF2024', 'wordpress-seo' ); + $title = \__( '30% OFF - BLACK FRIDAY', 'wordpress-seo' ); $assets_uri = \trailingslashit( \plugin_dir_url( \WPSEO_FILE ) ); $buy_yoast_seo_shortlink = WPSEO_Shortlinker::get( 'https://yoa.st/jj' ); @@ -90,9 +90,10 @@ class="attachment-full size-full content-visible"

    classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) { + echo \esc_html__( 'Only $/€/£99 per year (ex VAT)', 'wordpress-seo' ), '
    '; + } ?> -
    diff --git a/src/promotions/domain/black-friday-promotion.php b/src/promotions/domain/black-friday-promotion.php index 9e8d9f5c2af..e5cdd6ebea3 100644 --- a/src/promotions/domain/black-friday-promotion.php +++ b/src/promotions/domain/black-friday-promotion.php @@ -13,7 +13,7 @@ class Black_Friday_Promotion extends Abstract_Promotion implements Promotion_Int public function __construct() { parent::__construct( 'black-friday-2024-promotion', - new Time_Interval( \gmmktime( 12, 00, 00, 11, 28, 2024 ), \gmmktime( 12, 00, 00, 12, 3, 2024 ) ) + new Time_Interval( \gmmktime( 10, 00, 00, 11, 28, 2024 ), \gmmktime( 10, 00, 00, 12, 3, 2024 ) ) ); } } diff --git a/src/surfaces/helpers-surface.php b/src/surfaces/helpers-surface.php index b0e4e753387..9c0a020613b 100644 --- a/src/surfaces/helpers-surface.php +++ b/src/surfaces/helpers-surface.php @@ -35,7 +35,6 @@ * @property Helpers\Primary_Term_Helper $primary_term * @property Helpers\Product_Helper $product * @property Helpers\Redirect_Helper $redirect - * @property Helpers\Request_Helper $request * @property Helpers\Require_File_Helper $require_file * @property Helpers\Robots_Helper $robots * @property Helpers\Short_Link_Helper $short_link diff --git a/tests/Unit/Builders/Indexable_Link_Builder/Abstract_Indexable_Link_Builder_TestCase.php b/tests/Unit/Builders/Indexable_Link_Builder/Abstract_Indexable_Link_Builder_TestCase.php index a1258c61dbf..09ea9828d44 100644 --- a/tests/Unit/Builders/Indexable_Link_Builder/Abstract_Indexable_Link_Builder_TestCase.php +++ b/tests/Unit/Builders/Indexable_Link_Builder/Abstract_Indexable_Link_Builder_TestCase.php @@ -11,6 +11,7 @@ use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Post_Helper; use Yoast\WP\SEO\Helpers\Url_Helper; +use Yoast\WP\SEO\Images\Application\Image_Content_Extractor; use Yoast\WP\SEO\Models\SEO_Links; use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Repositories\SEO_Links_Repository; @@ -84,6 +85,13 @@ abstract class Abstract_Indexable_Link_Builder_TestCase extends TestCase { */ protected $image_url; + /** + * The Image content extractor instance. + * + * @var Mockery\MockInterface|Image_Content_Extractor + */ + protected $image_content_extractor; + /** * Sets up the tests. * @@ -92,20 +100,22 @@ abstract class Abstract_Indexable_Link_Builder_TestCase extends TestCase { protected function set_up() { parent::set_up(); - $this->seo_links_repository = Mockery::mock( SEO_Links_Repository::class ); - $this->url_helper = Mockery::mock( Url_Helper::class ); - $this->indexable_repository = Mockery::mock( Indexable_Repository::class ); - $this->image_helper = Mockery::mock( Image_Helper::class ); - $this->post_helper = Mockery::mock( Post_Helper::class ); - $this->options_helper = Mockery::mock( Options_Helper::class ); - $this->indexable_helper = Mockery::mock( Indexable_Helper::class ); + $this->seo_links_repository = Mockery::mock( SEO_Links_Repository::class ); + $this->url_helper = Mockery::mock( Url_Helper::class ); + $this->indexable_repository = Mockery::mock( Indexable_Repository::class ); + $this->image_helper = Mockery::mock( Image_Helper::class ); + $this->post_helper = Mockery::mock( Post_Helper::class ); + $this->options_helper = Mockery::mock( Options_Helper::class ); + $this->indexable_helper = Mockery::mock( Indexable_Helper::class ); + $this->image_content_extractor = Mockery::mock( Image_Content_Extractor::class ); $this->instance = new Indexable_Link_Builder( $this->seo_links_repository, $this->url_helper, $this->post_helper, $this->options_helper, - $this->indexable_helper + $this->indexable_helper, + $this->image_content_extractor ); $this->instance->set_dependencies( $this->indexable_repository, $this->image_helper ); diff --git a/tests/Unit/Builders/Indexable_Link_Builder/Build_Test.php b/tests/Unit/Builders/Indexable_Link_Builder/Build_Test.php index 23de91e49f6..49878393eb8 100644 --- a/tests/Unit/Builders/Indexable_Link_Builder/Build_Test.php +++ b/tests/Unit/Builders/Indexable_Link_Builder/Build_Test.php @@ -25,7 +25,7 @@ final class Build_Test extends Abstract_Indexable_Link_Builder_TestCase { /** * Data provider to test the build. * - * @return array The test data. + * @return array> The test data. */ public static function build_provider() { return [ @@ -36,9 +36,7 @@ public static function build_provider() { ', SEO_Links::TYPE_EXTERNAL, false, - false, - false, - false, + [], ], [ ' @@ -47,9 +45,11 @@ public static function build_provider() { ', SEO_Links::TYPE_EXTERNAL_IMAGE, true, - false, - false, - true, + + [ + 'https://link.com/newly-added-in-post' => 1, + 'https://link.com/already-existed-in-post' => 2, + ], ], [ ' @@ -58,9 +58,11 @@ public static function build_provider() { ', SEO_Links::TYPE_EXTERNAL_IMAGE, true, - true, - false, - false, + + [ + 'https://link.com/newly-added-in-post' => 1, + 'https://link.com/already-existed-in-post' => 2, + ], ], [ ' @@ -69,9 +71,11 @@ public static function build_provider() { ', SEO_Links::TYPE_EXTERNAL_IMAGE, true, - false, - true, - true, + + [ + 'https://link.com/newly-added-in-post' => 1, + 'https://link.com/already-existed-in-post' => 2, + ], ], [ ' @@ -80,9 +84,11 @@ public static function build_provider() { ', SEO_Links::TYPE_EXTERNAL_IMAGE, true, - false, - true, - true, + + [ + 'https://link.com/newly-added-in-post' => 1, + 'https://link.com/already-existed-in-post' => 2, + ], ], ]; } @@ -98,16 +104,14 @@ public static function build_provider() { * * @dataProvider build_provider * - * @param string $content The content. - * @param string $link_type The link type. - * @param bool $is_image Whether the link is an image. - * @param bool $ignore_content_scan Whether content scanning should be ignored. - * @param bool $should_content_regex Whether the image id should be extracted with a regex. - * @param bool $should_doc_scan Whether the doc document should be used. + * @param string $content The content. + * @param string $link_type The link type. + * @param bool $is_image Whether the link is an image. + * @param array $images The images that are in the content. * * @return void */ - public function test_build( $content, $link_type, $is_image, $ignore_content_scan, $should_content_regex, $should_doc_scan ) { + public function test_build( $content, $link_type, $is_image, $images ) { $indexable = Mockery::mock( Indexable_Mock::class ); $indexable->id = 1; $indexable->object_id = 2; @@ -116,6 +120,13 @@ public function test_build( $content, $link_type, $is_image, $ignore_content_sca $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); $this->post_helper->expects( 'get_post' )->once()->with( 2 )->andReturn( 'post' ); + if ( $is_image ) { + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( $images ); + } + else { + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [] ); + + } Functions\expect( 'setup_postdata' )->once()->with( 'post' ); Functions\expect( 'apply_filters' )->once()->with( 'the_content', $content )->andReturn( $content ); Functions\expect( 'wp_reset_postdata' )->once(); @@ -144,21 +155,6 @@ public function test_build( $content, $link_type, $is_image, $ignore_content_sca Functions\expect( 'home_url' )->once()->andReturn( 'https://site.com' ); Functions\expect( 'wp_parse_url' )->once()->with( 'https://site.com' )->andReturn( $parsed_home_url ); Functions\expect( 'wp_parse_url' )->once()->with( 'https://site.com/page' )->andReturn( $parsed_page_url ); - if ( $should_doc_scan ) { - Functions\expect( 'apply_filters' ) - ->once() - ->with( 'wpseo_image_attribute_containing_id', 'class' ) - ->andReturn( 'class' ); - } - if ( $should_content_regex ) { - Functions\expect( 'apply_filters' ) - ->twice() - ->with( 'wpseo_extract_id_pattern', '/(?andReturn( '/(?once()->with( 'wpseo_force_skip_image_content_parsing', false )->andReturn( true ); - } // Inside create_links->create_internal_link method. Functions\expect( 'wp_parse_url' )->once()->with( 'https://link.com/newly-added-in-post' )->andReturn( $parsed_new_link_url ); @@ -261,6 +257,8 @@ public function test_build_target_indexable_does_not_exist() { $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); $this->post_helper->expects( 'get_post' )->once()->with( 2 )->andReturn( 'post' ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [] ); + Functions\expect( 'setup_postdata' )->once()->with( 'post' ); Filters\expectApplied( 'the_content' )->with( $content )->andReturnFirstArg(); Functions\expect( 'wp_reset_postdata' )->once(); @@ -375,6 +373,7 @@ public function test_build_no_links() { $indexable->permalink = 'https://site.com/page'; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [] ); $this->seo_links_repository ->expects( 'find_all_by_indexable_id' ) @@ -406,6 +405,8 @@ public function test_build_ignore_content_scan( $input_content, $output_result ) $indexable->permalink = 'https://site.com/page'; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [] ); + Functions\expect( 'apply_filters' )->andReturn( true ); $this->seo_links_repository diff --git a/tests/Unit/Builders/Indexable_Link_Builder/Create_Internal_Link_Test.php b/tests/Unit/Builders/Indexable_Link_Builder/Create_Internal_Link_Test.php index 55dc508e7ba..50b3c21d9a2 100644 --- a/tests/Unit/Builders/Indexable_Link_Builder/Create_Internal_Link_Test.php +++ b/tests/Unit/Builders/Indexable_Link_Builder/Create_Internal_Link_Test.php @@ -44,6 +44,7 @@ public function test_build_create_internal_link() { $model->type = SEO_Links::TYPE_INTERNAL_IMAGE; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [ 'http://basic.wordpress.test/wp-content/uploads/2022/11/WordPress8.jpg?quality=90&grain=0.5' => 2 ] ); Functions\stubs( [ @@ -122,6 +123,7 @@ public function test_build_create_internal_link_disable_attachment_true_file_doe $model->target_post_id = 2; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [ 'http://basic.wordpress.test/wp-content/uploads/2022/11/WordPress8.jpg?quality=90&grain=0.5' => 2 ] ); // Executed in build->create_links->create_internal_link. Functions\stubs( @@ -155,12 +157,6 @@ public function test_build_create_internal_link_disable_attachment_true_file_doe // Executed in build->create_links->create_internal_link. $this->expect_seo_links_repository_query_create( $indexable, $model ); - // Executed in build->create_links->create_internal_link->WPSEO_Image_Utils::get_attachment_by_url. - Functions\expect( 'wp_get_upload_dir' ) - ->with( 'http://basic.wordpress.test/wp-content/uploads' ) - ->once() - ->andReturn( [ 'baseurl' => 'http://basic.wordpress.test/wp-content/uploads' ] ); - $this->expect_build_permalink( 'http://basic.wordpress.test' ); $this->options_helper @@ -203,6 +199,7 @@ public function test_build_create_internal_link_disable_attachment_true_file_exi $model->width = null; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [ 'http://basic.wordpress.test/wp-content/uploads/2022/11/WordPress8.jpg?quality=90&grain=0.5' => 2 ] ); // Executed in build->create_links->create_internal_link. Functions\stubs( @@ -268,6 +265,7 @@ public function test_build_create_internal_link_disable_attachment_true_file_not $model->target_post_id = 3; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [ 'http://basic.wordpress.test/wp-content/uploads/2022/11/WordPress8.jpg?quality=90&grain=0.5' => 2 ] ); Functions\stubs( [ @@ -348,6 +346,7 @@ public function test_build_create_internal_link_disable_attachment_true_get_atta $model->target_post_id = 2; $this->indexable_helper->expects( 'should_index_indexable' )->once()->andReturn( true ); + $this->image_content_extractor->expects( 'gather_images' )->once()->andReturn( [ 'http://basic.wordpress.test/wp-content/uploads/2022/11/WordPress8.jpg?quality=90&grain=0.5' => 2 ] ); Functions\stubs( [ diff --git a/tests/Unit/Builders/Indexable_Link_Builder/Get_Permalink_Test.php b/tests/Unit/Builders/Indexable_Link_Builder/Get_Permalink_Test.php index 7495510984d..0f2072c6566 100644 --- a/tests/Unit/Builders/Indexable_Link_Builder/Get_Permalink_Test.php +++ b/tests/Unit/Builders/Indexable_Link_Builder/Get_Permalink_Test.php @@ -26,7 +26,8 @@ protected function set_up() { $this->url_helper, $this->post_helper, $this->options_helper, - $this->indexable_helper + $this->indexable_helper, + $this->image_content_extractor ); $this->instance->set_dependencies( $this->indexable_repository, $this->image_helper ); diff --git a/tests/Unit/Builders/Indexable_Link_Builder/Update_Incoming_Links_For_Related_Test.php b/tests/Unit/Builders/Indexable_Link_Builder/Update_Incoming_Links_For_Related_Test.php index 92a04c7b4c3..ec0b1b5244f 100644 --- a/tests/Unit/Builders/Indexable_Link_Builder/Update_Incoming_Links_For_Related_Test.php +++ b/tests/Unit/Builders/Indexable_Link_Builder/Update_Incoming_Links_For_Related_Test.php @@ -28,7 +28,8 @@ protected function set_up() { $this->url_helper, $this->post_helper, $this->options_helper, - $this->indexable_helper + $this->indexable_helper, + $this->image_content_extractor ); $this->instance->set_dependencies( $this->indexable_repository, $this->image_helper ); diff --git a/tests/Unit/Builders/Indexable_Post_Builder_Test.php b/tests/Unit/Builders/Indexable_Post_Builder_Test.php index 45e64177db8..905264a926c 100644 --- a/tests/Unit/Builders/Indexable_Post_Builder_Test.php +++ b/tests/Unit/Builders/Indexable_Post_Builder_Test.php @@ -620,6 +620,10 @@ public function test_find_alternative_image_from_post_content() { ->with( 123 ) ->andReturn( $image_meta ); + $this->image->allows( 'get_post_content_image_id' ) + ->with( 123 ) + ->andReturn( '' ); + $actual = $this->instance->find_alternative_image( $this->indexable ); $expected = [ @@ -661,7 +665,9 @@ public function test_find_alternative_image_no_image() { $this->image->allows( 'get_post_content_image' ) ->with( 123 ) ->andReturn( false ); - + $this->image->allows( 'get_post_content_image_id' ) + ->with( 123 ) + ->andReturn( '' ); $this->assertFalse( $this->instance->find_alternative_image( $this->indexable ) ); } diff --git a/tests/Unit/Context/Meta_Tags_Context_Test.php b/tests/Unit/Context/Meta_Tags_Context_Test.php index 8a05a0c61e2..ac6e0ab27a1 100644 --- a/tests/Unit/Context/Meta_Tags_Context_Test.php +++ b/tests/Unit/Context/Meta_Tags_Context_Test.php @@ -12,7 +12,6 @@ use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Permalink_Helper; -use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Helpers\Schema\ID_Helper; use Yoast\WP\SEO\Helpers\Site_Helper; use Yoast\WP\SEO\Helpers\Url_Helper; @@ -92,13 +91,6 @@ final class Meta_Tags_Context_Test extends TestCase { */ private $indexable_helper; - /** - * The request helper. - * - * @var Request_Helper - */ - private $request_helper; - /** * The indexable repository. * @@ -131,7 +123,6 @@ protected function set_up() { $this->permalink_helper = Mockery::mock( Permalink_Helper::class ); $this->indexable_helper = Mockery::mock( Indexable_Helper::class ); $this->indexable_repository = Mockery::mock( Indexable_Repository::class ); - $this->request_helper = Mockery::mock( Request_Helper::class ); $this->instance = new Meta_Tags_Context( $this->options, @@ -143,8 +134,7 @@ protected function set_up() { $this->user, $this->permalink_helper, $this->indexable_helper, - $this->indexable_repository, - $this->request_helper + $this->indexable_repository ); } diff --git a/tests/Unit/Dashboard/User_Interface/New_Dashboard_Page_Integration_Test.php b/tests/Unit/Dashboard/User_Interface/New_Dashboard_Page_Integration_Test.php new file mode 100644 index 00000000000..9c22e154b12 --- /dev/null +++ b/tests/Unit/Dashboard/User_Interface/New_Dashboard_Page_Integration_Test.php @@ -0,0 +1,325 @@ +stubTranslationFunctions(); + + $this->asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); + $this->current_page_helper = Mockery::mock( Current_Page_Helper::class ); + $this->product_helper = Mockery::mock( Product_Helper::class ); + $this->shortlink_helper = Mockery::mock( Short_Link_Helper::class ); + $this->notifications_helper = Mockery::mock( Notification_Helper::class ); + $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); + $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); + + $this->instance = new New_Dashboard_Page_Integration( + $this->asset_manager, + $this->current_page_helper, + $this->product_helper, + $this->shortlink_helper, + $this->notifications_helper, + $this->alert_dismissal_action, + $this->promotion_manager + ); + } + + /** + * Tests __construct method. + * + * @covers ::__construct + * + * @return void + */ + public function test_construct() { + $this->assertInstanceOf( + New_Dashboard_Page_Integration::class, + new New_Dashboard_Page_Integration( + $this->asset_manager, + $this->current_page_helper, + $this->product_helper, + $this->shortlink_helper, + $this->notifications_helper, + $this->alert_dismissal_action, + $this->promotion_manager + ) + ); + } + + /** + * Tests the retrieval of the conditionals. + * + * @covers ::get_conditionals + * + * @return void + */ + public function test_get_conditionals() { + $this->assertEquals( + [ + Admin_Conditional::class, + New_Dashboard_Ui_Conditional::class, + ], + New_Dashboard_Page_Integration::get_conditionals() + ); + } + + /** + * Provider for test_register_hooks + * + * @return array> + */ + public static function register_hooks_provider() { + return [ + 'Not on dashboard' => [ + 'current_page' => 'not dashboard', + 'action_times' => 0, + ], + 'On dashboard page' => [ + 'current_page' => 'wpseo_dashboard', + 'action_times' => 1, + ], + ]; + } + + /** + * Tests the registration of the hooks. + * + * @covers ::register_hooks + * + * @dataProvider register_hooks_provider + * + * @param string $current_page The current page. + * @param int $action_times The number of times the action should be called. + * + * @return void + */ + public function test_register_hooks_on_dashboard_page( $current_page, $action_times ) { + + Monkey\Functions\expect( 'add_filter' ) + ->with( 'wpseo_submenu_page', [ $this->instance, 'add_page' ] ) + ->once(); + + $this->current_page_helper + ->expects( 'get_current_yoast_seo_page' ) + ->once() + ->andReturn( $current_page ); + + Monkey\Functions\expect( 'add_action' ) + ->with( 'admin_enqueue_scripts', [ $this->instance, 'enqueue_assets' ] ) + ->times( $action_times ); + + $this->instance->register_hooks(); + } + + /** + * Tests the addition of the page to the submenu. + * + * @covers ::add_page + * + * @return void + */ + public function test_add_page() { + $pages = $this->instance->add_page( + [ + [ 'page1', '', 'Page 1', 'manage_options', 'page1', [ 'custom_display_page' ] ], + [ 'page2', '', 'Page 2', 'manage_options', 'page2', [ 'custom_display_page' ] ], + [ 'page3', '', 'Page 3', 'manage_options', 'page3', [ 'custom_display_page' ] ], + ] + ); + + // Assert that the new page was added at index 0. + $this->assertEquals( 'wpseo_dashboard', $pages[0][0] ); + $this->assertEquals( 'page3', $pages[3][0] ); + $this->assertEquals( '', $pages[0][1] ); + $this->assertEquals( '', $pages[3][1] ); + $this->assertEquals( 'General', $pages[0][2] ); + $this->assertEquals( 'Page 3', $pages[3][2] ); + $this->assertEquals( 'wpseo_manage_options', $pages[0][3] ); + $this->assertEquals( 'manage_options', $pages[3][3] ); + $this->assertEquals( 'wpseo_dashboard', $pages[0][4] ); + $this->assertEquals( 'page3', $pages[3][4] ); + $this->assertEquals( [ $this->instance, 'display_page' ], $pages[0][5] ); + $this->assertEquals( [ 'custom_display_page' ], $pages[3][5] ); + } + + /** + * Test display_page + * + * @covers ::display_page + * + * @return void + */ + public function test_display_page() { + $this->expectOutputString( '

    ' ); + $this->instance->display_page(); + } + + /** + * Test enqueue_assets + * + * @covers ::enqueue_assets + * @covers ::get_script_data + * + * @return void + */ + public function test_enqueue_assets() { + Monkey\Functions\expect( 'remove_action' ) + ->with( 'admin_print_scripts', 'print_emoji_detection_script' ) + ->once(); + + Monkey\Functions\expect( 'wp_enqueue_media' )->once(); + + $this->asset_manager + ->expects( 'enqueue_script' ) + ->with( 'new-dashboard' ) + ->once(); + + $this->asset_manager + ->expects( 'enqueue_style' ) + ->with( 'new-dashboard' ) + ->once(); + + $this->asset_manager + ->expects( 'localize_script' ) + ->once(); + + $this->expect_get_script_data(); + + $this->instance->enqueue_assets(); + } + + /** + * Expectations for get_script_data. + * + * @return array> The expected data. + */ + public function expect_get_script_data() { + $link_params = [ + 'php_version' => '8.1', + 'platform' => 'wordpress', + 'platform_version' => '6.2', + 'software' => 'free', + 'software_version' => '20.6-RC2', + 'days_active' => '6-30', + 'user_language' => 'en_US', + ]; + + $this->product_helper + ->expects( 'is_premium' ) + ->once() + ->andReturn( false ); + + Monkey\Functions\expect( 'is_rtl' )->once()->andReturn( false ); + Monkey\Functions\expect( 'add_query_arg' )->once(); + Monkey\Functions\expect( 'admin_url' )->once(); + Monkey\Functions\expect( 'plugins_url' ) + ->once() + ->andReturn( 'http://basic.wordpress.test/wp-content/worspress-seo' ); + + $this->shortlink_helper + ->expects( 'get_query_params' ) + ->once() + ->andReturn( $link_params ); + + $this->notifications_helper + ->expects( 'get_alerts' ) + ->once() + ->andReturn( [] ); + + $this->promotion_manager + ->expects( 'get_current_promotions' ) + ->once() + ->andReturn( [] ); + + $this->alert_dismissal_action + ->expects( 'all_dismissed' ) + ->once() + ->andReturn( [] ); + + return $link_params; + } +} diff --git a/tests/Unit/Editors/Framework/Integrations/News_SEO_Test.php b/tests/Unit/Editors/Framework/Integrations/News_SEO_Test.php new file mode 100644 index 00000000000..00bc0f10a6a --- /dev/null +++ b/tests/Unit/Editors/Framework/Integrations/News_SEO_Test.php @@ -0,0 +1,95 @@ +addon_manager = Mockery::mock( WPSEO_Addon_Manager::class ); + $this->instance = new News_SEO( $this->addon_manager ); + } + + /** + * Tests the is_enabled method. + * + * @dataProvider data_provider_is_enabled + * + * @param bool $news_seo_enabled If the news plugin is enabled. + * @param bool $expected The expected outcome. + * + * @return void + */ + public function test_is_enabled( + bool $news_seo_enabled, + bool $expected + ) { + + $this->addon_manager + ->expects( 'get_plugin_file' ) + ->times( 3 ) + ->with( 'yoast-seo-news' ) + ->andReturn( 'wpseo-news/wpseo-news.php' ); + + Monkey\Functions\expect( 'is_plugin_active' ) + ->times( 3 ) + ->with( 'wpseo-news/wpseo-news.php' ) + ->andReturn( $news_seo_enabled ); + + $is_woo_seo_active = $this->instance->is_enabled(); + + $this->assertSame( $expected, $is_woo_seo_active ); + $this->assertSame( [ 'isNewsSeoActive' => $is_woo_seo_active ], $this->instance->to_legacy_array() ); + $this->assertSame( [ 'isNewsSeoActive' => $is_woo_seo_active ], $this->instance->to_array() ); + } + + /** + * Data provider for test_is_enabled. + * + * @return array> + */ + public static function data_provider_is_enabled() { + return [ + 'Enabled' => [ + 'news_seo_enabled' => true, + 'expected' => true, + ], + 'Disabled' => [ + 'news_seo_enabled' => false, + 'expected' => false, + ], + ]; + } +} diff --git a/tests/Unit/Editors/Framework/Integrations/WooCommerce_SEO_Test.php b/tests/Unit/Editors/Framework/Integrations/WooCommerce_SEO_Test.php new file mode 100644 index 00000000000..54338bcecfd --- /dev/null +++ b/tests/Unit/Editors/Framework/Integrations/WooCommerce_SEO_Test.php @@ -0,0 +1,95 @@ +addon_manager = Mockery::mock( WPSEO_Addon_Manager::class ); + $this->instance = new WooCommerce_SEO( $this->addon_manager ); + } + + /** + * Tests the is_enabled method. + * + * @dataProvider data_provider_is_enabled + * + * @param bool $woocommerce_seo_enabled If the woocommerce plugin is enabled. + * @param bool $expected The expected outcome. + * + * @return void + */ + public function test_is_enabled( + bool $woocommerce_seo_enabled, + bool $expected + ) { + + $this->addon_manager + ->expects( 'get_plugin_file' ) + ->times( 3 ) + ->with( 'yoast-seo-woocommerce' ) + ->andReturn( 'wpseo-woocommerce/wpseo-woocommerce.php' ); + + Monkey\Functions\expect( 'is_plugin_active' ) + ->times( 3 ) + ->with( 'wpseo-woocommerce/wpseo-woocommerce.php' ) + ->andReturn( $woocommerce_seo_enabled ); + + $is_woo_seo_active = $this->instance->is_enabled(); + + $this->assertSame( $expected, $is_woo_seo_active ); + $this->assertSame( [ 'isWooCommerceSeoActive' => $is_woo_seo_active ], $this->instance->to_legacy_array() ); + $this->assertSame( [ 'isWooCommerceSeoActive' => $is_woo_seo_active ], $this->instance->to_array() ); + } + + /** + * Data provider for test_is_enabled. + * + * @return array> + */ + public static function data_provider_is_enabled() { + return [ + 'Enabled' => [ + 'woocommerce_seo_enabled' => true, + 'expected' => true, + ], + 'Disabled' => [ + 'woocommerce_seo_enabled' => false, + 'expected' => false, + ], + ]; + } +} diff --git a/tests/Unit/Editors/Framework/Integrations/WooCommerce_Test.php b/tests/Unit/Editors/Framework/Integrations/WooCommerce_Test.php new file mode 100644 index 00000000000..6b1b66fa570 --- /dev/null +++ b/tests/Unit/Editors/Framework/Integrations/WooCommerce_Test.php @@ -0,0 +1,109 @@ +woocommerce_conditional = Mockery::mock( WooCommerce_Conditional::class ); + $this->instance = new WooCommerce( $this->woocommerce_conditional ); + } + + /** + * Tests the is_enabled method. + * + * @dataProvider data_provider_is_enabled + * + * @param bool $woocommerce_enabled If the woocommerce plugin is enabled. + * @param bool $expected The expected outcome. + * + * @return void + */ + public function test_is_enabled( + bool $woocommerce_enabled, + bool $expected + ) { + + $this->woocommerce_conditional + ->expects( 'is_met' ) + ->times( 3 ) + ->andReturn( $woocommerce_enabled ); + + $this->assertSame( $expected, $this->instance->is_enabled() ); + $this->assertSame( [ 'isWooCommerceActive' => $this->instance->is_enabled() ], $this->instance->to_legacy_array() ); + } + + /** + * Tests the to_array method. + * + * @dataProvider data_provider_is_enabled + * + * @param bool $woocommerce_enabled If the woocommerce plugin is enabled. + * @param bool $expected The expected outcome. + * + * @return void + */ + public function test_to_array( + bool $woocommerce_enabled, + bool $expected + ) { + + $this->woocommerce_conditional + ->expects( 'is_met' ) + ->times( 3 ) + ->andReturn( $woocommerce_enabled ); + + $this->assertSame( $expected, $this->instance->is_enabled() ); + $this->assertSame( [ 'isWooCommerceActive' => $this->instance->is_enabled() ], $this->instance->to_array() ); + } + + /** + * Data provider for test_is_enabled. + * + * @return array> + */ + public static function data_provider_is_enabled() { + return [ + 'Enabled' => [ + 'woocommerce_enabled' => true, + 'expected' => true, + ], + 'Disabled' => [ + 'woocommerce_enabled' => false, + 'expected' => false, + ], + ]; + } +} diff --git a/tests/Unit/Editors/Framework/Site/Post_Site_Information_Test.php b/tests/Unit/Editors/Framework/Site/Post_Site_Information_Test.php index c3ecc881b8f..87ba85d536d 100644 --- a/tests/Unit/Editors/Framework/Site/Post_Site_Information_Test.php +++ b/tests/Unit/Editors/Framework/Site/Post_Site_Information_Test.php @@ -7,6 +7,7 @@ use Mockery; use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Editors\Framework\Site\Post_Site_Information; +use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; @@ -68,6 +69,13 @@ final class Post_Site_Information_Test extends TestCase { */ private $product_helper; + /** + * The options helper. + * + * @var Mockery\MockInterface|Options_Helper $options_helper + */ + private $options_helper; + /** * The Post_Site_Information container. * @@ -88,8 +96,9 @@ protected function set_up() { $this->meta_surface = Mockery::mock( Meta_Surface::class ); $this->product_helper = Mockery::mock( Product_Helper::class ); $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); + $this->options_helper = Mockery::mock( Options_Helper::class ); - $this->instance = new Post_Site_Information( $this->promotion_manager, $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper, $this->alert_dismissal_action ); + $this->instance = new Post_Site_Information( $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper, $this->alert_dismissal_action, $this->options_helper, $this->promotion_manager ); $this->instance->set_permalink( 'perma' ); $this->set_mocks(); } @@ -111,12 +120,7 @@ public function test_legacy_site_information() { 'dismissedAlerts' => [ 'the alert', ], - 'currentPromotions' => [ - 'the promotion', - 'another one', - ], 'webinarIntroBlockEditorUrl' => 'https://expl.c', - 'blackFridayBlockEditorUrl' => '', 'metabox' => [ 'search_url' => 'https://example.org', 'post_edit_url' => 'https://example.org', @@ -127,6 +131,10 @@ public function test_legacy_site_information() { 'isRtl' => false, 'isPremium' => true, 'siteIconUrl' => 'https://example.org', + 'showSocial' => [ + 'facebook' => false, + 'twitter' => false, + ], ], 'adminUrl' => 'https://example.org', @@ -136,6 +144,13 @@ public function test_legacy_site_information() { ], 'pluginUrl' => '/location', 'wistiaEmbedPermission' => true, + 'sitewideSocialImage' => null, + 'isPrivateBlog' => false, + 'currentPromotions' => [ + 'the promotion', + 'another one', + ], + 'blackFridayBlockEditorUrl' => '', ]; @@ -146,6 +161,9 @@ public function test_legacy_site_information() { $this->promotion_manager->expects( 'get_current_promotions' )->andReturn( [ 'the promotion', 'another one' ] ); $this->promotion_manager->expects( 'is' )->andReturnFalse(); $this->short_link_helper->expects( 'get' )->andReturn( 'https://expl.c' ); + $this->options_helper->expects( 'get' )->with( 'opengraph', false )->andReturn( false ); + $this->options_helper->expects( 'get' )->with( 'twitter', false )->andReturn( false ); + $this->options_helper->expects( 'get' )->with( 'og_default_image' )->andReturn( null ); $this->assertSame( $expected, $this->instance->get_legacy_site_information() ); } @@ -168,12 +186,7 @@ public function test_site_information() { 'dismissedAlerts' => [ 'the alert', ], - 'currentPromotions' => [ - 'the promotion', - 'another one', - ], 'webinarIntroBlockEditorUrl' => 'https://expl.c', - 'blackFridayBlockEditorUrl' => '', 'search_url' => 'https://example.org', 'post_edit_url' => 'https://example.org', 'base_url' => 'https://example.org', @@ -191,16 +204,33 @@ public function test_site_information() { 'isRtl' => false, 'isPremium' => true, 'siteIconUrl' => 'https://example.org', - + 'showSocial' => [ + 'facebook' => false, + 'twitter' => false, + ], + 'sitewideSocialImage' => null, + 'isPrivateBlog' => false, + 'currentPromotions' => [ + 'the promotion', + 'another one', + ], + 'blackFridayBlockEditorUrl' => '', ]; Monkey\Functions\expect( 'admin_url' )->andReturn( 'https://example.org' ); Monkey\Functions\expect( 'home_url' )->andReturn( 'https://example.org' ); + Monkey\Functions\expect( 'get_option' ) + ->once() + ->with( 'blog_public' ) + ->andReturn( '1' ); $this->alert_dismissal_action->expects( 'all_dismissed' )->andReturn( [ 'the alert' ] ); $this->promotion_manager->expects( 'get_current_promotions' )->andReturn( [ 'the promotion', 'another one' ] ); $this->promotion_manager->expects( 'is' )->andReturnFalse(); $this->short_link_helper->expects( 'get' )->andReturn( 'https://expl.c' ); + $this->options_helper->expects( 'get' )->with( 'og_default_image' )->andReturn( null ); + $this->options_helper->expects( 'get' )->with( 'opengraph', false )->andReturn( false ); + $this->options_helper->expects( 'get' )->with( 'twitter', false )->andReturn( false ); $this->assertSame( $expected, $this->instance->get_site_information() ); } diff --git a/tests/Unit/Editors/Framework/Site/Term_Site_Information_Test.php b/tests/Unit/Editors/Framework/Site/Term_Site_Information_Test.php index b659aecb752..b1af653dd82 100644 --- a/tests/Unit/Editors/Framework/Site/Term_Site_Information_Test.php +++ b/tests/Unit/Editors/Framework/Site/Term_Site_Information_Test.php @@ -12,6 +12,7 @@ use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; +use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; use Yoast\WP\SEO\Surfaces\Meta_Surface; use Yoast\WP\SEO\Tests\Unit\Doubles\Editors\Site_Information_Mocks_Trait; use Yoast\WP\SEO\Tests\Unit\TestCase; @@ -62,6 +63,13 @@ final class Term_Site_Information_Test extends TestCase { */ private $product_helper; + /** + * Holds the Promotion_Manager instance. + * + * @var Mockery\MockInterface|Promotion_Manager + */ + private $promotion_manager; + /** * The Term_Site_Information container. * @@ -81,8 +89,9 @@ protected function set_up() { $this->wistia_embed_repo = Mockery::mock( Wistia_Embed_Permission_Repository::class ); $this->meta_surface = Mockery::mock( Meta_Surface::class ); $this->product_helper = Mockery::mock( Product_Helper::class ); + $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); - $this->instance = new Term_Site_Information( $this->options_helper, $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper ); + $this->instance = new Term_Site_Information( $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper, $this->options_helper, $this->promotion_manager ); $taxonomy = Mockery::mock( WP_Taxonomy::class )->makePartial(); $taxonomy->rewrite = false; $mock_term = Mockery::mock( WP_Term::class )->makePartial(); @@ -93,6 +102,9 @@ protected function set_up() { $this->instance->set_term( $mock_term ); $this->options_helper->expects( 'get' )->with( 'stripcategorybase', false )->andReturnFalse(); + $this->options_helper->expects( 'get' )->with( 'opengraph', false )->andReturn( false ); + $this->options_helper->expects( 'get' )->with( 'twitter', false )->andReturn( false ); + $this->options_helper->expects( 'get' )->with( 'og_default_image' )->andReturn( null ); $this->set_mocks(); } @@ -111,26 +123,44 @@ protected function set_up() { */ public function test_site_information() { $expected = [ - 'search_url' => 'https://example.org', - 'post_edit_url' => 'https://example.org', - 'base_url' => 'https://example.org', - 'adminUrl' => 'https://example.org', - 'linkParams' => [ + 'search_url' => 'https://example.org', + 'post_edit_url' => 'https://example.org', + 'base_url' => 'https://example.org', + 'adminUrl' => 'https://example.org', + 'linkParams' => [ 'param', 'param2', ], - 'pluginUrl' => '/location', - 'wistiaEmbedPermission' => true, - 'site_name' => 'examepl.com', - 'contentLocale' => 'nl_NL', - 'userLocale' => 'nl_NL', - 'isRtl' => false, - 'isPremium' => true, - 'siteIconUrl' => 'https://example.org', + 'pluginUrl' => '/location', + 'wistiaEmbedPermission' => true, + 'site_name' => 'examepl.com', + 'contentLocale' => 'nl_NL', + 'userLocale' => 'nl_NL', + 'isRtl' => false, + 'isPremium' => true, + 'siteIconUrl' => 'https://example.org', + 'showSocial' => [ + 'facebook' => false, + 'twitter' => false, + ], + 'sitewideSocialImage' => null, + 'isPrivateBlog' => true, + 'currentPromotions' => [ + 'the promotion', + 'another one', + ], + 'blackFridayBlockEditorUrl' => '', ]; Monkey\Functions\expect( 'admin_url' )->andReturn( 'https://example.org' ); Monkey\Functions\expect( 'home_url' )->andReturn( 'https://example.org' ); + Monkey\Functions\expect( 'get_option' ) + ->once() + ->with( 'blog_public' ) + ->andReturn( '0' ); + + $this->promotion_manager->expects( 'get_current_promotions' )->andReturn( [ 'the promotion', 'another one' ] ); + $this->promotion_manager->expects( 'is' )->andReturnFalse(); $this->assertSame( $expected, $this->instance->get_site_information() ); } @@ -150,7 +180,7 @@ public function test_site_information() { public function test_legacy_site_information() { $expected = [ - 'metabox' => [ + 'metabox' => [ 'search_url' => 'https://example.org', 'post_edit_url' => 'https://example.org', 'base_url' => 'https://example.org', @@ -160,20 +190,33 @@ public function test_legacy_site_information() { 'isRtl' => false, 'isPremium' => true, 'siteIconUrl' => 'https://example.org', + 'showSocial' => [ + 'facebook' => false, + 'twitter' => false, + ], ], - 'adminUrl' => 'https://example.org', - 'linkParams' => [ + 'adminUrl' => 'https://example.org', + 'linkParams' => [ 'param', 'param2', ], - 'pluginUrl' => '/location', - 'wistiaEmbedPermission' => true, - + 'pluginUrl' => '/location', + 'wistiaEmbedPermission' => true, + 'sitewideSocialImage' => null, + 'isPrivateBlog' => false, + 'currentPromotions' => [ + 'the promotion', + 'another one', + ], + 'blackFridayBlockEditorUrl' => '', ]; Monkey\Functions\expect( 'admin_url' )->andReturn( 'https://example.org' ); Monkey\Functions\expect( 'home_url' )->andReturn( 'https://example.org' ); + $this->promotion_manager->expects( 'get_current_promotions' )->andReturn( [ 'the promotion', 'another one' ] ); + $this->promotion_manager->expects( 'is' )->andReturnFalse(); + $this->assertSame( $expected, $this->instance->get_legacy_site_information() ); } } diff --git a/tests/Unit/Images/Application/Image_Content_Extractor_Test.php b/tests/Unit/Images/Application/Image_Content_Extractor_Test.php new file mode 100644 index 00000000000..d6edc3b18da --- /dev/null +++ b/tests/Unit/Images/Application/Image_Content_Extractor_Test.php @@ -0,0 +1,107 @@ +instance = new Image_Content_Extractor(); + } + + /** + * Data provider to test the build. + * + * @return array> The test data. + */ + public static function build_provider() { + return [ + [ + false, + true, + true, + '', + [ + 'https://link.com/newly-added-in-post' => 8, + ], + ], + [ + true, + false, + false, + '', + [ + 'https://link.com/newly-added-in-post' => 0, + ], + ], + ]; + } + + /** + * Tests the build function. + * + * @covers ::__construct + * @covers ::gather_images + * @covers ::gather_images_wp + * @covers ::gather_images_domdocument + * @covers ::extract_id_of_classes + * + * @dataProvider build_provider + * + * @param bool $ignore_content_scan Whether content scanning should be ignored. + * @param bool $should_content_regex Whether the image id should be extracted with a regex. + * @param bool $should_doc_scan Whether the doc document should be used. + * @param bool $content The content to check. + * @param bool $expected The expected result. + * + * @return void + */ + public function test_gather_images( $ignore_content_scan, $should_content_regex, $should_doc_scan, $content, $expected ) { + if ( $should_doc_scan ) { + Functions\expect( 'apply_filters' ) + ->once() + ->with( 'wpseo_image_attribute_containing_id', 'class' ) + ->andReturn( 'class' ); + + } + if ( $should_content_regex ) { + Functions\expect( 'apply_filters' ) + ->once() + ->with( 'wpseo_extract_id_pattern', '/(?andReturn( '/(?once() + ->with( 'wpseo_force_skip_image_content_parsing', false ) + ->andReturn( true ); + } + + $this->assertEquals( $expected, $this->instance->gather_images( $content ) ); + } +} diff --git a/tests/Unit/Inc/Addon_Manager_Test.php b/tests/Unit/Inc/Addon_Manager_Test.php index 0b9ce101aa9..824537a4f45 100644 --- a/tests/Unit/Inc/Addon_Manager_Test.php +++ b/tests/Unit/Inc/Addon_Manager_Test.php @@ -583,7 +583,7 @@ public function test_check_for_updates( $addons, $data, $expected, $message ) { // Now check that the Premium plugin won't show updates, if the requirement for the WP version coming from Yoast free, is not met. if ( isset( $addons['wp-seo-premium.php'] ) ) { - $wp_version = '6.3'; + $wp_version = '6.4'; $updates = $this->instance->check_for_updates( $data ); $this->assertTrue( isset( $updates->no_update['wp-seo-premium.php'] ) ); diff --git a/tests/Unit/Integrations/Admin/Background_Indexing_Integration_Test.php b/tests/Unit/Integrations/Admin/Background_Indexing_Integration_Test.php index 55404dd44c9..5ef22016158 100644 --- a/tests/Unit/Integrations/Admin/Background_Indexing_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Background_Indexing_Integration_Test.php @@ -744,7 +744,7 @@ public function test_schedule_cron_indexing( $admin_dashboard_conditional_result /** * Provides data to test_schedule_cron_indexing. * - * @return array The test data. + * @return array> The test data. */ public static function data_schedule_cron_indexing() { return [ diff --git a/tests/Unit/Integrations/Admin/Deactivated_Premium_Integration_Test.php b/tests/Unit/Integrations/Admin/Deactivated_Premium_Integration_Test.php index 99c47c10a34..728d335ee10 100644 --- a/tests/Unit/Integrations/Admin/Deactivated_Premium_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Deactivated_Premium_Integration_Test.php @@ -117,10 +117,8 @@ function dismiss_premium_deactivated_notice(){ var data = { 'action': 'dismiss_premium_deactivated_notice', }; - - jQuery.post( ajaxurl, data, function( response ) { - jQuery( '#yoast-premium-deactivated-notice' ).hide(); - }); + jQuery( '#yoast-premium-deactivated-notice' ).hide(); + jQuery.post( ajaxurl, data, function( response ) {}); } jQuery( document ).ready( function() { diff --git a/tests/Unit/Integrations/Admin/First_Time_Configuration_Notice_Integration_Test.php b/tests/Unit/Integrations/Admin/First_Time_Configuration_Notice_Integration_Test.php index e1ed3808a41..b2343eb4cd2 100644 --- a/tests/Unit/Integrations/Admin/First_Time_Configuration_Notice_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/First_Time_Configuration_Notice_Integration_Test.php @@ -6,6 +6,7 @@ use Mockery; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Helpers\First_Time_Configuration_Notice_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Notice_Integration; @@ -44,12 +45,19 @@ final class First_Time_Configuration_Notice_Integration_Test extends TestCase { private $first_time_configuration_notice_helper; /** - * The the mock for a notice. + * The mock for a notice. * * @var Yoast\WP\SEO\Presenters\Admin\Notice_Presenter */ private $notice_presenter; + /** + * The mock for the New Dashboard UI conditional. + * + * @var New_Dashboard_Ui_Conditional + */ + private $new_dashboard_ui_conditional; + /** * The instance under test. * @@ -71,11 +79,13 @@ protected function set_up() { $this->options_helper = Mockery::mock( Options_Helper::class ); $this->admin_asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); $this->first_time_configuration_notice_helper = Mockery::mock( First_Time_Configuration_Notice_Helper::class ); + $this->new_dashboard_ui_conditional = Mockery::mock( New_Dashboard_Ui_Conditional::class ); $this->instance = new First_Time_Configuration_Notice_Integration( $this->options_helper, $this->first_time_configuration_notice_helper, - $this->admin_asset_manager + $this->admin_asset_manager, + $this->new_dashboard_ui_conditional ); $this->notice_presenter = Mockery::mock( Notice_Presenter::class ); @@ -182,30 +192,28 @@ public function test_should_display_first_time_configuration_notice() { public static function first_time_configuration_notice_provider() { // In case of change in js, make sure to match the tabs and line breaks for this test to pass (avoid 4 spaces as tab). - $default_message = '

    First-time SEO configuration

    Get started quickly with the Yoast SEO First-time configuration and configure Yoast SEO with the optimal SEO settings for your site!

    '; - $alternate_message = '

    First-time SEO configuration

    We noticed that you haven\'t fully configured Yoast SEO yet. Optimize your SEO settings even further by using our improved First-time configuration.

    '; @@ -251,6 +259,11 @@ public function test_first_time_configuration_notice( $should_show_alternate_mes ->once() ->andReturn( $should_show_alternate_message ); + $this->new_dashboard_ui_conditional + ->expects( 'is_met' ) + ->once() + ->andReturnFalse(); + Monkey\Functions\expect( 'self_admin_url' ) ->once() ->with( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ); diff --git a/tests/Unit/Integrations/Admin/Installation_Success_Integration_Test.php b/tests/Unit/Integrations/Admin/Installation_Success_Integration_Test.php index 83309b2c7d8..a681fc6ac54 100644 --- a/tests/Unit/Integrations/Admin/Installation_Success_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Installation_Success_Integration_Test.php @@ -5,6 +5,7 @@ use Brain\Monkey; use Mockery; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\Conditionals\New_Dashboard_Ui_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Integrations\Admin\Installation_Success_Integration; @@ -40,6 +41,13 @@ final class Installation_Success_Integration_Test extends TestCase { */ protected $product_helper; + /** + * The New Dashboard UI conditional. + * + * @var New_Dashboard_Ui_Conditional + */ + private $new_dashboard_ui_conditional; + /** * Set up the fixtures for the tests. * @@ -48,11 +56,17 @@ final class Installation_Success_Integration_Test extends TestCase { protected function set_up() { parent::set_up(); - $this->options_helper = Mockery::mock( Options_Helper::class ); - $this->product_helper = Mockery::mock( Product_Helper::class ); - $this->instance = Mockery::mock( + $this->options_helper = Mockery::mock( Options_Helper::class ); + $this->product_helper = Mockery::mock( Product_Helper::class ); + $this->new_dashboard_ui_conditional = Mockery::mock( New_Dashboard_Ui_Conditional::class ); + + $this->instance = Mockery::mock( Installation_Success_Integration::class, - [ $this->options_helper, $this->product_helper ] + [ + $this->options_helper, + $this->product_helper, + $this->new_dashboard_ui_conditional, + ] )->makePartial(); } diff --git a/tests/Unit/Integrations/Admin/Integrations_Page_Integration_Test.php b/tests/Unit/Integrations/Admin/Integrations_Page_Integration_Test.php index cefb2846f92..e51203ed69f 100644 --- a/tests/Unit/Integrations/Admin/Integrations_Page_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Integrations_Page_Integration_Test.php @@ -7,8 +7,6 @@ use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; -use Yoast\WP\SEO\Helpers\Short_Link_Helper; -use Yoast\WP\SEO\Helpers\Url_Helper; use Yoast\WP\SEO\Helpers\Woocommerce_Helper; use Yoast\WP\SEO\Integrations\Admin\Integrations_Page; use Yoast\WP\SEO\Tests\Unit\TestCase; @@ -121,20 +119,6 @@ public function test_enqueue_assets() { Monkey\Functions\expect( 'get_site_url' ) ->andReturn( 'https://www.example.com' ); - $short_link = Mockery::mock( Short_Link_Helper::class ); - $short_link->expects( 'get' )->times( 3 )->andReturn( 'https://www.example.com?some=var' ); - $url_helper = Mockery::mock( Url_Helper::class ); - $url_helper->expects()->get_url_host( 'https://www.example.com' )->andReturn( 'https://www.example.com' ); - $container = $this->create_container_with( - [ - Url_Helper::class => $url_helper, - Short_Link_Helper::class => $short_link, - ] - ); - - Monkey\Functions\expect( 'YoastSEO' ) - ->andReturn( (object) [ 'helpers' => $this->create_helper_surface( $container ) ] ); - Monkey\Functions\expect( 'is_plugin_active' )->times( 5 )->andReturnTrue(); Monkey\Functions\expect( 'wp_nonce_url' )->times( 3 )->andReturn( 'nonce' ); Monkey\Functions\expect( 'self_admin_url' )->times( 3 )->andReturn( 'https://www.example.com' ); @@ -172,13 +156,6 @@ public function test_enqueue_assets() { 'mastodon_active' => false, 'is_multisite' => false, 'plugin_url' => 'https://www.example.com', - - 'jetpack-boost_active' => false, - 'jetpack-boost_premium' => false, - 'jetpack-boost_logo_link' => 'https://www.example.com?some=var', - 'jetpack-boost_get_link' => 'https://www.example.com?some=var', - 'jetpack-boost_upgrade_link' => 'https://www.example.com?some=var', - 'jetpack-boost_learn_more_link' => 'https://www.example.com', ] ); diff --git a/tests/Unit/Integrations/Admin/Migration_Error_Integration_Test.php b/tests/Unit/Integrations/Admin/Migration_Error_Integration_Test.php index 5471681e53d..09b5162e9c2 100644 --- a/tests/Unit/Integrations/Admin/Migration_Error_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Migration_Error_Integration_Test.php @@ -129,12 +129,13 @@ public function test_render_migration_error() { $this->expect_shortlinker(); - $expected = '
    '; + $expected = '
    '; + $expected .= '

    Yoast SEO is unable to create database tables

    '; $expected .= '

    Yoast SEO had problems creating the database tables needed to speed up your site.

    '; $expected .= '

    Please read this help article to find out how to resolve this problem.

    '; $expected .= '

    Your site will continue to work normally, but won\'t take full advantage of Yoast SEO.

    '; $expected .= '
    Show debug information

    test error

    '; - $expected .= '
    '; + $expected .= '
    '; $this->instance->render_migration_error(); diff --git a/tests/Unit/Integrations/Front_End_Integration_Test.php b/tests/Unit/Integrations/Front_End_Integration_Test.php index c05d1c910d5..78485815561 100644 --- a/tests/Unit/Integrations/Front_End_Integration_Test.php +++ b/tests/Unit/Integrations/Front_End_Integration_Test.php @@ -8,7 +8,6 @@ use WPSEO_Replace_Vars; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; -use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Integrations\Front_End_Integration; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presentations\Indexable_Presentation; @@ -48,13 +47,6 @@ final class Front_End_Integration_Test extends TestCase { */ private $options; - /** - * Represents the request helper. - * - * @var Mockery\MockInterface|Request_Helper - */ - private $request; - /** * Represents the meta tags context memoizer. * @@ -94,7 +86,6 @@ protected function set_up() { $this->context_memoizer = Mockery::mock( Meta_Tags_Context_Memoizer::class ); $this->container = Mockery::mock( ContainerInterface::class ); $this->options = Mockery::mock( Options_Helper::class ); - $this->request = Mockery::mock( Request_Helper::class ); $this->instance = Mockery::mock( Front_End_Integration::class, @@ -102,7 +93,6 @@ protected function set_up() { $this->context_memoizer, $this->container, $this->options, - $this->request, Mockery::mock( Helpers_Surface::class ), Mockery::mock( WPSEO_Replace_Vars::class ), ] @@ -219,8 +209,7 @@ public function test_get_presenters_for_singular_page() { ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -287,8 +276,7 @@ public function test_get_presenters_for_static_home_page() { ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -360,8 +348,7 @@ public function test_get_presenters_for_error_page() { ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -419,8 +406,7 @@ public function test_get_presenters_for_non_singular_page() { ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -486,8 +472,7 @@ public function test_get_presenters_for_theme_without_title_tag_and_force_rewrit ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -533,8 +518,7 @@ public function test_get_presenters_for_theme_on_rest_request() { ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnTrue(); @@ -588,8 +572,7 @@ public function test_get_presenters_for_theme_without_title_tag_and_force_rewrit ->once() ->andReturn( $this->context ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -662,8 +645,7 @@ public function test_filter_robots_presenter_and_wp_robots_to_wp_head_filter() { \add_action( 'wp_head', 'wp_robots' ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -694,8 +676,7 @@ public function test_rest_request_should_output_robots_presenter() { \add_action( 'wp_head', 'wp_robots' ); - $this->request - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnTrue(); diff --git a/tests/Unit/Integrations/Third_Party/Wincher_Publish_Test.php b/tests/Unit/Integrations/Third_Party/Wincher_Publish_Test.php index 63c6b0a19ad..e1c92a6a1c1 100644 --- a/tests/Unit/Integrations/Third_Party/Wincher_Publish_Test.php +++ b/tests/Unit/Integrations/Third_Party/Wincher_Publish_Test.php @@ -272,8 +272,7 @@ public function test_track_after_rest_api_request() { public function test_track_after_post_request() { $post = Mockery::mock( WP_Post::class ); - $this->instance - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnFalse(); @@ -295,8 +294,7 @@ public function test_track_after_post_request() { public function test_track_after_post_request_during_rest_request() { $post = Mockery::mock( WP_Post::class ); - $this->instance - ->expects( 'is_rest_request' ) + Monkey\Functions\expect( 'wp_is_serving_rest_request' ) ->once() ->andReturnTrue(); diff --git a/tests/Unit/Integrations/Watchers/Search_Engines_Discouraged_Watcher_Test.php b/tests/Unit/Integrations/Watchers/Search_Engines_Discouraged_Watcher_Test.php index a42e84ba90b..1c761cb0c61 100644 --- a/tests/Unit/Integrations/Watchers/Search_Engines_Discouraged_Watcher_Test.php +++ b/tests/Unit/Integrations/Watchers/Search_Engines_Discouraged_Watcher_Test.php @@ -135,10 +135,11 @@ public function test_constructor() { * @return void */ public function test_register_hooks() { - Monkey\Actions\expectAdded( 'admin_init' ); - Monkey\Actions\expectAdded( 'admin_notices' ); - $this->instance->register_hooks(); + + $this->assertNotFalse( \has_action( 'admin_init', [ $this->instance, 'manage_search_engines_discouraged_notification' ] ) ); + $this->assertNotFalse( \has_action( 'admin_notices', [ $this->instance, 'maybe_show_search_engines_discouraged_notice' ] ) ); + $this->assertNotFalse( \has_action( 'update_option_blog_public', [ $this->instance, 'restore_ignore_option' ] ) ); } /** @@ -435,4 +436,48 @@ public static function manage_search_engines_discouraged_notification_dataprovid 'Notification should be removed' => $notification_should_be_removed, ]; } + + /** + * Data provider for test_restore_ignore_option. + * + * @return array Data for test_restore_ignore_option. + */ + public static function data_provider_restore_ignore_option() { + return [ + 'should restore ignore_search_engines_discouraged_notice' => [ + 'blog_public' => '1', + 'set_ignore_option_times' => 1, + ], + 'should not restore ignore_search_engines_discouraged_notice' => [ + 'blog_public' => '0', + 'set_ignore_option_times' => 0, + ], + ]; + } + + /** + * Tests restore_ignore_option. + * + * @covers ::restore_ignore_option + * + * @dataProvider data_provider_restore_ignore_option + * + * @param string $blog_public The value of the blog_public option. + * @param int $set_ignore_option_times The number of times the ignore option should be set. + * + * @return void + */ + public function test_restore_ignore_option( $blog_public, $set_ignore_option_times ) { + Monkey\Functions\expect( 'get_option' ) + ->with( 'blog_public' ) + ->once() + ->andReturn( $blog_public ); + + $this->options_helper + ->expects( 'set' ) + ->times( $set_ignore_option_times ) + ->with( 'ignore_search_engines_discouraged_notice', false ); + + $this->instance->restore_ignore_option(); + } } diff --git a/tests/Unit/Presenters/Admin/Indexing_Notification_Presenter_Test.php b/tests/Unit/Presenters/Admin/Indexing_Notification_Presenter_Test.php index 6e45700b8e4..1ffcbce2e52 100644 --- a/tests/Unit/Presenters/Admin/Indexing_Notification_Presenter_Test.php +++ b/tests/Unit/Presenters/Admin/Indexing_Notification_Presenter_Test.php @@ -56,7 +56,7 @@ public function test_present_few_indexables() { '' ); - $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.

    We estimate this will take less than a minute.

    Start SEO data optimization'; + $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. We estimate this will take less than a minute.

    Start SEO data optimization'; $actual = $instance->present(); self::assertSame( $expected, $actual ); @@ -81,7 +81,7 @@ public function test_present_some_indexables() { '' ); - $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.

    We estimate this will take a couple of minutes.

    Start SEO data optimization'; + $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. We estimate this will take a couple of minutes.

    Start SEO data optimization'; $actual = $instance->present(); self::assertSame( $expected, $actual ); @@ -114,7 +114,7 @@ public function test_present_many_indexables() { 'A message to show in the notification.' ); - $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.

    We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:

    Start SEO data optimization'; + $expected = '

    You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:

    Start SEO data optimization'; $actual = $instance->present(); self::assertSame( $expected, $actual ); diff --git a/tests/Unit/Presenters/Admin/Migration_Error_Presenter_Test.php b/tests/Unit/Presenters/Admin/Migration_Error_Presenter_Test.php index 09ba7d2272a..5d0655c22ed 100644 --- a/tests/Unit/Presenters/Admin/Migration_Error_Presenter_Test.php +++ b/tests/Unit/Presenters/Admin/Migration_Error_Presenter_Test.php @@ -36,12 +36,13 @@ public function test_present() { $this->expect_shortlinker(); - $expected = '
    '; + $expected = '
    '; + $expected .= '

    Yoast SEO is unable to create database tables

    '; $expected .= '

    Yoast SEO had problems creating the database tables needed to speed up your site.

    '; $expected .= '

    Please read this help article to find out how to resolve this problem.

    '; $expected .= '

    Your site will continue to work normally, but won\'t take full advantage of Yoast SEO.

    '; $expected .= '
    Show debug information

    test error

    '; - $expected .= '
    '; + $expected .= '
    '; $instance = new Migration_Error_Presenter( $migration_error ); diff --git a/tests/Unit/Presenters/Admin/Notice_Presenter_Test.php b/tests/Unit/Presenters/Admin/Notice_Presenter_Test.php index 237faa0b83f..46218efbd43 100644 --- a/tests/Unit/Presenters/Admin/Notice_Presenter_Test.php +++ b/tests/Unit/Presenters/Admin/Notice_Presenter_Test.php @@ -66,10 +66,11 @@ public function test_default_notice() { . '
    ' . '
    ' . '' - . '

    title

    ' + . '

    title

    ' . '
    ' + . '
    ' . '

    content

    ' - . '
    ' + . '
    ' . '
    '; Monkey\Functions\expect( 'esc_html' )->andReturn( 'title' ); @@ -94,10 +95,11 @@ public function test_notice_with_image() { . '
    ' . '
    ' . '' - . '

    title

    ' + . '

    title

    ' . '
    ' + . '
    ' . '

    content

    ' - . '
    ' + . '
    ' . '' . '
    '; @@ -124,10 +126,11 @@ public function test_dismissble_notice() { . '
    ' . '
    ' . '' - . '

    title

    ' + . '

    title

    ' . '
    ' + . '
    ' . '

    content

    ' - . '
    ' + . '
    ' . '
    '; Monkey\Functions\expect( 'esc_html' )->andReturn( '' ); @@ -153,10 +156,11 @@ public function test_dismissble_notice_with_image() { . '
    ' . '
    ' . '' - . '

    title

    ' + . '

    title

    ' . '
    ' + . '
    ' . '

    content

    ' - . '
    ' + . '
    ' . '' . ''; @@ -185,11 +189,12 @@ public function test_dismissble_notice_with_image_and_button() { . '
    ' . '
    ' . '' - . '

    title

    ' + . '

    title

    ' . '
    ' + . '
    ' . '

    content

    ' . '

    Some text

    ' - . '
    ' + . '
    ' . '' . ''; diff --git a/tests/Unit/bootstrap.php b/tests/Unit/bootstrap.php index 54032e01604..a1d4bedbe0d 100644 --- a/tests/Unit/bootstrap.php +++ b/tests/Unit/bootstrap.php @@ -40,8 +40,8 @@ \define( 'YOAST_VENDOR_PREFIX_DIRECTORY', 'vendor_prefixed' ); \define( 'YOAST_SEO_PHP_REQUIRED', '7.2.5' ); -\define( 'YOAST_SEO_WP_TESTED', '6.6.1' ); -\define( 'YOAST_SEO_WP_REQUIRED', '6.4' ); +\define( 'YOAST_SEO_WP_TESTED', '6.6.2' ); +\define( 'YOAST_SEO_WP_REQUIRED', '6.5' ); if ( ! \defined( 'WPSEO_NAMESPACES' ) ) { \define( 'WPSEO_NAMESPACES', true ); diff --git a/tests/WP/Editors/Framework/Site/Post_Site_Information_Test.php b/tests/WP/Editors/Framework/Site/Post_Site_Information_Test.php index a7034a1c5c5..7ad259a6104 100644 --- a/tests/WP/Editors/Framework/Site/Post_Site_Information_Test.php +++ b/tests/WP/Editors/Framework/Site/Post_Site_Information_Test.php @@ -6,6 +6,7 @@ use Mockery; use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Editors\Framework\Site\Post_Site_Information; +use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository; @@ -64,6 +65,13 @@ final class Post_Site_Information_Test extends TestCase { */ private $product_helper; + /** + * The options helper. + * + * @var Mockery\MockInterface|Options_Helper $options_helper + */ + private $options_helper; + /** * The Post_Site_Information container. * @@ -84,9 +92,10 @@ public function set_up() { $this->wistia_embed_repo->expects( 'get_value_for_user' )->with( 0 )->andReturnTrue(); $this->meta_surface = \YoastSEO()->meta; $this->product_helper = \YoastSEO()->helpers->product; + $this->options_helper = \YoastSEO()->helpers->options; $this->alert_dismissal_action = \YoastSEO()->classes->get( Alert_Dismissal_Action::class ); - $this->instance = new Post_Site_Information( $this->promotion_manager, $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper, $this->alert_dismissal_action ); + $this->instance = new Post_Site_Information( $this->short_link_helper, $this->wistia_embed_repo, $this->meta_surface, $this->product_helper, $this->alert_dismissal_action, $this->options_helper, $this->promotion_manager ); $this->instance->set_permalink( 'perma' ); } @@ -105,9 +114,7 @@ public function set_up() { public function test_legacy_site_information() { $expected = [ 'dismissedAlerts' => false, - 'currentPromotions' => [], 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), - 'blackFridayBlockEditorUrl' => '', 'metabox' => [ 'search_url' => 'http://example.org/wp-admin/edit.php?seo_kw_filter={keyword}', @@ -119,11 +126,19 @@ public function test_legacy_site_information() { 'isRtl' => false, 'isPremium' => false, 'siteIconUrl' => '', + 'showSocial' => [ + 'facebook' => true, + 'twitter' => true, + ], ], 'adminUrl' => 'http://example.org/wp-admin/admin.php', 'linkParams' => $this->short_link_helper->get_query_params(), 'pluginUrl' => 'http://example.org/wp-content/plugins/wordpress-seo', 'wistiaEmbedPermission' => true, + 'sitewideSocialImage' => '', + 'isPrivateBlog' => false, + 'currentPromotions' => [], + 'blackFridayBlockEditorUrl' => '', ]; $this->assertSame( $expected, $this->instance->get_legacy_site_information() ); @@ -142,11 +157,11 @@ public function test_legacy_site_information() { * @return void */ public function test_site_information() { + \update_option( 'blog_public', '0' ); + $expected = [ 'dismissedAlerts' => false, - 'currentPromotions' => [], 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), - 'blackFridayBlockEditorUrl' => '', 'search_url' => 'http://example.org/wp-admin/edit.php?seo_kw_filter={keyword}', 'post_edit_url' => 'http://example.org/wp-admin/post.php?post={id}&action=edit', 'base_url' => 'http://example.org/', @@ -160,9 +175,21 @@ public function test_site_information() { 'isRtl' => false, 'isPremium' => false, 'siteIconUrl' => '', - + 'showSocial' => [ + 'facebook' => true, + 'twitter' => true, + ], + 'sitewideSocialImage' => '', + 'isPrivateBlog' => true, + 'currentPromotions' => [], + 'blackFridayBlockEditorUrl' => '', ]; - $this->assertSame( $expected, $this->instance->get_site_information() ); + $site_info = $this->instance->get_site_information(); + + // Reset the blog_public option before the next test. + \update_option( 'blog_public', '1' ); + + $this->assertSame( $expected, $site_info ); } } diff --git a/tests/WP/Frontend/Front_End_Integration_Test.php b/tests/WP/Frontend/Front_End_Integration_Test.php index 169fa21caeb..011e8672593 100644 --- a/tests/WP/Frontend/Front_End_Integration_Test.php +++ b/tests/WP/Frontend/Front_End_Integration_Test.php @@ -5,7 +5,6 @@ use Mockery; use WPSEO_Replace_Vars; use Yoast\WP\SEO\Helpers\Options_Helper; -use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presentations\Indexable_Presentation; use Yoast\WP\SEO\Surfaces\Helpers_Surface; @@ -41,13 +40,6 @@ final class Front_End_Integration_Test extends TestCase { */ protected $options; - /** - * Represents the request helper. - * - * @var Request_Helper - */ - protected $request; - /** * The helpers surface. * @@ -79,7 +71,6 @@ public function set_up(): void { $this->context_memoizer = Mockery::mock( Meta_Tags_Context_Memoizer::class ); $this->container = Mockery::mock( ContainerInterface::class ); $this->options = Mockery::mock( Options_Helper::class ); - $this->request = Mockery::mock( Request_Helper::class ); $this->helpers = Mockery::mock( Helpers_Surface::class ); $this->replace_vars = Mockery::mock( WPSEO_Replace_Vars::class ); @@ -87,7 +78,6 @@ public function set_up(): void { $this->context_memoizer, $this->container, $this->options, - $this->request, $this->helpers, $this->replace_vars ); diff --git a/wp-seo-main.php b/wp-seo-main.php index 335ee0e7ac4..b7e901e9c41 100644 --- a/wp-seo-main.php +++ b/wp-seo-main.php @@ -15,7 +15,7 @@ * {@internal Nobody should be able to overrule the real version number as this can cause * serious issues with the options, so no if ( ! defined() ).}} */ -define( 'WPSEO_VERSION', '23.5-RC1' ); +define( 'WPSEO_VERSION', '23.6-RC4' ); if ( ! defined( 'WPSEO_PATH' ) ) { @@ -35,8 +35,8 @@ define( 'YOAST_VENDOR_PREFIX_DIRECTORY', 'vendor_prefixed' ); define( 'YOAST_SEO_PHP_REQUIRED', '7.2.5' ); -define( 'YOAST_SEO_WP_TESTED', '6.6.1' ); -define( 'YOAST_SEO_WP_REQUIRED', '6.4' ); +define( 'YOAST_SEO_WP_TESTED', '6.6.2' ); +define( 'YOAST_SEO_WP_REQUIRED', '6.5' ); if ( ! defined( 'WPSEO_NAMESPACES' ) ) { define( 'WPSEO_NAMESPACES', true ); @@ -563,8 +563,14 @@ function yoast_wpseo_missing_filter_notice() { * @return void */ function yoast_wpseo_activation_failed_notice( $message ) { + $title = sprintf( + /* translators: %s: Yoast SEO. */ + esc_html__( '%s activation failed', 'wordpress-seo' ), + 'Yoast SEO' + ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This function is only called in 3 places that are safe. - echo '

    ' . esc_html__( 'Activation failed:', 'wordpress-seo' ) . ' ' . strip_tags( $message, '' ) . '

    '; + echo ''; } /** diff --git a/wp-seo.php b/wp-seo.php index ce752966023..20e412e82c9 100644 --- a/wp-seo.php +++ b/wp-seo.php @@ -8,7 +8,7 @@ * * @wordpress-plugin * Plugin Name: Yoast SEO - * Version: 23.5-RC1 + * Version: 23.6-RC4 * Plugin URI: https://yoa.st/1uj * Description: The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more. * Author: Team Yoast @@ -16,11 +16,11 @@ * Text Domain: wordpress-seo * Domain Path: /languages/ * License: GPL v3 - * Requires at least: 6.4 + * Requires at least: 6.5 * Requires PHP: 7.2.5 * * WC requires at least: 7.1 - * WC tested up to: 9.2 + * WC tested up to: 9.3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/yarn.lock b/yarn.lock index 1a23c2f22ed..8ed1bd5f3b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5849,6 +5849,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^18.2.25": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16", "@types/react@^16.9.0": version "16.14.5" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.5.tgz#2c39b5cadefaf4829818f9219e5e093325979f4d" @@ -5885,6 +5892,14 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.2.79": + version "18.3.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f" + integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/resolve@^1.20.2": version "1.20.6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" @@ -6419,6 +6434,15 @@ "@wordpress/dom-ready" "^3.48.0" "@wordpress/i18n" "^4.48.0" +"@wordpress/a11y@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/a11y/-/a11y-4.7.0.tgz#003dca403cdeaa274df97809f9c80660528f4cfe" + integrity sha512-qeh8TcJNNr9M0XL3OUDawBRrZypNLsnLjcXEBd6jp8Y4kOWxowmDDT6re1uToPdYTLLW2PZmZeBLYR9OS7pgpw== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/dom-ready" "^4.7.0" + "@wordpress/i18n" "^5.7.0" + "@wordpress/api-fetch@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@wordpress/api-fetch/-/api-fetch-4.0.0.tgz#86fa1ab40f320dc972d32635ed60059e90678a0d" @@ -6829,6 +6853,25 @@ mousetrap "^1.6.5" use-memo-one "^1.1.1" +"@wordpress/compose@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/compose/-/compose-7.7.0.tgz#3eec3a0027f72dbb4d03a2a64e956347169f79a3" + integrity sha512-TjhGcw9n/XbiMT63POESs1TF9O6eQRVhAPrMan5t2yusQbog5KLk4TetOasIWxD80pu5sg9P5NuupuU/oSEBYQ== + dependencies: + "@babel/runtime" "^7.16.0" + "@types/mousetrap" "^1.6.8" + "@wordpress/deprecated" "^4.7.0" + "@wordpress/dom" "^4.7.0" + "@wordpress/element" "^6.7.0" + "@wordpress/is-shallow-equal" "^5.7.0" + "@wordpress/keycodes" "^4.7.0" + "@wordpress/priority-queue" "^3.7.0" + "@wordpress/undo-manager" "^1.7.0" + change-case "^4.1.2" + clipboard "^2.0.11" + mousetrap "^1.6.5" + use-memo-one "^1.1.1" + "@wordpress/data-controls@^1.21.3": version "1.21.3" resolved "https://registry.yarnpkg.com/@wordpress/data-controls/-/data-controls-1.21.3.tgz#a82e2dedf7a8d190b3cb086dd13fe49a88e74149" @@ -6839,6 +6882,27 @@ "@wordpress/data" "^4.27.3" "@wordpress/deprecated" "^2.12.3" +"@wordpress/data@^10.7.0": + version "10.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/data/-/data-10.7.0.tgz#1b6c6c5bb1d0861db88070ed4ad96321a20c4f55" + integrity sha512-0NqDYIMOHdilSYoH6LRaq1CHcWlJiGP6xxkjI6pu2ZEf5mo9S/UblLCzVwaZMnhae/ZxEsgQQIypIQJJqor9uw== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/compose" "^7.7.0" + "@wordpress/deprecated" "^4.7.0" + "@wordpress/element" "^6.7.0" + "@wordpress/is-shallow-equal" "^5.7.0" + "@wordpress/priority-queue" "^3.7.0" + "@wordpress/private-apis" "^1.7.0" + "@wordpress/redux-routine" "^5.7.0" + deepmerge "^4.3.0" + equivalent-key-map "^0.2.2" + is-plain-object "^5.0.0" + is-promise "^4.0.0" + redux "^4.1.2" + rememo "^4.0.2" + use-memo-one "^1.1.1" + "@wordpress/data@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@wordpress/data/-/data-4.10.0.tgz#2161af0a3c1fb0fb2b63f95beb36a64b8acf65d6" @@ -6991,6 +7055,14 @@ "@babel/runtime" "^7.16.0" "@wordpress/hooks" "^3.3.1" +"@wordpress/deprecated@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/deprecated/-/deprecated-4.7.0.tgz#ae4f1f4c785ccc8d321c1a082e52f16aaa7fed52" + integrity sha512-FMtYPk+yxvEAs1LDBHk1yHz48vlp/TWrTQeMph5ov7dpw4Xgfd9darXorsl4zudJVrB+Nn6dYrPmrS08rAXtQQ== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/hooks" "^4.7.0" + "@wordpress/dom-ready@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@wordpress/dom-ready/-/dom-ready-1.2.0.tgz#d95877eb6f929532ec971983b55e731babb78d9c" @@ -7019,6 +7091,13 @@ dependencies: "@babel/runtime" "^7.16.0" +"@wordpress/dom-ready@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/dom-ready/-/dom-ready-4.7.0.tgz#8654aae7d5d8ea421fbb77547abebab081f84a17" + integrity sha512-moMbRCPNfgCc0dT0waEr0renEsptnDMV89fGpMijA66IyvYoYsxDT57w2JqHiaKbTvbIBmgdNgDjcVgZGv5JoA== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/dom@^2.18.0": version "2.18.0" resolved "https://registry.yarnpkg.com/@wordpress/dom/-/dom-2.18.0.tgz#8394f42e86dcca3f3bcddb805d05fdd65e9cfd07" @@ -7051,6 +7130,14 @@ "@babel/runtime" "^7.16.0" lodash "^4.17.21" +"@wordpress/dom@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/dom/-/dom-4.7.0.tgz#a1aa50bdd2cc19dcddc2ff6a64fb70285186cc4c" + integrity sha512-lGPEJHSHOT5Y9gWsX8V2tcsd5shDCTJqDxzL+pwDTfEsi/Os52nZCvzmavzGwRDzlm2Wmd3wNW+k/UCS/PhmXQ== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/deprecated" "^4.7.0" + "@wordpress/e2e-test-utils-playwright@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.13.0.tgz#c06d6d6288ac5a78c9148c7084a0db1bd10211c0" @@ -7167,6 +7254,20 @@ react "^18.2.0" react-dom "^18.2.0" +"@wordpress/element@^6.7.0": + version "6.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-6.7.0.tgz#8d8ac6568909135b8b86131ee689a2724f36df05" + integrity sha512-d0kiN8DCNDNoh5P5xLb496amoadvjsSnkyJHmQsw17qP4dHZaSLONiMi9yh3NQlwIu0pcbbn3WI/9ENA79HlFQ== + dependencies: + "@babel/runtime" "^7.16.0" + "@types/react" "^18.2.79" + "@types/react-dom" "^18.2.25" + "@wordpress/escape-html" "^3.7.0" + change-case "^4.1.2" + is-plain-object "^5.0.0" + react "^18.3.0" + react-dom "^18.3.0" + "@wordpress/escape-html@^1.12.2": version "1.12.2" resolved "https://registry.yarnpkg.com/@wordpress/escape-html/-/escape-html-1.12.2.tgz#dcc92178bacc69952cde9bb8fb1cbbea9deb2cc3" @@ -7209,6 +7310,13 @@ dependencies: "@babel/runtime" "^7.16.0" +"@wordpress/escape-html@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/escape-html/-/escape-html-3.7.0.tgz#cbd5a4ae4d68acd37ba4895b0bfb6eaa5b8b72be" + integrity sha512-VqLQGNMs1BF6LnS+5eNjpM/sCUQhjn4QOfhDlWdVDi0ZxpZgssPzKhJ1ils/7FC0qF3vrMg8EH5xXxw2xz8A/w== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/eslint-plugin@^17.2.0": version "17.2.0" resolved "https://registry.yarnpkg.com/@wordpress/eslint-plugin/-/eslint-plugin-17.2.0.tgz#8fb56d2f530e5d995f7239b2571e078a907b2bb3" @@ -7295,6 +7403,13 @@ dependencies: "@babel/runtime" "^7.16.0" +"@wordpress/hooks@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/hooks/-/hooks-4.7.0.tgz#172d101ef3298bbf84b81723c071c0e25b3fdd1a" + integrity sha512-EGHMsNCt+PyStm3o1JWujaTA+HKcTxuEXdSHBBFDavzsgOF13bxTf1LpDYgTZJT3K9TSMP983IwfckP5t66pDw== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/html-entities@^2.11.2": version "2.11.2" resolved "https://registry.yarnpkg.com/@wordpress/html-entities/-/html-entities-2.11.2.tgz#c0e757ee7369239e2a885f7db7e78b81b79f8963" @@ -7433,6 +7548,18 @@ sprintf-js "^1.1.1" tannin "^1.2.0" +"@wordpress/i18n@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/i18n/-/i18n-5.7.0.tgz#fce292bed35c41663bdf9639f1fb4926e8ca3840" + integrity sha512-o1cq1zutE5rMAM//Ra1hRfgHuWNBxFtd7XNk+BuAcILRENMaEoqAoGBmGj9lRtOcqAj+cdoWxFjBIxRa67vIrg== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/hooks" "^4.7.0" + gettext-parser "^1.3.1" + memize "^2.1.0" + sprintf-js "^1.1.1" + tannin "^1.2.0" + "@wordpress/icons@^2.10.3": version "2.10.3" resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-2.10.3.tgz#56253dd0119794c600c923cb2255778db66b97f3" @@ -7495,6 +7622,13 @@ dependencies: "@babel/runtime" "^7.16.0" +"@wordpress/is-shallow-equal@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/is-shallow-equal/-/is-shallow-equal-5.7.0.tgz#ac7e29811e8926eec93c5547d04ddcf21b584ac5" + integrity sha512-PW+OEkojwd8pZs7m8m9jVwVhLTA1kxmf01f0R2aC+bGfYvw0mlqcviCQTR6+EpRYpceh2nkDch2mD/LWT8c7ZA== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/jest-console@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@wordpress/jest-console/-/jest-console-5.0.1.tgz#8f2dff67980c75523b8231ac951cfe97a7b907e6" @@ -7597,6 +7731,14 @@ "@wordpress/i18n" "^4.45.0" change-case "^4.1.2" +"@wordpress/keycodes@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/keycodes/-/keycodes-4.7.0.tgz#a63ea09597a04f2f0714b5dd3802af2e53f28917" + integrity sha512-x8I0xjRM8U0RnpFHWN9mA+x3MqjhJNBldiCpb59GTi3BIzPeDPgxbosAsAAgF0pYdDtGyiRkrOZA23NTia63TA== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/i18n" "^5.7.0" + "@wordpress/notices@^2.13.3": version "2.13.3" resolved "https://registry.yarnpkg.com/@wordpress/notices/-/notices-2.13.3.tgz#1fc62ec581245275773b9f4f06d7dd5dc7e2e447" @@ -7702,6 +7844,21 @@ dependencies: "@babel/runtime" "^7.16.0" +"@wordpress/priority-queue@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/priority-queue/-/priority-queue-3.7.0.tgz#fbdc114787af7c430b75d2ccb8296e5d5b3d5d9f" + integrity sha512-WgKOhaQdaEeOxRLL49cp2YKfsZyUsR1qHoLid64Jux9FjFqLT8t52UTYJ796AhU4W0ifxf3R1SkNpW5zslxKOg== + dependencies: + "@babel/runtime" "^7.16.0" + requestidlecallback "^0.3.0" + +"@wordpress/private-apis@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/private-apis/-/private-apis-1.7.0.tgz#a1d2b20f3e64177563b34fb05d46f1bf14c69f12" + integrity sha512-H6bbWZRL7u2awmK14ZCz7OupeIjz1HxSlB785X53k9JZ5KsbSK/FCzAvOJ5vCU9poC1fa6IT33qkgx3JNX3JEA== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/redux-routine@^3.14.2": version "3.14.2" resolved "https://registry.yarnpkg.com/@wordpress/redux-routine/-/redux-routine-3.14.2.tgz#f49ce2e66eecb5bdaef86a1f90b4cc4137d5acf4" @@ -7742,6 +7899,16 @@ lodash "^4.17.21" rungen "^0.3.2" +"@wordpress/redux-routine@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/redux-routine/-/redux-routine-5.7.0.tgz#eec9fb6758ac2197407311597ab8a21aa40d2963" + integrity sha512-KOU1+qFEDrptLY6lOQ3pTR+MZwe35dHfCp8xJHUJa/RI9jKULvXWrEIX0qhEMNWlinyQmwdTfmZqKxP8RuFzag== + dependencies: + "@babel/runtime" "^7.16.0" + is-plain-object "^5.0.0" + is-promise "^4.0.0" + rungen "^0.3.2" + "@wordpress/rich-text@^3.25.3": version "3.25.3" resolved "https://registry.yarnpkg.com/@wordpress/rich-text/-/rich-text-3.25.3.tgz#af8533cbaa1c1313b4c6fa9667212a7b1c52016f" @@ -7777,6 +7944,22 @@ memize "^1.1.0" rememo "^4.0.0" +"@wordpress/rich-text@^7.0.2": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/rich-text/-/rich-text-7.7.0.tgz#578098777cf9520a1a5d70eb8038a260cf43b8aa" + integrity sha512-is96sOolYVeE/58jUr6GxZKY1XGWrF988lT8FUg7U4u0KgdDSIEPLacs4USE8OoqxZYCIAnwSPenMXN+ZPvOfw== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/a11y" "^4.7.0" + "@wordpress/compose" "^7.7.0" + "@wordpress/data" "^10.7.0" + "@wordpress/deprecated" "^4.7.0" + "@wordpress/element" "^6.7.0" + "@wordpress/escape-html" "^3.7.0" + "@wordpress/i18n" "^5.7.0" + "@wordpress/keycodes" "^4.7.0" + memize "^2.1.0" + "@wordpress/scripts@^26.16.0": version "26.16.0" resolved "https://registry.yarnpkg.com/@wordpress/scripts/-/scripts-26.16.0.tgz#e0baf60eb11d5d39a85ba6ac5eeb2ac6ee58522b" @@ -7892,6 +8075,14 @@ "@babel/runtime" "^7.13.10" lodash "^4.17.19" +"@wordpress/undo-manager@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/undo-manager/-/undo-manager-1.7.0.tgz#608ab321df7f347c93b923a5eb879f2cc2851512" + integrity sha512-FHkwMD/jbe5jhVXfD9bSNhEivhMeszm20/ymEP6vAsLVJB2K25iAMOGvsq5jtznyJiqQzNUmvPERN0IKnaHQnA== + dependencies: + "@babel/runtime" "^7.16.0" + "@wordpress/is-shallow-equal" "^5.7.0" + "@wordpress/url@^2.22.2": version "2.22.2" resolved "https://registry.yarnpkg.com/@wordpress/url/-/url-2.22.2.tgz#e4267befa6d421b31b40e6e8ff9c973468a6e947" @@ -10644,7 +10835,7 @@ client-only@^0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clipboard@^2.0.1: +clipboard@^2.0.1, clipboard@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== @@ -12291,7 +12482,7 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -deepmerge@^4.3.1: +deepmerge@^4.3.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -24525,6 +24716,14 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-dom@^18.3.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + react-element-to-jsx-string@^15.0.0: version "15.0.0" resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" @@ -24890,6 +25089,13 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" +react@^18.3.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -25328,7 +25534,7 @@ rememo@^3.0.0: resolved "https://registry.yarnpkg.com/rememo/-/rememo-3.0.0.tgz#06e8e76e108865cc1e9b73329db49f844eaf8392" integrity sha512-eWtut/7pqMRnSccbexb647iPjN7ir6Tmf4RG92ZVlykFEkHqGYy9tWnpHH3I+FS+WQ6lQ1i1iDgarYzGKgTcRQ== -rememo@^4.0.0: +rememo@^4.0.0, rememo@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/rememo/-/rememo-4.0.2.tgz#8af1f09fd3bf5809ca0bfd0b803926c67ead8c1e" integrity sha512-NVfSP9NstE3QPNs/TnegQY0vnJnstKQSpcrsI2kBTB3dB2PkdfKdTa+abbjMIDqpc63fE5LfjLgfMst0ULMFxQ== @@ -25924,6 +26130,13 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"