Skip to content

fix: lossless mode drops whitespace when a selector ends empty - #337

Open
theRizwan wants to merge 1 commit into
postcss:mainfrom
theRizwan:fix/lossless-trailing-space
Open

fix: lossless mode drops whitespace when a selector ends empty#337
theRizwan wants to merge 1 commit into
postcss:mainfrom
theRizwan:fix/lossless-trailing-space

Conversation

@theRizwan

Copy link
Copy Markdown
Contributor

Fixes #298. @alexander-akait said "pr welcome" on the issue.

The cause

space() parks leading whitespace on this.spaces for the next node to claim, and newNode is its only consumer:

if (this.spaces) {
  node.spaces.before = this.spaces;
  this.spaces = "";
}

Whitespace in this parser only exists as a property of a node. A selector that ends before any node is created has nowhere to keep it, so it is silently dropped. That is why :not( a ) round-trips — the space attaches to the tag — while :not(a, ) does not.

Two conditions in space() route whitespace onto this.spaces: the previous token is a comma or an opening parenthesis, or the selector so far holds nothing but comments. The second is why :not( /*c*/ ) loses its trailing space too.

Scope

The issue reports :not(a, ). The same defect covers more than that, including a case with no pseudo involved:

input before after
:not(a, ) :not(a,) :not(a, )
:not(a, b, ) :not(a, b,) :not(a, b, )
:not(a , ) :not(a ,) :not(a , )
div:has(a, ) div:has(a,) div:has(a, )
:not( ) :not() :not( )
:not( ) :not() :not( )
:not( /*c*/ ) :not( /*c*/) :not( /*c*/ )
a, a, a,

The change

Pending whitespace is flushed at the three points where a selector can close: at a comma, at a pseudo's closing parenthesis, and at end of input.

It attaches to the last node's spaces.after when there is one, and otherwise to an empty Str node — which is what parseWhitespaceEquivalentTokens already does for the comment-adjacent case, so the AST shape is not a new idea.

Verification

Across 480 generated selectors — six pseudo forms × sixteen argument shapes × five enclosing contexts:

newly round-tripping 240
regressions 0
lossless: false output vs main byte-identical

Lossy mode is deliberately untouched: :not(a, ) still minifies to :not(a,).

npm test passes, including oxlint, the type check and the coverage thresholds: 95.02% lines, 95.30% branches, 97.72% functions against gates of 94/94/96. Test count 789 to 798.

Tests

Nine cases in pseudos.mjs, which uses the test helper and so asserts the round-trip automatically. With the fix reverted but the tests kept, all nine fail — they do not pass incidentally.

A note on the AST

:not( ) now yields a selector containing one empty Str node rather than an empty selector. Anything iterating .nodes will see it. The alternative was to store the whitespace on the Selector itself and teach Container._stringify to emit it, which keeps the AST cleaner but changes serialisation for every container type. I took the lower-risk option, but happy to switch if you would rather have the other.

`space()` parks leading whitespace on `this.spaces` for the next node to claim,
and `newNode` is its only consumer. A selector that ends before any node is
created therefore loses it:

    :not(a, )  ->  :not(a,)
    :not( )    ->  :not()
    a,         ->  a,

Two conditions route whitespace there: the previous token is a comma or an
opening parenthesis, or the selector so far holds nothing but comments. The
second is why `:not( /*c*/ )` also loses its trailing space.

Pending whitespace is now flushed wherever a selector can close: at a comma, at
a pseudo's closing parenthesis, and at end of input. It attaches to the last
node's `spaces.after` when there is one, otherwise to an empty string node,
matching what `parseWhitespaceEquivalentTokens` already does.

Lossy output is unchanged. Across 480 generated selectors: 240 newly
round-trip, no regressions, and `lossless: false` is byte-identical to before.

Fixes postcss#298
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.

lossless: true option does not preserve trailing spaces in selectors

1 participant