Skip to content

Commit 254b80f

Browse files
committed
Add filtering mechanism for post block update command
1 parent e4c63d0 commit 254b80f

File tree

4 files changed

+295
-4
lines changed

4 files changed

+295
-4
lines changed

features/post-block.feature

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,96 @@ Feature: Manage blocks in post content
492492
"level": 3
493493
"""
494494

495+
@require-wp-5.0
496+
Scenario: Update heading level syncs HTML tag
497+
Given a WP install
498+
When I run `wp post create --post_title='Block Post' --post_content='<!-- wp:heading {"level":2} --><h2>Original Title</h2><!-- /wp:heading -->' --porcelain`
499+
Then save STDOUT as {POST_ID}
500+
501+
When I run `wp post block update {POST_ID} 0 --attrs='{"level":4}'`
502+
Then STDOUT should contain:
503+
"""
504+
Success: Updated block at index 0 in post {POST_ID}.
505+
"""
506+
507+
# Verify the attribute was updated
508+
When I run `wp post block parse {POST_ID}`
509+
Then STDOUT should contain:
510+
"""
511+
"level": 4
512+
"""
513+
514+
# Verify the HTML tag was updated to match
515+
When I run `wp post get {POST_ID} --field=post_content`
516+
Then STDOUT should contain:
517+
"""
518+
<h4>Original Title</h4>
519+
"""
520+
And STDOUT should not contain:
521+
"""
522+
<h2>
523+
"""
524+
525+
@require-wp-5.0
526+
Scenario: Update list ordered attribute syncs HTML tag
527+
Given a WP install
528+
When I run `wp post create --post_title='Block Post' --post_content='<!-- wp:list --><ul><li>Item 1</li><li>Item 2</li></ul><!-- /wp:list -->' --porcelain`
529+
Then save STDOUT as {POST_ID}
530+
531+
When I run `wp post block update {POST_ID} 0 --attrs='{"ordered":true}'`
532+
Then STDOUT should contain:
533+
"""
534+
Success: Updated block at index 0 in post {POST_ID}.
535+
"""
536+
537+
# Verify the HTML tag was updated from ul to ol
538+
When I run `wp post get {POST_ID} --field=post_content`
539+
Then STDOUT should contain:
540+
"""
541+
<ol>
542+
"""
543+
And STDOUT should contain:
544+
"""
545+
</ol>
546+
"""
547+
And STDOUT should not contain:
548+
"""
549+
<ul>
550+
"""
551+
552+
@require-wp-5.0
553+
Scenario: Update block with custom HTML sync filter via --require
554+
Given a WP install
555+
And a custom-sync-filter.php file:
556+
"""
557+
<?php
558+
WP_CLI::add_wp_hook( 'wp_cli_post_block_update_html', function( $block, $new_attrs, $block_name ) {
559+
if ( 'core/paragraph' === $block_name && isset( $new_attrs['customClass'] ) ) {
560+
$block['innerHTML'] = preg_replace(
561+
'/<p([^>]*)>/',
562+
'<p class="' . esc_attr( $new_attrs['customClass'] ) . '"$1>',
563+
$block['innerHTML']
564+
);
565+
$block['innerContent'] = [ $block['innerHTML'] ];
566+
}
567+
return $block;
568+
}, 10, 3 );
569+
"""
570+
When I run `wp post create --post_title='Block Post' --post_content='<!-- wp:paragraph --><p>Hello World</p><!-- /wp:paragraph -->' --porcelain`
571+
Then save STDOUT as {POST_ID}
572+
573+
When I run `wp post block update {POST_ID} 0 --attrs='{"customClass":"my-custom-class"}' --require=custom-sync-filter.php`
574+
Then STDOUT should contain:
575+
"""
576+
Success: Updated block at index 0 in post {POST_ID}.
577+
"""
578+
579+
When I run `wp post get {POST_ID} --field=post_content`
580+
Then STDOUT should contain:
581+
"""
582+
<p class="my-custom-class">Hello World</p>
583+
"""
584+
495585
@require-wp-5.0
496586
Scenario: Update block content
497587
Given a WP install

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<property name="prefixes" type="array">
4848
<element value="WP_CLI\Entity"/><!-- Namespaces. -->
4949
<element value="wpcli_entity"/><!-- Global variables and such. -->
50+
<element value="wp_cli_"/><!-- Public WP-CLI hooks. -->
5051
</property>
5152
</properties>
5253
</rule>

src/Block_HTML_Sync_Filters.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
/**
3+
* Default filters for synchronizing block HTML with attribute changes.
4+
*
5+
* This file contains the built-in filter callbacks that update block HTML
6+
* when attributes are changed via `wp post block update`. These are registered
7+
* as WordPress filters, allowing them to be modified, replaced, or extended
8+
* by custom code loaded via --require, plugins, or themes.
9+
*
10+
* @package WP_CLI\Entity
11+
*/
12+
13+
namespace WP_CLI\Entity;
14+
15+
/**
16+
* Handles synchronization of block HTML with updated attributes.
17+
*
18+
* Each method is a filter callback for 'wp_cli_post_block_update_html'.
19+
* Methods check if they handle the given block type and return early if not.
20+
*/
21+
class Block_HTML_Sync_Filters {
22+
23+
/**
24+
* Registers all default sync filters.
25+
*
26+
* Called during command initialization to set up the built-in handlers.
27+
* Users can remove these filters or add their own at different priorities.
28+
*
29+
* @return void
30+
*/
31+
public static function register() {
32+
add_filter( 'wp_cli_post_block_update_html', [ __CLASS__, 'sync_heading_level' ], 10, 3 );
33+
add_filter( 'wp_cli_post_block_update_html', [ __CLASS__, 'sync_list_type' ], 10, 3 );
34+
}
35+
36+
/**
37+
* Synchronizes heading HTML tag with the level attribute.
38+
*
39+
* When a heading's level attribute changes (e.g., from 2 to 3),
40+
* this updates the HTML tag from <h2> to <h3>.
41+
*
42+
* @param array $block The block structure.
43+
* @param array $new_attrs The newly applied attributes.
44+
* @param string $block_name The block type name.
45+
* @return array The block with synchronized HTML.
46+
*/
47+
public static function sync_heading_level( $block, $new_attrs, $block_name ) {
48+
if ( 'core/heading' !== $block_name ) {
49+
return $block;
50+
}
51+
52+
if ( ! isset( $new_attrs['level'] ) ) {
53+
return $block;
54+
}
55+
56+
$new_level = (int) $new_attrs['level'];
57+
if ( $new_level < 1 || $new_level > 6 ) {
58+
return $block;
59+
}
60+
61+
$inner_html = $block['innerHTML'] ?? '';
62+
if ( empty( $inner_html ) ) {
63+
return $block;
64+
}
65+
66+
// Replace opening and closing heading tags.
67+
// Pattern matches <h1> through <h6> with optional attributes.
68+
$updated_html = preg_replace(
69+
'/<h[1-6](\s[^>]*)?>/',
70+
"<h{$new_level}$1>",
71+
$inner_html
72+
);
73+
$updated_html = preg_replace(
74+
'/<\/h[1-6]>/',
75+
"</h{$new_level}>",
76+
$updated_html
77+
);
78+
79+
if ( null !== $updated_html && $updated_html !== $inner_html ) {
80+
$block['innerHTML'] = $updated_html;
81+
$block['innerContent'] = [ $updated_html ];
82+
}
83+
84+
return $block;
85+
}
86+
87+
/**
88+
* Synchronizes list HTML tag with the ordered attribute.
89+
*
90+
* When a list's ordered attribute changes, this updates the HTML
91+
* from <ul> to <ol> or vice versa.
92+
*
93+
* @param array $block The block structure.
94+
* @param array $new_attrs The newly applied attributes.
95+
* @param string $block_name The block type name.
96+
* @return array The block with synchronized HTML.
97+
*/
98+
public static function sync_list_type( $block, $new_attrs, $block_name ) {
99+
if ( 'core/list' !== $block_name ) {
100+
return $block;
101+
}
102+
103+
if ( ! isset( $new_attrs['ordered'] ) ) {
104+
return $block;
105+
}
106+
107+
$inner_html = $block['innerHTML'] ?? '';
108+
if ( empty( $inner_html ) ) {
109+
return $block;
110+
}
111+
112+
$is_ordered = (bool) $new_attrs['ordered'];
113+
$new_tag = $is_ordered ? 'ol' : 'ul';
114+
$old_tag = $is_ordered ? 'ul' : 'ol';
115+
116+
// Replace opening and closing list tags.
117+
$updated_html = preg_replace(
118+
"/<{$old_tag}(\s[^>]*)?>/",
119+
"<{$new_tag}$1>",
120+
$inner_html
121+
);
122+
$updated_html = preg_replace(
123+
"/<\/{$old_tag}>/",
124+
"</{$new_tag}>",
125+
$updated_html
126+
);
127+
128+
if ( null !== $updated_html && $updated_html !== $inner_html ) {
129+
$block['innerHTML'] = $updated_html;
130+
$block['innerContent'] = [ $updated_html ];
131+
}
132+
133+
return $block;
134+
}
135+
}

src/Post_Block_Command.php

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Mustangostang\Spyc;
4+
use WP_CLI\Entity\Block_HTML_Sync_Filters;
45
use WP_CLI\Entity\Block_Processor_Helper;
56
use WP_CLI\Formatter;
67
use WP_CLI\Fetchers\Post as PostFetcher;
@@ -38,6 +39,13 @@ class Post_Block_Command extends WP_CLI_Command {
3839
*/
3940
private $fetcher;
4041

42+
/**
43+
* Whether filters have been registered.
44+
*
45+
* @var bool
46+
*/
47+
private static $filters_registered = false;
48+
4149
/**
4250
* Default fields to display for block list.
4351
*
@@ -51,6 +59,12 @@ class Post_Block_Command extends WP_CLI_Command {
5159

5260
public function __construct() {
5361
$this->fetcher = new PostFetcher();
62+
63+
// Register default HTML sync filters once.
64+
if ( ! self::$filters_registered ) {
65+
Block_HTML_Sync_Filters::register();
66+
self::$filters_registered = true;
67+
}
5468
}
5569

5670
/**
@@ -149,7 +163,9 @@ public function get( $args, $assoc_args ) {
149163
/**
150164
* Updates a block's attributes or content by index.
151165
*
152-
* Modifies a specific block without changing its type.
166+
* Modifies a specific block without changing its type. For blocks where
167+
* attributes are reflected in HTML (like heading levels), the HTML is
168+
* automatically updated to match the new attributes.
153169
*
154170
* ## OPTIONS
155171
*
@@ -197,6 +213,11 @@ public function get( $args, $assoc_args ) {
197213
* $ wp post block update 123 0 --attrs='{"level":2}' --porcelain
198214
* 123
199215
*
216+
* # Use custom HTML sync logic via the wp_cli_post_block_update_html filter.
217+
* # Use WP_CLI::add_wp_hook() in a file loaded with --require.
218+
* $ wp post block update 123 0 --attrs='{"url":"https://example.com"}' --require=my-sync-filters.php
219+
* Success: Updated block at index 0 in post 123.
220+
*
200221
* @subcommand update
201222
*/
202223
public function update( $args, $assoc_args ) {
@@ -231,18 +252,21 @@ public function update( $args, $assoc_args ) {
231252

232253
if ( null !== $attrs_json ) {
233254
$new_attrs = json_decode( $attrs_json, true );
234-
if ( null === $new_attrs ) {
235-
WP_CLI::error( 'Invalid JSON provided for --attrs.' );
255+
if ( ! is_array( $new_attrs ) ) {
256+
WP_CLI::error( 'Invalid JSON provided for --attrs. Must be a JSON object.' );
236257
}
237258

238259
if ( $replace_attrs ) {
239260
$block['attrs'] = $new_attrs;
240261
} else {
241262
$block['attrs'] = array_merge(
242263
is_array( $block['attrs'] ) ? $block['attrs'] : [],
243-
is_array( $new_attrs ) ? $new_attrs : []
264+
$new_attrs
244265
);
245266
}
267+
268+
// Update HTML to reflect attribute changes for known block types.
269+
$block = $this->sync_html_with_attrs( $block, $new_attrs );
246270
}
247271

248272
if ( null !== $content ) {
@@ -1765,4 +1789,45 @@ private function deep_copy_block( $block ) {
17651789

17661790
return $copy;
17671791
}
1792+
1793+
/**
1794+
* Synchronizes block HTML with updated attributes.
1795+
*
1796+
* Applies the 'wp_cli_post_block_update_html' filter to allow handlers
1797+
* to update block HTML when attributes change.
1798+
*
1799+
* Custom handlers can be added via --require (using WP_CLI::add_wp_hook())
1800+
* or in plugins/themes (using add_filter()):
1801+
*
1802+
* WP_CLI::add_wp_hook( 'wp_cli_post_block_update_html', function( $block, $new_attrs, $block_name ) {
1803+
* if ( 'core/button' === $block_name && isset( $new_attrs['url'] ) ) {
1804+
* // Update href in the HTML...
1805+
* }
1806+
* return $block;
1807+
* }, 10, 3 );
1808+
*
1809+
* @see \WP_CLI\Entity\Block_HTML_Sync_Filters Default filter implementations.
1810+
*
1811+
* @param array $block The block structure.
1812+
* @param array $new_attrs The newly applied attributes.
1813+
* @return array The block with synchronized HTML.
1814+
*/
1815+
private function sync_html_with_attrs( $block, $new_attrs ) {
1816+
$block_name = $block['blockName'] ?? '';
1817+
1818+
/**
1819+
* Filters a block after attribute updates to sync HTML with attributes.
1820+
*
1821+
* Allows handlers to update block HTML when attributes change.
1822+
*
1823+
* @since 3.0.0
1824+
*
1825+
* @see \WP_CLI\Entity\Block_HTML_Sync_Filters Default filter implementations.
1826+
*
1827+
* @param array $block The block structure with updated attrs.
1828+
* @param array $new_attrs The newly applied attributes.
1829+
* @param string $block_name The block type name (e.g., 'core/heading').
1830+
*/
1831+
return apply_filters( 'wp_cli_post_block_update_html', $block, $new_attrs, $block_name );
1832+
}
17681833
}

0 commit comments

Comments
 (0)