Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
a491bef
CSS API: Add CSS token processor foundations
sirreal Jul 23, 2026
a80bd98
Harden CSS builder identifier serialization
sirreal Aug 26, 2026
db896db
Harden CSS token value updates
sirreal Aug 26, 2026
b2e6400
Point CSS API tests at Trac #65738 and set @since 7.2.0
sirreal Oct 6, 2026
6804231
Move string and ident serialization onto the token processor
sirreal Oct 7, 2026
c64ca47
Drop the token mutation API
sirreal Oct 7, 2026
59eece4
Drop untested token value getters
sirreal Oct 7, 2026
52576f1
Document corpus provenance
sirreal Oct 7, 2026
a9d4b5d
Report whether a token ended at its delimiter
sirreal Oct 7, 2026
051e8f5
Accept NUL in an unquoted url
sirreal Oct 9, 2026
27f45f1
Scan plain ASCII runs in idents and urls with strspn
sirreal Oct 9, 2026
dabd8c5
Decode token text with a static method
sirreal Oct 9, 2026
2dcba33
Add WP_CSS_Token and WP_CSS_Token_Processor::get_token()
sirreal Oct 9, 2026
892cd25
Move string and ident serialization into WP_CSS_Token_Serializer
sirreal Oct 9, 2026
8dbd398
Serialize tokens following CSS Syntax 3 §9
sirreal Oct 9, 2026
3458860
Document why comment tokens are kept
sirreal Oct 9, 2026
47206d9
Escape CR and FF in strings instead of normalizing them
sirreal Oct 9, 2026
2626801
Do not spell CDO or CDC from other tokens when serializing
sirreal Oct 9, 2026
08f3485
Make the token serializer internal
sirreal Oct 9, 2026
c177694
Build identifier escaping on name escaping
sirreal Oct 9, 2026
06c6e5d
Fill serializer test gaps
sirreal Oct 9, 2026
74f3310
Escape control characters in strings
sirreal Oct 13, 2026
52216bc
Write hex escapes in lowercase
sirreal Oct 13, 2026
5d86e10
Escape code points outside the non-ASCII ident code points
sirreal Oct 13, 2026
d9a0181
Add HTML style attribute processor
sirreal Jun 29, 2026
787ce33
Harden style attribute parsing
sirreal Jun 29, 2026
a91b7e4
Refresh style declarations after mutation
sirreal Jun 29, 2026
02ccb51
Respect comments when appending style declarations
sirreal Jun 29, 2026
c5e8fcd
Preserve comments when removing style declarations
sirreal Jun 29, 2026
36da63a
Lazily parse style attribute declarations
sirreal Jun 29, 2026
b7c0f68
Document style attribute processor contract
sirreal Jun 29, 2026
9ebe88d
Clarify style processor factory contract
sirreal Jun 29, 2026
46ca9fa
Implement style attribute processor contract
sirreal Jun 29, 2026
d321977
Pin style processor mutation sequencing
sirreal Aug 26, 2026
4cbb630
Reject declarations CSS drops at parse time
sirreal Oct 6, 2026
2625c51
Set style attribute processor @since to 7.2.0
sirreal Oct 6, 2026
00f4b82
Expose declaration values as a token view
sirreal Oct 7, 2026
cf1b73c
Include the dimension unit in the value token view
sirreal Oct 7, 2026
a2ea2a4
Trim declaration values by token, not by byte
sirreal Oct 12, 2026
02465b5
Store WP_CSS_Token objects for the style attribute
sirreal Oct 12, 2026
b8bbf3a
Read token termination for the EOF repair
sirreal Oct 12, 2026
e87aecf
Add serialize_declaration() to the style attribute processor
sirreal Oct 13, 2026
9907e3f
Document the cost of remove_declaration()
sirreal Oct 12, 2026
190cd2e
Write set_value() and append_declaration() from tokens
sirreal Oct 13, 2026
755bff5
Close strings and comments left open before an append
sirreal Oct 13, 2026
8f9a71e
Compare style attributes by their declarations in build_visual_html_t…
sirreal Oct 14, 2026
977555c
Add assertEqualStyleAttribute()
sirreal Oct 14, 2026
5cad306
Prototype a parser-based style attribute filter
sirreal Oct 6, 2026
5733c37
Add legacy versus prototype style filter differential
sirreal Oct 6, 2026
6de460c
Specify the declaration list filter
sirreal Oct 6, 2026
044b61f
Move the declaration list filter into kses.php
sirreal Oct 7, 2026
4c401ab
Reject bare parenthesis blocks and unterminated strings
sirreal Oct 7, 2026
e92ebaa
Test the declaration list filter
sirreal Oct 7, 2026
fd95114
Update kses tests for the parser-based filter
sirreal Oct 7, 2026
48b1867
Compare legacy and parser-based filters through the opt-in filter
sirreal Oct 7, 2026
8b466fd
Settle the declaration list filter spec
sirreal Oct 7, 2026
a5b4bcf
Reject declarations with unclosed blocks
sirreal Oct 7, 2026
803f7e9
Adapt core callers to the declaration list filter output
sirreal Oct 7, 2026
c34bb04
Update the declaration list filter spec
sirreal Oct 7, 2026
0d57067
Reject delimiters with HTML meaning
sirreal Oct 7, 2026
bee5865
Drop the last declaration when the input ends inside a token
sirreal Oct 7, 2026
9dc289b
Serialize tokens with decoded values
sirreal Oct 7, 2026
aa6ef6d
Deprecate the safecss_filter_attr_allow_css hook
sirreal Oct 7, 2026
49b6c94
Return the input unchanged when the allowed list is empty
sirreal Oct 7, 2026
0c166ad
Match allowed property names case-insensitively
sirreal Oct 7, 2026
335de8e
Reject a lone block in a standard property
sirreal Oct 7, 2026
c91c770
List the review fixes among the spec differences
sirreal Oct 8, 2026
c239538
Rename the legacy CSS filter opt-in to wp_kses_force_legacy_css_parser
sirreal Oct 8, 2026
6edf08c
Name no replacement for the deprecated allow_css hook
sirreal Oct 8, 2026
8d6bd3a
Allow the filter functions
sirreal Oct 8, 2026
7b12447
Allow the math functions
sirreal Oct 8, 2026
c7f9754
Allow the easing functions
sirreal Oct 8, 2026
a52bbef
Reject the src() function
sirreal Oct 8, 2026
71aefe0
Document the function admission rule
sirreal Oct 8, 2026
5e90e87
Reject only the ampersand delimiter
sirreal Oct 9, 2026
c153faa
Write accepted declarations with serialize_declaration()
sirreal Oct 12, 2026
19ea123
Close tokens cut off by the end of the input
sirreal Oct 12, 2026
139a581
Close blocks left open at the end of the input
sirreal Oct 12, 2026
54dbfef
Keep the legacy CSS filter in WP_Theme_JSON
sirreal Oct 12, 2026
70f2d80
Describe the ampersand rule in the declarations docblock
sirreal Oct 12, 2026
e31491e
Use the default list when safe_style_css returns a non-array
sirreal Oct 12, 2026
e234fe7
Ignore safe_style_css entries that are not strings
sirreal Oct 12, 2026
cf8e902
Reject control character delimiters
sirreal Oct 13, 2026
a13bee8
Reject backslash delimiters
sirreal Oct 13, 2026
01b992d
Join accepted declarations with a space
sirreal Oct 14, 2026
7900018
Merge remote-tracking branch 'upstream/trunk' into css-api/css-token-…
sirreal Oct 23, 2026
5f362c5
Merge branch 'css-api/css-token-processor-foundations' into css-token…
sirreal Oct 23, 2026
b5e249f
Merge branch 'css-token-style-attr-processor' into css-api/assert-equ…
sirreal Oct 23, 2026
708cb8f
Merge branch 'css-api/assert-equal-style-attribute' into css-api/safe…
sirreal Oct 23, 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
364 changes: 364 additions & 0 deletions docs/wp-css-declaration-list-filter-spec.md

