PluginProbe
Gutenberg / 17.5.0
Gutenberg v17.5.0
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.5.0, at lib/experimental/interactivity-api/class-wp-directive-processor.php

311 lines 8.2 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 class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 {
18 /**
19 * String containing the current root block.
20 *
21 * @var string
22 */
23 public static $root_block = null;
24
25 /**
26 * Array containing the direct children of interactive blocks.
27 *
28 * @var array
29 */
30 public static $children_of_interactive_block = array();
31
32 /**
33 * Sets the current root block.
34 *
35 * @param array $block The block to add.
36 */
37 public static function mark_root_block( $block ) {
38 if ( null !== $block['blockName'] ) {
39 self::$root_block = $block['blockName'] . md5( serialize( $block ) );
40 } else {
41 self::$root_block = md5( serialize( $block ) );
42 }
43 }
44
45 /**
46 * Resets the root block.
47 */
48 public static function unmark_root_block() {
49 self::$root_block = null;
50 }
51
52 /**
53 * Checks if block is a root block.
54 *
55 * @param array $block The block to check.
56 * @return bool True if block is a root block, false otherwise.
57 */
58 public static function is_marked_as_root_block( $block ) {
59 // If self::$root_block is null, is impossible that any block has been marked as root.
60 if ( is_null( self::$root_block ) ) {
61 return false;
62 }
63 // Blocks whose blockName is null are specifically intended to convey - "this is a freeform HTML block."
64 if ( null !== $block['blockName'] ) {
65 return str_contains( self::$root_block, $block['blockName'] ) && $block['blockName'] . md5( serialize( $block ) ) === self::$root_block;
66 }
67 return md5( serialize( $block ) ) === self::$root_block;
68 }
69
70 /**
71 * Checks if a root block has already been defined.
72 *
73 * @return bool True if there is a root block, false otherwise.
74 */
75 public static function has_root_block() {
76 return isset( self::$root_block );
77 }
78
79 /**
80 * Stores a reference to a direct children of an interactive block to be able
81 * to identify it later.
82 *
83 * @param array $block The block to add.
84 */
85 public static function mark_children_of_interactive_block( $block ) {
86 self::$children_of_interactive_block[] = md5( serialize( $block ) );
87 }
88
89 /**
90 * Checks if block is marked as children of an interactive block.
91 *
92 * @param array $block The block to check.
93 * @return bool True if block is a children of an interactive block, false otherwise.
94 */
95 public static function is_marked_as_children_of_interactive_block( $block ) {
96 return in_array( md5( serialize( $block ) ), self::$children_of_interactive_block, true );
97 }
98
99 /**
100 * Finds the matching closing tag for an opening tag.
101 *
102 * When called while on an open tag, traverse the HTML until we find the
103 * matching closing tag, respecting any in-between content, including nested
104 * tags of the same name. Return false when called on a closing or void tag,
105 * or if no matching closing tag was found.
106 *
107 * @return bool Whether a matching closing tag was found.
108 */
109 public function next_balanced_closer() {
110 $depth = 0;
111
112 $tag_name = $this->get_tag();
113
114 if ( self::is_html_void_element( $tag_name ) ) {
115 return false;
116 }
117
118 while ( $this->next_tag(
119 array(
120 'tag_name' => $tag_name,
121 'tag_closers' => 'visit',
122 )
123 ) ) {
124 if ( ! $this->is_tag_closer() ) {
125 ++$depth;
126 continue;
127 }
128
129 if ( 0 === $depth ) {
130 return true;
131 }
132
133 --$depth;
134 }
135
136 return false;
137 }
138
139 /**
140 * Returns the content between two balanced tags.
141 *
142 * When called on an opening tag, return the HTML content found between that
143 * opening tag and its matching closing tag.
144 *
145 * @return string The content between the current opening and its matching
146 * closing tag.
147 */
148 public function get_inner_html() {
149 $bookmarks = $this->get_balanced_tag_bookmarks();
150 if ( ! $bookmarks ) {
151 return false;
152 }
153 list( $start_name, $end_name ) = $bookmarks;
154
155 $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
156 $end = $this->bookmarks[ $end_name ]->start;
157
158 $this->seek( $start_name ); // Return to original position.
159 $this->release_bookmark( $start_name );
160 $this->release_bookmark( $end_name );
161
162 return substr( $this->html, $start, $end - $start );
163 }
164
165 /**
166 * Sets the content between two balanced tags.
167 *
168 * When called on an opening tag, set the HTML content found between that
169 * opening tag and its matching closing tag.
170 *
171 * @param string $new_html The string to replace the content between the
172 * matching tags with.
173 * @return bool Whether the content was successfully replaced.
174 */
175 public function set_inner_html( $new_html ) {
176 $this->get_updated_html(); // Apply potential previous updates.
177
178 $bookmarks = $this->get_balanced_tag_bookmarks();
179 if ( ! $bookmarks ) {
180 return false;
181 }
182 list( $start_name, $end_name ) = $bookmarks;
183
184 $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1;
185 $end = $this->bookmarks[ $end_name ]->start;
186
187 $this->seek( $start_name ); // Return to original position.
188 $this->release_bookmark( $start_name );
189 $this->release_bookmark( $end_name );
190
191 $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, $new_html );
192 return true;
193 }
194
195 /**
196 * Returns a pair of bookmarks for the current opening tag and the matching
197 * closing tag.
198 *
199 * @return array|false A pair of bookmarks, or false if there's no matching
200 * closing tag.
201 */
202 public function get_balanced_tag_bookmarks() {
203 $i = 0;
204 while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) {
205 ++$i;
206 }
207 $start_name = 'start' . $i;
208
209 $this->set_bookmark( $start_name );
210 if ( ! $this->next_balanced_closer() ) {
211 $this->release_bookmark( $start_name );
212 return false;
213 }
214
215 $i = 0;
216 while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) {
217 ++$i;
218 }
219 $end_name = 'end' . $i;
220 $this->set_bookmark( $end_name );
221
222 return array( $start_name, $end_name );
223 }
224
225 /**
226 * Checks whether a given HTML element is void (e.g. <br>).
227 *
228 * @see https://html.spec.whatwg.org/#elements-2
229 *
230 * @param string $tag_name The element in question.
231 * @return bool True if the element is void.
232 */
233 public static function is_html_void_element( $tag_name ) {
234 switch ( $tag_name ) {
235 case 'AREA':
236 case 'BASE':
237 case 'BR':
238 case 'COL':
239 case 'EMBED':
240 case 'HR':
241 case 'IMG':
242 case 'INPUT':
243 case 'LINK':
244 case 'META':
245 case 'SOURCE':
246 case 'TRACK':
247 case 'WBR':
248 return true;
249
250 default:
251 return false;
252 }
253 }
254
255 /**
256 * Extracts and return the directive type and the the part after the double
257 * hyphen from an attribute name (if present), in an array format.
258 *
259 * Examples:
260 *
261 * 'wp-island' => array( 'wp-island', null )
262 * 'wp-bind--src' => array( 'wp-bind', 'src' )
263 * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' )
264 *
265 * @param string $name The attribute name.
266 * @return array The resulting array.
267 */
268 public static function parse_attribute_name( $name ) {
269 return explode( '--', $name, 2 );
270 }
271
272 /**
273 * Parse and extract the namespace and path from the given value.
274 *
275 * If the value contains a JSON instead of a path, the function parses it
276 * and returns the resulting array.
277 *
278 * @param string $value Passed value.
279 * @param string $ns Namespace fallback.
280 * @return array The resulting array
281 */
282 public static function parse_attribute_value( $value, $ns = null ) {
283 $matches = array();
284 $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches );
285
286 /*
287 * Overwrite both `$ns` and `$value` variables if `$value` explicitly
288 * contains a namespace.
289 */
290 if ( $has_ns ) {
291 list( , $ns, $value ) = $matches;
292 }
293
294 /*
295 * Try to decode `$value` as a JSON object. If it works, `$value` is
296 * replaced with the resulting array. The original string is preserved
297 * otherwise.
298 *
299 * Note that `json_decode` returns `null` both for an invalid JSON or
300 * the `'null'` string (a valid JSON). In the latter case, `$value` is
301 * replaced with `null`.
302 */
303 $data = json_decode( $value, true );
304 if ( null !== $data || 'null' === trim( $value ) ) {
305 $value = $data;
306 }
307
308 return array( $ns, $value );
309 }
310 }
311