Skip to content

Commit 6a68156

Browse files
anaemnesismatticbot
authored andcommitted
Inline Search: Highlight searched term in returned results (#43110)
* Add Search Result Highlighting - Adds search result highlighting using the same <mark> 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. * changelog * Reorganise class to better group methods * First pass at highlighting search corrections * Move to a simpler highlighting solution as the previous was overengineered * Refactor highlighting handling to better leverage the 1.3api * Remove excerpt highlighting as unnecessary at this time * Let us move highlighting to a class of its own * We don't need this require * Use highlight data returned from the API instead of building our own * Address PHPStan * Highlight content returned without p tags When the API sends highlighted data back, it's returned without the expected <p> tags that would wrap content. Let's add those back. * Cleanup related refactor * Rename highlighter class to reflect Inline Search * Fix tests * Fix @var for highlighter to match new class name * Fix reversion to highlighting and associated tests * Revise content and excerpt approach - Add new method to return modified excerpt block w/ highlighting. - Add new method to return modified content block w/ highlighting. - Retain filter_* methods for classic theme compatibility. * New Content block approach Attempt a new Content block approach that preserves the original content block render, while applying highlighting. This is the difference between returning the API response as an excerpt when rendering the content block, and actually rendering the content block. * Cleanup * Iterate on the filter_render_content_block() method * Remove unnecessary if/foreach * No more implicit nullable parameter * Remove unnecessary getter * Remove unneeded method - Use the 'excerpt' highlighting method for both excerpts and content - Remove default parameters from two add_filters() since they're the default * Rename method * We were not actually using * Update comment * Update inline filters test * Remove content highlighting for excerpt highlighting exclusively * Remove comment highlighting as too problematic across themes * This method doesn't exist anymore Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/15069599825 Upstream-Ref: Automattic/jetpack@2e78034
1 parent fd67934 commit 6a68156

File tree

5 files changed

+338
-31
lines changed

5 files changed

+338
-31
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.49.1-alpha] - unreleased
8+
## [0.50.0-alpha] - unreleased
99

1010
This is an alpha version! The changes listed here are not final.
1111

12+
### Added
13+
- Add highlighting of search term in returned search results.
14+
1215
### Changed
1316
- Update package dependencies.
1417

@@ -1225,7 +1228,7 @@ This is an alpha version! The changes listed here are not final.
12251228
- Updated package dependencies.
12261229
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
12271230

