Skip to content

Script Loader: Add an API for filtering inline style attributes - #13250

Open
Sukhendu2002 wants to merge 75 commits into
WordPress:trunkfrom
Sukhendu2002:ticket/51325-inline-style-attributes
Open

Sukhendu2002 wants to merge 75 commits into
WordPress:trunkfrom
Sukhendu2002:ticket/51325-inline-style-attributes

Conversation

@Sukhendu2002

@Sukhendu2002 Sukhendu2002 commented Aug 24, 2026 •

Copy link
Copy Markdown

What this PR does

Adds a structured API for generating and filtering inline <style> tags, mirroring the existing inline script tag API:

  • Adds wp_get_inline_style_tag( $data, array $attributes = array() ).
  • Adds wp_print_inline_style_tag().
  • Adds the wp_inline_style_attributes filter, which receives the attributes and the CSS.
  • Uses WP_HTML_Tag_Processor for safe attribute and CSS handling, including escaping of a closing </style> in the CSS.
  • Documents $attributes as array<string, string|bool>, matching wp_get_inline_script_tag().

This allows plugins to add attributes such as CSP nonces without modifying complete HTML strings. The script portion of the original ticket is already covered by wp_inline_script_attributes, so this PR focuses on the remaining inline style functionality.

Usage in WP_Styles and script-loader.php

The WP_Styles output for normal, no-source, and concatenated inline styles, plus the hoisted extra block library styles and block support styles in script-loader.php, now go through the new API instead of building the tag with WP_HTML_Tag_Processor directly. This is the same way r61485 updated WP_Scripts along with the script tag functions.

Other STYLE tags

At the reviewer's request, the conversion of the other STYLE tags to the new API has been split into separate PRs, which depend on this one:

Testing

New tests in tests/phpunit/tests/dependencies/wpInlineStyleTag.php and tests/phpunit/tests/dependencies/styles.php use assertEqualHTML() and WP_HTML_Tag_Processor to verify the output.

Trac ticket: https://core.trac.wordpress.org/ticket/51325

Use of AI Tools

AI assistance: Yes
Tool(s): OpenAI Codex
Model(s): GPT-5.6
Used for: Initial code skeleton, test suggestions and code review; final implementation and tests were reviewed and edited by me.

