Make WordPress Core

Opened 4 months ago

Closed 2 months ago

Last modified 3 weeks ago

#59209 closed enhancement (fixed)

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

Reported by: dmsnell's profile dmsnell Owned by: bernhard-reiter's profile Bernhard Reiter
Milestone: 6.4 Priority: normal
Severity: normal Version: 6.4
Component: HTML API Keywords: has-patch has-unit-tests commit
Focuses: Cc:

Description

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' ).

Change History (12)

This ticket was mentioned in PR #5096 on WordPress/wordpress-develop by @dmsnell.


4 months ago
#1

  • Keywords has-unit-tests added

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' ).

@Bernhard Reiter commented on PR #5096:


3 months ago
#2

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

<details>

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">
 		[[Image(https://i0.wp.com/sergioestevaofolio.files.wordpress.com/2016/09/cropped-img_9054-1.jpg?w=190)]]
+<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-[https://github.com/WordPress/wordpress-develop/actions/runs/5982522041/job/16231701670?pr=5096#step:14:683 682]x1024.jpg">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/09/test-image-edited-1-682x1024.jpg)]]</a></figure>
 		<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg)]]</a></figure>
 		<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x[https://github.com/WordPress/wordpress-develop/actions/runs/5982522041/job/16231701670?pr=5096#step:14:684 683].jpg">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg)]]</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">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/09/test-image-edited-1-682x1024.jpg)]]</a></figure>
 	<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/09/test-image-edited-1024x682.jpg)]]</a></figure>
 	<figure class="wp-block-image size-large"><a href="http://wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg">[[Image(https://i0.wp.com/wptest.local/wp-content/uploads/2020/04/test-image-1024x683.jpg)]]</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

</details>

@dmsnell commented on PR #5096:


3 months ago
#3

@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 commented on PR #5096:


3 months ago
#4

@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 commented on PR #5096:


2 months ago
#5

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

@dmsnell commented on PR #5096:


2 months ago
#6

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 commented on PR #5096:


2 months ago
#7

tests seem to be passing now after my last update.

@Bernhard Reiter commented on PR #5096:


2 months ago
#8

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:

  • tests/phpunit/tests/html-api/wpHtmlTagProcessor.php

    diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
    index 4469f90c4f..4db85070e7 100644
    a b HTML; 
    20702070               $this->assertSame( array( 'one' ), $found_classes, 'Visited multiple copies of the same class name when it should have skipped the duplicates.' );
    20712071       }
    20722072
     2073       public function test_class_list_finds_unflushed_class_names() {
     2074               $p = new WP_HTML_Tag_Processor( '<div class="one">' );
     2075               $p->next_tag();
     2076
     2077               $p->add_class( 'two' );
     2078
     2079               $found_classes = array();
     2080               foreach ( $p->class_list() as $class ) {
     2081                       $found_classes[] = $class;
     2082               }
     2083               $this->assertSame( array( 'one', 'two' ), $found_classes, 'Failed to report class names added via add_class.' );
     2084       }
     2085
     2086
    20732087       /**
    20742088        * @ticket 59209
    20752089        *

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.

#9 @Bernhard Reiter
2 months ago

  • Keywords commit added
  • Milestone changed from Awaiting Review to 6.4

#10 @Bernhard Reiter
2 months ago

  • Owner set to Bernhard Reiter
  • Resolution set to fixed
  • Status changed from new to closed

In 56703:

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

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' ).

Props dmsnell.
Fixes #59209.

@dmsnell commented on PR #5096:


2 months ago
#12

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.

Note: See TracTickets for help on using tickets.