Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
79df64b
Update BASE_TAG default version to 6.8.0
mukeshpanchal27 Apr 16, 2026
57bd3b6
Merge branch 'WordPress:trunk' into trunk
mukeshpanchal27 Apr 30, 2026
98aa939
Return early if no custom CSS class is provided
mukeshpanchal27 Apr 30, 2026
391f5b7
Update default BASE_TAG version to 6.7.0
mukeshpanchal27 Apr 30, 2026
ac2597e
Merge branch 'trunk' into perf/10777
mukeshpanchal27 Apr 30, 2026
7e5a7d0
Merge branch 'trunk' into perf/10777
mukeshpanchal27 May 1, 2026
a331593
Refactor custom CSS support validation logic
mukeshpanchal27 May 1, 2026
68c4f7e
Merge branch 'trunk' into perf/10777
mukeshpanchal27 May 6, 2026
439b39f
Apply suggestions from code review
mukeshpanchal27 May 6, 2026
87d952c
Merge branch 'trunk' into perf/10777
westonruter May 10, 2026
a291f83
Guard against malformed attrs from parsed block
westonruter May 10, 2026
d637df3
Guard against non-string className in attrs
westonruter May 10, 2026
fba41c3
Harden types for wp_render_custom_css_class_name()
westonruter May 10, 2026
62781ae
Omit irrelevant types from phpdoc for wp_render_custom_css_support_st…
westonruter May 10, 2026
0bbcfe2
Merge branch 'trunk' into perf/10777
mukeshpanchal27 May 11, 2026
aa22994
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter May 12, 2026
2fc9025
Use phpdoc tag for function reference
westonruter May 12, 2026
5eb8c72
Block Supports: Short-circuit custom CSS class rendering with a cheap…
westonruter May 12, 2026
bd93fe8
Ensure wp-custom-css-* class is not the suffix of another class after…
westonruter May 12, 2026
a89feac
Leverage HTML Tag Processor to parse out wp-custom-css-* class name
westonruter May 12, 2026
e002ecf
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter May 13, 2026
3313806
Add test cases
westonruter May 13, 2026
833170a
Use strtok() instead of HTML Tag Processor
westonruter May 13, 2026
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
62 changes: 48 additions & 14 deletions src/wp-includes/block-supports/custom-css.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,28 @@
*
* @param array $parsed_block The parsed block.
* @return array The same parsed block with custom CSS class name added if appropriate.
*
* @phpstan-param array{
* blockName: string|null,
* attrs: array{
* className?: string,
* style?: array{
* css?: string,
* ...
* },
* ...
* },
* ...
* } $parsed_block
*/
function wp_render_custom_css_support_styles( $parsed_block ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );

if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
$custom_css = $parsed_block['attrs']['style']['css'] ?? null;
if ( ! is_string( $custom_css ) || '' === trim( $custom_css ) ) {
return $parsed_block;
}

$custom_css = trim( $parsed_block['attrs']['style']['css'] ?? '' );

if ( empty( $custom_css ) ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
return $parsed_block;
}

Expand All @@ -32,9 +43,10 @@ function wp_render_custom_css_support_styles( $parsed_block ) {
}

// Generate a unique class name for this block instance.
$class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
$updated_class_name = isset( $parsed_block['attrs']['className'] )
? $parsed_block['attrs']['className'] . " $class_name"
$class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
$existing_class_name = $parsed_block['attrs']['className'] ?? null;
$updated_class_name = is_string( $existing_class_name )
? "$existing_class_name $class_name"
: $class_name;

_wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name );
Expand Down Expand Up @@ -68,28 +80,50 @@ function wp_enqueue_block_custom_css() {
/**
* Applies the custom CSS class name to the block's rendered HTML.
*
* The class name is generated in `wp_render_custom_css_support_styles`
* The class name is generated in {@see wp_render_custom_css_support_styles()}
* and stored in block attributes. This filter adds it to the actual markup.
*
* @since 7.0.0
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
*
* @phpstan-param array{
* attrs: array{
* className?: string,
* ...
* },
* ...
* } $block
*/
function wp_render_custom_css_class_name( $block_content, $block ) {
$class_string = $block['attrs']['className'] ?? '';
preg_match( '/\bwp-custom-css-\S+\b/', $class_string, $matches );
$class_name_attr = $block['attrs']['className'] ?? null;

if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, 'wp-custom-css-' ) ) {
return $block_content;
}

