diff --git a/admin/class-expose-shortlinks.php b/admin/class-expose-shortlinks.php index 7a26b0d7dc6..45f83b31df9 100644 --- a/admin/class-expose-shortlinks.php +++ b/admin/class-expose-shortlinks.php @@ -39,6 +39,9 @@ class WPSEO_Expose_Shortlinks implements WPSEO_WordPress_Integration { 'shortlinks.upsell.sidebar.keyphrase_distribution' => 'https://yoa.st/keyphrase-distribution-sidebar', 'shortlinks.upsell.sidebar.word_complexity' => 'https://yoa.st/word-complexity-sidebar', 'shortlinks.upsell.sidebar.internal_linking_suggestions' => 'https://yoa.st/internal-linking-suggestions-sidebar', + 'shortlinks.upsell.sidebar.highlighting_seo_analysis' => 'https://yoa.st/highlighting-seo-analysis', + 'shortlinks.upsell.sidebar.highlighting_readability_analysis' => 'https://yoa.st/highlighting-readability-analysis', + 'shortlinks.upsell.sidebar.highlighting_inclusive_analysis' => 'https://yoa.st/highlighting-inclusive-analysis', 'shortlinks.upsell.metabox.news' => 'https://yoa.st/get-news-metabox', 'shortlinks.upsell.metabox.go_premium' => 'https://yoa.st/pe-premium-page', 'shortlinks.upsell.metabox.focus_keyword_synonyms_button' => 'https://yoa.st/keyword-synonyms-popup', diff --git a/packages/analysis-report/src/AnalysisList.js b/packages/analysis-report/src/AnalysisList.js index 7e1dca3c8e3..5270756dabf 100644 --- a/packages/analysis-report/src/AnalysisList.js +++ b/packages/analysis-report/src/AnalysisList.js @@ -108,6 +108,8 @@ export default function AnalysisList( props ) { isPremium={ props.isPremium } onResultChange={ props.onResultChange } markButtonFactory={ props.markButtonFactory } + shouldUpsellHighlighting={ props.shouldUpsellHighlighting } + renderHighlightingUpsell={ props.renderHighlightingUpsell } />; } ) } ; @@ -124,6 +126,8 @@ AnalysisList.propTypes = { onEditButtonClick: PropTypes.func, isPremium: PropTypes.bool, onResultChange: PropTypes.func, + shouldUpsellHighlighting: PropTypes.bool, + renderHighlightingUpsell: PropTypes.func, }; AnalysisList.defaultProps = { @@ -135,4 +139,6 @@ AnalysisList.defaultProps = { onEditButtonClick: noop, isPremium: false, onResultChange: noop, + shouldUpsellHighlighting: false, + renderHighlightingUpsell: noop, }; diff --git a/packages/analysis-report/src/AnalysisResult.js b/packages/analysis-report/src/AnalysisResult.js index 626caeee7e5..060f62b1940 100644 --- a/packages/analysis-report/src/AnalysisResult.js +++ b/packages/analysis-report/src/AnalysisResult.js @@ -1,4 +1,5 @@ -import React, { useEffect } from "react"; +/* eslint-disable complexity */ +import React, { useCallback, useEffect, useState } from "react"; import PropTypes from "prop-types"; import styled from "styled-components"; import { noop } from "lodash"; @@ -16,6 +17,7 @@ const AnalysisResultBase = styled.li` padding: 0; display: flex; align-items: flex-start; + position: relative; `; const ScoreIcon = styled( SvgIcon )` @@ -43,12 +45,13 @@ const areMarkButtonsHidden = function( props ) { /** * Factory method which creates a new instance of the default mark button. * - * @param {String} ariaLabel The button aria-label. - * @param {String} id The button id. - * @param {String} className The button class name. - * @param {String} status Status of the buttons. Supports: "enabled", "disabled". - * @param {Function} onClick Onclick handler. - * @param {Boolean} isPressed Whether the button is in a pressed state. + * @param {string} ariaLabel The button aria-label. + * @param {string} id The button id. + * @param {string} className The button class name. + * @param {string} status Status of the buttons. Supports: "enabled", "disabled", "hidden". + * @param {function} onClick Onclick handler. + * @param {boolean} isPressed Whether the button is in a pressed state. + * * @returns {JSX.Element} A new mark button. */ const createMarkButton = ( { @@ -79,6 +82,11 @@ const createMarkButton = ( { * @returns {ReactElement} The rendered AnalysisResult component. */ const AnalysisResult = ( { markButtonFactory, ...props } ) => { + const [ isOpen, setIsOpen ] = useState( false ); + + const closeModal = useCallback( () => setIsOpen( false ), [] ); + const openModal = useCallback( () => setIsOpen( true ), [] ); + markButtonFactory = markButtonFactory || createMarkButton; const { id, marker, hasMarksButton } = props; @@ -86,7 +94,7 @@ const AnalysisResult = ( { markButtonFactory, ...props } ) => { if ( ! areMarkButtonsHidden( props ) ) { marksButton = markButtonFactory( { - onClick: props.onButtonClickMarks, + onClick: props.shouldUpsellHighlighting ? openModal : props.onButtonClickMarks, status: props.marksButtonStatus, className: props.marksButtonClassName, id: props.buttonIdMarks, @@ -118,6 +126,7 @@ const AnalysisResult = ( { markButtonFactory, ...props } ) => { { marksButton } + { props.renderHighlightingUpsell( isOpen, closeModal ) } { props.hasEditButton && props.isPremium && ); @@ -152,6 +154,8 @@ ContentAnalysis.propTypes = { goodResults: PropTypes.string, } ), onResultChange: PropTypes.func, + shouldUpsellHighlighting: PropTypes.bool, + renderHighlightingUpsell: PropTypes.func, }; ContentAnalysis.defaultProps = { @@ -172,6 +176,8 @@ ContentAnalysis.defaultProps = { isPremium: false, resultCategoryLabels: {}, onResultChange: () => {}, + shouldUpsellHighlighting: false, + renderHighlightingUpsell: () => {}, }; export default ContentAnalysis; diff --git a/packages/analysis-report/tests/__snapshots__/AnalysisResultTest.js.snap b/packages/analysis-report/tests/__snapshots__/AnalysisResultTest.js.snap index d99beb26e50..f9fa3b24728 100644 --- a/packages/analysis-report/tests/__snapshots__/AnalysisResultTest.js.snap +++ b/packages/analysis-report/tests/__snapshots__/AnalysisResultTest.js.snap @@ -18,9 +18,20 @@ exports[`the AnalysisResult component matches the snapshot 1`] = ` } .c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #a4286a; box-shadow: inset 0 2px 0 rgba( 93,35,122,0.7 ); @@ -52,6 +63,7 @@ exports[`the AnalysisResult component matches the snapshot 1`] = ` -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -166,6 +178,7 @@ exports[`the AnalysisResult component with a activated premium matches the snaps -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -258,6 +271,7 @@ exports[`the AnalysisResult component with a beta badge label matches the snapsh -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -326,9 +340,20 @@ exports[`the AnalysisResult component with disabled buttons matches the snapshot } .c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #a4286a; box-shadow: inset 0 2px 0 rgba( 93,35,122,0.7 ); @@ -360,6 +385,7 @@ exports[`the AnalysisResult component with disabled buttons matches the snapshot -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -449,6 +475,7 @@ exports[`the AnalysisResult component with hidden buttons matches the snapshot 1 -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -512,9 +539,20 @@ exports[`the AnalysisResult component with html in the text matches the snapshot } .c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #a4286a; box-shadow: inset 0 2px 0 rgba( 93,35,122,0.7 ); @@ -546,6 +584,7 @@ exports[`the AnalysisResult component with html in the text matches the snapshot -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { @@ -635,6 +674,7 @@ exports[`the AnalysisResult component with suppressed text matches the snapshot -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c2 { diff --git a/packages/analysis-report/tests/__snapshots__/ContentAnalysisTest.js.snap b/packages/analysis-report/tests/__snapshots__/ContentAnalysisTest.js.snap index 90d3a9da8c4..b80f27ae0de 100644 --- a/packages/analysis-report/tests/__snapshots__/ContentAnalysisTest.js.snap +++ b/packages/analysis-report/tests/__snapshots__/ContentAnalysisTest.js.snap @@ -147,9 +147,20 @@ exports[`ContentAnalysis the ContentAnalysis component with custom result catego } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -181,6 +192,7 @@ exports[`ContentAnalysis the ContentAnalysis component with custom result catego -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -823,9 +835,20 @@ exports[`ContentAnalysis the ContentAnalysis component with disabled buttons mat } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -857,6 +880,7 @@ exports[`ContentAnalysis the ContentAnalysis component with disabled buttons mat -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -1509,6 +1533,7 @@ exports[`ContentAnalysis the ContentAnalysis component with hidden buttons match -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -2103,9 +2128,20 @@ exports[`ContentAnalysis the ContentAnalysis component with specified header lev } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -2137,6 +2173,7 @@ exports[`ContentAnalysis the ContentAnalysis component with specified header lev -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -2779,9 +2816,20 @@ exports[`ContentAnalysis the ContentAnalysis component with upsell results match } .c20 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -2813,6 +2861,7 @@ exports[`ContentAnalysis the ContentAnalysis component with upsell results match -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -3492,9 +3541,20 @@ exports[`ContentAnalysis the ContentAnalysis component without language notice m } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -3526,6 +3586,7 @@ exports[`ContentAnalysis the ContentAnalysis component without language notice m -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -4168,9 +4229,20 @@ exports[`ContentAnalysis the ContentAnalysis component without problems and cons } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -4202,6 +4274,7 @@ exports[`ContentAnalysis the ContentAnalysis component without problems and cons -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -4674,9 +4747,20 @@ exports[`ContentAnalysis the ContentAnalysis component without problems and impr } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -4708,6 +4792,7 @@ exports[`ContentAnalysis the ContentAnalysis component without problems and impr -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -5180,9 +5265,20 @@ exports[`ContentAnalysis the ContentAnalysis component without problems matches } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -5214,6 +5310,7 @@ exports[`ContentAnalysis the ContentAnalysis component without problems matches -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { @@ -5759,9 +5856,20 @@ exports[`ContentAnalysis the ContentAnalysis component without problems, improve } .c19 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -5793,6 +5901,7 @@ exports[`ContentAnalysis the ContentAnalysis component without problems, improve -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; + position: relative; } .c17 { diff --git a/packages/components/src/IconButtonToggle.js b/packages/components/src/IconButtonToggle.js index 8c6bffda85a..bc6bf13fbe8 100644 --- a/packages/components/src/IconButtonToggle.js +++ b/packages/components/src/IconButtonToggle.js @@ -9,9 +9,11 @@ import { colors, rgba } from "@yoast/style-guide"; import SvgIcon from "./SvgIcon"; const IconButtonBase = styled.button` + align-items: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: inline-flex; border: 1px solid ${ colors.$color_button_border }; background-color: ${ props => props.pressed ? props.pressedBackground : props.unpressedBackground }; box-shadow: ${ props => props.pressed diff --git a/packages/components/tests/__snapshots__/IconButtonToggleTest.js.snap b/packages/components/tests/__snapshots__/IconButtonToggleTest.js.snap index 1ced2c7b4ec..0adcef6b6e0 100644 --- a/packages/components/tests/__snapshots__/IconButtonToggleTest.js.snap +++ b/packages/components/tests/__snapshots__/IconButtonToggleTest.js.snap @@ -10,9 +10,20 @@ exports[`the disabled IconButtonToggle matches the snapshot 1`] = ` } .c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); @@ -69,9 +80,20 @@ exports[`the pressed IconButtonToggle matches the snapshot 1`] = ` } .c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #a4286a; box-shadow: inset 0 2px 0 rgba( 93,35,122,0.7 ); @@ -128,9 +150,20 @@ exports[`the unpressed IconButtonToggle matches the snapshot 1`] = ` } .c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; box-sizing: border-box; min-width: 32px; - display: inline-block; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; border: 1px solid #ccc; background-color: #f7f7f7; box-shadow: 0 1px 0 rgba( 204,204,204,0.7 ); diff --git a/packages/js/src/analysis/getApplyMarks.js b/packages/js/src/analysis/getApplyMarks.js index 3c56601da37..9681580c388 100644 --- a/packages/js/src/analysis/getApplyMarks.js +++ b/packages/js/src/analysis/getApplyMarks.js @@ -4,6 +4,7 @@ import { select } from "@wordpress/data"; import * as tinyMCEHelper from "../lib/tinymce"; import { tinyMCEDecorator } from "../decorator/tinyMCE"; import { isAnnotationAvailable, applyAsAnnotations } from "../decorator/gutenberg"; +import { doAction } from "@wordpress/hooks"; /** * Create decorators for each editor and applies marks to each editor @@ -49,6 +50,8 @@ function applyMarks( paper, marks ) { // Apply marks to other blocks applyAsAnnotations( marks ); } + + doAction( "yoast.analysis.applyMarks", marks ); } /** diff --git a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js index 6b065bfc123..0cab3eee2a9 100644 --- a/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js +++ b/packages/js/src/components/contentAnalysis/InclusiveLanguageAnalysis.js @@ -65,6 +65,8 @@ const InclusiveLanguageAnalysis = ( props ) => { * @returns {JSX.Element} The results of the analysis. */ function renderResults() { + const highlightingUpsellLink = "shortlinks.upsell.sidebar.highlighting_inclusive_analysis"; + return ( @@ -89,6 +91,8 @@ const InclusiveLanguageAnalysis = ( props ) => { problems: __( "Non-inclusive phrases", "wordpress-seo" ), improvements: __( "Potentially non-inclusive phrases", "wordpress-seo" ), } } + highlightingUpsellLink={ highlightingUpsellLink } + shouldUpsellHighlighting={ props.shouldUpsellHighlighting } /> ); @@ -232,11 +236,13 @@ InclusiveLanguageAnalysis.propTypes = { // eslint-disable-next-line react/no-unused-prop-types marksButtonStatus: PropTypes.oneOf( [ "enabled", "disabled", "hidden" ] ).isRequired, overallScore: PropTypes.number, + shouldUpsellHighlighting: PropTypes.bool, }; InclusiveLanguageAnalysis.defaultProps = { results: [], overallScore: null, + shouldUpsellHighlighting: false, }; export default withSelect( select => { diff --git a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js index 0b44e9aa110..7500c72dcee 100644 --- a/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js +++ b/packages/js/src/components/contentAnalysis/ReadabilityAnalysis.js @@ -46,6 +46,8 @@ class ReadabilityAnalysis extends Component { * @returns {wp.Element} The Readability Analysis results. */ renderResults( upsellResults ) { + const highlightingUpsellLink = "shortlinks.upsell.sidebar.highlighting_readability_analysis"; + return ( @@ -67,6 +69,8 @@ class ReadabilityAnalysis extends Component { upsellResults={ upsellResults } marksButtonClassName="yoast-tooltip yoast-tooltip-w" marksButtonStatus={ this.props.marksButtonStatus } + highlightingUpsellLink={ highlightingUpsellLink } + shouldUpsellHighlighting={ this.props.shouldUpsellHighlighting } /> ); @@ -183,11 +187,13 @@ ReadabilityAnalysis.propTypes = { marksButtonStatus: PropTypes.string.isRequired, overallScore: PropTypes.number, shouldUpsell: PropTypes.bool, + shouldUpsellHighlighting: PropTypes.bool, }; ReadabilityAnalysis.defaultProps = { overallScore: null, shouldUpsell: false, + shouldUpsellHighlighting: false, }; export default withSelect( select => { diff --git a/packages/js/src/components/contentAnalysis/Results.js b/packages/js/src/components/contentAnalysis/Results.js index c92a0d90ce2..bc323b1b5b0 100644 --- a/packages/js/src/components/contentAnalysis/Results.js +++ b/packages/js/src/components/contentAnalysis/Results.js @@ -1,12 +1,19 @@ -import { __ } from "@wordpress/i18n"; -import { doAction } from "@wordpress/hooks"; -import PropTypes from "prop-types"; import { ContentAnalysis } from "@yoast/analysis-report"; +import { IconButtonToggle } from "@yoast/components"; +import { Badge } from "@yoast/ui-library"; +import { LockClosedIcon } from "@heroicons/react/solid"; +import { __ } from "@wordpress/i18n"; import { Component, Fragment } from "@wordpress/element"; +import { doAction } from "@wordpress/hooks"; + import { isUndefined } from "lodash"; +import PropTypes from "prop-types"; import { Paper } from "yoastseo"; import mapResults from "./mapResults"; +import { ModalSmallContainer } from "../modals/Container"; +import Modal, { defaultModalClassName } from "../modals/Modal"; +import PremiumSEOAnalysisUpsell from "../modals/PremiumSEOAnalysisUpsell"; /** * Wrapper to provide functionality to the ContentAnalysis component. @@ -37,6 +44,8 @@ class Results extends Component { this.handleMarkButtonClick = this.handleMarkButtonClick.bind( this ); this.handleEditButtonClick = this.handleEditButtonClick.bind( this ); this.handleResultsChange = this.handleResultsChange.bind( this ); + this.renderHighlightingUpsell = this.renderHighlightingUpsell.bind( this ); + this.createMarkButton = this.createMarkButton.bind( this ); } /** @@ -57,6 +66,46 @@ class Results extends Component { } } + /** + * Factory method which creates a new instance of the default mark button. + * + * @param {string} ariaLabel The button aria-label. + * @param {string} id The button id. + * @param {string} className The button class name. + * @param {string} status Status of the buttons. Supports: "enabled", "disabled", "hidden". + * @param {function} onClick Onclick handler. + * @param {boolean} isPressed Whether the button is in a pressed state. + * + * @returns {JSX.Element} A new mark button. + */ + createMarkButton( { + ariaLabel, + id, + className, + status, + onClick, + isPressed, + } ) { + return + + { this.props.shouldUpsellHighlighting && +
+ + + +
+ } +
; + } + /** * Deactivates the marker from the page. * @returns {void} @@ -236,6 +285,34 @@ class Results extends Component { window.YoastSEO.analysis.applyMarks( new Paper( "", {} ), [] ); } + /** + * Renders the modal for the highlighting upsell. + * + * @param {boolean} isOpen Whether the modal should be opened. + * @param {function} closeModal A callback function invoked when the modal is closed. + * @returns {boolean|wp.Element} The modal for the highlighting upsell element, or false if the modal is closed. + */ + renderHighlightingUpsell( isOpen, closeModal ) { + const upsellDescription = __( + "Highlight areas of improvement in your text, no more searching for a needle in a haystack, straight to optimizing! Now also in Elementor!", + "wordpress-seo" ); + + return isOpen && ( + + + + + + ); + } + /** * Renders the Results component. * @@ -289,6 +366,9 @@ class Results extends Component { isPremium={ this.props.isPremium } resultCategoryLabels={ labels } onResultChange={ this.handleResultsChange } + shouldUpsellHighlighting={ this.props.shouldUpsellHighlighting } + renderHighlightingUpsell={ this.renderHighlightingUpsell } + markButtonFactory={ this.createMarkButton } /> ); @@ -315,6 +395,8 @@ Results.propTypes = { goodResults: PropTypes.string, } ), shortcodesForParsing: PropTypes.array, + shouldUpsellHighlighting: PropTypes.bool, + highlightingUpsellLink: PropTypes.string, }; Results.defaultProps = { @@ -329,6 +411,8 @@ Results.defaultProps = { isPremium: false, resultCategoryLabels: {}, shortcodesForParsing: [], + shouldUpsellHighlighting: false, + highlightingUpsellLink: "", }; export default Results; diff --git a/packages/js/src/components/contentAnalysis/SeoAnalysis.js b/packages/js/src/components/contentAnalysis/SeoAnalysis.js index 3fdd17080c2..1853924c592 100644 --- a/packages/js/src/components/contentAnalysis/SeoAnalysis.js +++ b/packages/js/src/components/contentAnalysis/SeoAnalysis.js @@ -197,6 +197,7 @@ class SeoAnalysis extends Component { render() { const score = getIndicatorForScore( this.props.overallScore ); const isPremium = getL10nObject().isPremium; + const highlightingUpsellLink = "shortlinks.upsell.sidebar.highlighting_seo_analysis"; if ( score.className !== "loading" && this.props.keyword === "" ) { score.className = "na"; @@ -244,6 +245,8 @@ class SeoAnalysis extends Component { editButtonClassName="yoast-tooltip yoast-tooltip-w" marksButtonStatus={ this.props.marksButtonStatus } location={ location } + shouldUpsellHighlighting={ this.props.shouldUpsellHighlighting } + highlightingUpsellLink={ highlightingUpsellLink } /> { this.renderTabIcon( location, score.className ) } @@ -265,6 +268,7 @@ SeoAnalysis.propTypes = { shouldUpsell: PropTypes.bool, shouldUpsellWordFormRecognition: PropTypes.bool, overallScore: PropTypes.number, + shouldUpsellHighlighting: PropTypes.bool, }; SeoAnalysis.defaultProps = { @@ -274,6 +278,7 @@ SeoAnalysis.defaultProps = { shouldUpsell: false, shouldUpsellWordFormRecognition: false, overallScore: null, + shouldUpsellHighlighting: false, }; export default withSelect( ( select, ownProps ) => { diff --git a/packages/js/src/components/modals/PremiumSEOAnalysisUpsell.js b/packages/js/src/components/modals/PremiumSEOAnalysisUpsell.js index a742c5fcab3..fd9369b0e5e 100644 --- a/packages/js/src/components/modals/PremiumSEOAnalysisUpsell.js +++ b/packages/js/src/components/modals/PremiumSEOAnalysisUpsell.js @@ -5,6 +5,10 @@ import { useRootContext } from "@yoast/externals/contexts"; import PropTypes from "prop-types"; import UpsellBox from "../UpsellBox"; +const upsellDescription = __( + "Check your text on even more SEO criteria and get an enhanced keyphrase analysis, making it easier to optimize your content.", + "wordpress-seo" ); + /** * Creates the content for a PremiumSEOAnalysisUpsell modal. * @@ -28,7 +32,7 @@ const PremiumSEOAnalysisUpsell = ( props ) => { return ( { PremiumSEOAnalysisUpsell.propTypes = { buyLink: PropTypes.string.isRequired, + description: PropTypes.string, +}; + +PremiumSEOAnalysisUpsell.defaultProps = { + description: upsellDescription, }; diff --git a/packages/js/src/elementor/components/fills/ElementorFill.js b/packages/js/src/elementor/components/fills/ElementorFill.js index 2fb75f65cc0..62aed626c18 100644 --- a/packages/js/src/elementor/components/fills/ElementorFill.js +++ b/packages/js/src/elementor/components/fills/ElementorFill.js @@ -81,6 +81,7 @@ export default function ElementorFill( { isLoading, onLoad, settings } ) { { settings.shouldUpsell && } @@ -88,10 +89,13 @@ export default function ElementorFill( { isLoading, onLoad, settings } ) { { settings.isContentAnalysisActive && } { settings.isInclusiveLanguageAnalysisActive && - + } { settings.isKeywordAnalysisActive && { settings.shouldUpsell && } diff --git a/packages/js/src/elementor/initializers/editor-store.js b/packages/js/src/elementor/initializers/editor-store.js index db30a1325c1..4c470d5c3b4 100644 --- a/packages/js/src/elementor/initializers/editor-store.js +++ b/packages/js/src/elementor/initializers/editor-store.js @@ -17,8 +17,8 @@ const populateStore = store => { store.dispatch( actions.loadCornerstoneContent() ); // Initialize the focus keyphrase. store.dispatch( actions.loadFocusKeyword() ); - // Hide marker buttons. - store.dispatch( actions.setMarkerStatus( "hidden" ) ); + // Show marker buttons. + store.dispatch( actions.setMarkerStatus( "enabled" ) ); store.dispatch( actions.setSettings( { diff --git a/packages/js/src/helpers/elementorHook.js b/packages/js/src/helpers/elementorHook.js index eacc50d34ee..3fc87f8c1ab 100644 --- a/packages/js/src/helpers/elementorHook.js +++ b/packages/js/src/helpers/elementorHook.js @@ -113,6 +113,20 @@ export function registerElementorUIHookAfter( hook, id, callback ) { $e.hooks.registerUIAfter( new ElementorUIHook( hook, id, callback ) ); } + +/** + * Initializes the Elementor UI before hooks and registers them. + * + * @param {string} hook The hook to register to. + * @param {string} id The id to register our callback behind. + * @param {function} callback The function to call when the hook is fired. + * + * @returns {void} + */ +export function registerElementorUIHookBefore( hook, id, callback ) { + $e.hooks.registerUIBefore( new ElementorUIHook( hook, id, callback ) ); +} + /** * Initializes the Elementor Data hooks and registers them. * diff --git a/packages/js/src/initializers/analysis.js b/packages/js/src/initializers/analysis.js index 9860a3fc0d2..24ae28502a5 100644 --- a/packages/js/src/initializers/analysis.js +++ b/packages/js/src/initializers/analysis.js @@ -7,6 +7,7 @@ import handleWorkerError from "../analysis/handleWorkerError"; import { sortResultsByIdentifier } from "../analysis/refreshAnalysis"; import { createAnalysisWorker, getAnalysisConfiguration } from "../analysis/worker"; import { applyModifications } from "./pluggable"; +import getApplyMarks from "../analysis/getApplyMarks"; /** * Runs the analysis. @@ -28,6 +29,11 @@ async function runAnalysis( worker, data ) { // Only update the main results, which are located under the empty string key. const seoResults = seo[ "" ]; + // Recreate the getMarker function after the worker is done. + seoResults.results.forEach( result => { + result.getMarker = () => () => window.YoastSEO.analysis.applyMarks( paper, result.marks ); + } ); + seoResults.results = sortResultsByIdentifier( seoResults.results ); dispatch( "yoast-seo/editor" ).setSeoResultsForKeyword( paper.getKeyword(), seoResults.results ); @@ -35,6 +41,11 @@ async function runAnalysis( worker, data ) { } if ( readability ) { + // Recreate the getMarker function after the worker is done. + readability.results.forEach( result => { + result.getMarker = () => () => window.YoastSEO.analysis.applyMarks( paper, result.marks ); + } ); + readability.results = sortResultsByIdentifier( readability.results ); dispatch( "yoast-seo/editor" ).setReadabilityResults( readability.results ); @@ -42,6 +53,11 @@ async function runAnalysis( worker, data ) { } if ( inclusiveLanguage ) { + // Recreate the getMarker function after the worker is done. + inclusiveLanguage.results.forEach( result => { + result.getMarker = () => () => window.YoastSEO.analysis.applyMarks( paper, result.marks ); + } ); + inclusiveLanguage.results = sortResultsByIdentifier( inclusiveLanguage.results ); dispatch( "yoast-seo/editor" ).setInclusiveLanguageResults( inclusiveLanguage.results ); @@ -106,10 +122,14 @@ export default function initAnalysis() { // Create and initialize the worker. const worker = createAnalysisWorker(); worker.initialize( - // Get the analysis configuration and extend it with the is cornerstone content value. - getAnalysisConfiguration( { useCornerstone: isCornerstoneContent() } ) + // Get the analysis configuration and extend it with the is cornerstone content value and the marker function. + getAnalysisConfiguration( { + useCornerstone: isCornerstoneContent(), + marker: getApplyMarks(), + } ) ).catch( handleWorkerError ); + window.YoastSEO.analysis.applyMarks = ( paper, marks ) => getApplyMarks()( paper, marks ); // Initialize the data for the "is dirty" checks. let previousAnalysisData = collectData(); let previousIsCornerstone = isCornerstoneContent(); diff --git a/packages/js/src/watchers/elementorWatcher.js b/packages/js/src/watchers/elementorWatcher.js index a926ef511a4..364626dec17 100644 --- a/packages/js/src/watchers/elementorWatcher.js +++ b/packages/js/src/watchers/elementorWatcher.js @@ -1,7 +1,8 @@ -import { dispatch } from "@wordpress/data"; -import { get, debounce } from "lodash"; +import { dispatch, select } from "@wordpress/data"; +import { debounce, get } from "lodash"; import firstImageUrlInContent from "../helpers/firstImageUrlInContent"; -import { registerElementorUIHookAfter } from "../helpers/elementorHook"; +import { registerElementorUIHookAfter, registerElementorUIHookBefore } from "../helpers/elementorHook"; +import { markers, Paper } from "yoastseo"; const editorData = { content: "", @@ -11,6 +12,39 @@ const editorData = { imageUrl: "", }; +const MARK_TAG = "yoastmark"; + +/** + * Checks whether the given Elementor widget has Yoast marks. + * + * @param {Object} widget The widget. + * @returns {boolean} Whether there are marks in the HTML of the widget. + */ +function widgetHasMarks( widget ) { + return widget.innerHTML.indexOf( "<" + MARK_TAG ) !== -1; +} + +/** + * Retrieves all Elementor widget containers. + * @returns {jQuery[]} Elementor widget containers. + */ +function getWidgetContainers() { + const currentDocument = window.elementor.documents.getCurrent(); + return currentDocument.$element.find( ".elementor-widget-container" ); +} + +/** + * Removes all marks from Elementor widgets. + * + * @returns {void} + */ +function removeMarks() { + getWidgetContainers().each( ( index, element ) => { + if ( widgetHasMarks( element ) ) { + element.innerHTML = markers.removeMarks( element.innerHTML ); + } + } ); +} /** * Gets the post content. * @@ -22,7 +56,12 @@ function getContent( editorDocument ) { const content = []; editorDocument.$element.find( ".elementor-widget-container" ).each( ( index, element ) => { - content.push( element.innerHTML.trim() ); + // We remove \n and \t from the HTML as Elementor formats the HTML after saving. + // As this spacing is purely cosmetic, we can remove it for analysis purposes. + // We also convert   elements to regular spaces. + // When we apply the marks, we do need to make the same amendments. + const rawHtml = element.innerHTML.replace( /[\n\t]/g, "" ).replace( / /g, " " ).trim(); + content.push( rawHtml ); } ); return content.join( "" ); @@ -64,6 +103,7 @@ function getEditorData( editorDocument ) { }; } +/* eslint-disable complexity */ /** * Dispatches new data when the editor is dirty. * @@ -72,14 +112,17 @@ function getEditorData( editorDocument ) { function handleEditorChange() { const currentDocument = window.elementor.documents.getCurrent(); - /* - Quit early if the change was caused by switching out of the wp-post/page document. - This can happen when users go to Site Settings, for example. - */ + // Quit early if the change was caused by switching out of the wp-post/page document. + // This can happen when users go to Site Settings, for example. if ( ! [ "wp-post", "wp-page" ].includes( currentDocument.config.type ) ) { return; } + // Quit early if the highlighting functionality is on. + if ( select( "yoast-seo/editor" ).getActiveMarker() ) { + return; + } + const data = getEditorData( currentDocument ); if ( data.content !== editorData.content ) { @@ -102,17 +145,48 @@ function handleEditorChange() { dispatch( "yoast-seo/editor" ).setEditorDataImageUrl( editorData.imageUrl ); } } +/* eslint-enable complexity */ -const debouncedHandleEditorChange = debounce( handleEditorChange, 500 ); +/** + * Removes highlighting from Elementor widgets and reset the highlighting button. + * + * @returns {void} + */ +function resetMarks() { + removeMarks(); + + dispatch( "yoast-seo/editor" ).setActiveMarker( null ); + dispatch( "yoast-seo/editor" ).setMarkerPauseStatus( false ); + + window.YoastSEO.analysis.applyMarks( new Paper( "", {} ), [] ); +} + +const debouncedHandleEditorChange = debounce( handleEditorChange, 1500 ); + +/** + * Observes changes to the whole document through a MutationObserver. + * + * @returns {void} + */ +function observeChanges() { + const observer = new MutationObserver( debouncedHandleEditorChange ); + observer.observe( window.document, { attributes: true, childList: true, subtree: true, characterData: true } ); +} /** - * Initializes the watcher by coupling the change handler to the change event. + * Initializes the watcher by coupling the change handlers to the change events. * * @returns {void} */ export default function initialize() { + // This hook will fire 500ms after a widget is edited -- this allows Elementor to set the cursor at the end of the widget. + registerElementorUIHookBefore( "panel/editor/open", "yoast-seo-reset-marks-edit", debounce( resetMarks, 500 ) ); + // This hook will fire just before the document is saved. + registerElementorUIHookBefore( "document/save/save", "yoast-seo-reset-marks-save", resetMarks ); + // This hook will fire when the Elementor preview becomes available. - registerElementorUIHookAfter( "editor/documents/attach-preview", "yoast-seo-content-scraper-attach-preview", debouncedHandleEditorChange ); + registerElementorUIHookAfter( "editor/documents/attach-preview", "yoast-seo-content-scraper-initial", debouncedHandleEditorChange ); + registerElementorUIHookAfter( "editor/documents/attach-preview", "yoast-seo-content-scraper", debounce( observeChanges, 500 ) ); // This hook will fire when the contents of the editor are modified. registerElementorUIHookAfter( "document/save/set-is-modified", "yoast-seo-content-scraper-on-modified", debouncedHandleEditorChange ); diff --git a/packages/yoastseo/spec/languageProcessing/helpers/html/htmlParserSpec.js b/packages/yoastseo/spec/languageProcessing/helpers/html/htmlParserSpec.js index 0b3895ed60f..084d556a64d 100644 --- a/packages/yoastseo/spec/languageProcessing/helpers/html/htmlParserSpec.js +++ b/packages/yoastseo/spec/languageProcessing/helpers/html/htmlParserSpec.js @@ -1,6 +1,6 @@ import htmlParser from "../../../../src/languageProcessing/helpers/html/htmlParser.js"; -describe( "A function to remove the entire HTML style/script tag block.", function() { +describe( "Filters various elements from HTML", function() { it( "filters an entire style block", function() { expect( htmlParser( "" ) ).toEqual( "" ); } ); @@ -48,4 +48,82 @@ describe( "A function to remove the entire HTML style/script tag block.", functi it( "filters out all textareas", function() { expect( htmlParser( "Hi, this is a test." ) ).toEqual( "Hi, this is a test." ); } ); + it( "filters out comments", function() { + expect( htmlParser( "" ) ).toEqual( "" ); + } ); + it( "filters out elements that have a class name that should be ignored", function() { + const text = '" + + '
' + + '
' + + "
" + + "
"; + expect( htmlParser( text ) ).toEqual( "" ); + } ); + it( "filters out elements with multiple classes correctly", function() { + expect( htmlParser( "
Test

Hi, this is a test.

" ) ) + .toEqual( "

Hi, this is a test.

" ); + } ); +} ); + +describe( "Strips the table of contents from the text.", function() { + it( "should return a text without the table of contents", function() { + const text = "

Here is the list of food you can give your cat.

" + + "

Food that are raw

" + + "

Lorem ipsum dolor sit amet, est minim reprimique et, impetus interpretaris eos ea.

" + + "

Food from fresh meat

" + + "

Aperiri scripserit per cu, at mea graeci numquam.

" + + "

Food that contains vegetables

" + + "

Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. " + + "Cu semper comprehensam duo, pro fugit animal reprehendunt et.

" + + "

Food that are cooked

" + + "

Has an natum errem, vix oratio mediocrem an, pro ponderum senserit dignissim ut.

"; + + expect( htmlParser( text ) ).toBe( "

Here is the list of food you can give your cat.

" + + "

Food that are raw

Lorem ipsum dolor sit amet, est minim " + + "reprimique et, impetus interpretaris eos ea.

Food from fresh meat

" + + "

Aperiri scripserit per cu, at mea graeci numquam.

Food that contains vegetables

" + + "

Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. Cu semper comprehensam duo, " + + "pro fugit animal reprehendunt et.

Food that are cooked

" + + "

Has an natum errem, vix oratio mediocrem an, pro ponderum senserit dignissim ut.