1228-
[0.49.1-alpha]: https://github.com/Automattic/jetpack-search/compare/v0.49.0...v0.49.1-alpha
1231+
[0.50.0-alpha]: https://github.com/Automattic/jetpack-search/compare/v0.49.0...v0.50.0-alpha
12291232
[0.49.0]: https://github.com/Automattic/jetpack-search/compare/v0.48.0...v0.49.0
12301233
[0.48.0]: https://github.com/Automattic/jetpack-search/compare/v0.47.24...v0.48.0
12311234
[0.47.24]: https://github.com/Automattic/jetpack-search/compare/v0.47.23...v0.47.24

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"link-template": "https://github.com/Automattic/jetpack-search/compare/v${old}...v${new}"
6464
},
6565
"branch-alias": {
66-
"dev-trunk": "0.49.x-dev"
66+
"dev-trunk": "0.50.x-dev"
6767
},
6868
"version-constants": {
6969
"::VERSION": "src/class-package.php"

src/class-package.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Search package general information
1212
*/
1313
class Package {
14-
const VERSION = '0.49.1-alpha';
14+
const VERSION = '0.50.0-alpha';
1515
const SLUG = 'search';
1616

1717
/**
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
/**
3+
* Search Highlighter: Handles highlighting of search terms in content
4+
*
5+
* @package automattic/jetpack-search
6+
*/
7+
8+
namespace Automattic\Jetpack\Search;
9+
10+
/**
11+
* Search Highlighter class
12+
*/
13+
class Inline_Search_Highlighter {
14+
/**
15+
* Stores highlighted content from search results.
16+
*
17+
* @var array
18+
*/
19+
private $highlighted_content;
20+
21+
/**
22+
* Stores the list of post IDs that are actual search results.
23+
*
24+
* @var array
25+
*/
26+
private $search_result_ids;
27+
28+
/**
29+
* Constructor
30+
*
31+
* @param array $search_result_ids Array of post IDs from search results.
32+
* @param array|null $results Optional. The search result data from the API to process immediately.
33+
*/
34+
public function __construct( array $search_result_ids = array(), ?array $results = null ) {
35+
$this->search_result_ids = $search_result_ids;
36+
$this->highlighted_content = array();
37+
38+
// Process API results immediately if provided
39+
if ( $results !== null ) {
40+
$this->process_results( $results );
41+
}
42+
}
43+
44+
/**
45+
* Set up the WordPress filters for highlighting.
46+
*/
47+
public function setup(): void {
48+
add_filter( 'the_title', array( $this, 'filter_highlighted_title' ), 10, 2 );
49+
add_filter( 'the_excerpt', array( $this, 'filter_highlighted_excerpt' ) );
50+
add_filter( 'render_block_core/post-excerpt', array( $this, 'filter_render_highlighted_block' ), 10, 3 );
51+
}
52+
53+
/**
54+
* Process highlighting data for search results.
55+
*
56+
* @param array $results The search result data from the API.
57+
*/
58+
public function process_results( array $results ): void {
59+
$this->highlighted_content = array();
60+
61+
if ( empty( $results ) ) {
62+
return;
63+
}
64+
65+
foreach ( $results as $result ) {
66+
$post_id = (int) ( $result['fields']['post_id'] ?? 0 );
67+
$this->process_result_highlighting( $result, $post_id );
68+
}
69+
}
70+
71+
/**
72+
* Filter the post title to show highlighted version.
73+
*
74+
* @param string $title The post title.
75+
* @param int $post_id The post ID.
76+
*
77+
* @return string The filtered title.
78+
*/
79+
public function filter_highlighted_title( string $title, int $post_id ): string {
80+
if ( ! $this->is_search_result( $post_id ) ) {
81+
return $title;
82+
}
83+
84+
if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) {
85+
return $this->highlighted_content[ $post_id ]['title'];
86+
}
87+
88+
return $title;
89+
}
90+
91+
/**
92+
* Filter the post content to show highlighted version.
93+
*
94+
* @param string $content The post content.
95+
*
96+
* @return string The filtered content.
97+
*/
98+
public function filter_highlighted_excerpt( string $content ): string {
99+
$post_id = get_the_ID();
100+
101+
if ( ! $this->is_search_result( $post_id ) ) {
102+
return $content;
103+
}
104+
105+
// Skip highlighting if we're in a block theme context in favour of filter_render_highlighted_block().
106+
if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
107+
return $content;
108+
}
109+
110+
if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) {
111+
return wpautop( $this->highlighted_content[ $post_id ]['content'] );
112+
}
113+
114+
return $content;
115+
}
116+
117+
/**
118+
* Process highlighting data for a single search result.
119+
*
120+
* @param array $result The search result data from the API.
121+
* @param int $post_id The post ID for this result.
122+
*/
123+
private function process_result_highlighting( array $result, int $post_id ): void {
124+
if ( empty( $result['highlight'] ) ) {
125+
return;
126+
}
127+
128+
$title = $this->extract_highlight_field( $result, 'title' );
129+
$content = $this->extract_highlight_field( $result, 'content' );
130+
131+
$this->highlighted_content[ $post_id ] = array(
132+
'title' => $title,
133+
'content' => $content,
134+
);
135+
}
136+
137+
/**
138+
* Extract a highlight field from the search result, handling different field formats.
139+
*
140+
* @param array $result The search result data from the API.
141+
* @param string $field The field name to extract.
142+
*
143+
* @return string The extracted highlighted field.
144+
*/
145+
private function extract_highlight_field( array $result, string $field ): string {
146+
if ( isset( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) {
147+
return $result['highlight'][ $field ][0];
148+
}
149+
150+
// Try field variants with suffixes (e.g., 'title.default') if no direct match found.
151+
foreach ( $result['highlight'] as $key => $value ) {
152+
if ( str_starts_with( $key, $field . '.' ) ) {
153+
if ( is_array( $value ) && ! empty( $value ) ) {
154+
return $value[0];
155+
}
156+
}
157+
}
158+
159+
return '';
160+
}
161+
162+
/**
163+
* Check if the current post is a search result from our API
164+
*
165+
* @param int $post_id The post ID to check.
166+
*
167+
* @return bool Whether the post is a search result.
168+
*/
169+
public function is_search_result( int $post_id ): bool {
170+
return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true );
171+
}
172+
173+
/**
174+
* Filter for rendering highlighted content when highlighting is returned from the API.
175+
*
176+
* @param string $block_content The block content.
177+
* @param array $block The block data.
178+
* @param object $instance The block instance.
179+
*
180+
* @return string The filtered block content.
181+
* @since 0.50.0-alpha
182+
*/
183+
public function filter_render_highlighted_block( string $block_content, array $block, object $instance ): string {
184+
if ( ! isset( $instance->context['postId'] ) || ! $this->is_search_result( $instance->context['postId'] ) ) {
185+
return $block_content;
186+
}
187+
188+
$highlighted_content = $this->highlighted_content[ $instance->context['postId'] ] ?? null;
189+
190+
// If we don't have any highlighted content or comments, return the original block content
191+
if ( empty( $highlighted_content['content'] ) && empty( $highlighted_content['comments'] ) ) {
192+
return $block_content;
193+
}
194+
195+
// Start with the content highlights if available
196+
if ( ! empty( $highlighted_content['content'] ) ) {
197+
$block_content = wpautop( $highlighted_content['content'] );
198+
}
199+
200+
// Append comment highlights if available
201+
if ( ! empty( $highlighted_content['comments'] ) ) {
202+
$block_content .= ' ... ' . $highlighted_content['comments'];
203+
}
204+
205+
// Handle more text display if needed
206+
$more_text = ! empty( $block['attrs']['moreText'] ) ? '<a class="wp-block-post-excerpt__more-text">' . $block['attrs']['moreText'] . '</a>' : '';
207+
208+
$classes = array();
209+
if ( isset( $block['attrs']['textAlign'] ) ) {
210+
$classes[] = 'has-text-align-' . $block['attrs']['textAlign'];
211+
}
212+
213+
if ( isset( $block['attrs']['style']['elements']['link']['color']['text'] ) ) {
214+
$classes[] = 'has-link-color';
215+
}
216+
217+
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );
218+
219+
// Determine if we should show more text on new line based on block attributes
220+
$show_more_on_new_line = ! isset( $block['attrs']['showMoreOnNewLine'] ) || $block['attrs']['showMoreOnNewLine'];
221+
222+
if ( $show_more_on_new_line && ! empty( $more_text ) ) {
223+
$block_content .= '</p><p class="wp-block-post-excerpt__more-text">' . $more_text . '</p>';
224+
} elseif ( ! empty( $more_text ) ) {
225+
$block_content .= " $more_text</p>";
226+
}
227+
228+
return sprintf( '<div %1$s>%2$s</div>', $wrapper_attributes, $block_content );
229+
}
230+
}

0 commit comments

Comments
 (0)