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

HTML API: Add class name utilities has_class() and class_list() #5096

Closed

Conversation

dmsnell
Copy link
Member

@dmsnell dmsnell commented Aug 26, 2023

Status:


Trac ticket: #59209

This patch adds two new public methods to the HTML Tag Processor:

  • has_class() indicates if a matched tag contains a given CSS class name.
  • class_list() returns a generator to iterate over all the class names in a matched tag.

Included in this patch is a refactoring of the internal logic when matching a tag to reuse the new has_class() function. Previously it was relying on optimized code in the matches() function which performed byte-for-byte class name comparison. With the change in this patch it will perform class name matching on the decoded value, which might differ if a class attribute contains character references.

These methods may be useful for running more complicated queries based on the presence or absence of CSS class names. The use of these methods avoids the need to manually decode the class attribute as reported by $process->get_attribute( 'class' ).

@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch from 219de8a to 8d49680 Compare August 26, 2023 03:16
@ockham
Copy link
Contributor

ockham commented Aug 28, 2023

Unit tests are currently failing with what seems to be related to this change.

1) Tests_Blocks_Render::test_do_block_output with data set #9 ('core__columns.html', 'core__columns.server.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__columns.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
-<div class="wp-block-columns has-3-columns is-layout-flex wp-container-1 wp-block-columns-is-layout-flex">
+<div class="wp-block-columns has-3-columns">
 	<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
 		<p>Column One, Paragraph One</p>
 		<p>Column One, Paragraph Two</p>

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

2) Tests_Blocks_Render::test_do_block_output with data set #10 ('core__columns__deprecated.html', 'core__columns__deprecated.server.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__columns__deprecated.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
-<div class="wp-block-columns has-3-columns is-layout-flex wp-container-1 wp-block-columns-is-layout-flex">
+<div class="wp-block-columns has-3-columns">
 	<p class="layout-column-1">Column One, Paragraph One</p>
 	<p class="layout-column-1">Column One, Paragraph Two</p>
 	<p class="layout-column-2">Column Two, Paragraph One</p>
 	<p class="layout-column-3">Column Three, Paragraph One</p>
 </div>'

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

3) Tests_Blocks_Render::test_do_block_output with data set #21 ('core__gallery-with-caption.html', 'core__gallery-with-caption.server.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__gallery-with-caption.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
 <figure
-	class="wp-block-gallery has-nested-images columns-default is-cropped columns-2 wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex"
+	class="wp-block-gallery has-nested-images columns-default is-cropped columns-2 wp-block-gallery-1"
 >
 	<figure class="wp-block-image size-large">
 		<img data-id="1421"

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

4) Tests_Blocks_Render::test_do_block_output with data set #22 ('core__gallery.html', 'core__gallery.server.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__gallery.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
 <figure
-	class="wp-block-gallery has-nested-images columns-default is-cropped columns-2 wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex"
+	class="wp-block-gallery has-nested-images columns-default is-cropped columns-2 wp-block-gallery-1"
 >
 	<figure class="wp-block-image size-large">
 		<img data-id="1421"

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

5) Tests_Blocks_Render::test_do_block_output with data set #23 ('core__gallery__columns.html', 'core__gallery__columns.server.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__gallery__columns.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
-<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
+<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-1">
 	<figure class="wp-block-image size-large">
 		<img data-id="1421"
 			src="https://sergioestevaofolio.files.wordpress.com/2016/09/cropped-img_9054-1.jpg?w=190"

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