" ); + } ); +} ); + +describe( "Strips the estimated reading time from the analysis text.", function() { + it( "should return a text without the estimated reading time", function() { + const text = "

" + + "" + + "" + + "Estimated reading time: " + + "2 minutes

" + + "

For the first time in 70 years, India’s forests will be home to cheetahs.

" + + "

Eight of them are set to arrive in August from Namibia, home to one of the world’s largest populations of the wild cat.

" + + "

Their return comes decades after India’s indigenous population was declared officially extinct in 1952.

" + + "

The world’s fastest land animal, the cheetah can reach speeds of 70 miles (113km) an hour.

"; + expect( htmlParser( text ) ).toEqual( + "

For the first time in 70 years, India’s forests will be home to cheetahs.

" + + "

Eight of them are set to arrive in August from Namibia, home to one of the world’s largest populations of the wild cat.

" + + "

Their return comes decades after India’s indigenous population was declared officially extinct in 1952.

" + + "

The world’s fastest land animal, the cheetah can reach speeds of 70 miles (113km) an hour.

" ); + } ); + + it( "should return a text without the estimated reading time, even if additional classes are added to the p element", function() { + const text = "

" + + "" + + "" + + "Estimated reading time: " + + "2 minutes

" + + "

This test has some more class(es).

"; + expect( htmlParser( text ) ).toEqual( + "

