11import {
22 tokenize ,
3+ TOKEN_DELIM ,
34 TOKEN_FUNCTION ,
5+ TOKEN_COMMA ,
6+ TOKEN_COLON ,
7+ TOKEN_SEMICOLON ,
8+ TOKEN_LEFT_PAREN ,
9+ TOKEN_RIGHT_PAREN ,
10+ TOKEN_LEFT_BRACKET ,
11+ TOKEN_RIGHT_BRACKET ,
12+ TOKEN_LEFT_BRACE ,
13+ TOKEN_RIGHT_BRACE ,
414 TOKEN_WHITESPACE ,
515 TOKEN_COMMENT ,
616 TOKEN_CDO ,
717 TOKEN_CDC ,
18+ type TokenType ,
819} from '@projectwallace/css-parser'
920
21+ // Char codes for the DELIM characters that are unambiguous in every CSS
22+ // context: ! / < = > ~
23+ function is_unambiguous_delim ( code : number ) : boolean {
24+ return code === 33 || code === 47 || code === 60 || code === 61 || code === 62 || code === 126
25+ }
26+
27+ /**
28+ * True if a token of this `type` (and, for a DELIM, this char `code`) never
29+ * needs a space immediately BEFORE it, in any CSS context.
30+ *
31+ * `,` `:` `;` and the unambiguous delimiters above are always tight on both
32+ * sides. `)` `]` only drop their leading space here; their trailing space
33+ * is left to the default rule (see `no_trailing_space`), since e.g. the gap
34+ * between `)` and a following `and` in a media query must survive.
35+ */
36+ function no_leading_space ( type : TokenType , code : number ) : boolean {
37+ switch ( type ) {
38+ case TOKEN_COMMA :
39+ case TOKEN_COLON :
40+ case TOKEN_SEMICOLON :
41+ case TOKEN_LEFT_BRACE :
42+ case TOKEN_RIGHT_BRACE :
43+ case TOKEN_RIGHT_PAREN :
44+ case TOKEN_RIGHT_BRACKET :
45+ return true
46+ case TOKEN_DELIM :
47+ return is_unambiguous_delim ( code )
48+ default :
49+ return false
50+ }
51+ }
52+
1053/**
11- * Characters where the space on one particular side is always safe to
12- * drop, in every CSS context: separators/operators that are unambiguous
13- * wherever they appear, plus grouping punctuation.
54+ * True if a token of this `type` (and, for a DELIM, this char `code`) never
55+ * needs a space immediately AFTER it, in any CSS context.
1456 *
15- * `+`, `-` and `*` are deliberately left out of both sets: they're also
16- * used where whitespace IS significant (calc's mandatory `+`/`-`, the
17- * universal selector `*` right before a descendant combinator), and
18- * telling those uses apart from "just an operator" would mean knowing
19- * what kind of CSS construct is currently being printed - the whole point
20- * here is to not track that. A few spots (`calc(1px * 2)`,
21- * `:nth-child(-n + 3)`, `a + b`) keep an optional space a fussier
22- * minifier would strip - a good trade for never needing a parser.
57+ * `(` `[` only drop their trailing space here: an ident directly before
58+ * either (`and(` vs `and (`, `a[href]` vs `a [href]`) can change meaning, so
59+ * their leading space is left to the default rule instead. A FUNCTION token
60+ * already has its own `(` fused into it (`rgb(`), so it's tight the same way.
2361 *
24- * `(` and `[` only drop their trailing space: an ident directly before
25- * either (`and(` vs `and (`, `a[href]` vs `a [href]`) can change meaning,
26- * so the space before them is left to the default rule below instead.
62+ * `+`, `-` and `*` are deliberately unhandled by either function: they're
63+ * also used where whitespace IS significant (calc's mandatory `+`/`-`, the
64+ * universal selector `*` right before a descendant combinator), and telling
65+ * those uses apart from "just an operator" would mean knowing what kind of
66+ * CSS construct is currently being printed - the whole point here is to not
67+ * track that. A few spots (`calc(1px * 2)`, `:nth-child(-n + 3)`, `a + b`)
68+ * keep an optional space a fussier minifier would strip - a good trade for
69+ * never needing a parser.
2770 */
28- const TIGHT_BEFORE = new Set ( [ ',' , ':' , ';' , '!' , '>' , '<' , '=' , '~' , '/' , ')' , ']' , '}' , '{' ] )
29- const TIGHT_AFTER = new Set ( [ ',' , ':' , ';' , '!' , '>' , '<' , '=' , '~' , '/' , '(' , '[' , '{' , '}' ] )
71+ function no_trailing_space ( type : TokenType | - 1 , code : number ) : boolean {
72+ switch ( type ) {
73+ case TOKEN_COMMA :
74+ case TOKEN_COLON :
75+ case TOKEN_SEMICOLON :
76+ case TOKEN_LEFT_BRACE :
77+ case TOKEN_RIGHT_BRACE :
78+ case TOKEN_LEFT_PAREN :
79+ case TOKEN_LEFT_BRACKET :
80+ case TOKEN_FUNCTION :
81+ return true
82+ case TOKEN_DELIM :
83+ return is_unambiguous_delim ( code )
84+ default :
85+ return false
86+ }
87+ }
3088
3189/**
3290 * Minify a string of CSS: print every token, drop every comment, and drop
3391 * whitespace except the one space needed to keep meaning intact.
3492 *
3593 * Unlike `format()`, this never builds a syntax tree - it walks the raw
36- * token stream once, tracking only the previous token's last character,
37- * and reconstructs the minimal text for it.
94+ * token stream once, tracking only the previous token's type and (for a
95+ * DELIM) char code, and reconstructs the minimal text for it.
3896 */
3997export function minify ( css : string ) : string {
4098 let out = ''
4199 let had_space = false
42- let prev_char = ''
43- let prev_was_function = false
100+ let prev_type : TokenType | - 1 = - 1
101+ let prev_code = 0
44102
45103 for ( let { type, start, end } of tokenize ( css ) ) {
46104 if (
@@ -53,31 +111,29 @@ export function minify(css: string): string {
53111 continue
54112 }
55113
56- let text = css . slice ( start , end )
57- let char = text . length === 1 ? text : ''
114+ let code = type === TOKEN_DELIM ? css . charCodeAt ( start ) : 0
58115
59116 // A redundant `;` right before the block closes can just go.
60- if ( char === '}' && prev_char === ';' ) {
117+ if ( type === TOKEN_RIGHT_BRACE && prev_type === TOKEN_SEMICOLON ) {
61118 out = out . slice ( 0 , - 1 )
62119 }
63120
64- if ( prev_char === ':' && ( char === ';' || char === '}' ) ) {
121+ if ( prev_type === TOKEN_COLON && ( type === TOKEN_SEMICOLON || type === TOKEN_RIGHT_BRACE ) ) {
65122 // An empty declaration value (the `--foo: ;` "space toggle" trick)
66123 // must keep at least one space, or it silently becomes invalid.
67124 out += ' '
68125 } else if (
69126 out !== '' &&
70127 had_space &&
71- ! TIGHT_BEFORE . has ( char ) &&
72- ! TIGHT_AFTER . has ( prev_char ) &&
73- ! prev_was_function
128+ ! no_leading_space ( type , code ) &&
129+ ! no_trailing_space ( prev_type , prev_code )
74130 ) {
75131 out += ' '
76132 }
77133
78- out += text
79- prev_char = char
80- prev_was_function = type === TOKEN_FUNCTION
134+ out += css . slice ( start , end )
135+ prev_type = type
136+ prev_code = code
81137 had_space = false
82138 }
83139
0 commit comments