alleyinteractive/wp-match-blocks

Match WordPress blocks in the given content.

Installs: 260 184

Dependents: 3

Suggesters: 0

Security: 0

Stars: 9

Watchers: 22

Forks: 1

Open Issues: 3

pkg:composer/alleyinteractive/wp-match-blocks

v4.3.0 2025-11-25 01:02 UTC

README

match_blocks() selects the blocks in post content, or in a given set of blocks, inner blocks, or block HTML, that match the given criteria, such as the block name, block attributes, or position within the set of blocks.

Blocks can be matched by:

  • Block name or names (name)
  • Block attributes (attrs, with_attrs)
  • Block inner HTML (with_innerhtml)
  • The block's positive or negative index within the set (position)
  • Whether the block represents only space (skip_empty_blocks)
  • Whether the block has inner blocks (has_innerblocks)
  • Custom validation classes (is_valid)
  • Xpath queries (__experimental_xpath) (huh?)

Passing matching parameters is optional; all non-empty blocks match by default.

Additionally:

  • Recursion into inner blocks is supported (flatten).
  • The set of matching blocks can be limited by size (limit) or their position in the set of matches (nth_of_type).
  • The number of matches can be returned instead of the matched blocks (count).
  • The companion match_block() function reduces the filtered set of results to a single parsed block.
  • Passing a single block instance will return matches from its inner blocks.

match_blocks() is powered by a set of block validation classes that utilize the Laminas Validator framework and Laminas Validator Extensions package. These validators, along with a base class for validating blocks, are included here. See the validators section for their documentation.

Installation

Install the latest version with:

composer require alleyinteractive/wp-match-blocks

Basic usage

Find all paragraph blocks in a post:

<?php

$grafs = \Alley\WP\match_blocks( $post, [ 'name' => 'core/paragraph' ] );

Include paragraphs in inner blocks:

<?php

$grafs = \Alley\WP\match_blocks(
    $post,
    [
        'flatten' => true,
        'name'    => 'core/paragraph',
    ]
);

Get the number of paragraph blocks:

<?php

$count = \Alley\WP\match_blocks(
    $post,
    [
        'count' => true,
        'name'  => 'core/paragraph',
    ]
);

Get the number of paragraph blocks that are inner blocks of the given group block:

<?php

$blocks = parse_blocks( '<!-- wp:group --><div class="wp-block-group"><!-- wp:paragraph -->…<!-- wp:group /-->' );

$count = \Alley\WP\match_blocks(
    $blocks[0],
    [
        'count' => true,
        'name'  => 'core/paragraph',
    ]
);

Get all paragraphs and headings:

<?php

$blocks = \Alley\WP\match_blocks(
    '<!-- wp:paragraph -->…',
    [
        'name' => [ 'core/heading', 'core/paragraph' ],
    ]
);

Get only paragraphs that have been explicitly aligned:

<?php

$blocks = \Alley\WP\match_blocks(
    [ /* blocks */ ],
    [
        'name'       => 'core/paragraph',
        'with_attrs' => 'align',
    ]
);

Get only paragraphs that have been right-aligned:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'attrs' => [
            [
                'key'   => 'align',
                'value' => 'right',
            ],
        ],
        'name'  => 'core/paragraph',
    ]
);

Get only paragraphs that have been aligned, but not to the right:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'attrs' => [
            [
                'key'      => 'align',
                'value'    => 'right',
                'operator' => '!==',
            ],
        ],
        'name'  => 'core/paragraph',
    ]
);

Get only paragraphs that have been aligned to the left or the right:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'attrs' => [
            [
                'key'      => 'align',
                'value'    => [ 'left', 'right' ],
                'operator' => 'IN',
            ],
        ],
        'name'  => 'core/paragraph',
    ]
);

Get all images credited to the Associated Press:

<?php

