fix: lossless mode drops whitespace when a selector ends empty - #337
Open
theRizwan wants to merge 1 commit into
Open
fix: lossless mode drops whitespace when a selector ends empty#337theRizwan wants to merge 1 commit into
theRizwan wants to merge 1 commit into
Conversation
`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
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 #298. @alexander-akait said "pr welcome" on the issue.
The cause
space()parks leading whitespace onthis.spacesfor the next node to claim, andnewNodeis its only consumer: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 ontothis.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::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.afterwhen there is one, and otherwise to an emptyStrnode — which is whatparseWhitespaceEquivalentTokensalready 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:
lossless: falseoutput vsmainLossy mode is deliberately untouched:
:not(a, )still minifies to:not(a,).npm testpasses, includingoxlint, 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 thetesthelper 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 emptyStrnode rather than an empty selector. Anything iterating.nodeswill see it. The alternative was to store the whitespace on theSelectoritself and teachContainer._stringifyto 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.