Additional commits were made with Claude Code (Claude Opus 5.5) at the direction of @westonruter, to make the tests more robust with assertEqualHTML() and WP_HTML_Tag_Processor, to route the remaining core <style> tags through the new API (since split into #14216 and #14217), and to address code review findings. The changes were reviewed by @westonruter.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@Sukhendu2002
Sukhendu2002 marked this pull request as ready for review August 24, 2026 12:43
@github-actions

github-actions Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props sukhendu2002, westonruter, jonsurrell, huubl.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Comment thread tests/phpunit/tests/dependencies/wpInlineStyleTag.php Outdated
Comment thread tests/phpunit/tests/dependencies/wpInlineStyleTag.php Outdated
Comment thread src/wp-includes/script-loader.php
Comment thread src/wp-includes/script-loader.php
Comment thread src/wp-includes/script-loader.php Outdated
Comment thread src/wp-includes/script-loader.php Outdated
Comment thread src/wp-includes/script-loader.php Outdated
Sukhendu2002 and others added 19 commits October 7, 2026 13:57
Replace substring and exact-string assertions with assertEqualHTML()
for fully deterministic output, so attribute order, quoting style and
self-closing syntax no longer cause spurious failures.

Use WP_HTML_Tag_Processor where full-markup comparison is a poor fit:
the core concatenation test (whose load-styles.php URL is noisy) and
the closing-tag escaping test, which now checks that the CSS cannot
break out of the STYLE element rather than pinning the exact escape
sequence.

Also verify the attributes passed to the wp_inline_style_attributes
filter during normal style output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Widen the documented attribute value type from string|true to
string|bool for wp_get_inline_style_tag(), wp_print_inline_style_tag()
and the wp_inline_style_attributes filter. This matches
wp_get_script_tag() and wp_get_inline_script_tag(), and reflects that
a false value omits the attribute, as the filter docblock already
describes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When wp_hoist_late_printed_styles() splits CSS added via
wp_add_inline_style( 'wp-block-library', ... ) into a separate
wp-block-library-inline-css-extra STYLE tag, build that tag with
wp_get_inline_style_tag() so it receives the attributes from the
wp_inline_style_attributes filter. Previously the original tag had a
filtered nonce but the split-off tag did not, so a nonce-based Content
Security Policy would block the CSS after hoisting.

Add a hoisting test data set which filters in a nonce, and loosen the
global styles placeholder pattern so that it still matches the
placeholder STYLE tag when it has additional attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In WP_Styles::do_item(), the inline style for a handle is already
obtained up front via print_inline_style( $handle, false ). When not
concatenating, calling print_inline_style( $handle ) again re-read the
after data and rebuilt the sourceURL comment. Instead, append the inline
style tag to the LINK tag once, and then print or buffer the result,
which also removes the duplicated wp_get_inline_style_tag() call between
the concat and non-concat branches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the remaining hand-built front-end STYLE tags via
wp_print_inline_style_tag() so that they also receive attributes from
the wp_inline_style_attributes filter, such as a nonce for a Content
Security Policy:

* Custom background (including the empty Customizer preview tag).
* Additional CSS from the Customizer.
* Custom logo header text hiding.
* Block support styles.
* Font faces.
* Recent Comments widget.

Add a data-provider test covering each STYLE tag printed outside of
WP_Styles, and extend the hoisting test data set with a filtered nonce
to assert that every STYLE tag in the page has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The core inline style data sets add and remove theme support, which is
not restored automatically between tests. The leaked custom-background
support caused WP_Test_REST_Themes_Controller to fail with an undefined
$custom_background global when run after these tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the error page styles with wp_print_inline_style_tag() so that
they receive attributes from the wp_inline_style_attributes filter. Since
wp_die() may be called before script-loader.php and the HTML API are
loaded, fall back to a literal STYLE tag in that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the transparent background style for the interim login modal with
wp_print_inline_style_tag() so that it receives filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the styles for wp-signup.php and wp-activate.php with
wp_print_inline_style_tag() so that they receive filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the banner background styles in the plugin information modal
with wp_print_inline_style_tag() so that they receive filtered
attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the site icon URL custom property with wp_print_inline_style_tag()
so that it receives filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the Customizer preview loading styles, the highlighted widget
styles and the site icon control template styles with
wp_print_inline_style_tag() so that they receive filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the site icon URL custom property in the tmpl-site-icon-preview-crop
template with wp_print_inline_style_tag() so that it receives filtered
attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build the gallery shortcode's default STYLE tag with
wp_get_inline_style_tag() so that it receives filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print the reset styles in the Legacy Widget block preview and the REST
API widget preview iframe with wp_print_inline_style_tag() so that they
receive filtered attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
westonruter and others added 4 commits October 12, 2026 10:58
Trim the complete set of whitespace characters defined by CSS (space,
tab, form feed, carriage return and line feed) from the start and end of
the CSS passed to wp_get_inline_style_tag(), rather than an arbitrary
subset. This is the same set, in the same order, that the HTML API uses.

Co-Authored-By: Jon Surrell <jonsurrell@git.wordpress.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a test that leading and trailing CSS whitespace always becomes a
single newline wrapping the contents of the STYLE element, with the
handling of null data from a misbehaving filter folded into it.

Remove the test that a closing STYLE tag in the CSS is escaped, since
that is the HTML API's responsibility and is already covered by the
tests for WP_HTML_Tag_Processor::set_modifiable_text().

Co-Authored-By: Jon Surrell <jonsurrell@git.wordpress.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test for calling WP_Styles::print_inline_style() directly exercised
the same path as the test for normal style output, since that is how
inline styles are printed when concatenation is off.

Co-Authored-By: Jon Surrell <jonsurrell@git.wordpress.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sirreal sirreal left a comment

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.

The attribute filter here (and already existing on script tag function) should be passed the script tag data from the processor instead of the original data before setting.

Comment thread src/wp-includes/script-loader.php Outdated
Comment thread src/wp-includes/script-loader.php Outdated
@westonruter

Copy link
Copy Markdown
Member

@Sukhendu2002 Do you want to action on Jon's feedback?

Comment thread src/wp-includes/script-loader.php

@sirreal sirreal left a comment

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.

Thanks, this looks good to me.

westonruter and others added 10 commits October 16, 2026 22:52
Replace the closures that wrote the filter arguments to a by-reference variable with a `MockAction`, which records the arguments and call count. Where a closure is still needed to add the `nonce` attribute, a separate `MockAction` at a later priority captures what the filter is passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…style_tag()`.

For a STYLE tag, `WP_HTML_Tag_Processor::set_modifiable_text()` escapes any `</style` in the text rather than rejecting it, so it always returns true. The branch returning an empty string could never run, so remove it and document the escaping instead of a failure case that does not happen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tributes` filter.

A filter callback that forgets to return its value makes `$attributes` null, which caused a warning when iterating it and dropped every attribute, including the `id`. Fall back to the unfiltered attributes in that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h `wp_inline_style_attributes`.

When `wp_hoist_late_printed_styles()` removed the placeholder from the `wp-block-library` inline style, it replaced only the text of the STYLE tag. The `wp_inline_style_attributes` filter had already run on the original text, so any attribute derived from the CSS, such as a hash for a Content Security Policy, no longer matched what was printed. Rebuild the tag with `wp_get_inline_style_tag()` so that the filter runs again on the new CSS.

The STYLE tag was also located by its `id`, which the filter can change or remove. In that case the placeholder was left in the page and the block styles were not inserted after the block library styles. Locate the tag by the placeholder in its text instead.

Tests are added for both cases. The regular expression the test uses to find the global styles placeholder no longer assumes `id` is the first attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…style_attributes` filter."

This reverts commit 8841042. Guarding against a non-array return is only a partial validation of the filtered value, which would also need its keys and values checked to satisfy `WP_HTML_Tag_Processor::set_attribute()`. The equivalent `wp_inline_script_attributes` filter has no such guard and no errors have been reported from it, so the guard is unnecessary here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S rather than their IDs.

The global styles and block styles placeholder STYLE tags were located by their `id` attributes, which the `wp_inline_style_attributes` filter can change. If it did, the placeholder CSS was left in the page, and the global styles and block styles were printed at the end of HEAD instead of at their normal positions. Locate them by their placeholder CSS instead, as is now done for the block library inline style.

The test now renames all three IDs, and it checks for the placeholders by their CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed block library inline style.

The block library inline style was rebuilt with only its `id`, so any other attributes already on the tag in the output buffer were lost unless the `wp_inline_style_attributes` filter happened to add them again, for example ones added by an earlier `wp_template_enhancement_output_buffer` filter. Pass the existing attributes when rebuilding the tag, so that they are kept while the filter may still override them. This also keeps an `id` that the filter had changed.

The attribute names are narrowed from `non-falsy-string` to `non-empty-string` in the PHPStan types of `wp_get_inline_style_tag()` and `wp_print_inline_style_tag()`, since `0` is a valid attribute name and the names read from the tag are typed this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or an inline style.

Since `wp_hoist_late_printed_styles()` rebuilds the `wp-block-library` inline style after removing a placeholder from its CSS, the filter first runs on CSS that is never printed. Document this so that a filter which collects values per call, such as hashes for a Content Security Policy, can account for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ders are located.

Locating the placeholder STYLE tags by their CSS means reading the text of each STYLE tag in HEAD, which copies it. Once all of the placeholders have been found, skip reading the text of the remaining STYLE tags, such as the Customizer's Additional CSS, while still looking for the end of HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

I just found something problematic with this. By allowing the attributes to be filtered, it can result in the id being passed in to get dropped or modified. This can break things. For example, wp_custom_css_cb() constructs a STYLE tag with an id of wp-custom-css. In the Customizer JS, it looks for a STYLE with that id specifically to rewrite:

if ( api.settings.theme.isBlockTheme ) {
style = $( 'style#global-styles-inline-css' );
// Forbid milestone comments from appearing in Custom CSS which would break live preview.
value = value.replace( /\/\*(BEGIN|END)_CUSTOMIZER_CUSTOM_CSS\*\//g, '' );
var textContent = style.text().replace(
/(\/\*BEGIN_CUSTOMIZER_CUSTOM_CSS\*\/)((?:.|\s)*?)(\/\*END_CUSTOMIZER_CUSTOM_CSS\*\/)/,
function ( match, beforeComment, oldValue, afterComment ) {
return beforeComment + '\n' + value + '\n' + afterComment;
}
);
style.text( textContent );
} else {
style = $( 'style#wp-custom-css' );
style.text( value );
}

There are other integrations like this in PHP, for example in wp_hoist_late_printed_styles() it looks for a STYLE that has a specific id:

// Locate the insertion points in the HEAD.
while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
if (
'STYLE' === $processor->get_tag() &&
'wp-global-styles-placeholder-inline-css' === $processor->get_attribute( 'id' )
) {
/** This is added in {@see wp_enqueue_global_styles()} */
$processor->set_bookmark( 'wp_global_styles_placeholder' );
} elseif (
'STYLE' === $processor->get_tag() &&
'wp-block-styles-placeholder-inline-css' === $processor->get_attribute( 'id' )
) {
/** This is added in {@see wp_enqueue_registered_block_scripts_and_styles()} */
$processor->set_bookmark( 'wp_block_styles_placeholder' );
} elseif (
'STYLE' === $processor->get_tag() &&
'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
) {
/** This is added here in {@see wp_hoist_late_printed_styles()} */
$processor->set_bookmark( 'wp_block_library' );
} elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) {
$processor->set_bookmark( 'head_end' );
break;
}
}

This would break if the id is changed with a filter.

Perhaps the id should be considered immutable, that any changes are ignored. If the id being filtered doesn't match the initial id passed in, it could issue a _doing_it_wrong().

westonruter and others added 4 commits October 16, 2026 23:43
…placeholders are located."

This reverts commit 44dfa2b. The placeholder STYLE tags will again be located by their IDs, which are being made immutable in the `wp_inline_style_attributes` filter, so their CSS no longer needs to be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their CSS rather than their IDs."

This reverts commit ea09b6e. The IDs of inline STYLE tags are being made immutable in the `wp_inline_style_attributes` filter, since other code locates the tags by them, such as the Customizer preview with `wp-custom-css`. So the placeholder STYLE tags can be located by their IDs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hanging a supplied ID.

Other code locates inline STYLE tags by their IDs, such as the Customizer preview with `wp-custom-css` and `wp_hoist_late_printed_styles()` with its placeholders, so changing or removing an ID with the filter breaks them. When an `id` is supplied to `wp_get_inline_style_tag()`, it is now always printed, and a `_doing_it_wrong()` is issued if the filter changed or removed it. The ID is set before the filtered attributes, so a lexical variation of the name such as `ID` cannot override it either. The filter may still add an ID when none is supplied.

The hoisting test case which renamed the block library ID is removed, since the rename is now ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…when hoisting styles.

The block library inline style was located by its placeholder CSS in 8db7a9c because the `wp_inline_style_attributes` filter could change its ID. Now that the filter cannot change a supplied ID, go back to locating it by ID, consistent with the other placeholder STYLE tags, and without reading the CSS of each STYLE tag in HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

This PR now has merge conflicts after r64585 which will need to be resolved.

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.

4 participants