$images = \Alley\WP\match_blocks(
    $post,
    [
        'attrs' => [
            [
                'key'      => 'credit',
                'value'    => '/(The )?Associated Press/i',
                'operator' => 'REGEX',
            ],
            [
                'key'   => 'credit',
                'value' => 'AP',
            ],
            'relation' => 'OR',
        ],
        'name'  => 'core/image',
    ]
);

Get shortcode blocks with a specific shortcode:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'           => 'core/shortcode',
        'with_innerhtml' => '[bc_video',
    ]
);

Audit a post for YouTube embed blocks that reference videos that are no longer accessible.

<?php

final class YouTube_Video_Exists extends \Alley\WP\Validator\Block_Validator {
    // ...
}

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'     => 'core/embed',
        'attrs'    => [
            [
                'key'   => 'providerNameSlug',
                'value' => 'youtube',
            ],
        ],
        'is_valid' => new \Alley\Validator\Not( new YouTube_Video_Exists(), '' ),
    ],
);

Get the first three blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'limit' => 3,
    ]
);

Get the first three paragraph blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'limit' => 3,
        'name'  => 'core/paragraph',
    ]
);

Get the third paragraph:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'        => 'core/paragraph',
        'nth_of_type' => 3,
    ]
);

// Or, skip straight to the parsed block:

$block = \Alley\WP\match_block(
    $post,
    [
        'name'        => 'core/paragraph',
        'nth_of_type' => '3n',
    ]
);

Get every third paragraph:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'        => 'core/paragraph',
        'nth_of_type' => '3n',
    ]
);

Get paragraphs 3-8:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'        => 'core/paragraph',
        'nth_of_type' => [ 'n+3', '-n+8' ],
    ]
);

Get the block at position 3 in the set if it's a paragraph:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'     => 'core/paragraph',
        'position' => 3,
    ]
);

Get the last two blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'position' => [ -1, -2 ],
    ]
);

Get all non-empty blocks:

<?php

$blocks = \Alley\WP\match_blocks( $post );

Get all empty blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'              => null,
        'skip_empty_blocks' => false,
    ]
);

Get only blocks with inner blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'has_innerblocks' => true,
    ]
);

Get only blocks without inner blocks:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'has_innerblocks' => false,
    ]
);

Validators

This package provides classes for validating blocks based on the Laminas Validator framework and Laminas Validator Extensions package, plus a custom base block validation class.

match_blocks() also uses these validators internally, and they can be passed as the is_valid parameter to match_blocks() or used on their own.

Base Validator

The abstract Alley\WP\Validator\BlockValidator class extends Alley\Validator\BaseValidator and, much like BaseValidator, standardizes validation of blocks.

When extending BlockValidator, validation logic goes into a test_block() method. test_block() always receives a \WP_Block_Parser_Block instance; validation will automatically fail if the input is not an instance of \WP_Block, \WP_Block_Parser_Block, or an array representation of a block.

Block_Attribute

Alley\WP\Validator\Block_Attribute validates whether the block contains, or does not contain, the specified attribute name, value, or name-value pair.

The block passes if a name comparison is specified, and the block contains an attribute whose name matches the comparison; if a value comparison is specified, and the block contains an attribute whose value matches the comparison; or if both name and value comparisons are specified, and the block contains an attribute with a matching name and value.

Supported options

The following options are supported for Alley\WP\Validator\Block_Attribute:

  • key: The name of a block attribute, or an array of names, or a regular expression pattern. Default none.
  • value: A block attribute value, or an array of values, or regular expression pattern. Default none.
  • operator: The operator with which to compare $value to block attributes. Accepts CONTAINS, NOT CONTAINS (case-sensitive), IN, NOT IN, LIKE, NOT LIKE (case-insensitive), REGEX, NOT REGEX, or any operator supported by \Alley\Validator\Comparison. Default is ===.
  • key_operator: Equivalent to operator but for $key.

Basic usage

<?php

// '<!-- wp:media-text {"mediaId":617,"mediaType":"image","isStackedOnMobile":false,"className":"alignwide"} -->';

$valid = new Alley\WP\Validator\Block_Attribute(
    [
        'key'   => 'mediaType',
        'value' => 'image',
    ],
);

$valid = new Alley\WP\Validator\Block_Attribute(
    [
        'key'          => [ 'mediaType', 'mediaId' ],
        'key_operator' => 'IN',
    ],
);

$valid = new Alley\WP\Validator\Block_Attribute(
    [
        'key'          => '/^media/',
        'key_operator' => 'REGEX',
        'value'        => [ 'image', 'video' ],
        'operator'     => 'IN',
    ],
);

$valid = new Alley\WP\Validator\Block_Attribute(
    [
        'key'          => '/^media/',
        'key_operator' => 'REGEX',
        'value'        => [ 'audio', 'document' ],
        'operator'     => 'NOT IN',
    ],
);

Block_InnerHTML

Alley\WP\Validator\Block_InnerHTML validates whether the block contains, or does not contain, the specified content in its innerHTML property. The block passes if it contains an innerHTML value that matches the comparison.

Supported options

The following options are supported for Alley\WP\Validator\Block_InnerHTML:

  • content: The content to find or a regular expression pattern.
  • operator: The operator with which to compare $content to the block inner HTML. Accepts CONTAINS, NOT CONTAINS (case-sensitive), IN, NOT IN, LIKE, NOT LIKE (case-insensitive), REGEX, NOT REGEX, or any operator supported by \Alley\Validator\Comparison. Default is LIKE.

Basic usage

<?php

// '
// <!-- wp:paragraph -->
// <p>The goal of this new editor is to make adding rich content to WordPress simple and enjoyable.</p>
// <!-- /wp:paragraph -->
// '

$valid = new Alley\WP\Validator\Block_InnerHTML(
    [
        'content'  => 'wordpress',
        'operator' => 'LIKE',
    ],
);

$valid = new Alley\WP\Validator\Block_InnerHTML(
    [
        'content'  => 'WordPress',
        'operator' => 'CONTAINS',
    ],
);

$valid = new Alley\WP\Validator\Block_InnerHTML(
    [
        'content'  => '/^\s*<p>\s*</p>/',
        'operator' => 'NOT REGEX',
    ],
);

Block_Name

Alley\WP\Validator\Block_Name validates whether a block has a given name or one of a set of names. The block passes validation if the block name is in the set.

Supported options

The following options are supported for Alley\WP\Validator\Block_Name:

-name: The block name or names.

Basic usage

<?php

$valid = new Alley\WP\Validator\Block_Name(
    [
        'name' => 'core/paragraph',
    ]
);

$valid = new Alley\WP\Validator\Block_Name(
    [
        'name' => [ 'core/gallery', 'jetpack/slideshow', 'jetpack/tiled-gallery' ],
    ]
);

Block_Offset

Alley\WP\Validator\Block_Offset validates whether the block appears at one of the given numeric offsets within a list of blocks.

The block matches if it appears at one of the offsets in the list.

Identity is determined by comparing the \WP_Block_Parser_Block instances as arrays.

Supported options

The following options are supported for Alley\WP\Validator\Block_Offset:

  • blocks: An array or iterable of blocks.
  • offset: The expected offset or offsets. Negative offsets count from the end of the list.
  • skip_empty_blocks: Whether to skip blocks that are "empty" according to the Nonempty_Block_Validator when indexing $blocks. Default true.

Basic usage

<?php

$blocks = parse_blocks(
    <<<HTML
<!-- wp:paragraph --><p>Hello, world!</p><!-- /wp:paragraph -->

<!-- wp:archives {\"displayAsDropdown\":true,\"showPostCounts\":true} /-->

<!-- wp:media-text {\"mediaId\":617,\"mediaType\":\"image\",\"isStackedOnMobile\":false,\"className\":\"alignwide\"} -->
HTML
);

$valid = new Alley\WP\Validator\Block_Offset(
    [
        'blocks' => $blocks,
        'offset' => 1,
    ],
);
$valid->isValid( $blocks[2] ); // true

$valid = new Alley\WP\Validator\Block_Offset(
    [
        'blocks'            => $blocks,
        'offset'            => [ 4 ],
        'skip_empty_blocks' => false,
    ],
);
$valid->isValid( $blocks[4] ); // true

$valid = new Alley\WP\Validator\Block_Offset(
    [
        'blocks' => $blocks,
        'offset' => -2,
    ],
);
$valid->isValid( $blocks[2] ); // true

Block_InnerBlocks_Count

Alley\WP\Validator\Block_InnerBlocks_Count validates whether the number of inner blocks in the given block passes the specified comparison.

The block passes validation if the comparison is true for the count of inner blocks. Inner blocks within inner blocks don't count towards the total.

Supported options

The following options are supported for Alley\WP\Validator\Block_InnerBlocks_Count:

  • count: The expected number of inner blocks for the comparison.
  • operator: The PHP comparison operator used to compare the input block's inner blocks and count.

Basic usage

<?php

$blocks = parse_blocks(
    <<<HTML
<!-- wp:foo -->
    <!-- wp:bar -->
        <!-- wp:baz /-->
    <!-- /wp:bar -->
<!-- /wp:foo -->
HTML
);

$valid = new \Alley\WP\Validator\Block_InnerBlocks_Count(
    [
        'count'    => 1,
        'operator' => '===',
    ]
);
$valid->isValid( $blocks[0] ); // true

$valid = new \Alley\WP\Validator\Block_InnerBlocks_Count(
    [
        'count'    => 0,
        'operator' => '>',
    ]
);
$valid->isValid( $blocks[0] ); // true

$valid = new \Alley\WP\Validator\Block_InnerBlocks_Count(
    [
        'count'    => 42,
        'operator' => '<=',
    ]
);
$valid->isValid( $blocks[0] ); // true

Nonempty_Block

Alley\WP\Validator\Nonempty_Block validates that the given block is not "empty" -- for example, not a block representing only line breaks.

The block passes validation if it has a non-null name.

Supported options

None.

Basic usage

<?php

$blocks = parse_blocks( "\n" );

$valid = new \Alley\WP\Validator\Nonempty_Block();
$valid->isValid( $blocks[0] ); // false

Matching blocks with XPath

match_blocks() has experimental support for matching blocks with XPath queries. These are made possible by converting the source blocks to a custom XML structure.

This feature may be changed without backwards compatibility in future releases.

Basic usage

Find all paragraph blocks that are inner blocks of a cover block:

<?php

$grafs = \Alley\WP\match_blocks(
    $post,
    [
        '__experimental_xpath' => '//block[blockName="core/cover"]/innerBlocks/block[blockName="core/paragraph"]',
    ],
);

Find list blocks with zero or one list items:

<?php

$lists = \Alley\WP\match_blocks(
    $post,
    [
        '__experimental_xpath' => '//block[blockName="core/list" and count(innerBlocks/block[blockName="core/list-item"]) <= 1]',
    ],
);

Find the second paragraph block:

<?php

$graf = \Alley\WP\match_block(
    $post,
    [
        '__experimental_xpath' => '//block[blockName="core/paragraph"][2]',
    ],
);

Find full-width images:

<?php

$images = \Alley\WP\match_blocks(
    $post,
    [
        '__experimental_xpath' => '//block[blockName="core/image"][attrs/sizeSlug="full"]',
    ],
);

The XML document currently has the following structure:

<blocks>
  <block>
    <blockName />
    <attrs />
    <innerBlocks />
    <innerHTML />
  </block>
</blocks>

For example, this block HTML:

<!-- wp:paragraph -->
<p>The Common category includes the following blocks: <em>Paragraph, image, headings, list, gallery, quote, audio, cover, video.</em></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"align":"right"} -->
<p class="has-text-align-right"><em>This italic paragraph is right aligned.</em></p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":968,"sizeSlug":"full","className":"is-style-circle-mask"} -->
<figure class="wp-block-image size-full is-style-circle-mask"><img src="https://example.com/wp-content/uploads/2013/03/image-alignment-150x150-13.jpg" alt="Image Alignment 150x150" class="wp-image-968"/></figure>
<!-- /wp:image -->

<!-- wp:cover {"url":"https://example.com/wp-content/uploads/2008/06/dsc04563-12.jpg","id":759,"minHeight":274} -->
<div class="wp-block-cover has-background-dim" style="background-image:url(https://example.com/wp-content/uploads/2008/06/dsc04563-12.jpg);min-height:274px">
  <div class="wp-block-cover__inner-container">
    <!-- wp:paragraph {"align":"center","placeholder":"Write title…","fontSize":"large"} -->
    <p class="has-text-align-center has-large-font-size">Cover block with background image</p>
    <!-- /wp:paragraph -->
  </div>
</div>
<!-- /wp:cover -->

will be converted to this XML:

<blocks>
  <block>
    <blockName>core/paragraph</blockName>
    <attrs/>
    <innerBlocks/>
    <innerHTML><![CDATA[
<p>The Common category includes the following blocks: <em>Paragraph, image, headings, list, gallery, quote, audio, cover, video.</em></p>
]]></innerHTML>
  </block>

  <block>
    <blockName>core/paragraph</blockName>
    <attrs>
      <align>right</align>
    </attrs>
    <innerBlocks/>
    <innerHTML><![CDATA[
<p class="has-text-align-right"><em>This italic paragraph is right aligned.</em></p>
]]></innerHTML>
  </block>

  <block>
    <blockName>core/image</blockName>
    <attrs>
      <id>968</id>
      <sizeSlug>full</sizeSlug>
      <className>is-style-circle-mask</className>
    </attrs>
    <innerBlocks/>
    <innerHTML><![CDATA[
<figure class="wp-block-image size-full is-style-circle-mask"><img src="https://example.com/wp-content/uploads/2013/03/image-alignment-150x150-13.jpg" alt="Image Alignment 150x150" class="wp-image-968"/></figure>
]]></innerHTML>
  </block>

  <block>
    <blockName>core/cover</blockName>
    <attrs>
      <url>https://example.com/wp-content/uploads/2008/06/dsc04563-12.jpg</url>
      <id>759</id>
      <minHeight>274</minHeight>
    </attrs>
    <innerBlocks>
      <block>
        <blockName>core/paragraph</blockName>
        <attrs>
          <align>center</align>
          <placeholder>Write title&#x2026;</placeholder>
          <fontSize>large</fontSize>
        </attrs>
        <innerBlocks/>
        <innerHTML><![CDATA[
    <p class="has-text-align-center has-large-font-size">Cover block with background image</p>
    ]]></innerHTML>
      </block>
    </innerBlocks>
    <innerHTML><![CDATA[
<div class="wp-block-cover has-background-dim" style="background-image:url(https://example.com/wp-content/uploads/2008/06/dsc04563-12.jpg);min-height:274px">
  <div class="wp-block-cover__inner-container">

  </div>
</div>
]]></innerHTML>
  </block>
</blocks>

Limitations

Although it's possible to use XPath queries in conjunction with other match_blocks() arguments, the results with some arguments might be unexpected.

Typically, match_blocks() returns the blocks that match all the arguments. But when the __experimental_xpath argument is used, the set of source blocks will be first reduced to the blocks that match the XPath query, and then the remaining arguments will be applied.

For example, compare these sets of arguments:

<?php

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        'name'     => 'core/paragraph',
        'position' => 3,
    ],
);

$blocks = \Alley\WP\match_blocks(
    $post,
    [
        '__experimental_xpath' => '//block[blockName="core/paragraph"]',
        'position'             => 3,
    ],
);

In the top example, the third block in the set of blocks will be returned, but only if it's a paragraph.

In the bottom example, the XPath query will match all paragraphs in the document, regardless of their depth, and then the third paragraph out of that set will be returned.

About

License

GPL-2.0-or-later

Maintainers

Alley Interactive