1// Package css minifies CSS3 following the specifications at http://www.w3.org/TR/css-syntax-3/.
   2package css
   3
   4import (
   5	"bytes"
   6	"fmt"
   7	"io"
   8	"math"
   9	"sort"
  10	"strconv"
  11	"strings"
  12
  13	"github.com/tdewolff/minify/v2"
  14	"github.com/tdewolff/parse/v2"
  15	"github.com/tdewolff/parse/v2/css"
  16	strconvParse "github.com/tdewolff/parse/v2/strconv"
  17)
  18
  19var (
  20	spaceBytes        = []byte(" ")
  21	colonBytes        = []byte(":")
  22	semicolonBytes    = []byte(";")
  23	commaBytes        = []byte(",")
  24	leftBracketBytes  = []byte("{")
  25	rightBracketBytes = []byte("}")
  26	rightParenBytes   = []byte(")")
  27	urlBytes          = []byte("url(")
  28	zeroBytes         = []byte("0")
  29	oneBytes          = []byte("1")
  30	transparentBytes  = []byte("transparent")
  31	blackBytes        = []byte("#0000")
  32	initialBytes      = []byte("initial")
  33	noneBytes         = []byte("none")
  34	autoBytes         = []byte("auto")
  35	leftBytes         = []byte("left")
  36	topBytes          = []byte("top")
  37	n400Bytes         = []byte("400")
  38	n700Bytes         = []byte("700")
  39	n50pBytes         = []byte("50%")
  40	n100pBytes        = []byte("100%")
  41	repeatXBytes      = []byte("repeat-x")
  42	repeatYBytes      = []byte("repeat-y")
  43	importantBytes    = []byte("!important")
  44	dataSchemeBytes   = []byte("data:")
  45)
  46
  47type cssMinifier struct {
  48	m *minify.M
  49	w io.Writer
  50	p *css.Parser
  51	o *Minifier
  52
  53	tokenBuffer []Token
  54	tokensLevel int
  55}
  56
  57////////////////////////////////////////////////////////////////
  58
  59// Minifier is a CSS minifier.
  60type Minifier struct {
  61	KeepCSS2     bool
  62	Precision    int // number of significant digits
  63	newPrecision int // precision for new numbers
  64}
  65
  66// Minify minifies CSS data, it reads from r and writes to w.
  67func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  68	return (&Minifier{}).Minify(m, w, r, params)
  69}
  70
  71// Token is a parsed token with extra information for functions.
  72type Token struct {
  73	css.TokenType
  74	Data       []byte
  75	Args       []Token // only filled for functions
  76	Fun, Ident Hash    // only filled for functions and identifiers respectively
  77}
  78
  79func (t Token) String() string {
  80	if len(t.Args) == 0 {
  81		return t.TokenType.String() + "(" + string(t.Data) + ")"
  82	}
  83	return fmt.Sprint(t.Args)
  84}
  85
  86// Equal returns true if both tokens are equal.
  87func (t Token) Equal(t2 Token) bool {
  88	if t.TokenType == t2.TokenType && bytes.Equal(t.Data, t2.Data) && len(t.Args) == len(t2.Args) {
  89		for i := 0; i < len(t.Args); i++ {
  90			if t.Args[i].TokenType != t2.Args[i].TokenType || !bytes.Equal(t.Args[i].Data, t2.Args[i].Data) {
  91				return false
  92			}
  93		}
  94		return true
  95	}
  96	return false
  97}
  98
  99// IsZero return true if a dimension, percentage, or number token is zero.
 100func (t Token) IsZero() bool {
 101	// as each number is already minified, starting with a zero means it is zero
 102	return (t.TokenType == css.DimensionToken || t.TokenType == css.PercentageToken || t.TokenType == css.NumberToken) && t.Data[0] == '0'
 103}
 104
 105// IsLength returns true if the token is a length.
 106func (t Token) IsLength() bool {
 107	if t.TokenType == css.DimensionToken {
 108		return true
 109	} else if t.TokenType == css.NumberToken && t.Data[0] == '0' {
 110		return true
 111	} else if t.TokenType == css.FunctionToken {
 112		fun := ToHash(t.Data[:len(t.Data)-1])
 113		if fun == Calc || fun == Min || fun == Max || fun == Clamp || fun == Attr || fun == Var || fun == Env {
 114			return true
 115		}
 116	}
 117	return false
 118}
 119
 120// IsLengthPercentage returns true if the token is a length or percentage token.
 121func (t Token) IsLengthPercentage() bool {
 122	return t.TokenType == css.PercentageToken || t.IsLength()
 123}
 124
 125////////////////////////////////////////////////////////////////
 126
 127// Minify minifies CSS data, it reads from r and writes to w.
 128func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
 129	o.newPrecision = o.Precision
 130	if o.newPrecision <= 0 || 15 < o.newPrecision {
 131		o.newPrecision = 15 // minimum number of digits a double can represent exactly
 132	}
 133
 134	z := parse.NewInput(r)
 135	defer z.Restore()
 136
 137	isInline := params != nil && params["inline"] == "1"
 138	c := &cssMinifier{
 139		m: m,
 140		w: w,
 141		p: css.NewParser(z, isInline),
 142		o: o,
 143	}
 144	c.minifyGrammar()
 145
 146	if _, err := w.Write(nil); err != nil {
 147		return err
 148	}
 149	if c.p.Err() == io.EOF {
 150		return nil
 151	}
 152	return c.p.Err()
 153}
 154
 155func (c *cssMinifier) minifyGrammar() {
 156	semicolonQueued := false
 157	for {
 158		gt, _, data := c.p.Next()
 159		switch gt {
 160		case css.ErrorGrammar:
 161			if c.p.HasParseError() {
 162				if semicolonQueued {
 163					c.w.Write(semicolonBytes)
 164				}
 165
 166				// write out the offending declaration (but save the semicolon)
 167				vals := c.p.Values()
 168				if len(vals) > 0 && vals[len(vals)-1].TokenType == css.SemicolonToken {
 169					vals = vals[:len(vals)-1]
 170					semicolonQueued = true
 171				}
 172				for _, val := range vals {
 173					c.w.Write(val.Data)
 174				}
 175				continue
 176			}
 177			return
 178		case css.EndAtRuleGrammar, css.EndRulesetGrammar:
 179			c.w.Write(rightBracketBytes)
 180			semicolonQueued = false
 181			continue
 182		}
 183
 184		if semicolonQueued {
 185			c.w.Write(semicolonBytes)
 186			semicolonQueued = false
 187		}
 188
 189		switch gt {
 190		case css.AtRuleGrammar:
 191			c.w.Write(data)
 192			values := c.p.Values()
 193			if ToHash(data[1:]) == Import && len(values) == 2 && values[1].TokenType == css.URLToken && 4 < len(values[1].Data) && values[1].Data[len(values[1].Data)-1] == ')' {
 194				url := values[1].Data
 195				if url[4] != '"' && url[4] != '\'' {
 196					a := 4
 197					for parse.IsWhitespace(url[a]) || parse.IsNewline(url[a]) {
 198						a++
 199					}
 200					b := len(url) - 2
 201					for a < b && (parse.IsWhitespace(url[b]) || parse.IsNewline(url[b])) {
 202						b--
 203					}
 204					if a == b {
 205						url = url[:2]
 206					} else {
 207						url = url[a-1 : b+2]
 208					}
 209					url[0] = '"'
 210					url[len(url)-1] = '"'
 211				} else {
 212					url = url[4 : len(url)-1]
 213				}
 214				values[1].Data = url
 215			}
 216			for _, val := range values {
 217				c.w.Write(val.Data)
 218			}
 219			semicolonQueued = true
 220		case css.BeginAtRuleGrammar:
 221			c.w.Write(data)
 222			for _, val := range c.p.Values() {
 223				c.w.Write(val.Data)
 224			}
 225			c.w.Write(leftBracketBytes)
 226		case css.QualifiedRuleGrammar:
 227			c.minifySelectors(data, c.p.Values())
 228			c.w.Write(commaBytes)
 229		case css.BeginRulesetGrammar:
 230			c.minifySelectors(data, c.p.Values())
 231			c.w.Write(leftBracketBytes)
 232		case css.DeclarationGrammar:
 233			c.minifyDeclaration(data, c.p.Values())
 234			semicolonQueued = true
 235		case css.CustomPropertyGrammar:
 236			c.w.Write(data)
 237			c.w.Write(colonBytes)
 238			value := parse.TrimWhitespace(c.p.Values()[0].Data)
 239			if len(c.p.Values()[0].Data) != 0 && len(value) == 0 {
 240				value = spaceBytes
 241			}
 242			c.w.Write(value)
 243			semicolonQueued = true
 244		case css.CommentGrammar:
 245			if len(data) > 5 && data[1] == '*' && data[2] == '!' {
 246				c.w.Write(data[:3])
 247				comment := parse.TrimWhitespace(parse.ReplaceMultipleWhitespace(data[3 : len(data)-2]))
 248				c.w.Write(comment)
 249				c.w.Write(data[len(data)-2:])
 250			}
 251		default:
 252			c.w.Write(data)
 253		}
 254	}
 255}
 256
 257func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) {
 258	inAttr := false
 259	isClass := false
 260	for _, val := range c.p.Values() {
 261		if !inAttr {
 262			if val.TokenType == css.IdentToken {
 263				if !isClass {
 264					parse.ToLower(val.Data)
 265				}
 266				isClass = false
 267			} else if val.TokenType == css.DelimToken && val.Data[0] == '.' {
 268				isClass = true
 269			} else if val.TokenType == css.LeftBracketToken {
 270				inAttr = true
 271			}
 272		} else {
 273			if val.TokenType == css.StringToken && len(val.Data) > 2 {
 274				s := val.Data[1 : len(val.Data)-1]
 275				if css.IsIdent(s) {
 276					c.w.Write(s)
 277					continue
 278				}
 279			} else if val.TokenType == css.RightBracketToken {
 280				inAttr = false
 281			} else if val.TokenType == css.IdentToken && len(val.Data) == 1 && (val.Data[0] == 'i' || val.Data[0] == 'I') {
 282				c.w.Write(spaceBytes)
 283			}
 284		}
 285		c.w.Write(val.Data)
 286	}
 287}
 288
 289func (c *cssMinifier) parseFunction(values []css.Token) ([]Token, int) {
 290	i := 1
 291	level := 0
 292	args := []Token{}
 293	for ; i < len(values); i++ {
 294		tt := values[i].TokenType
 295		data := values[i].Data
 296		if tt == css.LeftParenthesisToken {
 297			level++
 298		} else if tt == css.RightParenthesisToken {
 299			if level == 0 {
 300				i++
 301				break
 302			}
 303			level--
 304		}
 305		if tt == css.FunctionToken {
 306			subArgs, di := c.parseFunction(values[i:])
 307			h := ToHash(parse.ToLower(parse.Copy(data[:len(data)-1]))) // TODO: use ToHashFold
 308			args = append(args, Token{tt, data, subArgs, h, 0})
 309			i += di - 1
 310		} else {
 311			var h Hash
 312			if tt == css.IdentToken {
 313				h = ToHash(parse.ToLower(parse.Copy(data))) // TODO: use ToHashFold
 314			}
 315			args = append(args, Token{tt, data, nil, 0, h})
 316		}
 317	}
 318	return args, i
 319}
 320
 321func (c *cssMinifier) parseDeclaration(values []css.Token) []Token {
 322	// Check if this is a simple list of values separated by whitespace or commas, otherwise we'll not be processing
 323	prevSep := true
 324	tokens := c.tokenBuffer[:0]
 325	for i := 0; i < len(values); i++ {
 326		tt := values[i].TokenType
 327		data := values[i].Data
 328		if tt == css.LeftParenthesisToken || tt == css.LeftBraceToken || tt == css.LeftBracketToken ||
 329			tt == css.RightParenthesisToken || tt == css.RightBraceToken || tt == css.RightBracketToken {
 330			return nil
 331		}
 332
 333		if !prevSep && tt != css.WhitespaceToken && tt != css.CommaToken && (tt != css.DelimToken || values[i].Data[0] != '/') {
 334			return nil
 335		}
 336
 337		if tt == css.WhitespaceToken || tt == css.CommaToken || tt == css.DelimToken && values[i].Data[0] == '/' {
 338			if tt != css.WhitespaceToken {
 339				tokens = append(tokens, Token{tt, data, nil, 0, 0})
 340			}
 341			prevSep = true
 342		} else if tt == css.FunctionToken {
 343			args, di := c.parseFunction(values[i:])
 344			h := ToHash(parse.ToLower(parse.Copy(data[:len(data)-1]))) // TODO: use ToHashFold
 345			tokens = append(tokens, Token{tt, data, args, h, 0})
 346			prevSep = true
 347			i += di - 1
 348		} else {
 349			var h Hash
 350			if tt == css.IdentToken {
 351				h = ToHash(parse.ToLower(parse.Copy(data))) // TODO: use ToHashFold
 352			}
 353			tokens = append(tokens, Token{tt, data, nil, 0, h})
 354			prevSep = tt == css.URLToken
 355		}
 356	}
 357	c.tokenBuffer = tokens // update buffer size for memory reuse
 358	return tokens
 359}
 360
 361func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) {
 362	c.w.Write(property)
 363	c.w.Write(colonBytes)
 364
 365	if len(components) == 0 {
 366		return
 367	}
 368
 369	// Strip !important from the component list, this will be added later separately
 370	important := false
 371	if len(components) > 2 && components[len(components)-2].TokenType == css.DelimToken && components[len(components)-2].Data[0] == '!' && ToHash(components[len(components)-1].Data) == Important {
 372		components = components[:len(components)-2]
 373		important = true
 374	}
 375
 376	prop := ToHash(property)
 377	values := c.parseDeclaration(components)
 378
 379	// Do not process complex values (eg. containing blocks or is not alternated between whitespace/commas and flat values
 380	if values == nil {
 381		if prop == Filter && len(components) == 11 {
 382			if bytes.Equal(components[0].Data, []byte("progid")) &&
 383				components[1].TokenType == css.ColonToken &&
 384				bytes.Equal(components[2].Data, []byte("DXImageTransform")) &&
 385				components[3].Data[0] == '.' &&
 386				bytes.Equal(components[4].Data, []byte("Microsoft")) &&
 387				components[5].Data[0] == '.' &&
 388				bytes.Equal(components[6].Data, []byte("Alpha(")) &&
 389				bytes.Equal(parse.ToLower(components[7].Data), []byte("opacity")) &&
 390				components[8].Data[0] == '=' &&
 391				components[10].Data[0] == ')' {
 392				components = components[6:]
 393				components[0].Data = []byte("alpha(")
 394			}
 395		}
 396
 397		for _, component := range components {
 398			c.w.Write(component.Data)
 399		}
 400		if important {
 401			c.w.Write(importantBytes)
 402		}
 403		return
 404	}
 405
 406	values = c.minifyTokens(prop, 0, values)
 407	if len(values) > 0 {
 408		values = c.minifyProperty(prop, values)
 409	}
 410	c.writeDeclaration(values, important)
 411}
 412
 413func (c *cssMinifier) writeFunction(args []Token) {
 414	for _, arg := range args {
 415		c.w.Write(arg.Data)
 416		if arg.TokenType == css.FunctionToken {
 417			c.writeFunction(arg.Args)
 418			c.w.Write(rightParenBytes)
 419		}
 420	}
 421}
 422
 423func (c *cssMinifier) writeDeclaration(values []Token, important bool) {
 424	prevSep := true
 425	for _, value := range values {
 426		if !prevSep && value.TokenType != css.CommaToken && (value.TokenType != css.DelimToken || value.Data[0] != '/') {
 427			c.w.Write(spaceBytes)
 428		}
 429
 430		c.w.Write(value.Data)
 431		if value.TokenType == css.FunctionToken {
 432			c.writeFunction(value.Args)
 433			c.w.Write(rightParenBytes)
 434		}
 435
 436		if value.TokenType == css.CommaToken || value.TokenType == css.DelimToken && value.Data[0] == '/' || value.TokenType == css.FunctionToken || value.TokenType == css.URLToken {
 437			prevSep = true
 438		} else {
 439			prevSep = false
 440		}
 441	}
 442
 443	if important {
 444		c.w.Write(importantBytes)
 445	}
 446}
 447
 448func (c *cssMinifier) minifyTokens(prop Hash, fun Hash, values []Token) []Token {
 449	if 100 < c.tokensLevel+1 {
 450		return values
 451	}
 452	c.tokensLevel++
 453
 454	for i, value := range values {
 455		tt := value.TokenType
 456		switch tt {
 457		case css.NumberToken:
 458			if prop == Z_Index || prop == Counter_Increment || prop == Counter_Reset || prop == Orphans || prop == Widows {
 459				break // integers
 460			}
 461			if c.o.KeepCSS2 {
 462				values[i].Data = minify.Decimal(values[i].Data, c.o.Precision) // don't use exponents
 463			} else {
 464				values[i].Data = minify.Number(values[i].Data, c.o.Precision)
 465			}
 466		case css.PercentageToken:
 467			n := len(values[i].Data) - 1
 468			if c.o.KeepCSS2 {
 469				values[i].Data = minify.Decimal(values[i].Data[:n], c.o.Precision) // don't use exponents
 470			} else {
 471				values[i].Data = minify.Number(values[i].Data[:n], c.o.Precision)
 472			}
 473			values[i].Data = append(values[i].Data, '%')
 474		case css.DimensionToken:
 475			var dim []byte
 476			values[i], dim = c.minifyDimension(values[i])
 477			if 1 < len(values[i].Data) && values[i].Data[0] == '0' && optionalZeroDimension[string(dim)] && prop != Flex && fun == 0 {
 478				// cut dimension for zero value, TODO: don't hardcode check for Flex and remove the dimension in minifyDimension
 479				values[i].Data = values[i].Data[:1]
 480			}
 481		case css.StringToken:
 482			values[i].Data = removeMarkupNewlines(values[i].Data)
 483		case css.URLToken:
 484			if 10 < len(values[i].Data) {
 485				uri := parse.TrimWhitespace(values[i].Data[4 : len(values[i].Data)-1])
 486				delim := byte('"')
 487				if 1 < len(uri) && (uri[0] == '\'' || uri[0] == '"') {
 488					delim = uri[0]
 489					uri = removeMarkupNewlines(uri)
 490					uri = uri[1 : len(uri)-1]
 491				}
 492				if 4 < len(uri) && parse.EqualFold(uri[:5], dataSchemeBytes) {
 493					uri = minify.DataURI(c.m, uri)
 494				}
 495				if css.IsURLUnquoted(uri) {
 496					values[i].Data = append(append(urlBytes, uri...), ')')
 497				} else {
 498					values[i].Data = append(append(append(urlBytes, delim), uri...), delim, ')')
 499				}
 500			}
 501		case css.FunctionToken:
 502			values[i].Args = c.minifyTokens(prop, values[i].Fun, values[i].Args)
 503
 504			fun := values[i].Fun
 505			args := values[i].Args
 506			if fun == Rgb || fun == Rgba || fun == Hsl || fun == Hsla {
 507				valid := true
 508				vals := []float64{}
 509				for i, arg := range args {
 510					numeric := arg.TokenType == css.NumberToken || arg.TokenType == css.PercentageToken
 511					separator := arg.TokenType == css.CommaToken || i != 5 && arg.TokenType == css.WhitespaceToken || i == 5 && arg.TokenType == css.DelimToken && arg.Data[0] == '/'
 512					if i%2 == 0 && !numeric || i%2 == 1 && !separator {
 513						valid = false
 514						break
 515					} else if numeric {
 516						var d float64
 517						if arg.TokenType == css.PercentageToken {
 518							var err error
 519							d, err = strconv.ParseFloat(string(arg.Data[:len(arg.Data)-1]), 32) // can overflow
 520							if err != nil {
 521								valid = false
 522								break
 523							}
 524							d /= 100.0
 525							if d < minify.Epsilon {
 526								d = 0.0
 527							} else if 1.0-minify.Epsilon < d {
 528								d = 1.0
 529							}
 530						} else {
 531							var err error
 532							d, err = strconv.ParseFloat(string(arg.Data), 32) // can overflow
 533							if err != nil {
 534								valid = false
 535								break
 536							}
 537						}
 538						vals = append(vals, d)
 539					}
 540				}
 541				if !valid {
 542					break
 543				}
 544
 545				a := 1.0
 546				if len(vals) == 4 {
 547					if vals[0] < minify.Epsilon && vals[1] < minify.Epsilon && vals[2] < minify.Epsilon && vals[3] < minify.Epsilon {
 548						values[i] = Token{css.IdentToken, transparentBytes, nil, 0, Transparent}
 549						break
 550					} else if 1.0-minify.Epsilon < vals[3] {
 551						vals = vals[:3]
 552						values[i].Args = values[i].Args[:len(values[i].Args)-2]
 553						if fun == Rgba || fun == Hsla {
 554							values[i].Data = values[i].Data[:len(values[i].Data)-1]
 555							values[i].Data[len(values[i].Data)-1] = '('
 556						}
 557					} else {
 558						a = vals[3]
 559					}
 560				}
 561
 562				if a == 1.0 && (len(vals) == 3 || len(vals) == 4) { // only minify color if fully opaque
 563					if fun == Rgb || fun == Rgba {
 564						for j := 0; j < 3; j++ {
 565							if args[j*2].TokenType == css.NumberToken {
 566								vals[j] /= 255.0
 567								if vals[j] < minify.Epsilon {
 568									vals[j] = 0.0
 569								} else if 1.0-minify.Epsilon < vals[j] {
 570									vals[j] = 1.0
 571								}
 572							}
 573						}
 574						values[i] = rgbToToken(vals[0], vals[1], vals[2])
 575						break
 576					} else if fun == Hsl || fun == Hsla && args[0].TokenType == css.NumberToken && args[2].TokenType == css.PercentageToken && args[4].TokenType == css.PercentageToken {
 577						vals[0] /= 360.0
 578						_, vals[0] = math.Modf(vals[0])
 579						if vals[0] < 0.0 {
 580							vals[0] = 1.0 + vals[0]
 581						}
 582						r, g, b := css.HSL2RGB(vals[0], vals[1], vals[2])
 583						values[i] = rgbToToken(r, g, b)
 584						break
 585					}
 586				} else if len(vals) == 4 {
 587					args[6] = minifyNumberPercentage(args[6])
 588				}
 589
 590				if 3 <= len(vals) && (fun == Rgb || fun == Rgba) {
 591					// 0%, 20%, 40%, 60%, 80% and 100% can be represented exactly as, 51, 102, 153, 204, and 255 respectively
 592					removePercentage := true
 593					for j := 0; j < 3; j++ {
 594						if args[j*2].TokenType != css.PercentageToken || 2.0*minify.Epsilon <= math.Mod(vals[j]+minify.Epsilon, 0.2) {
 595							removePercentage = false
 596							break
 597						}
 598					}
 599					if removePercentage {
 600						for j := 0; j < 3; j++ {
 601							args[j*2].TokenType = css.NumberToken
 602							if vals[j] < minify.Epsilon {
 603								args[j*2].Data = zeroBytes
 604							} else if math.Abs(vals[j]-0.2) < minify.Epsilon {
 605								args[j*2].Data = []byte("51")
 606							} else if math.Abs(vals[j]-0.4) < minify.Epsilon {
 607								args[j*2].Data = []byte("102")
 608							} else if math.Abs(vals[j]-0.6) < minify.Epsilon {
 609								args[j*2].Data = []byte("153")
 610							} else if math.Abs(vals[j]-0.8) < minify.Epsilon {
 611								args[j*2].Data = []byte("204")
 612							} else if math.Abs(vals[j]-1.0) < minify.Epsilon {
 613								args[j*2].Data = []byte("255")
 614							}
 615						}
 616					}
 617				}
 618			}
 619		}
 620	}
 621	c.tokensLevel--
 622	return values
 623}
 624
 625func (c *cssMinifier) minifyProperty(prop Hash, values []Token) []Token {
 626	// limit maximum to prevent slow recursions (e.g. for background's append)
 627	if 100 < len(values) {
 628		return values
 629	}
 630
 631	switch prop {
 632	case Font:
 633		if len(values) > 1 { // must contain atleast font-size and font-family
 634			// the font-families are separated by commas and are at the end of font
 635			// get index for last token before font family names
 636			i := len(values) - 1
 637			for j, value := range values[2:] {
 638				if value.TokenType == css.CommaToken {
 639					i = 2 + j - 1 // identifier before first comma is a font-family
 640					break
 641				}
 642			}
 643			i--
 644
 645			// advance i while still at font-families when they contain spaces but no quotes
 646			for ; i > 0; i-- { // i cannot be 0, font-family must be prepended by font-size
 647				if values[i-1].TokenType == css.DelimToken && values[i-1].Data[0] == '/' {
 648					break
 649				} else if values[i].TokenType != css.IdentToken && values[i].TokenType != css.StringToken {
 650					break
 651				} else if h := values[i].Ident; h == Xx_Small || h == X_Small || h == Small || h == Medium || h == Large || h == X_Large || h == Xx_Large || h == Smaller || h == Larger || h == Inherit || h == Initial || h == Unset {
 652					// inherit, initial and unset are followed by an IdentToken/StringToken, so must be for font-size
 653					break
 654				}
 655			}
 656
 657			// font-family minified in place
 658			values = append(values[:i+1], c.minifyProperty(Font_Family, values[i+1:])...)
 659
 660			// fix for IE9, IE10, IE11: font name starting with `-` is not recognized
 661			if values[i+1].Data[0] == '-' {
 662				v := make([]byte, len(values[i+1].Data)+2)
 663				v[0] = '\''
 664				copy(v[1:], values[i+1].Data)
 665				v[len(v)-1] = '\''
 666				values[i+1].Data = v
 667			}
 668
 669			if i > 0 {
 670				// line-height
 671				if i > 1 && values[i-1].TokenType == css.DelimToken && values[i-1].Data[0] == '/' {
 672					if values[i].Ident == Normal {
 673						values = append(values[:i-1], values[i+1:]...)
 674					}
 675					i -= 2
 676				}
 677
 678				// font-size
 679				i--
 680
 681				for ; i > -1; i-- {
 682					if values[i].Ident == Normal {
 683						values = append(values[:i], values[i+1:]...)
 684					} else if values[i].Ident == Bold {
 685						values[i].TokenType = css.NumberToken
 686						values[i].Data = n700Bytes
 687					} else if values[i].TokenType == css.NumberToken && bytes.Equal(values[i].Data, n400Bytes) {
 688						values = append(values[:i], values[i+1:]...)
 689					}
 690				}
 691			}
 692		}
 693	case Font_Family:
 694		for i, value := range values {
 695			if value.TokenType == css.StringToken && 2 < len(value.Data) {
 696				unquote := true
 697				parse.ToLower(value.Data)
 698				s := value.Data[1 : len(value.Data)-1]
 699				if 0 < len(s) {
 700					for _, split := range bytes.Split(s, spaceBytes) {
 701						// if len is zero, it contains two consecutive spaces
 702						if len(split) == 0 || !css.IsIdent(split) {
 703							unquote = false
 704							break
 705						}
 706					}
 707				}
 708				if unquote {
 709					values[i].Data = s
 710				}
 711			}
 712		}
 713	case Font_Weight:
 714		if values[0].Ident == Normal {
 715			values[0].TokenType = css.NumberToken
 716			values[0].Data = n400Bytes
 717		} else if values[0].Ident == Bold {
 718			values[0].TokenType = css.NumberToken
 719			values[0].Data = n700Bytes
 720		}
 721	case Url:
 722		for i := 0; i < len(values); i++ {
 723			if values[i].TokenType == css.FunctionToken && len(values[i].Args) == 1 {
 724				fun := values[i].Fun
 725				data := values[i].Args[0].Data
 726				if fun == Local && (data[0] == '\'' || data[0] == '"') {
 727					if css.IsURLUnquoted(data[1 : len(data)-1]) {
 728						data = data[1 : len(data)-1]
 729					}
 730					values[i].Args[0].Data = data
 731				}
 732			}
 733		}
 734	case Margin, Padding, Border_Width:
 735		switch len(values) {
 736		case 2:
 737			if values[0].Equal(values[1]) {
 738				values = values[:1]
 739			}
 740		case 3:
 741			if values[0].Equal(values[1]) && values[0].Equal(values[2]) {
 742				values = values[:1]
 743			} else if values[0].Equal(values[2]) {
 744				values = values[:2]
 745			}
 746		case 4:
 747			if values[0].Equal(values[1]) && values[0].Equal(values[2]) && values[0].Equal(values[3]) {
 748				values = values[:1]
 749			} else if values[0].Equal(values[2]) && values[1].Equal(values[3]) {
 750				values = values[:2]
 751			} else if values[1].Equal(values[3]) {
 752				values = values[:3]
 753			}
 754		}
 755	case Border, Border_Bottom, Border_Left, Border_Right, Border_Top:
 756		for i := 0; i < len(values); i++ {
 757			if values[i].Ident == None || values[i].Ident == Currentcolor || values[i].Ident == Medium {
 758				values = append(values[:i], values[i+1:]...)
 759				i--
 760			} else {
 761				values[i] = minifyColor(values[i])
 762			}
 763		}
 764		if len(values) == 0 {
 765			values = []Token{{css.IdentToken, noneBytes, nil, 0, None}}
 766		}
 767	case Outline:
 768		for i := 0; i < len(values); i++ {
 769			if values[i].Ident == Invert || values[i].Ident == None || values[i].Ident == Medium {
 770				values = append(values[:i], values[i+1:]...)
 771				i--
 772			} else {
 773				values[i] = minifyColor(values[i])
 774			}
 775		}
 776		if len(values) == 0 {
 777			values = []Token{{css.IdentToken, noneBytes, nil, 0, None}}
 778		}
 779	case Background:
 780		start := 0
 781		for end := 0; end <= len(values); end++ { // loop over comma-separated lists
 782			if end != len(values) && values[end].TokenType != css.CommaToken {
 783				continue
 784			} else if start == end {
 785				start++
 786				continue
 787			}
 788
 789			// minify background-size and lowercase all identifiers
 790			for i := start; i < end; i++ {
 791				if values[i].TokenType == css.DelimToken && values[i].Data[0] == '/' {
 792					// background-size consists of either [<length-percentage> | auto | cover | contain] or [<length-percentage> | auto]{2}
 793					// we can only minify the latter
 794					if i+1 < end && (values[i+1].TokenType == css.NumberToken || values[i+1].IsLengthPercentage() || values[i+1].Ident == Auto) {
 795						if i+2 < end && (values[i+2].TokenType == css.NumberToken || values[i+2].IsLengthPercentage() || values[i+2].Ident == Auto) {
 796							sizeValues := c.minifyProperty(Background_Size, values[i+1:i+3])
 797							if len(sizeValues) == 1 && sizeValues[0].Ident == Auto {
 798								// remove background-size if it is '/ auto' after minifying the property
 799								values = append(values[:i], values[i+3:]...)
 800								end -= 3
 801								i--
 802							} else {
 803								values = append(values[:i+1], append(sizeValues, values[i+3:]...)...)
 804								end -= 2 - len(sizeValues)
 805								i += len(sizeValues) - 1
 806							}
 807						} else if values[i+1].Ident == Auto {
 808							// remove background-size if it is '/ auto'
 809							values = append(values[:i], values[i+2:]...)
 810							end -= 2
 811							i--
 812						}
 813					}
 814				}
 815			}
 816
 817			// minify all other values
 818			iPaddingBox := -1 // position of background-origin that is padding-box
 819			for i := start; i < end; i++ {
 820				h := values[i].Ident
 821				values[i] = minifyColor(values[i])
 822				if values[i].TokenType == css.IdentToken {
 823					if i+1 < end && values[i+1].TokenType == css.IdentToken && (h == Space || h == Round || h == Repeat || h == No_Repeat) {
 824						if h2 := values[i+1].Ident; h2 == Space || h2 == Round || h2 == Repeat || h2 == No_Repeat {
 825							repeatValues := c.minifyProperty(Background_Repeat, values[i:i+2])
 826							if len(repeatValues) == 1 && repeatValues[0].Ident == Repeat {
 827								values = append(values[:i], values[i+2:]...)
 828								end -= 2
 829								i--
 830							} else {
 831								values = append(values[:i], append(repeatValues, values[i+2:]...)...)
 832								end -= 2 - len(repeatValues)
 833								i += len(repeatValues) - 1
 834							}
 835							continue
 836						}
 837					} else if h == None || h == Scroll || h == Transparent {
 838						values = append(values[:i], values[i+1:]...)
 839						end--
 840						i--
 841						continue
 842					} else if h == Border_Box || h == Padding_Box {
 843						if iPaddingBox == -1 && h == Padding_Box { // background-origin
 844							iPaddingBox = i
 845						} else if iPaddingBox != -1 && h == Border_Box { // background-clip
 846							values = append(values[:i], values[i+1:]...)
 847							values = append(values[:iPaddingBox], values[iPaddingBox+1:]...)
 848							end -= 2
 849							i -= 2
 850						}
 851						continue
 852					}
 853				} else if values[i].TokenType == css.HashToken && bytes.Equal(values[i].Data, blackBytes) {
 854					values = append(values[:i], values[i+1:]...)
 855					end--
 856					i--
 857					continue
 858				}
 859
 860				// further minify background-position and background-size combination
 861				if values[i].TokenType == css.NumberToken || values[i].IsLengthPercentage() || h == Left || h == Right || h == Top || h == Bottom || h == Center {
 862					j := i + 1
 863					for ; j < len(values); j++ {
 864						if h := values[j].Ident; h == Left || h == Right || h == Top || h == Bottom || h == Center {
 865							continue
 866						} else if values[j].TokenType == css.NumberToken || values[j].IsLengthPercentage() {
 867							continue
 868						}
 869						break
 870					}
 871
 872					positionValues := c.minifyProperty(Background_Position, values[i:j])
 873					hasSize := j < len(values) && values[j].TokenType == css.DelimToken && values[j].Data[0] == '/'
 874					if !hasSize && len(positionValues) == 2 && positionValues[0].IsZero() && positionValues[1].IsZero() {
 875						if end-start == 2 {
 876							values[i] = Token{css.NumberToken, zeroBytes, nil, 0, 0}
 877							values[i+1] = Token{css.NumberToken, zeroBytes, nil, 0, 0}
 878							i++
 879						} else {
 880							values = append(values[:i], values[j:]...)
 881							end -= j - i
 882							i--
 883						}
 884					} else {
 885						if len(positionValues) == j-i {
 886							for k, positionValue := range positionValues {
 887								values[i+k] = positionValue
 888							}
 889						} else {
 890							values = append(values[:i], append(positionValues, values[j:]...)...)
 891							end -= j - i - len(positionValues)
 892						}
 893						i += len(positionValues) - 1
 894					}
 895				}
 896			}
 897
 898			if end-start == 0 {
 899				values = append(values[:start], append([]Token{{css.NumberToken, zeroBytes, nil, 0, 0}, {css.NumberToken, zeroBytes, nil, 0, 0}}, values[end:]...)...)
 900				end += 2
 901			}
 902			start = end + 1
 903		}
 904	case Background_Size:
 905		start := 0
 906		for end := 0; end <= len(values); end++ { // loop over comma-separated lists
 907			if end != len(values) && values[end].TokenType != css.CommaToken {
 908				continue
 909			} else if start == end {
 910				start++
 911				continue
 912			}
 913
 914			if end-start == 2 && values[start+1].Ident == Auto {
 915				values = append(values[:start+1], values[start+2:]...)
 916				end--
 917			}
 918			start = end + 1
 919		}
 920	case Background_Repeat:
 921		start := 0
 922		for end := 0; end <= len(values); end++ { // loop over comma-separated lists
 923			if end != len(values) && values[end].TokenType != css.CommaToken {
 924				continue
 925			} else if start == end {
 926				start++
 927				continue
 928			}
 929
 930			if end-start == 2 && values[start].TokenType == css.IdentToken && values[start+1].TokenType == css.IdentToken {
 931				if values[start].Ident == values[start+1].Ident {
 932					values = append(values[:start+1], values[start+2:]...)
 933					end--
 934				} else if values[start].Ident == Repeat && values[start+1].Ident == No_Repeat {
 935					values[start].Data = repeatXBytes
 936					values[start].Ident = Repeat_X
 937					values = append(values[:start+1], values[start+2:]...)
 938					end--
 939				} else if values[start].Ident == No_Repeat && values[start+1].Ident == Repeat {
 940					values[start].Data = repeatYBytes
 941					values[start].Ident = Repeat_Y
 942					values = append(values[:start+1], values[start+2:]...)
 943					end--
 944				}
 945			}
 946			start = end + 1
 947		}
 948	case Background_Position:
 949		start := 0
 950		for end := 0; end <= len(values); end++ { // loop over comma-separated lists
 951			if end != len(values) && values[end].TokenType != css.CommaToken {
 952				continue
 953			} else if start == end {
 954				start++
 955				continue
 956			}
 957
 958			if end-start == 3 || end-start == 4 {
 959				// remove zero offsets
 960				for _, i := range []int{end - start - 1, start + 1} {
 961					if 2 < end-start && values[i].IsZero() {
 962						values = append(values[:i], values[i+1:]...)
 963						end--
 964					}
 965				}
 966
 967				j := start + 1 // position of second set of horizontal/vertical values
 968				if 2 < end-start && values[start+2].TokenType == css.IdentToken {
 969					j = start + 2
 970				}
 971
 972				b := make([]byte, 0, 4)
 973				offsets := make([]Token, 2)
 974				for _, i := range []int{j, start} {
 975					if i+1 < end && i+1 != j {
 976						if values[i+1].TokenType == css.PercentageToken {
 977							// change right or bottom with percentage offset to left or top respectively
 978							if values[i].Ident == Right || values[i].Ident == Bottom {
 979								n, _ := strconvParse.ParseInt(values[i+1].Data[:len(values[i+1].Data)-1])
 980								b = strconv.AppendInt(b[:0], 100-n, 10)
 981								b = append(b, '%')
 982								values[i+1].Data = b
 983								if values[i].Ident == Right {
 984									values[i].Data = leftBytes
 985									values[i].Ident = Left
 986								} else {
 987									values[i].Data = topBytes
 988									values[i].Ident = Top
 989								}
 990							}
 991						}
 992						if values[i].Ident == Left {
 993							offsets[0] = values[i+1]
 994						} else if values[i].Ident == Top {
 995							offsets[1] = values[i+1]
 996						}
 997					} else if values[i].Ident == Left {
 998						offsets[0] = Token{css.NumberToken, zeroBytes, nil, 0, 0}
 999					} else if values[i].Ident == Top {
1000						offsets[1] = Token{css.NumberToken, zeroBytes, nil, 0, 0}
1001					} else if values[i].Ident == Right {
1002						offsets[0] = Token{css.PercentageToken, n100pBytes, nil, 0, 0}
1003						values[i].Ident = Left
1004					} else if values[i].Ident == Bottom {
1005						offsets[1] = Token{css.PercentageToken, n100pBytes, nil, 0, 0}
1006						values[i].Ident = Top
1007					}
1008				}
1009
1010				if values[start].Ident == Center || values[j].Ident == Center {
1011					if values[start].Ident == Left || values[j].Ident == Left {
1012						offsets = offsets[:1]
1013					} else if values[start].Ident == Top || values[j].Ident == Top {
1014						offsets[0] = Token{css.NumberToken, n50pBytes, nil, 0, 0}
1015					}
1016				}
1017
1018				if offsets[0].Data != nil && (len(offsets) == 1 || offsets[1].Data != nil) {
1019					values = append(append(values[:start], offsets...), values[end:]...)
1020					end -= end - start - len(offsets)
1021				}
1022			}
1023			// removing zero offsets in the previous loop might make it eligible for the next loop
1024			if end-start == 1 || end-start == 2 {
1025				if end-start == 1 && (values[start].Ident == Top || values[start].Ident == Bottom) {
1026					// we can't make this smaller, and converting to a number will break it
1027					// (https://github.com/tdewolff/minify/issues/221#issuecomment-415419918)
1028					break
1029				}
1030
1031				if end-start == 2 && (values[start].Ident == Top || values[start].Ident == Bottom || values[start+1].Ident == Left || values[start+1].Ident == Right) {
1032					// if it's a vertical position keyword, swap it with the next element
1033					// since otherwise converted number positions won't be valid anymore
1034					// (https://github.com/tdewolff/minify/issues/221#issue-353067229)
1035					values[start], values[start+1] = values[start+1], values[start]
1036				}
1037
1038				// transform keywords to lengths|percentages
1039				for i := start; i < end; i++ {
1040					if values[i].TokenType == css.IdentToken {
1041						if values[i].Ident == Left || values[i].Ident == Top {
1042							values[i].TokenType = css.NumberToken
1043							values[i].Data = zeroBytes
1044							values[i].Ident = 0
1045						} else if values[i].Ident == Right || values[i].Ident == Bottom {
1046							values[i].TokenType = css.PercentageToken
1047							values[i].Data = n100pBytes
1048							values[i].Ident = 0
1049						} else if values[i].Ident == Center {
1050							if i == start {
1051								values[i].TokenType = css.PercentageToken
1052								values[i].Data = n50pBytes
1053								values[i].Ident = 0
1054							} else {
1055								values = append(values[:start+1], values[start+2:]...)
1056								end--
1057							}
1058						}
1059					} else if i == start+1 && values[i].TokenType == css.PercentageToken && bytes.Equal(values[i].Data, n50pBytes) {
1060						values = append(values[:start+1], values[start+2:]...)
1061						end--
1062					} else if values[i].TokenType == css.PercentageToken && values[i].Data[0] == '0' {
1063						values[i].TokenType = css.NumberToken
1064						values[i].Data = zeroBytes
1065						values[i].Ident = 0
1066					}
1067				}
1068			}
1069			start = end + 1
1070		}
1071	case Box_Shadow:
1072		start := 0
1073		for end := 0; end <= len(values); end++ { // loop over comma-separated lists
1074			if end != len(values) && values[end].TokenType != css.CommaToken {
1075				continue
1076			} else if start == end {
1077				start++
1078				continue
1079			}
1080
1081			if end-start == 1 && values[start].Ident == Initial {
1082				values[start].Ident = None
1083				values[start].Data = noneBytes
1084			} else {
1085				numbers := []int{}
1086				for i := start; i < end; i++ {
1087					if values[i].IsLength() {
1088						numbers = append(numbers, i)
1089					}
1090				}
1091				if len(numbers) == 4 && values[numbers[3]].IsZero() {
1092					values = append(values[:numbers[3]], values[numbers[3]+1:]...)
1093					numbers = numbers[:3]
1094					end--
1095				}
1096				if len(numbers) == 3 && values[numbers[2]].IsZero() {
1097					values = append(values[:numbers[2]], values[numbers[2]+1:]...)
1098					end--
1099				}
1100			}
1101			start = end + 1
1102		}
1103	case Ms_Filter:
1104		alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=")
1105		if values[0].TokenType == css.StringToken && 2 < len(values[0].Data) && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) {
1106			values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...)
1107		}
1108	case Color:
1109		values[0] = minifyColor(values[0])
1110	case Background_Color:
1111		values[0] = minifyColor(values[0])
1112		if !c.o.KeepCSS2 {
1113			if values[0].Ident == Transparent {
1114				values[0].Data = initialBytes
1115				values[0].Ident = Initial
1116			}
1117		}
1118	case Border_Color:
1119		sameValues := true
1120		for i := range values {
1121			if values[i].Ident == Currentcolor {
1122				values[i].Data = initialBytes
1123				values[i].Ident = Initial
1124			} else {
1125				values[i] = minifyColor(values[i])
1126			}
1127			if 0 < i && sameValues && !bytes.Equal(values[0].Data, values[i].Data) {
1128				sameValues = false
1129			}
1130		}
1131		if sameValues {
1132			values = values[:1]
1133		}
1134	case Border_Left_Color, Border_Right_Color, Border_Top_Color, Border_Bottom_Color, Text_Decoration_Color, Text_Emphasis_Color:
1135		if values[0].Ident == Currentcolor {
1136			values[0].Data = initialBytes
1137			values[0].Ident = Initial
1138		} else {
1139			values[0] = minifyColor(values[0])
1140		}
1141	case Caret_Color, Outline_Color, Fill, Stroke:
1142		values[0] = minifyColor(values[0])
1143	case Column_Rule:
1144		for i := 0; i < len(values); i++ {
1145			if values[i].Ident == Currentcolor || values[i].Ident == None || values[i].Ident == Medium {
1146				values = append(values[:i], values[i+1:]...)
1147				i--
1148			} else {
1149				values[i] = minifyColor(values[i])
1150			}
1151		}
1152		if len(values) == 0 {
1153			values = []Token{{css.IdentToken, noneBytes, nil, 0, None}}
1154		}
1155	case Text_Shadow:
1156		// TODO: minify better (can be comma separated list)
1157		for i := 0; i < len(values); i++ {
1158			values[i] = minifyColor(values[i])
1159		}
1160	case Text_Decoration:
1161		for i := 0; i < len(values); i++ {
1162			if values[i].Ident == Currentcolor || values[i].Ident == None || values[i].Ident == Solid {
1163				values = append(values[:i], values[i+1:]...)
1164				i--
1165			} else {
1166				values[i] = minifyColor(values[i])
1167			}
1168		}
1169		if len(values) == 0 {
1170			values = []Token{{css.IdentToken, noneBytes, nil, 0, None}}
1171		}
1172	case Text_Emphasis:
1173		for i := 0; i < len(values); i++ {
1174			if values[i].Ident == Currentcolor || values[i].Ident == None {
1175				values = append(values[:i], values[i+1:]...)
1176				i--
1177			} else {
1178				values[i] = minifyColor(values[i])
1179			}
1180		}
1181		if len(values) == 0 {
1182			values = []Token{{css.IdentToken, noneBytes, nil, 0, None}}
1183		}
1184	case Flex:
1185		if len(values) == 2 && values[0].TokenType == css.NumberToken {
1186			if values[1].TokenType != css.NumberToken && values[1].IsZero() {
1187				values = values[:1] // remove <flex-basis> if it is zero
1188			}
1189		} else if len(values) == 3 && values[0].TokenType == css.NumberToken && values[1].TokenType == css.NumberToken {
1190			if len(values[0].Data) == 1 && len(values[1].Data) == 1 {
1191				if values[2].Ident == Auto {
1192					if values[0].Data[0] == '0' && values[1].Data[0] == '1' {
1193						values = values[:1]
1194						values[0].TokenType = css.IdentToken
1195						values[0].Data = initialBytes
1196						values[0].Ident = Initial
1197					} else if values[0].Data[0] == '1' && values[1].Data[0] == '1' {
1198						values = values[:1]
1199						values[0].TokenType = css.IdentToken
1200						values[0].Data = autoBytes
1201						values[0].Ident = Auto
1202					} else if values[0].Data[0] == '0' && values[1].Data[0] == '0' {
1203						values = values[:1]
1204						values[0].TokenType = css.IdentToken
1205						values[0].Data = noneBytes
1206						values[0].Ident = None
1207					}
1208				} else if values[1].Data[0] == '1' && values[2].IsZero() {
1209					values = values[:1] // remove <flex-shrink> and <flex-basis> if they are 1 and 0 respectively
1210				} else if values[2].IsZero() {
1211					values = values[:2] // remove auto to write 2-value syntax of <flex-grow> <flex-shrink>
1212				} else {
1213					values[2] = minifyLengthPercentage(values[2])
1214				}
1215			}
1216		}
1217	case Flex_Basis:
1218		if values[0].Ident == Initial {
1219			values[0].Data = autoBytes
1220			values[0].Ident = Auto
1221		} else {
1222			values[0] = minifyLengthPercentage(values[0])
1223		}
1224	case Order, Flex_Grow:
1225		if values[0].Ident == Initial {
1226			values[0].TokenType = css.NumberToken
1227			values[0].Data = zeroBytes
1228			values[0].Ident = 0
1229		}
1230	case Flex_Shrink:
1231		if values[0].Ident == Initial {
1232			values[0].TokenType = css.NumberToken
1233			values[0].Data = oneBytes
1234			values[0].Ident = 0
1235		}
1236	case Unicode_Range:
1237		ranges := [][2]int{}
1238		for _, value := range values {
1239			if value.TokenType == css.CommaToken {
1240				continue
1241			} else if value.TokenType != css.UnicodeRangeToken {
1242				return values
1243			}
1244
1245			i := 2
1246			iWildcard := 0
1247			start := 0
1248			for i < len(value.Data) && value.Data[i] != '-' {
1249				start *= 16
1250				if '0' <= value.Data[i] && value.Data[i] <= '9' {
1251					start += int(value.Data[i] - '0')
1252				} else if 'a' <= value.Data[i]|32 && value.Data[i]|32 <= 'f' {
1253					start += int(value.Data[i]|32-'a') + 10
1254				} else if iWildcard == 0 && value.Data[i] == '?' {
1255					iWildcard = i
1256				}
1257				i++
1258			}
1259			end := start
1260			if iWildcard != 0 {
1261				end = start + int(math.Pow(16.0, float64(len(value.Data)-iWildcard))) - 1
1262			} else if i < len(value.Data) && value.Data[i] == '-' {
1263				i++
1264				end = 0
1265				for i < len(value.Data) {
1266					end *= 16
1267					if '0' <= value.Data[i] && value.Data[i] <= '9' {
1268						end += int(value.Data[i] - '0')
1269					} else if 'a' <= value.Data[i]|32 && value.Data[i]|32 <= 'f' {
1270						end += int(value.Data[i]|32-'a') + 10
1271					}
1272					i++
1273				}
1274				if end <= start {
1275					end = start
1276				}
1277			}
1278			ranges = append(ranges, [2]int{start, end})
1279		}
1280
1281		// sort and remove overlapping ranges
1282		sort.Slice(ranges, func(i, j int) bool { return ranges[i][0] < ranges[j][0] })
1283		for i := 0; i < len(ranges)-1; i++ {
1284			if ranges[i+1][1] <= ranges[i][1] {
1285				// next range is fully contained in the current range
1286				ranges = append(ranges[:i+1], ranges[i+2:]...)
1287			} else if ranges[i+1][0] <= ranges[i][1]+1 {
1288				// next range is partially covering the current range
1289				ranges[i][1] = ranges[i+1][1]
1290				ranges = append(ranges[:i+1], ranges[i+2:]...)
1291			}
1292		}
1293
1294		values = values[:0]
1295		for i, ran := range ranges {
1296			if i != 0 {
1297				values = append(values, Token{css.CommaToken, commaBytes, nil, 0, None})
1298			}
1299			if ran[0] == ran[1] {
1300				urange := []byte(fmt.Sprintf("U+%X", ran[0]))
1301				values = append(values, Token{css.UnicodeRangeToken, urange, nil, 0, None})
1302			} else if ran[0] == 0 && ran[1] == 0x10FFFF {
1303				values = append(values, Token{css.IdentToken, initialBytes, nil, 0, None})
1304			} else {
1305				k := 0
1306				for k < 6 && (ran[0]>>(k*4))&0xF == 0 && (ran[1]>>(k*4))&0xF == 0xF {
1307					k++
1308				}
1309				wildcards := k
1310				for k < 6 {
1311					if (ran[0]>>(k*4))&0xF != (ran[1]>>(k*4))&0xF {
1312						wildcards = 0
1313						break
1314					}
1315					k++
1316				}
1317				var urange []byte
1318				if wildcards != 0 {
1319					if ran[0]>>(wildcards*4) == 0 {
1320						urange = []byte(fmt.Sprintf("U+%s", strings.Repeat("?", wildcards)))
1321					} else {
1322						urange = []byte(fmt.Sprintf("U+%X%s", ran[0]>>(wildcards*4), strings.Repeat("?", wildcards)))
1323					}
1324				} else {
1325					urange = []byte(fmt.Sprintf("U+%X-%X", ran[0], ran[1]))
1326				}
1327				values = append(values, Token{css.UnicodeRangeToken, urange, nil, 0, None})
1328			}
1329		}
1330	}
1331	return values
1332}
1333
1334func minifyColor(value Token) Token {
1335	data := value.Data
1336	if value.TokenType == css.IdentToken {
1337		if hexValue, ok := ShortenColorName[value.Ident]; ok {
1338			value.TokenType = css.HashToken
1339			value.Data = hexValue
1340		}
1341	} else if value.TokenType == css.HashToken {
1342		parse.ToLower(data[1:])
1343		if len(data) == 9 && data[7] == data[8] {
1344			if data[7] == 'f' {
1345				data = data[:7]
1346			} else if data[7] == '0' {
1347				data = blackBytes
1348			}
1349		}
1350		if ident, ok := ShortenColorHex[string(data)]; ok {
1351			value.TokenType = css.IdentToken
1352			data = ident
1353		} else if len(data) == 7 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] {
1354			value.TokenType = css.HashToken
1355			data[2] = data[3]
1356			data[3] = data[5]
1357			data = data[:4]
1358		} else if len(data) == 9 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] && data[7] == data[8] {
1359			// from working draft Color Module Level 4
1360			value.TokenType = css.HashToken
1361			data[2] = data[3]
1362			data[3] = data[5]
1363			data[4] = data[7]
1364			data = data[:5]
1365		}
1366		value.Data = data
1367	}
1368	return value
1369}
1370
1371func minifyNumberPercentage(value Token) Token {
1372	// assumes input already minified
1373	if value.TokenType == css.PercentageToken && len(value.Data) == 3 && value.Data[len(value.Data)-2] == '0' {
1374		value.Data[1] = value.Data[0]
1375		value.Data[0] = '.'
1376		value.Data = value.Data[:2]
1377		value.TokenType = css.NumberToken
1378	} else if value.TokenType == css.NumberToken && 2 < len(value.Data) && value.Data[0] == '.' && value.Data[1] == '0' {
1379		if value.Data[2] == '0' {
1380			value.Data[0] = '.'
1381			copy(value.Data[1:], value.Data[3:])
1382			value.Data[len(value.Data)-2] = '%'
1383			value.Data = value.Data[:len(value.Data)-1]
1384			value.TokenType = css.PercentageToken
1385		} else if len(value.Data) == 3 {
1386			value.Data[0] = value.Data[2]
1387			value.Data[1] = '%'
1388			value.Data = value.Data[:2]
1389			value.TokenType = css.PercentageToken
1390		}
1391	}
1392	return value
1393}
1394
1395func minifyLengthPercentage(value Token) Token {
1396	if value.TokenType != css.NumberToken && value.IsZero() {
1397		value.TokenType = css.NumberToken
1398		value.Data = value.Data[:1] // remove dimension for zero value
1399	}
1400	return value
1401}
1402
1403func (c *cssMinifier) minifyDimension(value Token) (Token, []byte) {
1404	// TODO: add check for zero value
1405	var dim []byte
1406	if value.TokenType == css.DimensionToken {
1407		n := len(value.Data)
1408		for 0 < n {
1409			lower := 'a' <= value.Data[n-1] && value.Data[n-1] <= 'z'
1410			upper := 'A' <= value.Data[n-1] && value.Data[n-1] <= 'Z'
1411			if !lower && !upper {
1412				break
1413			} else if upper {
1414				value.Data[n-1] = value.Data[n-1] + ('a' - 'A')
1415			}
1416			n--
1417		}
1418
1419		num := value.Data[:n]
1420		if c.o.KeepCSS2 {
1421			num = minify.Decimal(num, c.o.Precision) // don't use exponents
1422		} else {
1423			num = minify.Number(num, c.o.Precision)
1424		}
1425		dim = value.Data[n:]
1426		value.Data = append(num, dim...)
1427	}
1428	return value, dim
1429
1430	// TODO: optimize
1431	//if value.TokenType == css.DimensionToken {
1432	//	// TODO: reverse; parse dim not number
1433	//	n := parse.Number(value.Data)
1434	//	num := value.Data[:n]
1435	//	dim = value.Data[n:]
1436	//	parse.ToLower(dim)
1437
1438	//	if c.o.KeepCSS2 {
1439	//		num = minify.Decimal(num, c.o.Precision) // don't use exponents
1440	//	} else {
1441	//		num = minify.Number(num, c.o.Precision)
1442	//	}
1443
1444	//	// change dimension to compress number
1445	//	h := ToHash(dim)
1446	//	if h == Px || h == Pt || h == Pc || h == In || h == Mm || h == Cm || h == Q || h == Deg || h == Grad || h == Rad || h == Turn || h == S || h == Ms || h == Hz || h == Khz || h == Dpi || h == Dpcm || h == Dppx {
1447	//		d, _ := strconv.ParseFloat(string(num), 64) // can never fail
1448	//		var dimensions []Hash
1449	//		var multipliers []float64
1450	//		switch h {
1451	//		case Px:
1452	//			//dimensions = []Hash{In, Cm, Pc, Mm, Pt, Q}
1453	//			//multipliers = []float64{0.010416666666666667, 0.026458333333333333, 0.0625, 0.26458333333333333, 0.75, 1.0583333333333333}
1454	//			dimensions = []Hash{In, Pc, Pt}
1455	//			multipliers = []float64{0.010416666666666667, 0.0625, 0.75}
1456	//		case Pt:
1457	//			//dimensions = []Hash{In, Cm, Pc, Mm, Px, Q}
1458	//			//multipliers = []float64{0.013888888888888889, 0.035277777777777778, 0.083333333333333333, 0.35277777777777778, 1.3333333333333333, 1.4111111111111111}
1459	//			dimensions = []Hash{In, Pc, Px}
1460	//			multipliers = []float64{0.013888888888888889, 0.083333333333333333, 1.3333333333333333}
1461	//		case Pc:
1462	//			//dimensions = []Hash{In, Cm, Mm, Pt, Px, Q}
1463	//			//multipliers = []float64{0.16666666666666667, 0.42333333333333333, 4.2333333333333333, 12.0, 16.0, 16.933333333333333}
1464	//			dimensions = []Hash{In, Pt, Px}
1465	//			multipliers = []float64{0.16666666666666667, 12.0, 16.0}
1466	//		case In:
1467	//			//dimensions = []Hash{Cm, Pc, Mm, Pt, Px, Q}
1468	//			//multipliers = []float64{2.54, 6.0, 25.4, 72.0, 96.0, 101.6}
1469	//			dimensions = []Hash{Pc, Pt, Px}
1470	//			multipliers = []float64{6.0, 72.0, 96.0}
1471	//		case Cm:
1472	//			//dimensions = []Hash{In, Pc, Mm, Pt, Px, Q}
1473	//			//multipliers = []float64{0.39370078740157480, 2.3622047244094488, 10.0, 28.346456692913386, 37.795275590551181, 40.0}
1474	//			dimensions = []Hash{Mm, Q}
1475	//			multipliers = []float64{10.0, 40.0}
1476	//		case Mm:
1477	//			//dimensions = []Hash{In, Cm, Pc, Pt, Px, Q}
1478	//			//multipliers = []float64{0.039370078740157480, 0.1, 0.23622047244094488, 2.8346456692913386, 3.7795275590551181, 4.0}
1479	//			dimensions = []Hash{Cm, Q}
1480	//			multipliers = []float64{0.1, 4.0}
1481	//		case Q:
1482	//			//dimensions = []Hash{In, Cm, Pc, Pt, Px} // Q to mm is never smaller
1483	//			//multipliers = []float64{0.0098425196850393701, 0.025, 0.059055118110236220, 0.70866141732283465, 0.94488188976377953}
1484	//			dimensions = []Hash{Cm} // Q to mm is never smaller
1485	//			multipliers = []float64{0.025}
1486	//		case Deg:
1487	//			//dimensions = []Hash{Turn, Rad, Grad}
1488	//			//multipliers = []float64{0.0027777777777777778, 0.017453292519943296, 1.1111111111111111}
1489	//			dimensions = []Hash{Turn, Grad}
1490	//			multipliers = []float64{0.0027777777777777778, 1.1111111111111111}
1491	//		case Grad:
1492	//			//dimensions = []Hash{Turn, Rad, Deg}
1493	//			//multipliers = []float64{0.0025, 0.015707963267948966, 0.9}
1494	//			dimensions = []Hash{Turn, Deg}
1495	//			multipliers = []float64{0.0025, 0.9}
1496	//		case Turn:
1497	//			//dimensions = []Hash{Rad, Deg, Grad}
1498	//			//multipliers = []float64{6.2831853071795865, 360.0, 400.0}
1499	//			dimensions = []Hash{Deg, Grad}
1500	//			multipliers = []float64{360.0, 400.0}
1501	//		case Rad:
1502	//			//dimensions = []Hash{Turn, Deg, Grad}
1503	//			//multipliers = []float64{0.15915494309189534, 57.295779513082321, 63.661977236758134}
1504	//		case S:
1505	//			dimensions = []Hash{Ms}
1506	//			multipliers = []float64{1000.0}
1507	//		case Ms:
1508	//			dimensions = []Hash{S}
1509	//			multipliers = []float64{0.001}
1510	//		case Hz:
1511	//			dimensions = []Hash{Khz}
1512	//			multipliers = []float64{0.001}
1513	//		case Khz:
1514	//			dimensions = []Hash{Hz}
1515	//			multipliers = []float64{1000.0}
1516	//		case Dpi:
1517	//			dimensions = []Hash{Dppx, Dpcm}
1518	//			multipliers = []float64{0.010416666666666667, 0.39370078740157480}
1519	//		case Dpcm:
1520	//			//dimensions = []Hash{Dppx, Dpi}
1521	//			//multipliers = []float64{0.026458333333333333, 2.54}
1522	//			dimensions = []Hash{Dpi}
1523	//			multipliers = []float64{2.54}
1524	//		case Dppx:
1525	//			//dimensions = []Hash{Dpcm, Dpi}
1526	//			//multipliers = []float64{37.795275590551181, 96.0}
1527	//			dimensions = []Hash{Dpi}
1528	//			multipliers = []float64{96.0}
1529	//		}
1530	//		for i := range dimensions {
1531	//			if dimensions[i] != h { //&& (d < 1.0) == (multipliers[i] > 1.0) {
1532	//				b, _ := strconvParse.AppendFloat([]byte{}, d*multipliers[i], -1)
1533	//				if c.o.KeepCSS2 {
1534	//					b = minify.Decimal(b, c.o.newPrecision) // don't use exponents
1535	//				} else {
1536	//					b = minify.Number(b, c.o.newPrecision)
1537	//				}
1538	//				newDim := []byte(dimensions[i].String())
1539	//				if len(b)+len(newDim) < len(num)+len(dim) {
1540	//					num = b
1541	//					dim = newDim
1542	//				}
1543	//			}
1544	//		}
1545	//	}
1546	//	value.Data = append(num, dim...)
1547	//}
1548	//return value, dim
1549}