Skip to content

Commit ad409dc

Browse files
committed
Replace string/char-set comparisons with token type and char codes
, : ; ( ) [ ] { } each have their own dedicated token type, so checking type (a number) is both cheaper and clearer than slicing the source into a 1-char string and hashing it through a Set<string>. Only the remaining delimiter characters (! / < = > ~ + - *) share a single DELIM token type and still need a char-level check, but that's now a numeric charCodeAt comparison instead of a string. No behavior change - same 263 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
1 parent 4c62861 commit ad409dc

1 file changed

Lines changed: 86 additions & 30 deletions

File tree

src/lib/minify.ts

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,104 @@
11
import {
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
*/
3997
export 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

Comments
 (0)