PluginProbe
Gutenberg / 17.5.2
Gutenberg v17.5.2
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 7.4.0 All 402 releases
gutenberg / lib / experimental / interactivity-api / directive-processing.php

directive-processing.php in Gutenberg 17.5.2, at lib/experimental/interactivity-api/directive-processing.php

364 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions and hooks to process the Interactivity API directives in the
4 * server.
5 *
6 * @package Gutenberg
7 * @subpackage Interactivity API
8 */
9
10 /**
11 * Marks the block as a root block. Checks that there is already a root block in
12 * order not to mark template-parts or synced patterns as root blocks, where the
13 * parent is null.
14 *
15 * @param array $parsed_block The parsed block.
16 * @param array $source_block The source block.
17 * @param array $parent_block The parent block.
18 *
19 * @return array The parsed block.
20 */
21 function gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $parent_block ) {
22 if ( ! isset( $parent_block ) && ! WP_Directive_Processor::has_root_block() ) {
23 WP_Directive_Processor::mark_root_block( $parsed_block );
24 }
25
26 return $parsed_block;
27 }
28 add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_blocks', 10, 3 );
29
30 /**
31 * Processes the directives in the root blocks.
32 *
33 * @param string $block_content The block content.
34 * @param array $block The full block.
35 *
36 * @return string Filtered block content.
37 */
38 function gutenberg_process_directives_in_root_blocks( $block_content, $block ) {
39 if ( WP_Directive_Processor::is_marked_as_root_block( $block ) ) {
40 WP_Directive_Processor::unmark_root_block();
41
42 // Parse our own block delimiters for interactive and non-interactive blocks.
43 $parsed_blocks = parse_blocks( $block_content );
44 $context = new WP_Directive_Context();
45 $processed_content = '';
46 $namespace_stack = array();
47
48 foreach ( $parsed_blocks as $parsed_block ) {
49 if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) {
50 $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context, $namespace_stack );
51 } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) {
52 $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context, $namespace_stack );
53 } else {
54 $processed_content .= $parsed_block['innerHTML'];
55 }
56 }
57 return $processed_content;
58 }
59
60 return $block_content;
61 }
62 add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 20, 2 );
63
64 /**
65 * Marks the block as a children of an interactive block.
66 *
67 * @param array $parsed_block The parsed block.
68 * @param array $source_block The source block.
69 * @param WP_Block $parent_block The parent block.
70 */
71 function gutenberg_mark_chidren_of_interactive_block( $parsed_block, $source_block, $parent_block ) {
72 if (
73 isset( $parent_block ) &&
74 isset( $parent_block->block_type->supports['interactivity'] ) &&
75 $parent_block->block_type->supports['interactivity']
76 ) {
77 WP_Directive_Processor::mark_children_of_interactive_block( $source_block );
78 }
79 return $parsed_block;
80 }
81 add_filter( 'render_block_data', 'gutenberg_mark_chidren_of_interactive_block', 100, 3 );
82
83 /**
84 * Adds a comment delimiter to mark if the block is interactive or not.
85 *
86 * @param string $block_content The block content.
87 * @param array $block The full block, including name and attributes.
88 * @param WP_Block $block_instance The block instance.
89 */
90 function gutenberg_mark_block_interactivity( $block_content, $block, $block_instance ) {
91 if (
92 isset( $block_instance->block_type->supports['interactivity'] ) &&
93 $block_instance->block_type->supports['interactivity']
94 ) {
95 // Wraps the interactive block with a comment delimiter to be able to
96 // process it later.
97 return get_comment_delimited_block_content(
98 'core/interactivity-wrapper',
99 array(),
100 $block_content
101 );
102 } elseif ( WP_Directive_Processor::is_marked_as_children_of_interactive_block( $block ) ) {
103 // Wraps the non-interactive block with a comment delimiter to be able to
104 // skip it later.
105 return get_comment_delimited_block_content(
106 'core/non-interactivity-wrapper',
107 array(),
108 $block_content
109 );
110 }
111 return $block_content;
112 }
113 add_filter( 'render_block', 'gutenberg_mark_block_interactivity', 10, 3 );
114
115 /**
116 * Traverses the HTML of an interactive block, searching for Interactivity API
117 * directives and processing them. For the inner blocks, it calls the
118 * corresponding function depending on the wrapper type.
119 *
120 * @param array $interactive_block The interactive block to process.
121 * @param WP_Directive_Context $context The context to use when processing.
122 * @param array $namespace_stack Stack of namespackes passed by reference.
123 *
124 * @return string The processed HTML.
125 */
126 function gutenberg_process_interactive_block( $interactive_block, $context, &$namespace_stack ) {
127 $block_index = 0;
128 $content = '';
129 $interactive_inner_blocks = array();
130
131 foreach ( $interactive_block['innerContent'] as $inner_content ) {
132 if ( is_string( $inner_content ) ) {
133 $content .= $inner_content;
134 } else {
135 // This is an inner block. It may be an interactive block or a
136 // non-interactive block.
137 $content .= '<wp-inner-blocks-' . $block_index . '></wp-inner-blocks-' . $block_index . '>';
138 $interactive_inner_blocks[] = $interactive_block['innerBlocks'][ $block_index++ ];
139 }
140 }
141
142 return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks, $namespace_stack );
143 }
144
145 /**
146 * Returns the HTML of a non-interactive block without processing the
147 * directives. For the inner blocks, it calls the corresponding function
148 * depending on the wrapper type.
149 *
150 * @param array $non_interactive_block The non-interactive block to process.
151 * @param WP_Directive_Context $context The context to use when processing.
152 * @param array $namespace_stack Stack of namespackes passed by reference.
153 *
154 * @return string The processed HTML.
155 */
156 function gutenberg_process_non_interactive_block( $non_interactive_block, $context, &$namespace_stack ) {
157 $block_index = 0;
158 $content = '';
159 foreach ( $non_interactive_block['innerContent'] as $inner_content ) {
160 if ( is_string( $inner_content ) ) {
161 // This content belongs to a non interactive block and therefore it cannot
162 // contain directives. We add the HTML directly to the final output.
163 $content .= $inner_content;
164 } else {
165 // This is an inner block. It may be an interactive block or a
166 // non-interactive block.
167 $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ];
168
169 if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) {
170 $content .= gutenberg_process_interactive_block( $inner_block, $context, $namespace_stack );
171 } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) {
172 $content .= gutenberg_process_non_interactive_block( $inner_block, $context, $namespace_stack );
173 }
174 }
175 }
176 return $content;
177 }
178
179 /**
180 * Processes interactive HTML by applying directives to the HTML tags.
181 *
182 * It uses the WP_Directive_Processor class to parse the HTML and apply the
183 * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner
184 * blocks to process, the function processes these inner blocks and replaces the
185 * 'WP-INNER-BLOCKS' tag in the HTML with those blocks.
186 *
187 * @param string $html The HTML to process.
188 * @param mixed $context The context to use when processing.
189 * @param array $inner_blocks The inner blocks to process.
190 * @param array $namespace_stack Stack of namespackes passed by reference.
191 *
192 * @return string The processed HTML.
193 */
194 function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array(), &$namespace_stack = array() ) {
195 static $directives = array(
196 'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive',
197 'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
198 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
199 'data-wp-class' => 'gutenberg_interactivity_process_wp_class',
200 'data-wp-style' => 'gutenberg_interactivity_process_wp_style',
201 'data-wp-text' => 'gutenberg_interactivity_process_wp_text',
202 );
203
204 $tags = new WP_Directive_Processor( $html );
205 $prefix = 'data-wp-';
206 $tag_stack = array();
207 $inner_processed_blocks = array();
208 $inner_blocks_index = 0;
209 while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
210 $tag_name = $tags->get_tag();
211
212 // Processes the inner blocks.
213 if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) {
214 if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) {
215 $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack );
216 } elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) {
217 $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack );
218 }
219 }
220 if ( $tags->is_tag_closer() ) {
221 if ( 0 === count( $tag_stack ) ) {
222 continue;
223 }
224 list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
225 if ( $latest_opening_tag_name === $tag_name ) {
226 array_pop( $tag_stack );
227 // If the matching opening tag didn't have any directives, we move on.
228 if ( 0 === count( $attributes ) ) {
229 continue;
230 }
231 }
232 } else {
233 $attributes = array();
234 foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
235 /*
236 * Removes the part after the double hyphen before looking for
237 * the directive processor inside `$directives`, e.g., "wp-bind"
238 * from "wp-bind--src" and "wp-context" from "wp-context" etc...
239 */
240 list( $type ) = $tags::parse_attribute_name( $name );
241 if ( array_key_exists( $type, $directives ) ) {
242 $attributes[] = $type;
243 }
244 }
245
246 /*
247 * If this is an open tag, and if it either has directives, or if
248 * we're inside a tag that does, take note of this tag and its
249 * directives so we can call its directive processor once we
250 * encounter the matching closing tag.
251 */
252 if (
253 ! $tags::is_html_void_element( $tag_name ) &&
254 ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
255 ) {
256 $tag_stack[] = array( $tag_name, $attributes );
257 }
258 }
259
260 // Extract all directive names. They'll be used later on.
261 $directive_names = array_keys( $directives );
262 $directive_names_rev = array_reverse( $directive_names );
263
264 /*
265 * Sort attributes by the order they appear in the `$directives`
266 * argument, considering it as the priority order in which
267 * directives should be processed. Note that the order is reversed
268 * for tag closers.
269 */
270 $sorted_attrs = array_intersect(
271 $tags->is_tag_closer()
272 ? $directive_names_rev
273 : $directive_names,
274 $attributes
275 );
276
277 foreach ( $sorted_attrs as $attribute ) {
278 call_user_func_array(
279 $directives[ $attribute ],
280 array(
281 $tags,
282 $context,
283 end( $namespace_stack ),
284 &$namespace_stack,
285 )
286 );
287 }
288 }
289
290 $processed_html = $tags->get_updated_html();
291
292 // Replaces the inner block tags with the content of each inner block
293 // processed.
294 if ( ! empty( $inner_processed_blocks ) ) {
295 foreach ( $inner_processed_blocks as $inner_block_tag => $inner_block_content ) {
296 if ( str_contains( $processed_html, $inner_block_tag ) ) {
297 $processed_html = str_replace( '<' . $inner_block_tag . '></' . $inner_block_tag . '>', $inner_block_content, $processed_html );
298 }
299 }
300 }
301
302 return $processed_html;
303 }
304
305 /**
306 * Resolves the passed reference from the store and the context under the given
307 * namespace.
308 *
309 * A reference could be either a single path or a namespace followed by a path,
310 * separated by two colons, i.e, `namespace::path.to.prop`. If the reference
311 * contains a namespace, that namespace overrides the one passed as argument.
312 *
313 * @param string $reference Reference value.
314 * @param string $ns Inherited namespace.
315 * @param array $context Context data.
316 * @return mixed Resolved value.
317 */
318 function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) {
319 // Extract the namespace from the reference (if present).
320 list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns );
321
322 $store = array(
323 'state' => WP_Interactivity_Initial_State::get_state( $ns ),
324 'context' => $context[ $ns ] ?? array(),
325 );
326
327 /*
328 * Checks first if the directive path is preceded by a negator operator (!),
329 * indicating that the value obtained from the Interactivity Store (or the
330 * passed context) using the subsequent path should be negated.
331 */
332 $should_negate_value = '!' === $path[0];
333 $path = $should_negate_value ? substr( $path, 1 ) : $path;
334 $path_segments = explode( '.', $path );
335 $current = $store;
336 foreach ( $path_segments as $p ) {
337 if ( isset( $current[ $p ] ) ) {
338 $current = $current[ $p ];
339 } else {
340 return null;
341 }
342 }
343
344 /*
345 * Checks if $current is an anonymous function or an arrow function, and if
346 * so, call it passing the store. Other types of callables are ignored on
347 * purpose, as arbitrary strings or arrays could be wrongly evaluated as
348 * "callables".
349 *
350 * E.g., "file" is an string and a "callable" (the "file" function exists).
351 */
352 if ( $current instanceof Closure ) {
353 /*
354 * TODO: Figure out a way to implement derived state without having to
355 * pass the store as argument:
356 *
357 * $current = call_user_func( $current );
358 */
359 }
360
361 // Returns the opposite if it has a negator operator (!).
362 return $should_negate_value ? ! $current : $current;
363 }
364