diff --git a/composer.json b/composer.json index 21d853c5..67b00871 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,9 @@ "comment unspam", "comment untrash", "comment update", + "block", + "block get", + "block list", "menu", "menu create", "menu delete", @@ -96,6 +99,9 @@ "option update", "option set-autoload", "option get-autoload", + "pattern", + "pattern get", + "pattern list", "post", "post create", "post delete", @@ -103,6 +109,8 @@ "post exists", "post generate", "post get", + "post has-block", + "post has-blocks", "post list", "post meta", "post meta add", @@ -113,6 +121,8 @@ "post meta patch", "post meta pluck", "post meta update", + "post parse-blocks", + "post render-blocks", "post term", "post term add", "post term list", diff --git a/entity-command.php b/entity-command.php index 6cb54ff3..827326aa 100644 --- a/entity-command.php +++ b/entity-command.php @@ -92,3 +92,28 @@ }, ) ); + +// Block and pattern commands require WordPress 5.0+. +WP_CLI::add_command( + 'block', + 'Block_Command', + array( + 'before_invoke' => function () { + if ( Utils\wp_version_compare( '5.0', '<' ) ) { + WP_CLI::error( 'Requires WordPress 5.0 or greater.' ); + } + }, + ) +); + +WP_CLI::add_command( + 'pattern', + 'Pattern_Command', + array( + 'before_invoke' => function () { + if ( Utils\wp_version_compare( '5.5', '<' ) ) { + WP_CLI::error( 'Requires WordPress 5.5 or greater.' ); + } + }, + ) +); diff --git a/features/block.feature b/features/block.feature new file mode 100644 index 00000000..a3066df8 --- /dev/null +++ b/features/block.feature @@ -0,0 +1,71 @@ +Feature: Manage WordPress block types + + Background: + Given a WP install + + @require-wp-5.0 + Scenario: Listing block types + When I run `wp block list --format=csv` + Then STDOUT should contain: + """ + name,title + """ + And STDOUT should contain: + """ + core/paragraph + """ + + @require-wp-5.0 + Scenario: Listing block types with specific fields + When I run `wp block list --fields=name,title,category` + Then STDOUT should be a table containing rows: + | name | title | category | + | core/paragraph | Paragraph | text | + + @require-wp-5.0 + Scenario: Getting a specific block type + When I run `wp block get core/paragraph --fields=name,title,category` + Then STDOUT should be a table containing rows: + | Field | Value | + | name | core/paragraph | + | title | Paragraph | + | category | text | + + @require-wp-5.0 + Scenario: Getting a non-existent block type + When I try `wp block get core/nonexistent-block` + Then STDERR should contain: + """ + Error: Block type 'core/nonexistent-block' is not registered. + """ + And the return code should be 1 + + @require-wp-5.0 + Scenario: Getting a specific field from a block type + When I run `wp block get core/paragraph --field=title` + Then STDOUT should be: + """ + Paragraph + """ + + @require-wp-5.0 + Scenario: Listing block types in JSON format + When I run `wp block list --format=json` + Then STDOUT should contain: + """ + {"name":"core\/paragraph","title":"Paragraph","description":"Start with the basic building block of all narrative.","category":"text"} + """ + + @require-wp-5.0 + Scenario: Count block types + When I run `wp block list --format=count` + Then STDOUT should match /^\d+$/ + + @less-than-wp-5.0 + Scenario: Block commands require WordPress 5.0+ + When I try `wp block list` + Then STDERR should contain: + """ + Error: Requires WordPress 5.0 or greater. + """ + And the return code should be 1 diff --git a/features/pattern.feature b/features/pattern.feature new file mode 100644 index 00000000..89eaf790 --- /dev/null +++ b/features/pattern.feature @@ -0,0 +1,57 @@ +Feature: Manage WordPress block patterns + + Background: + Given a WP install + + @require-wp-5.5 + Scenario: Listing block patterns + When I run `wp pattern list --format=csv` + Then STDOUT should contain: + """ + name,title + """ + + When I run `wp pattern list --format=json` + Then STDOUT should be JSON containing: + """ + [{"name":"core\/query-standard-posts","title":"Standard"}] + """ + + @require-wp-5.5 + Scenario: Filtering block patterns by category + When I run `wp pattern list --category=buttons --format=count` + Then STDOUT should match /^\d+$/ + + @require-wp-5.5 + Scenario: Getting a specific block pattern + When I run `wp pattern list --format=csv --fields=name` + Then STDOUT should contain: + """ + name + """ + + When I run `wp pattern list --format=count` + Then STDOUT should match /^\d+$/ + + @require-wp-5.5 + Scenario: Getting a non-existent block pattern + When I try `wp pattern get nonexistent/pattern` + Then STDERR should contain: + """ + Error: Block pattern 'nonexistent/pattern' is not registered. + """ + And the return code should be 1 + + @require-wp-5.5 + Scenario: Count block patterns + When I run `wp pattern list --format=count` + Then STDOUT should match /^\d+$/ + + @less-than-wp-5.0 + Scenario: Pattern commands require WordPress 5.5+ + When I try `wp pattern list` + Then STDERR should contain: + """ + Error: Requires WordPress 5.5 or greater. + """ + And the return code should be 1 diff --git a/features/post-blocks.feature b/features/post-blocks.feature new file mode 100644 index 00000000..8bad21ce --- /dev/null +++ b/features/post-blocks.feature @@ -0,0 +1,106 @@ +Feature: Manage WordPress post blocks + + Background: + Given a WP install + + @require-wp-5.0 + Scenario: Check if a post has blocks + When I run `wp post create --post_title='Block post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post has-blocks {POST_ID}` + Then STDOUT should contain: + """ + Success: Post {POST_ID} has blocks. + """ + And the return code should be 0 + + @require-wp-5.0 + Scenario: Check if a post does not have blocks + When I run `wp post create --post_title='Regular post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I try `wp post has-blocks {POST_ID}` + Then STDERR should be empty + And the return code should be 1 + + @require-wp-5.0 + Scenario: Check if a post contains a specific block type + When I run `wp post create --post_title='Block post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post has-block {POST_ID} core/paragraph` + Then STDOUT should contain: + """ + Success: Post {POST_ID} contains the block 'core/paragraph'. + """ + And the return code should be 0 + + When I try `wp post has-block {POST_ID} core/image` + Then STDERR should be empty + And the return code should be 1 + + @require-wp-5.0 + Scenario: Parse blocks from a post + When I run `wp post create --post_title='Block post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post parse-blocks {POST_ID}` + Then STDOUT should be JSON containing: + """ + [ + { + "blockName": "core/paragraph", + "attrs": [], + "innerBlocks": [], + "innerHTML": "

