Skip to content

Commit a61e707

Browse files
committed
Document style attribute processor contract
1 parent 5d767ea commit a61e707

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# WP_HTML_Style_Attribute_Processor Specification
2+
3+
## Purpose
4+
5+
`WP_HTML_Style_Attribute_Processor` inspects and modifies decoded CSS text from an
6+
HTML `style` attribute. Its input is the declaration-list contents of the
7+
attribute, excluding HTML attribute syntax, entity decoding, and any surrounding
8+
declaration-block braces.
9+
10+
The processor preserves the CSS declaration-list model. Duplicate declarations
11+
remain distinct. Invalid CSS fragments are skipped or preserved according to CSS
12+
parser semantics unless a requested mutation cannot be proven safe.
13+
14+
The processor's north star is:
15+
16+
1. preserve CSS structure;
17+
2. preserve user intent;
18+
3. preserve the semantic CSS value of any existing declaration it rewrites;
19+
4. make minimal edits when doing so does not compromise the first three goals.
20+
21+
## Construction
22+
23+
Creation uses:
24+
25+
```php
26+
WP_HTML_Style_Attribute_Processor::create( string $decoded_css_text ): static
27+
```
28+
29+
The constructor is private. `create()` always returns a processor instance for a
30+
string input.
31+
32+
Callers must not pass raw `WP_HTML_Tag_Processor::get_attribute( 'style' )`
33+
results directly unless they have already checked that the result is a string.
34+
Missing or boolean attributes are an HTML-layer concern, not accepted CSS text.
35+
36+
Parsing is lazy. Creating a processor does not scan the style text until cursor
37+
movement, mutation validation, or serialization requires it.
38+
39+
## Declaration Model
40+
41+
The processor models each CSS declaration as:
42+
43+
```text
44+
property-name: component-value-list !important?
45+
```
46+
47+
The internal declaration data consists of:
48+
49+
- decoded property name;
50+
- authored property-name source when available;
51+
- value component list source range;
52+
- `!important` flag;
53+
- nullable source range for the parsed priority span;
54+
- source ranges needed for safe mutation and verification.
55+
56+
`!important` is not part of the value, including for custom properties.
57+
58+
## Public API
59+
60+
Initial public methods:
61+
62+
```php
63+
public static function create( string $decoded_css_text ): static;
64+
public function next_declaration( ?string $property_name = null ): bool;
65+
public function get_property_name(): ?string;
66+
public function is_important(): ?bool;
67+
public function set_value( string $value, ?bool $important = null ): bool;
68+
public function set_important( bool $important ): bool;
69+
public function remove_declaration(): bool;
70+
public function append_declaration( string $property_name, string $value, bool $important = false ): bool;
71+
public function get_updated_style(): string;
72+
```
73+
74+
The first API does not expose `get_value()` or `get_raw_value()`. Raw CSS values
75+
are difficult for callers to use safely, and a higher-level value API needs a
76+
separate design.
77+
78+
Bookmarks, seek, and rewind are out of scope. Callers that need to rescan should
79+
create a new processor from the updated style text.
80+
81+
## Cursor Semantics
82+
83+
`next_declaration()` advances a forward-only cursor over CSS declarations.
84+
85+
All current-declaration getters return `null` when the cursor is not positioned
86+
on a valid declaration. This includes `is_important()`.
87+
88+
After `remove_declaration()` succeeds, the cursor becomes invalid until
89+
`next_declaration()` advances it. A second removal on the same invalid cursor
90+
returns `false`.
91+
92+
After `set_value()` or `set_important()` succeeds without removing the
93+
declaration, the cursor remains on the same logical declaration.
94+
95+
`set_value( '' )` removes the current declaration. On success it has the same
96+
cursor semantics as `remove_declaration()`.
97+
98+
`append_declaration()` does not move the cursor. If the cursor was exhausted, it
99+
remains invalid until a later `next_declaration()` advances to the appended
100+
declaration.
101+
102+
`next_declaration()` flushes pending safe edits before advancing because
103+
advancement must reflect the updated declaration list.
104+
105+
Current-position getters should reflect successful pending edits without forcing
106+
a full reparse where practical, following HTML API style.
107+
108+
## Property Names
109+
110+
All public property-name arguments are decoded/plaintext CSS property names, not
111+
CSS-escaped identifier source.
112+
113+
Ordinary properties match ASCII case-insensitively. Custom properties match
114+
exactly because casing is semantic.
115+
116+
`get_property_name()` exposes CSS semantics:
117+
118+
- ordinary property names are returned lowercase;
119+
- custom property names preserve exact decoded casing.
120+
121+
Edits preserve authored property-name spelling when the declaration already
122+
exists. Appended ordinary properties serialize lowercase because there is no
123+
authored spelling to preserve. Appended custom properties preserve exact casing.
124+
125+
Appended property names must be valid plaintext CSS property names. The processor
126+
does not escape arbitrary strings into identifiers. Custom properties follow the
127+
same validity rule with the required `--` prefix.
128+
129+
## Value Validation
130+
131+
Mutation inputs accept CSS declaration values, not declaration-list text.
132+
133+
The supplied value must fit safely in both of these declaration slots, depending
134+
on the property being changed:
135+
136+
```css
137+
foo: <value>;
138+
--foo: <value>;
139+
```
140+
141+
Validation checks structure and CSS syntax safety, not property-specific browser
142+
support. The processor should not maintain a browser-style matrix of supported
143+
properties and value grammars.
144+
145+
Values must be complete and self-contained. Reject inputs that:
146+
147+
- are empty after CSS whitespace trimming, except `set_value( '' )`, which
148+
removes the current declaration;
149+
- contain top-level declaration separators such as semicolons;
150+
- contain a top-level `!important`, because priority is a separate argument;
151+
- contain bad strings or bad URLs;
152+
- contain unmatched delimiters or constructs that are valid only because CSS
153+
closes them at EOF;
154+
- cannot be proven to occupy only the declaration value slot.
155+
156+
Nested semicolons and `!important` text inside balanced blocks, functions,
157+
strings, or URLs are allowed when the tokenizer and parser say they are part of
158+
the value.
159+
160+
## Importance
161+
162+
`set_value( $value, null )` preserves the existing important flag.
163+
`set_value( $value, true )` sets it.
164+
`set_value( $value, false )` clears it.
165+
166+
`append_declaration()` receives importance as a separate boolean and rejects a
167+
top-level `!important` in `$value`.
168+
169+
`set_important()` changes only declaration priority.
170+
171+
When clearing importance, the processor may remove only the parsed priority span
172+
when that is safe. If minimal removal is unsafe, it may normalize the current
173+
declaration as long as it preserves CSS structure and the existing value's
174+
semantics. If that cannot be proven, it returns `false`.
175+
176+
When setting importance, the processor adds canonical ` !important` at the
177+
parsed value end or normalizes the declaration if necessary. It must verify that
178+
the updated declaration reparses as important.
179+
180+
## Mutation Atomicity And Verification
181+
182+
Invalid or inapplicable mutations return `false` without `_doing_it_wrong()`.
183+
184+
Failed mutations are atomic: style text, cursor state, and previous successful
185+
edits are preserved.
186+
187+
A mutation returning `true` guarantees that `get_updated_style()` includes it.
188+
189+
Every successful mutation must preserve the expected declaration-list structure.
190+
`set_value()` must not let an input value merge, split, hide, or otherwise change
191+
declarations beyond the intended current declaration.
192+
193+
The public contract describes guarantees, not the exact validation mechanism.
194+
Synthetic declarations, parser comparisons, token-level checks, and reparsing are
195+
implementation details.
196+
197+
Multiple mutations to the same logical declaration collapse to the final
198+
intended state. Mutations to different declarations in one pass are allowed when
199+
each mutation can be independently verified against the updated declaration
200+
list.
201+
202+
## Formatting And Normalization
203+
204+
The processor preserves surrounding text, comments, ignored invalid fragments,
205+
and authored spelling when cheap and safe.
206+
207+
Minimal edits are a lower priority than safety, correctness, preserved CSS
208+
structure, and semantic preservation. Any normalization required to satisfy
209+
those higher priorities is allowed, but normalization that rewrites an existing
210+
declaration value must be strict: it must serialize from parsed token/component
211+
data with equivalent CSS semantics. If equivalence cannot be proven, the
212+
mutation returns `false`.
213+
214+
`set_value()` trims supplied values according to CSS whitespace.
215+
216+
When possible, `set_value()` replaces the current declaration's value and
217+
priority portion rather than normalizing the whole declaration. Correctness wins
218+
over whitespace preservation.
219+
220+
## Invalid Fragments
221+
222+
Declaration discovery follows CSS semantics. If CSS would ignore text as a
223+
declaration in a style attribute declaration-list context, the processor does
224+
not expose it as a declaration.
225+
226+
Ignored invalid fragments are preserved where possible. Mutations touching a
227+
style containing invalid chunks must verify that the declaration-list structure
228+
remains safe. If preservation and a requested mutation are incompatible, the
229+
mutation returns `false` or normalizes only the range necessary to preserve
230+
semantics and structure.
231+
232+
## Append And EOF Repair
233+
234+
`append_declaration()` may succeed even when trailing text is malformed, but only
235+
when the processor can preserve existing CSS semantics.
236+
237+
When trailing declaration text is valid only because CSS closes an unclosed
238+
function or block at EOF, appending must materialize the missing closing
239+
delimiters before inserting the new declaration.
240+
241+
Example:
242+
243+
```css
244+
color: var(--x
245+
```
246+
247+
Appending `background: white` should produce a structurally safe equivalent such
248+
as:
249+
250+
```css
251+
color: var(--x); background: white;
252+
```
253+
254+
EOF repair is part of a successful append and appears in `get_updated_style()`.
255+
Repair should insert only the specific missing delimiters discovered from parser
256+
or tokenizer state, plus the boundary needed before the appended declaration. If
257+
the processor cannot determine a precise repair, append returns `false`.
258+
259+
EOF repair is essential for append. `set_value()` replacement values must be
260+
self-contained and do not receive EOF repair.
261+
262+
## Test Requirements
263+
264+
Tests should cover:
265+
266+
- private construction and `create()` behavior;
267+
- decoded string input boundary;
268+
- declaration traversal, duplicate preservation, and property-name matching;
269+
- getter `null` behavior off-cursor;
270+
- absence of public raw value getter in the first API;
271+
- `set_value()` priority preservation, setting, clearing, and empty-value
272+
removal;
273+
- `set_important()` setting, clearing, cursor behavior, and safe normalization;
274+
- rejection of unsafe values and property names with atomic failure;
275+
- append behavior, duplicate appends, exhausted cursor behavior, and empty-value
276+
rejection;
277+
- EOF repair for append and failure when repair cannot be precise;
278+
- invalid fragments, comments, adjacent declarations, and removal ranges;
279+
- preservation of authored spelling where required;
280+
- integration with `WP_HTML_Tag_Processor::set_attribute()` after callers supply
281+
decoded string input.

0 commit comments

Comments
 (0)