Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental support for matching blocks with XPath #18

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ This library adheres to [Semantic Versioning](https://semver.org/) and [Keep a C

Nothing yet.

## 4.1.0

### Added

- `__experimental_xpath` parameter for matching blocks by XPath queries.
- `Matched_Blocks` implementation of the `Alley\WP\Types\Blocks` interface from the [Type Extensions](https://github.com/alleyinteractive/wp-type-extensions/) library.

## 4.0.0

### Changed
Expand All @@ -16,13 +23,13 @@ Nothing yet.

### Changed

* Reduce uses of validators within validators.
- Reduce uses of validators within validators.

## 3.0.0

### Changed

* "Classic" blocks, which have inner HTML but no block name, are no longer considered empty.
- "Classic" blocks, which have inner HTML but no block name, are no longer considered empty.

## 2.0.1

Expand Down
201 changes: 201 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Blocks can be matched by:
* 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?](#matching-blocks-with-xpath))

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

Expand Down Expand Up @@ -658,6 +659,206 @@ $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
<?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
<?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
<?php

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

Find full-width images:

```php
<?php

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

The XML document currently has the following structure:

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

For example, this block HTML:

```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:

```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
<?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
Expand Down
9 changes: 6 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
],
"require": {
"php": "^8.2",
"alleyinteractive/composer-wordpress-autoloader": "^1.0.0",
"alleyinteractive/laminas-validator-extensions": "^2.0.0"
"ext-simplexml": "*",
"alleyinteractive/composer-wordpress-autoloader": "^1.0",
"alleyinteractive/laminas-validator-extensions": "^2.0",
"alleyinteractive/wp-type-extensions": "dev-fix/match-blocks",
"symfony/serializer": "^7.2"
},
"require-dev": {
"alleyinteractive/alley-coding-standards": "^2.0",
Expand Down Expand Up @@ -69,4 +72,4 @@
],
"tidy": "[ $COMPOSER_DEV_MODE -eq 0 ] || composer normalize"
}
}
}
39 changes: 39 additions & 0 deletions src/blocks/class-matched-blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* Matched_Blocks class file
*
* @package wp-match-blocks
*/

namespace Alley\WP\Blocks;

use Alley\WP\Types\Serialized_Blocks;

use function Alley\WP\match_blocks;

/**
* Blocks matched with {@see match_blocks()}.
*/
final class Matched_Blocks implements Serialized_Blocks {
/**
* Set up.
*
* @param array<string, mixed> $args Args for {@see match_blocks()}.
* @param Serialized_Blocks $origin Blocks to search.
*/
public function __construct(
private readonly array $args,
private readonly Serialized_Blocks $origin,
) {}

/**
* Serialized block content.
*
* @return string
*/
public function serialized_blocks(): string {
$matched = match_blocks( $this->origin->serialized_blocks(), $this->args );

return \is_array( $matched ) ? serialize_blocks( $matched ) : ''; // @phpstan-ignore-line argument.type
}
}
Loading
Loading