This test has some more class(es).

" ); + } ); } ); diff --git a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeEstimatedReadingTimeSpec.js b/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeEstimatedReadingTimeSpec.js deleted file mode 100644 index 84fd4486533..00000000000 --- a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeEstimatedReadingTimeSpec.js +++ /dev/null @@ -1,31 +0,0 @@ -import excludeEstimatedReadingTime from "../../../../src/languageProcessing/helpers/sanitize/excludeEstimatedReadingTime.js"; - -describe( "Strips the estimated reading time from the analysis text.", function() { - it( "returns a text without the estimated reading time", function() { - const text = "

" + - "" + - "" + - "Estimated reading time: " + - "2 minutes

" + - "

For the first time in 70 years, India’s forests will be home to cheetahs.

" + - "

Eight of them are set to arrive in August from Namibia, home to one of the world’s largest populations of the wild cat.

" + - "

Their return comes decades after India’s indigenous population was declared officially extinct in 1952.

" + - "

The world’s fastest land animal, the cheetah can reach speeds of 70 miles (113km) an hour.

"; - expect( excludeEstimatedReadingTime( text ) ).toEqual( - "

For the first time in 70 years, India’s forests will be home to cheetahs.

" + - "

Eight of them are set to arrive in August from Namibia, home to one of the world’s largest populations of the wild cat.