if ( empty( $matches ) ) {
// Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles().
$custom_class_name = null;
$token_delimiter = " \t\f\r\n";
$class_token = strtok( $class_name_attr, $token_delimiter );
while ( false !== $class_token ) {
if ( str_starts_with( $class_token, 'wp-custom-css-' ) ) {
$custom_class_name = $class_token;
break;
}
$class_token = strtok( $token_delimiter );
}
Comment on lines +103 to +117
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dmsnell I'd appreciate your feedback on this approach.

This ensures correctness, as it won't incorrectly match my-wp-custom-css-0acafc53 as previously due to the \b word boundary matching at hyphens.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note the additional computation involved for WP_HTML_Tag_Processor is guarded against via the str_contains( $class_name_attr, 'wp-custom-css-' ) check above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also add a unit test to validate the edge cases that WP_HTML_Tag_Processor covers (vs the regex)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ramonjd citing your comment here from WordPress/gutenberg#78217 (review) to keep reviews on the canonical PR:

Tentative approval from me pending feedback on the use of WP_HTML_Tag_Processor

WP_HTML_Tag_Processor-based lookup of the generated wp-custom-css-* class, so token matching matches how the class was added and how add_class() reads it.

Just so I understand the intention here, is idea that class_list() is the canonical tokenizer for HTML class attributes, and add_class() uses that same tokenization internally to avoid duplicates? In other words, same input/output for all operations?

For custom CSS the class gets written to $block['attrs']['className'] in gutenberg_render_custom_css_support_styles() via plain string concatenation.

I guess the WP_HTML_Tag_Processor approach does future-proof things if write side changes later.

Yes, the intention is to avoid reinventing the wheel for parsing class names. Leveraging the HTML Tag Processor as a utility like this is something I learned from @sirreal when populating text in a script or style tag. I recall @dmsnell also intended to expose a static method that provided a wrapper to do this parsing.

Alternatively, I first tried just improving the regex in bd93fe8 but then I decided to go down the route of using the tag processor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I recall … a static method that provided a wrapper to do this parsing.

I believe that's #10043 proposing a class list utility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I took a look at this and did some testing. I think this HTML round-trip to extract the class name is safe to avoid here.

Consider this non-blocking feedback. There are merits to using the tag processor for greater consistency and correctness guarantees.

Given the performance focus of this PR, I'll leave feedback on alternative strictly for the purpose of your consideration. I'm happy to leave the final call up to @westonruter. There shouldn't be any downside to using the tag processor here aside from some possibly redundant overhead.


  • $block_content contains HTML. The class attribute there appears to have passed through esc_attr().
  • $class_name_attr is a plain string, it does not include escaped HTML and is not encoded.
  • Going through the tag processor will ensure that the class names are correctly split. There's a round-trip to HTML encoding and decoding that should not be necessary here. There's a hidden normalization here that should not be necessary since the $class_name_attr is a plain text representation.
  • Using the tag processor will do a number of things:
    • Deduplicate class names (not relevant)
    • Normalize null bytes (not relevant)
    • Split the string correctly 👍
    • Lowercase class names in quirks mode (not relevant, not using quirks mode)

Given we don't need normalization at this phase and we'll later add the plaintext class name using the add_class() method, I think we can use some simpler string whitespace splitting that matches HTML rules for a set of space-separated tokens (like the class attribute).

In short, I think it's safe to save some additional overhead and split like this:

$class_name_attr = "custom&super GREAT\n\r\f\t  <swell>\nwp-custom-css-f493a295";
$space_separated_token_delimiter = " \t\f\r\n";
$class_token = strtok( $class_name_attr, $space_separated_token_delimiter );
while ( false !== $class_token ) {
    var_dump( $class_token ); // Test for matching class name here.
    $class_token = strtok( $space_separated_token_delimiter );
}

strtok() has some pitfalls, so a regular expression pattern should also work, it just needs to align with how class names are matched according to standards:

// Named pattern match
$pattern = '/(?:\A|[ \t\f\r\n])(?P<CLASS_NAME>wp-custom-css-[^ \t\f\r\n]+)(?=[ \t\f\r\n]|\z)/';
// Same result using `\K` "reset match"
$pattern = '/(?:\A|[ \t\f\r\n])\Kwp-custom-css-[^ \t\f\r\n]+(?=[ \t\f\r\n]|\z)/';
$res = preg_match( $pattern, $class_name_attr, $matches );

Again, please take this as non-blocking feedback. Any of the 3 approaches (tag processor or my suggestions here) seem correct and have different tradeoffs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also add a unit test to validate the edge cases that WP_HTML_Tag_Processor covers (vs the regex)?

@ramonjd Added "prefixed custom CSS class" test case in 3313806 which tests the bug and verifies the fix.

In short, I think it's safe to save some additional overhead and split like this:

@sirreal Thank you. I've implemented strtok() in 833170a.

if ( null === $custom_class_name ) {
return $block_content;
}

$tags = new WP_HTML_Tag_Processor( $block_content );

if ( $tags->next_tag() ) {
$tags->add_class( 'has-custom-css' );
$tags->add_class( $matches[0] );
$tags->add_class( $custom_class_name );
}

return $tags->get_updated_html();
Expand Down
6 changes: 3 additions & 3 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -2604,9 +2604,9 @@ function unregister_block_style( $block_name, $block_style_name ) {
* @since 5.8.0
* @since 6.4.0 The `$feature` parameter now supports a string.
*
* @param WP_Block_Type $block_type Block type to check for support.
* @param string|array $feature Feature slug, or path to a specific feature to check support for.
* @param mixed $default_value Optional. Fallback value for feature support. Default false.
* @param WP_Block_Type|null $block_type Block type to check for support.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change is because the return value of WP_Block_Type_Registry::get_instance()->get_registered() is often passed right into block_has_support(). And the function specifically is checking for whether the arg is a WP_Block_Type.

* @param string|array $feature Feature slug, or path to a specific feature to check support for.
* @param mixed $default_value Optional. Fallback value for feature support. Default false.
* @return bool Whether the feature is supported.
*/
function block_has_support( $block_type, $feature, $default_value = false ) {
Expand Down
2 changes: 2 additions & 0 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,8 @@ public function paused_at_incomplete_token(): bool {
* // Outputs: "free <egg> lang-en "
*
* @since 6.4.0
*
* @return Generator<int, non-empty-string>
*/
public function class_list() {
if ( self::STATE_MATCHED_TAG !== $this->parser_state ) {
Expand Down
32 changes: 30 additions & 2 deletions tests/phpunit/tests/block-supports/wpRenderCustomCssClassName.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,18 @@ public function test_adds_class_to_content( $block_content, $block, $expected_cl
/**
* Data provider.
*
* @return array
* @return array<string, array{
* block_content: string,
* block: array{
* blockName: string,
* attrs: array{
* className: string,
* },
* },
* expected_class: string,
* }>
*/
public function data_adds_class_to_content() {
public function data_adds_class_to_content(): array {
return array(
'class is added to block content' => array(
'block_content' => '<div class="wp-block-paragraph">Test content</div>',
Expand All @@ -53,6 +62,16 @@ public function data_adds_class_to_content() {
),
'expected_class' => 'wp-custom-css-mixed123',
),
'class between whitespace is added' => array(
'block_content' => '<div class="wp-block-paragraph">Test content</div>',
'block' => array(
'blockName' => 'core/paragraph',
'attrs' => array(
'className' => "\twp-custom-css-123abc\t",
),
),
'expected_class' => 'wp-custom-css-123abc',
),
);
}

Expand Down Expand Up @@ -113,6 +132,15 @@ public function data_returns_unchanged_content() {
),
),
),
'prefixed custom CSS class' => array(
'block_content' => '<div class="wp-block-paragraph">Test content</div>',
'block' => array(
'blockName' => 'core/paragraph',
'attrs' => array(
'className' => 'my-wp-custom-css-456def',
),
),
),
'className is not set in attrs' => array(
'block_content' => '<div class="wp-block-paragraph">Test content</div>',
'block' => array(
Expand Down
Loading