Fix order-independent shorthand matching for list-style and columns (#185) - #216
Merged
TylerBrinks merged 1 commit intoJul 23, 2026
Conversation
jhaygood86
force-pushed
the
bugfix/unordered-options-permutations
branch
from
July 23, 2026 00:06
362359e to
ec5d61e
Compare
…ylerBrinks#185) The list-style and columns shorthands are order-independent, but WithAny (UnorderedOptionsConverter) matches its converters greedily in declaration order. When a token is accepted by more than one operand, an earlier converter claims it before a later one can, and the value is rejected: - "list-style: none square" - "none" is valid for both list-style-type and list-style-image, so type takes it and square is left stranded, even though "list-style: square none" parses (TylerBrinks#185). - "columns: auto 12em" - "auto" is valid for both column-width and column-count, so column-width takes it and 12em is left stranded, even though "columns: 12em auto" parses. Add an OrderIndependentOptionsConverter that first tries the existing in-order match (so already-ordered values keep identical output) and, only on failure, searches converter orderings until one consumes every token, mapping the results back to canonical order for ExtractFor. Point the list-style and columns shorthands at it via a new WithAnyOrderIndependent factory. WithAny is left as-is for every other shorthand. An audit of the remaining WithAny users found none affected: border/outline/column-rule/ text-decoration and border-image match on disjoint token sets, so order never matters. background and animation are also any-order (||) grammars, but the spec assigns some of their longhands by position - animation's first/second <time> are animation-duration/animation-delay (CSS Animations 1), background's two <visual-box> values are background-origin/ background-clip (CSS Backgrounds 3) - and the declaration-order matcher already satisfies those rules, so reordering would regress them; transform-origin and perspective-origin likewise rely on order to keep their length form axis-ordered.
jhaygood86
force-pushed
the
bugfix/unordered-options-permutations
branch
from
July 23, 2026 00:14
ec5d61e to
4d32a5d
Compare
jhaygood86
marked this pull request as ready for review
July 23, 2026 00:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #185.
Bug
Some order-independent shorthands reject a value in one operand order but accept it in another.
list-style: none squareis rejected whilelist-style: square noneparses; the same fault affectscolumns:Both are legal:
list-style—<'list-style-position'> || <'list-style-image'> || <'list-style-type'>columns—<'column-width'> || <'column-count'>Cause
WithAny(UnorderedOptionsConverter) matches its converters greedily in declaration order (VaryAllper converter, left to right). That fails whenever a token is accepted by more than one operand and an earlier converter claims it:list-style:noneis valid for bothlist-style-typeandlist-style-image. Fornone square,list-style-typetakesnone, leavingsquarewith no taker (image doesn't acceptsquare).square noneonly works becausesquarecan only be a type.columns:autois valid for bothcolumn-widthandcolumn-count. Forauto 12em,column-widthtakesauto, leaving12emwith no taker (count is<integer>).12em autoonly works because12emcan only be the width.Fix
A new
OrderIndependentOptionsConverter:WithAny. Every already-ordered value parses through this and serializes byte-for-byte as before.ExtractForstill resolves each longhand. Bounded by a small operand cap as an O(n!) safety net.list-styleandcolumnsare pointed at it through a newWithAnyOrderIndependentfactory.WithAnyitself is left untouched.Why only these two
I audited every
WithAnyuser:list-style,columnsnone/auto) is shared between operands, so greedy order mis-claims it.border(+-top/right/bottom/left),outline,column-rule,text-decoration,border-image,border-image-sliceanimation,backgroundtransform-origin,perspective-origin<length>form axis-ordered.Tests
ListProperty.cs: the#185repro (none square→square none), the already-ordered control, and the position-before-type reverse.PropertyTests/ColumnsProperty.cs: theauto 12emrepro (→12em auto), the12em autocontrol, andauto 2. Verified fail-first — with the shorthands still on plainWithAny,none squareandauto 12emfail; with the fix they pass.The full existing suite (1263 tests) stays green and unmodified, and all seven target frameworks build with no new warnings.