Large diffs are not rendered by default.

356 changes: 356 additions & 0 deletions docs/wp-html-style-attribute-processor-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
# WP_HTML_Style_Attribute_Processor Specification

## Purpose

`WP_HTML_Style_Attribute_Processor` inspects and modifies decoded CSS text from an
HTML `style` attribute. Its input is the declaration-list contents of the
attribute, excluding HTML attribute syntax, entity decoding, and any surrounding
declaration-block braces.

The processor preserves the CSS declaration-list model. Duplicate declarations
remain distinct. Invalid CSS fragments are skipped or preserved according to CSS
parser semantics unless a requested mutation cannot be proven safe.

The processor's north star is:

1. preserve CSS structure;
2. preserve user intent;
3. preserve the semantic CSS value of any existing declaration it rewrites;
4. make minimal edits when doing so does not compromise the first three goals.

## Construction

Creation uses:

```php
WP_HTML_Style_Attribute_Processor::create( $decoded_css_text )
```

The constructor is private. `create()` accepts only string input and returns a
processor instance of the called class. The implementation must remain
compatible with WordPress' supported PHP versions, so the runtime signature may
use PHPDoc and explicit validation instead of PHP syntax unavailable in those
versions.

Callers must not pass raw `WP_HTML_Tag_Processor::get_attribute( 'style' )`
results directly unless they have already checked that the result is a string.
Missing or boolean attributes are an HTML-layer concern, not accepted CSS text.

Parsing is lazy. Creating a processor does not scan the style text until cursor
movement, mutation validation, or serialization requires it.

## Declaration Model

The processor models each CSS declaration as:

```text
property-name: component-value-list !important?
```

The internal declaration data consists of:

- decoded property name;
- authored property-name source when available;
- value component list source range and token index range;
- `!important` flag;
- nullable source range for the parsed priority span;
- source ranges needed for safe mutation and verification.

`!important` is not part of the value, including for custom properties.

## Public API

Initial public methods:

```php
public static function create( $decoded_css_text );
public function next_declaration( ?string $property_name = null ): bool;
public function get_property_name(): ?string;
public function is_important(): ?bool;
public function get_value_tokens(): ?array;
public function serialize_declaration(): ?string;
public function set_value( string $value, ?bool $important = null ): bool;
public function set_important( bool $important ): bool;
public function remove_declaration(): bool;
public function append_declaration( string $property_name, string $value, bool $important = false ): bool;
public function get_updated_style(): string;
```

The processor exposes a declaration value as tokens, through
`get_value_tokens()`, and not as a string: there is no `get_value()` or
`get_raw_value()`. Consumers that check or re-serialize values need tokens; a
string would be re-tokenized by every consumer.

Bookmarks, seek, and rewind are out of scope. Callers that need to rescan should
create a new processor from the updated style text.

## Cursor Semantics

`next_declaration()` advances a forward-only cursor over CSS declarations.

All current-declaration getters return `null` when the cursor is not positioned
on a valid declaration. This includes `is_important()` and `get_value_tokens()`.

After `remove_declaration()` succeeds, the cursor becomes invalid until
`next_declaration()` advances it. A second removal on the same invalid cursor
returns `false`.

After `set_value()` or `set_important()` succeeds without removing the
declaration, the cursor remains on the same logical declaration.

`set_value( '' )` removes the current declaration. On success it has the same
cursor semantics as `remove_declaration()`.

`append_declaration()` does not move the cursor. If the cursor was exhausted, it
remains invalid until a later `next_declaration()` advances to the appended
declaration.

`next_declaration()` flushes pending safe edits before advancing because
advancement must reflect the updated declaration list.

Current-position getters should reflect successful pending edits without forcing
a full reparse where practical, following HTML API style.

Each successful mutation parses the whole style text again. Removing most of the
declarations of a long style one at a time therefore takes time that grows with
the square of its length. To keep some declarations, read them and join the
`serialize_declaration()` output of the ones to keep.

## Property Names

All public property-name arguments are decoded/plaintext CSS property names, not
CSS-escaped identifier source.

Ordinary properties match ASCII case-insensitively. Custom properties match
exactly because casing is semantic.

`get_property_name()` exposes CSS semantics:

- ordinary property names are returned lowercase;
- custom property names preserve exact decoded casing.

Edits and appends write the property name as `get_property_name()` returns it,
escaped as an identifier: ordinary properties lowercase, custom properties with
their exact casing.

Appended property names must be valid plaintext CSS property names. The processor
does not escape arbitrary strings into identifiers. Custom properties follow the
same validity rule with the required `--` prefix.

## Value Tokens

`get_value_tokens()` returns the current declaration's value as a list of
`WP_CSS_Token` objects, or `null` when the cursor is not on a declaration. A
token's getters give its type (a `WP_CSS_Token_Processor` `TOKEN_*` constant),
type flag, decoded value, decoded dimension unit, byte offset and byte length in
the style text, and whether it ended at its closing delimiter.

The list covers the value only. Leading and trailing whitespace and comments
and the `!important` priority are excluded; whitespace and comments between
value tokens are included. A declaration with no value tokens, such as
`color:;`, returns an empty list.

Offsets index the current style text, which `get_updated_style()` returns.
Before any mutation that is the text passed to `create()`. A successful
mutation reparses the updated text, and the offsets follow it.