" + - "

Their return comes decades after India’s indigenous population was declared officially extinct in 1952.

" + - "

The world’s fastest land animal, the cheetah can reach speeds of 70 miles (113km) an hour.

" ); - } ); - - it( "returns a text without the estimated reading time, even if additional classes are added to the p element", function() { - const text = "

" + - "" + - "" + - "Estimated reading time: " + - "2 minutes

" + - "

This test has some more class(es).

"; - expect( excludeEstimatedReadingTime( text ) ).toEqual( - "

This test has some more class(es).

" ); - } ); -} ); diff --git a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeTableOfContentsTagSpec.js b/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeTableOfContentsTagSpec.js deleted file mode 100644 index ce27c45075c..00000000000 --- a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/excludeTableOfContentsTagSpec.js +++ /dev/null @@ -1,28 +0,0 @@ -import excludeTableOfContentsTag from "../../../../src/languageProcessing/helpers/sanitize/excludeTableOfContentsTag.js"; - -describe( "Strips the table of contents from the text.", function() { - it( "returns a text without the table of contents", function() { - const text = "

Here is the list of food you can give your cat.

" + - "

Food that are raw

" + - "

Lorem ipsum dolor sit amet, est minim reprimique et, impetus interpretaris eos ea.

" + - "

Food from fresh meat

