css.go 49 KB

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