4
0

css.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. // Package css minifies CSS3 following the specifications at http://www.w3.org/TR/css-syntax-3/.
  2. package css // import "github.com/tdewolff/minify/css"
  3. import (
  4. "bytes"
  5. "encoding/hex"
  6. "io"
  7. "strconv"
  8. "github.com/tdewolff/minify"
  9. "github.com/tdewolff/parse"
  10. "github.com/tdewolff/parse/css"
  11. )
  12. var (
  13. spaceBytes = []byte(" ")
  14. colonBytes = []byte(":")
  15. semicolonBytes = []byte(";")
  16. commaBytes = []byte(",")
  17. leftBracketBytes = []byte("{")
  18. rightBracketBytes = []byte("}")
  19. zeroBytes = []byte("0")
  20. msfilterBytes = []byte("-ms-filter")
  21. backgroundNoneBytes = []byte("0 0")
  22. )
  23. type cssMinifier struct {
  24. m *minify.M
  25. w io.Writer
  26. p *css.Parser
  27. o *Minifier
  28. }
  29. ////////////////////////////////////////////////////////////////
  30. // DefaultMinifier is the default minifier.
  31. var DefaultMinifier = &Minifier{Decimals: -1}
  32. // Minifier is a CSS minifier.
  33. type Minifier struct {
  34. Decimals int
  35. }
  36. // Minify minifies CSS data, it reads from r and writes to w.
  37. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  38. return DefaultMinifier.Minify(m, w, r, params)
  39. }
  40. // Minify minifies CSS data, it reads from r and writes to w.
  41. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  42. isInline := params != nil && params["inline"] == "1"
  43. c := &cssMinifier{
  44. m: m,
  45. w: w,
  46. p: css.NewParser(r, isInline),
  47. o: o,
  48. }
  49. defer c.p.Restore()
  50. if err := c.minifyGrammar(); err != nil && err != io.EOF {
  51. return err
  52. }
  53. return nil
  54. }
  55. func (c *cssMinifier) minifyGrammar() error {
  56. semicolonQueued := false
  57. for {
  58. gt, _, data := c.p.Next()
  59. if gt == css.ErrorGrammar {
  60. if perr, ok := c.p.Err().(*parse.Error); ok && perr.Message == "unexpected token in declaration" {
  61. if semicolonQueued {
  62. if _, err := c.w.Write(semicolonBytes); err != nil {
  63. return err
  64. }
  65. }
  66. // write out the offending declaration
  67. if _, err := c.w.Write(data); err != nil {
  68. return err
  69. }
  70. for _, val := range c.p.Values() {
  71. if _, err := c.w.Write(val.Data); err != nil {
  72. return err
  73. }
  74. }
  75. semicolonQueued = true
  76. continue
  77. } else {
  78. return c.p.Err()
  79. }
  80. } else if gt == css.EndAtRuleGrammar || gt == css.EndRulesetGrammar {
  81. if _, err := c.w.Write(rightBracketBytes); err != nil {
  82. return err
  83. }
  84. semicolonQueued = false
  85. continue
  86. }
  87. if semicolonQueued {
  88. if _, err := c.w.Write(semicolonBytes); err != nil {
  89. return err
  90. }
  91. semicolonQueued = false
  92. }
  93. if gt == css.AtRuleGrammar {
  94. if _, err := c.w.Write(data); err != nil {
  95. return err
  96. }
  97. for _, val := range c.p.Values() {
  98. if _, err := c.w.Write(val.Data); err != nil {
  99. return err
  100. }
  101. }
  102. semicolonQueued = true
  103. } else if gt == css.BeginAtRuleGrammar {
  104. if _, err := c.w.Write(data); err != nil {
  105. return err
  106. }
  107. for _, val := range c.p.Values() {
  108. if _, err := c.w.Write(val.Data); err != nil {
  109. return err
  110. }
  111. }
  112. if _, err := c.w.Write(leftBracketBytes); err != nil {
  113. return err
  114. }
  115. } else if gt == css.QualifiedRuleGrammar {
  116. if err := c.minifySelectors(data, c.p.Values()); err != nil {
  117. return err
  118. }
  119. if _, err := c.w.Write(commaBytes); err != nil {
  120. return err
  121. }
  122. } else if gt == css.BeginRulesetGrammar {
  123. if err := c.minifySelectors(data, c.p.Values()); err != nil {
  124. return err
  125. }
  126. if _, err := c.w.Write(leftBracketBytes); err != nil {
  127. return err
  128. }
  129. } else if gt == css.DeclarationGrammar {
  130. if _, err := c.w.Write(data); err != nil {
  131. return err
  132. }
  133. if _, err := c.w.Write(colonBytes); err != nil {
  134. return err
  135. }
  136. if err := c.minifyDeclaration(data, c.p.Values()); err != nil {
  137. return err
  138. }
  139. semicolonQueued = true
  140. } else if gt == css.CustomPropertyGrammar {
  141. if _, err := c.w.Write(data); err != nil {
  142. return err
  143. }
  144. if _, err := c.w.Write(colonBytes); err != nil {
  145. return err
  146. }
  147. if _, err := c.w.Write(c.p.Values()[0].Data); err != nil {
  148. return err
  149. }
  150. semicolonQueued = true
  151. } else if gt == css.CommentGrammar {
  152. if len(data) > 5 && data[1] == '*' && data[2] == '!' {
  153. if _, err := c.w.Write(data[:3]); err != nil {
  154. return err
  155. }
  156. comment := parse.TrimWhitespace(parse.ReplaceMultipleWhitespace(data[3 : len(data)-2]))
  157. if _, err := c.w.Write(comment); err != nil {
  158. return err
  159. }
  160. if _, err := c.w.Write(data[len(data)-2:]); err != nil {
  161. return err
  162. }
  163. }
  164. } else if _, err := c.w.Write(data); err != nil {
  165. return err
  166. }
  167. }
  168. }
  169. func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) error {
  170. inAttr := false
  171. isClass := false
  172. for _, val := range c.p.Values() {
  173. if !inAttr {
  174. if val.TokenType == css.IdentToken {
  175. if !isClass {
  176. parse.ToLower(val.Data)
  177. }
  178. isClass = false
  179. } else if val.TokenType == css.DelimToken && val.Data[0] == '.' {
  180. isClass = true
  181. } else if val.TokenType == css.LeftBracketToken {
  182. inAttr = true
  183. }
  184. } else {
  185. if val.TokenType == css.StringToken && len(val.Data) > 2 {
  186. s := val.Data[1 : len(val.Data)-1]
  187. if css.IsIdent([]byte(s)) {
  188. if _, err := c.w.Write(s); err != nil {
  189. return err
  190. }
  191. continue
  192. }
  193. } else if val.TokenType == css.RightBracketToken {
  194. inAttr = false
  195. }
  196. }
  197. if _, err := c.w.Write(val.Data); err != nil {
  198. return err
  199. }
  200. }
  201. return nil
  202. }
  203. func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) error {
  204. if len(values) == 0 {
  205. return nil
  206. }
  207. prop := css.ToHash(property)
  208. inProgid := false
  209. for i, value := range values {
  210. if inProgid {
  211. if value.TokenType == css.FunctionToken {
  212. inProgid = false
  213. }
  214. continue
  215. } else if value.TokenType == css.IdentToken && css.ToHash(value.Data) == css.Progid {
  216. inProgid = true
  217. continue
  218. }
  219. value.TokenType, value.Data = c.shortenToken(prop, value.TokenType, value.Data)
  220. if prop == css.Font || prop == css.Font_Family || prop == css.Font_Weight {
  221. if value.TokenType == css.IdentToken && (prop == css.Font || prop == css.Font_Weight) {
  222. val := css.ToHash(value.Data)
  223. if val == css.Normal && prop == css.Font_Weight {
  224. // normal could also be specified for font-variant, not just font-weight
  225. value.TokenType = css.NumberToken
  226. value.Data = []byte("400")
  227. } else if val == css.Bold {
  228. value.TokenType = css.NumberToken
  229. value.Data = []byte("700")
  230. }
  231. } else if value.TokenType == css.StringToken && (prop == css.Font || prop == css.Font_Family) && len(value.Data) > 2 {
  232. unquote := true
  233. parse.ToLower(value.Data)
  234. s := value.Data[1 : len(value.Data)-1]
  235. if len(s) > 0 {
  236. for _, split := range bytes.Split(s, spaceBytes) {
  237. val := css.ToHash(split)
  238. // if len is zero, it contains two consecutive spaces
  239. if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default ||
  240. len(split) == 0 || !css.IsIdent(split) {
  241. unquote = false
  242. break
  243. }
  244. }
  245. }
  246. if unquote {
  247. value.Data = s
  248. }
  249. }
  250. } else if prop == css.Outline || prop == css.Border || prop == css.Border_Bottom || prop == css.Border_Left || prop == css.Border_Right || prop == css.Border_Top {
  251. if css.ToHash(value.Data) == css.None {
  252. value.TokenType = css.NumberToken
  253. value.Data = zeroBytes
  254. }
  255. }
  256. values[i].TokenType, values[i].Data = value.TokenType, value.Data
  257. }
  258. important := false
  259. if len(values) > 2 && values[len(values)-2].TokenType == css.DelimToken && values[len(values)-2].Data[0] == '!' && css.ToHash(values[len(values)-1].Data) == css.Important {
  260. values = values[:len(values)-2]
  261. important = true
  262. }
  263. if len(values) == 1 {
  264. if prop == css.Background && css.ToHash(values[0].Data) == css.None {
  265. values[0].Data = backgroundNoneBytes
  266. } else if bytes.Equal(property, msfilterBytes) {
  267. alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=")
  268. if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) {
  269. values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...)
  270. }
  271. }
  272. } else {
  273. if prop == css.Margin || prop == css.Padding || prop == css.Border_Width {
  274. if (values[0].TokenType == css.NumberToken || values[0].TokenType == css.DimensionToken || values[0].TokenType == css.PercentageToken) && (len(values)+1)%2 == 0 {
  275. valid := true
  276. for i := 1; i < len(values); i += 2 {
  277. if values[i].TokenType != css.WhitespaceToken || values[i+1].TokenType != css.NumberToken && values[i+1].TokenType != css.DimensionToken && values[i+1].TokenType != css.PercentageToken {
  278. valid = false
  279. break
  280. }
  281. }
  282. if valid {
  283. n := (len(values) + 1) / 2
  284. if n == 2 {
  285. if bytes.Equal(values[0].Data, values[2].Data) {
  286. values = values[:1]
  287. }
  288. } else if n == 3 {
  289. if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) {
  290. values = values[:1]
  291. } else if bytes.Equal(values[0].Data, values[4].Data) {
  292. values = values[:3]
  293. }
  294. } else if n == 4 {
  295. if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[0].Data, values[6].Data) {
  296. values = values[:1]
  297. } else if bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[2].Data, values[6].Data) {
  298. values = values[:3]
  299. } else if bytes.Equal(values[2].Data, values[6].Data) {
  300. values = values[:5]
  301. }
  302. }
  303. }
  304. }
  305. } else if prop == css.Filter && len(values) == 11 {
  306. if bytes.Equal(values[0].Data, []byte("progid")) &&
  307. values[1].TokenType == css.ColonToken &&
  308. bytes.Equal(values[2].Data, []byte("DXImageTransform")) &&
  309. values[3].Data[0] == '.' &&
  310. bytes.Equal(values[4].Data, []byte("Microsoft")) &&
  311. values[5].Data[0] == '.' &&
  312. bytes.Equal(values[6].Data, []byte("Alpha(")) &&
  313. bytes.Equal(parse.ToLower(values[7].Data), []byte("opacity")) &&
  314. values[8].Data[0] == '=' &&
  315. values[10].Data[0] == ')' {
  316. values = values[6:]
  317. values[0].Data = []byte("alpha(")
  318. }
  319. }
  320. }
  321. for i := 0; i < len(values); i++ {
  322. if values[i].TokenType == css.FunctionToken {
  323. n, err := c.minifyFunction(values[i:])
  324. if err != nil {
  325. return err
  326. }
  327. i += n - 1
  328. } else if _, err := c.w.Write(values[i].Data); err != nil {
  329. return err
  330. }
  331. }
  332. if important {
  333. if _, err := c.w.Write([]byte("!important")); err != nil {
  334. return err
  335. }
  336. }
  337. return nil
  338. }
  339. func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) {
  340. n := 1
  341. simple := true
  342. for i, value := range values[1:] {
  343. if value.TokenType == css.RightParenthesisToken {
  344. n++
  345. break
  346. }
  347. if i%2 == 0 && (value.TokenType != css.NumberToken && value.TokenType != css.PercentageToken) || (i%2 == 1 && value.TokenType != css.CommaToken) {
  348. simple = false
  349. }
  350. n++
  351. }
  352. values = values[:n]
  353. if simple && (n-1)%2 == 0 {
  354. fun := css.ToHash(values[0].Data[:len(values[0].Data)-1])
  355. nArgs := (n - 1) / 2
  356. if (fun == css.Rgba || fun == css.Hsla) && nArgs == 4 {
  357. d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if simple == true than this is a NumberToken or PercentageToken
  358. if d-1.0 > -minify.Epsilon {
  359. if fun == css.Rgba {
  360. values[0].Data = []byte("rgb(")
  361. fun = css.Rgb
  362. } else {
  363. values[0].Data = []byte("hsl(")
  364. fun = css.Hsl
  365. }
  366. values = values[:len(values)-2]
  367. values[len(values)-1].Data = []byte(")")
  368. nArgs = 3
  369. } else if d < minify.Epsilon {
  370. values[0].Data = []byte("transparent")
  371. values = values[:1]
  372. fun = 0
  373. nArgs = 0
  374. }
  375. }
  376. if fun == css.Rgb && nArgs == 3 {
  377. var err [3]error
  378. rgb := [3]byte{}
  379. for j := 0; j < 3; j++ {
  380. val := values[j*2+1]
  381. if val.TokenType == css.NumberToken {
  382. var d int64
  383. d, err[j] = strconv.ParseInt(string(val.Data), 10, 32)
  384. if d < 0 {
  385. d = 0
  386. } else if d > 255 {
  387. d = 255
  388. }
  389. rgb[j] = byte(d)
  390. } else if val.TokenType == css.PercentageToken {
  391. var d float64
  392. d, err[j] = strconv.ParseFloat(string(val.Data[:len(val.Data)-1]), 32)
  393. if d < 0.0 {
  394. d = 0.0
  395. } else if d > 100.0 {
  396. d = 100.0
  397. }
  398. rgb[j] = byte((d / 100.0 * 255.0) + 0.5)
  399. }
  400. }
  401. if err[0] == nil && err[1] == nil && err[2] == nil {
  402. val := make([]byte, 7)
  403. val[0] = '#'
  404. hex.Encode(val[1:], rgb[:])
  405. parse.ToLower(val)
  406. if s, ok := ShortenColorHex[string(val)]; ok {
  407. if _, err := c.w.Write(s); err != nil {
  408. return 0, err
  409. }
  410. } else {
  411. if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
  412. val[2] = val[3]
  413. val[3] = val[5]
  414. val = val[:4]
  415. }
  416. if _, err := c.w.Write(val); err != nil {
  417. return 0, err
  418. }
  419. }
  420. return n, nil
  421. }
  422. } else if fun == css.Hsl && nArgs == 3 {
  423. if values[1].TokenType == css.NumberToken && values[3].TokenType == css.PercentageToken && values[5].TokenType == css.PercentageToken {
  424. h, err1 := strconv.ParseFloat(string(values[1].Data), 32)
  425. s, err2 := strconv.ParseFloat(string(values[3].Data[:len(values[3].Data)-1]), 32)
  426. l, err3 := strconv.ParseFloat(string(values[5].Data[:len(values[5].Data)-1]), 32)
  427. if err1 == nil && err2 == nil && err3 == nil {
  428. r, g, b := css.HSL2RGB(h/360.0, s/100.0, l/100.0)
  429. rgb := []byte{byte((r * 255.0) + 0.5), byte((g * 255.0) + 0.5), byte((b * 255.0) + 0.5)}
  430. val := make([]byte, 7)
  431. val[0] = '#'
  432. hex.Encode(val[1:], rgb[:])
  433. parse.ToLower(val)
  434. if s, ok := ShortenColorHex[string(val)]; ok {
  435. if _, err := c.w.Write(s); err != nil {
  436. return 0, err
  437. }
  438. } else {
  439. if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
  440. val[2] = val[3]
  441. val[3] = val[5]
  442. val = val[:4]
  443. }
  444. if _, err := c.w.Write(val); err != nil {
  445. return 0, err
  446. }
  447. }
  448. return n, nil
  449. }
  450. }
  451. }
  452. }
  453. for _, value := range values {
  454. if _, err := c.w.Write(value.Data); err != nil {
  455. return 0, err
  456. }
  457. }
  458. return n, nil
  459. }
  460. func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte) (css.TokenType, []byte) {
  461. if tt == css.NumberToken || tt == css.PercentageToken || tt == css.DimensionToken {
  462. if tt == css.NumberToken && (prop == css.Z_Index || prop == css.Counter_Increment || prop == css.Counter_Reset || prop == css.Orphans || prop == css.Widows) {
  463. return tt, data // integers
  464. }
  465. n := len(data)
  466. if tt == css.PercentageToken {
  467. n--
  468. } else if tt == css.DimensionToken {
  469. n = parse.Number(data)
  470. }
  471. dim := data[n:]
  472. parse.ToLower(dim)
  473. data = minify.Number(data[:n], c.o.Decimals)
  474. if tt == css.PercentageToken && (len(data) != 1 || data[0] != '0' || prop == css.Color) {
  475. data = append(data, '%')
  476. } else if tt == css.DimensionToken && (len(data) != 1 || data[0] != '0' || requiredDimension[string(dim)]) {
  477. data = append(data, dim...)
  478. }
  479. } else if tt == css.IdentToken {
  480. //parse.ToLower(data) // TODO: not all identifiers are case-insensitive; all <custom-ident> properties are case-sensitive
  481. if hex, ok := ShortenColorName[css.ToHash(data)]; ok {
  482. tt = css.HashToken
  483. data = hex
  484. }
  485. } else if tt == css.HashToken {
  486. parse.ToLower(data)
  487. if ident, ok := ShortenColorHex[string(data)]; ok {
  488. tt = css.IdentToken
  489. data = ident
  490. } else if len(data) == 7 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] {
  491. tt = css.HashToken
  492. data[2] = data[3]
  493. data[3] = data[5]
  494. data = data[:4]
  495. }
  496. } else if tt == css.StringToken {
  497. // remove any \\\r\n \\\r \\\n
  498. for i := 1; i < len(data)-2; i++ {
  499. if data[i] == '\\' && (data[i+1] == '\n' || data[i+1] == '\r') {
  500. // encountered first replacee, now start to move bytes to the front
  501. j := i + 2
  502. if data[i+1] == '\r' && len(data) > i+2 && data[i+2] == '\n' {
  503. j++
  504. }
  505. for ; j < len(data); j++ {
  506. if data[j] == '\\' && len(data) > j+1 && (data[j+1] == '\n' || data[j+1] == '\r') {
  507. if data[j+1] == '\r' && len(data) > j+2 && data[j+2] == '\n' {
  508. j++
  509. }
  510. j++
  511. } else {
  512. data[i] = data[j]
  513. i++
  514. }
  515. }
  516. data = data[:i]
  517. break
  518. }
  519. }
  520. } else if tt == css.URLToken {
  521. parse.ToLower(data[:3])
  522. if len(data) > 10 {
  523. uri := data[4 : len(data)-1]
  524. delim := byte('"')
  525. if uri[0] == '\'' || uri[0] == '"' {
  526. delim = uri[0]
  527. uri = uri[1 : len(uri)-1]
  528. }
  529. uri = minify.DataURI(c.m, uri)
  530. if css.IsURLUnquoted(uri) {
  531. data = append(append([]byte("url("), uri...), ')')
  532. } else {
  533. data = append(append(append([]byte("url("), delim), uri...), delim, ')')
  534. }
  535. }
  536. }
  537. return tt, data
  538. }