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}