Re-tokenizing the source slice from the first token's start to the end of the
last token with `WP_CSS_Token_Processor` yields the same type, flag, value,
unit and termination sequence. Consumers may rely on this to re-serialize a
value or to hand the slice to another tokenizer-based check.

Two alternatives were considered. A `WP_CSS_Token_Processor` positioned over the
value's range would need range support in the tokenizer and give consumers a
forward-only cursor over tokens the processor already holds. A raw string would
be re-tokenized by every consumer.

## Serializing A Declaration

`serialize_declaration()` writes the current declaration from its parsed parts,
or returns `null` when the cursor is not on a declaration. The property name is
escaped as an identifier. The value is written from its tokens with
`WP_CSS_Token_Serializer::serialize()`: comments are dropped, each whitespace run
becomes one space, and blocks left open at the end of the style are closed. The
result is `name: value;` or `name: value !important;`, and CSS reads it as the same
declaration.

This is the public way to write CSS from a parsed declaration. Token lists are
not a public unit for writing CSS: `WP_CSS_Token_Serializer` is internal.

## Value Validation

Mutation inputs accept CSS declaration values, not declaration-list text.

The supplied value must fit safely in both of these declaration slots, depending
on the property being changed:

```css
foo: <value>;
--foo: <value>;
```

Validation checks structure and CSS syntax safety, not property-specific browser
support. The processor should not maintain a browser-style matrix of supported
properties and value grammars.

Values must be complete and self-contained. Reject inputs that:

- are empty after CSS whitespace trimming, except `set_value( '' )`, which
removes the current declaration;
- contain top-level declaration separators such as semicolons;
- contain a top-level `!important`, because priority is a separate argument;
- contain bad strings or bad URLs;
- contain unmatched delimiters or constructs that are valid only because CSS
closes them at EOF;
- cannot be proven to occupy only the declaration value slot.

Nested semicolons and `!important` text inside balanced blocks, functions,
strings, or URLs are allowed when the tokenizer and parser say they are part of
the value.

## Importance

`set_value( $value, null )` preserves the existing important flag.
`set_value( $value, true )` sets it.
`set_value( $value, false )` clears it.

`append_declaration()` receives importance as a separate boolean and rejects a
top-level `!important` in `$value`.

`set_important()` changes only declaration priority.

When clearing importance, the processor may remove only the parsed priority span
when that is safe. If minimal removal is unsafe, it may normalize the current
declaration as long as it preserves CSS structure and the existing value's
semantics. If that cannot be proven, it returns `false`.

When setting importance, the processor adds canonical ` !important` at the
parsed value end or normalizes the declaration if necessary. It must verify that
the updated declaration reparses as important.

## Mutation Atomicity And Verification

Invalid or inapplicable mutations return `false` without `_doing_it_wrong()`.

Failed mutations are atomic: style text, cursor state, and previous successful
edits are preserved.

A mutation returning `true` guarantees that `get_updated_style()` includes it.

Every successful mutation must preserve the expected declaration-list structure.
`set_value()` must not let an input value merge, split, hide, or otherwise change
declarations beyond the intended current declaration.

The public contract describes guarantees, not the exact validation mechanism.
Synthetic declarations, parser comparisons, token-level checks, and reparsing are
implementation details.

Multiple mutations to the same logical declaration collapse to the final
intended state. Mutations to different declarations in one pass are allowed when
each mutation can be independently verified against the updated declaration
list.

## Formatting And Normalization

The processor preserves surrounding text, comments, ignored invalid fragments,
and authored spelling when cheap and safe.

Minimal edits are a lower priority than safety, correctness, preserved CSS
structure, and semantic preservation. Any normalization required to satisfy
those higher priorities is allowed, but normalization that rewrites an existing
declaration value must be strict: it must serialize from parsed token/component
data with equivalent CSS semantics. If equivalence cannot be proven, the
mutation returns `false`.

`set_value()` and `append_declaration()` write a declaration as
`serialize_declaration()` does: `name: value;` or `name: value !important;`, with
the name escaped as an identifier and the value written from its tokens. Comments
and whitespace before and after a supplied value are removed, comments inside it
are dropped, and each whitespace run inside it becomes one space. Nothing is
copied from the supplied text.

`set_value()` rewrites the whole current declaration; text around it is
unchanged.

## Invalid Fragments

Declaration discovery follows CSS semantics. If CSS would ignore text as a
declaration in a style attribute declaration-list context, the processor does
not expose it as a declaration. That includes a declaration whose value, apart
from its priority, contains:

