PluginProbe
Gutenberg / 17.2.1
Gutenberg v17.2.1
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / experimental / interactivity-api / class-wp-directive-processor.php

class-wp-directive-processor.php in Gutenberg 17.2.1, at lib/experimental/interactivity-api/class-wp-directive-processor.php

316 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP_Directive_Processor class
4 *
5 * @package Gutenberg
6 * @subpackage Interactivity API
7 */
8
9 if ( class_exists( 'WP_Directive_Processor' ) ) {
10 return;
11 }
12
13 /**
14 * This processor is built on top of the HTML Tag Processor and augments its
15 * capabilities to process the Interactivity API directives.
16 *
17 * IMPORTANT DISCLAIMER: This code is highly experimental and its only purpose
18 * is to provide a way to test the server-side rendering of the Interactivity
19 * API. Most of this code will be discarded once the HTML Processor is
20 * available. Please restrain from investing unnecessary time and effort trying
21 * to improve this code.
22 */
23 class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 {
24
25 /**
26 * An array of root blocks.
27 *
28 * @var array
29 */
30 public static $root_block = null;
31
32 /**
33 * Add a root block to the variable.
34 *
35 * @param array $block The block to add.
36 *
37 * @return void
38 */
39 public static function mark_root_block( $block ) {
40 self::$root_block = md5( serialize( $block ) );
41 }
42
43 /**
44 * Remove a root block to the variable.
45 *
46 * @return void
47 */
48 public static function unmark_root_block() {
49 self::$root_block = null;
50 }
51
52 /**
53 * Check if block is a root block.
54 *
55 * @param array $block The block to check.
56 *
57 * @return bool True if block is a root block, false otherwise.
58 */
59 public static function is_marked_as_root_block( $block ) {
60 return md5( serialize( $block ) ) === self::$root_block;
61 }
62
63 /**
64 * Check if a root block has already been defined.
65 *
66 * @return bool True if block is a root block, false otherwise.
67 */
68 public static function has_root_block() {
69 return isset( self::$root_block );
70 }
71
72
73 /**
74 * Find the matching closing tag for an opening tag.
75 *
76 * When called while on an open tag, traverse the HTML until we find the
77 * matching closing tag, respecting any in-between content, including nested
78 * tags of the same name. Return false when called on a closing or void tag,
79 * or if no matching closing tag was found.
80 *
81 * @return bool Whether a matching closing tag was found.
82 */
83 public function next_balanced_closer() {
84 $depth = 0;
85
86 $tag_name = $this->get_tag();
87
88 if ( self::is_html_void_element( $tag_name ) ) {
89 return false;
90 }
91
92 while ( $this->next_tag(
93 array(
94 'tag_name' => $tag_name,
95 'tag_closers' => 'visit',
96 )
97 ) ) {
98 if ( ! $this->is_tag_closer() ) {
99 ++$depth;
100 continue;
101 }
102
103 if ( 0 === $depth ) {
104 return true;
105 }
106
107 --$depth;
108 }
109
110 return false;
111 }
112
113 /**
114 * Traverses the HTML searching for Interactivity API directives and processing
115 * them.
116 *
117 * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor.
118 * @param string $prefix Attribute prefix.
119 * @param string[] $directives Directives.
120 *
121 * @return WP_Directive_Processor The modified instance of the
122 * WP_Directive_Processor.
123 */
124 public function process_rendered_html( $tags, $prefix, $directives ) {
125 $context = new WP_Directive_Context();
126 $tag_stack = array();
127
128 while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
129 $tag_name = $tags->get_tag();
130
131 // Is this a tag that closes the latest opening tag?
132 if ( $tags->is_tag_closer() ) {
133 if ( 0 === count( $tag_stack ) ) {
134 continue;
135 }
136
137 list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
138 if ( $latest_opening_tag_name === $tag_name ) {
139 array_pop( $tag_stack );
140
141 // If the matching opening tag didn't have any directives, we move on.
142 if ( 0 === count( $attributes ) ) {
143 continue;
144 }
145 }
146 } else {
147 $attributes = array();
148 foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
149 /*
150 * Removes the part after the double hyphen before looking for
151 * the directive processor inside `$directives`, e.g., "wp-bind"
152 * from "wp-bind--src" and "wp-context" from "wp-context" etc...
153 */
154 list( $type ) = WP_Directive_Processor::parse_attribute_name( $name );
155 if ( array_key_exists( $type, $directives ) ) {
156 $attributes[] = $type;
157 }
158 }
159
160 /*
161 * If this is an open tag, and if it either has directives, or if
162 * we're inside a tag that does, take note of this tag and its
163 * directives so we can call its directive processor once we
164 * encounter the matching closing tag.
165 */
166 if (
167 ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) &&
168 ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
169 ) {
170 $tag_stack[] = array( $tag_name, $attributes );
171 }
172 }
173
174 foreach ( $attributes as $attribute ) {
175 call_user_func( $directives[ $attribute ], $tags, $context );
176 }
177 }
178
179 return $tags;
180 }
181
182 /**
183 * Return the content between two balanced tags.
184 *
185 * When called on an opening tag, return the HTML content found between that
186 * opening tag and its matching closing tag.
187 *
188 * @return string The content between the current opening and its matching
189 * closing tag.
190 */
191 public function get_inner_html() {
192 $bookmarks = $this->get_balanced_tag_bookmarks();
193 if ( ! $bookmarks ) {
194 return false;
195 }
196 list( $start_name, $end_name ) = $bookmarks;
197
198 $start = $this->bookmarks[ $start_name ]->end + 1;
199 $end = $this->bookmarks[ $end_name ]->start;
200
201 $this->seek( $start_name ); // Return to original position.
202 $this->release_bookmark( $start_name );
203 $this->release_bookmark( $end_name );
204
205 return substr( $this->html, $start, $end - $start );
206 }
207
208 /**
209 * Set the content between two balanced tags.
210 *
211 * When called on an opening tag, set the HTML content found between that
212 * opening tag and its matching closing tag.
213 *
214 * @param string $new_html The string to replace the content between the
215 * matching tags with.
216 *
217 * @return bool Whether the content was successfully replaced.
218 */
219 public function set_inner_html( $new_html ) {
220 $this->get_updated_html(); // Apply potential previous updates.
221
222 $bookmarks = $this->get_balanced_tag_bookmarks();
223 if ( ! $bookmarks ) {
224 return false;
225 }
226 list( $start_name, $end_name ) = $bookmarks;
227
228 $start = $this->bookmarks[ $start_name ]->end + 1;
229 $end = $this->bookmarks[ $end_name ]->start;
230
231 $this->seek( $start_name ); // Return to original position.
232 $this->release_bookmark( $start_name );
233 $this->release_bookmark( $end_name );
234
235 $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html );
236 return true;
237 }
238
239 /**
240 * Return a pair of bookmarks for the current opening tag and the matching
241 * closing tag.
242 *
243 * @return array|false A pair of bookmarks, or false if there's no matching
244 * closing tag.
245 */
246 public function get_balanced_tag_bookmarks() {
247 $i = 0;
248 while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) {
249 ++$i;
250 }
251 $start_name = 'start' . $i;
252
253 $this->set_bookmark( $start_name );
254 if ( ! $this->next_balanced_closer() ) {
255 $this->release_bookmark( $start_name );
256 return false;
257 }
258
259 $i = 0;
260 while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) {
261 ++$i;
262 }
263 $end_name = 'end' . $i;
264 $this->set_bookmark( $end_name );
265
266 return array( $start_name, $end_name );
267 }
268
269 /**
270 * Whether a given HTML element is void (e.g. <br>).
271 *
272 * @param string $tag_name The element in question.
273 * @return bool True if the element is void.
274 *
275 * @see https://html.spec.whatwg.org/#elements-2
276 */
277 public static function is_html_void_element( $tag_name ) {
278 switch ( $tag_name ) {
279 case 'AREA':
280 case 'BASE':
281 case 'BR':
282 case 'COL':
283 case 'EMBED':
284 case 'HR':
285 case 'IMG':
286 case 'INPUT':
287 case 'LINK':
288 case 'META':
289 case 'SOURCE':
290 case 'TRACK':
291 case 'WBR':
292 return true;
293
294 default:
295 return false;
296 }
297 }
298
299 /**
300 * Extract and return the directive type and the the part after the double
301 * hyphen from an attribute name (if present), in an array format.
302 *
303 * Examples:
304 *
305 * 'wp-island' => array( 'wp-island', null )
306 * 'wp-bind--src' => array( 'wp-bind', 'src' )
307 * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' )
308 *
309 * @param string $name The attribute name.
310 * @return array The resulting array
311 */
312 public static function parse_attribute_name( $name ) {
313 return explode( '--', $name, 2 );
314 }
315 }
316