6) Tests_Blocks_Render::test_do_block_output with data set #30 ('core__gallery__deprecated-7.html', 'core__gallery__deprecated-7.s...r.html')
File '/var/www/tests/phpunit/includes/../data/blocks/fixtures/core__gallery__deprecated-7.html' does not match expected value
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '
-<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
+<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1">
 		<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1-[682](https://github.com/WordPress/wordpress-develop/actions/runs/5982522041/job/16231701670?pr=5096#step:14:683)x1024.jpg"><img data-id="705" src="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1-682x1024.jpg" alt="" class="wp-image-705"/></a></figure>
 		<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg"><img data-id="704" src="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg" alt="" class="wp-image-704"/></a></figure>
 		<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x[683](https://github.com/WordPress/wordpress-develop/actions/runs/5982522041/job/16231701670?pr=5096#step:14:684).jpg"><img data-id="703" src="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg" alt="" class="wp-image-703"/></a></figure>
 </figure>
-<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
+<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1">
 	<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1-682x1024.jpg"><img data-id="705" src="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1-682x1024.jpg" alt="" class="wp-image-705"/></a></figure>
 	<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg"><img data-id="704" src="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg" alt="" class="wp-image-704"/></a></figure>
 	<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg"><img data-id="703" src="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg" alt="" class="wp-image-703"/></a></figure>
 	<figcaption class="blocks-gallery-caption">This gallery has a caption</figcaption>
 </figure>'

/var/www/tests/phpunit/tests/blocks/render.php:237
phpvfscomposer:///var/www/vendor/phpunit/phpunit/phpunit:106
/var/www/vendor/bin/phpunit:118

@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch from 8d49680 to b2b68f5 Compare August 28, 2023 19:19
@dmsnell
Copy link
Member Author

dmsnell commented Aug 28, 2023

@ockham I saw those errors but assumed they were unrelated. I'm running them again rebased against an updated trunk and will investigate further if they still fail

@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch from bfbb7c9 to 74ae887 Compare August 28, 2023 23:21
@dmsnell
Copy link
Member Author

dmsnell commented Aug 28, 2023

@ockham quick update: I found some code calling the Tag Processor to search by class name, but it was searching for the whole class attribute including multiple class names. this is something we ran into early on in the adoption: the expectation that add_class (singular) works with classes (plural).

so what broke is that when improving the class_name query parameter to focus on actual class names, it broke code that relied on doing a full byte-for-byte comparison of the value. not only did it perform byte-for-byte comparison before, but it wasn't breaking apart the sought-after class, so whitespace inside that should be splitting class names was treated as one big class name.

I believe we want to maintain the new behavior as it's going to be a faulty endeavor to search by class name with multiple classes. simple reordering of class names will break that. I'm going to update the code that depends on this behavior.

@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch 5 times, most recently from 790fdd3 to 354fc0e Compare August 29, 2023 18:02
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block-supports code is unfortunately also duplicated in Gutenberg -- outside of lib/compat (e.g. layout.php). We'll likely need to update that file, and make sure that any viable combination of Core and Gutenberg versions will continue to work 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Started an update in WordPress/gutenberg#54075.

I'm thinking we can push the layout change first into Gutenberg and delay this PR. Since Gutenberg relies on gutenberg_-prefixed functions, are we going to face problems or does that kind of free us as long as the same change is applied to Core?

dmsnell added a commit to WordPress/gutenberg that referenced this pull request Aug 30, 2023
…lasses.

In #45364 (WordPress/wordpress-develop#3976) the Block Supports was extended to
add layout class names using the HTML API, new in WordPress 6.2. The initial
patch opened up two opportunities to refine the code, however:

 - There are multiple instances of the `WP_HTML_Tag_Processor` created when a
   single one suffices. (There is an exception in that a second processor is
   necessary in order to find an inner block wrapper).

 - The code relies on the incidental fact that searching by a whitespace-separated
   list of class names works if the class names in the target tag are in the same
   order.

In this patch the use of the HTML API is refactored to address these opportunities
and clean up a few places where there could be stronger consistency with other use
patterns of the HTML API:

 - Multiple instances of the Tag Processor have been combined to remove overhead,
   extra variables, and code confusion. The new flow is more linear throughout the
   function instead of branching.

 - Updated HTML is returned via `get_updated_html()` instead of casting to a string.

 - The matching logic to find the inner block wrapper has been commented and the
   condition uses the null-coalescing operator now that WordPress requires PHP 7.0+.

 - When attempting to find the inner block wrapper at the end, a custom comparison
   is made against the `class` attribute instead of relying on `next_tag()` to find
   a tag with the given set of class names.

The last refactor is important as a preliminary step to WordPress/wordpress-develop#5096
where `has_class()` and `class_list()` methods are being introduced to the Tag Processor.
In that patch the implicit functionality of matching `'class_name' => 'more than one class'`
is removed since that's not a single class name, but many.
dmsnell added a commit to WordPress/gutenberg that referenced this pull request Sep 1, 2023
…lasses.

In #45364 (WordPress/wordpress-develop#3976) the Block Supports was extended to
add layout class names using the HTML API, new in WordPress 6.2. The initial
patch opened up two opportunities to refine the code, however:

 - There are multiple instances of the `WP_HTML_Tag_Processor` created when a
   single one suffices. (There is an exception in that a second processor is
   necessary in order to find an inner block wrapper).

 - The code relies on the incidental fact that searching by a whitespace-separated
   list of class names works if the class names in the target tag are in the same
   order.

In this patch the use of the HTML API is refactored to address these opportunities
and clean up a few places where there could be stronger consistency with other use
patterns of the HTML API:

 - Multiple instances of the Tag Processor have been combined to remove overhead,
   extra variables, and code confusion. The new flow is more linear throughout the
   function instead of branching.

 - Updated HTML is returned via `get_updated_html()` instead of casting to a string.

 - The matching logic to find the inner block wrapper has been commented and the
   condition uses the null-coalescing operator now that WordPress requires PHP 7.0+.

 - When attempting to find the inner block wrapper at the end, a custom comparison
   is made against the `class` attribute instead of relying on `next_tag()` to find
   a tag with the given set of class names.

The last refactor is important as a preliminary step to WordPress/wordpress-develop#5096
where `has_class()` and `class_list()` methods are being introduced to the Tag Processor.
In that patch the implicit functionality of matching `'class_name' => 'more than one class'`
is removed since that's not a single class name, but many.
dmsnell added a commit to WordPress/gutenberg that referenced this pull request Sep 4, 2023
…lasses.

In #45364 (WordPress/wordpress-develop#3976) the Block Supports was extended to
add layout class names using the HTML API, new in WordPress 6.2. The initial
patch opened up two opportunities to refine the code, however:

 - There are multiple instances of the `WP_HTML_Tag_Processor` created when a
   single one suffices. (There is an exception in that a second processor is
   necessary in order to find an inner block wrapper).

 - The code relies on the incidental fact that searching by a whitespace-separated
   list of class names works if the class names in the target tag are in the same
   order.

In this patch the use of the HTML API is refactored to address these opportunities
and clean up a few places where there could be stronger consistency with other use
patterns of the HTML API:

 - Multiple instances of the Tag Processor have been combined to remove overhead,
   extra variables, and code confusion. The new flow is more linear throughout the
   function instead of branching.

 - Updated HTML is returned via `get_updated_html()` instead of casting to a string.

 - The matching logic to find the inner block wrapper has been commented and the
   condition uses the null-coalescing operator now that WordPress requires PHP 7.0+.

 - When attempting to find the inner block wrapper at the end, a custom comparison
   is made against the `class` attribute instead of relying on `next_tag()` to find
   a tag with the given set of class names.

The last refactor is important as a preliminary step to WordPress/wordpress-develop#5096
where `has_class()` and `class_list()` methods are being introduced to the Tag Processor.
In that patch the implicit functionality of matching `'class_name' => 'more than one class'`
is removed since that's not a single class name, but many.
@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch from 621ca75 to 01d0e77 Compare September 19, 2023 23:16
dmsnell added a commit to WordPress/gutenberg that referenced this pull request Sep 19, 2023
@dmsnell
Copy link
Member Author

dmsnell commented Sep 26, 2023

@ockham with the merge of #5252 I think this one is also ready to go

This patch adds two new public methods to the HTML Tag Processor:
 - `has_class()` indicates if a matched tag contains a given CSS class name.
 - `class_list()` returns a generator to iterate over all the class names in a matched tag.

Included in this patch is a refactoring of the internal logic when matching
a tag to reuse the new `has_class()` function. Previously it was relying on
optimized code in the `matches()` function which performed byte-for-byte
class name comparison. With the change in this patch it will perform class
name matching on the decoded value, which might differ if a class attribute
contains character references.

These methods may be useful for running more complicated queries based
on the presence or absence of CSS class names. The use of these methods
avoids the need to manually decode the class attribute as reported by
`$process->get_attribute( 'class' )`.
@dmsnell dmsnell force-pushed the html-api/add-class-name-utilities branch from 01d0e77 to 1521aa8 Compare September 26, 2023 05:24
@dmsnell
Copy link
Member Author

dmsnell commented Sep 26, 2023

Unit tests are currently failing with what seems to be related to this change.

oh darn I missed this, and I won't be able to address it before you're active. I'll try and prioritize it tomorrow and see if it's something small

@dmsnell
Copy link
Member Author

dmsnell commented Sep 26, 2023

tests seem to be passing now after my last update.

@ockham
Copy link
Contributor

ockham commented Sep 26, 2023

I was curious if class_list() would pick up classes added via add_class() (which are only flushed into the class attribute when class_name_updates_to_attributes_updates() is run), remembering this problem we had in a similar scenario with attributes.

Fortunately, it seems to work:

diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
index 4469f90c4f..4db85070e7 100644
--- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
+++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
@@ -2070,6 +2070,20 @@ HTML;
                $this->assertSame( array( 'one' ), $found_classes, 'Visited multiple copies of the same class name when it should have skipped the duplicates.' );
        }
 
+       public function test_class_list_finds_unflushed_class_names() {
+               $p = new WP_HTML_Tag_Processor( '<div class="one">' );
+               $p->next_tag();
+
+               $p->add_class( 'two' );
+
+               $found_classes = array();
+               foreach ( $p->class_list() as $class ) {
+                       $found_classes[] = $class;
+               }
+               $this->assertSame( array( 'one', 'two' ), $found_classes, 'Failed to report class names added via add_class.' );
+       }
+
+
        /**
         * @ticket 59209
         *

I won't add this test for now, but we might want to consider adding a bit of coverage in a follow-up for how changes made by add_class and remove_class are picked up by class_list and has_class.

@ockham
Copy link
Contributor

ockham commented Sep 26, 2023

Committed to Core in https://core.trac.wordpress.org/changeset/56703.

@ockham ockham closed this Sep 26, 2023
@dmsnell
Copy link
Member Author

dmsnell commented Sep 26, 2023

Fortunately, it seems to work:

this is the OO principle of delegation at work here. by the way, I had the same thought and double-checked at some point early on. as long as don't reach into our own internals inside our internals then all these invariants should remain in place.

good thinking for double-checking this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants