PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.1-a.5
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-search / src / inline-search / class-inline-search-highlighter.php
jetpack / jetpack_vendor / automattic / jetpack-search / src / inline-search Last commit date
class-inline-search-correction.php 9 months ago class-inline-search-highlighter.php 9 months ago class-inline-search.php 4 weeks ago
class-inline-search-highlighter.php
251 lines
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|null $post_id Optional. The post ID. Default null.
76 *
77 * @return string The filtered title.
78 */
79 public function filter_highlighted_title( string $title, ?int $post_id = null ): string {
80 $incompatible_themes = array(
81 'blaskan', // Blaskan incorrectly uses wp_nav_menu() in body_class filter, causing infinite recursion.
82 'p2020', // P2 loads o2 as a plugin, which calls the_title() filter incorrectly.
83 );
84
85 $theme = wp_get_theme();
86 $theme_name = strtolower( $theme->get( 'Name' ) );
87 $theme_template = strtolower( $theme->get_template() );
88
89 foreach ( $incompatible_themes as $incompatible_theme ) {
90 if ( $theme_name === $incompatible_theme || $theme_template === $incompatible_theme ) {
91 return $title;
92 }
93 }
94
95 if ( ! $this->is_search_result( $post_id ) ) {
96 return $title;
97 }
98
99 if ( ! empty( $this->highlighted_content[ $post_id ]['title'] ) ) {
100 return $this->highlighted_content[ $post_id ]['title'];
101 }
102
103 return $title;
104 }
105
106 /**
107 * Filter the post content to show highlighted version.
108 *
109 * @param string $content The post content.
110 *
111 * @return string The filtered content.
112 */
113 public function filter_highlighted_excerpt( string $content ): string {
114 $post_id = get_the_ID();
115
116 if ( ! $this->is_search_result( $post_id ) ) {
117 return $content;
118 }
119
120 // Skip highlighting if we're in a block theme context in favour of filter_render_highlighted_block().
121 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
122 return $content;
123 }
124
125 if ( ! empty( $this->highlighted_content[ $post_id ]['content'] ) ) {
126 return wpautop( $this->highlighted_content[ $post_id ]['content'] );
127 }
128
129 return $content;
130 }
131
132 /**
133 * Process highlighting data for a single search result.
134 *
135 * @param array $result The search result data from the API.
136 * @param int $post_id The post ID for this result.
137 */
138 private function process_result_highlighting( array $result, int $post_id ): void {
139 if ( empty( $result['highlight'] ) ) {
140 return;
141 }
142
143 $title = $this->extract_highlight_field( $result, 'title' );
144 $content = $this->extract_highlight_field( $result, 'content' );
145
146 $this->highlighted_content[ $post_id ] = array(
147 'title' => $title,
148 'content' => $content,
149 );
150 }
151
152 /**
153 * Extract a highlight field from the search result, handling different field formats.
154 *
155 * @param array $result The search result data from the API.
156 * @param string $field The field name to extract.
157 *
158 * @return string The extracted highlighted field.
159 */
160 private function extract_highlight_field( array $result, string $field ): string {
161 if ( isset( $result['highlight'][ $field ] ) && is_array( $result['highlight'][ $field ] ) ) {
162 return $result['highlight'][ $field ][0];
163 }
164
165 // Try field variants with suffixes (e.g., 'title.default') if no direct match found.
166 foreach ( $result['highlight'] as $key => $value ) {
167 if ( str_starts_with( $key, $field . '.' ) ) {
168 if ( is_array( $value ) && ! empty( $value ) ) {
169 return $value[0];
170 }
171 }
172 }
173
174 return '';
175 }
176
177 /**
178 * Check if the current post is a search result from our API
179 *
180 * @param int|null $post_id The post ID to check.
181 *
182 * @return bool Whether the post is a search result.
183 */
184 public function is_search_result( ?int $post_id ): bool {
185 // o2 is initially returning null due to mishandling of the_title() filter.
186 if ( null === $post_id ) {
187 return false;
188 }
189
190 return is_search() && in_the_loop() && ! empty( $this->search_result_ids ) && in_array( $post_id, $this->search_result_ids, true );
191 }
192
193 /**
194 * Filter for rendering highlighted content when highlighting is returned from the API.
195 *
196 * @param string $block_content The block content.
197 * @param array $block The block data.
198 * @param object $instance The block instance.
199 *
200 * @return string The filtered block content.
201 * @since 0.50.0
202 */
203 public function filter_render_highlighted_block( string $block_content, array $block, object $instance ): string {
204 if ( ! isset( $instance->context['postId'] ) || ! $this->is_search_result( $instance->context['postId'] ) ) {
205 return $block_content;
206 }
207
208 $highlighted_content = $this->highlighted_content[ $instance->context['postId'] ] ?? null;
209
210 // If we don't have any highlighted content or comments, return the original block content
211 if ( empty( $highlighted_content['content'] ) && empty( $highlighted_content['comments'] ) ) {
212 return $block_content;
213 }
214
215 // Start with the content highlights if available
216 if ( ! empty( $highlighted_content['content'] ) ) {
217 $block_content = wpautop( $highlighted_content['content'] );
218 }
219
220 // Append comment highlights if available
221 if ( ! empty( $highlighted_content['comments'] ) ) {
222 $block_content .= ' ... ' . $highlighted_content['comments'];
223 }
224
225 // Handle more text display if needed
226 $more_text = ! empty( $block['attrs']['moreText'] ) ? '<a class="wp-block-post-excerpt__more-text">' . $block['attrs']['moreText'] . '</a>' : '';
227
228 $classes = array();
229 if ( isset( $block['attrs']['textAlign'] ) ) {
230 $classes[] = 'has-text-align-' . $block['attrs']['textAlign'];
231 }
232
233 if ( isset( $block['attrs']['style']['elements']['link']['color']['text'] ) ) {
234 $classes[] = 'has-link-color';
235 }
236
237 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );
238
239 // Determine if we should show more text on new line based on block attributes
240 $show_more_on_new_line = ! isset( $block['attrs']['showMoreOnNewLine'] ) || $block['attrs']['showMoreOnNewLine'];
241
242 if ( $show_more_on_new_line && ! empty( $more_text ) ) {
243 $block_content .= '</p><p class="wp-block-post-excerpt__more-text">' . $more_text . '</p>';
244 } elseif ( ! empty( $more_text ) ) {
245 $block_content .= " $more_text</p>";
246 }
247
248 return sprintf( '<div %1$s>%2$s</div>', $wrapper_attributes, $block_content );
249 }
250 }
251