PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-search / src / inline-search / class-inline-search-highlighter.php

class-inline-search-highlighter.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3, at jetpack_vendor/automattic/jetpack-search/src/inline-search/class-inline-search-highlighter.php

251 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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