Hello World

", + "innerContent": [ + "

Hello World

" + ] + } + ] + """ + + When I run `wp post parse-blocks {POST_ID} --format=yaml` + Then STDOUT should contain: + """ + blockName: + """ + And STDOUT should contain: + """ + core/paragraph + """ + + @require-wp-5.0 + Scenario: Render blocks from a post + When I run `wp post create --post_title='Block post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post render-blocks {POST_ID}` + Then STDOUT should contain: + """ +

Hello World

+ """ + + @require-wp-5.0 + Scenario: Post get command includes block_version field + When I run `wp post create --post_title='Block post' --post_content='

Hello World

' --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post get {POST_ID} --field=block_version` + Then STDOUT should match /^\d+$/ + + @less-than-wp-5.0 + Scenario: Post block commands require WordPress 5.0+ + When I try `wp post has-blocks 1` + Then STDERR should contain: + """ + Error: This command requires WordPress 5.0 or greater. + """ + And the return code should be 1 diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 2df6f26d..287aec77 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -69,6 +69,8 @@ */src/Site(_Meta|_Option)?_Command\.php$ */src/Term(_Meta)?_Command\.php$ */src/User(_Application_Password|_Meta|_Session|_Term)?_Command\.php$ + */src/Block_Command\.php$ + */src/Pattern_Command\.php$ diff --git a/src/Block_Command.php b/src/Block_Command.php new file mode 100644 index 00000000..c2b85380 --- /dev/null +++ b/src/Block_Command.php @@ -0,0 +1,219 @@ +] + * : Prints the value of a single field for each block type. + * + * [--fields=] + * : Limit the output to specific block type fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - count + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each block type: + * + * * name + * * title + * * description + * * category + * + * These fields are optionally available: + * + * * parent + * * icon + * * keywords + * * textdomain + * * supports + * * styles + * * variations + * * api_version + * * editor_script + * * editor_style + * * script + * * style + * + * ## EXAMPLES + * + * # List all registered block types + * $ wp block list + * +-------------------+-------------------+----------------------------------------+----------+ + * | name | title | description | category | + * +-------------------+-------------------+----------------------------------------+----------+ + * | core/paragraph | Paragraph | Start with the building block of all.. | text | + * | core/heading | Heading | Introduce new sections and organize... | text | + * +-------------------+-------------------+----------------------------------------+----------+ + * + * # List all block types with 'text' category + * $ wp block list --format=csv + * name,title,description,category + * core/paragraph,Paragraph,"Start with the building block of all narrative.",text + * + * @subcommand list + */ + public function list_( $args, $assoc_args ) { + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + $items = []; + foreach ( $blocks as $block ) { + $items[] = $this->prepare_block_for_output( $block ); + } + + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_items( $items ); + } + + /** + * Gets details about a registered block type. + * + * ## OPTIONS + * + * + * : Block type name (e.g., core/paragraph). + * + * [--field=] + * : Instead of returning the whole block type, returns the value of a single field. + * + * [--fields=] + * : Limit the output to specific fields. Defaults to all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * * name + * * title + * * description + * * category + * * parent + * * icon + * * keywords + * * textdomain + * * supports + * * styles + * * variations + * * api_version + * * editor_script + * * editor_style + * * script + * * style + * + * ## EXAMPLES + * + * # Get details about the core/paragraph block type. + * $ wp block get core/paragraph --fields=name,title,category + * +----------------+-----------+----------+ + * | name | title | category | + * +----------------+-----------+----------+ + * | core/paragraph | Paragraph | text | + * +----------------+-----------+----------+ + */ + public function get( $args, $assoc_args ) { + $block_name = $args[0]; + $registry = WP_Block_Type_Registry::get_instance(); + $block = $registry->get_registered( $block_name ); + + if ( ! $block ) { + WP_CLI::error( "Block type '{$block_name}' is not registered." ); + } + + $data = $this->prepare_block_for_output( $block ); + + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_item( $data ); + } + + /** + * Prepares block data for output. + * + * @param WP_Block_Type $block Block type object. + * @return array Prepared block data. + */ + private function prepare_block_for_output( $block ) { + return [ + 'name' => $block->name, + 'title' => $block->title ?? '', // @phpstan-ignore-line (added in WP 5.5) + 'description' => $block->description ?? '', // @phpstan-ignore-line (added in WP 5.5) + 'category' => $block->category ?? '', + 'parent' => $block->parent ?? null, + 'icon' => $block->icon ?? '', + 'keywords' => $block->keywords ?? [], // @phpstan-ignore-line (added in WP 5.5) + 'textdomain' => $block->textdomain ?? '', + 'supports' => $block->supports ?? [], + 'styles' => $block->styles ?? [], // @phpstan-ignore-line (added in WP 5.5) + 'variations' => $block->variations ?? [], // added in WP 5.8 and replaced with magic getter in 6.1 + 'api_version' => $block->api_version ?? 1, // @phpstan-ignore-line (added in WP 5.6) + 'editor_script' => $block->editor_script ?? '', + 'editor_style' => $block->editor_style ?? '', + 'script' => $block->script ?? '', + 'style' => $block->style ?? '', + ]; + } + + /** + * Gets a formatter instance. + * + * @param array $assoc_args Associative arguments. + * @return Formatter Formatter instance. + */ + private function get_formatter( &$assoc_args ) { + return new Formatter( $assoc_args, $this->fields, 'block' ); + } +} diff --git a/src/Pattern_Command.php b/src/Pattern_Command.php new file mode 100644 index 00000000..fe2b2b23 --- /dev/null +++ b/src/Pattern_Command.php @@ -0,0 +1,212 @@ +] + * : Filter patterns by category slug. + * + * [--field=] + * : Prints the value of a single field for each pattern. + * + * [--fields=] + * : Limit the output to specific pattern fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - count + * - yaml + * - ids + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each pattern: + * + * * name + * * title + * + * These fields are optionally available: + * + * * description + * * content + * * categories + * * keywords + * * viewportWidth + * * blockTypes + * * inserter + * + * ## EXAMPLES + * + * # List all registered block patterns + * $ wp pattern list + * +---------------------------+---------------------------+ + * | name | title | + * +---------------------------+---------------------------+ + * | core/text-three-columns | Three Columns of Text | + * +---------------------------+---------------------------+ + * + * # List patterns in a specific category + * $ wp pattern list --category=buttons + * + * # List patterns with all fields + * $ wp pattern list --format=json + * + * @subcommand list + */ + public function list_( $args, $assoc_args ) { + $registry = WP_Block_Patterns_Registry::get_instance(); + $patterns = $registry->get_all_registered(); + + // Filter by category if specified. + if ( isset( $assoc_args['category'] ) ) { + $category = $assoc_args['category']; + $patterns = array_filter( + $patterns, + function ( $pattern ) use ( $category ) { + return isset( $pattern['categories'] ) && in_array( $category, $pattern['categories'], true ); + } + ); + unset( $assoc_args['category'] ); + } + + $items = array(); + foreach ( $patterns as $pattern ) { + $items[] = $this->prepare_pattern_for_output( $pattern ); + } + + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_items( $items ); + } + + /** + * Gets details about a registered block pattern. + * + * ## OPTIONS + * + * + * : Pattern name. + * + * [--field=] + * : Instead of returning the whole pattern, returns the value of a single field. + * + * [--fields=] + * : Limit the output to specific fields. Defaults to all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * * name + * * title + * * description + * * content + * * categories + * * keywords + * * viewportWidth + * * blockTypes + * * inserter + * + * ## EXAMPLES + * + * # Get details about a specific block pattern. + * $ wp pattern get core/text-three-columns + * +-------------+---------------------------+ + * | Field | Value | + * +-------------+---------------------------+ + * | name | core/text-three-columns | + * | title | Three Columns of Text | + * | description | ... | + * +-------------+---------------------------+ + */ + public function get( $args, $assoc_args ) { + $pattern_name = $args[0]; + $registry = WP_Block_Patterns_Registry::get_instance(); + $pattern = $registry->get_registered( $pattern_name ); + + if ( ! $pattern ) { + WP_CLI::error( "Block pattern '{$pattern_name}' is not registered." ); + } + + $data = $this->prepare_pattern_for_output( $pattern ); + + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_item( $data ); + } + + /** + * Prepares pattern data for output. + * + * @param array $pattern Pattern data. + * @return array Prepared pattern data. + */ + private function prepare_pattern_for_output( $pattern ) { + return array( + 'name' => $pattern['name'] ?? '', + 'title' => $pattern['title'] ?? '', + 'description' => $pattern['description'] ?? '', + 'content' => $pattern['content'] ?? '', + 'categories' => $pattern['categories'] ?? array(), + 'keywords' => $pattern['keywords'] ?? array(), + 'viewportWidth' => $pattern['viewportWidth'] ?? null, + 'blockTypes' => $pattern['blockTypes'] ?? array(), + 'inserter' => $pattern['inserter'] ?? true, + ); + } + + /** + * Gets a formatter instance. + * + * @param array $assoc_args Associative arguments. + * @return Formatter Formatter instance. + */ + private function get_formatter( &$assoc_args ) { + return new Formatter( $assoc_args, $this->fields, 'pattern' ); + } +} diff --git a/src/Post_Command.php b/src/Post_Command.php index b8dd833d..26729a4f 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -434,6 +434,11 @@ public function get( $args, $assoc_args ) { $post_arr['url'] = get_permalink( $post->ID ); } + // Add block_version field if the function exists (WordPress 5.0+). + if ( function_exists( 'block_version' ) ) { + $post_arr['block_version'] = block_version( $post->post_content ); + } + if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $post_arr ); } @@ -1059,6 +1064,145 @@ public function exists( $args ) { } } + /** + * Checks whether a post has blocks. + * + * ## OPTIONS + * + * + * : The ID of the post to check. + * + * ## EXAMPLES + * + * # Check if post has blocks + * $ wp post has-blocks 123 + * Success: Post 123 has blocks. + * + * @subcommand has-blocks + */ + public function has_blocks( $args, $assoc_args ) { + if ( ! function_exists( 'has_blocks' ) ) { + WP_CLI::error( 'This command requires WordPress 5.0 or greater.' ); + } + + $post = $this->fetcher->get_check( $args[0] ); + + if ( has_blocks( $post->post_content ) ) { + WP_CLI::success( "Post {$post->ID} has blocks." ); + } else { + WP_CLI::halt( 1 ); + } + } + + /** + * Checks whether a post contains a specific block type. + * + * ## OPTIONS + * + * + * : The ID of the post to check. + * + * + * : Block type name (e.g., core/paragraph). + * + * ## EXAMPLES + * + * # Check if post contains core/paragraph block + * $ wp post has-block 123 core/paragraph + * Success: Post 123 contains the block 'core/paragraph'. + * + * @subcommand has-block + */ + public function has_block( $args, $assoc_args ) { + if ( ! function_exists( 'has_block' ) ) { + WP_CLI::error( 'This command requires WordPress 5.0 or greater.' ); + } + + $post = $this->fetcher->get_check( $args[0] ); + $block_name = $args[1]; + + if ( has_block( $block_name, $post->post_content ) ) { + WP_CLI::success( "Post {$post->ID} contains the block '{$block_name}'." ); + } else { + WP_CLI::halt( 1 ); + } + } + + /** + * Parses blocks from a post and returns the block structure. + * + * ## OPTIONS + * + * + * : The ID of the post to parse blocks from. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: json + * options: + * - json + * - yaml + * --- + * + * ## EXAMPLES + * + * # Parse blocks from post and output as JSON + * $ wp post parse-blocks 123 + * [{"blockName":"core/paragraph","attrs":{},"innerBlocks":[],"innerHTML":"

