|
1 <?php |
|
2 /** |
|
3 * Interactivity API: WP_Interactivity_API_Directives_Processor class. |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Interactivity API |
|
7 * @since 6.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Class used to iterate over the tags of an HTML string and help process the |
|
12 * directive attributes. |
|
13 * |
|
14 * @since 6.5.0 |
|
15 * |
|
16 * @access private |
|
17 */ |
|
18 final class WP_Interactivity_API_Directives_Processor extends WP_HTML_Tag_Processor { |
|
19 /** |
|
20 * List of tags whose closer tag is not visited by the WP_HTML_Tag_Processor. |
|
21 * |
|
22 * @since 6.5.0 |
|
23 * @var string[] |
|
24 */ |
|
25 const TAGS_THAT_DONT_VISIT_CLOSER_TAG = array( |
|
26 'SCRIPT', |
|
27 'IFRAME', |
|
28 'NOEMBED', |
|
29 'NOFRAMES', |
|
30 'STYLE', |
|
31 'TEXTAREA', |
|
32 'TITLE', |
|
33 'XMP', |
|
34 ); |
|
35 |
|
36 /** |
|
37 * Returns the content between two balanced template tags. |
|
38 * |
|
39 * It positions the cursor in the closer tag of the balanced template tag, |
|
40 * if it exists. |
|
41 * |
|
42 * @since 6.5.0 |
|
43 * |
|
44 * @access private |
|
45 * |
|
46 * @return string|null The content between the current opener template tag and its matching closer tag or null if it |
|
47 * doesn't find the matching closing tag or the current tag is not a template opener tag. |
|
48 */ |
|
49 public function get_content_between_balanced_template_tags() { |
|
50 if ( 'TEMPLATE' !== $this->get_tag() ) { |
|
51 return null; |
|
52 } |
|
53 |
|
54 $positions = $this->get_after_opener_tag_and_before_closer_tag_positions(); |
|
55 if ( ! $positions ) { |
|
56 return null; |
|
57 } |
|
58 list( $after_opener_tag, $before_closer_tag ) = $positions; |
|
59 |
|
60 return substr( $this->html, $after_opener_tag, $before_closer_tag - $after_opener_tag ); |
|
61 } |
|
62 |
|
63 /** |
|
64 * Sets the content between two balanced tags. |
|
65 * |
|
66 * @since 6.5.0 |
|
67 * |
|
68 * @access private |
|
69 * |
|
70 * @param string $new_content The string to replace the content between the matching tags. |
|
71 * @return bool Whether the content was successfully replaced. |
|
72 */ |
|
73 public function set_content_between_balanced_tags( string $new_content ): bool { |
|
74 $positions = $this->get_after_opener_tag_and_before_closer_tag_positions( true ); |
|
75 if ( ! $positions ) { |
|
76 return false; |
|
77 } |
|
78 list( $after_opener_tag, $before_closer_tag ) = $positions; |
|
79 |
|
80 $this->lexical_updates[] = new WP_HTML_Text_Replacement( |
|
81 $after_opener_tag, |
|
82 $before_closer_tag - $after_opener_tag, |
|
83 esc_html( $new_content ) |
|
84 ); |
|
85 |
|
86 return true; |
|
87 } |
|
88 |
|
89 /** |
|
90 * Appends content after the closing tag of a template tag. |
|
91 * |
|
92 * It positions the cursor in the closer tag of the balanced template tag, |
|
93 * if it exists. |
|
94 * |
|
95 * @access private |
|
96 * |
|
97 * @param string $new_content The string to append after the closing template tag. |
|
98 * @return bool Whether the content was successfully appended. |
|
99 */ |
|
100 public function append_content_after_template_tag_closer( string $new_content ): bool { |
|
101 if ( empty( $new_content ) || 'TEMPLATE' !== $this->get_tag() || ! $this->is_tag_closer() ) { |
|
102 return false; |
|
103 } |
|
104 |
|
105 // Flushes any changes. |
|
106 $this->get_updated_html(); |
|
107 |
|
108 $bookmark = 'append_content_after_template_tag_closer'; |
|
109 $this->set_bookmark( $bookmark ); |
|
110 $after_closing_tag = $this->bookmarks[ $bookmark ]->start + $this->bookmarks[ $bookmark ]->length; |
|
111 $this->release_bookmark( $bookmark ); |
|
112 |
|
113 // Appends the new content. |
|
114 $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closing_tag, 0, $new_content ); |
|
115 |
|
116 return true; |
|
117 } |
|
118 |
|
119 /** |
|
120 * Gets the positions right after the opener tag and right before the closer |
|
121 * tag in a balanced tag. |
|
122 * |
|
123 * By default, it positions the cursor in the closer tag of the balanced tag. |
|
124 * If $rewind is true, it seeks back to the opener tag. |
|
125 * |
|
126 * @since 6.5.0 |
|
127 * |
|
128 * @access private |
|
129 * |
|
130 * @param bool $rewind Optional. Whether to seek back to the opener tag after finding the positions. Defaults to false. |
|
131 * @return array|null Start and end byte position, or null when no balanced tag bookmarks. |
|
132 */ |
|
133 private function get_after_opener_tag_and_before_closer_tag_positions( bool $rewind = false ) { |
|
134 // Flushes any changes. |
|
135 $this->get_updated_html(); |
|
136 |
|
137 $bookmarks = $this->get_balanced_tag_bookmarks(); |
|
138 if ( ! $bookmarks ) { |
|
139 return null; |
|
140 } |
|
141 list( $opener_tag, $closer_tag ) = $bookmarks; |
|
142 |
|
143 $after_opener_tag = $this->bookmarks[ $opener_tag ]->start + $this->bookmarks[ $opener_tag ]->length; |
|
144 $before_closer_tag = $this->bookmarks[ $closer_tag ]->start; |
|
145 |
|
146 if ( $rewind ) { |
|
147 $this->seek( $opener_tag ); |
|
148 } |
|
149 |
|
150 $this->release_bookmark( $opener_tag ); |
|
151 $this->release_bookmark( $closer_tag ); |
|
152 |
|
153 return array( $after_opener_tag, $before_closer_tag ); |
|
154 } |
|
155 |
|
156 /** |
|
157 * Returns a pair of bookmarks for the current opener tag and the matching |
|
158 * closer tag. |
|
159 * |
|
160 * It positions the cursor in the closer tag of the balanced tag, if it |
|
161 * exists. |
|
162 * |
|
163 * @since 6.5.0 |
|
164 * |
|
165 * @return array|null A pair of bookmarks, or null if there's no matching closing tag. |
|
166 */ |
|
167 private function get_balanced_tag_bookmarks() { |
|
168 static $i = 0; |
|
169 $opener_tag = 'opener_tag_of_balanced_tag_' . ++$i; |
|
170 |
|
171 $this->set_bookmark( $opener_tag ); |
|
172 if ( ! $this->next_balanced_tag_closer_tag() ) { |
|
173 $this->release_bookmark( $opener_tag ); |
|
174 return null; |
|
175 } |
|
176 |
|
177 $closer_tag = 'closer_tag_of_balanced_tag_' . ++$i; |
|
178 $this->set_bookmark( $closer_tag ); |
|
179 |
|
180 return array( $opener_tag, $closer_tag ); |
|
181 } |
|
182 |
|
183 /** |
|
184 * Skips processing the content between tags. |
|
185 * |
|
186 * It positions the cursor in the closer tag of the foreign element, if it |
|
187 * exists. |
|
188 * |
|
189 * This function is intended to skip processing SVG and MathML inner content |
|
190 * instead of bailing out the whole processing. |
|
191 * |
|
192 * @since 6.5.0 |
|
193 * |
|
194 * @access private |
|
195 * |
|
196 * @return bool Whether the foreign content was successfully skipped. |
|
197 */ |
|
198 public function skip_to_tag_closer(): bool { |
|
199 $depth = 1; |
|
200 $tag_name = $this->get_tag(); |
|
201 |
|
202 while ( $depth > 0 && $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) { |
|
203 if ( ! $this->is_tag_closer() && $this->get_attribute_names_with_prefix( 'data-wp-' ) ) { |
|
204 /* translators: 1: SVG or MATH HTML tag. */ |
|
205 $message = sprintf( __( 'Interactivity directives were detected inside an incompatible %1$s tag. These directives will be ignored in the server side render.' ), $tag_name ); |
|
206 _doing_it_wrong( __METHOD__, $message, '6.6.0' ); |
|
207 } |
|
208 if ( $this->get_tag() === $tag_name ) { |
|
209 if ( $this->has_self_closing_flag() ) { |
|
210 continue; |
|
211 } |
|
212 $depth += $this->is_tag_closer() ? -1 : 1; |
|
213 } |
|
214 } |
|
215 |
|
216 return 0 === $depth; |
|
217 } |
|
218 |
|
219 /** |
|
220 * Finds the matching closing tag for an opening tag. |
|
221 * |
|
222 * When called while the processor is on an open tag, it traverses the HTML |
|
223 * until it finds the matching closer tag, respecting any in-between content, |
|
224 * including nested tags of the same name. Returns false when called on a |
|
225 * closer tag, a tag that doesn't have a closer tag (void), a tag that |
|
226 * doesn't visit the closer tag, or if no matching closing tag was found. |
|
227 * |
|
228 * @since 6.5.0 |
|
229 * |
|
230 * @access private |
|
231 * |
|
232 * @return bool Whether a matching closing tag was found. |
|
233 */ |
|
234 public function next_balanced_tag_closer_tag(): bool { |
|
235 $depth = 0; |
|
236 $tag_name = $this->get_tag(); |
|
237 |
|
238 if ( ! $this->has_and_visits_its_closer_tag() ) { |
|
239 return false; |
|
240 } |
|
241 |
|
242 while ( $this->next_tag( |
|
243 array( |
|
244 'tag_name' => $tag_name, |
|
245 'tag_closers' => 'visit', |
|
246 ) |
|
247 ) ) { |
|
248 if ( ! $this->is_tag_closer() ) { |
|
249 ++$depth; |
|
250 continue; |
|
251 } |
|
252 |
|
253 if ( 0 === $depth ) { |
|
254 return true; |
|
255 } |
|
256 |
|
257 --$depth; |
|
258 } |
|
259 |
|
260 return false; |
|
261 } |
|
262 |
|
263 /** |
|
264 * Checks whether the current tag has and will visit its matching closer tag. |
|
265 * |
|
266 * @since 6.5.0 |
|
267 * |
|
268 * @access private |
|
269 * |
|
270 * @return bool Whether the current tag has a closer tag. |
|
271 */ |
|
272 public function has_and_visits_its_closer_tag(): bool { |
|
273 $tag_name = $this->get_tag(); |
|
274 |
|
275 return null !== $tag_name && ( |
|
276 ! WP_HTML_Processor::is_void( $tag_name ) && |
|
277 ! in_array( $tag_name, self::TAGS_THAT_DONT_VISIT_CLOSER_TAG, true ) |
|
278 ); |
|
279 } |
|
280 } |