// Package css minifies CSS3 following the specifications at http://www.w3.org/TR/css-syntax-3/. package css import ( "bytes" "encoding/hex" "fmt" "io" "strconv" "github.com/tdewolff/minify/v2" "github.com/tdewolff/parse/v2" "github.com/tdewolff/parse/v2/css" strconvParse "github.com/tdewolff/parse/v2/strconv" ) var ( spaceBytes = []byte(" ") colonBytes = []byte(":") semicolonBytes = []byte(";") commaBytes = []byte(",") leftBracketBytes = []byte("{") rightBracketBytes = []byte("}") zeroBytes = []byte("0") transparentBytes = []byte("transparent") importantBytes = []byte("!important") ) type cssMinifier struct { m *minify.M w io.Writer p *css.Parser o *Minifier valuesBuffer []Token } //////////////////////////////////////////////////////////////// // DefaultMinifier is the default minifier. var DefaultMinifier = &Minifier{Decimals: -1, KeepCSS2: false} // Minifier is a CSS minifier. type Minifier struct { Decimals int KeepCSS2 bool } // Minify minifies CSS data, it reads from r and writes to w. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error { return DefaultMinifier.Minify(m, w, r, params) } 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) + ")" } return fmt.Sprint(t.Components) } func (a Token) Equal(b Token) bool { if a.TokenType == b.TokenType && bytes.Equal(a.Data, b.Data) && len(a.Components) == len(b.Components) { for i := 0; i < len(a.Components); i++ { if a.Components[i].TokenType != b.Components[i].TokenType || !bytes.Equal(a.Components[i].Data, b.Components[i].Data) { return false } } return true } return false } // Minify minifies CSS data, it reads from r and writes to w. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error { isInline := params != nil && params["inline"] == "1" c := &cssMinifier{ m: m, w: w, p: css.NewParser(r, isInline), o: o, } defer c.p.Restore() if err := c.minifyGrammar(); err != nil && err != io.EOF { return err } return nil } func (c *cssMinifier) minifyGrammar() error { semicolonQueued := false for { gt, _, data := c.p.Next() switch gt { case css.ErrorGrammar: if _, ok := c.p.Err().(*parse.Error); ok { if semicolonQueued { if _, err := c.w.Write(semicolonBytes); err != nil { return err } } // write out the offending declaration (but save the semicolon) vals := c.p.Values() if len(vals) > 0 && vals[len(vals)-1].TokenType == css.SemicolonToken { vals = vals[:len(vals)-1] semicolonQueued = true } for _, val := range vals { if _, err := c.w.Write(val.Data); err != nil { return err } } continue } return c.p.Err() case css.EndAtRuleGrammar, css.EndRulesetGrammar: if _, err := c.w.Write(rightBracketBytes); err != nil { return err } semicolonQueued = false continue } if semicolonQueued { if _, err := c.w.Write(semicolonBytes); err != nil { return err } semicolonQueued = false } switch gt { case css.AtRuleGrammar: if _, err := c.w.Write(data); err != nil { return err } values := c.p.Values() if css.ToHash(data[1:]) == css.Import && len(values) == 2 && values[1].TokenType == css.URLToken { 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 { return err } } semicolonQueued = true case css.BeginAtRuleGrammar: if _, err := c.w.Write(data); err != nil { return err } for _, val := range c.p.Values() { if _, err := c.w.Write(val.Data); err != nil { return err } } if _, err := c.w.Write(leftBracketBytes); err != nil { return err } case css.QualifiedRuleGrammar: if err := c.minifySelectors(data, c.p.Values()); err != nil { return err } if _, err := c.w.Write(commaBytes); err != nil { return err } case css.BeginRulesetGrammar: if err := c.minifySelectors(data, c.p.Values()); err != nil { return err } if _, err := c.w.Write(leftBracketBytes); err != nil { return err } case css.DeclarationGrammar: if _, err := c.w.Write(data); err != nil { return err } if _, err := c.w.Write(colonBytes); err != nil { return err } if err := c.minifyDeclaration(data, c.p.Values()); err != nil { return err } semicolonQueued = true case css.CustomPropertyGrammar: if _, err := c.w.Write(data); err != nil { return err } if _, err := c.w.Write(colonBytes); err != nil { return err } if _, err := c.w.Write(c.p.Values()[0].Data); err != nil { return err } semicolonQueued = true case css.CommentGrammar: if len(data) > 5 && data[1] == '*' && data[2] == '!' { if _, err := c.w.Write(data[:3]); err != nil { return err } comment := parse.TrimWhitespace(parse.ReplaceMultipleWhitespace(data[3 : len(data)-2])) if _, err := c.w.Write(comment); err != nil { return err } if _, err := c.w.Write(data[len(data)-2:]); err != nil { return err } } default: if _, err := c.w.Write(data); err != nil { return err } } } } func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) error { inAttr := false isClass := false for _, val := range c.p.Values() { if !inAttr { if val.TokenType == css.IdentToken { if !isClass { parse.ToLower(val.Data) } isClass = false } else if val.TokenType == css.DelimToken && val.Data[0] == '.' { isClass = true } else if val.TokenType == css.LeftBracketToken { inAttr = true } } else { if val.TokenType == css.StringToken && len(val.Data) > 2 { s := val.Data[1 : len(val.Data)-1] if css.IsIdent(s) { if _, err := c.w.Write(s); err != nil { return err } continue } } else if val.TokenType == css.RightBracketToken { inAttr = false } else if val.TokenType == css.IdentToken && len(val.Data) == 1 && (val.Data[0] == 'i' || val.Data[0] == 'I') { if _, err := c.w.Write(spaceBytes); err != nil { return err } } } if _, err := c.w.Write(val.Data); err != nil { return err } } return nil } func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) error { if len(components) == 0 { return nil } // 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 := c.valuesBuffer[:0] for i := 0; i < len(components); i++ { comp := components[i] tt := comp.TokenType if tt == css.LeftParenthesisToken || tt == css.LeftBraceToken || tt == css.LeftBracketToken || tt == css.RightParenthesisToken || tt == css.RightBraceToken || tt == css.RightBracketToken { simple = false break } if !prevSep && tt != css.WhitespaceToken && tt != css.CommaToken && (tt != css.DelimToken || comp.Data[0] != '/') { simple = false break } if tt == css.WhitespaceToken || tt == css.CommaToken || tt == css.DelimToken && comp.Data[0] == '/' { prevSep = true if tt != css.WhitespaceToken { values = append(values, Token{tt, comp.Data, nil}) } } else if tt == 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 } level-- } } 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}) } } c.valuesBuffer = values prop := css.ToHash(property) // 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(") } } for _, component := range components { if _, err := c.w.Write(component.Data); err != nil { return err } } if important { if _, err := c.w.Write(importantBytes); 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 { values = c.minifyProperty(prop, values) } prevSep = true for _, value := range values { if !prevSep && value.TokenType != css.CommaToken && (value.TokenType != css.DelimToken || value.Data[0] != '/') { if _, err := c.w.Write(spaceBytes); err != nil { return err } } if value.TokenType == css.FunctionToken { err := c.minifyFunction(value.Components) if err != nil { return err } } else if _, err := c.w.Write(value.Data); err != nil { return err } if value.TokenType == css.CommaToken || value.TokenType == css.DelimToken && value.Data[0] == '/' || value.TokenType == css.FunctionToken || value.TokenType == css.URLToken { prevSep = true } else { prevSep = false } } if important { if _, err := c.w.Write(importantBytes); err != nil { return err } } return nil } func (c *cssMinifier) minifyProperty(prop css.Hash, values []Token) []Token { switch prop { case css.Font: if len(values) > 1 { // the font-families are separated by commas and are at the end of font // get index for the first font-family given i := len(values) - 1 for j, value := range values[2:] { if value.TokenType == css.CommaToken { i = 2 + j - 1 // identifier before first comma is a font-family break } } i-- // advance i while still at font-families when they contain spaces but no quotes for ; i > 0; i-- { // i cannot be 0, font-family must be prepended by font-size if values[i-1].TokenType == css.DelimToken && values[i-1].Data[0] == '/' { break } else if values[i].TokenType != css.IdentToken && values[i].TokenType != css.StringToken { break } else if values[i].TokenType == css.IdentToken { h := css.ToHash(values[i].Data) // inherit, initial and unset are followed by an IdentToken/StringToken, so must be for font-size if h == css.Xx_Small || h == css.X_Small || h == css.Small || h == css.Medium || h == css.Large || h == css.X_Large || h == css.Xx_Large || h == css.Smaller || h == css.Larger || h == css.Inherit || h == css.Initial || h == css.Unset { break } } } // font-family minified in place values = append(values[:i+1], c.minifyProperty(css.Font_Family, values[i+1:])...) if i > 0 { // line-height if i > 1 && values[i-1].TokenType == css.DelimToken && values[i-1].Data[0] == '/' { if values[i].TokenType == css.IdentToken && bytes.Equal(values[i].Data, []byte("normal")) { values = append(values[:i-1], values[i+1:]...) } i -= 2 } // font-size i-- for ; i > -1; i-- { if values[i].TokenType == css.IdentToken { val := css.ToHash(values[i].Data) if val == css.Normal { values = append(values[:i], values[i+1:]...) } else if val == css.Bold { values[i].TokenType = css.NumberToken values[i].Data = []byte("700") } } else if values[i].TokenType == css.NumberToken && bytes.Equal(values[i].Data, []byte("400")) { values = append(values[:i], values[i+1:]...) } } } } case css.Font_Family: for i, value := range values { 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) { // if len is zero, it contains two consecutive spaces if len(split) == 0 || !css.IsIdent(split) { unquote = false break } } } if unquote { values[i].Data = s } } } case css.Font_Weight: if len(values) == 1 && values[0].TokenType == css.IdentToken { val := css.ToHash(values[0].Data) if val == css.Normal { values[0].TokenType = css.NumberToken values[0].Data = []byte("400") } else if val == css.Bold { values[0].TokenType = css.NumberToken values[0].Data = []byte("700") } } case css.Url: for i := 0; i < len(values); i++ { if values[i].TokenType == css.FunctionToken && 2 < len(values[i].Components) { fun := values[i].Components name := css.ToHash(fun[0].Data[0 : len(fun[0].Data)-1]) if name == css.Local { data := fun[1].Data if data[0] == '\'' || data[0] == '"' { data = removeStringNewlinex(data) if css.IsURLUnquoted(data[1 : len(data)-1]) { data = data[1 : len(data)-1] } fun[1].Data = data } } } } case css.Margin, css.Padding, css.Border_Width: switch len(values) { case 2: if values[0].Equal(values[1]) { values = values[:1] } case 3: if values[0].Equal(values[1]) && values[0].Equal(values[2]) { values = values[:1] } else if values[0].Equal(values[2]) { values = values[:2] } case 4: if values[0].Equal(values[1]) && values[0].Equal(values[2]) && values[0].Equal(values[3]) { values = values[:1] } else if values[0].Equal(values[2]) && values[1].Equal(values[3]) { values = values[:2] } else if values[1].Equal(values[3]) { values = values[:3] } } case css.Border, css.Border_Bottom, css.Border_Left, css.Border_Right, css.Border_Top: for i := 0; i < len(values); i++ { if values[i].TokenType == css.IdentToken { val := css.ToHash(values[i].Data) if val == css.None || val == css.Currentcolor || val == css.Medium { values = append(values[:i], values[i+1:]...) i-- } } } if len(values) == 0 { values = []Token{{css.IdentToken, []byte("none"), nil}} } case css.Outline: for i := 0; i < len(values); i++ { if values[i].TokenType == css.IdentToken { val := css.ToHash(values[i].Data) if val == css.None || val == css.Medium { // color=invert is not supported by all browsers values = append(values[:i], values[i+1:]...) i-- } } } if len(values) == 0 { values = []Token{{css.IdentToken, []byte("none"), nil}} } case css.Background: // TODO: multiple background layers separated by comma hasSize := false for i := 0; i < len(values); i++ { if values[i].TokenType == css.DelimToken && values[i].Data[0] == '/' { hasSize = true // background-size consists of either [ | auto | cover | contain] or [ | auto]{2} // we can only minify the latter if i+1 < len(values) && (values[i+1].TokenType == css.NumberToken || values[i+1].TokenType == css.PercentageToken || values[i+1].TokenType == css.IdentToken && bytes.Equal(values[i+1].Data, []byte("auto")) || values[i+1].TokenType == css.FunctionToken) { if i+2 < len(values) && (values[i+2].TokenType == css.NumberToken || values[i+2].TokenType == css.PercentageToken || values[i+2].TokenType == css.IdentToken && bytes.Equal(values[i+2].Data, []byte("auto")) || values[i+2].TokenType == css.FunctionToken) { sizeValues := c.minifyProperty(css.Background_Size, values[i+1:i+3]) if len(sizeValues) == 1 && bytes.Equal(sizeValues[0].Data, []byte("auto")) { // remove background-size if it is '/ auto' after minifying the property values = append(values[:i], values[i+3:]...) hasSize = false i-- } else { values = append(values[:i+1], append(sizeValues, values[i+3:]...)...) i += len(sizeValues) - 1 } } else if values[i+1].TokenType == css.IdentToken && bytes.Equal(values[i+1].Data, []byte("auto")) { // remove background-size if it is '/ auto' values = append(values[:i], values[i+2:]...) hasSize = false i-- } } } } var h css.Hash iPaddingBox := -1 // position of background-origin that is padding-box for i := 0; i < len(values); i++ { if values[i].TokenType == css.IdentToken { h = css.ToHash(values[i].Data) if i+1 < len(values) && values[i+1].TokenType == css.IdentToken && (h == css.Space || h == css.Round || h == css.Repeat || h == css.No_Repeat) { if h2 := css.ToHash(values[i+1].Data); h2 == css.Space || h2 == css.Round || h2 == css.Repeat || h2 == css.No_Repeat { repeatValues := c.minifyProperty(css.Background_Repeat, values[i:i+2]) if len(repeatValues) == 1 && bytes.Equal(repeatValues[0].Data, []byte("repeat")) { values = append(values[:i], values[i+2:]...) i-- } else { values = append(values[:i], append(repeatValues, values[i+2:]...)...) i += len(repeatValues) - 1 } continue } } else if h == css.None || h == css.Scroll || h == css.Transparent { values = append(values[:i], values[i+1:]...) i-- continue } else if h == css.Border_Box || h == css.Padding_Box { if iPaddingBox == -1 && h == css.Padding_Box { // background-origin iPaddingBox = i } else if iPaddingBox != -1 && h == css.Border_Box { // background-clip values = append(values[:i], values[i+1:]...) values = append(values[:iPaddingBox], values[iPaddingBox+1:]...) i -= 2 } continue } } else if values[i].TokenType == css.HashToken && bytes.Equal(values[i].Data, []byte("#0000")) { values = append(values[:i], values[i+1:]...) i-- continue } // background-position or background-size // TODO: allow only functions that return Number, Percentage or Dimension token. Make whitelist? if values[i].TokenType == css.NumberToken || values[i].TokenType == css.DimensionToken || values[i].TokenType == css.PercentageToken || values[i].TokenType == css.IdentToken && (h == css.Left || h == css.Right || h == css.Top || h == css.Bottom || h == css.Center) || values[i].TokenType == css.FunctionToken && bytes.Equal(values[i].Data, []byte("calc(")) { j := i + 1 for ; j < len(values); j++ { if values[j].TokenType == css.IdentToken { h := css.ToHash(values[j].Data) if h == css.Left || h == css.Right || h == css.Top || h == css.Bottom || h == css.Center { continue } } else if values[j].TokenType == css.NumberToken || values[j].TokenType == css.DimensionToken || values[j].TokenType == css.PercentageToken || values[i].TokenType == css.FunctionToken && bytes.Equal(values[i].Data, []byte("calc(")) { continue } break } positionValues := c.minifyProperty(css.Background_Position, values[i:j]) if !hasSize && len(positionValues) == 2 && positionValues[0].TokenType == css.NumberToken && bytes.Equal(positionValues[0].Data, []byte("0")) && positionValues[0].Equal(positionValues[1]) { values = append(values[:i], values[j:]...) i-- } else { values = append(values[:i], append(positionValues, values[j:]...)...) i += len(positionValues) - 1 } } } if len(values) == 0 { values = []Token{{css.NumberToken, []byte("0"), nil}, {css.NumberToken, []byte("0"), nil}} } case css.Background_Size: if len(values) == 2 && values[1].TokenType == css.IdentToken && bytes.Equal(values[1].Data, []byte("auto")) { values = values[:1] } case css.Background_Repeat: if len(values) == 2 && values[0].TokenType == css.IdentToken && values[1].TokenType == css.IdentToken { h0 := css.ToHash(values[0].Data) h1 := css.ToHash(values[1].Data) if h0 == h1 { values = values[:1] } else if h0 == css.Repeat && h1 == css.No_Repeat { values = values[:1] values[0].Data = []byte("repeat-x") } else if h0 == css.No_Repeat && h1 == css.Repeat { values = values[:1] values[0].Data = []byte("repeat-y") } } case css.Background_Position: if len(values) == 3 || len(values) == 4 { // remove zero offsets for _, i := range []int{len(values) - 1, 1} { if 2 < len(values) && (values[i].TokenType == css.NumberToken && bytes.Equal(values[i].Data, []byte("0")) || values[i].TokenType == css.PercentageToken && bytes.Equal(values[i].Data, []byte("0%"))) { values = append(values[:i], values[i+1:]...) } } j := 1 // position of second set of horizontal/vertical values if 2 < len(values) && values[2].TokenType == css.IdentToken { j = 2 } hs := make([]css.Hash, 3) hs[0] = css.ToHash(values[0].Data) hs[j] = css.ToHash(values[j].Data) b := make([]byte, 0, 4) offsets := make([]Token, 2) for _, i := range []int{j, 0} { if i+1 < len(values) && i+1 != j { if values[i+1].TokenType == css.PercentageToken { // change right or bottom with percentage offset to left or top respectively if hs[i] == css.Right || hs[i] == css.Bottom { n, _ := strconvParse.ParseInt(values[i+1].Data[:len(values[i+1].Data)-1]) b = strconv.AppendInt(b[:0], 100-n, 10) b = append(b, '%') values[i+1].Data = b if hs[i] == css.Right { values[i].Data = []byte("left") hs[i] = css.Left } else { values[i].Data = []byte("top") hs[i] = css.Top } } } if hs[i] == css.Left { offsets[0] = values[i+1] } else if hs[i] == css.Top { offsets[1] = values[i+1] } } else if hs[i] == css.Left { offsets[0] = Token{css.NumberToken, []byte("0"), nil} } else if hs[i] == css.Top { offsets[1] = Token{css.NumberToken, []byte("0"), nil} } else if hs[i] == css.Right { offsets[0] = Token{css.PercentageToken, []byte("100%"), nil} hs[i] = css.Left } else if hs[i] == css.Bottom { offsets[1] = Token{css.PercentageToken, []byte("100%"), nil} hs[i] = css.Top } } if hs[0] == css.Center || hs[j] == css.Center { if hs[0] == css.Left || hs[j] == css.Left { offsets = offsets[:1] } else if hs[0] == css.Top || hs[j] == css.Top { offsets[0] = Token{css.NumberToken, []byte("50%"), nil} } } if offsets[0].Data != nil && (len(offsets) == 1 || offsets[1].Data != nil) { values = offsets } } // removing zero offsets in the previous loop might make it eligible for the next loop if len(values) == 1 || len(values) == 2 { if values[0].TokenType == css.IdentToken { h := css.ToHash(values[0].Data) if h == css.Top || h == css.Bottom { if len(values) == 1 { // we can't make this smaller, and converting to a number will break it // (https://github.com/tdewolff/minify/issues/221#issuecomment-415419918) break } // if it's a vertical position keyword, swap it with the next element // since otherwise converted number positions won't be valid anymore // (https://github.com/tdewolff/minify/issues/221#issue-353067229) values[0], values[1] = values[1], values[0] } } // transform keywords to lengths|percentages for i := 0; i < len(values); i++ { if values[i].TokenType == css.IdentToken { h := css.ToHash(values[i].Data) if h == css.Left || h == css.Top { values[i].TokenType = css.NumberToken values[i].Data = []byte("0") } else if h == css.Right || h == css.Bottom { values[i].TokenType = css.PercentageToken values[i].Data = []byte("100%") } else if h == css.Center { if i == 0 { values[i].TokenType = css.PercentageToken values[i].Data = []byte("50%") } else { values = values[:1] } } } else if i == 1 && values[i].TokenType == css.PercentageToken && bytes.Equal(values[i].Data, []byte("50%")) { values = values[:1] } else if values[i].TokenType == css.PercentageToken && bytes.Equal(values[i].Data, []byte("0%")) { values[i].TokenType = css.NumberToken values[i].Data = []byte("0") } } } 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] } case css.Ms_Filter: alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=") if values[0].TokenType == css.StringToken && 2 < len(values[0].Data) && 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):]...) } } return values } func (c *cssMinifier) minifyColorAsHex(rgba [3]byte) error { val := make([]byte, 7) val[0] = '#' hex.Encode(val[1:], rgba[:]) parse.ToLower(val) if s, ok := ShortenColorHex[string(val[:7])]; ok { if _, err := c.w.Write(s); err != nil { return err } return nil } else if val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { val[2] = val[3] val[3] = val[5] val = val[:4] } else { val = val[:7] } _, err := c.w.Write(val) return err } func (c *cssMinifier) minifyFunction(values []css.Token) error { if n := len(values); n > 2 { fun := css.ToHash(values[0].Data[0 : len(values[0].Data)-1]) if fun == css.Rgb || fun == css.Rgba || fun == css.Hsl || fun == css.Hsla { valid := true vals := make([]*css.Token, 0, 4) for i, value := range values[1 : n-1] { numeric := value.TokenType == css.NumberToken || value.TokenType == css.PercentageToken separator := value.TokenType == css.CommaToken || i != 5 && value.TokenType == css.WhitespaceToken || i == 5 && value.TokenType == css.DelimToken && value.Data[0] == '/' if i%2 == 0 && !numeric || i%2 == 1 && !separator { valid = false } else if numeric { vals = append(vals, &values[i+1]) } } if valid { for _, val := range vals { val.TokenType, val.Data = c.shortenToken(0, val.TokenType, val.Data) } a := byte(255) if len(vals) == 4 { d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if valid == true than this is a NumberToken or PercentageToken if d < minify.Epsilon { // zero or less _, err := c.w.Write(transparentBytes) return err } else if d >= 1.0 { values = values[:7] } else { a = byte(d*255.0 + 0.5) } } if a == 255 { // only minify color if fully opaque if (fun == css.Rgb || fun == css.Rgba) && (len(vals) == 3 || len(vals) == 4) { rgb := [3]byte{} for j, val := range vals[:3] { if val.TokenType == css.NumberToken { d, _ := strconv.ParseInt(string(val.Data), 10, 32) if d < 0 { d = 0 } else if d > 255 { d = 255 } rgb[j] = byte(d) } else if val.TokenType == css.PercentageToken { d, _ := strconv.ParseFloat(string(val.Data[:len(val.Data)-1]), 32) if d < 0.0 { d = 0.0 } else if d > 100.0 { d = 100.0 } rgb[j] = byte((d / 100.0 * 255.0) + 0.5) } } return c.minifyColorAsHex(rgb) } else if (fun == css.Hsl || fun == css.Hsla) && (len(vals) == 3 || len(vals) == 4) && vals[0].TokenType == css.NumberToken && vals[1].TokenType == css.PercentageToken && vals[2].TokenType == css.PercentageToken { h, _ := strconv.ParseFloat(string(vals[0].Data), 32) s, _ := strconv.ParseFloat(string(vals[1].Data[:len(vals[1].Data)-1]), 32) l, _ := strconv.ParseFloat(string(vals[2].Data[:len(vals[2].Data)-1]), 32) for h > 360.0 { h -= 360.0 } if s < 0.0 { s = 0.0 } else if s > 100.0 { s = 100.0 } if l < 0.0 { l = 0.0 } else if l > 100.0 { l = 100.0 } r, g, b := css.HSL2RGB(h/360.0, s/100.0, l/100.0) rgb := [3]byte{byte((r * 255.0) + 0.5), byte((g * 255.0) + 0.5), byte((b * 255.0) + 0.5)} return c.minifyColorAsHex(rgb) } } } } } for _, value := range values { if _, err := c.w.Write(value.Data); err != nil { return err } } return nil } func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte) (css.TokenType, []byte) { switch tt { case css.NumberToken, css.PercentageToken, css.DimensionToken: if tt == css.NumberToken && (prop == css.Z_Index || prop == css.Counter_Increment || prop == css.Counter_Reset || prop == css.Orphans || prop == css.Widows) { return tt, data // integers } n := len(data) if tt == css.PercentageToken { n-- } else if tt == css.DimensionToken { n = parse.Number(data) } dim := data[n:] parse.ToLower(dim) if !c.o.KeepCSS2 { data = minify.Number(data[:n], c.o.Decimals) } else { data = minify.Decimal(data[:n], c.o.Decimals) // don't use exponents } if tt == css.DimensionToken && (len(data) != 1 || data[0] != '0' || !optionalZeroDimension[string(dim)] || prop == css.Flex) { data = append(data, dim...) } else if tt == css.PercentageToken { data = append(data, '%') // TODO: drop percentage for properties that accept and } case css.IdentToken: parse.ToLower(parse.Copy(data)) // not all identifiers are case-insensitive; all properties are case-sensitive hash := css.ToHash(data) if hexValue, ok := ShortenColorName[hash]; ok { tt = css.HashToken data = hexValue } case css.HashToken: parse.ToLower(data) if len(data) == 9 && data[7] == data[8] { if data[7] == 'f' { data = data[:7] } else if data[7] == '0' { data = []byte("#0000") } } if ident, ok := ShortenColorHex[string(data)]; ok { tt = css.IdentToken data = ident } else if len(data) == 7 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] { tt = css.HashToken data[2] = data[3] data[3] = data[5] data = data[:4] } else if len(data) == 9 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] && data[7] == data[8] { // from working draft Color Module Level 4 tt = css.HashToken data[2] = data[3] data[3] = data[5] data[4] = data[7] data = data[:5] } case css.StringToken: data = removeStringNewlinex(data) case css.URLToken: parse.ToLower(data[:3]) if 10 < len(data) { uri := parse.TrimWhitespace(data[4 : len(data)-1]) delim := byte('"') if 1 < len(uri) && (uri[0] == '\'' || uri[0] == '"') { delim = uri[0] uri = removeStringNewlinex(uri) uri = uri[1 : len(uri)-1] } uri = minify.DataURI(c.m, uri) if css.IsURLUnquoted(uri) { data = append(append([]byte("url("), uri...), ')') } else { data = append(append(append([]byte("url("), delim), uri...), delim, ')') } } } return tt, data } func removeStringNewlinex(data []byte) []byte { // remove any \\\r\n \\\r \\\n for i := 1; i < len(data)-2; i++ { if data[i] == '\\' && (data[i+1] == '\n' || data[i+1] == '\r') { // encountered first replacee, now start to move bytes to the front j := i + 2 if data[i+1] == '\r' && len(data) > i+2 && data[i+2] == '\n' { j++ } for ; j < len(data); j++ { if data[j] == '\\' && len(data) > j+1 && (data[j+1] == '\n' || data[j+1] == '\r') { if data[j+1] == '\r' && len(data) > j+2 && data[j+2] == '\n' { j++ } j++ } else { data[i] = data[j] i++ } } data = data[:i] break } } return data }