Stop zero-value minification from producing invalid CSS - #13
Merged
Conversation
Two long-standing rules in CssCompressor rewrite a zero into a form the CSS
grammar does not accept in that position. A browser drops the whole
declaration, so the rule costs the declaration to save a byte, and it does so
silently - the build reports no warning.
Zero-percentage stripping inside a group
hsla(0,0%,100%,.5) -> hsla(0,0,100%,.5)
rgb(0%,50%,100%) -> rgb(0,50%,100%)
color-mix(in srgb,red 0%,b) -> color-mix(in srgb,red 0,b)
min(0%,10px) -> min(0,10px)
The rule "a zero <length> may drop its unit" is true, but it was applied to
<percentage> as well, and it fired on anything inside a "(". The colour
functions' comma-separated legacy form takes <percentage> arguments, and the
math functions type-check their arguments against each other, so none of the
outputs above is a valid value.
The "%" case now consults the enclosing function name. The name is captured
rather than looked behind, so "min" does not also match the tail of "minmax()",
where a zero really may lose its unit. Length units inside groups, gradient
colour stops and minmax() are unchanged.
Collapsing a run of zeroes
box-shadow:0 0 -> box-shadow:0
text-shadow:0 0 0 -> text-shadow:0
perspective-origin:0 0 -> perspective-origin:0
"margin:0 0 0 0" is the box-model shorthand and collapses exactly, but a
<shadow> needs both of its offsets, so "box-shadow:0" is invalid - and taking
the declaration down takes every other shadow in the same comma-separated list
with it. "perspective-origin:0" means "0 center", not "0 0". The collapse now
matches on the property name (vendor prefix removed) and skips those three;
"flex" keeps the exclusion it already had.
Verification
- 143 real-world stylesheets (Bootstrap, Font Awesome, Swiper, Vuetify and
Fess/CodeLibs themes) were compressed and every declaration checked against
the CSS grammar with css-tree's lexer. Declarations valid in the source but
invalid after compression: 26 files before, 0 after - the 16 files still
flagged are var() fallbacks css-tree cannot resolve, and are flagged
identically in the source.
- All 143 stay byte-identical when compressed a second time.
- CssColorFunctionTest pins each corrupted value plus the neighbouring cases
that must keep compressing: minmax(), gradient stops, translate(0px),
margin/padding/border-radius/gap, background-position, transform-origin.
- The three percentage entries and the min() entry in KnownCssLimitationsTest
recorded these defects with instructions to delete them once fixed; they are
removed. The <time>-inside-a-group case there is untouched and still fails.
This was referenced Sep 5, 2026
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.
Problem
Two zero-value minification rules in
CssCompressorrewrite a zero into a form the CSS grammar does not accept in that position. The browser then drops the whole declaration, so the rule costs the declaration in order to save a byte — silently, with no warning from the build.1. Zero-percentage stripping inside a group
hsla(0,0%,100%,.5)hsla(0,0,100%,.5)hsl()/hsla()take<percentage>for saturation and lightnessrgb(0%,50%,100%)rgb(0,50%,100%)rgb()is all-<percentage>or all-<number>, never mixedcolor-mix(in srgb,red 0%,blue)color-mix(in srgb,red 0,blue)color-mix()takes<percentage [0,100]>min(0%,10px)min(0,10px)<number>vs<length>The underlying rule — "a zero
<length>may drop its unit" — is correct, but it was applied to<percentage>as well, and it fired on anything inside a(, so it reached colour and math functions it was never meant for.2. Collapsing a run of zeroes
box-shadow:0 0box-shadow:0<shadow>needs both offsetstext-shadow:0 0 0text-shadow:0perspective-origin:0 0perspective-origin:00 center, not0 0margin:0 0 0 0→margin:0is the box-model shorthand and collapses exactly;box-shadowis not a shorthand at all. Losing abox-shadowdeclaration also takes down every other shadow in the same comma-separated list.flexwas already excluded here for the same class of reason.Both defects predate the fork —
yuicompressor-2.4.8from Maven Central produces the same output — and the first was already recorded inKnownCssLimitationsTestwith instructions to delete the entries once fixed.Fix
%branch of the in-group zero-unit stripping is split out and now consults the enclosing function name againstPERCENTAGE_REQUIRED_FUNCTIONS. The name is captured rather than looked behind, somindoes not also match the tail ofminmax(), where a zero really may lose its unit.ZERO_RUN_NOT_COLLAPSIBLE.Unchanged: length units inside groups (
translate(0px,10px)→translate(0,10px)), gradient colour stops (red 0%→red 0),minmax(0%,1fr)→minmax(0,1fr),margin/padding/border-radius/gapcollapsing, and thebackground-position/transform-originre-expansion.Verification
css-tree's lexer. Declarations that are valid in the source but invalid after compression: 26 files before, 0 after. The 16 files still flagged arevar()fallbackscss-treecannot resolve — flagged identically in the source.CssColorFunctionTest(new, 20 cases) pins every corrupted value plus the neighbouring cases that must keep compressing.KnownCssLimitationsTest: the three percentage rows and themin()case are removed, as that file instructs. The<time>-inside-a-group case is untouched and still recorded.