" + - "

Aperiri scripserit per cu, at mea graeci numquam.

" + - "

Food that contains vegetables

" + - "

Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. " + - "Cu semper comprehensam duo, pro fugit animal reprehendunt et.

" + - "

Food that are cooked

" + - "

Has an natum errem, vix oratio mediocrem an, pro ponderum senserit dignissim ut.

"; - - expect( excludeTableOfContentsTag( text ) ).toBe( "

Here is the list of food you can give your cat.

" + - "

Food that are raw

Lorem ipsum dolor sit amet, est minim " + - "reprimique et, impetus interpretaris eos ea.

Food from fresh meat

" + - "

Aperiri scripserit per cu, at mea graeci numquam.

Food that contains vegetables

" + - "

Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. Cu semper comprehensam duo, " + - "pro fugit animal reprehendunt et.

Food that are cooked

" + - "

Has an natum errem, vix oratio mediocrem an, pro ponderum senserit dignissim ut.

" ); - } ); -} ); diff --git a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/sanitizeStringSpec.js b/packages/yoastseo/spec/languageProcessing/helpers/sanitize/sanitizeStringSpec.js index dbc9c6014cd..01559148029 100644 --- a/packages/yoastseo/spec/languageProcessing/helpers/sanitize/sanitizeStringSpec.js +++ b/packages/yoastseo/spec/languageProcessing/helpers/sanitize/sanitizeStringSpec.js @@ -10,29 +10,6 @@ describe( "Test for removing unwanted characters.", function() { expect( sanitizeString( "50/50" ) ).toBe( "50/50" ); expect( sanitizeString( "

50/50

" ) ).toBe( "50/50" ); } ); - it( "excludes Table of Content from the text", () => { - const text = "

Here is the list of food you can give your cat.

" + - "

Food that are raw

" + - "

Lorem ipsum dolor sit amet, est minim reprimique et, impetus interpretaris eos ea.

" + - "

Food from fresh meat

" + - "

Aperiri scripserit per cu, at mea graeci numquam.

" + - "

Food that contains vegetables

" + - "

Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. " + - "Cu semper comprehensam duo, pro fugit animal reprehendunt et.

" + - "

Food that are cooked

" + - "

Has an natum errem, vix oratio mediocrem an, pro ponderum senserit dignissim ut.

"; - - expect( sanitizeString( text ) ).toEqual( "Here is the list of food you can give your cat. Food that are raw Lorem ipsum dolor sit amet, " + - "est minim reprimique et, impetus interpretaris eos ea. Food from fresh meat Aperiri scripserit per cu, at mea graeci numquam." + - " Food that contains vegetables Ne vix clita soluta persecuti, vel at fugit labores, mentitum intellegebat ius ex. " + - "Cu semper comprehensam duo, pro fugit animal reprehendunt et. Food that are cooked Has an natum errem, vix oratio mediocrem an, " + - "pro ponderum senserit dignissim ut." - ); - } ); it( "unifies whitespaces and non-breaking spaces", () => { const text = "A text\u0020string."; diff --git a/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/countCharactersSpec.js b/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/countCharactersSpec.js index a601da67476..8132753b4a6 100644 --- a/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/countCharactersSpec.js +++ b/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/countCharactersSpec.js @@ -16,27 +16,6 @@ describe( "counts characters in a string", function() { expect( countCharactersFunction( "Низът в компютърните науки е крайна поредица от символи " + "(представляващи краен брой знаци)." ) ).toBe( 78 ); } ); - it( "makes sure that the table of contents is excluded from the calculation", function() { - const text = "

どんぐりころころ」は、大正時代に作られた" + - "唱歌、広義の。

発表時期[編集]

大正時代に青木存義によって作られた唱歌集『かはいい唱歌』(共益商社書店)が初出である。発表年は2説ある。" + - "これは初出の『かはいい唱歌 二冊目』の奥付が、初版本とその後の重版本とで異なることに起因する。巷に比較的現存している部数が多い重版本では、「一冊目」と同一日付の" + - "「大正十年十月」発行との記載があり、この1921年(大正10年)10月であるとする説が主流である。もう1説は、初版本に由来する。青木の故郷である松島町では昭和後期から青木の歌" + - "を歌い継ごうとする動きが活発となり、そうした活動を通じて地元の郷土史家らが青木家の関係者から本作が掲載されている「二冊目」を譲り受けた。

" + - "

制作状況

" + - "

本作品が掲載された『かはいい唱歌』は「幼稚園又は小学校初年級程度」の子どもを対象として作成されている。青木は当時「文部省図書監修官」及び「小学校唱歌教科書編纂委員」" + - "の任にあったものの、この唱歌集は私的に民間の出版社から出したものであり、いわゆる文部省編纂の「\">文部省唱歌」にはあたらない。「一冊目」「二冊目」ともに10編、" + - "1 2 計20編が収録されており、本作品は「二冊目」の第7番目に掲載されている。" + - "作詞は全て青木自身であり、青木の詞に曲をつけた作曲者は計12名、本作品の作曲者である梁田貞は" + - "『兎と狸』と併せ計2曲の作曲を担当している。

" + - "

