From 93d363b99563d4a034a3b8c17e457fd4fe1ba507 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Fri, 14 Mar 2025 16:55:18 +0000 Subject: [PATCH 01/42] Initial search correction commit - Add two new methods, init_corrected_query_hooks() and update_search_options_with_correction() - These are likely to be changed - Adjust search-results.jsx to (1) search against the correct query while (2) display informative text - This will be adjusted as the current functionality is to establish a foundation while ensuring things work - Adds new styles for the "Did you mean?" line --- .../src/inline-search/class-inline-search.php | 32 +++++++++++++ .../components/search-results.jsx | 29 ++++++++++++ .../components/search-results.scss | 46 +++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index ded60c3fd4549..a3a4f57c882e2 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -421,4 +421,36 @@ public function get_search_result( ) { return $this->search_result; } + + /** + * Initialize hooks for handling corrected query functionality. + */ + public function init_corrected_query_hooks() { + parent::init_hooks(); + + // Add hook to update search options with corrected query + add_action( 'pre_get_posts', array( $this, 'update_search_options_with_correction' ) ); + } + + /** + * Updates Instant Search options with corrected query if one exists. + * + * @param \WP_Query $query The WP_Query instance. + */ + public function update_search_options_with_correction( $query ) { + if ( ! $this->should_handle_query( $query ) ) { + return; + } + + if ( isset( $this->search_result['corrected_query'] ) && $this->search_result['corrected_query'] ) { + // Add the corrected query to the Instant Search options + add_filter( + 'jetpack_instant_search_options', + function ( $options ) { + $options['correctedQuery'] = $this->search_result['corrected_query']; + return $options; + } + ); + } + } } diff --git a/projects/packages/search/src/instant-search/components/search-results.jsx b/projects/packages/search/src/instant-search/components/search-results.jsx index 76f975445deda..b3366917246a2 100644 --- a/projects/packages/search/src/instant-search/components/search-results.jsx +++ b/projects/packages/search/src/instant-search/components/search-results.jsx @@ -117,6 +117,32 @@ class SearchResults extends Component { return __( 'Showing popular results', 'jetpack-search-pkg' ); } + getCorrectedSearchQuery() { + const { corrected_query = false, corrected_query_total = 0 } = this.props.response; + const hasCorrectedQuery = corrected_query !== false; + const hasCorrectedResults = corrected_query_total > 0; + + if ( ! hasCorrectedQuery || ! hasCorrectedResults ) { + return ''; + } + + return ( + { + e.preventDefault(); + this.props.onChangeSearch( corrected_query ); + } } + className="jetpack-instant-search__search-results-corrected-query-link" + > + { + // translators: %s: Suggested search query + sprintf( __( 'Did you mean "%s"?', 'jetpack-search-pkg' ), corrected_query ) + } + + ); + } + renderPrimarySection() { const { highlightColor, searchQuery } = this.props; const { results = [], total = 0, corrected_query = false } = this.props.response; @@ -156,6 +182,9 @@ class SearchResults extends Component { }

) } + + { this.getCorrectedSearchQuery() } + { this.props.hasError && ( { getErrorMessage( this.props.response.error ) } ) } diff --git a/projects/packages/search/src/instant-search/components/search-results.scss b/projects/packages/search/src/instant-search/components/search-results.scss index 0d8be26f70993..c0a04620d787f 100644 --- a/projects/packages/search/src/instant-search/components/search-results.scss +++ b/projects/packages/search/src/instant-search/components/search-results.scss @@ -223,6 +223,52 @@ $colophon-height: 40px; } } +.jetpack-instant-search__search-results-corrected-query { + margin: 0 $results-margin-lg 1.5em; + font-style: italic; + color: var(--jp-gray-60); + + @include multiple-breaks-for-customberg( ' Date: Fri, 14 Mar 2025 17:44:00 +0000 Subject: [PATCH 02/42] changelog --- .../search/changelog/enhance-surface-search-correction | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/search/changelog/enhance-surface-search-correction diff --git a/projects/packages/search/changelog/enhance-surface-search-correction b/projects/packages/search/changelog/enhance-surface-search-correction new file mode 100644 index 0000000000000..2ceb54c606616 --- /dev/null +++ b/projects/packages/search/changelog/enhance-surface-search-correction @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Surface search corrections when correcting search terms From dd3585b4ec7dd19643f8061e00ecc5347bde14b8 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 17 Mar 2025 14:36:55 +0000 Subject: [PATCH 03/42] Update wording/order - Add proper "Did you mean?" text - Remove code from last week's testing - Prep for turning the search suggestion into a link --- .../components/search-results.jsx | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/projects/packages/search/src/instant-search/components/search-results.jsx b/projects/packages/search/src/instant-search/components/search-results.jsx index b3366917246a2..833fdef73cc91 100644 --- a/projects/packages/search/src/instant-search/components/search-results.jsx +++ b/projects/packages/search/src/instant-search/components/search-results.jsx @@ -62,24 +62,16 @@ class SearchResults extends Component { return __( 'Searching…', 'jetpack-search-pkg', /* dummy arg to avoid bad minification */ 0 ); } - if ( total === 0 || this.props.hasError ) { - return __( 'No results found', 'jetpack-search-pkg' ); + if ( total === 0 || this.props.hasError || ( hasQuery && hasCorrectedQuery ) ) { + return sprintf( + /* translators: %s: search query. */ + __( 'No results found for "%s"', 'jetpack-search-pkg' ), + this.props.searchQuery + ); } const num = new Intl.NumberFormat().format( total ); - if ( hasQuery && hasCorrectedQuery ) { - return sprintf( - /* translators: %1$s: number of results. %2$s: the corrected search query. */ - _n( - 'Found %1$s result for "%2$s"', - 'Found %1$s results for "%2$s"', - total, - 'jetpack-search-pkg' - ), - num, - corrected_query - ); - } else if ( isMultiSite ) { + if ( isMultiSite ) { const group = getAvailableStaticFilters().find( item => item.filter_id === 'group_id' ); const filterKey = group?.filter_id; @@ -144,7 +136,7 @@ class SearchResults extends Component { } renderPrimarySection() { - const { highlightColor, searchQuery } = this.props; + const { highlightColor } = this.props; const { results = [], total = 0, corrected_query = false } = this.props.response; const textColor = getConstrastingColor( highlightColor ); const hasCorrectedQuery = corrected_query !== false; @@ -174,11 +166,11 @@ class SearchResults extends Component {

{ this.getSearchTitle() }

- { hasResults && hasCorrectedQuery && ( + { hasCorrectedQuery && (

{ - /* translators: %s: Search query. */ - sprintf( __( 'No results for "%s"', 'jetpack-search-pkg' ), searchQuery ) + /* translators: %s: Corrected search query. */ + sprintf( __( 'Did you mean "%s"?', 'jetpack-search-pkg' ), corrected_query ) }

) } From ade1bfee71f60ea1b0935f86564280708f12b4e4 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 18 Mar 2025 10:10:13 +0000 Subject: [PATCH 04/42] Initial linking of corrected search result --- .../components/search-results.jsx | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/projects/packages/search/src/instant-search/components/search-results.jsx b/projects/packages/search/src/instant-search/components/search-results.jsx index 833fdef73cc91..a9735586394f1 100644 --- a/projects/packages/search/src/instant-search/components/search-results.jsx +++ b/projects/packages/search/src/instant-search/components/search-results.jsx @@ -110,36 +110,39 @@ class SearchResults extends Component { } getCorrectedSearchQuery() { - const { corrected_query = false, corrected_query_total = 0 } = this.props.response; + const { corrected_query = false } = this.props.response; const hasCorrectedQuery = corrected_query !== false; - const hasCorrectedResults = corrected_query_total > 0; - if ( ! hasCorrectedQuery || ! hasCorrectedResults ) { + if ( ! hasCorrectedQuery ) { return ''; } - return ( - { - e.preventDefault(); - this.props.onChangeSearch( corrected_query ); - } } - className="jetpack-instant-search__search-results-corrected-query-link" +

- { - // translators: %s: Suggested search query - sprintf( __( 'Did you mean "%s"?', 'jetpack-search-pkg' ), corrected_query ) - } - + { sprintf( + /* translators: %s: Suggested search query */ + __( 'Did you mean %s?', 'jetpack-search-pkg' ), + { + e.preventDefault(); + this.props.onChangeSearch( corrected_query ); + } } + className="jetpack-instant-search__search-results-corrected-query-link" + > + { corrected_query } + + ) } +

); } renderPrimarySection() { const { highlightColor } = this.props; - const { results = [], total = 0, corrected_query = false } = this.props.response; + const { results = [], total = 0 } = this.props.response; const textColor = getConstrastingColor( highlightColor ); - const hasCorrectedQuery = corrected_query !== false; const hasResults = total > 0; const isMultiSite = @@ -166,15 +169,6 @@ class SearchResults extends Component {

{ this.getSearchTitle() }

- { hasCorrectedQuery && ( -

- { - /* translators: %s: Corrected search query. */ - sprintf( __( 'Did you mean "%s"?', 'jetpack-search-pkg' ), corrected_query ) - } -

- ) } - { this.getCorrectedSearchQuery() } { this.props.hasError && ( From accc0d67693d8b88b58c35331e18ea8319a5fb19 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 18 Mar 2025 13:51:26 +0000 Subject: [PATCH 05/42] - Change order of "No results" and "Did you mean" text - Update styling - Rather than offering a "Did you mean?" choice, automatically try search suggestion --- .../components/search-results.jsx | 34 +++++++------------ .../components/search-results.scss | 1 - 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/projects/packages/search/src/instant-search/components/search-results.jsx b/projects/packages/search/src/instant-search/components/search-results.jsx index a9735586394f1..d9b9b5539f97e 100644 --- a/projects/packages/search/src/instant-search/components/search-results.jsx +++ b/projects/packages/search/src/instant-search/components/search-results.jsx @@ -63,10 +63,13 @@ class SearchResults extends Component { } if ( total === 0 || this.props.hasError || ( hasQuery && hasCorrectedQuery ) ) { - return sprintf( - /* translators: %s: search query. */ - __( 'No results found for "%s"', 'jetpack-search-pkg' ), - this.props.searchQuery + return ( + hasCorrectedQuery && + sprintf( + /* translators: %s: suggested search query */ + __( 'Showing results for "%s"', 'jetpack-search-pkg' ), + corrected_query + ) ); } @@ -117,24 +120,11 @@ class SearchResults extends Component { return ''; } return ( -

- { sprintf( - /* translators: %s: Suggested search query */ - __( 'Did you mean %s?', 'jetpack-search-pkg' ), - { - e.preventDefault(); - this.props.onChangeSearch( corrected_query ); - } } - className="jetpack-instant-search__search-results-corrected-query-link" - > - { corrected_query } - - ) } +

+ { + /* translators: %s: Original search query */ + sprintf( __( 'No results found for "%s"', 'jetpack-search-pkg' ), this.props.searchQuery ) + }

); } diff --git a/projects/packages/search/src/instant-search/components/search-results.scss b/projects/packages/search/src/instant-search/components/search-results.scss index c0a04620d787f..f7086c85ef430 100644 --- a/projects/packages/search/src/instant-search/components/search-results.scss +++ b/projects/packages/search/src/instant-search/components/search-results.scss @@ -225,7 +225,6 @@ $colophon-height: 40px; .jetpack-instant-search__search-results-corrected-query { margin: 0 $results-margin-lg 1.5em; - font-style: italic; color: var(--jp-gray-60); @include multiple-breaks-for-customberg( ' Date: Wed, 26 Mar 2025 16:13:16 +0000 Subject: [PATCH 06/42] Restore Instant Search Changes Restoring Instance Search changes back to trunk as our changes are going to apply to Inline Search only. --- .../components/search-results.jsx | 43 ++++++------------ .../components/search-results.scss | 45 ------------------- 2 files changed, 13 insertions(+), 75 deletions(-) diff --git a/projects/packages/search/src/instant-search/components/search-results.jsx b/projects/packages/search/src/instant-search/components/search-results.jsx index f2de8c940a65a..eefa04b9d1652 100644 --- a/projects/packages/search/src/instant-search/components/search-results.jsx +++ b/projects/packages/search/src/instant-search/components/search-results.jsx @@ -62,15 +62,8 @@ class SearchResults extends Component { return __( 'Searching…', 'jetpack-search-pkg', /* dummy arg to avoid bad minification */ 0 ); } - if ( total === 0 || this.props.hasError || ( hasQuery && hasCorrectedQuery ) ) { - return ( - hasCorrectedQuery && - sprintf( - /* translators: %s: suggested search query */ - __( 'Showing results for "%s"', 'jetpack-search-pkg' ), - corrected_query - ) - ); + if ( total === 0 || this.props.hasError ) { + return __( 'No results found', 'jetpack-search-pkg' ); } const num = new Intl.NumberFormat().format( total ); @@ -131,27 +124,11 @@ class SearchResults extends Component { return __( 'Showing popular results', 'jetpack-search-pkg' ); } - getCorrectedSearchQuery() { - const { corrected_query = false } = this.props.response; - const hasCorrectedQuery = corrected_query !== false; - - if ( ! hasCorrectedQuery ) { - return ''; - } - return ( -

- { - /* translators: %s: Original search query */ - sprintf( __( 'No results found for "%s"', 'jetpack-search-pkg' ), this.props.searchQuery ) - } -

- ); - } - renderPrimarySection() { - const { highlightColor } = this.props; - const { results = [], total = 0 } = this.props.response; + const { highlightColor, searchQuery } = this.props; + const { results = [], total = 0, corrected_query = false } = this.props.response; const textColor = getConstrastingColor( highlightColor ); + const hasCorrectedQuery = corrected_query !== false; const hasResults = total > 0; const isMultiSite = @@ -178,8 +155,14 @@ class SearchResults extends Component {

{ this.getSearchTitle() }

- { this.getCorrectedSearchQuery() } - + { hasResults && hasCorrectedQuery && ( +

+ { + /* translators: %s: Search query. */ + sprintf( __( 'No results for "%s"', 'jetpack-search-pkg' ), searchQuery ) + } +

+ ) } { this.props.hasError && ( { getErrorMessage( this.props.response.error ) } ) } diff --git a/projects/packages/search/src/instant-search/components/search-results.scss b/projects/packages/search/src/instant-search/components/search-results.scss index f7086c85ef430..0d8be26f70993 100644 --- a/projects/packages/search/src/instant-search/components/search-results.scss +++ b/projects/packages/search/src/instant-search/components/search-results.scss @@ -223,51 +223,6 @@ $colophon-height: 40px; } } -.jetpack-instant-search__search-results-corrected-query { - margin: 0 $results-margin-lg 1.5em; - color: var(--jp-gray-60); - - @include multiple-breaks-for-customberg( ' Date: Thu, 27 Mar 2025 15:47:12 +0000 Subject: [PATCH 07/42] Initial Inline Search changes - Hook into pre_get_posts() for later displaying the corrected_query text - Add a new setup_corrected_query_hooks() method which is to be modified in a future commit. - Add a new maybe_use_corrected_query() method - Add a new prepend_corrected_query_to_first_result() method to ensure that the corrected_query, if it's used, displays in the "Search results for:..." heading - Add get_corrected_query_html() for outputting markup - Add JS for injecting our new markup. This will be moved to its own file in a future commit. Functionally: - If a spelling error is corrected, and there is a result for the corrected spelling, the search form will return with two headings: (H1) "Search results for... { corrected_query }" followed by (H2) "No results found for... { original_query }" - If a spelling error is discovered but there is no result for the corrected spelling, the search form will return with one headaing: (H1) "No results found for... { original_query }" --- .../src/inline-search/class-inline-search.php | 183 ++++++++++++++++-- 1 file changed, 167 insertions(+), 16 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index a3a4f57c882e2..7d723cdede071 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -39,6 +39,9 @@ public static function instance( $blog_id = null ) { } self::$instance = new static(); self::$instance->setup( $blog_id ); + + // Add hooks for displaying corrected query notice + add_action( 'pre_get_posts', array( self::$instance, 'setup_corrected_query_hooks' ) ); } return self::$instance; @@ -423,34 +426,182 @@ public function get_search_result( } /** - * Initialize hooks for handling corrected query functionality. + * Setup hooks for displaying corrected query notice. + * + * @param \WP_Query $query The current query. */ - public function init_corrected_query_hooks() { - parent::init_hooks(); + public function setup_corrected_query_hooks( $query ) { + if ( ! $query->is_search() || ! $query->is_main_query() ) { + return; + } + + // Only add the hooks once + static $hooks_added = false; + if ( $hooks_added ) { + return; + } + $hooks_added = true; + + // Try multiple hooks to ensure our notice appears + add_filter( 'get_the_archive_title', array( $this, 'append_corrected_query_to_title' ) ); + add_filter( 'the_title', array( $this, 'prepend_corrected_query_to_first_result' ), 10, 2 ); - // Add hook to update search options with corrected query - add_action( 'pre_get_posts', array( $this, 'update_search_options_with_correction' ) ); + // Update the search title with corrected query + add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); + + // This is our fallback to ensure the notice appears somewhere + add_action( 'wp_footer', array( $this, 'output_corrected_query_script' ) ); } /** - * Updates Instant Search options with corrected query if one exists. + * Replaces the search query with the corrected query in the title. * - * @param \WP_Query $query The WP_Query instance. + * @param string $query The original search query. + * @return string The corrected query if available, otherwise the original query. */ - public function update_search_options_with_correction( $query ) { - if ( ! $this->should_handle_query( $query ) ) { + public function maybe_use_corrected_query( $query ) { + if ( ! is_search() ) { + return $query; + } + + // Return the corrected query if available + if ( ! empty( $this->search_result['corrected_query'] ) ) { + return $this->search_result['corrected_query']; + } + + return $query; + } + + /** + * Add corrected query notice before the first search result title. + * + * @param string $title The post title. + * @param int $id The post ID. + * @return string The modified title. + */ + public function prepend_corrected_query_to_first_result( $title, $id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Only modify search page titles of the first result + if ( ! is_search() || ! in_the_loop() ) { + return $title; + } + + // Only append once + static $modified = false; + if ( $modified ) { + return $title; + } + $modified = true; + + // Get the notice HTML + $notice_html = $this->get_corrected_query_html(); + if ( empty( $notice_html ) ) { + return $title; + } + + return $notice_html . $title; + } + + /** + * Append corrected query notice to the search page title. + * + * @param string $title The archive title. + * @return string The modified title. + */ + public function append_corrected_query_to_title( $title ) { + // Only modify search page titles + if ( ! is_search() ) { + return $title; + } + + // Only append once + static $modified = false; + if ( $modified ) { + return $title; + } + $modified = true; + + // Get the notice HTML + $notice_html = $this->get_corrected_query_html(); + if ( empty( $notice_html ) ) { + return $title; + } + + return $title . $notice_html; + } + + /** + * Output JavaScript to inject the corrected query notice after the search title. + * This is a fallback in case the filters don't work. + */ + public function output_corrected_query_script() { + if ( ! is_search() ) { + return; + } + + // Get the notice HTML + $notice_html = $this->get_corrected_query_html(); + if ( empty( $notice_html ) ) { return; } - if ( isset( $this->search_result['corrected_query'] ) && $this->search_result['corrected_query'] ) { - // Add the corrected query to the Instant Search options - add_filter( - 'jetpack_instant_search_options', - function ( $options ) { - $options['correctedQuery'] = $this->search_result['corrected_query']; - return $options; + // Escape the HTML for JavaScript + $escaped_html = str_replace( + array( "'", "\n", "\r" ), + array( "\'", "\\n", '' ), + $notice_html + ); + + ?> + + search_result['corrected_query'] ) ) { + // Store the original search query before it was potentially replaced by maybe_use_corrected_query + $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + return sprintf( + '

+ %s%s +

', + esc_html( $original_query ) ); } + + return ''; } } From ce345c39c5983cf7c7a0ca3f6e74d53e738c7e5e Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Fri, 28 Mar 2025 17:01:16 +0000 Subject: [PATCH 08/42] Cleanup --- .../src/inline-search/class-inline-search.php | 136 +++--------------- 1 file changed, 22 insertions(+), 114 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 7d723cdede071..b33d06849ae14 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -435,21 +435,7 @@ public function setup_corrected_query_hooks( $query ) { return; } - // Only add the hooks once - static $hooks_added = false; - if ( $hooks_added ) { - return; - } - $hooks_added = true; - - // Try multiple hooks to ensure our notice appears - add_filter( 'get_the_archive_title', array( $this, 'append_corrected_query_to_title' ) ); - add_filter( 'the_title', array( $this, 'prepend_corrected_query_to_first_result' ), 10, 2 ); - - // Update the search title with corrected query add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); - - // This is our fallback to ensure the notice appears somewhere add_action( 'wp_footer', array( $this, 'output_corrected_query_script' ) ); } @@ -460,87 +446,19 @@ public function setup_corrected_query_hooks( $query ) { * @return string The corrected query if available, otherwise the original query. */ public function maybe_use_corrected_query( $query ) { - if ( ! is_search() ) { - return $query; - } - - // Return the corrected query if available - if ( ! empty( $this->search_result['corrected_query'] ) ) { + if ( ! empty( $this->search_result['corrected_query'] ) && ! empty( $this->search_result['results'] ) ) { return $this->search_result['corrected_query']; } return $query; } - /** - * Add corrected query notice before the first search result title. - * - * @param string $title The post title. - * @param int $id The post ID. - * @return string The modified title. - */ - public function prepend_corrected_query_to_first_result( $title, $id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - // Only modify search page titles of the first result - if ( ! is_search() || ! in_the_loop() ) { - return $title; - } - - // Only append once - static $modified = false; - if ( $modified ) { - return $title; - } - $modified = true; - - // Get the notice HTML - $notice_html = $this->get_corrected_query_html(); - if ( empty( $notice_html ) ) { - return $title; - } - - return $notice_html . $title; - } - - /** - * Append corrected query notice to the search page title. - * - * @param string $title The archive title. - * @return string The modified title. - */ - public function append_corrected_query_to_title( $title ) { - // Only modify search page titles - if ( ! is_search() ) { - return $title; - } - - // Only append once - static $modified = false; - if ( $modified ) { - return $title; - } - $modified = true; - - // Get the notice HTML - $notice_html = $this->get_corrected_query_html(); - if ( empty( $notice_html ) ) { - return $title; - } - - return $title . $notice_html; - } - /** * Output JavaScript to inject the corrected query notice after the search title. - * This is a fallback in case the filters don't work. */ public function output_corrected_query_script() { - if ( ! is_search() ) { - return; - } - - // Get the notice HTML - $notice_html = $this->get_corrected_query_html(); - if ( empty( $notice_html ) ) { + $corrected_query_html = $this->get_corrected_query_html(); + if ( empty( $corrected_query_html ) ) { return; } @@ -548,37 +466,26 @@ public function output_corrected_query_script() { $escaped_html = str_replace( array( "'", "\n", "\r" ), array( "\'", "\\n", '' ), - $notice_html + $corrected_query_html ); ?> search_result['corrected_query'] ) ) { - // Store the original search query before it was potentially replaced by maybe_use_corrected_query - $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $corrected_query = $this->search_result['corrected_query']; + $search_results = $this->search_result['results']; + if ( ! empty( $corrected_query ) && ! count( $search_results ) ) { return sprintf( '

%s%s

', + esc_html__( 'No results found for: ', 'jetpack-search-pkg' ), esc_html( $original_query ) ); } From 738a11ced43d88cec15b0922fa885f7e39a0d496 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 31 Mar 2025 16:25:53 +0100 Subject: [PATCH 09/42] Cleanup and Refactor - Refactor output_corrected_query_script() method - New get_title_selectors() method - New enqueue_corrected_query_script() method - Refactor output_corrected_query_html() method - Moved JS to new ../inline-search/js file - Add a new filter to allow users to specify their own search title selectors if their theme does not use one of the three selectors specified. These were checked across themes Twenty Twenty Five going back to Twenty Ten. --- .../src/inline-search/class-inline-search.php | 93 +++++++++++++------ .../src/inline-search/js/corrected-query.js | 32 +++++++ 2 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 projects/packages/search/src/inline-search/js/corrected-query.js diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index b33d06849ae14..fc8a0cb8fb020 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -462,32 +462,64 @@ public function output_corrected_query_script() { return; } - // Escape the HTML for JavaScript - $escaped_html = str_replace( - array( "'", "\n", "\r" ), - array( "\'", "\\n", '' ), - $corrected_query_html - ); + $selectors = $this->get_title_selectors(); + $this->enqueue_corrected_query_script( $corrected_query_html, $selectors ); + } - ?> - - $html, + 'selectors' => $selectors, + ) + ); } /** @@ -496,16 +528,19 @@ public function output_corrected_query_script() { * @return string The HTML for the corrected query notice or empty string if none. */ private function get_corrected_query_html() { - $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $corrected_query = $this->search_result['corrected_query']; - $search_results = $this->search_result['results']; + $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + // Show message when there's a corrected query + if ( ! empty( $this->search_result['corrected_query'] ) ) { + $message = ! empty( $this->search_result['results'] ) + ? esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ) + : esc_html__( 'No results found for: ', 'jetpack-search-pkg' ); - if ( ! empty( $corrected_query ) && ! count( $search_results ) ) { return sprintf( - '

+ '

%s%s

', - esc_html__( 'No results found for: ', 'jetpack-search-pkg' ), + $message, esc_html( $original_query ) ); } diff --git a/projects/packages/search/src/inline-search/js/corrected-query.js b/projects/packages/search/src/inline-search/js/corrected-query.js new file mode 100644 index 0000000000000..3f9632f9a1121 --- /dev/null +++ b/projects/packages/search/src/inline-search/js/corrected-query.js @@ -0,0 +1,32 @@ +/** + * Script to display corrected query notice after search titles. + */ +document.addEventListener( 'DOMContentLoaded', function () { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); +} ); From 6005d0e791802da6ae5be40458e34ed589cb4f0e Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 14:02:24 +0100 Subject: [PATCH 10/42] Let us try adding some tests --- .../src/inline-search/class-inline-search.php | 1 - .../search/tests/php/Inline_Search_Test.php | 291 ++++++++++++++++++ 2 files changed, 291 insertions(+), 1 deletion(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index fc8a0cb8fb020..cf9d7025c7972 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -535,7 +535,6 @@ private function get_corrected_query_html() { $message = ! empty( $this->search_result['results'] ) ? esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ) : esc_html__( 'No results found for: ', 'jetpack-search-pkg' ); - return sprintf( '

%s%s diff --git a/projects/packages/search/tests/php/Inline_Search_Test.php b/projects/packages/search/tests/php/Inline_Search_Test.php index 00a117b5aa1ef..94db744add062 100644 --- a/projects/packages/search/tests/php/Inline_Search_Test.php +++ b/projects/packages/search/tests/php/Inline_Search_Test.php @@ -179,6 +179,97 @@ public static function data_provider(): array { ); } + /** + * Test setup_corrected_query_hooks + * + * This test verifies that the setup_corrected_query_hooks method correctly adds the necessary hooks to the WP_Query object. + */ + public function test_setup_corrected_query_hooks() { + $search = Inline_Search::instance(); + + // Create a mock WP_Query that is a main search query + $query = $this->getMockBuilder( '\WP_Query' ) + ->disableOriginalConstructor() + ->getMock(); + $query->method( 'is_search' )->willReturn( true ); + $query->method( 'is_main_query' )->willReturn( true ); + + // Call the method we're testing + $search->setup_corrected_query_hooks( $query ); + + // Verify that both hooks were added + $this->assertEquals( + 10, + has_filter( 'get_search_query', array( $search, 'maybe_use_corrected_query' ) ), + 'get_search_query filter was not added correctly' + ); + } + + /** + * Test maybe_use_corrected_query method + * + * This test verifies that maybe_use_corrected_query returns the corrected query + * only when both corrected_query and results exist in search_result. + */ + public function test_maybe_use_corrected_query() { + $search = Inline_Search::instance(); + $original_query = 'original search'; + + // Use reflection to access protected property + $reflection = new \ReflectionClass( get_class( $search ) ); + $property = $reflection->getProperty( 'search_result' ); + $property->setAccessible( true ); + + // Test when search_result is empty + $property->setValue( $search, array() ); + $this->assertEquals( + $original_query, + $search->maybe_use_corrected_query( $original_query ), + 'Should return original query when search_result is empty' + ); + + // Test when corrected_query exists but no results + $property->setValue( + $search, + array( + 'corrected_query' => 'corrected search', + 'results' => array(), + ) + ); + $this->assertEquals( + $original_query, + $search->maybe_use_corrected_query( $original_query ), + 'Should return original query when results is empty' + ); + + // Test when results exist but no corrected_query + $property->setValue( + $search, + array( + 'results' => array( 'some result' ), + ) + ); + $this->assertEquals( + $original_query, + $search->maybe_use_corrected_query( $original_query ), + 'Should return original query when corrected_query is not set' + ); + + // Test when both corrected_query and results exist + $property->setValue( + $search, + array( + 'corrected_query' => 'corrected search', + 'results' => array( 'some result' ), + ) + ); + $this->assertEquals( + 'corrected search', + $search->maybe_use_corrected_query( $original_query ), + 'Should return corrected query when both corrected_query and results exist' + ); + } + /** * Test search request * @@ -194,4 +285,204 @@ public function test_search( array $wp_query_args, array $expected_api_args ) { parse_str( wp_parse_url( $this->last_search_url, PHP_URL_QUERY ), $actual_api_args ); $this->assertEquals( $expected_api_args, $actual_api_args ); } + + /** + * Test get_title_selectors method + */ + public function test_get_title_selectors() { + $search = Inline_Search::instance(); + + // Use reflection to access private method + $reflection = new \ReflectionClass( get_class( $search ) ); + $method = $reflection->getMethod( 'get_title_selectors' ); + $method->setAccessible( true ); + + // Test default selectors + $default_selectors = array( + '.wp-block-query-title', + '.page-title', + '.archive-title', + ); + $this->assertEquals( + $default_selectors, + $method->invoke( $search ), + 'Default selectors should match expected values' + ); + + // Test with filter + add_filter( + 'jetpack_search_title_selectors', + function () { + return array( '.custom-title', '.my-title' ); + } + ); + + $this->assertEquals( + array( '.custom-title', '.my-title' ), + $method->invoke( $search ), + 'Filtered selectors should match custom values' + ); + + // Clean up + remove_all_filters( 'jetpack_search_title_selectors' ); + } + + /** + * Test get_corrected_query_html method + */ + public function test_get_corrected_query_html() { + $search = Inline_Search::instance(); + + // Use reflection to access private method and protected property + $reflection = new \ReflectionClass( get_class( $search ) ); + $method = $reflection->getMethod( 'get_corrected_query_html' ); + $method->setAccessible( true ); + $property = $reflection->getProperty( 'search_result' ); + $property->setAccessible( true ); + + // Test with no search query + $this->assertSame( + '', + $method->invoke( $search ), + 'Should return empty string when no search query' + ); + + // Mock 's' parameter + $_GET['s'] = 'originl speling'; + + // Test with corrected query and results + $property->setValue( + $search, + array( + 'corrected_query' => 'original spelling', + 'results' => array( 'some result' ), + ) + ); + + $expected_html = sprintf( + '

' . "\n" . + ' %s%s' . "\n" . + '

', + esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ), + 'originl speling' + ); + + $this->assertEquals( + $expected_html, + $method->invoke( $search ), + 'Should return correction notice when query is corrected with results' + ); + + // Test with corrected query but no results + $property->setValue( + $search, + array( + 'corrected_query' => 'original spelling', + 'results' => array(), + ) + ); + + $expected_html = sprintf( + '

' . "\n" . + ' %s%s' . "\n" . + '

', + esc_html__( 'No results found for: ', 'jetpack-search-pkg' ), + 'originl speling' + ); + + $this->assertEquals( + $expected_html, + $method->invoke( $search ), + 'Should return no results notice when query is corrected without results' + ); + + // Clean up + unset( $_GET['s'] ); + } + + /** + * Test enqueue_corrected_query_script method + */ + public function test_enqueue_corrected_query_script() { + $search = Inline_Search::instance(); + + // Use reflection to access private method + $reflection = new \ReflectionClass( get_class( $search ) ); + $method = $reflection->getMethod( 'enqueue_corrected_query_script' ); + $method->setAccessible( true ); + + // Test script enqueuing + $html = '
Test HTML
'; + $selectors = array( '.test-selector' ); + $method->invoke( $search, $html, $selectors ); + + $this->assertTrue( + wp_script_is( 'jetpack-search-inline-corrected-query', 'registered' ), + 'Script should be registered' + ); + + $this->assertTrue( + wp_script_is( 'jetpack-search-inline-corrected-query', 'enqueued' ), + 'Script should be enqueued' + ); + + // Test localized data + $data = wp_scripts()->get_data( 'jetpack-search-inline-corrected-query', 'data' ); + $this->assertStringContainsString( + 'JetpackSearchCorrectedQuery', + $data, + 'Script data should be localized with correct object name' + ); + $this->assertStringContainsString( + '"html":"
Test HTML<\\/div>"', + $data, + 'Script data should contain the HTML' + ); + $this->assertStringContainsString( + json_encode( $selectors ), + $data, + 'Script data should contain the selectors' + ); + } + + /** + * Test output_corrected_query_script method + */ + public function test_output_corrected_query_script() { + $search = Inline_Search::instance(); + + // Use reflection to access protected property + $reflection = new \ReflectionClass( get_class( $search ) ); + $property = $reflection->getProperty( 'search_result' ); + $property->setAccessible( true ); + + // Test with no corrected query + $property->setValue( $search, array() ); + ob_start(); + $search->output_corrected_query_script(); + $output = ob_get_clean(); + $this->assertEmpty( $output, 'Should output nothing when no corrected query exists' ); + + // Test with corrected query + $_GET['s'] = 'test query'; + $property->setValue( + $search, + array( + 'corrected_query' => 'corrected query', + 'results' => array( 'some result' ), + ) + ); + + ob_start(); + $search->output_corrected_query_script(); + $output = ob_get_clean(); + + $this->assertTrue( + wp_script_is( 'jetpack-search-inline-corrected-query', 'enqueued' ), + 'Script should be enqueued when corrected query exists' + ); + + // Clean up + unset( $_GET['s'] ); + } } From c913e783b27acccc6c4e5ca0136def78dfe19309 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 14:48:06 +0100 Subject: [PATCH 11/42] Test JS --- .../js/test/corrected-query.test.js | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 projects/packages/search/src/inline-search/js/test/corrected-query.test.js diff --git a/projects/packages/search/src/inline-search/js/test/corrected-query.test.js b/projects/packages/search/src/inline-search/js/test/corrected-query.test.js new file mode 100644 index 0000000000000..abbf08146d1b5 --- /dev/null +++ b/projects/packages/search/src/inline-search/js/test/corrected-query.test.js @@ -0,0 +1,294 @@ +/** + * @jest-environment jsdom + */ + +describe( 'Corrected Query Notice', () => { + let originalJetpackSearchCorrectedQuery; + + beforeEach( () => { + // Store original JetpackSearchCorrectedQuery + originalJetpackSearchCorrectedQuery = window.JetpackSearchCorrectedQuery; + + // Reset the DOM + document.body.innerHTML = ''; + + // Reset window.JetpackSearchCorrectedQuery + delete window.JetpackSearchCorrectedQuery; + } ); + + afterEach( () => { + // Restore original JetpackSearchCorrectedQuery + if ( originalJetpackSearchCorrectedQuery ) { + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: originalJetpackSearchCorrectedQuery, + configurable: true, + } ); + } else { + delete window.JetpackSearchCorrectedQuery; + } + } ); + + test( 'should not add notice when JetpackSearchCorrectedQuery is not defined', () => { + // Setup + document.body.innerHTML = '

Search Results

'; + + // Execute the function directly instead of relying on the event + // This is the function from corrected-query.js + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); + } + + correctedQueryFunction(); + + // Assert + expect( document.querySelectorAll( '.search-title' ) ).toHaveLength( 1 ); + expect( document.querySelectorAll( '.search-title + div' ) ).toHaveLength( 0 ); + } ); + + test( 'should not add notice when JetpackSearchCorrectedQuery has no html', () => { + // Setup + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: { selectors: [ '.search-title' ] }, + configurable: true, + } ); + document.body.innerHTML = '

Search Results

'; + + // Execute the function directly instead of relying on the event + // This is the function from corrected-query.js + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); + } + + correctedQueryFunction(); + + // Assert + expect( document.querySelectorAll( '.search-title' ) ).toHaveLength( 1 ); + expect( document.querySelectorAll( '.search-title + div' ) ).toHaveLength( 0 ); + } ); + + test( 'should not add notice when no matching selector is found', () => { + // Setup + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: { + selectors: [ '.non-existent-selector' ], + html: '
Did you mean: example?
', + }, + configurable: true, + } ); + document.body.innerHTML = '

Search Results

'; + + // Execute the function directly instead of relying on the event + // This is the function from corrected-query.js + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); + } + + correctedQueryFunction(); + + // Assert + expect( document.querySelectorAll( '.corrected-query' ) ).toHaveLength( 0 ); + } ); + + test( 'should add notice with correct styling when all conditions are met', () => { + // Setup + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: { + selectors: [ '.search-title' ], + html: '
Did you mean: example?
', + }, + configurable: true, + } ); + document.body.innerHTML = '

Search Results

'; + + // Execute the function directly instead of relying on the event + // This is the function from corrected-query.js + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); + } + + correctedQueryFunction(); + + // Get the notice element + const notice = document.querySelector( '.search-title + div' ); + + // Assert + expect( notice ).not.toBeNull(); + expect( notice ).toHaveClass( 'custom-class', 'corrected-query' ); + expect( notice ).toHaveStyle( { + fontSize: '0.9em', + marginTop: '10px', + paddingTop: '0', + } ); + expect( notice ).toHaveTextContent( 'Did you mean: example?' ); + } ); + + test( 'should handle multiple selectors', () => { + // Setup + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: { + selectors: [ '.non-existent', '.search-title' ], + html: '
Did you mean: example?
', + }, + configurable: true, + } ); + document.body.innerHTML = '

Search Results

'; + + // Execute the function directly instead of relying on the event + // This is the function from corrected-query.js + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Only proceed if we have corrected query data + if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { + return; + } + + // Get the selectors and join them for querySelector + const selectors = window.JetpackSearchCorrectedQuery.selectors; + const selectorString = selectors.join( ', ' ); + + // Find the title element using the selectors + const titleElement = document.querySelector( selectorString ); + if ( ! titleElement ) { + return; + } + + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; + const notice = tempDiv.firstChild; + + // Apply styling and insert + const originalClass = notice.className; + notice.className = titleElement.className + ' ' + originalClass; + notice.style.fontSize = '0.9em'; + notice.style.marginTop = '10px'; + notice.style.paddingTop = '0'; + + titleElement.insertAdjacentElement( 'afterend', notice ); + } + + correctedQueryFunction(); + + // Assert + const notice = document.querySelector( '.search-title + div' ); + expect( notice ).not.toBeNull(); + expect( notice ).toHaveClass( 'corrected-query' ); + } ); +} ); From 0bd7df3be7c7df75dedd76f7fc885745b7dcb128 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 15:26:52 +0100 Subject: [PATCH 12/42] Revise and update our tests --- .../search/src/inline-search/js/helpers.js | 39 ++ .../src/inline-search/js/helpers.test.js | 61 +++ .../js/test/corrected-query.test.js | 373 ++++++------------ 3 files changed, 213 insertions(+), 260 deletions(-) create mode 100644 projects/packages/search/src/inline-search/js/helpers.js create mode 100644 projects/packages/search/src/inline-search/js/helpers.test.js diff --git a/projects/packages/search/src/inline-search/js/helpers.js b/projects/packages/search/src/inline-search/js/helpers.js new file mode 100644 index 0000000000000..eb3c50ffcf7fa --- /dev/null +++ b/projects/packages/search/src/inline-search/js/helpers.js @@ -0,0 +1,39 @@ +/** + * Sets up JetpackSearchCorrectedQuery with the provided data. + * + * @param {object} data - The data to set for JetpackSearchCorrectedQuery + */ +export function setupJetpackSearchCorrectedQuery( data ) { + Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { + value: data, + configurable: true, + } ); +} + +/** + * Resets JetpackSearchCorrectedQuery to undefined. + */ +export function resetJetpackSearchCorrectedQuery() { + delete window.JetpackSearchCorrectedQuery; +} + +/** + * Creates a DOM element from HTML string using Range API. + * + * @param {string} html - The HTML string to convert to an element + * @return {Element} The first element from the created HTML + */ +export function createElementFromHtml( html ) { + const range = document.createRange(); + return range.createContextualFragment( html ).firstChild; +} + +/** + * Applies styles to a DOM element. + * + * @param {Element} element - The element to style + * @param {object} styles - Object containing style properties and values + */ +export function applyStyles( element, styles ) { + Object.assign( element.style, styles ); +} diff --git a/projects/packages/search/src/inline-search/js/helpers.test.js b/projects/packages/search/src/inline-search/js/helpers.test.js new file mode 100644 index 0000000000000..aa8fcd9738436 --- /dev/null +++ b/projects/packages/search/src/inline-search/js/helpers.test.js @@ -0,0 +1,61 @@ +/** + * @jest-environment jsdom + */ + +import { + setupJetpackSearchCorrectedQuery, + resetJetpackSearchCorrectedQuery, + createElementFromHtml, + applyStyles, +} from './helpers'; + +describe( 'Helpers', () => { + describe( 'setupJetpackSearchCorrectedQuery', () => { + afterEach( () => { + delete window.JetpackSearchCorrectedQuery; + } ); + + test( 'sets JetpackSearchCorrectedQuery on window', () => { + const testData = { test: 'data' }; + setupJetpackSearchCorrectedQuery( testData ); + expect( window.JetpackSearchCorrectedQuery ).toEqual( testData ); + } ); + } ); + + describe( 'resetJetpackSearchCorrectedQuery', () => { + test( 'removes JetpackSearchCorrectedQuery from window', () => { + window.JetpackSearchCorrectedQuery = { test: 'data' }; + resetJetpackSearchCorrectedQuery(); + expect( window.JetpackSearchCorrectedQuery ).toBeUndefined(); + } ); + } ); + + describe( 'createElementFromHtml', () => { + test( 'creates element from HTML string', () => { + const html = '
Test Content
'; + const element = createElementFromHtml( html ); + expect( element.tagName ).toBe( 'DIV' ); + expect( element.className ).toBe( 'test' ); + expect( element ).toHaveTextContent( 'Test Content' ); + } ); + + test( 'returns first child element', () => { + const html = '
First
Second
'; + const element = createElementFromHtml( html ); + expect( element ).toHaveTextContent( 'First' ); + } ); + } ); + + describe( 'applyStyles', () => { + test( 'applies styles to element', () => { + const element = document.createElement( 'div' ); + const styles = { + color: 'red', + backgroundColor: 'blue', + }; + applyStyles( element, styles ); + expect( element ).toHaveStyle( { color: 'red' } ); + expect( element ).toHaveStyle( { backgroundColor: 'blue' } ); + } ); + } ); +} ); diff --git a/projects/packages/search/src/inline-search/js/test/corrected-query.test.js b/projects/packages/search/src/inline-search/js/test/corrected-query.test.js index abbf08146d1b5..22be5584fc2fa 100644 --- a/projects/packages/search/src/inline-search/js/test/corrected-query.test.js +++ b/projects/packages/search/src/inline-search/js/test/corrected-query.test.js @@ -2,293 +2,146 @@ * @jest-environment jsdom */ +import { + setupJetpackSearchCorrectedQuery, + resetJetpackSearchCorrectedQuery, + createElementFromHtml, + applyStyles, +} from '../helpers'; + describe( 'Corrected Query Notice', () => { + // Test constants + const TEST_HTML = '
Did you mean: example?
'; + const TEST_SELECTORS = [ '.search-title' ]; + const TEST_TITLE_HTML = '

Search Results

'; + const TEST_TITLE_WITH_CLASS_HTML = '

Search Results

'; + const NOTICE_STYLES = { + fontSize: '0.9em', + marginTop: '10px', + paddingTop: '0', + }; + let originalJetpackSearchCorrectedQuery; + /** + * Adds a corrected query notice after search titles when correction data is available. + */ + function correctedQueryFunction() { + // Get query data and validate + const queryData = window.JetpackSearchCorrectedQuery; + if ( ! queryData?.html || ! queryData?.selectors?.length ) { + return; + } + + // Find title element using selectors + const titleElement = document.querySelector( queryData.selectors.join( ', ' ) ); + if ( ! titleElement ) { + return; + } + + // Create and configure notice element + const noticeElement = createElementFromHtml( queryData.html ); + noticeElement.className = `${ titleElement.className } ${ noticeElement.className }`; + applyStyles( noticeElement, NOTICE_STYLES ); + + // Insert notice after title + titleElement.insertAdjacentElement( 'afterend', noticeElement ); + } + beforeEach( () => { - // Store original JetpackSearchCorrectedQuery + // Store original state originalJetpackSearchCorrectedQuery = window.JetpackSearchCorrectedQuery; - - // Reset the DOM + // Reset test environment document.body.innerHTML = ''; - - // Reset window.JetpackSearchCorrectedQuery - delete window.JetpackSearchCorrectedQuery; + resetJetpackSearchCorrectedQuery(); } ); afterEach( () => { - // Restore original JetpackSearchCorrectedQuery + // Restore original state if ( originalJetpackSearchCorrectedQuery ) { - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: originalJetpackSearchCorrectedQuery, - configurable: true, - } ); + setupJetpackSearchCorrectedQuery( originalJetpackSearchCorrectedQuery ); } else { - delete window.JetpackSearchCorrectedQuery; - } - } ); - - test( 'should not add notice when JetpackSearchCorrectedQuery is not defined', () => { - // Setup - document.body.innerHTML = '

Search Results

'; - - // Execute the function directly instead of relying on the event - // This is the function from corrected-query.js - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { - return; - } - - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); - - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); - if ( ! titleElement ) { - return; - } - - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; - - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; - - titleElement.insertAdjacentElement( 'afterend', notice ); + resetJetpackSearchCorrectedQuery(); } - - correctedQueryFunction(); - - // Assert - expect( document.querySelectorAll( '.search-title' ) ).toHaveLength( 1 ); - expect( document.querySelectorAll( '.search-title + div' ) ).toHaveLength( 0 ); } ); - test( 'should not add notice when JetpackSearchCorrectedQuery has no html', () => { - // Setup - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: { selectors: [ '.search-title' ] }, - configurable: true, + describe( 'when JetpackSearchCorrectedQuery is not properly configured', () => { + test( 'should not add notice when JetpackSearchCorrectedQuery is not defined', () => { + document.body.innerHTML = TEST_TITLE_HTML; + correctedQueryFunction(); + expect( document.querySelector( '.corrected-query' ) ).toBeNull(); } ); - document.body.innerHTML = '

Search Results

'; - - // Execute the function directly instead of relying on the event - // This is the function from corrected-query.js - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { - return; - } - - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); - - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); - if ( ! titleElement ) { - return; - } - - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; - - titleElement.insertAdjacentElement( 'afterend', notice ); - } - - correctedQueryFunction(); - - // Assert - expect( document.querySelectorAll( '.search-title' ) ).toHaveLength( 1 ); - expect( document.querySelectorAll( '.search-title + div' ) ).toHaveLength( 0 ); - } ); - - test( 'should not add notice when no matching selector is found', () => { - // Setup - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: { - selectors: [ '.non-existent-selector' ], - html: '
Did you mean: example?
', - }, - configurable: true, + test( 'should not add notice when JetpackSearchCorrectedQuery has no html', () => { + setupJetpackSearchCorrectedQuery( { selectors: TEST_SELECTORS } ); + document.body.innerHTML = TEST_TITLE_HTML; + correctedQueryFunction(); + expect( document.querySelector( '.corrected-query' ) ).toBeNull(); } ); - document.body.innerHTML = '

Search Results

'; - - // Execute the function directly instead of relying on the event - // This is the function from corrected-query.js - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { - return; - } - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); - - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); - if ( ! titleElement ) { - return; - } - - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; - - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; - - titleElement.insertAdjacentElement( 'afterend', notice ); - } - - correctedQueryFunction(); - - // Assert - expect( document.querySelectorAll( '.corrected-query' ) ).toHaveLength( 0 ); - } ); - - test( 'should add notice with correct styling when all conditions are met', () => { - // Setup - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: { - selectors: [ '.search-title' ], - html: '
Did you mean: example?
', - }, - configurable: true, + test( 'should not add notice when no matching selector is found', () => { + setupJetpackSearchCorrectedQuery( { + selectors: [ '.non-existent-selector' ], + html: TEST_HTML, + } ); + document.body.innerHTML = TEST_TITLE_HTML; + correctedQueryFunction(); + expect( document.querySelector( '.corrected-query' ) ).toBeNull(); } ); - document.body.innerHTML = '

Search Results

'; - - // Execute the function directly instead of relying on the event - // This is the function from corrected-query.js - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { - return; - } - - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); - if ( ! titleElement ) { - return; - } - - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; - - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; - - titleElement.insertAdjacentElement( 'afterend', notice ); - } - - correctedQueryFunction(); - - // Get the notice element - const notice = document.querySelector( '.search-title + div' ); - - // Assert - expect( notice ).not.toBeNull(); - expect( notice ).toHaveClass( 'custom-class', 'corrected-query' ); - expect( notice ).toHaveStyle( { - fontSize: '0.9em', - marginTop: '10px', - paddingTop: '0', + test( 'should not add notice when selectors array is empty', () => { + setupJetpackSearchCorrectedQuery( { + selectors: [], + html: TEST_HTML, + } ); + document.body.innerHTML = TEST_TITLE_HTML; + correctedQueryFunction(); + expect( document.querySelector( '.corrected-query' ) ).toBeNull(); } ); - expect( notice ).toHaveTextContent( 'Did you mean: example?' ); } ); - test( 'should handle multiple selectors', () => { - // Setup - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: { - selectors: [ '.non-existent', '.search-title' ], - html: '
Did you mean: example?
', - }, - configurable: true, + describe( 'when JetpackSearchCorrectedQuery is properly configured', () => { + test( 'should add notice with correct styling when all conditions are met', () => { + setupJetpackSearchCorrectedQuery( { + selectors: TEST_SELECTORS, + html: TEST_HTML, + } ); + document.body.innerHTML = TEST_TITLE_WITH_CLASS_HTML; + correctedQueryFunction(); + + const notice = document.querySelector( '.corrected-query' ); + expect( notice ).not.toBeNull(); + expect( notice ).toHaveClass( 'custom-class', 'corrected-query' ); + expect( notice ).toHaveStyle( NOTICE_STYLES ); + expect( notice ).toHaveTextContent( 'Did you mean: example?' ); } ); - document.body.innerHTML = '

Search Results

'; - - // Execute the function directly instead of relying on the event - // This is the function from corrected-query.js - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { - return; - } - - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); - if ( ! titleElement ) { - return; - } - - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; - - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; + test( 'should handle multiple selectors', () => { + setupJetpackSearchCorrectedQuery( { + selectors: [ '.non-existent', '.search-title' ], + html: TEST_HTML, + } ); + document.body.innerHTML = TEST_TITLE_HTML; + correctedQueryFunction(); - titleElement.insertAdjacentElement( 'afterend', notice ); - } + const notice = document.querySelector( '.corrected-query' ); + expect( notice ).not.toBeNull(); + expect( notice ).toHaveClass( 'corrected-query' ); + expect( notice ).toHaveStyle( NOTICE_STYLES ); + } ); - correctedQueryFunction(); + test( 'should preserve original notice class when adding title classes', () => { + setupJetpackSearchCorrectedQuery( { + selectors: TEST_SELECTORS, + html: TEST_HTML, + } ); + document.body.innerHTML = TEST_TITLE_WITH_CLASS_HTML; + correctedQueryFunction(); - // Assert - const notice = document.querySelector( '.search-title + div' ); - expect( notice ).not.toBeNull(); - expect( notice ).toHaveClass( 'corrected-query' ); + const notice = document.querySelector( '.corrected-query' ); + expect( notice.className ).toContain( 'search-title' ); + expect( notice.className ).toContain( 'custom-class' ); + expect( notice.className ).toContain( 'corrected-query' ); + } ); } ); } ); From 102d5fe91a4f1f6d57259f6e575a64f11901c44f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 17:20:24 +0100 Subject: [PATCH 13/42] Revert tests for inclusion in a latter PR --- .../search/src/inline-search/js/helpers.js | 39 --- .../src/inline-search/js/helpers.test.js | 61 ---- .../js/test/corrected-query.test.js | 147 --------- .../search/tests/php/Inline_Search_Test.php | 291 ------------------ 4 files changed, 538 deletions(-) delete mode 100644 projects/packages/search/src/inline-search/js/helpers.js delete mode 100644 projects/packages/search/src/inline-search/js/helpers.test.js delete mode 100644 projects/packages/search/src/inline-search/js/test/corrected-query.test.js diff --git a/projects/packages/search/src/inline-search/js/helpers.js b/projects/packages/search/src/inline-search/js/helpers.js deleted file mode 100644 index eb3c50ffcf7fa..0000000000000 --- a/projects/packages/search/src/inline-search/js/helpers.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Sets up JetpackSearchCorrectedQuery with the provided data. - * - * @param {object} data - The data to set for JetpackSearchCorrectedQuery - */ -export function setupJetpackSearchCorrectedQuery( data ) { - Object.defineProperty( window, 'JetpackSearchCorrectedQuery', { - value: data, - configurable: true, - } ); -} - -/** - * Resets JetpackSearchCorrectedQuery to undefined. - */ -export function resetJetpackSearchCorrectedQuery() { - delete window.JetpackSearchCorrectedQuery; -} - -/** - * Creates a DOM element from HTML string using Range API. - * - * @param {string} html - The HTML string to convert to an element - * @return {Element} The first element from the created HTML - */ -export function createElementFromHtml( html ) { - const range = document.createRange(); - return range.createContextualFragment( html ).firstChild; -} - -/** - * Applies styles to a DOM element. - * - * @param {Element} element - The element to style - * @param {object} styles - Object containing style properties and values - */ -export function applyStyles( element, styles ) { - Object.assign( element.style, styles ); -} diff --git a/projects/packages/search/src/inline-search/js/helpers.test.js b/projects/packages/search/src/inline-search/js/helpers.test.js deleted file mode 100644 index aa8fcd9738436..0000000000000 --- a/projects/packages/search/src/inline-search/js/helpers.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { - setupJetpackSearchCorrectedQuery, - resetJetpackSearchCorrectedQuery, - createElementFromHtml, - applyStyles, -} from './helpers'; - -describe( 'Helpers', () => { - describe( 'setupJetpackSearchCorrectedQuery', () => { - afterEach( () => { - delete window.JetpackSearchCorrectedQuery; - } ); - - test( 'sets JetpackSearchCorrectedQuery on window', () => { - const testData = { test: 'data' }; - setupJetpackSearchCorrectedQuery( testData ); - expect( window.JetpackSearchCorrectedQuery ).toEqual( testData ); - } ); - } ); - - describe( 'resetJetpackSearchCorrectedQuery', () => { - test( 'removes JetpackSearchCorrectedQuery from window', () => { - window.JetpackSearchCorrectedQuery = { test: 'data' }; - resetJetpackSearchCorrectedQuery(); - expect( window.JetpackSearchCorrectedQuery ).toBeUndefined(); - } ); - } ); - - describe( 'createElementFromHtml', () => { - test( 'creates element from HTML string', () => { - const html = '
Test Content
'; - const element = createElementFromHtml( html ); - expect( element.tagName ).toBe( 'DIV' ); - expect( element.className ).toBe( 'test' ); - expect( element ).toHaveTextContent( 'Test Content' ); - } ); - - test( 'returns first child element', () => { - const html = '
First
Second
'; - const element = createElementFromHtml( html ); - expect( element ).toHaveTextContent( 'First' ); - } ); - } ); - - describe( 'applyStyles', () => { - test( 'applies styles to element', () => { - const element = document.createElement( 'div' ); - const styles = { - color: 'red', - backgroundColor: 'blue', - }; - applyStyles( element, styles ); - expect( element ).toHaveStyle( { color: 'red' } ); - expect( element ).toHaveStyle( { backgroundColor: 'blue' } ); - } ); - } ); -} ); diff --git a/projects/packages/search/src/inline-search/js/test/corrected-query.test.js b/projects/packages/search/src/inline-search/js/test/corrected-query.test.js deleted file mode 100644 index 22be5584fc2fa..0000000000000 --- a/projects/packages/search/src/inline-search/js/test/corrected-query.test.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { - setupJetpackSearchCorrectedQuery, - resetJetpackSearchCorrectedQuery, - createElementFromHtml, - applyStyles, -} from '../helpers'; - -describe( 'Corrected Query Notice', () => { - // Test constants - const TEST_HTML = '
Did you mean: example?
'; - const TEST_SELECTORS = [ '.search-title' ]; - const TEST_TITLE_HTML = '

Search Results

'; - const TEST_TITLE_WITH_CLASS_HTML = '

Search Results

'; - const NOTICE_STYLES = { - fontSize: '0.9em', - marginTop: '10px', - paddingTop: '0', - }; - - let originalJetpackSearchCorrectedQuery; - - /** - * Adds a corrected query notice after search titles when correction data is available. - */ - function correctedQueryFunction() { - // Get query data and validate - const queryData = window.JetpackSearchCorrectedQuery; - if ( ! queryData?.html || ! queryData?.selectors?.length ) { - return; - } - - // Find title element using selectors - const titleElement = document.querySelector( queryData.selectors.join( ', ' ) ); - if ( ! titleElement ) { - return; - } - - // Create and configure notice element - const noticeElement = createElementFromHtml( queryData.html ); - noticeElement.className = `${ titleElement.className } ${ noticeElement.className }`; - applyStyles( noticeElement, NOTICE_STYLES ); - - // Insert notice after title - titleElement.insertAdjacentElement( 'afterend', noticeElement ); - } - - beforeEach( () => { - // Store original state - originalJetpackSearchCorrectedQuery = window.JetpackSearchCorrectedQuery; - // Reset test environment - document.body.innerHTML = ''; - resetJetpackSearchCorrectedQuery(); - } ); - - afterEach( () => { - // Restore original state - if ( originalJetpackSearchCorrectedQuery ) { - setupJetpackSearchCorrectedQuery( originalJetpackSearchCorrectedQuery ); - } else { - resetJetpackSearchCorrectedQuery(); - } - } ); - - describe( 'when JetpackSearchCorrectedQuery is not properly configured', () => { - test( 'should not add notice when JetpackSearchCorrectedQuery is not defined', () => { - document.body.innerHTML = TEST_TITLE_HTML; - correctedQueryFunction(); - expect( document.querySelector( '.corrected-query' ) ).toBeNull(); - } ); - - test( 'should not add notice when JetpackSearchCorrectedQuery has no html', () => { - setupJetpackSearchCorrectedQuery( { selectors: TEST_SELECTORS } ); - document.body.innerHTML = TEST_TITLE_HTML; - correctedQueryFunction(); - expect( document.querySelector( '.corrected-query' ) ).toBeNull(); - } ); - - test( 'should not add notice when no matching selector is found', () => { - setupJetpackSearchCorrectedQuery( { - selectors: [ '.non-existent-selector' ], - html: TEST_HTML, - } ); - document.body.innerHTML = TEST_TITLE_HTML; - correctedQueryFunction(); - expect( document.querySelector( '.corrected-query' ) ).toBeNull(); - } ); - - test( 'should not add notice when selectors array is empty', () => { - setupJetpackSearchCorrectedQuery( { - selectors: [], - html: TEST_HTML, - } ); - document.body.innerHTML = TEST_TITLE_HTML; - correctedQueryFunction(); - expect( document.querySelector( '.corrected-query' ) ).toBeNull(); - } ); - } ); - - describe( 'when JetpackSearchCorrectedQuery is properly configured', () => { - test( 'should add notice with correct styling when all conditions are met', () => { - setupJetpackSearchCorrectedQuery( { - selectors: TEST_SELECTORS, - html: TEST_HTML, - } ); - document.body.innerHTML = TEST_TITLE_WITH_CLASS_HTML; - correctedQueryFunction(); - - const notice = document.querySelector( '.corrected-query' ); - expect( notice ).not.toBeNull(); - expect( notice ).toHaveClass( 'custom-class', 'corrected-query' ); - expect( notice ).toHaveStyle( NOTICE_STYLES ); - expect( notice ).toHaveTextContent( 'Did you mean: example?' ); - } ); - - test( 'should handle multiple selectors', () => { - setupJetpackSearchCorrectedQuery( { - selectors: [ '.non-existent', '.search-title' ], - html: TEST_HTML, - } ); - document.body.innerHTML = TEST_TITLE_HTML; - correctedQueryFunction(); - - const notice = document.querySelector( '.corrected-query' ); - expect( notice ).not.toBeNull(); - expect( notice ).toHaveClass( 'corrected-query' ); - expect( notice ).toHaveStyle( NOTICE_STYLES ); - } ); - - test( 'should preserve original notice class when adding title classes', () => { - setupJetpackSearchCorrectedQuery( { - selectors: TEST_SELECTORS, - html: TEST_HTML, - } ); - document.body.innerHTML = TEST_TITLE_WITH_CLASS_HTML; - correctedQueryFunction(); - - const notice = document.querySelector( '.corrected-query' ); - expect( notice.className ).toContain( 'search-title' ); - expect( notice.className ).toContain( 'custom-class' ); - expect( notice.className ).toContain( 'corrected-query' ); - } ); - } ); -} ); diff --git a/projects/packages/search/tests/php/Inline_Search_Test.php b/projects/packages/search/tests/php/Inline_Search_Test.php index 94db744add062..00a117b5aa1ef 100644 --- a/projects/packages/search/tests/php/Inline_Search_Test.php +++ b/projects/packages/search/tests/php/Inline_Search_Test.php @@ -179,97 +179,6 @@ public static function data_provider(): array { ); } - /** - * Test setup_corrected_query_hooks - * - * This test verifies that the setup_corrected_query_hooks method correctly adds the necessary hooks to the WP_Query object. - */ - public function test_setup_corrected_query_hooks() { - $search = Inline_Search::instance(); - - // Create a mock WP_Query that is a main search query - $query = $this->getMockBuilder( '\WP_Query' ) - ->disableOriginalConstructor() - ->getMock(); - $query->method( 'is_search' )->willReturn( true ); - $query->method( 'is_main_query' )->willReturn( true ); - - // Call the method we're testing - $search->setup_corrected_query_hooks( $query ); - - // Verify that both hooks were added - $this->assertEquals( - 10, - has_filter( 'get_search_query', array( $search, 'maybe_use_corrected_query' ) ), - 'get_search_query filter was not added correctly' - ); - } - - /** - * Test maybe_use_corrected_query method - * - * This test verifies that maybe_use_corrected_query returns the corrected query - * only when both corrected_query and results exist in search_result. - */ - public function test_maybe_use_corrected_query() { - $search = Inline_Search::instance(); - $original_query = 'original search'; - - // Use reflection to access protected property - $reflection = new \ReflectionClass( get_class( $search ) ); - $property = $reflection->getProperty( 'search_result' ); - $property->setAccessible( true ); - - // Test when search_result is empty - $property->setValue( $search, array() ); - $this->assertEquals( - $original_query, - $search->maybe_use_corrected_query( $original_query ), - 'Should return original query when search_result is empty' - ); - - // Test when corrected_query exists but no results - $property->setValue( - $search, - array( - 'corrected_query' => 'corrected search', - 'results' => array(), - ) - ); - $this->assertEquals( - $original_query, - $search->maybe_use_corrected_query( $original_query ), - 'Should return original query when results is empty' - ); - - // Test when results exist but no corrected_query - $property->setValue( - $search, - array( - 'results' => array( 'some result' ), - ) - ); - $this->assertEquals( - $original_query, - $search->maybe_use_corrected_query( $original_query ), - 'Should return original query when corrected_query is not set' - ); - - // Test when both corrected_query and results exist - $property->setValue( - $search, - array( - 'corrected_query' => 'corrected search', - 'results' => array( 'some result' ), - ) - ); - $this->assertEquals( - 'corrected search', - $search->maybe_use_corrected_query( $original_query ), - 'Should return corrected query when both corrected_query and results exist' - ); - } - /** * Test search request * @@ -285,204 +194,4 @@ public function test_search( array $wp_query_args, array $expected_api_args ) { parse_str( wp_parse_url( $this->last_search_url, PHP_URL_QUERY ), $actual_api_args ); $this->assertEquals( $expected_api_args, $actual_api_args ); } - - /** - * Test get_title_selectors method - */ - public function test_get_title_selectors() { - $search = Inline_Search::instance(); - - // Use reflection to access private method - $reflection = new \ReflectionClass( get_class( $search ) ); - $method = $reflection->getMethod( 'get_title_selectors' ); - $method->setAccessible( true ); - - // Test default selectors - $default_selectors = array( - '.wp-block-query-title', - '.page-title', - '.archive-title', - ); - $this->assertEquals( - $default_selectors, - $method->invoke( $search ), - 'Default selectors should match expected values' - ); - - // Test with filter - add_filter( - 'jetpack_search_title_selectors', - function () { - return array( '.custom-title', '.my-title' ); - } - ); - - $this->assertEquals( - array( '.custom-title', '.my-title' ), - $method->invoke( $search ), - 'Filtered selectors should match custom values' - ); - - // Clean up - remove_all_filters( 'jetpack_search_title_selectors' ); - } - - /** - * Test get_corrected_query_html method - */ - public function test_get_corrected_query_html() { - $search = Inline_Search::instance(); - - // Use reflection to access private method and protected property - $reflection = new \ReflectionClass( get_class( $search ) ); - $method = $reflection->getMethod( 'get_corrected_query_html' ); - $method->setAccessible( true ); - $property = $reflection->getProperty( 'search_result' ); - $property->setAccessible( true ); - - // Test with no search query - $this->assertSame( - '', - $method->invoke( $search ), - 'Should return empty string when no search query' - ); - - // Mock 's' parameter - $_GET['s'] = 'originl speling'; - - // Test with corrected query and results - $property->setValue( - $search, - array( - 'corrected_query' => 'original spelling', - 'results' => array( 'some result' ), - ) - ); - - $expected_html = sprintf( - '

' . "\n" . - ' %s%s' . "\n" . - '

', - esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ), - 'originl speling' - ); - - $this->assertEquals( - $expected_html, - $method->invoke( $search ), - 'Should return correction notice when query is corrected with results' - ); - - // Test with corrected query but no results - $property->setValue( - $search, - array( - 'corrected_query' => 'original spelling', - 'results' => array(), - ) - ); - - $expected_html = sprintf( - '

' . "\n" . - ' %s%s' . "\n" . - '

', - esc_html__( 'No results found for: ', 'jetpack-search-pkg' ), - 'originl speling' - ); - - $this->assertEquals( - $expected_html, - $method->invoke( $search ), - 'Should return no results notice when query is corrected without results' - ); - - // Clean up - unset( $_GET['s'] ); - } - - /** - * Test enqueue_corrected_query_script method - */ - public function test_enqueue_corrected_query_script() { - $search = Inline_Search::instance(); - - // Use reflection to access private method - $reflection = new \ReflectionClass( get_class( $search ) ); - $method = $reflection->getMethod( 'enqueue_corrected_query_script' ); - $method->setAccessible( true ); - - // Test script enqueuing - $html = '
Test HTML
'; - $selectors = array( '.test-selector' ); - $method->invoke( $search, $html, $selectors ); - - $this->assertTrue( - wp_script_is( 'jetpack-search-inline-corrected-query', 'registered' ), - 'Script should be registered' - ); - - $this->assertTrue( - wp_script_is( 'jetpack-search-inline-corrected-query', 'enqueued' ), - 'Script should be enqueued' - ); - - // Test localized data - $data = wp_scripts()->get_data( 'jetpack-search-inline-corrected-query', 'data' ); - $this->assertStringContainsString( - 'JetpackSearchCorrectedQuery', - $data, - 'Script data should be localized with correct object name' - ); - $this->assertStringContainsString( - '"html":"
Test HTML<\\/div>"', - $data, - 'Script data should contain the HTML' - ); - $this->assertStringContainsString( - json_encode( $selectors ), - $data, - 'Script data should contain the selectors' - ); - } - - /** - * Test output_corrected_query_script method - */ - public function test_output_corrected_query_script() { - $search = Inline_Search::instance(); - - // Use reflection to access protected property - $reflection = new \ReflectionClass( get_class( $search ) ); - $property = $reflection->getProperty( 'search_result' ); - $property->setAccessible( true ); - - // Test with no corrected query - $property->setValue( $search, array() ); - ob_start(); - $search->output_corrected_query_script(); - $output = ob_get_clean(); - $this->assertEmpty( $output, 'Should output nothing when no corrected query exists' ); - - // Test with corrected query - $_GET['s'] = 'test query'; - $property->setValue( - $search, - array( - 'corrected_query' => 'corrected query', - 'results' => array( 'some result' ), - ) - ); - - ob_start(); - $search->output_corrected_query_script(); - $output = ob_get_clean(); - - $this->assertTrue( - wp_script_is( 'jetpack-search-inline-corrected-query', 'enqueued' ), - 'Script should be enqueued when corrected query exists' - ); - - // Clean up - unset( $_GET['s'] ); - } } From a492ba0ec756978764a1b248bc79c251ff9668d0 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 18:17:53 +0100 Subject: [PATCH 14/42] Revise methods further and make proper use of Assets::class --- .../src/inline-search/class-inline-search.php | 97 +++++++++---------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index cf9d7025c7972..b45926305d852 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -7,6 +7,8 @@ namespace Automattic\Jetpack\Search; +use Automattic\Jetpack\Assets; + /** * Inline Search class */ @@ -436,7 +438,44 @@ public function setup_corrected_query_hooks( $query ) { } add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); - add_action( 'wp_footer', array( $this, 'output_corrected_query_script' ) ); + add_action( 'wp_footer', array( $this, 'register_corrected_query_script' ) ); + } + + /** + * Register and configure the JavaScript for displaying the corrected query notice. + * + * @since $$next-version$$ + */ + public function register_corrected_query_script() { + $corrected_query_html = $this->get_corrected_query_html(); + if ( empty( $corrected_query_html ) ) { + return; + } + + $handle = 'jetpack-search-inline-corrected-query'; + + Assets::register_script( + $handle, + 'js/corrected-query.js', + __FILE__, + array( + 'in_footer' => true, + 'textdomain' => 'jetpack-search-pkg', + 'enqueue' => true, + ) + ); + + wp_localize_script( + $handle, + 'JetpackSearchCorrectedQuery', + array( + 'html' => $corrected_query_html, + 'selectors' => $this->get_title_selectors(), + 'i18n' => array( + 'error' => esc_html__( 'Error displaying search correction', 'jetpack-search-pkg' ), + ), + ) + ); } /** @@ -454,26 +493,12 @@ public function maybe_use_corrected_query( $query ) { } /** - * Output JavaScript to inject the corrected query notice after the search title. - */ - public function output_corrected_query_script() { - $corrected_query_html = $this->get_corrected_query_html(); - if ( empty( $corrected_query_html ) ) { - return; - } - - $selectors = $this->get_title_selectors(); - $this->enqueue_corrected_query_script( $corrected_query_html, $selectors ); - } - - /** - * Get selectors for finding the search title element, with filter applied. + * Get selectors where corrected query notice will be displayed. * * @since $$next-version$$ - * @return array Array of CSS selectors to target the search title element. + * @return array CSS selectors for search title elements. */ private function get_title_selectors() { - // Default selectors for search title elements $default_selectors = array( '.wp-block-query-title', '.page-title', @@ -481,47 +506,14 @@ private function get_title_selectors() { ); /** - * Filter the CSS selectors used to find the search title element. + * Filter the selectors where corrected query notice appears. * * @since $$next-version$$ - * - * @param array $default_selectors Array of CSS selectors to target the search title element. + * @param array $default_selectors CSS selectors for search title elements. */ return apply_filters( 'jetpack_search_title_selectors', $default_selectors ); } - /** - * Enqueue the JavaScript for displaying the corrected query notice. - * - * @since $$next-version$$ - * @param string $html The HTML content to display. - * @param array $selectors CSS selectors for finding the element to place the notice after. - */ - private function enqueue_corrected_query_script( $html, $selectors ) { - // Load the script using a path relative to this file - $script_url = plugins_url( 'js/corrected-query.js', __FILE__ ); - - // Register and enqueue the script - wp_register_script( - 'jetpack-search-inline-corrected-query', - $script_url, - array(), - Package::VERSION, - true - ); - wp_enqueue_script( 'jetpack-search-inline-corrected-query' ); - - // Pass the HTML and selectors as JavaScript variables - wp_localize_script( - 'jetpack-search-inline-corrected-query', - 'JetpackSearchCorrectedQuery', - array( - 'html' => $html, - 'selectors' => $selectors, - ) - ); - } - /** * Generate the HTML for the corrected query notice. * @@ -530,7 +522,6 @@ private function enqueue_corrected_query_script( $html, $selectors ) { private function get_corrected_query_html() { $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - // Show message when there's a corrected query if ( ! empty( $this->search_result['corrected_query'] ) ) { $message = ! empty( $this->search_result['results'] ) ? esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ) From c0b4809c07184b2db12ba9882c1d7b26b4001ece Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 18:26:48 +0100 Subject: [PATCH 15/42] Modernise our JS --- .../src/inline-search/js/corrected-query.js | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/projects/packages/search/src/inline-search/js/corrected-query.js b/projects/packages/search/src/inline-search/js/corrected-query.js index 3f9632f9a1121..80d382096f377 100644 --- a/projects/packages/search/src/inline-search/js/corrected-query.js +++ b/projects/packages/search/src/inline-search/js/corrected-query.js @@ -1,32 +1,26 @@ /** * Script to display corrected query notice after search titles. */ -document.addEventListener( 'DOMContentLoaded', function () { - // Only proceed if we have corrected query data - if ( ! window.JetpackSearchCorrectedQuery || ! window.JetpackSearchCorrectedQuery.html ) { +document.addEventListener( 'DOMContentLoaded', () => { + if ( ! window.JetpackSearchCorrectedQuery?.html ) { return; } - // Get the selectors and join them for querySelector - const selectors = window.JetpackSearchCorrectedQuery.selectors; - const selectorString = selectors.join( ', ' ); + const { selectors, html } = window.JetpackSearchCorrectedQuery; + const titleElement = document.querySelector( selectors.join( ', ' ) ); - // Find the title element using the selectors - const titleElement = document.querySelector( selectorString ); if ( ! titleElement ) { return; } - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = window.JetpackSearchCorrectedQuery.html; - const notice = tempDiv.firstChild; + const notice = document.createElement( 'div' ); + notice.innerHTML = html; - // Apply styling and insert - const originalClass = notice.className; - notice.className = titleElement.className + ' ' + originalClass; - notice.style.fontSize = '0.9em'; - notice.style.marginTop = '10px'; - notice.style.paddingTop = '0'; + notice.className = `${ titleElement.className } ${ notice.className }`; + notice.style.cssText = 'font-size: 0.9em; margin-top: 10px; padding-top: 0;'; + + notice.setAttribute( 'role', 'status' ); + notice.setAttribute( 'aria-live', 'polite' ); titleElement.insertAdjacentElement( 'afterend', notice ); } ); From 3f2d9a8691547e5472a2d218d62cee9f33d3430c Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 1 Apr 2025 20:58:24 +0100 Subject: [PATCH 16/42] Simply corrected query html --- .../search/src/inline-search/class-inline-search.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index b45926305d852..5377ed699977b 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -522,15 +522,10 @@ private function get_title_selectors() { private function get_corrected_query_html() { $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! empty( $this->search_result['corrected_query'] ) ) { - $message = ! empty( $this->search_result['results'] ) - ? esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ) - : esc_html__( 'No results found for: ', 'jetpack-search-pkg' ); + if ( ! empty( $this->search_result['corrected_query'] ) && ! empty( $this->search_result['results'] ) ) { return sprintf( - '

- %s%s -

', - $message, + '

%s%s

', + esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ), esc_html( $original_query ) ); } From bde3ebbad6b2914f7e9ae070b52a4cfa2731ea7f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Wed, 2 Apr 2025 10:20:15 +0100 Subject: [PATCH 17/42] Refactor get_corrected_query_html() - Check $_GET['s'] with null coalescing instead of isset - Favour an early return instead of the ! checks - Change message language to 'No results for %s' - Remove tags to avoid forcing bold I also looked into an option like inserting tags in case a user wanted to style the $original_query word themselves, but I wasn't happy with the implications of this for translators, and the word can always be styled with a bit of fairly straightforward JS. --- .../src/inline-search/class-inline-search.php | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 5377ed699977b..fe53ae0a6052a 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -520,16 +520,21 @@ private function get_title_selectors() { * @return string The HTML for the corrected query notice or empty string if none. */ private function get_corrected_query_html() { - $original_query = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $original_query = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a search query. - if ( ! empty( $this->search_result['corrected_query'] ) && ! empty( $this->search_result['results'] ) ) { - return sprintf( - '

%s%s

', - esc_html__( 'Search term corrected from: ', 'jetpack-search-pkg' ), - esc_html( $original_query ) - ); + if ( empty( $this->search_result['corrected_query'] ) || empty( $this->search_result['results'] ) ) { + return ''; } - return ''; + $message = sprintf( + /* translators: %s: Original search term the user entered */ + esc_html__( 'No results for %s', 'jetpack-search-pkg' ), + esc_html( $original_query ) + ); + + return sprintf( + '

%s

', + $message + ); } } From 2e3052d9713ddd0b198ee5f35cd585d3bea364e0 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Wed, 16 Apr 2025 17:28:39 +0100 Subject: [PATCH 18/42] Add Search Result Highlighting - Adds search result highlighting using the same tag approach as Instant Search. - Splits up filter__pre_get_posts() with more discrete methods to avoid overloading filter__pre_get_posts() by giving it too much to do. - Adds new filters and new methods as appropriate. --- .../src/inline-search/class-inline-search.php | 288 +++++++++++++++++- 1 file changed, 277 insertions(+), 11 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index ded60c3fd4549..2815dd1e76aef 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -18,6 +18,41 @@ class Inline_Search extends Classic_Search { */ private static $instance; + /** + * Stores highlighted content from search results. + * + * @var array + */ + private $highlighted_content = array(); + + /** + * Stores the search term used in the query. + * + * @var string + */ + private $search_term; + + /** + * Stores the list of post IDs that are actual search results. + * + * @var array + */ + private $search_result_ids = array(); + + /** + * Set up the WordPress filters. + * + * @param string $blog_id The blog ID to set up for. + */ + public function setup( $blog_id ) { + parent::setup( $blog_id ); + + // Add filters to display highlighted content + add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); + add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); + add_filter( 'get_the_excerpt', array( $this, 'filter_highlighted_excerpt' ), 10, 2 ); + } + /** * Returns whether this class should be used instead of Classic_Search. */ @@ -89,30 +124,200 @@ public function filter__posts_pre_query( $posts, $query ) { return array(); } - $post_ids = array(); + // Process the search results to extract post IDs and highlighted content. + $this->process_search_results( $query ); + + // Create a WP_Query to fetch the actual posts. + $posts_query = $this->create_posts_query( $query ); + + // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually. + $query->found_posts = $this->found_posts; + $query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) ); + + return $posts_query->posts; + } + + /** + * Process search results to extract post IDs and highlighted content. + * + * @param \WP_Query $query The original WP_Query. + */ + private function process_search_results( $query ) { + $post_ids = array(); + $this->highlighted_content = array(); + $this->search_term = $query->get( 's' ); foreach ( $this->search_result['results'] as $result ) { - $post_ids[] = (int) ( $result['fields']['post_id'] ?? 0 ); + $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); + $post_ids[] = $post_id; + + $this->process_result_highlighting( $result, $post_id ); + } + + $this->search_result_ids = $post_ids; + } + + /** + * Process highlighting data for a single search result. + * + * @param array $result The search result data from the API. + * @param int $post_id The post ID for this result. + */ + private function process_result_highlighting( $result, $post_id ) { + if ( empty( $result['highlight'] ) ) { + return; + } + + // Check for data in various highlight field formats. + $title = $this->extract_highlight_field( $result, 'title' ); + $content = $this->extract_highlight_field( $result, 'content' ); + $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); + + $this->highlighted_content[ $post_id ] = array( + 'title' => $title, + 'content' => $content, + 'excerpt' => $excerpt, + ); + + // If we don't have highlighted content, create some by highlighting the search term. + if ( empty( $title ) && ! empty( $result['fields']['title'] ) && ! empty( $this->search_term ) ) { + $title_with_highlights = $this->apply_highlight_patterns( $result['fields']['title'], $this->search_term ); + $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; + } + + if ( empty( $content ) && ! empty( $result['fields']['content'] ) && ! empty( $this->search_term ) ) { + $content_with_highlights = $this->apply_highlight_patterns( $result['fields']['content'], $this->search_term ); + $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; + } + } + + /** + * Extract a highlight field from the search result, handling different field formats. + * + * @param array $result The search result data from the API. + * @param string $field The field name to extract. + * @return string The extracted highlighted field. + */ + private function extract_highlight_field( $result, $field ) { + if ( ! empty( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) { + return $result['highlight'][ $field ][0]; + } elseif ( ! empty( $result['highlight'][ $field . '.default' ] ) && is_array( $result['highlight'][ $field . '.default' ] ) ) { + return $result['highlight'][ $field . '.default' ][0]; } + return ''; + } - // Query all posts now. + /** + * Create a WP_Query to fetch the posts for search results. + * + * @param \WP_Query $original_query The original WP_Query. + * @return \WP_Query The new query with posts matching the search results. + */ + private function create_posts_query( $original_query ) { $args = array( - 'post__in' => $post_ids, + 'post__in' => $this->search_result_ids, 'orderby' => 'post__in', 'perm' => 'readable', 'post_type' => 'any', 'ignore_sticky_posts' => true, 'suppress_filters' => true, - 'posts_per_page' => $query->get( 'posts_per_page' ), + 'posts_per_page' => $original_query->get( 'posts_per_page' ), ); - $posts_query = new \WP_Query( $args ); + return new \WP_Query( $args ); + } - // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually. - $query->found_posts = $this->found_posts; - $query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) ); + /** + * Check if the current post is a search result from our API + * + * @param int $post_id The post ID to check. + * @return bool Whether the post is a search result. + */ + private function is_search_result( $post_id ) { + return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true ); + } - return $posts_query->posts; + /** + * Filter the post title to show highlighted version. + * + * @param string $title The post title. + * @param int $post_id The post ID. + * @return string The filtered title. + */ + public function filter_highlighted_title( $title, $post_id ) { + // Only process if this is one of our search results + if ( ! $this->is_search_result( $post_id ) ) { + return $title; + } + + // Check if we have a highlighted title from the API + if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) { + // Return the highlighted content + return $this->highlighted_content[ $post_id ]['title']; + } + + // If no pre-highlighted title, manually highlight the search term + if ( ! empty( $this->search_term ) ) { + return $this->apply_highlight_patterns( $title, $this->search_term ); + } + + return $title; + } + + /** + * Filter the post content to show highlighted version. + * + * @param string $content The post content. + * @return string The filtered content. + */ + public function filter_highlighted_content( $content ) { + // Get current post ID + $post_id = get_the_ID(); + + // Only process if this is one of our search results + if ( ! $this->is_search_result( $post_id ) ) { + return $content; + } + + if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) { + return $this->highlighted_content[ $post_id ]['content']; + } + + // If we don't have highlighted content, manually highlight the search term + if ( ! empty( $this->search_term ) ) { + return $this->apply_highlight_patterns( $content, $this->search_term ); + } + + return $content; + } + + /** + * Filter the post excerpt to show highlighted version. + * + * @param string $excerpt The post excerpt. + * @param int $post_id The post ID. + * @return string The filtered excerpt. + */ + public function filter_highlighted_excerpt( $excerpt, $post_id = 0 ) { + if ( 0 === $post_id ) { + $post_id = get_the_ID(); + } + + // Only process if this is one of our search results + if ( ! $this->is_search_result( $post_id ) ) { + return $excerpt; + } + + if ( ! empty( $this->highlighted_content[ $post_id ]['excerpt'] ) ) { + return $this->highlighted_content[ $post_id ]['excerpt']; + } + + // If we don't have highlighted excerpt, manually highlight the search term + if ( ! empty( $this->search_term ) ) { + return $this->apply_highlight_patterns( $excerpt, $this->search_term ); + } + + return $excerpt; } /** @@ -301,11 +506,22 @@ public function convert_wp_query_to_api_args( array $args ) { } } + $highlight_options = array( + 'pre_tags' => array( '' ), + 'post_tags' => array( '' ), + 'fields' => array( + 'title' => array( 'number_of_fragments' => 0 ), + 'content' => array( 'number_of_fragments' => 0 ), + 'excerpt' => array( 'number_of_fragments' => 0 ), + ), + ); + return array( 'blog_id' => $this->jetpack_blog_id, 'size' => absint( $args['posts_per_page'] ), 'from' => min( $from, Helper::get_max_offset() ), - 'fields' => array( 'blog_id', 'post_id' ), + 'fields' => array( 'blog_id', 'post_id', 'title', 'content', 'excerpt' ), + 'highlight' => $highlight_options, 'query' => $args['query'] ?? '', 'sort' => $sort, 'aggregations' => empty( $aggregations ) ? null : $aggregations, @@ -421,4 +637,54 @@ public function get_search_result( ) { return $this->search_result; } + + /** + * Prepare search pattern for highlighting + * + * @param string $search_term The search term to highlight. + * @return array Array of patterns to use for highlighting. + */ + private function prepare_highlight_patterns( $search_term ) { + // Split search term into words + $terms = preg_split( '/\s+/', trim( $search_term ) ); + $patterns = array(); + + // Add patterns for each individual word + foreach ( $terms as $term ) { + if ( strlen( $term ) < 3 ) { + // Use exact matching for very short terms + $patterns[] = '/\b(' . preg_quote( $term, '/' ) . ')\b/i'; + } else { + // Word boundary for normal words + $patterns[] = '/\b(' . preg_quote( $term, '/' ) . ')\b/i'; + // Also try without word boundary + $patterns[] = '/(' . preg_quote( $term, '/' ) . ')/i'; + } + } + + // Also try the full phrase + if ( count( $terms ) > 1 ) { + $patterns[] = '/(' . preg_quote( $search_term, '/' ) . ')/i'; + } + + return $patterns; + } + + /** + * Apply highlight patterns to content + * + * @param string $content The content to highlight. + * @param string $search_term The search term to highlight. + * @return string The highlighted content. + */ + private function apply_highlight_patterns( $content, $search_term ) { + $patterns = $this->prepare_highlight_patterns( $search_term ); + $highlighted = $content; + + foreach ( $patterns as $pattern ) { + $highlighted = preg_replace( $pattern, '$1', $highlighted ); + } + + return $highlighted; + } } From ab9ec4c4d3171620b4226b3568dd7152b22a8da1 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Wed, 16 Apr 2025 17:32:40 +0100 Subject: [PATCH 19/42] changelog --- .../search/changelog/add-inline-search-term-highlighting | 4 ++++ .../jetpack/changelog/add-inline-search-term-highlighting | 4 ++++ .../search/changelog/add-inline-search-term-highlighting | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 projects/packages/search/changelog/add-inline-search-term-highlighting create mode 100644 projects/plugins/jetpack/changelog/add-inline-search-term-highlighting create mode 100644 projects/plugins/search/changelog/add-inline-search-term-highlighting diff --git a/projects/packages/search/changelog/add-inline-search-term-highlighting b/projects/packages/search/changelog/add-inline-search-term-highlighting new file mode 100644 index 0000000000000..e8bd7beb03ccb --- /dev/null +++ b/projects/packages/search/changelog/add-inline-search-term-highlighting @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add highlighting of search term in returned search results. diff --git a/projects/plugins/jetpack/changelog/add-inline-search-term-highlighting b/projects/plugins/jetpack/changelog/add-inline-search-term-highlighting new file mode 100644 index 0000000000000..ac65f026dc6bd --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-inline-search-term-highlighting @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add highlighting of search term in returned search results. diff --git a/projects/plugins/search/changelog/add-inline-search-term-highlighting b/projects/plugins/search/changelog/add-inline-search-term-highlighting new file mode 100644 index 0000000000000..e8bd7beb03ccb --- /dev/null +++ b/projects/plugins/search/changelog/add-inline-search-term-highlighting @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add highlighting of search term in returned search results. From fc6e5024785ee48b33f7d80f736e965feb72b64f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 17 Apr 2025 11:13:22 +0100 Subject: [PATCH 20/42] Reorganise class to better group methods --- .../src/inline-search/class-inline-search.php | 230 +++++++++--------- 1 file changed, 116 insertions(+), 114 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 2815dd1e76aef..fe86f61bd9904 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -39,20 +39,6 @@ class Inline_Search extends Classic_Search { */ private $search_result_ids = array(); - /** - * Set up the WordPress filters. - * - * @param string $blog_id The blog ID to set up for. - */ - public function setup( $blog_id ) { - parent::setup( $blog_id ); - - // Add filters to display highlighted content - add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); - add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); - add_filter( 'get_the_excerpt', array( $this, 'filter_highlighted_excerpt' ), 10, 2 ); - } - /** * Returns whether this class should be used instead of Classic_Search. */ @@ -94,6 +80,20 @@ public static function get_instance_maybe_fallback_to_classic( $blog_id = null ) } } + /** + * Set up the WordPress filters. + * + * @param string $blog_id The blog ID to set up for. + */ + public function setup( $blog_id ) { + parent::setup( $blog_id ); + + // Add filters to display highlighted content + add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); + add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); + add_filter( 'get_the_excerpt', array( $this, 'filter_highlighted_excerpt' ), 10, 2 ); + } + /** * Bypass WP search and offload it to 1.3 search API instead. * @@ -137,106 +137,6 @@ public function filter__posts_pre_query( $posts, $query ) { return $posts_query->posts; } - /** - * Process search results to extract post IDs and highlighted content. - * - * @param \WP_Query $query The original WP_Query. - */ - private function process_search_results( $query ) { - $post_ids = array(); - $this->highlighted_content = array(); - $this->search_term = $query->get( 's' ); - - foreach ( $this->search_result['results'] as $result ) { - $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); - $post_ids[] = $post_id; - - $this->process_result_highlighting( $result, $post_id ); - } - - $this->search_result_ids = $post_ids; - } - - /** - * Process highlighting data for a single search result. - * - * @param array $result The search result data from the API. - * @param int $post_id The post ID for this result. - */ - private function process_result_highlighting( $result, $post_id ) { - if ( empty( $result['highlight'] ) ) { - return; - } - - // Check for data in various highlight field formats. - $title = $this->extract_highlight_field( $result, 'title' ); - $content = $this->extract_highlight_field( $result, 'content' ); - $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); - - $this->highlighted_content[ $post_id ] = array( - 'title' => $title, - 'content' => $content, - 'excerpt' => $excerpt, - ); - - // If we don't have highlighted content, create some by highlighting the search term. - if ( empty( $title ) && ! empty( $result['fields']['title'] ) && ! empty( $this->search_term ) ) { - $title_with_highlights = $this->apply_highlight_patterns( $result['fields']['title'], $this->search_term ); - $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; - } - - if ( empty( $content ) && ! empty( $result['fields']['content'] ) && ! empty( $this->search_term ) ) { - $content_with_highlights = $this->apply_highlight_patterns( $result['fields']['content'], $this->search_term ); - $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; - } - } - - /** - * Extract a highlight field from the search result, handling different field formats. - * - * @param array $result The search result data from the API. - * @param string $field The field name to extract. - * @return string The extracted highlighted field. - */ - private function extract_highlight_field( $result, $field ) { - if ( ! empty( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) { - return $result['highlight'][ $field ][0]; - } elseif ( ! empty( $result['highlight'][ $field . '.default' ] ) && is_array( $result['highlight'][ $field . '.default' ] ) ) { - return $result['highlight'][ $field . '.default' ][0]; - } - return ''; - } - - /** - * Create a WP_Query to fetch the posts for search results. - * - * @param \WP_Query $original_query The original WP_Query. - * @return \WP_Query The new query with posts matching the search results. - */ - private function create_posts_query( $original_query ) { - $args = array( - 'post__in' => $this->search_result_ids, - 'orderby' => 'post__in', - 'perm' => 'readable', - 'post_type' => 'any', - 'ignore_sticky_posts' => true, - 'suppress_filters' => true, - 'posts_per_page' => $original_query->get( 'posts_per_page' ), - ); - - return new \WP_Query( $args ); - } - - /** - * Check if the current post is a search result from our API - * - * @param int $post_id The post ID to check. - * @return bool Whether the post is a search result. - */ - private function is_search_result( $post_id ) { - return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true ); - } - /** * Filter the post title to show highlighted version. * @@ -638,6 +538,98 @@ public function get_search_result( return $this->search_result; } + // PRIVATE HELPER METHODS + + /** + * Process search results to extract post IDs and highlighted content. + * + * @param \WP_Query $query The original WP_Query. + */ + private function process_search_results( $query ) { + $post_ids = array(); + $this->highlighted_content = array(); + $this->search_term = $query->get( 's' ); + + foreach ( $this->search_result['results'] as $result ) { + $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); + $post_ids[] = $post_id; + + $this->process_result_highlighting( $result, $post_id ); + } + + $this->search_result_ids = $post_ids; + } + + /** + * Process highlighting data for a single search result. + * + * @param array $result The search result data from the API. + * @param int $post_id The post ID for this result. + */ + private function process_result_highlighting( $result, $post_id ) { + if ( empty( $result['highlight'] ) ) { + return; + } + + // Check for data in various highlight field formats. + $title = $this->extract_highlight_field( $result, 'title' ); + $content = $this->extract_highlight_field( $result, 'content' ); + $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); + + $this->highlighted_content[ $post_id ] = array( + 'title' => $title, + 'content' => $content, + 'excerpt' => $excerpt, + ); + + // If we don't have highlighted content, create some by highlighting the search term. + if ( empty( $title ) && ! empty( $result['fields']['title'] ) && ! empty( $this->search_term ) ) { + $title_with_highlights = $this->apply_highlight_patterns( $result['fields']['title'], $this->search_term ); + $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; + } + + if ( empty( $content ) && ! empty( $result['fields']['content'] ) && ! empty( $this->search_term ) ) { + $content_with_highlights = $this->apply_highlight_patterns( $result['fields']['content'], $this->search_term ); + $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; + } + } + + /** + * Extract a highlight field from the search result, handling different field formats. + * + * @param array $result The search result data from the API. + * @param string $field The field name to extract. + * @return string The extracted highlighted field. + */ + private function extract_highlight_field( $result, $field ) { + if ( ! empty( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) { + return $result['highlight'][ $field ][0]; + } elseif ( ! empty( $result['highlight'][ $field . '.default' ] ) && is_array( $result['highlight'][ $field . '.default' ] ) ) { + return $result['highlight'][ $field . '.default' ][0]; + } + return ''; + } + + /** + * Create a WP_Query to fetch the posts for search results. + * + * @param \WP_Query $original_query The original WP_Query. + * @return \WP_Query The new query with posts matching the search results. + */ + private function create_posts_query( $original_query ) { + $args = array( + 'post__in' => $this->search_result_ids, + 'orderby' => 'post__in', + 'perm' => 'readable', + 'post_type' => 'any', + 'ignore_sticky_posts' => true, + 'suppress_filters' => true, + 'posts_per_page' => $original_query->get( 'posts_per_page' ), + ); + + return new \WP_Query( $args ); + } + /** * Prepare search pattern for highlighting * @@ -687,4 +679,14 @@ private function apply_highlight_patterns( $content, $search_term ) { return $highlighted; } + + /** + * Check if the current post is a search result from our API + * + * @param int $post_id The post ID to check. + * @return bool Whether the post is a search result. + */ + private function is_search_result( $post_id ) { + return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true ); + } } From 6e37b0925a8823ec061c8041b405704b16d77319 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 17 Apr 2025 11:21:31 +0100 Subject: [PATCH 21/42] First pass at highlighting search corrections --- .../src/inline-search/class-inline-search.php | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index fe86f61bd9904..aab3c2d924c57 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -32,6 +32,13 @@ class Inline_Search extends Classic_Search { */ private $search_term; + /** + * Stores the corrected search term if provided by the API. + * + * @var string + */ + private $corrected_search_term; + /** * Stores the list of post IDs that are actual search results. * @@ -550,6 +557,13 @@ private function process_search_results( $query ) { $this->highlighted_content = array(); $this->search_term = $query->get( 's' ); + // Store corrected query if available + if ( ! empty( $this->search_result['corrected_query'] ) ) { + $this->corrected_search_term = $this->search_result['corrected_query']; + } else { + $this->corrected_search_term = ''; + } + foreach ( $this->search_result['results'] as $result ) { $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); $post_ids[] = $post_id; @@ -583,14 +597,48 @@ private function process_result_highlighting( $result, $post_id ) { ); // If we don't have highlighted content, create some by highlighting the search term. - if ( empty( $title ) && ! empty( $result['fields']['title'] ) && ! empty( $this->search_term ) ) { - $title_with_highlights = $this->apply_highlight_patterns( $result['fields']['title'], $this->search_term ); - $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; + if ( empty( $title ) && ! empty( $result['fields']['title'] ) ) { + // First use the original search term + if ( ! empty( $this->search_term ) ) { + $title_with_highlights = $this->apply_highlight_patterns( + $result['fields']['title'], + $this->search_term, + false // Don't use corrected term yet + ); + + // Then apply the corrected term if available + if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { + $title_with_highlights = $this->apply_highlight_patterns( + $title_with_highlights, + $this->corrected_search_term, + false // Don't recursively apply correction + ); + } + + $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; + } } - if ( empty( $content ) && ! empty( $result['fields']['content'] ) && ! empty( $this->search_term ) ) { - $content_with_highlights = $this->apply_highlight_patterns( $result['fields']['content'], $this->search_term ); - $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; + if ( empty( $content ) && ! empty( $result['fields']['content'] ) ) { + // First use the original search term + if ( ! empty( $this->search_term ) ) { + $content_with_highlights = $this->apply_highlight_patterns( + $result['fields']['content'], + $this->search_term, + false // Don't use corrected term yet + ); + + // Then apply the corrected term if available + if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { + $content_with_highlights = $this->apply_highlight_patterns( + $content_with_highlights, + $this->corrected_search_term, + false // Don't recursively apply correction + ); + } + + $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; + } } } @@ -667,16 +715,26 @@ private function prepare_highlight_patterns( $search_term ) { * * @param string $content The content to highlight. * @param string $search_term The search term to highlight. + * @param bool $use_corrected Whether to also use the corrected search term. * @return string The highlighted content. */ - private function apply_highlight_patterns( $content, $search_term ) { + private function apply_highlight_patterns( $content, $search_term, $use_corrected = true ) { $patterns = $this->prepare_highlight_patterns( $search_term ); $highlighted = $content; + // Apply original search term patterns foreach ( $patterns as $pattern ) { $highlighted = preg_replace( $pattern, '$1', $highlighted ); } + // Also apply corrected search term patterns if available and requested + if ( $use_corrected && ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $search_term ) { + $corrected_patterns = $this->prepare_highlight_patterns( $this->corrected_search_term ); + foreach ( $corrected_patterns as $pattern ) { + $highlighted = preg_replace( $pattern, '$1', $highlighted ); + } + } + return $highlighted; } From 36bf325a6162005beee745f223496a961eedb35e Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 17 Apr 2025 13:04:15 +0100 Subject: [PATCH 22/42] Move to a simpler highlighting solution as the previous was overengineered --- .../src/inline-search/class-inline-search.php | 63 ++++++------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index aab3c2d924c57..a85af3a56cbba 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -163,7 +163,7 @@ public function filter_highlighted_title( $title, $post_id ) { return $this->highlighted_content[ $post_id ]['title']; } - // If no pre-highlighted title, manually highlight the search term + // If we don't have highlighted title, manually highlight the search term if ( ! empty( $this->search_term ) ) { return $this->apply_highlight_patterns( $title, $this->search_term ); } @@ -679,39 +679,7 @@ private function create_posts_query( $original_query ) { } /** - * Prepare search pattern for highlighting - * - * @param string $search_term The search term to highlight. - * @return array Array of patterns to use for highlighting. - */ - private function prepare_highlight_patterns( $search_term ) { - // Split search term into words - $terms = preg_split( '/\s+/', trim( $search_term ) ); - $patterns = array(); - - // Add patterns for each individual word - foreach ( $terms as $term ) { - if ( strlen( $term ) < 3 ) { - // Use exact matching for very short terms - $patterns[] = '/\b(' . preg_quote( $term, '/' ) . ')\b/i'; - } else { - // Word boundary for normal words - $patterns[] = '/\b(' . preg_quote( $term, '/' ) . ')\b/i'; - // Also try without word boundary - $patterns[] = '/(' . preg_quote( $term, '/' ) . ')/i'; - } - } - - // Also try the full phrase - if ( count( $terms ) > 1 ) { - $patterns[] = '/(' . preg_quote( $search_term, '/' ) . ')/i'; - } - - return $patterns; - } - - /** - * Apply highlight patterns to content + * Apply highlight markup to content * * @param string $content The content to highlight. * @param string $search_term The search term to highlight. @@ -719,25 +687,34 @@ private function prepare_highlight_patterns( $search_term ) { * @return string The highlighted content. */ private function apply_highlight_patterns( $content, $search_term, $use_corrected = true ) { - $patterns = $this->prepare_highlight_patterns( $search_term ); $highlighted = $content; - // Apply original search term patterns - foreach ( $patterns as $pattern ) { - $highlighted = preg_replace( $pattern, '$1', $highlighted ); + // Highlight the original search term + if ( ! empty( $search_term ) ) { + $highlighted = $this->add_mark_tags( $highlighted, $search_term ); } - // Also apply corrected search term patterns if available and requested + // Also highlight corrected search term if available and requested if ( $use_corrected && ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $search_term ) { - $corrected_patterns = $this->prepare_highlight_patterns( $this->corrected_search_term ); - foreach ( $corrected_patterns as $pattern ) { - $highlighted = preg_replace( $pattern, '$1', $highlighted ); - } + $highlighted = $this->add_mark_tags( $highlighted, $this->corrected_search_term ); } return $highlighted; } + /** + * Add mark tags around search terms in content + * + * @param string $content The content to search within. + * @param string $term The term to highlight. + * @return string The content with highlighted terms. + */ + private function add_mark_tags( $content, $term ) { + // Case-insensitive matching but preserve original case + $pattern = '/(' . preg_quote( $term, '/' ) . ')/i'; + return preg_replace( $pattern, '$1', $content ); + } + /** * Check if the current post is a search result from our API * From ef34d722dbe1b8a0ac2dfff66415d6a61268dde5 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 17 Apr 2025 17:15:29 +0100 Subject: [PATCH 23/42] Refactor highlighting handling to better leverage the 1.3api --- .../src/inline-search/class-inline-search.php | 85 ++++++++++++++----- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index a85af3a56cbba..04117d3eb96e5 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -99,6 +99,7 @@ public function setup( $blog_id ) { add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); add_filter( 'get_the_excerpt', array( $this, 'filter_highlighted_excerpt' ), 10, 2 ); + add_filter( 'comment_text', array( $this, 'filter_highlighted_comment' ), 10, 2 ); } /** @@ -159,11 +160,11 @@ public function filter_highlighted_title( $title, $post_id ) { // Check if we have a highlighted title from the API if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) { - // Return the highlighted content return $this->highlighted_content[ $post_id ]['title']; } - // If we don't have highlighted title, manually highlight the search term + // Fallback: Even though the API should provide highlighted titles, + // in some cases it doesn't, so we need to apply our own highlighting if ( ! empty( $this->search_term ) ) { return $this->apply_highlight_patterns( $title, $this->search_term ); } @@ -219,12 +220,34 @@ public function filter_highlighted_excerpt( $excerpt, $post_id = 0 ) { return $this->highlighted_content[ $post_id ]['excerpt']; } - // If we don't have highlighted excerpt, manually highlight the search term - if ( ! empty( $this->search_term ) ) { - return $this->apply_highlight_patterns( $excerpt, $this->search_term ); + return $excerpt; + } + + /** + * Filter comment text to show highlighted version. + * + * @param string $comment_text The comment text. + * @return string The filtered comment text. + */ + public function filter_highlighted_comment( $comment_text ) { + // Only process if this is one of our search results and we're in a search context + if ( ! is_search() || ! in_the_loop() ) { + return $comment_text; } - return $excerpt; + $post_id = get_the_ID(); + + // Check if this post is a search result and we have highlighted comments for it + if ( ! $this->is_search_result( $post_id ) || empty( $this->highlighted_content[ $post_id ]['comments'] ) ) { + return $comment_text; + } + + // Simple check to see if this comment contains our search term + if ( ! empty( $this->search_term ) && stripos( $comment_text, $this->search_term ) !== false ) { + return $this->apply_highlight_patterns( $comment_text, $this->search_term ); + } + + return $comment_text; } /** @@ -414,20 +437,30 @@ public function convert_wp_query_to_api_args( array $args ) { } $highlight_options = array( - 'pre_tags' => array( '' ), - 'post_tags' => array( '' ), + 'pre_tags' => array( '__MARK__' ), + 'post_tags' => array( '__/MARK__' ), 'fields' => array( - 'title' => array( 'number_of_fragments' => 0 ), - 'content' => array( 'number_of_fragments' => 0 ), - 'excerpt' => array( 'number_of_fragments' => 0 ), + 'title', + 'content', + 'excerpt', + 'comments', ), ); + $fields = array( + 'blog_id', + 'post_id', + 'title', + 'content', + 'excerpt', + 'comments', + ); + return array( 'blog_id' => $this->jetpack_blog_id, 'size' => absint( $args['posts_per_page'] ), 'from' => min( $from, Helper::get_max_offset() ), - 'fields' => array( 'blog_id', 'post_id', 'title', 'content', 'excerpt' ), + 'fields' => $fields, 'highlight' => $highlight_options, 'query' => $args['query'] ?? '', 'sort' => $sort, @@ -586,14 +619,16 @@ private function process_result_highlighting( $result, $post_id ) { } // Check for data in various highlight field formats. - $title = $this->extract_highlight_field( $result, 'title' ); - $content = $this->extract_highlight_field( $result, 'content' ); - $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); + $title = $this->extract_highlight_field( $result, 'title' ); + $content = $this->extract_highlight_field( $result, 'content' ); + $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); + $comments = $this->extract_highlight_field( $result, 'comments' ); $this->highlighted_content[ $post_id ] = array( - 'title' => $title, - 'content' => $content, - 'excerpt' => $excerpt, + 'title' => $title, + 'content' => $content, + 'excerpt' => $excerpt, + 'comments' => $comments, ); // If we don't have highlighted content, create some by highlighting the search term. @@ -650,11 +685,16 @@ private function process_result_highlighting( $result, $post_id ) { * @return string The extracted highlighted field. */ private function extract_highlight_field( $result, $field ) { - if ( ! empty( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) { - return $result['highlight'][ $field ][0]; - } elseif ( ! empty( $result['highlight'][ $field . '.default' ] ) && is_array( $result['highlight'][ $field . '.default' ] ) ) { - return $result['highlight'][ $field . '.default' ][0]; + // Try all possible field variants in order of likelihood + foreach ( $result['highlight'] as $key => $value ) { + // Check if this key is for our requested field (exact match or with suffix) + if ( $key === $field || strpos( $key, $field . '.' ) === 0 ) { + if ( is_array( $value ) && ! empty( $value ) ) { + return $value[0]; + } + } } + return ''; } @@ -710,7 +750,6 @@ private function apply_highlight_patterns( $content, $search_term, $use_correcte * @return string The content with highlighted terms. */ private function add_mark_tags( $content, $term ) { - // Case-insensitive matching but preserve original case $pattern = '/(' . preg_quote( $term, '/' ) . ')/i'; return preg_replace( $pattern, '$1', $content ); } From 7f45b3877ea87a5e5f1a80f70fa835355887ffbe Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 21 Apr 2025 10:20:28 +0100 Subject: [PATCH 24/42] Remove excerpt highlighting as unnecessary at this time --- .../src/inline-search/class-inline-search.php | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 04117d3eb96e5..1f968f105bbe4 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -98,7 +98,6 @@ public function setup( $blog_id ) { // Add filters to display highlighted content add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); - add_filter( 'get_the_excerpt', array( $this, 'filter_highlighted_excerpt' ), 10, 2 ); add_filter( 'comment_text', array( $this, 'filter_highlighted_comment' ), 10, 2 ); } @@ -199,30 +198,6 @@ public function filter_highlighted_content( $content ) { return $content; } - /** - * Filter the post excerpt to show highlighted version. - * - * @param string $excerpt The post excerpt. - * @param int $post_id The post ID. - * @return string The filtered excerpt. - */ - public function filter_highlighted_excerpt( $excerpt, $post_id = 0 ) { - if ( 0 === $post_id ) { - $post_id = get_the_ID(); - } - - // Only process if this is one of our search results - if ( ! $this->is_search_result( $post_id ) ) { - return $excerpt; - } - - if ( ! empty( $this->highlighted_content[ $post_id ]['excerpt'] ) ) { - return $this->highlighted_content[ $post_id ]['excerpt']; - } - - return $excerpt; - } - /** * Filter comment text to show highlighted version. * @@ -442,7 +417,6 @@ public function convert_wp_query_to_api_args( array $args ) { 'fields' => array( 'title', 'content', - 'excerpt', 'comments', ), ); @@ -452,7 +426,6 @@ public function convert_wp_query_to_api_args( array $args ) { 'post_id', 'title', 'content', - 'excerpt', 'comments', ); @@ -621,13 +594,11 @@ private function process_result_highlighting( $result, $post_id ) { // Check for data in various highlight field formats. $title = $this->extract_highlight_field( $result, 'title' ); $content = $this->extract_highlight_field( $result, 'content' ); - $excerpt = $this->extract_highlight_field( $result, 'excerpt' ); $comments = $this->extract_highlight_field( $result, 'comments' ); $this->highlighted_content[ $post_id ] = array( 'title' => $title, 'content' => $content, - 'excerpt' => $excerpt, 'comments' => $comments, ); From a38a9f84d04da11df61ed7c9e1f40a83a5cdfb8b Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 21 Apr 2025 15:52:14 +0100 Subject: [PATCH 25/42] Let us move highlighting to a class of its own --- .../src/inline-search/class-inline-search.php | 264 +------------- .../class-search-highlighter.php | 327 ++++++++++++++++++ 2 files changed, 343 insertions(+), 248 deletions(-) create mode 100644 projects/packages/search/src/inline-search/class-search-highlighter.php diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 1f968f105bbe4..8b7c767ef4793 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -7,6 +7,8 @@ namespace Automattic\Jetpack\Search; +require_once __DIR__ . '/class-search-highlighter.php'; + /** * Inline Search class */ @@ -19,25 +21,12 @@ class Inline_Search extends Classic_Search { private static $instance; /** - * Stores highlighted content from search results. + * The Search Highlighter instance. * - * @var array + * @var Search_Highlighter + * @since $$next-version$$ */ - private $highlighted_content = array(); - - /** - * Stores the search term used in the query. - * - * @var string - */ - private $search_term; - - /** - * Stores the corrected search term if provided by the API. - * - * @var string - */ - private $corrected_search_term; + private $highlighter; /** * Stores the list of post IDs that are actual search results. @@ -88,17 +77,14 @@ public static function get_instance_maybe_fallback_to_classic( $blog_id = null ) } /** - * Set up the WordPress filters. + * Set up the highlighter. * * @param string $blog_id The blog ID to set up for. */ public function setup( $blog_id ) { parent::setup( $blog_id ); - - // Add filters to display highlighted content - add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); - add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); - add_filter( 'comment_text', array( $this, 'filter_highlighted_comment' ), 10, 2 ); + // The highlighter will be initialized with data during search processing + $this->highlighter = null; } /** @@ -144,87 +130,6 @@ public function filter__posts_pre_query( $posts, $query ) { return $posts_query->posts; } - /** - * Filter the post title to show highlighted version. - * - * @param string $title The post title. - * @param int $post_id The post ID. - * @return string The filtered title. - */ - public function filter_highlighted_title( $title, $post_id ) { - // Only process if this is one of our search results - if ( ! $this->is_search_result( $post_id ) ) { - return $title; - } - - // Check if we have a highlighted title from the API - if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) { - return $this->highlighted_content[ $post_id ]['title']; - } - - // Fallback: Even though the API should provide highlighted titles, - // in some cases it doesn't, so we need to apply our own highlighting - if ( ! empty( $this->search_term ) ) { - return $this->apply_highlight_patterns( $title, $this->search_term ); - } - - return $title; - } - - /** - * Filter the post content to show highlighted version. - * - * @param string $content The post content. - * @return string The filtered content. - */ - public function filter_highlighted_content( $content ) { - // Get current post ID - $post_id = get_the_ID(); - - // Only process if this is one of our search results - if ( ! $this->is_search_result( $post_id ) ) { - return $content; - } - - if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) { - return $this->highlighted_content[ $post_id ]['content']; - } - - // If we don't have highlighted content, manually highlight the search term - if ( ! empty( $this->search_term ) ) { - return $this->apply_highlight_patterns( $content, $this->search_term ); - } - - return $content; - } - - /** - * Filter comment text to show highlighted version. - * - * @param string $comment_text The comment text. - * @return string The filtered comment text. - */ - public function filter_highlighted_comment( $comment_text ) { - // Only process if this is one of our search results and we're in a search context - if ( ! is_search() || ! in_the_loop() ) { - return $comment_text; - } - - $post_id = get_the_ID(); - - // Check if this post is a search result and we have highlighted comments for it - if ( ! $this->is_search_result( $post_id ) || empty( $this->highlighted_content[ $post_id ]['comments'] ) ) { - return $comment_text; - } - - // Simple check to see if this comment contains our search term - if ( ! empty( $this->search_term ) && stripos( $comment_text, $this->search_term ) !== false ) { - return $this->apply_highlight_patterns( $comment_text, $this->search_term ); - } - - return $comment_text; - } - /** * Execute 1.3 search API request. * @@ -551,122 +456,31 @@ public function get_search_result( return $this->search_result; } - // PRIVATE HELPER METHODS - /** * Process search results to extract post IDs and highlighted content. * * @param \WP_Query $query The original WP_Query. */ private function process_search_results( $query ) { - $post_ids = array(); - $this->highlighted_content = array(); - $this->search_term = $query->get( 's' ); + $post_ids = array(); + $search_term = $query->get( 's' ); + $corrected_search_term = ''; // Store corrected query if available if ( ! empty( $this->search_result['corrected_query'] ) ) { - $this->corrected_search_term = $this->search_result['corrected_query']; - } else { - $this->corrected_search_term = ''; + $corrected_search_term = $this->search_result['corrected_query']; } foreach ( $this->search_result['results'] as $result ) { $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); $post_ids[] = $post_id; - - $this->process_result_highlighting( $result, $post_id ); } $this->search_result_ids = $post_ids; - } - /** - * Process highlighting data for a single search result. - * - * @param array $result The search result data from the API. - * @param int $post_id The post ID for this result. - */ - private function process_result_highlighting( $result, $post_id ) { - if ( empty( $result['highlight'] ) ) { - return; - } - - // Check for data in various highlight field formats. - $title = $this->extract_highlight_field( $result, 'title' ); - $content = $this->extract_highlight_field( $result, 'content' ); - $comments = $this->extract_highlight_field( $result, 'comments' ); - - $this->highlighted_content[ $post_id ] = array( - 'title' => $title, - 'content' => $content, - 'comments' => $comments, - ); - - // If we don't have highlighted content, create some by highlighting the search term. - if ( empty( $title ) && ! empty( $result['fields']['title'] ) ) { - // First use the original search term - if ( ! empty( $this->search_term ) ) { - $title_with_highlights = $this->apply_highlight_patterns( - $result['fields']['title'], - $this->search_term, - false // Don't use corrected term yet - ); - - // Then apply the corrected term if available - if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { - $title_with_highlights = $this->apply_highlight_patterns( - $title_with_highlights, - $this->corrected_search_term, - false // Don't recursively apply correction - ); - } - - $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; - } - } - - if ( empty( $content ) && ! empty( $result['fields']['content'] ) ) { - // First use the original search term - if ( ! empty( $this->search_term ) ) { - $content_with_highlights = $this->apply_highlight_patterns( - $result['fields']['content'], - $this->search_term, - false // Don't use corrected term yet - ); - - // Then apply the corrected term if available - if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { - $content_with_highlights = $this->apply_highlight_patterns( - $content_with_highlights, - $this->corrected_search_term, - false // Don't recursively apply correction - ); - } - - $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; - } - } - } - - /** - * Extract a highlight field from the search result, handling different field formats. - * - * @param array $result The search result data from the API. - * @param string $field The field name to extract. - * @return string The extracted highlighted field. - */ - private function extract_highlight_field( $result, $field ) { - // Try all possible field variants in order of likelihood - foreach ( $result['highlight'] as $key => $value ) { - // Check if this key is for our requested field (exact match or with suffix) - if ( $key === $field || strpos( $key, $field . '.' ) === 0 ) { - if ( is_array( $value ) && ! empty( $value ) ) { - return $value[0]; - } - } - } - - return ''; + // Initialize the highlighter with search data and process results in one step + $this->highlighter = new Search_Highlighter( $search_term, $corrected_search_term, $post_ids, $this->search_result['results'] ); + $this->highlighter->setup(); } /** @@ -688,50 +502,4 @@ private function create_posts_query( $original_query ) { return new \WP_Query( $args ); } - - /** - * Apply highlight markup to content - * - * @param string $content The content to highlight. - * @param string $search_term The search term to highlight. - * @param bool $use_corrected Whether to also use the corrected search term. - * @return string The highlighted content. - */ - private function apply_highlight_patterns( $content, $search_term, $use_corrected = true ) { - $highlighted = $content; - - // Highlight the original search term - if ( ! empty( $search_term ) ) { - $highlighted = $this->add_mark_tags( $highlighted, $search_term ); - } - - // Also highlight corrected search term if available and requested - if ( $use_corrected && ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $search_term ) { - $highlighted = $this->add_mark_tags( $highlighted, $this->corrected_search_term ); - } - - return $highlighted; - } - - /** - * Add mark tags around search terms in content - * - * @param string $content The content to search within. - * @param string $term The term to highlight. - * @return string The content with highlighted terms. - */ - private function add_mark_tags( $content, $term ) { - $pattern = '/(' . preg_quote( $term, '/' ) . ')/i'; - return preg_replace( $pattern, '$1', $content ); - } - - /** - * Check if the current post is a search result from our API - * - * @param int $post_id The post ID to check. - * @return bool Whether the post is a search result. - */ - private function is_search_result( $post_id ) { - return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true ); - } } diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-search-highlighter.php new file mode 100644 index 0000000000000..5a0800405fee0 --- /dev/null +++ b/projects/packages/search/src/inline-search/class-search-highlighter.php @@ -0,0 +1,327 @@ +search_term = $search_term; + $this->corrected_search_term = $corrected_search_term; + $this->search_result_ids = $search_result_ids; + $this->highlighted_content = array(); + + // Process results immediately if provided + if ( $results !== null ) { + $this->process_results( $results ); + } + } + + /** + * Set up the WordPress filters for highlighting. + */ + public function setup() { + add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 ); + add_filter( 'the_content', array( $this, 'filter_highlighted_content' ), 10, 1 ); + add_filter( 'comment_text', array( $this, 'filter_highlighted_comment' ), 10, 2 ); + } + + /** + * Process highlighting data for search results. + * + * @param array $results The search result data from the API. + */ + public function process_results( $results ) { + $this->highlighted_content = array(); + + if ( empty( $results ) || ! is_array( $results ) ) { + return; + } + + foreach ( $results as $result ) { + $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); + $this->process_result_highlighting( $result, $post_id ); + } + } + + /** + * Update search terms and result IDs. + * + * @param string $search_term The original search term. + * @param string $corrected_search_term The corrected search term (if any). + * @param array $search_result_ids Array of post IDs from search results. + */ + public function update_search_data( $search_term, $corrected_search_term = '', $search_result_ids = array() ) { + $this->search_term = $search_term; + $this->corrected_search_term = $corrected_search_term; + $this->search_result_ids = $search_result_ids; + } + + /** + * Filter the post title to show highlighted version. + * + * @param string $title The post title. + * @param int $post_id The post ID. + * @return string The filtered title. + */ + public function filter_highlighted_title( $title, $post_id ) { + // Only process if this is one of our search results + if ( ! $this->is_search_result( $post_id ) ) { + return $title; + } + + // Check if we have a highlighted title from the API + if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) { + return $this->highlighted_content[ $post_id ]['title']; + } + + // Fallback: Even though the API should provide highlighted titles, + // in some cases it doesn't, so we need to apply our own highlighting + if ( ! empty( $this->search_term ) ) { + return $this->apply_highlight_patterns( $title, $this->search_term ); + } + + return $title; + } + + /** + * Filter the post content to show highlighted version. + * + * @param string $content The post content. + * @return string The filtered content. + */ + public function filter_highlighted_content( $content ) { + // Get current post ID + $post_id = get_the_ID(); + + // Only process if this is one of our search results + if ( ! $this->is_search_result( $post_id ) ) { + return $content; + } + + if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) { + return $this->highlighted_content[ $post_id ]['content']; + } + + // If we don't have highlighted content, manually highlight the search term + if ( ! empty( $this->search_term ) ) { + return $this->apply_highlight_patterns( $content, $this->search_term ); + } + + return $content; + } + + /** + * Filter comment text to show highlighted version. + * + * @param string $comment_text The comment text. + * @return string The filtered comment text. + */ + public function filter_highlighted_comment( $comment_text ) { + // Only process if this is one of our search results and we're in a search context + if ( ! is_search() || ! in_the_loop() ) { + return $comment_text; + } + + $post_id = get_the_ID(); + + // Check if this post is a search result and we have highlighted comments for it + if ( ! $this->is_search_result( $post_id ) || empty( $this->highlighted_content[ $post_id ]['comments'] ) ) { + return $comment_text; + } + + // Simple check to see if this comment contains our search term + if ( ! empty( $this->search_term ) && stripos( $comment_text, $this->search_term ) !== false ) { + return $this->apply_highlight_patterns( $comment_text, $this->search_term ); + } + + return $comment_text; + } + + /** + * Process highlighting data for a single search result. + * + * @param array $result The search result data from the API. + * @param int $post_id The post ID for this result. + */ + private function process_result_highlighting( $result, $post_id ) { + if ( empty( $result['highlight'] ) ) { + return; + } + + // Check for data in various highlight field formats. + $title = $this->extract_highlight_field( $result, 'title' ); + $content = $this->extract_highlight_field( $result, 'content' ); + $comments = $this->extract_highlight_field( $result, 'comments' ); + + $this->highlighted_content[ $post_id ] = array( + 'title' => $title, + 'content' => $content, + 'comments' => $comments, + ); + + // If we don't have highlighted content, create some by highlighting the search term. + if ( empty( $title ) && ! empty( $result['fields']['title'] ) ) { + // First use the original search term + if ( ! empty( $this->search_term ) ) { + $title_with_highlights = $this->apply_highlight_patterns( + $result['fields']['title'], + $this->search_term, + false // Don't use corrected term yet + ); + + // Then apply the corrected term if available + if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { + $title_with_highlights = $this->apply_highlight_patterns( + $title_with_highlights, + $this->corrected_search_term, + false // Don't recursively apply correction + ); + } + + $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; + } + } + + if ( empty( $content ) && ! empty( $result['fields']['content'] ) ) { + // First use the original search term + if ( ! empty( $this->search_term ) ) { + $content_with_highlights = $this->apply_highlight_patterns( + $result['fields']['content'], + $this->search_term, + false // Don't use corrected term yet + ); + + // Then apply the corrected term if available + if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { + $content_with_highlights = $this->apply_highlight_patterns( + $content_with_highlights, + $this->corrected_search_term, + false // Don't recursively apply correction + ); + } + + $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; + } + } + } + + /** + * Extract a highlight field from the search result, handling different field formats. + * + * @param array $result The search result data from the API. + * @param string $field The field name to extract. + * @return string The extracted highlighted field. + */ + private function extract_highlight_field( $result, $field ) { + // Try all possible field variants in order of likelihood + foreach ( $result['highlight'] as $key => $value ) { + // Check if this key is for our requested field (exact match or with suffix) + if ( $key === $field || strpos( $key, $field . '.' ) === 0 ) { + if ( is_array( $value ) && ! empty( $value ) ) { + return $value[0]; + } + } + } + + return ''; + } + + /** + * Apply highlight markup to content + * + * @param string $content The content to highlight. + * @param string $search_term The search term to highlight. + * @param bool $use_corrected Whether to also use the corrected search term. + * @return string The highlighted content. + */ + private function apply_highlight_patterns( $content, $search_term, $use_corrected = true ) { + $highlighted = $content; + + // Highlight the original search term + if ( ! empty( $search_term ) ) { + $highlighted = $this->add_mark_tags( $highlighted, $search_term ); + } + + // Also highlight corrected search term if available and requested + if ( $use_corrected && ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $search_term ) { + $highlighted = $this->add_mark_tags( $highlighted, $this->corrected_search_term ); + } + + return $highlighted; + } + + /** + * Add mark tags around search terms in content + * + * @param string $content The content to search within. + * @param string $term The term to highlight. + * @return string The content with highlighted terms. + */ + private function add_mark_tags( $content, $term ) { + $pattern = '/(' . preg_quote( $term, '/' ) . ')/i'; + return preg_replace( $pattern, '$1', $content ); + } + + /** + * Check if the current post is a search result from our API + * + * @param int $post_id The post ID to check. + * @return bool Whether the post is a search result. + */ + private function is_search_result( $post_id ) { + return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true ); + } + + /** + * Get the highlighted content for a post. + * + * @param int $post_id The post ID. + * @return array|null The highlighted content array or null if not found. + */ + public function get_highlighted_content( $post_id ) { + return isset( $this->highlighted_content[ $post_id ] ) ? $this->highlighted_content[ $post_id ] : null; + } +} From 588f8554c547c5f1c1134f4c5736d93b01c642eb Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 24 Apr 2025 17:53:27 +0100 Subject: [PATCH 26/42] New class for search correction surfacing Split out search correction methods into their own class --- .../class-inline-search-correction.php | 140 ++++++++++++++++++ .../src/inline-search/class-inline-search.php | 125 ++-------------- 2 files changed, 151 insertions(+), 114 deletions(-) create mode 100644 projects/packages/search/src/inline-search/class-inline-search-correction.php diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php new file mode 100644 index 0000000000000..eca17dfbe11ef --- /dev/null +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -0,0 +1,140 @@ +is_search() || ! $query->is_main_query() ) { + return; + } + + add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); + add_action( 'wp_footer', array( $this, 'register_corrected_query_script' ) ); + } + + /** + * Register and configure the JavaScript for displaying the corrected query notice. + * + * @since $$next-version$$ + */ + public function register_corrected_query_script() { + $corrected_query_html = $this->get_corrected_query_html(); + if ( empty( $corrected_query_html ) ) { + return; + } + + $handle = 'jetpack-search-inline-corrected-query'; + + Assets::register_script( + $handle, + 'js/corrected-query.js', + __FILE__, + array( + 'in_footer' => true, + 'textdomain' => 'jetpack-search-pkg', + 'enqueue' => true, + ) + ); + + wp_localize_script( + $handle, + 'JetpackSearchCorrectedQuery', + array( + 'html' => $corrected_query_html, + 'selectors' => $this->get_title_selectors(), + 'i18n' => array( + 'error' => esc_html__( 'Error displaying search correction', 'jetpack-search-pkg' ), + ), + ) + ); + } + + /** + * Replaces the search query with the corrected query in the title. + * + * @param string $query The original search query. + * @return string The corrected query if available, otherwise the original query. + */ + public function maybe_use_corrected_query( $query ) { + $search_result = $this->get_search_result(); + if ( ! empty( $search_result['corrected_query'] ) && ! empty( $search_result['results'] ) ) { + return $search_result['corrected_query']; + } + + return $query; + } + + /** + * Get selectors where corrected query notice will be displayed. + * + * @since $$next-version$$ + * @return array CSS selectors for search title elements. + */ + private function get_title_selectors() { + $default_selectors = array( + '.wp-block-query-title', + '.page-title', + '.archive-title', + ); + + /** + * Filter the selectors where corrected query notice appears. + * + * @since $$next-version$$ + * @param array $default_selectors CSS selectors for search title elements. + */ + return apply_filters( 'jetpack_search_title_selectors', $default_selectors ); + } + + /** + * Generate the HTML for the corrected query notice. + * + * @return string The HTML for the corrected query notice or empty string if none. + */ + private function get_corrected_query_html() { + $original_query = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a search query. + $search_result = $this->get_search_result(); + + if ( empty( $search_result['corrected_query'] ) || empty( $search_result['results'] ) ) { + return ''; + } + + $message = sprintf( + /* translators: %s: Original search term the user entered */ + esc_html__( 'No results for %s', 'jetpack-search-pkg' ), + esc_html( $original_query ) + ); + + return sprintf( + '

%s

', + $message + ); + } + + /** + * Get the search result from the Inline_Search instance. + * + * @return array|null The search result or null if not available. + */ + private function get_search_result() { + $inline_search = Inline_Search::instance(); + return $inline_search->get_search_result(); + } +} diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index fe53ae0a6052a..7e99bbd1b02a2 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -7,8 +7,6 @@ namespace Automattic\Jetpack\Search; -use Automattic\Jetpack\Assets; - /** * Inline Search class */ @@ -20,6 +18,13 @@ class Inline_Search extends Classic_Search { */ private static $instance; + /** + * The search correction instance. + * + * @var Inline_Search_Correction + */ + private $correction; + /** * Returns whether this class should be used instead of Classic_Search. */ @@ -42,8 +47,11 @@ public static function instance( $blog_id = null ) { self::$instance = new static(); self::$instance->setup( $blog_id ); + // Initialize search correction handling + self::$instance->correction = new Inline_Search_Correction(); + // Add hooks for displaying corrected query notice - add_action( 'pre_get_posts', array( self::$instance, 'setup_corrected_query_hooks' ) ); + add_action( 'pre_get_posts', array( self::$instance->correction, 'setup_corrected_query_hooks' ) ); } return self::$instance; @@ -426,115 +434,4 @@ public function get_search_result( ) { return $this->search_result; } - - /** - * Setup hooks for displaying corrected query notice. - * - * @param \WP_Query $query The current query. - */ - public function setup_corrected_query_hooks( $query ) { - if ( ! $query->is_search() || ! $query->is_main_query() ) { - return; - } - - add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); - add_action( 'wp_footer', array( $this, 'register_corrected_query_script' ) ); - } - - /** - * Register and configure the JavaScript for displaying the corrected query notice. - * - * @since $$next-version$$ - */ - public function register_corrected_query_script() { - $corrected_query_html = $this->get_corrected_query_html(); - if ( empty( $corrected_query_html ) ) { - return; - } - - $handle = 'jetpack-search-inline-corrected-query'; - - Assets::register_script( - $handle, - 'js/corrected-query.js', - __FILE__, - array( - 'in_footer' => true, - 'textdomain' => 'jetpack-search-pkg', - 'enqueue' => true, - ) - ); - - wp_localize_script( - $handle, - 'JetpackSearchCorrectedQuery', - array( - 'html' => $corrected_query_html, - 'selectors' => $this->get_title_selectors(), - 'i18n' => array( - 'error' => esc_html__( 'Error displaying search correction', 'jetpack-search-pkg' ), - ), - ) - ); - } - - /** - * Replaces the search query with the corrected query in the title. - * - * @param string $query The original search query. - * @return string The corrected query if available, otherwise the original query. - */ - public function maybe_use_corrected_query( $query ) { - if ( ! empty( $this->search_result['corrected_query'] ) && ! empty( $this->search_result['results'] ) ) { - return $this->search_result['corrected_query']; - } - - return $query; - } - - /** - * Get selectors where corrected query notice will be displayed. - * - * @since $$next-version$$ - * @return array CSS selectors for search title elements. - */ - private function get_title_selectors() { - $default_selectors = array( - '.wp-block-query-title', - '.page-title', - '.archive-title', - ); - - /** - * Filter the selectors where corrected query notice appears. - * - * @since $$next-version$$ - * @param array $default_selectors CSS selectors for search title elements. - */ - return apply_filters( 'jetpack_search_title_selectors', $default_selectors ); - } - - /** - * Generate the HTML for the corrected query notice. - * - * @return string The HTML for the corrected query notice or empty string if none. - */ - private function get_corrected_query_html() { - $original_query = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a search query. - - if ( empty( $this->search_result['corrected_query'] ) || empty( $this->search_result['results'] ) ) { - return ''; - } - - $message = sprintf( - /* translators: %s: Original search term the user entered */ - esc_html__( 'No results for %s', 'jetpack-search-pkg' ), - esc_html( $original_query ) - ); - - return sprintf( - '

%s

', - $message - ); - } } From 6b6c299593ee88cfe3faf89a53740dca32c61465 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 28 Apr 2025 10:51:38 +0100 Subject: [PATCH 27/42] Create Inline Search Webpack config We need a webpack config for Inline Search to allow us to add styles sustainably, i.e., not inline. This is for the process of our interim "per theme" approach for surfacing search corrections on the X most popular WPcom themes. --- projects/packages/search/package.json | 5 +- .../class-inline-search-correction.php | 4 +- .../src/inline-search/js/corrected-query.js | 3 +- .../search/src/inline-search/js/index.js | 6 ++ .../inline-search/styles/corrected-query.scss | 5 ++ .../search/tools/webpack.inline.config.js | 57 +++++++++++++++++++ 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 projects/packages/search/src/inline-search/js/index.js create mode 100644 projects/packages/search/src/inline-search/styles/corrected-query.scss create mode 100644 projects/packages/search/tools/webpack.inline.config.js diff --git a/projects/packages/search/package.json b/projects/packages/search/package.json index 4567b64b3cbc5..a99704447c3bb 100644 --- a/projects/packages/search/package.json +++ b/projects/packages/search/package.json @@ -6,10 +6,11 @@ "test": "tests" }, "scripts": { - "build": "pnpm run clean && pnpm run build-instant && pnpm run build-customberg && pnpm run build-dashboard", + "build": "pnpm run clean && pnpm run build-instant && pnpm run build-customberg && pnpm run build-dashboard && pnpm run build-inline", "build-production": "NODE_ENV=production BABEL_ENV=production pnpm run build && pnpm run validate", "build-development": "NODE_ENV=development BABEL_ENV=development pnpm run build", "build-instant": "webpack --config ./tools/webpack.instant.config.js", + "build-inline": "webpack --config ./tools/webpack.inline.config.js", "build-customberg": "webpack --config ./tools/webpack.customberg.config.js", "build-dashboard": "webpack --config ./tools/webpack.dashboard.config.js", "clean": "rm -rf build/ .cache/", @@ -18,7 +19,7 @@ "test-scripts": "jest --passWithNoTests", "test-size": "NODE_ENV=production BABEL_ENV=production pnpm run build-instant && size-limit", "validate": "pnpm exec validate-es --no-error-on-unmatched-pattern build/", - "watch": "concurrently 'pnpm:build-instant --watch' 'pnpm:build-customberg --watch' 'pnpm:build-dashboard --watch'" + "watch": "concurrently 'pnpm:build-instant --watch' 'pnpm:build-customberg --watch' 'pnpm:build-dashboard --watch' 'pnpm:build-inline --watch'" }, "repository": { "type": "git", diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index eca17dfbe11ef..4c8e5f7b77046 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -44,8 +44,8 @@ public function register_corrected_query_script() { Assets::register_script( $handle, - 'js/corrected-query.js', - __FILE__, + 'build/inline-search/jp-search-inline.js', + Package::get_installed_path() . '/src', array( 'in_footer' => true, 'textdomain' => 'jetpack-search-pkg', diff --git a/projects/packages/search/src/inline-search/js/corrected-query.js b/projects/packages/search/src/inline-search/js/corrected-query.js index 80d382096f377..bf6e078e216cc 100644 --- a/projects/packages/search/src/inline-search/js/corrected-query.js +++ b/projects/packages/search/src/inline-search/js/corrected-query.js @@ -16,8 +16,7 @@ document.addEventListener( 'DOMContentLoaded', () => { const notice = document.createElement( 'div' ); notice.innerHTML = html; - notice.className = `${ titleElement.className } ${ notice.className }`; - notice.style.cssText = 'font-size: 0.9em; margin-top: 10px; padding-top: 0;'; + notice.className = `${ titleElement.className } ${ notice.className } jp-inline-search-corrected-query-notice`; notice.setAttribute( 'role', 'status' ); notice.setAttribute( 'aria-live', 'polite' ); diff --git a/projects/packages/search/src/inline-search/js/index.js b/projects/packages/search/src/inline-search/js/index.js new file mode 100644 index 0000000000000..ef0d042935a09 --- /dev/null +++ b/projects/packages/search/src/inline-search/js/index.js @@ -0,0 +1,6 @@ +/** + * Entry point for inline search styles. + * This file imports the CSS and the modules for inline search. + */ +import '../styles/corrected-query.scss'; +import './corrected-query'; diff --git a/projects/packages/search/src/inline-search/styles/corrected-query.scss b/projects/packages/search/src/inline-search/styles/corrected-query.scss new file mode 100644 index 0000000000000..a4b07fea7e0c4 --- /dev/null +++ b/projects/packages/search/src/inline-search/styles/corrected-query.scss @@ -0,0 +1,5 @@ +.jp-inline-search-corrected-query-notice { + font-size: 0.9em; + margin-top: 10px; + padding-top: 0; +} \ No newline at end of file diff --git a/projects/packages/search/tools/webpack.inline.config.js b/projects/packages/search/tools/webpack.inline.config.js new file mode 100644 index 0000000000000..f884b1f16ddf2 --- /dev/null +++ b/projects/packages/search/tools/webpack.inline.config.js @@ -0,0 +1,57 @@ +const path = require( 'path' ); +const jetpackWebpackConfig = require( '@automattic/jetpack-webpack-config/webpack' ); + +module.exports = { + mode: jetpackWebpackConfig.mode, + devtool: jetpackWebpackConfig.devtool, + entry: { + 'jp-search-inline': path.join( __dirname, '../src/inline-search/js/index.js' ), + }, + output: { + ...jetpackWebpackConfig.output, + path: path.join( __dirname, '../build/inline-search' ), + }, + optimization: { + ...jetpackWebpackConfig.optimization, + }, + resolve: { + ...jetpackWebpackConfig.resolve, + modules: [ + path.resolve( __dirname, '../src/inline-search' ), + 'node_modules', + path.resolve( __dirname, '../node_modules' ), + ], + }, + plugins: [ ...jetpackWebpackConfig.StandardPlugins() ], + module: { + strictExportPresence: true, + rules: [ + // Transpile JavaScript except node modules. + jetpackWebpackConfig.TranspileRule( { + exclude: /node_modules\//, + } ), + + // Transpile @automattic/jetpack-* in node_modules too. + jetpackWebpackConfig.TranspileRule( { + includeNodeModules: [ '@automattic/jetpack-' ], + } ), + + // Handle CSS. + jetpackWebpackConfig.CssRule( { + extensions: [ 'css', 'sass', 'scss' ], + extraLoaders: [ + { + loader: 'postcss-loader', + options: { + postcssOptions: { config: path.join( __dirname, '../postcss.config.js' ) }, + }, + }, + 'sass-loader', + ], + } ), + + // Handle images. + jetpackWebpackConfig.FileRule(), + ], + }, +}; From f6695f6fa7a464ff8cdc7ed13870e52c1e42c4d0 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Mon, 28 Apr 2025 18:05:51 +0100 Subject: [PATCH 28/42] Add styling for default themes where necesssary: pub/twentyten pub/twentyeleven pub/twentytwelve pub/twentyfifteen pub/twentysixteen pub/twentyseventeen pub/twentynineteen pub/twentytwenty pub/twentytwentytwo pub/twentytwentythree pub/twentytwentyfour pub/twentytwentyfive --- .../class-inline-search-correction.php | 57 ++++++++++++++++++- .../src/inline-search/js/corrected-query.js | 10 +--- .../inline-search/styles/corrected-query.scss | 35 ++++++++++-- .../search/tools/webpack.inline.config.js | 1 + 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index 4c8e5f7b77046..b1bf257c197e6 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -26,9 +26,26 @@ public function setup_corrected_query_hooks( $query ) { } add_filter( 'get_search_query', array( $this, 'maybe_use_corrected_query' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); add_action( 'wp_footer', array( $this, 'register_corrected_query_script' ) ); } + /** + * Enqueue theme-specific styles for the search correction. + * This is hooked to wp_enqueue_scripts to ensure styles load properly in the head. + * + * @since $$next-version$$ + */ + public function enqueue_styles() { + $corrected_query_html = $this->get_corrected_query_html(); + if ( empty( $corrected_query_html ) ) { + return; + } + + $handle = 'jetpack-search-inline-corrected-query'; + $this->register_corrected_query_style( $handle ); + } + /** * Register and configure the JavaScript for displaying the corrected query notice. * @@ -66,6 +83,42 @@ public function register_corrected_query_script() { ); } + /** + * Register and enqueue theme-specific styles for corrected query. + * + * @since $$next-version$$ + * @param string $handle The script handle to use for the stylesheet. + */ + private function register_corrected_query_style( $handle ) { + $css_path = 'build/inline-search/'; + $css_file = 'corrected-query.css'; + $full_css_path = $css_path . $css_file; + $package_path = Package::get_installed_path(); + $css_full_path = $package_path . '/' . $full_css_path; + + // Verify the CSS file exists before trying to enqueue it + if ( ! file_exists( $css_full_path ) ) { + return; + } + + // Use the Jetpack Assets class to get the file URL properly + $file_url = Assets::get_file_url_for_environment( + $full_css_path, + $full_css_path, + $package_path + ); + + // Use the file's modification time for more precise cache busting + $file_version = file_exists( $css_full_path ) ? filemtime( $css_full_path ) : Package::VERSION; + + wp_enqueue_style( + $handle, + $file_url, + array(), + $file_version // Use file modification time for cache busting + ); + } + /** * Replaces the search query with the corrected query in the title. * @@ -118,12 +171,12 @@ private function get_corrected_query_html() { $message = sprintf( /* translators: %s: Original search term the user entered */ - esc_html__( 'No results for %s', 'jetpack-search-pkg' ), + esc_html__( 'No results for "%s"', 'jetpack-search-pkg' ), esc_html( $original_query ) ); return sprintf( - '

%s

', + '

%s

', $message ); } diff --git a/projects/packages/search/src/inline-search/js/corrected-query.js b/projects/packages/search/src/inline-search/js/corrected-query.js index bf6e078e216cc..94357dba61242 100644 --- a/projects/packages/search/src/inline-search/js/corrected-query.js +++ b/projects/packages/search/src/inline-search/js/corrected-query.js @@ -13,13 +13,5 @@ document.addEventListener( 'DOMContentLoaded', () => { return; } - const notice = document.createElement( 'div' ); - notice.innerHTML = html; - - notice.className = `${ titleElement.className } ${ notice.className } jp-inline-search-corrected-query-notice`; - - notice.setAttribute( 'role', 'status' ); - notice.setAttribute( 'aria-live', 'polite' ); - - titleElement.insertAdjacentElement( 'afterend', notice ); + titleElement.insertAdjacentHTML( 'afterend', html ); } ); diff --git a/projects/packages/search/src/inline-search/styles/corrected-query.scss b/projects/packages/search/src/inline-search/styles/corrected-query.scss index a4b07fea7e0c4..45c74d40553b1 100644 --- a/projects/packages/search/src/inline-search/styles/corrected-query.scss +++ b/projects/packages/search/src/inline-search/styles/corrected-query.scss @@ -1,5 +1,30 @@ -.jp-inline-search-corrected-query-notice { - font-size: 0.9em; - margin-top: 10px; - padding-top: 0; -} \ No newline at end of file +/** + * Twenty Twenty-Four theme specific styles for search correction + */ +.wp-theme-twentythirteen { + + .jetpack-search-corrected-query { + margin: 0 auto; + max-width: 1040px; + padding: 0 0 30px 0; + width: 100%; + } +} + +.wp-theme-twentyfifteen { + + .jetpack-search-corrected-query { + margin-left: -7px; + } +} + +.wp-theme-twentytwentythree, +.wp-theme-twentytwentyfour { + + .jetpack-search-corrected-query { + margin-top: -1em; + max-width: var(--wp--style--global--wide-size); + } +} + + diff --git a/projects/packages/search/tools/webpack.inline.config.js b/projects/packages/search/tools/webpack.inline.config.js index f884b1f16ddf2..8810137f842ff 100644 --- a/projects/packages/search/tools/webpack.inline.config.js +++ b/projects/packages/search/tools/webpack.inline.config.js @@ -6,6 +6,7 @@ module.exports = { devtool: jetpackWebpackConfig.devtool, entry: { 'jp-search-inline': path.join( __dirname, '../src/inline-search/js/index.js' ), + 'corrected-query': path.join( __dirname, '../src/inline-search/styles/corrected-query.scss' ), }, output: { ...jetpackWebpackConfig.output, From 15a00b552bdc858f1e69c555060420b23c78b43f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 29 Apr 2025 11:29:02 +0100 Subject: [PATCH 29/42] More theme styling Styles for pub/attar pub/assembler pub/hever pub/dara pub/creatio-2 pub/zoologist pub/maywood pub/shawburn pub/lodestar pub/barnsbury pub/morden pub/karuna pub/baskerville-2 pub/exford pub/radcliffe-2 --- .../class-inline-search-correction.php | 7 +- .../inline-search/styles/corrected-query.scss | 73 ++++++++++++++++++- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index b1bf257c197e6..e9f656b660bb7 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -101,11 +101,10 @@ private function register_corrected_query_style( $handle ) { return; } - // Use the Jetpack Assets class to get the file URL properly - $file_url = Assets::get_file_url_for_environment( + // We need to use plugins_url for reliable URL generation + $file_url = plugins_url( $full_css_path, - $full_css_path, - $package_path + $package_path . '/package.json' ); // Use the file's modification time for more precise cache busting diff --git a/projects/packages/search/src/inline-search/styles/corrected-query.scss b/projects/packages/search/src/inline-search/styles/corrected-query.scss index 45c74d40553b1..f16da7c9bb346 100644 --- a/projects/packages/search/src/inline-search/styles/corrected-query.scss +++ b/projects/packages/search/src/inline-search/styles/corrected-query.scss @@ -1,7 +1,12 @@ /** - * Twenty Twenty-Four theme specific styles for search correction + * Jetpack Search - Corrected Query Styles + * + * Theme-specific styles for the search correction functionality. */ -.wp-theme-twentythirteen { + +/* Twenty Theme Family */ +.wp-theme-twentythirteen, +.wp-theme-pubtwentythirteen { .jetpack-search-corrected-query { margin: 0 auto; @@ -11,7 +16,8 @@ } } -.wp-theme-twentyfifteen { +.wp-theme-twentyfifteen, +.wp-theme-pubtwentyfifteen { .jetpack-search-corrected-query { margin-left: -7px; @@ -19,7 +25,9 @@ } .wp-theme-twentytwentythree, -.wp-theme-twentytwentyfour { +.wp-theme-pubtwentytwentythree, +.wp-theme-twentytwentyfour, +.wp-theme-pubtwentytwentyfour { .jetpack-search-corrected-query { margin-top: -1em; @@ -27,4 +35,61 @@ } } +/* Center-aligned themes */ +.wp-theme-pubassembler, +.wp-theme-assembler-wpcom, +.wp-child-theme-maywood-wpcom, +.wp-child-theme-pubmaywood, +.wp-child-theme-hever-wpcom, +.wp-child-theme-pubhever, +.wp-child-theme-pubexford, +.wp-child-theme-exford-wpcom { + + .jetpack-search-corrected-query { + text-align: center; + } +} + +/* Dara theme */ +.wp-theme-pubdara, +.wp-theme-dara-wpcom { + + @media screen and (min-width: 1000px) { + + .jetpack-search-corrected-query { + padding-left: 211px; + } + } +} + +/* Morden theme */ +.wp-child-theme-pubmorden, +.wp-child-theme-morden-wpcom { + .jetpack-search-corrected-query { + background: var(--wp--preset--color--background-high-contrast); + text-align: center; + padding-bottom: 32px; + + @media screen and (min-width: 560px) { + margin-top: -96px; + padding-bottom: 64px; + } + } +} + +/* Baskerville theme */ +.wp-theme-baskerville-2-wpcom { + + .jetpack-search-corrected-query { + margin-top: 1em; + + @media screen and (max-width: 1440px) { + margin-top: -3%; + } + + @media screen and (max-width: 600px) { + margin-top: -15px; + } + } +} \ No newline at end of file From 916f4df917388a3420937aecd1848c2b04ba9d47 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 29 Apr 2025 14:42:06 +0100 Subject: [PATCH 30/42] Premium theme styling Premium themes already make good use of the existing implementation. What we needed for them instead is the addition of a few other selectors to target. --- .../src/inline-search/class-inline-search-correction.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index e9f656b660bb7..530cb91986e2f 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -144,6 +144,9 @@ private function get_title_selectors() { '.wp-block-query-title', '.page-title', '.archive-title', + '.entry-title', + '.nv-page-title', + '.page-subheading', ); /** From 0bae6e9b4f8de3b3f2c05f1c3e4a6bf1ea50fe2b Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 29 Apr 2025 18:23:32 +0100 Subject: [PATCH 31/42] Add query string to activate There are some themes that have proven difficult to test, so let's add a query string to enable Inline Search on demand so that we can merge this PR test further, and iterate. --- .../src/inline-search/class-inline-search.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 7e99bbd1b02a2..68a75fa0e9b1a 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -25,10 +25,24 @@ class Inline_Search extends Classic_Search { */ private $correction; + /** + * The query parameter that triggers inline search. + * + * @var string + */ + private static $inline_search_param = 'inline-search'; + /** * Returns whether this class should be used instead of Classic_Search. */ public static function should_replace_classic_search(): bool { + // Check for the inline search query parameter + // @TODO: Remove this once we go live with the new inline search + if ( isset( $_GET[ self::$inline_search_param ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return true; + } + + // Otherwise use the filter return (bool) apply_filters( 'jetpack_search_replace_classic', false ); } From 73f3faf1d40ff0e4ed1d65515b2cdfb0198ba0de Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 1 May 2025 14:57:33 +0100 Subject: [PATCH 32/42] Refactored implementation of search branding - Leverage a similar approach to Search Correction to inject Search Branding - Test against the 34 most popular Atomic themes - Refactor existing classes to make use of abstraction due to similarity of methods between the colophone and search-correction classes - Update webpack config --- .../class-inline-search-colophon.php | 147 ++++++++++++++++++ .../class-inline-search-component.php | 113 ++++++++++++++ .../class-inline-search-correction.php | 68 +------- .../src/inline-search/class-inline-search.php | 13 ++ .../src/inline-search/js/corrected-query.js | 25 +-- .../search/src/inline-search/js/index.js | 6 +- .../src/inline-search/js/inline-search.js | 24 +++ .../src/inline-search/styles/colophon.scss | 130 ++++++++++++++++ .../src/inline-search/styles/index.scss | 2 + .../search/tools/webpack.inline.config.js | 1 - 10 files changed, 453 insertions(+), 76 deletions(-) create mode 100644 projects/packages/search/src/inline-search/class-inline-search-colophon.php create mode 100644 projects/packages/search/src/inline-search/class-inline-search-component.php create mode 100644 projects/packages/search/src/inline-search/js/inline-search.js create mode 100644 projects/packages/search/src/inline-search/styles/colophon.scss create mode 100644 projects/packages/search/src/inline-search/styles/index.scss diff --git a/projects/packages/search/src/inline-search/class-inline-search-colophon.php b/projects/packages/search/src/inline-search/class-inline-search-colophon.php new file mode 100644 index 0000000000000..fac59f775ccba --- /dev/null +++ b/projects/packages/search/src/inline-search/class-inline-search-colophon.php @@ -0,0 +1,147 @@ +is_valid_search_query( $query ) ) { + return; + } + + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); + add_action( 'wp_footer', array( $this, 'register_colophon_script' ) ); + } + + /** + * Enqueue theme-specific styles for the search colophon. + * This is hooked to wp_enqueue_scripts to ensure styles load properly in the head. + * + * @since $$next-version$$ + */ + public function enqueue_styles() { + $handle = 'jetpack-search-inline-colophon'; + $this->register_component_style( $handle, 'colophon.css' ); + } + + /** + * Register and configure the JavaScript for displaying the colophon. + * + * @since $$next-version$$ + */ + public function register_colophon_script() { + $this->register_inline_search_script(); + + // Only localize the script, don't register it again as it's handled by the base class + wp_localize_script( + self::SCRIPT_HANDLE, + 'JetpackSearchColophon', + array( + 'html' => $this->get_colophon_html(), + 'selector' => $this->format_selectors_for_query( $this->get_content_selectors() ), + ) + ); + } + + /** + * Get selectors where colophon will be displayed. + * + * @since $$next-version$$ + * @return array CSS selectors for content container elements. + */ + private function get_content_selectors() { + $default_selectors = array( + '.wp-block-query', + '.content', + '#content', + '#site-content', + '.site-main', + '.content-area', + ); + + /** + * Filter the selectors where colophon appears. + * + * @since $$next-version$$ + * @param array $default_selectors CSS selectors for content container elements. + */ + return apply_filters( 'jetpack_search_colophon_selectors', $default_selectors ); + } + + /** + * Get the locale for the Jetpack URL. + * + * @return string|null The locale prefix or null. + */ + private function get_locale_prefix() { + $locale = get_locale(); + if ( empty( $locale ) ) { + return null; + } + + $locale_prefix = explode( '-', $locale )[0]; + return $locale_prefix !== 'en' ? $locale_prefix : null; + } + + /** + * Generate the HTML for the colophon. + * + * @return string The HTML for the colophon. + */ + private function get_colophon_html() { + $locale_prefix = $this->get_locale_prefix(); + $url = $locale_prefix + ? 'https://' . $locale_prefix . '.jetpack.com/upgrade/search?utm_source=poweredby' + : 'https://jetpack.com/upgrade/search/?utm_source=poweredby'; + + $logo_svg = $this->get_logo_svg(); + + return sprintf( + '', + esc_url( $url ), + $logo_svg, + esc_html__( 'Search powered by Jetpack', 'jetpack-search-pkg' ) + ); + } + + /** + * Get the SVG markup for the Jetpack logo. + * + * @return string The SVG markup. + */ + private function get_logo_svg() { + $color_jetpack = '#069E08'; + $color_white = '#ffffff'; + $logo_size = 12; + + return sprintf( + '', + $logo_size, + $color_jetpack, + $color_white + ); + } +} diff --git a/projects/packages/search/src/inline-search/class-inline-search-component.php b/projects/packages/search/src/inline-search/class-inline-search-component.php new file mode 100644 index 0000000000000..763d6dfe9a2fb --- /dev/null +++ b/projects/packages/search/src/inline-search/class-inline-search-component.php @@ -0,0 +1,113 @@ +is_search() && $query->is_main_query(); + } + + /** + * Register and enqueue a component stylesheet. + * + * @since $$next-version$$ + * @param string $handle The script handle to use for the stylesheet. + * @param string $css_file The CSS filename (without the path). + * @return bool Whether the style was successfully registered and enqueued. + */ + protected function register_component_style( $handle, $css_file ) { + $css_path = 'build/inline-search/'; + $full_css_path = $css_path . $css_file; + $package_path = Package::get_installed_path(); + $css_full_path = $package_path . '/' . $full_css_path; + + // Verify the CSS file exists before trying to enqueue it + if ( ! file_exists( $css_full_path ) ) { + return false; + } + + // We need to use plugins_url for reliable URL generation + $file_url = plugins_url( + $full_css_path, + $package_path . '/package.json' + ); + + // Use the file's modification time for more precise cache busting + $file_version = file_exists( $css_full_path ) ? filemtime( $css_full_path ) : Package::VERSION; + + wp_enqueue_style( + $handle, + $file_url, + array(), + $file_version // Use file modification time for cache busting + ); + + return true; + } + + /** + * Register the common inline search script if not already registered. + * + * @since $$next-version$$ + * @return bool Whether the script was registered. + */ + protected function register_inline_search_script() { + if ( wp_script_is( self::SCRIPT_HANDLE, 'registered' ) ) { + return true; + } + + return Assets::register_script( + self::SCRIPT_HANDLE, + 'build/inline-search/jp-search-inline.js', + Package::get_installed_path() . '/src', + array( + 'in_footer' => true, + 'textdomain' => 'jetpack-search-pkg', + 'enqueue' => true, + ) + ); + } + + /** + * Get the search result from the Inline_Search instance. + * + * @return array|null The search result or null if not available. + */ + protected function get_search_result() { + $inline_search = Inline_Search::instance(); + return $inline_search->get_search_result(); + } + + /** + * Convert an array of selectors to a comma-separated string for querySelector. + * + * @param array $selectors Array of CSS selectors. + * @return string Comma-separated string of selectors. + */ + protected function format_selectors_for_query( $selectors ) { + return implode( ', ', $selectors ); + } +} diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index 530cb91986e2f..7b3169fb225b5 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -7,21 +7,19 @@ namespace Automattic\Jetpack\Search; -use Automattic\Jetpack\Assets; - /** * Class for handling search correction display * * @since $$next-version$$ */ -class Inline_Search_Correction { +class Inline_Search_Correction extends Inline_Search_Component { /** * Setup hooks for displaying corrected query notice. * * @param \WP_Query $query The current query. */ public function setup_corrected_query_hooks( $query ) { - if ( ! $query->is_search() || ! $query->is_main_query() ) { + if ( ! $this->is_valid_search_query( $query ) ) { return; } @@ -43,7 +41,7 @@ public function enqueue_styles() { } $handle = 'jetpack-search-inline-corrected-query'; - $this->register_corrected_query_style( $handle ); + $this->register_component_style( $handle, 'corrected-query.css' ); } /** @@ -57,21 +55,10 @@ public function register_corrected_query_script() { return; } - $handle = 'jetpack-search-inline-corrected-query'; - - Assets::register_script( - $handle, - 'build/inline-search/jp-search-inline.js', - Package::get_installed_path() . '/src', - array( - 'in_footer' => true, - 'textdomain' => 'jetpack-search-pkg', - 'enqueue' => true, - ) - ); + $this->register_inline_search_script(); wp_localize_script( - $handle, + self::SCRIPT_HANDLE, 'JetpackSearchCorrectedQuery', array( 'html' => $corrected_query_html, @@ -83,41 +70,6 @@ public function register_corrected_query_script() { ); } - /** - * Register and enqueue theme-specific styles for corrected query. - * - * @since $$next-version$$ - * @param string $handle The script handle to use for the stylesheet. - */ - private function register_corrected_query_style( $handle ) { - $css_path = 'build/inline-search/'; - $css_file = 'corrected-query.css'; - $full_css_path = $css_path . $css_file; - $package_path = Package::get_installed_path(); - $css_full_path = $package_path . '/' . $full_css_path; - - // Verify the CSS file exists before trying to enqueue it - if ( ! file_exists( $css_full_path ) ) { - return; - } - - // We need to use plugins_url for reliable URL generation - $file_url = plugins_url( - $full_css_path, - $package_path . '/package.json' - ); - - // Use the file's modification time for more precise cache busting - $file_version = file_exists( $css_full_path ) ? filemtime( $css_full_path ) : Package::VERSION; - - wp_enqueue_style( - $handle, - $file_url, - array(), - $file_version // Use file modification time for cache busting - ); - } - /** * Replaces the search query with the corrected query in the title. * @@ -182,14 +134,4 @@ private function get_corrected_query_html() { $message ); } - - /** - * Get the search result from the Inline_Search instance. - * - * @return array|null The search result or null if not available. - */ - private function get_search_result() { - $inline_search = Inline_Search::instance(); - return $inline_search->get_search_result(); - } } diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 68a75fa0e9b1a..2ecb0bb0737fd 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -25,6 +25,13 @@ class Inline_Search extends Classic_Search { */ private $correction; + /** + * The colophon instance. + * + * @var Inline_Search_Colophon + */ + private $colophon; + /** * The query parameter that triggers inline search. * @@ -64,8 +71,14 @@ public static function instance( $blog_id = null ) { // Initialize search correction handling self::$instance->correction = new Inline_Search_Correction(); + // Initialize colophon handling + self::$instance->colophon = new Inline_Search_Colophon(); + // Add hooks for displaying corrected query notice add_action( 'pre_get_posts', array( self::$instance->correction, 'setup_corrected_query_hooks' ) ); + + // Add hooks for displaying the Jetpack colophon + add_action( 'pre_get_posts', array( self::$instance->colophon, 'setup_colophon_hooks' ) ); } return self::$instance; diff --git a/projects/packages/search/src/inline-search/js/corrected-query.js b/projects/packages/search/src/inline-search/js/corrected-query.js index 94357dba61242..5bc70bf580623 100644 --- a/projects/packages/search/src/inline-search/js/corrected-query.js +++ b/projects/packages/search/src/inline-search/js/corrected-query.js @@ -1,17 +1,24 @@ /** - * Script to display corrected query notice after search titles. + * Script to display corrected query notice after search titles and colophon at the bottom of search results. */ document.addEventListener( 'DOMContentLoaded', () => { - if ( ! window.JetpackSearchCorrectedQuery?.html ) { - return; + // Handle corrected query display + if ( window.JetpackSearchCorrectedQuery?.html ) { + const { selectors, html } = window.JetpackSearchCorrectedQuery; + const titleElement = document.querySelector( selectors.join( ', ' ) ); + + if ( titleElement ) { + titleElement.insertAdjacentHTML( 'afterend', html ); + } } - const { selectors, html } = window.JetpackSearchCorrectedQuery; - const titleElement = document.querySelector( selectors.join( ', ' ) ); + // Handle colophon display + if ( window.JetpackSearchColophon?.html ) { + const { selector, html } = window.JetpackSearchColophon; + const contentElement = document.querySelector( selector ); - if ( ! titleElement ) { - return; + if ( contentElement ) { + contentElement.insertAdjacentHTML( 'beforeend', html ); + } } - - titleElement.insertAdjacentHTML( 'afterend', html ); } ); diff --git a/projects/packages/search/src/inline-search/js/index.js b/projects/packages/search/src/inline-search/js/index.js index ef0d042935a09..43434b34f8966 100644 --- a/projects/packages/search/src/inline-search/js/index.js +++ b/projects/packages/search/src/inline-search/js/index.js @@ -1,6 +1,6 @@ /** * Entry point for inline search styles. - * This file imports the CSS and the modules for inline search. + * This file imports the modules for inline search. */ -import '../styles/corrected-query.scss'; -import './corrected-query'; +import '../styles/index.scss'; +import './inline-search'; diff --git a/projects/packages/search/src/inline-search/js/inline-search.js b/projects/packages/search/src/inline-search/js/inline-search.js new file mode 100644 index 0000000000000..5bc70bf580623 --- /dev/null +++ b/projects/packages/search/src/inline-search/js/inline-search.js @@ -0,0 +1,24 @@ +/** + * Script to display corrected query notice after search titles and colophon at the bottom of search results. + */ +document.addEventListener( 'DOMContentLoaded', () => { + // Handle corrected query display + if ( window.JetpackSearchCorrectedQuery?.html ) { + const { selectors, html } = window.JetpackSearchCorrectedQuery; + const titleElement = document.querySelector( selectors.join( ', ' ) ); + + if ( titleElement ) { + titleElement.insertAdjacentHTML( 'afterend', html ); + } + } + + // Handle colophon display + if ( window.JetpackSearchColophon?.html ) { + const { selector, html } = window.JetpackSearchColophon; + const contentElement = document.querySelector( selector ); + + if ( contentElement ) { + contentElement.insertAdjacentHTML( 'beforeend', html ); + } + } +} ); diff --git a/projects/packages/search/src/inline-search/styles/colophon.scss b/projects/packages/search/src/inline-search/styles/colophon.scss new file mode 100644 index 0000000000000..d6289533c3937 --- /dev/null +++ b/projects/packages/search/src/inline-search/styles/colophon.scss @@ -0,0 +1,130 @@ +$colophon-color: #555; +$colophon-hover-color: #069E08; +$colophon-margin-top: 2rem; +$colophon-margin-bottom: 2rem; +$colophon-padding-top: 1rem; +$colophon-padding-bottom: 1rem; +$colophon-font-size: 0.8rem; +$colophon-logo-margin-right: 4px; + +.jetpack-search-inline-colophon { + display: flex; + justify-content: center; + margin-top: $colophon-margin-top; + margin-bottom: $colophon-margin-bottom; + padding-top: $colophon-padding-top; + padding-bottom: $colophon-padding-bottom; + font-size: $colophon-font-size; + + &-link { + display: flex; + align-items: center; + text-decoration: none; + color: $colophon-color; + + &:hover { + color: $colophon-hover-color; + } + } + + &-logo { + margin-right: $colophon-logo-margin-right; + } +} + +.wp-theme-pubdara, +.wp-theme-dara-wpcom, +.wp-theme-radcliffe-2-wpcom, +.wp-theme-pubradcliffe, +.wp-theme-twentyfifteen, +.wp-theme-pubtwentyfifteen, +.wp-theme-twentytwenty, +.wp-theme-pubtwentytwenty, +.wp-theme-oceanwp { + + .jetpack-search-inline-colophon { + font-size: 1.25rem; + } +} + +.wp-theme-varia-wpcom, +.wp-theme-pubvaria, +.wp-theme-lodestar-wpcom, +.wp-theme-publodestar, +.wp-theme-creatio-2-wpcom, +.wp-theme-pubcreatio-2, +.wp-theme-twentytwentythree, +.wp-theme-pubtwentytwentythree, +.wp-theme-twentytwentyfour, +.wp-theme-pubtwentytwentyfour { + + .jetpack-search-inline-colophon { + justify-content: flex-start; + } +} + +.wp-theme-varia-wpcom, +.wp-theme-pubvaria { + @media only screen and (min-width: 768px) { + .jetpack-search-inline-colophon { + margin: 0 auto; + max-width: calc(782px - 32px); + } + } +} + +.wp-theme-twentysixteen, +.wp-theme-pubtwentysixteen { + + .jetpack-search-inline-colophon { + margin-top: 0; + padding-top: 0; + + @media screen and (min-width: 56.875em) { + float: left; + justify-content: flex-start; + margin-right: -100px; + width: 70%; + } + } +} +.wp-theme-twentynineteen, +.wp-theme-pubtwentynineteen { + + .jetpack-search-inline-colophon { + justify-content: flex-start; + margin: 1rem 1rem calc(3 * 1rem); + + @media only screen and (min-width: 768px) { + margin: 0 calc(10% + 60px) calc(3 * 1rem); + max-width: 80%; + } + } +} + +.wp-theme-karuna-wpcom, +.wp-theme-pubkaruna { + + .jetpack-search-inline-colophon { + justify-content: flex-start; + + @media only screen and (min-width: 768px) { + margin: 0 40% 0 0; + } + } +} + +.wp-theme-generatepress { + .jetpack-search-inline-colophon { + display: block; + margin-top: 2rem; + clear: both; + } +} +.wp-theme-hevor { + .jetpack-search-inline-colophon { + justify-content: flex-start; + max-width: var(--wp--style--global--wide-size); + } +} + diff --git a/projects/packages/search/src/inline-search/styles/index.scss b/projects/packages/search/src/inline-search/styles/index.scss new file mode 100644 index 0000000000000..eed1d0da2ce26 --- /dev/null +++ b/projects/packages/search/src/inline-search/styles/index.scss @@ -0,0 +1,2 @@ +@import 'corrected-query'; +@import 'colophon'; \ No newline at end of file diff --git a/projects/packages/search/tools/webpack.inline.config.js b/projects/packages/search/tools/webpack.inline.config.js index 8810137f842ff..f884b1f16ddf2 100644 --- a/projects/packages/search/tools/webpack.inline.config.js +++ b/projects/packages/search/tools/webpack.inline.config.js @@ -6,7 +6,6 @@ module.exports = { devtool: jetpackWebpackConfig.devtool, entry: { 'jp-search-inline': path.join( __dirname, '../src/inline-search/js/index.js' ), - 'corrected-query': path.join( __dirname, '../src/inline-search/styles/corrected-query.scss' ), }, output: { ...jetpackWebpackConfig.output, From 5e5148544c56acf6cd6998398390a9cb3fc329fa Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 6 May 2025 12:31:37 +0100 Subject: [PATCH 33/42] We don't need this require --- .../packages/search/src/inline-search/class-inline-search.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 8b7c767ef4793..849bf3fbf985a 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -7,8 +7,6 @@ namespace Automattic\Jetpack\Search; -require_once __DIR__ . '/class-search-highlighter.php'; - /** * Inline Search class */ From 4b880947987a52ca72cc2cfc32370ef7415419f8 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 6 May 2025 15:06:28 +0100 Subject: [PATCH 34/42] Use highlight data returned from the API instead of building our own --- .../src/inline-search/class-inline-search.php | 59 +++++++---- .../class-search-highlighter.php | 99 +------------------ 2 files changed, 40 insertions(+), 118 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 849bf3fbf985a..31cf884454e9c 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -314,14 +314,11 @@ public function convert_wp_query_to_api_args( array $args ) { } } - $highlight_options = array( - 'pre_tags' => array( '__MARK__' ), - 'post_tags' => array( '__/MARK__' ), - 'fields' => array( - 'title', - 'content', - 'comments', - ), + // The API endpoint expects highlight_fields array instead of a full highlight configuration + $highlight_fields = array( + 'title', + 'content', + 'comments', ); $fields = array( @@ -333,16 +330,16 @@ public function convert_wp_query_to_api_args( array $args ) { ); return array( - 'blog_id' => $this->jetpack_blog_id, - 'size' => absint( $args['posts_per_page'] ), - 'from' => min( $from, Helper::get_max_offset() ), - 'fields' => $fields, - 'highlight' => $highlight_options, - 'query' => $args['query'] ?? '', - 'sort' => $sort, - 'aggregations' => empty( $aggregations ) ? null : $aggregations, - 'langs' => $this->get_langs(), - 'filter' => array( + 'blog_id' => $this->jetpack_blog_id, + 'size' => absint( $args['posts_per_page'] ), + 'from' => min( $from, Helper::get_max_offset() ), + 'fields' => $fields, + 'highlight_fields' => $highlight_fields, + 'query' => $args['query'] ?? '', + 'sort' => $sort, + 'aggregations' => empty( $aggregations ) ? null : $aggregations, + 'langs' => $this->get_langs(), + 'filter' => array( 'bool' => array( 'must' => $this->build_es_filters( $args ), ), @@ -463,6 +460,7 @@ private function process_search_results( $query ) { $post_ids = array(); $search_term = $query->get( 's' ); $corrected_search_term = ''; + $highlighted_results = array(); // Store corrected query if available if ( ! empty( $this->search_result['corrected_query'] ) ) { @@ -472,12 +470,33 @@ private function process_search_results( $query ) { foreach ( $this->search_result['results'] as $result ) { $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); $post_ids[] = $post_id; + + // Store the highlight data keyed by post ID for later use + if ( ! empty( $result['highlight'] ) ) { + $highlighted_results[ $post_id ] = $result['highlight']; + } } $this->search_result_ids = $post_ids; - // Initialize the highlighter with search data and process results in one step - $this->highlighter = new Search_Highlighter( $search_term, $corrected_search_term, $post_ids, $this->search_result['results'] ); + // Initialize the highlighter with search data and highlight results + $this->highlighter = new Search_Highlighter( $search_term, $corrected_search_term, $post_ids ); + + // Process results if we have highlight data + if ( ! empty( $highlighted_results ) ) { + // Process the API highlighted results to prepare them for the highlighter + $processed_results = array(); + foreach ( $highlighted_results as $post_id => $highlight_data ) { + $processed_results[] = array( + 'fields' => array( + 'post_id' => $post_id, + ), + 'highlight' => $highlight_data, + ); + } + $this->highlighter->process_results( $processed_results ); + } + $this->highlighter->setup(); } diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-search-highlighter.php index 5a0800405fee0..c17f5c700fc7e 100644 --- a/projects/packages/search/src/inline-search/class-search-highlighter.php +++ b/projects/packages/search/src/inline-search/class-search-highlighter.php @@ -117,12 +117,6 @@ public function filter_highlighted_title( $title, $post_id ) { return $this->highlighted_content[ $post_id ]['title']; } - // Fallback: Even though the API should provide highlighted titles, - // in some cases it doesn't, so we need to apply our own highlighting - if ( ! empty( $this->search_term ) ) { - return $this->apply_highlight_patterns( $title, $this->search_term ); - } - return $title; } @@ -145,11 +139,6 @@ public function filter_highlighted_content( $content ) { return $this->highlighted_content[ $post_id ]['content']; } - // If we don't have highlighted content, manually highlight the search term - if ( ! empty( $this->search_term ) ) { - return $this->apply_highlight_patterns( $content, $this->search_term ); - } - return $content; } @@ -172,12 +161,7 @@ public function filter_highlighted_comment( $comment_text ) { return $comment_text; } - // Simple check to see if this comment contains our search term - if ( ! empty( $this->search_term ) && stripos( $comment_text, $this->search_term ) !== false ) { - return $this->apply_highlight_patterns( $comment_text, $this->search_term ); - } - - return $comment_text; + return $this->highlighted_content[ $post_id ]['comments']; } /** @@ -201,51 +185,6 @@ private function process_result_highlighting( $result, $post_id ) { 'content' => $content, 'comments' => $comments, ); - - // If we don't have highlighted content, create some by highlighting the search term. - if ( empty( $title ) && ! empty( $result['fields']['title'] ) ) { - // First use the original search term - if ( ! empty( $this->search_term ) ) { - $title_with_highlights = $this->apply_highlight_patterns( - $result['fields']['title'], - $this->search_term, - false // Don't use corrected term yet - ); - - // Then apply the corrected term if available - if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { - $title_with_highlights = $this->apply_highlight_patterns( - $title_with_highlights, - $this->corrected_search_term, - false // Don't recursively apply correction - ); - } - - $this->highlighted_content[ $post_id ]['title'] = $title_with_highlights; - } - } - - if ( empty( $content ) && ! empty( $result['fields']['content'] ) ) { - // First use the original search term - if ( ! empty( $this->search_term ) ) { - $content_with_highlights = $this->apply_highlight_patterns( - $result['fields']['content'], - $this->search_term, - false // Don't use corrected term yet - ); - - // Then apply the corrected term if available - if ( ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $this->search_term ) { - $content_with_highlights = $this->apply_highlight_patterns( - $content_with_highlights, - $this->corrected_search_term, - false // Don't recursively apply correction - ); - } - - $this->highlighted_content[ $post_id ]['content'] = $content_with_highlights; - } - } } /** @@ -269,42 +208,6 @@ private function extract_highlight_field( $result, $field ) { return ''; } - /** - * Apply highlight markup to content - * - * @param string $content The content to highlight. - * @param string $search_term The search term to highlight. - * @param bool $use_corrected Whether to also use the corrected search term. - * @return string The highlighted content. - */ - private function apply_highlight_patterns( $content, $search_term, $use_corrected = true ) { - $highlighted = $content; - - // Highlight the original search term - if ( ! empty( $search_term ) ) { - $highlighted = $this->add_mark_tags( $highlighted, $search_term ); - } - - // Also highlight corrected search term if available and requested - if ( $use_corrected && ! empty( $this->corrected_search_term ) && $this->corrected_search_term !== $search_term ) { - $highlighted = $this->add_mark_tags( $highlighted, $this->corrected_search_term ); - } - - return $highlighted; - } - - /** - * Add mark tags around search terms in content - * - * @param string $content The content to search within. - * @param string $term The term to highlight. - * @return string The content with highlighted terms. - */ - private function add_mark_tags( $content, $term ) { - $pattern = '/(' . preg_quote( $term, '/' ) . ')/i'; - return preg_replace( $pattern, '$1', $content ); - } - /** * Check if the current post is a search result from our API * From f6c2605715fb810062b86b5a83f00d4f7676d8c8 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 6 May 2025 15:33:53 +0100 Subject: [PATCH 35/42] Address PHPStan --- .../packages/search/src/inline-search/class-inline-search.php | 3 +-- .../search/src/inline-search/class-search-highlighter.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 31cf884454e9c..7d102ba04563c 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -21,7 +21,7 @@ class Inline_Search extends Classic_Search { /** * The Search Highlighter instance. * - * @var Search_Highlighter + * @var Search_Highlighter|null * @since $$next-version$$ */ private $highlighter; @@ -314,7 +314,6 @@ public function convert_wp_query_to_api_args( array $args ) { } } - // The API endpoint expects highlight_fields array instead of a full highlight configuration $highlight_fields = array( 'title', 'content', diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-search-highlighter.php index c17f5c700fc7e..2081f242fc55e 100644 --- a/projects/packages/search/src/inline-search/class-search-highlighter.php +++ b/projects/packages/search/src/inline-search/class-search-highlighter.php @@ -225,6 +225,6 @@ private function is_search_result( $post_id ) { * @return array|null The highlighted content array or null if not found. */ public function get_highlighted_content( $post_id ) { - return isset( $this->highlighted_content[ $post_id ] ) ? $this->highlighted_content[ $post_id ] : null; + return $this->highlighted_content[ $post_id ] ?? null; } } From f5329f318b252a29d09a40d9eecaba50fca36ed3 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Tue, 6 May 2025 15:38:38 +0100 Subject: [PATCH 36/42] Highlight content returned without p tags When the API sends highlighted data back, it's returned without the expected

tags that would wrap content. Let's add those back. --- .../search/src/inline-search/class-search-highlighter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-search-highlighter.php index 2081f242fc55e..954e2f5d3619a 100644 --- a/projects/packages/search/src/inline-search/class-search-highlighter.php +++ b/projects/packages/search/src/inline-search/class-search-highlighter.php @@ -136,7 +136,8 @@ public function filter_highlighted_content( $content ) { } if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) { - return $this->highlighted_content[ $post_id ]['content']; + // Apply wpautop to maintain paragraph formatting + return wpautop( $this->highlighted_content[ $post_id ]['content'] ); } return $content; From 5f74b7922d4092bd8b868375ec34751ec03e5a5f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 8 May 2025 11:54:13 +0100 Subject: [PATCH 37/42] Cleanup related refactor --- .../src/inline-search/class-inline-search.php | 34 +++++------ .../class-search-highlighter.php | 61 ++++++------------- 2 files changed, 33 insertions(+), 62 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 4eb95e5734e01..7b912542142ef 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -26,6 +26,14 @@ class Inline_Search extends Classic_Search { */ private $highlighter; + /** + * The search correction instance. + * + * @var Inline_Search_Correction|null + * @since $$next-version$$ + */ + private $correction; + /** * Stores the list of post IDs that are actual search results. * @@ -122,7 +130,7 @@ public function filter__posts_pre_query( $posts, $query ) { } // Process the search results to extract post IDs and highlighted content. - $this->process_search_results( $query ); + $this->process_search_results(); // Create a WP_Query to fetch the actual posts. $posts_query = $this->create_posts_query( $query ); @@ -458,38 +466,26 @@ public function get_search_result( /** * Process search results to extract post IDs and highlighted content. - * - * @param \WP_Query $query The original WP_Query. */ - private function process_search_results( $query ) { - $post_ids = array(); - $search_term = $query->get( 's' ); - $corrected_search_term = ''; - $highlighted_results = array(); - - // Store corrected query if available - if ( ! empty( $this->search_result['corrected_query'] ) ) { - $corrected_search_term = $this->search_result['corrected_query']; - } + private function process_search_results() { + $post_ids = array(); + $highlighted_results = array(); foreach ( $this->search_result['results'] as $result ) { $post_id = (int) ( $result['fields']['post_id'] ?? 0 ); $post_ids[] = $post_id; - // Store the highlight data keyed by post ID for later use + // Collect highlight data for processing if ( ! empty( $result['highlight'] ) ) { $highlighted_results[ $post_id ] = $result['highlight']; } } $this->search_result_ids = $post_ids; + $this->highlighter = new Search_Highlighter( $post_ids ); - // Initialize the highlighter with search data and highlight results - $this->highlighter = new Search_Highlighter( $search_term, $corrected_search_term, $post_ids ); - - // Process results if we have highlight data if ( ! empty( $highlighted_results ) ) { - // Process the API highlighted results to prepare them for the highlighter + // Format highlight data for the highlighter $processed_results = array(); foreach ( $highlighted_results as $post_id => $highlight_data ) { $processed_results[] = array( diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-search-highlighter.php index 954e2f5d3619a..a13046fbf6414 100644 --- a/projects/packages/search/src/inline-search/class-search-highlighter.php +++ b/projects/packages/search/src/inline-search/class-search-highlighter.php @@ -18,20 +18,6 @@ class Search_Highlighter { */ private $highlighted_content = array(); - /** - * Stores the search term used in the query. - * - * @var string - */ - private $search_term; - - /** - * Stores the corrected search term if provided by the API. - * - * @var string - */ - private $corrected_search_term; - /** * Stores the list of post IDs that are actual search results. * @@ -42,18 +28,14 @@ class Search_Highlighter { /** * Constructor * - * @param string $search_term The original search term. - * @param string $corrected_search_term The corrected search term (if any). - * @param array $search_result_ids Array of post IDs from search results. - * @param array $results Optional. The search result data from the API to process immediately. + * @param array $search_result_ids Array of post IDs from search results. + * @param array $results Optional. The search result data from the API to process immediately. */ - public function __construct( $search_term = '', $corrected_search_term = '', $search_result_ids = array(), $results = null ) { - $this->search_term = $search_term; - $this->corrected_search_term = $corrected_search_term; - $this->search_result_ids = $search_result_ids; - $this->highlighted_content = array(); + public function __construct( $search_result_ids = array(), $results = null ) { + $this->search_result_ids = $search_result_ids; + $this->highlighted_content = array(); - // Process results immediately if provided + // Process API results immediately if provided if ( $results !== null ) { $this->process_results( $results ); } @@ -87,16 +69,12 @@ public function process_results( $results ) { } /** - * Update search terms and result IDs. + * Update search result IDs. * - * @param string $search_term The original search term. - * @param string $corrected_search_term The corrected search term (if any). - * @param array $search_result_ids Array of post IDs from search results. + * @param array $search_result_ids Array of post IDs from search results. */ - public function update_search_data( $search_term, $corrected_search_term = '', $search_result_ids = array() ) { - $this->search_term = $search_term; - $this->corrected_search_term = $corrected_search_term; - $this->search_result_ids = $search_result_ids; + public function update_search_data( $search_result_ids = array() ) { + $this->search_result_ids = $search_result_ids; } /** @@ -107,12 +85,10 @@ public function update_search_data( $search_term, $corrected_search_term = '', $ * @return string The filtered title. */ public function filter_highlighted_title( $title, $post_id ) { - // Only process if this is one of our search results if ( ! $this->is_search_result( $post_id ) ) { return $title; } - // Check if we have a highlighted title from the API if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) { return $this->highlighted_content[ $post_id ]['title']; } @@ -127,16 +103,14 @@ public function filter_highlighted_title( $title, $post_id ) { * @return string The filtered content. */ public function filter_highlighted_content( $content ) { - // Get current post ID $post_id = get_the_ID(); - // Only process if this is one of our search results if ( ! $this->is_search_result( $post_id ) ) { return $content; } if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) { - // Apply wpautop to maintain paragraph formatting + // Apply wpautop to maintain paragraph formatting. return wpautop( $this->highlighted_content[ $post_id ]['content'] ); } @@ -150,14 +124,12 @@ public function filter_highlighted_content( $content ) { * @return string The filtered comment text. */ public function filter_highlighted_comment( $comment_text ) { - // Only process if this is one of our search results and we're in a search context if ( ! is_search() || ! in_the_loop() ) { return $comment_text; } $post_id = get_the_ID(); - // Check if this post is a search result and we have highlighted comments for it if ( ! $this->is_search_result( $post_id ) || empty( $this->highlighted_content[ $post_id ]['comments'] ) ) { return $comment_text; } @@ -176,7 +148,6 @@ private function process_result_highlighting( $result, $post_id ) { return; } - // Check for data in various highlight field formats. $title = $this->extract_highlight_field( $result, 'title' ); $content = $this->extract_highlight_field( $result, 'content' ); $comments = $this->extract_highlight_field( $result, 'comments' ); @@ -196,10 +167,14 @@ private function process_result_highlighting( $result, $post_id ) { * @return string The extracted highlighted field. */ private function extract_highlight_field( $result, $field ) { - // Try all possible field variants in order of likelihood + // Try exact match first + if ( isset( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) && ! empty( $result['highlight'][ $field ] ) ) { + return $result['highlight'][ $field ][0]; + } + + // Try field variants with suffixes (e.g., 'title.default') foreach ( $result['highlight'] as $key => $value ) { - // Check if this key is for our requested field (exact match or with suffix) - if ( $key === $field || strpos( $key, $field . '.' ) === 0 ) { + if ( strpos( $key, $field . '.' ) === 0 ) { if ( is_array( $value ) && ! empty( $value ) ) { return $value[0]; } From 619c72a96a2870cab0734cb36755ffc84856c37d Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 8 May 2025 14:50:49 +0100 Subject: [PATCH 38/42] changelog --- projects/packages/search/changelog/add-inline-search-branding | 4 ++++ projects/plugins/jetpack/changelog/add-inline-search-branding | 4 ++++ projects/plugins/search/changelog/add-inline-search-branding | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 projects/packages/search/changelog/add-inline-search-branding create mode 100644 projects/plugins/jetpack/changelog/add-inline-search-branding create mode 100644 projects/plugins/search/changelog/add-inline-search-branding diff --git a/projects/packages/search/changelog/add-inline-search-branding b/projects/packages/search/changelog/add-inline-search-branding new file mode 100644 index 0000000000000..3630b8beef243 --- /dev/null +++ b/projects/packages/search/changelog/add-inline-search-branding @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Inline Search: add Jetpack Branding to search results. diff --git a/projects/plugins/jetpack/changelog/add-inline-search-branding b/projects/plugins/jetpack/changelog/add-inline-search-branding new file mode 100644 index 0000000000000..90ef0e14a70cf --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-inline-search-branding @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +add-inline-search-branding diff --git a/projects/plugins/search/changelog/add-inline-search-branding b/projects/plugins/search/changelog/add-inline-search-branding new file mode 100644 index 0000000000000..3630b8beef243 --- /dev/null +++ b/projects/plugins/search/changelog/add-inline-search-branding @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Inline Search: add Jetpack Branding to search results. From b624e6ec61d4e83b64bbbd7453289deeea7cf3b3 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 8 May 2025 17:11:24 +0100 Subject: [PATCH 39/42] Have the Search Correction class use our abstracted class methods --- .../class-inline-search-correction.php | 70 ++----------------- 1 file changed, 7 insertions(+), 63 deletions(-) diff --git a/projects/packages/search/src/inline-search/class-inline-search-correction.php b/projects/packages/search/src/inline-search/class-inline-search-correction.php index b76049f609d64..2d3d74bea4de1 100644 --- a/projects/packages/search/src/inline-search/class-inline-search-correction.php +++ b/projects/packages/search/src/inline-search/class-inline-search-correction.php @@ -7,21 +7,19 @@ namespace Automattic\Jetpack\Search; -use Automattic\Jetpack\Assets; - /** * Class for handling search correction display * * @since $$next-version$$ */ -class Inline_Search_Correction { +class Inline_Search_Correction extends Inline_Search_Component { /** * Setup hooks for displaying corrected query notice. * * @param \WP_Query $query The current query. */ public function setup_corrected_query_hooks( $query ) { - if ( ! $query->is_search() || ! $query->is_main_query() ) { + if ( ! $this->is_valid_search_query( $query ) ) { return; } @@ -42,8 +40,8 @@ public function enqueue_styles() { return; } - $handle = 'jetpack-search-inline-corrected-query'; - $this->register_corrected_query_style( $handle ); + $handle = self::SCRIPT_HANDLE . '-corrected-query'; + $this->register_component_style( $handle, 'corrected-query.css' ); } /** @@ -57,7 +55,7 @@ public function register_corrected_query_script() { return; } - $handle = 'jetpack-search-inline-corrected-query'; + $handle = self::SCRIPT_HANDLE . '-corrected-query'; // Don't localize if already localized to prevent duplication if ( wp_script_is( $handle, 'data' ) ) { @@ -67,19 +65,10 @@ public function register_corrected_query_script() { } } - Assets::register_script( - $handle, - 'build/inline-search/jp-search-inline.js', - Package::get_installed_path() . '/src', - array( - 'in_footer' => true, - 'textdomain' => 'jetpack-search-pkg', - 'enqueue' => true, - ) - ); + $this->register_inline_search_script(); wp_localize_script( - $handle, + self::SCRIPT_HANDLE, 'JetpackSearchCorrectedQuery', array( 'html' => $corrected_query_html, @@ -91,41 +80,6 @@ public function register_corrected_query_script() { ); } - /** - * Register and enqueue theme-specific styles for corrected query. - * - * @since $$next-version$$ - * @param string $handle The script handle to use for the stylesheet. - */ - private function register_corrected_query_style( $handle ) { - $css_path = 'build/inline-search/'; - $css_file = 'corrected-query.css'; - $full_css_path = $css_path . $css_file; - $package_path = Package::get_installed_path(); - $css_full_path = $package_path . '/' . $full_css_path; - - // Verify the CSS file exists before trying to enqueue it - if ( ! file_exists( $css_full_path ) ) { - return; - } - - // We need to use plugins_url for reliable URL generation - $file_url = plugins_url( - $full_css_path, - $package_path . '/package.json' - ); - - // Use the file's modification time for more precise cache busting - $file_version = file_exists( $css_full_path ) ? filemtime( $css_full_path ) : Package::VERSION; - - wp_enqueue_style( - $handle, - $file_url, - array(), - $file_version // Use file modification time for cache busting - ); - } - /** * Replaces the search query with the corrected query in the title. * @@ -191,14 +145,4 @@ private function get_corrected_query_html() { $message ); } - - /** - * Get the search result from the Inline_Search instance. - * - * @return array|\WP_Error|null The search result or null if not available. - */ - private function get_search_result() { - $inline_search = Inline_Search::instance(); - return $inline_search->get_search_result(); - } } From 595b5c3a750434d224c50d7721b1174ec4b82413 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Thu, 8 May 2025 17:15:08 +0100 Subject: [PATCH 40/42] Fix linter notices --- .../packages/search/src/inline-search/styles/colophon.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/packages/search/src/inline-search/styles/colophon.scss b/projects/packages/search/src/inline-search/styles/colophon.scss index d6289533c3937..ae822c9667000 100644 --- a/projects/packages/search/src/inline-search/styles/colophon.scss +++ b/projects/packages/search/src/inline-search/styles/colophon.scss @@ -65,7 +65,9 @@ $colophon-logo-margin-right: 4px; .wp-theme-varia-wpcom, .wp-theme-pubvaria { + @media only screen and (min-width: 768px) { + .jetpack-search-inline-colophon { margin: 0 auto; max-width: calc(782px - 32px); @@ -88,6 +90,7 @@ $colophon-logo-margin-right: 4px; } } } + .wp-theme-twentynineteen, .wp-theme-pubtwentynineteen { @@ -115,16 +118,18 @@ $colophon-logo-margin-right: 4px; } .wp-theme-generatepress { + .jetpack-search-inline-colophon { display: block; margin-top: 2rem; clear: both; } } + .wp-theme-hevor { + .jetpack-search-inline-colophon { justify-content: flex-start; max-width: var(--wp--style--global--wide-size); } } - From 0da3a8709328330d00fa972b2ffe91915b4c9c3f Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Fri, 9 May 2025 15:59:45 +0100 Subject: [PATCH 41/42] Rename highlighter class to reflect Inline Search --- ...ch-highlighter.php => class-inline-search-highlighter.php} | 2 +- .../packages/search/src/inline-search/class-inline-search.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename projects/packages/search/src/inline-search/{class-search-highlighter.php => class-inline-search-highlighter.php} (99%) diff --git a/projects/packages/search/src/inline-search/class-search-highlighter.php b/projects/packages/search/src/inline-search/class-inline-search-highlighter.php similarity index 99% rename from projects/packages/search/src/inline-search/class-search-highlighter.php rename to projects/packages/search/src/inline-search/class-inline-search-highlighter.php index a13046fbf6414..489ade9fae6aa 100644 --- a/projects/packages/search/src/inline-search/class-search-highlighter.php +++ b/projects/packages/search/src/inline-search/class-inline-search-highlighter.php @@ -10,7 +10,7 @@ /** * Search Highlighter class */ -class Search_Highlighter { +class Inline_Search_Highlighter { /** * Stores highlighted content from search results. * diff --git a/projects/packages/search/src/inline-search/class-inline-search.php b/projects/packages/search/src/inline-search/class-inline-search.php index 409f5a005b980..9d1dd132987d8 100644 --- a/projects/packages/search/src/inline-search/class-inline-search.php +++ b/projects/packages/search/src/inline-search/class-inline-search.php @@ -21,7 +21,7 @@ class Inline_Search extends Classic_Search { /** * The Search Highlighter instance. * - * @var Search_Highlighter|null + * @var Inline_Search_Highlighter|null * @since $$next-version$$ */ private $highlighter; @@ -495,7 +495,7 @@ private function process_search_results() { } $this->search_result_ids = $post_ids; - $this->highlighter = new Search_Highlighter( $post_ids ); + $this->highlighter = new Inline_Search_Highlighter( $post_ids ); if ( ! empty( $highlighted_results ) ) { // Format highlight data for the highlighter From f49a2e633943018a9c7bb30f9717b8fe1503e635 Mon Sep 17 00:00:00 2001 From: Katja Paavola Date: Fri, 9 May 2025 16:02:44 +0100 Subject: [PATCH 42/42] Fix tests --- .../search/tests/php/Inline_Search_Test.php | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/projects/packages/search/tests/php/Inline_Search_Test.php b/projects/packages/search/tests/php/Inline_Search_Test.php index fd89e47854152..ff59916896ece 100644 --- a/projects/packages/search/tests/php/Inline_Search_Test.php +++ b/projects/packages/search/tests/php/Inline_Search_Test.php @@ -104,13 +104,13 @@ public static function data_provider(): array { 'post_type' => 'any', ), 'expected_api_args' => array( - 'size' => 5, - 'from' => 0, - 'fields' => array( 'post_id' ), - 'query' => 'hello_world', - 'sort' => 'score_recency', - 'langs' => array( 'en_US' ), - 'filter' => array( + 'size' => '5', + 'from' => '0', + 'fields' => array( 'post_id' ), + 'query' => 'hello_world', + 'sort' => 'score_recency', + 'langs' => array( 'en_US' ), + 'filter' => array( 'bool' => array( 'must' => array( array( @@ -121,6 +121,7 @@ public static function data_provider(): array { ), ), ), + 'highlight' => array( 'fields' => array( 'post_title', 'post_content' ) ), ), ), 'only_posts' => array( @@ -130,13 +131,13 @@ public static function data_provider(): array { 'post_type' => 'post', ), 'expected_api_args' => array( - 'size' => 5, - 'from' => 0, - 'fields' => array( 'post_id' ), - 'query' => 'only search posts', - 'sort' => 'score_recency', - 'langs' => array( 'en_US' ), - 'filter' => array( + 'size' => '5', + 'from' => '0', + 'fields' => array( 'post_id' ), + 'query' => 'only search posts', + 'sort' => 'score_recency', + 'langs' => array( 'en_US' ), + 'filter' => array( 'bool' => array( 'must' => array( array( @@ -147,6 +148,7 @@ public static function data_provider(): array { ), ), ), + 'highlight' => array( 'fields' => array( 'post_title', 'post_content' ) ), ), ), 'sort_by_date_asc' => array( @@ -158,13 +160,13 @@ public static function data_provider(): array { 'orderby' => 'date', ), 'expected_api_args' => array( - 'size' => 5, - 'from' => 0, - 'fields' => array( 'post_id' ), - 'query' => 'search by date descending', - 'sort' => 'date_asc', - 'langs' => array( 'en_US' ), - 'filter' => array( + 'size' => '5', + 'from' => '0', + 'fields' => array( 'post_id' ), + 'query' => 'search by date descending', + 'sort' => 'date_asc', + 'langs' => array( 'en_US' ), + 'filter' => array( 'bool' => array( 'must' => array( array( @@ -175,6 +177,7 @@ public static function data_provider(): array { ), ), ), + 'highlight' => array( 'fields' => array( 'post_title', 'post_content' ) ), ), ), );