From 18fdbbe71ec02ba72c57fe88a30686b6c7394eb0 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Fri, 16 Feb 2018 14:43:55 +1300 Subject: [PATCH 1/5] Refactor minifyDeclaration --- css/css.go | 330 ++++++++++++++++++++++++++++++------------------ css/css_test.go | 5 + 2 files changed, 211 insertions(+), 124 deletions(-) diff --git a/css/css.go b/css/css.go index ff87119268..672eeef893 100644 --- a/css/css.go +++ b/css/css.go @@ -4,6 +4,7 @@ package css // import "github.com/tdewolff/minify/css" import ( "bytes" "encoding/hex" + "fmt" "io" "strconv" @@ -216,138 +217,220 @@ func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) error return nil } -func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) error { - if len(values) == 0 { +type Token struct { + css.TokenType + Data []byte + Components []css.Token // only filled for functions +} + +func (t Token) String() string { + if len(t.Components) == 0 { + return t.TokenType.String() + "(" + string(t.Data) + ")" + } else { + return fmt.Sprint(t.Components) + } +} + +func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) error { + if len(components) == 0 { return nil } + prop := css.ToHash(property) - inProgid := false - for i, value := range values { - if inProgid { - if value.TokenType == css.FunctionToken { - inProgid = false - } - continue - } else if value.TokenType == css.IdentToken && css.ToHash(value.Data) == css.Progid { - inProgid = true - continue + + // Strip !important from the component list, this will be added later separately + important := false + if len(components) > 2 && components[len(components)-2].TokenType == css.DelimToken && components[len(components)-2].Data[0] == '!' && css.ToHash(components[len(components)-1].Data) == css.Important { + components = components[:len(components)-2] + important = true + } + + // Check if this is a simple list of values separated by whitespace or commas, otherwise we'll not be processing + simple := true + prevSep := true + values := []Token{} + for i := 0; i < len(components); i++ { + comp := components[i] + if comp.TokenType == css.LeftParenthesisToken || comp.TokenType == css.LeftBraceToken || comp.TokenType == css.LeftBracketToken || comp.TokenType == css.RightParenthesisToken || comp.TokenType == css.RightBraceToken || comp.TokenType == css.RightBracketToken { + simple = false + break } - value.TokenType, value.Data = c.shortenToken(prop, value.TokenType, value.Data) - if prop == css.Font || prop == css.Font_Family || prop == css.Font_Weight { - if value.TokenType == css.IdentToken && (prop == css.Font || prop == css.Font_Weight) { - val := css.ToHash(value.Data) - if val == css.Normal && prop == css.Font_Weight { - // normal could also be specified for font-variant, not just font-weight - value.TokenType = css.NumberToken - value.Data = []byte("400") - } else if val == css.Bold { - value.TokenType = css.NumberToken - value.Data = []byte("700") - } - } else if value.TokenType == css.StringToken && (prop == css.Font || prop == css.Font_Family) && len(value.Data) > 2 { - unquote := true - parse.ToLower(value.Data) - s := value.Data[1 : len(value.Data)-1] - if len(s) > 0 { - for _, split := range bytes.Split(s, spaceBytes) { - val := css.ToHash(split) - // if len is zero, it contains two consecutive spaces - if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default || - len(split) == 0 || !css.IsIdent(split) { - unquote = false - break - } + + if !prevSep && comp.TokenType != css.WhitespaceToken && comp.TokenType != css.CommaToken { + simple = false + break + } + + if comp.TokenType == css.WhitespaceToken || comp.TokenType == css.CommaToken { + prevSep = true + if comp.TokenType == css.CommaToken { + values = append(values, Token{components[i].TokenType, components[i].Data, nil}) + } + } else if comp.TokenType == css.FunctionToken { + prevSep = false + j := i + 1 + level := 0 + for ; j < len(components); j++ { + if components[j].TokenType == css.LeftParenthesisToken { + level++ + } else if components[j].TokenType == css.RightParenthesisToken { + if level == 0 { + j++ + break } - } - if unquote { - value.Data = s + level-- } } - } else if prop == css.Outline || prop == css.Border || prop == css.Border_Bottom || prop == css.Border_Left || prop == css.Border_Right || prop == css.Border_Top { - if css.ToHash(value.Data) == css.None { - value.TokenType = css.NumberToken - value.Data = zeroBytes - } + values = append(values, Token{components[i].TokenType, components[i].Data, components[i:j]}) + i = j - 1 + } else { + prevSep = false + values = append(values, Token{components[i].TokenType, components[i].Data, nil}) } - values[i].TokenType, values[i].Data = value.TokenType, value.Data } - important := false - if len(values) > 2 && values[len(values)-2].TokenType == css.DelimToken && values[len(values)-2].Data[0] == '!' && css.ToHash(values[len(values)-1].Data) == css.Important { - values = values[:len(values)-2] - important = true - } + // Do not process complex values (eg. containing blocks or is not alternated between whitespace/commas and flat values + if !simple { + if prop == css.Filter && len(components) == 11 { + if bytes.Equal(components[0].Data, []byte("progid")) && + components[1].TokenType == css.ColonToken && + bytes.Equal(components[2].Data, []byte("DXImageTransform")) && + components[3].Data[0] == '.' && + bytes.Equal(components[4].Data, []byte("Microsoft")) && + components[5].Data[0] == '.' && + bytes.Equal(components[6].Data, []byte("Alpha(")) && + bytes.Equal(parse.ToLower(components[7].Data), []byte("opacity")) && + components[8].Data[0] == '=' && + components[10].Data[0] == ')' { + components = components[6:] + components[0].Data = []byte("alpha(") + } + } - if len(values) == 1 { - if prop == css.Background && css.ToHash(values[0].Data) == css.None { - values[0].Data = backgroundNoneBytes - } else if bytes.Equal(property, msfilterBytes) { - alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=") - if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) { - values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...) + for _, component := range components { + if _, err := c.w.Write(component.Data); err != nil { + return err } } - } else { - if prop == css.Margin || prop == css.Padding || prop == css.Border_Width { - if (values[0].TokenType == css.NumberToken || values[0].TokenType == css.DimensionToken || values[0].TokenType == css.PercentageToken) && (len(values)+1)%2 == 0 { - valid := true - for i := 1; i < len(values); i += 2 { - if values[i].TokenType != css.WhitespaceToken || values[i+1].TokenType != css.NumberToken && values[i+1].TokenType != css.DimensionToken && values[i+1].TokenType != css.PercentageToken { - valid = false - break + if important { + if _, err := c.w.Write([]byte("!important")); err != nil { + return err + } + } + return nil + } + + for i := range values { + values[i].TokenType, values[i].Data = c.shortenToken(prop, values[i].TokenType, values[i].Data) + } + + if len(values) > 0 { + switch prop { + case css.Font, css.Font_Weight, css.Font_Family: + for i, value := range values { + if value.TokenType == css.IdentToken { + val := css.ToHash(value.Data) + if val == css.Normal { + values[i].TokenType = css.NumberToken + values[i].Data = []byte("400") + } else if val == css.Bold { + values[i].TokenType = css.NumberToken + values[i].Data = []byte("700") } - } - if valid { - n := (len(values) + 1) / 2 - if n == 2 { - if bytes.Equal(values[0].Data, values[2].Data) { - values = values[:1] - } - } else if n == 3 { - if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) { - values = values[:1] - } else if bytes.Equal(values[0].Data, values[4].Data) { - values = values[:3] - } - } else if n == 4 { - if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[0].Data, values[6].Data) { - values = values[:1] - } else if bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[2].Data, values[6].Data) { - values = values[:3] - } else if bytes.Equal(values[2].Data, values[6].Data) { - values = values[:5] + } else if value.TokenType == css.StringToken && len(value.Data) > 2 { + unquote := true + parse.ToLower(value.Data) + s := value.Data[1 : len(value.Data)-1] + if len(s) > 0 { + for _, split := range bytes.Split(s, spaceBytes) { + val := css.ToHash(split) + // if len is zero, it contains two consecutive spaces + if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default || + len(split) == 0 || !css.IsIdent(split) { + unquote = false + break + } } } + if unquote { + values[i].Data = s + } } } - } else if prop == css.Filter && len(values) == 11 { - if bytes.Equal(values[0].Data, []byte("progid")) && - values[1].TokenType == css.ColonToken && - bytes.Equal(values[2].Data, []byte("DXImageTransform")) && - values[3].Data[0] == '.' && - bytes.Equal(values[4].Data, []byte("Microsoft")) && - values[5].Data[0] == '.' && - bytes.Equal(values[6].Data, []byte("Alpha(")) && - bytes.Equal(parse.ToLower(values[7].Data), []byte("opacity")) && - values[8].Data[0] == '=' && - values[10].Data[0] == ')' { - values = values[6:] - values[0].Data = []byte("alpha(") + case css.Margin, css.Padding, css.Border_Width: + n := len(values) + if n == 2 { + if bytes.Equal(values[0].Data, values[1].Data) { + values = values[:1] + } + } else if n == 3 { + if bytes.Equal(values[0].Data, values[1].Data) && bytes.Equal(values[0].Data, values[2].Data) { + values = values[:1] + } else if bytes.Equal(values[0].Data, values[2].Data) { + values = values[:2] + } + } else if n == 4 { + if bytes.Equal(values[0].Data, values[1].Data) && bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[3].Data) { + values = values[:1] + } else if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[1].Data, values[3].Data) { + values = values[:2] + } else if bytes.Equal(values[1].Data, values[3].Data) { + values = values[:3] + } + } + case css.Outline, css.Border, css.Border_Bottom, css.Border_Left, css.Border_Right, css.Border_Top: + none := false + for _, value := range values { + if len(value.Data) == 1 && value.Data[0] == '0' || css.ToHash(value.Data) == css.None { + none = true + break + } + } + if none { + values = values[:1] + values[0].TokenType = css.NumberToken + values[0].Data = zeroBytes + } + case css.Background: + if len(values) == 1 && css.ToHash(values[0].Data) == css.None { + values[0].Data = backgroundNoneBytes + } + default: + if bytes.Equal(property, msfilterBytes) { + alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=") + if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) { + values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...) + } } } } - for i := 0; i < len(values); i++ { - if values[i].TokenType == css.FunctionToken { - n, err := c.minifyFunction(values[i:]) + prevComma := true + for _, value := range values { + if !prevComma && value.TokenType != css.CommaToken { + if _, err := c.w.Write([]byte(" ")); err != nil { + return err + } + } + + if value.TokenType == css.FunctionToken { + err := c.minifyFunction(value.Components) if err != nil { return err } - i += n - 1 - } else if _, err := c.w.Write(values[i].Data); err != nil { - return err + } else { + if _, err := c.w.Write(value.Data); err != nil { + return err + } + } + + if value.TokenType == css.CommaToken { + prevComma = true + } else { + prevComma = false } } + if important { if _, err := c.w.Write([]byte("!important")); err != nil { return err @@ -356,22 +439,21 @@ func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) err return nil } -func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { - n := 1 +func (c *cssMinifier) minifyFunction(values []css.Token) error { + n := len(values) simple := true - for i, value := range values[1:] { - if value.TokenType == css.RightParenthesisToken { - n++ - break - } + for i, value := range values[1 : n-1] { if i%2 == 0 && (value.TokenType != css.NumberToken && value.TokenType != css.PercentageToken) || (i%2 == 1 && value.TokenType != css.CommaToken) { simple = false } - n++ } - values = values[:n] - if simple && (n-1)%2 == 0 { - fun := css.ToHash(values[0].Data[:len(values[0].Data)-1]) + + if simple && n%2 == 1 { + fun := css.ToHash(values[0].Data[0 : len(values[0].Data)-1]) + for i := 1; i < n; i += 2 { + values[i].TokenType, values[i].Data = c.shortenToken(0, values[i].TokenType, values[i].Data) + } + nArgs := (n - 1) / 2 if (fun == css.Rgba || fun == css.Hsla) && nArgs == 4 { d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if simple == true than this is a NumberToken or PercentageToken @@ -425,7 +507,7 @@ func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { parse.ToLower(val) if s, ok := ShortenColorHex[string(val)]; ok { if _, err := c.w.Write(s); err != nil { - return 0, err + return err } } else { if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { @@ -434,10 +516,10 @@ func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { val = val[:4] } if _, err := c.w.Write(val); err != nil { - return 0, err + return err } } - return n, nil + return nil } } else if fun == css.Hsl && nArgs == 3 { if values[1].TokenType == css.NumberToken && values[3].TokenType == css.PercentageToken && values[5].TokenType == css.PercentageToken { @@ -453,7 +535,7 @@ func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { parse.ToLower(val) if s, ok := ShortenColorHex[string(val)]; ok { if _, err := c.w.Write(s); err != nil { - return 0, err + return err } } else { if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { @@ -462,20 +544,20 @@ func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { val = val[:4] } if _, err := c.w.Write(val); err != nil { - return 0, err + return err } } - return n, nil + return nil } } } } for _, value := range values { if _, err := c.w.Write(value.Data); err != nil { - return 0, err + return err } } - return n, nil + return nil } func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte) (css.TokenType, []byte) { diff --git a/css/css_test.go b/css/css_test.go index c881b8b4e7..4412cdcb80 100644 --- a/css/css_test.go +++ b/css/css_test.go @@ -95,6 +95,8 @@ func TestCSSInline(t *testing.T) { {"font-weight: bold; font-weight: normal;", "font-weight:700;font-weight:400"}, {"font: bold \"Times new Roman\",\"Sans-Serif\";", "font:700 times new roman,\"sans-serif\""}, {"outline: none;", "outline:0"}, + {"outline: solid black 0;", "outline:0"}, + {"outline: none black 5px;", "outline:0"}, {"outline: none !important;", "outline:0!important"}, {"border-left: none;", "border-left:0"}, {"margin: 1 1 1 1;", "margin:1"}, @@ -113,6 +115,7 @@ func TestCSSInline(t *testing.T) { {"content: \"a\\\nb\";", "content:\"ab\""}, {"content: \"a\\\r\nb\\\r\nc\";", "content:\"abc\""}, {"content: \"\";", "content:\"\""}, + {"x: white , white", "x:#fff,#fff"}, {"font:27px/13px arial,sans-serif", "font:27px/13px arial,sans-serif"}, {"text-decoration: none !important", "text-decoration:none!important"}, @@ -144,6 +147,8 @@ func TestCSSInline(t *testing.T) { {"z-index:1000", "z-index:1000"}, {"any:0deg 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz", "any:0 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz"}, + {"width:calc(0%-0px)", "width:calc(0%-0px)"}, + {"border-left:0 none", "border-left:0"}, {"--custom-variable:0px;", "--custom-variable:0px"}, {"--foo: if(x > 5) this.width = 10", "--foo: if(x > 5) this.width = 10"}, {"--foo: ;", "--foo: "}, From 7f750760397351001e8c3c6ca55c76bef4668474 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Fri, 16 Feb 2018 14:50:09 +1300 Subject: [PATCH 2/5] Add test --- css/css_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/css/css_test.go b/css/css_test.go index 4412cdcb80..d865c58551 100644 --- a/css/css_test.go +++ b/css/css_test.go @@ -99,6 +99,7 @@ func TestCSSInline(t *testing.T) { {"outline: none black 5px;", "outline:0"}, {"outline: none !important;", "outline:0!important"}, {"border-left: none;", "border-left:0"}, + {"border-left: none 0;", "border-left:0"}, {"margin: 1 1 1 1;", "margin:1"}, {"margin: 1 2 1 2;", "margin:1 2"}, {"margin: 1 2 3 2;", "margin:1 2 3"}, From e2165da5278431acecd84936e48b241abea263ed Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Fri, 16 Feb 2018 14:56:03 +1300 Subject: [PATCH 3/5] Use values buffer --- css/css.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/css/css.go b/css/css.go index 672eeef893..2a7c8d909e 100644 --- a/css/css.go +++ b/css/css.go @@ -30,6 +30,8 @@ type cssMinifier struct { w io.Writer p *css.Parser o *Minifier + + valuesBuffer []Token } //////////////////////////////////////////////////////////////// @@ -248,7 +250,7 @@ func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) // Check if this is a simple list of values separated by whitespace or commas, otherwise we'll not be processing simple := true prevSep := true - values := []Token{} + values := c.valuesBuffer[:0] for i := 0; i < len(components); i++ { comp := components[i] if comp.TokenType == css.LeftParenthesisToken || comp.TokenType == css.LeftBraceToken || comp.TokenType == css.LeftBracketToken || comp.TokenType == css.RightParenthesisToken || comp.TokenType == css.RightBraceToken || comp.TokenType == css.RightBracketToken { @@ -288,6 +290,7 @@ func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) values = append(values, Token{components[i].TokenType, components[i].Data, nil}) } } + c.valuesBuffer = values // Do not process complex values (eg. containing blocks or is not alternated between whitespace/commas and flat values if !simple { From 88fb937e52437a5d525fbe11a70a0055b6f90603 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Fri, 16 Feb 2018 15:31:14 +1300 Subject: [PATCH 4/5] Add more rules --- css/css.go | 13 +++++++++++-- css/css_test.go | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/css/css.go b/css/css.go index 2a7c8d909e..7e1e6133df 100644 --- a/css/css.go +++ b/css/css.go @@ -111,7 +111,11 @@ func (c *cssMinifier) minifyGrammar() error { if _, err := c.w.Write(data); err != nil { return err } - for _, val := range c.p.Values() { + values := c.p.Values() + if css.ToHash(data[1:]) == css.Import && len(values) == 2 && values[1].TokenType == css.URLToken { + values[1].Data = values[1].Data[4 : len(values[1].Data)-1] + } + for _, val := range values { if _, err := c.w.Write(val.Data); err != nil { return err } @@ -395,9 +399,14 @@ func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) values[0].Data = zeroBytes } case css.Background: - if len(values) == 1 && css.ToHash(values[0].Data) == css.None { + ident := css.ToHash(values[0].Data) + if len(values) == 1 && (ident == css.None || ident == css.Transparent) { values[0].Data = backgroundNoneBytes } + case css.Box_Shadow: + if len(values) == 4 && len(values[0].Data) == 1 && values[0].Data[0] == '0' && len(values[1].Data) == 1 && values[1].Data[0] == '0' && len(values[2].Data) == 1 && values[2].Data[0] == '0' && len(values[3].Data) == 1 && values[3].Data[0] == '0' { + values = values[:2] + } default: if bytes.Equal(property, msfilterBytes) { alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=") diff --git a/css/css_test.go b/css/css_test.go index d865c58551..e3e6cdb2bd 100644 --- a/css/css_test.go +++ b/css/css_test.go @@ -23,6 +23,7 @@ func TestCSS(t *testing.T) { {".cla[id ^= L] { x:y; }", ".cla[id^=L]{x:y}"}, {"area:focus { outline : 0;}", "area:focus{outline:0}"}, {"@import 'file';", "@import 'file'"}, + {"@import url('file');", "@import 'file'"}, {"@font-face { x:y; }", "@font-face{x:y}"}, {"input[type=\"radio\"]{x:y}", "input[type=radio]{x:y}"}, @@ -145,7 +146,10 @@ func TestCSSInline(t *testing.T) { {"margin:0 0 18px 0;", "margin:0 0 18px"}, {"background:none", "background:0 0"}, {"background:none 1 1", "background:none 1 1"}, + {"background:transparent", "background:0 0"}, + {"background:transparent no-repeat", "background:transparent no-repeat"}, {"z-index:1000", "z-index:1000"}, + {"box-shadow:0 0 0 0", "box-shadow:0 0"}, {"any:0deg 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz", "any:0 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz"}, {"width:calc(0%-0px)", "width:calc(0%-0px)"}, From a38c8f10ec7071f13006c4da5e150d4f85fbe739 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Sat, 24 Feb 2018 08:29:37 +1300 Subject: [PATCH 5/5] Bugfix: add quotes to unquoted import URL --- css/css.go | 10 +++++++++- css/css_test.go | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/css/css.go b/css/css.go index 7e1e6133df..cca43117ee 100644 --- a/css/css.go +++ b/css/css.go @@ -113,7 +113,15 @@ func (c *cssMinifier) minifyGrammar() error { } values := c.p.Values() if css.ToHash(data[1:]) == css.Import && len(values) == 2 && values[1].TokenType == css.URLToken { - values[1].Data = values[1].Data[4 : len(values[1].Data)-1] + url := values[1].Data + if url[4] != '"' && url[4] != '\'' { + url = url[3:] + url[0] = '"' + url[len(url)-1] = '"' + } else { + url = url[4 : len(url)-1] + } + values[1].Data = url } for _, val := range values { if _, err := c.w.Write(val.Data); err != nil { diff --git a/css/css_test.go b/css/css_test.go index e3e6cdb2bd..dbf9fe6c13 100644 --- a/css/css_test.go +++ b/css/css_test.go @@ -24,6 +24,7 @@ func TestCSS(t *testing.T) { {"area:focus { outline : 0;}", "area:focus{outline:0}"}, {"@import 'file';", "@import 'file'"}, {"@import url('file');", "@import 'file'"}, + {"@import url(//url);", `@import "//url"`}, {"@font-face { x:y; }", "@font-face{x:y}"}, {"input[type=\"radio\"]{x:y}", "input[type=radio]{x:y}"},