教育現場での使用[\">編集]

" + - "

戦後においては一般に広義の童謡にカテゴライズされる本作品は、" + - "初出本の題名にもあるとおり青木自身は「唱歌」であるとし、「学校や家庭で」歌ってもらえれば本懐であるとしている。しかし発表当時の教育現場では、" + - "本作品を歌うことは原則上はできなかった。

"; - expect( countCharactersFunction( text ) ).toBe( 744 ); - } ); it( "makes sure that no characters are counted when a URL is embedded in video tags", function() { const text = "\n" + diff --git a/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/getWordsSpec.js b/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/getWordsSpec.js index 775f57b6633..4e04bc36719 100644 --- a/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/getWordsSpec.js +++ b/packages/yoastseo/spec/languageProcessing/languages/ja/helpers/getWordsSpec.js @@ -21,18 +21,4 @@ describe( "test for getting Japanese segmented words", function() { expect( words ).toEqual( [ "計画", "段階", "で", "は", "東海道", "新線", "と", "呼ば", "れ", "て", "い", "た", "が", "開業", "時", "に", "は", "東海道", "新幹線", "と", "命名", "さ", "れ", "た" ] ); } ); - - it( "excludes Table of Contents from the segmenter and strips html tags", function() { - const words = getWords( "" + - "

猫の種類< meta charset='utf-8'>

" + - "

ベロでは、毛皮の色に基づいて猫の種類を見つけることができます。

" + - "

キャットフード

" + - "

猫が食べることができる食べ物の例は以下にあります。

" ); - - expect( words ).toEqual( [ "猫", "の", "種類", "ベロ", "で", "は", "毛皮", "の", "色", "に", "基づい", "て", "猫", - "の", "種類", "を", "見つける", "こと", "が", "でき", "ます", "キャットフード", "猫", "が", "食べる", "こと", "が", "できる", - "食べ物", "の", "例", "は", "以下", "に", "あり", "ます" ] - ); - } ); } ); diff --git a/packages/yoastseo/spec/parse/build/buildSpec.js b/packages/yoastseo/spec/parse/build/buildSpec.js index e85cf7fe33d..cad0ea56596 100644 --- a/packages/yoastseo/spec/parse/build/buildSpec.js +++ b/packages/yoastseo/spec/parse/build/buildSpec.js @@ -853,17 +853,6 @@ describe( "The parse function", () => { name: "#document-fragment", attributes: {}, childNodes: [ { - name: "p", - isImplicit: true, - attributes: {}, - sentences: [], - childNodes: [], - sourceCodeLocation: { - startOffset: 0, - endOffset: 45, - }, - }, - { name: "p", isImplicit: false, attributes: {}, diff --git a/packages/yoastseo/spec/parse/build/private/filterTreeSpec.js b/packages/yoastseo/spec/parse/build/private/filterTreeSpec.js index f54d00d605f..f530da24680 100644 --- a/packages/yoastseo/spec/parse/build/private/filterTreeSpec.js +++ b/packages/yoastseo/spec/parse/build/private/filterTreeSpec.js @@ -278,6 +278,14 @@ describe( "Miscellaneous tests", () => { const tree = adapt( parseFragment( html, { sourceCodeLocationInfo: true } ) ); expect( tree.findAll( child => child.name === "abbr" ) ).toHaveLength( 0 ); } ); + + it( "should filter out span elements and remove the implicit paragraph it's part of", () => { + const html = 'My cat loves me.'; + const tree = adapt( parseFragment( html, { sourceCodeLocationInfo: true } ) ); + const filteredTree = filterTree( tree, permanentFilters ); + expect( filteredTree.findAll( child => [ "span", "p" ].includes( child.name ) ) ).toHaveLength( 0 ); + } ); + it( "should filter out the Elementor Yoast Breadcrumbs widget ", () => { // When the HTML enters the paper, the Breadcrumbs widget doesn't include the div tag. let html = "

Home

IGNORED_CLASSES.includes( className ) ) ) { inIgnorableBlock = true; + ignoreStack.push( tagName ); return; } @@ -49,7 +88,7 @@ const parser = new htmlparser.Parser( { }, /** * Handles the text that doesn't contain opening or closing tags. - * If inIgnorableBlock is false, the text gets pushed to the textArray array. + * If `inIgnorableBlock` is false, the text gets pushed to the `textArray` array. * * @param {string} text The text that doesn't contain opening or closing tags. * @@ -62,20 +101,22 @@ const parser = new htmlparser.Parser( { }, /** * Handles the closing tag. - * If the closing tag is included in the ignoredTags array, set inIgnorableBlock to false. - * Otherwise, if we are not currently in an ignorable block, push the tag to the textArray. + * If the closing tag is the last tag on the `ignoreStack`, jump out of the ignorable block. + * Otherwise, if we are not currently in an ignorable block, push the tag to the `textArray`. * * @param {string} tagName The tag name. * * @returns {void} */ onclosetag: function( tagName ) { - if ( includes( ignoredTags, tagName ) ) { + if ( ignoreStack.length === 1 && ignoreStack[ 0 ] === tagName ) { inIgnorableBlock = false; + ignoreStack = []; return; } if ( inIgnorableBlock ) { + ignoreStack.pop(); return; } @@ -84,14 +125,19 @@ const parser = new htmlparser.Parser( { }, { decodeEntities: true } ); /** - * Calls the htmlparser and returns the text without the HTML blocks as defined in the ignoredTags array. + * Calls htmlparser2 and returns the text without HTML blocks that we do not want to consider for the content analysis. + * Note that this function will soon be deprecated in favour of our own HTML parser. * * @param {string} text The text to parse. * - * @returns {string} The text without the HTML blocks as defined in the ignoredTags array. + * @returns {string} The text without the HTML blocks. */ export default function( text ) { + // Return the globals to their starting values. textArray = []; + inIgnorableBlock = false; + ignoreStack = []; + parser.write( text ); // Make sure to complete the process of parsing and reset the parser to avoid side effects. parser.parseComplete(); diff --git a/packages/yoastseo/src/languageProcessing/helpers/sanitize/excludeEstimatedReadingTime.js b/packages/yoastseo/src/languageProcessing/helpers/sanitize/excludeEstimatedReadingTime.js deleted file mode 100644 index 638af294194..00000000000 --- a/packages/yoastseo/src/languageProcessing/helpers/sanitize/excludeEstimatedReadingTime.js +++ /dev/null @@ -1,13 +0,0 @@ -const estimatedReadingTimeRegex = new RegExp( "

).*?(

)", "igs" ); - -/** - * Excludes table of contents from text. - * - * @param {String} text The text to check. - * - * @returns {String} The text without table of contents. - */ -export default function excludeTableOfContentsTag( text ) { - text = text.replace( tableOfContentsTagRegex, "" ); - return text; -} diff --git a/packages/yoastseo/src/languageProcessing/helpers/sanitize/sanitizeString.js b/packages/yoastseo/src/languageProcessing/helpers/sanitize/sanitizeString.js index ec6615d56c0..c2c3ba064c6 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/sanitize/sanitizeString.js +++ b/packages/yoastseo/src/languageProcessing/helpers/sanitize/sanitizeString.js @@ -1,5 +1,3 @@ -import excludeTableOfContentsTag from "./excludeTableOfContentsTag"; -import excludeEstimatedReadingTime from "../sanitize/excludeEstimatedReadingTime"; import { stripFullTags as stripTags } from "./stripHTMLTags.js"; import { unifyAllSpaces } from "./unifyWhitespace"; @@ -13,10 +11,6 @@ import { unifyAllSpaces } from "./unifyWhitespace"; export default function( text ) { // Unify whitespaces and non-breaking spaces. text = unifyAllSpaces( text ); - // Remove Table of Contents. - text = excludeTableOfContentsTag( text ); - // Remove Estimated reading time - text = excludeEstimatedReadingTime( text ); // Strip the tags and multiple spaces. text = stripTags( text ); diff --git a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentences.js b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentences.js index 0072e9b450f..6189751fe73 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentences.js +++ b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentences.js @@ -4,8 +4,6 @@ import { filter, flatMap, isEmpty, negate } from "lodash-es"; // Internal dependencies. import { getBlocks } from "../html/html.js"; import { imageRegex } from "../image/imageInText"; -import excludeTableOfContentsTag from "../sanitize/excludeTableOfContentsTag"; -import excludeEstimatedReadingTime from "../sanitize/excludeEstimatedReadingTime"; import { stripBlockTagsAtStartEnd } from "../sanitize/stripHTMLTags"; import { unifyNonBreakingSpace } from "../sanitize/unifyWhitespace"; import defaultSentenceTokenizer from "./memoizedSentenceTokenizer"; @@ -27,10 +25,6 @@ const paragraphTagsRegex = new RegExp( "^(

|

)$" ); */ export default function( text, memoizedTokenizer = defaultSentenceTokenizer ) { // We don't remove the other HTML tags here since removing them might lead to incorrect results when running the sentence tokenizer. - // Remove Table of Contents. - text = excludeTableOfContentsTag( text ); - // Remove Estimated reading time. - text = excludeEstimatedReadingTime( text ); // Unify only non-breaking spaces and not the other whitespaces since a whitespace could signify a sentence break or a new line. text = unifyNonBreakingSpace( text ); /* diff --git a/packages/yoastseo/src/languageProcessing/languages/ja/helpers/countCharacters.js b/packages/yoastseo/src/languageProcessing/languages/ja/helpers/countCharacters.js index 5c2cec887af..180d803370f 100644 --- a/packages/yoastseo/src/languageProcessing/languages/ja/helpers/countCharacters.js +++ b/packages/yoastseo/src/languageProcessing/languages/ja/helpers/countCharacters.js @@ -4,8 +4,7 @@ import removeURLs from "../../../helpers/sanitize/removeURLs.js"; /** * Calculates the character count which serves as a measure of text length. - * The character count includes letters, punctuation, and numbers. It doesn't include URLs, HTML tags, spaces, and the - * content of the Table of Contents and Estimated Reading Time blocks. + * The character count includes letters, punctuation, and numbers. It doesn't include URLs, HTML tags, and spaces. * * @param {string} text The text to be counted. * diff --git a/packages/yoastseo/src/languageProcessing/languages/ja/helpers/getWords.js b/packages/yoastseo/src/languageProcessing/languages/ja/helpers/getWords.js index 69e653883bf..401db21393c 100644 --- a/packages/yoastseo/src/languageProcessing/languages/ja/helpers/getWords.js +++ b/packages/yoastseo/src/languageProcessing/languages/ja/helpers/getWords.js @@ -13,7 +13,7 @@ const segmenter = new TinySegmenter(); * @returns {Array} The array with all words. */ export default function( text ) { - // Strips HTML tags and exclude Table of Contents from the analysis. + // Strips HTML tags. text = sanitizeString( text ); if ( text === "" ) { return []; diff --git a/packages/yoastseo/src/languageProcessing/researches/getParagraphLength.js b/packages/yoastseo/src/languageProcessing/researches/getParagraphLength.js index a85cb29d00f..f45b223f85e 100644 --- a/packages/yoastseo/src/languageProcessing/researches/getParagraphLength.js +++ b/packages/yoastseo/src/languageProcessing/researches/getParagraphLength.js @@ -1,6 +1,4 @@ import { imageRegex } from "../helpers/image/imageInText"; -import excludeTableOfContentsTag from "../helpers/sanitize/excludeTableOfContentsTag"; -import excludeEstimatedReadingTime from "../helpers/sanitize/excludeEstimatedReadingTime"; import sanitizeLineBreakTag from "../helpers/sanitize/sanitizeLineBreakTag"; import countWords from "../helpers/word/countWords.js"; import matchParagraphs from "../helpers/html/matchParagraphs.js"; @@ -21,9 +19,6 @@ export default function( paper, researcher ) { text = removeHtmlBlocks( text ); text = filterShortcodesFromHTML( text, paper._attributes && paper._attributes.shortcodes ); - text = excludeTableOfContentsTag( text ); - // Exclude the Estimated Reading time text from the research - text = excludeEstimatedReadingTime( text ); // Remove images from text before retrieving the paragraphs. // This step is done here so that applying highlight in captions is possible for ParagraphTooLongAssessment. text = text.replace( imageRegex, "" ); diff --git a/packages/yoastseo/src/languageProcessing/researches/getSubheadingTextLengths.js b/packages/yoastseo/src/languageProcessing/researches/getSubheadingTextLengths.js index 618413675e1..9cc81f4acac 100644 --- a/packages/yoastseo/src/languageProcessing/researches/getSubheadingTextLengths.js +++ b/packages/yoastseo/src/languageProcessing/researches/getSubheadingTextLengths.js @@ -1,5 +1,4 @@ import getSubheadingTexts from "../helpers/html/getSubheadingTexts"; -import excludeTableOfContentsTag from "../helpers/sanitize/excludeTableOfContentsTag"; import countWords from "../helpers/word/countWords"; import { forEach } from "lodash-es"; import removeHtmlBlocks from "../helpers/html/htmlParser"; @@ -17,7 +16,6 @@ export default function( paper, researcher ) { let text = paper.getText(); text = removeHtmlBlocks( text ); text = filterShortcodesFromHTML( text, paper._attributes && paper._attributes.shortcodes ); - text = excludeTableOfContentsTag( text ); const matches = getSubheadingTexts( text ); // An optional custom helper to count length to use instead of countWords. diff --git a/packages/yoastseo/src/languageProcessing/researches/matchKeywordInSubheadings.js b/packages/yoastseo/src/languageProcessing/researches/matchKeywordInSubheadings.js index 82e4a332912..21de6552fe8 100644 --- a/packages/yoastseo/src/languageProcessing/researches/matchKeywordInSubheadings.js +++ b/packages/yoastseo/src/languageProcessing/researches/matchKeywordInSubheadings.js @@ -1,5 +1,4 @@ import { getSubheadingContentsTopLevel } from "../helpers/html/getSubheadings"; -import excludeTableOfContentsTag from "../helpers/sanitize/excludeTableOfContentsTag"; import stripSomeTags from "../helpers/sanitize/stripNonTextTags"; import { findTopicFormsInString } from "../helpers/match/findKeywordFormsInString"; import removeHtmlBlocks from "../helpers/html/htmlParser"; @@ -46,7 +45,7 @@ export default function matchKeywordInSubheadings( paper, researcher ) { let text = paper.getText(); text = removeHtmlBlocks( text ); text = filterShortcodesFromHTML( text, paper._attributes && paper._attributes.shortcodes ); - text = stripSomeTags( excludeTableOfContentsTag( text ) ); + text = stripSomeTags( text ); const topicForms = researcher.getResearch( "morphology" ); const locale = paper.getLocale(); const result = { count: 0, matches: 0, percentReflectingTopic: 0 }; diff --git a/packages/yoastseo/src/parse/build/private/alwaysFilterElements.js b/packages/yoastseo/src/parse/build/private/alwaysFilterElements.js index 7a8f99c3363..52b68fb3020 100644 --- a/packages/yoastseo/src/parse/build/private/alwaysFilterElements.js +++ b/packages/yoastseo/src/parse/build/private/alwaysFilterElements.js @@ -15,8 +15,23 @@ const permanentFilters = [ // Comments are filtered out in `filterBeforeTokenizing.js` step. elementHasClass( "yoast-table-of-contents" ), elementHasClass( "yoast-reading-time__wrapper" ), - // Filters for the Elementor widget Yoast Breadcrumbs. + // Filters for Elementor widgets elementHasID( "breadcrumbs" ), + elementHasClass( "elementor-button-wrapper" ), + elementHasClass( "elementor-divider" ), + elementHasClass( "elementor-spacer" ), + elementHasClass( "elementor-custom-embed" ), + elementHasClass( "elementor-icon-wrapper" ), + elementHasClass( "elementor-icon-box-wrapper" ), + elementHasClass( "elementor-counter" ), + elementHasClass( "elementor-progress-wrapper" ), + // This element is used for the progress bar widget title. + elementHasClass( "elementor-title" ), + elementHasClass( "elementor-alert" ), + elementHasClass( "elementor-soundcloud-wrapper" ), + elementHasClass( "elementor-shortcode" ), + elementHasClass( "elementor-menu-anchor" ), + elementHasClass( "e-rating" ), // Filters out HTML elements. /* Elements are filtered out when: they contain content outside of the author's control (incl. quotes and embedded content); their content isn't natural language (e.g. code); they contain metadata hidden from the page visitor diff --git a/packages/yoastseo/src/parse/build/private/filterTree.js b/packages/yoastseo/src/parse/build/private/filterTree.js index f187dc8a1a4..1e2660726f2 100644 --- a/packages/yoastseo/src/parse/build/private/filterTree.js +++ b/packages/yoastseo/src/parse/build/private/filterTree.js @@ -1,3 +1,5 @@ +import { Paragraph } from "../../structure"; + /** * Checks if a node should be kept or discarded. * @param {Node} node A node. @@ -29,6 +31,11 @@ export default function filterTree( node, filters ) { // Recursively filters the node's children. if ( node.childNodes ) { node.childNodes = node.childNodes.filter( childNode => filterTree( childNode, filters ) ); + + // Drops implicit paragraphs if all their child nodes have been removed. + if ( node.childNodes.length === 0 && node instanceof Paragraph && node.isImplicit ) { + return; + } } return node;