- a bad string or a bad URL;
- a closing `)`, `]`, or `}` that does not match the innermost open block;
- a top-level `!`;
- a top-level `{}` block alongside other tokens, unless the property is a custom
property.

Blocks and functions left open at EOF are closed by CSS, so a value ending
inside one is exposed.

Splitting follows CSS Style Attributes and the published CSS Syntax Level 3
"parse a list of declarations": a stray `}` is an invalid token, and the
invalid declaration runs to the next top-level semicolon. The CSS Syntax editor's
draft ends parsing at a top-level `}` instead; browsers and web-platform-tests do
not, and csswg-drafts#11113 tracks the conflict.

Ignored invalid fragments are preserved where possible. Mutations touching a
style containing invalid chunks must verify that the declaration-list structure
remains safe. If preservation and a requested mutation are incompatible, the
mutation returns `false` or normalizes only the range necessary to preserve
semantics and structure.

## Append And EOF Repair

`append_declaration()` may succeed even when trailing text is malformed, but only
when the processor can preserve existing CSS semantics.

When trailing declaration text is valid only because CSS closes an unclosed
string, comment, function or block at EOF, appending must materialize the
missing closing delimiters before inserting the new declaration. A string is
closed with the quote it starts with and a comment with `*/`. A `\` at EOF
cannot be closed this way, so append returns `false`.

Example:

```css
color: var(--x
```

Appending `background: white` should produce a structurally safe equivalent such
as:

```css
color: var(--x); background: white;
```

EOF repair is part of a successful append and appears in `get_updated_style()`.
Repair should insert only the specific missing delimiters discovered from parser
or tokenizer state, plus the boundary needed before the appended declaration. If
the processor cannot determine a precise repair, append returns `false`.

EOF repair is essential for append. `set_value()` replacement values must be
self-contained and do not receive EOF repair.

## Test Requirements

Tests should cover:

- private construction and `create()` behavior;
- decoded string input boundary;
- declaration traversal, duplicate preservation, and property-name matching;
- getter `null` behavior off-cursor;
- absence of public raw value getter in the first API;
- value token view: `null` off-cursor, trivia and priority exclusion, offsets
into the style text, and the re-tokenization invariant;
- `set_value()` priority preservation, setting, clearing, and empty-value
removal;
- `set_important()` setting, clearing, cursor behavior, and safe normalization;
- rejection of unsafe values and property names with atomic failure;
- append behavior, duplicate appends, exhausted cursor behavior, and empty-value
rejection;
- EOF repair for append and failure when repair cannot be precise;
- invalid fragments, comments, adjacent declarations, and removal ranges;
- preservation of authored spelling where required;
- integration with `WP_HTML_Tag_Processor::set_attribute()` after callers supply
decoded string input.
18 changes: 15 additions & 3 deletions src/wp-includes/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,21 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false
? $wide_size
: ( $content_size && ! $has_wide_size_override ? $content_size : 'var(--wp--style--global--wide-size, none)' );

// Make sure there is a single CSS rule, and all tags are stripped for security.
$all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
$wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
/*
* Make sure there is a single CSS rule, and all tags are stripped for security.
* safecss_filter_attr() filters declarations, so the value is filtered as a
* `max-width` declaration and the value of the result is kept.
*/
$filter_max_width = static function ( $value ) {
$declaration = safecss_filter_attr( 'max-width:' . explode( ';', $value )[0] );
if ( ! str_starts_with( $declaration, 'max-width:' ) ) {
return '';
}
$value = substr( $declaration, strlen( 'max-width:' ) );
return str_ends_with( $value, ';' ) ? substr( $value, 0, -1 ) : $value;
};
$all_max_width_value = $filter_max_width( $all_max_width_value );
$wide_max_width_value = $filter_max_width( $wide_max_width_value );

$margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
$margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
Expand Down
4 changes: 3 additions & 1 deletion src/wp-includes/class-wp-block-supports.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ function get_block_wrapper_attributes( $extra_attributes = array() ) {
rtrim( trim( $extra_attribute ), ';' ),
)
);
return safecss_filter_attr( implode( ';', array_filter( $styles ) ) );
$style = safecss_filter_attr( implode( ';', array_filter( $styles ) ) );
// safecss_filter_attr() may end the list with `;`. The merged attribute has none, like its inputs.
return str_ends_with( $style, ';' ) ? substr( $style, 0, -1 ) : $style;
},
'class' => static function ( $new_attribute, $extra_attribute ) {
$classes = array_merge(
Expand Down
Loading
Loading