Hello World

","innerContent":["

Hello World

"]}] + * + * # Parse blocks and output as YAML + * $ wp post parse-blocks 123 --format=yaml + * + * @subcommand parse-blocks + */ + public function parse_blocks( $args, $assoc_args ) { + if ( ! function_exists( 'parse_blocks' ) ) { + WP_CLI::error( 'This command requires WordPress 5.0 or greater.' ); + } + + $post = $this->fetcher->get_check( $args[0] ); + $blocks = parse_blocks( $post->post_content ); + + $fields = [ + 'blockName', + 'attrs', + 'innerBlocks', + 'innerHTML', + 'innerContent', + ]; + + $formatter = new \WP_CLI\Formatter( $assoc_args, $fields ); + $formatter->display_items( $blocks ); + } + + /** + * Renders blocks from a post and returns the final HTML. + * + * ## OPTIONS + * + * + * : The ID of the post to render blocks from. + * + * ## EXAMPLES + * + * # Render blocks from post + * $ wp post render-blocks 123 + *

Hello World

+ * + * @subcommand render-blocks + */ + public function render_blocks( $args, $assoc_args ) { + if ( ! function_exists( 'do_blocks' ) ) { + WP_CLI::error( 'This command requires WordPress 5.0 or greater.' ); + } + + $post = $this->fetcher->get_check( $args[0] ); + $html = do_blocks( $post->post_content ); + + WP_CLI::line( $html ); + } + /** * Convert a date-time string with a hyphen separator to a space separator. *