Skip to content

Fix order-independent shorthand matching for list-style and columns (#185) - #216

Merged
TylerBrinks merged 1 commit into
TylerBrinks:masterfrom
jhaygood86:bugfix/unordered-options-permutations
Jul 23, 2026
Merged

Fix order-independent shorthand matching for list-style and columns (#185)#216
TylerBrinks merged 1 commit into
TylerBrinks:masterfrom
jhaygood86:bugfix/unordered-options-permutations

Conversation

@jhaygood86

@jhaygood86 jhaygood86 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #185.

Bug

Some order-independent shorthands reject a value in one operand order but accept it in another. list-style: none square is rejected while list-style: square none parses; the same fault affects columns:

var sheet = new StylesheetParser().Parse("a { list-style: none square; columns: auto 12em }");
var style = sheet.StyleRules.First().Style;
// master:  style.ListStyle == "" , style.Columns == ""   (both dropped)
// here:    style.ListStyle == "square none" , style.Columns == "12em auto"

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 (VaryAll per converter, left to right). That fails whenever a token is accepted by more than one operand and an earlier converter claims it:

  • list-style: none is valid for both list-style-type and list-style-image. For none square, list-style-type takes none, leaving square with no taker (image doesn't accept square). square none only works because square can only be a type.
  • columns: auto is valid for both column-width and column-count. For auto 12em, column-width takes auto, leaving 12em with no taker (count is <integer>). 12em auto only works because 12em can only be the width.

Fix

A new OrderIndependentOptionsConverter:

  1. Fast path — matches in declaration order, identical to WithAny. Every already-ordered value parses through this and serializes byte-for-byte as before.
  2. Fallback (only when the in-order match fails) — a depth-first search over converter orderings until one consumes every token, then maps the results back to canonical converter order so ExtractFor still resolves each longhand. Bounded by a small operand cap as an O(n!) safety net.

list-style and columns are pointed at it through a new WithAnyOrderIndependent factory. WithAny itself is left untouched.

Why only these two

I audited every WithAny user:

Shorthand(s) Verdict
list-style, columns Fixed — order-independent and a token (none / 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-slice No change — order-independent but operands match disjoint token sets (width / style / color, etc.), so order never matters. Verified: every permutation already parses.
animation, background No change — also `
transform-origin, perspective-origin No change — rely on operand order to keep the <length> form axis-ordered.

Tests

ListProperty.cs: the #185 repro (none squaresquare none), the already-ordered control, and the position-before-type reverse. PropertyTests/ColumnsProperty.cs: the auto 12em repro (→ 12em auto), the 12em auto control, and auto 2. Verified fail-first — with the shorthands still on plain WithAny, none square and auto 12em fail; 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.

@jhaygood86
jhaygood86 force-pushed the bugfix/unordered-options-permutations branch from 362359e to ec5d61e Compare July 23, 2026 00:06
…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 jhaygood86 changed the title Fix list-style value order-independence (#185) Fix order-independent shorthand matching for list-style and columns (#185) Jul 23, 2026
@jhaygood86
jhaygood86 force-pushed the bugfix/unordered-options-permutations branch from ec5d61e to 4d32a5d Compare July 23, 2026 00:14
@jhaygood86
jhaygood86 marked this pull request as ready for review July 23, 2026 00:15
@TylerBrinks
TylerBrinks merged commit 9bee3eb into TylerBrinks:master Jul 23, 2026
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.

CSS list-style isn't properly parsed

2 participants