Wha?
For years i've wanted a "minifier" for CSS (Cascading Style Sheets) but all of them i've found are either tens of megabytes huge, with insane amounts of dependencies, and/or are based on node.js
(against which i have a strong allergy). Well, i'm not getting any younger, and my desire for a CSS minifier is starting to get acute, so i finally sat down for a few hours to Get This Done...
cssminc is a tiny CSS minifier. It's made up of only a small amount of dependency-free, portable, C89-compliant source code. It has only two small files, one C and one header, and can be built as either a library or a standalone binary. (Building the binary also requires one additional C/header pair with app-level utility code.) The library-only bits compile down to a mere 9kb of binary code.
cssminc is intended to be pronounced "see-ess-ess mincy," "see-ess-ess min-see," "see-ess-ess mink," "see-ess-ess mince," "see-smink," or however else one prefers.
Features
Tiny (two-file), portable strictly-C89-compatible code with no external dependencies beyond the standard C library. The binary, as opposed to the library, requires 4 files (2 C, 2 headers), two of them being generic app-level utility code.
A minuscule library API which can stream its input and output from/to any source/destination via a simple, abstract I/O interface (one function per direction of the data). The binary reads from
stdin
and writes tostdout
, as any well-behaved Unix-style app should.Has more API documentation than most projects with 20x as many developers and 1000x as much code. Seriously.
Plenty fast and relatively efficient. At any given time it only stores the previous 2 bytes of the input stream and it actively prefers constant-time logic branches (
switch
/case
) over linear-time ones (if
/else
).The core library has constant memory costs and uses no dynamic memory, but its data source/sink very probably do (opening
FILE
handles isn't cheap!).Can consume ASCII or UTF-8, including data with "weird characters" in them (so long as they're encoded as UTF-8, not latin-1, some "code page" dialect, or similar).
Can optionally try to compress everything, leaving zero (or very close to zero) extraneous space, or it can, for slightly improved readability and the avoidance of ultra-long lines, insert a space or newline after each rule block. (Some tools diagnose files with exceptionally long lines as binary, which may or may not cause usability problems.)
Misfeatures
It is not yet known to work 100% reliably on all valid CSS, but is also not known to not work reliably. Bug reports and patches in that regard would be much appreciated. That said: it is used to minify the CSS for my main site, so it's known to work reasonably well.
It does not "lint" or validate CSS: it assumes that its inputs are already valid CSS (because they were tested for semantic validity before minification, right? Right?).
It does not explicitly support any CSS-like dialects such as SASS and LESS or whatever the cool kids are using these days. Projects using such tools already have their favourite
node.js
-based infrastructure to process it. i have no intentions whatsoever of going down the rabbit hold of trying to support arbitrary non-standard CSS dialects (minor patches to that effect would be gladly accepted, so long as they're not invasive). That said: if a dialect supports the same basic format as CSS, without imposing new syntactic rules, then it "might" work as-is with this code.
License
License: dual Public Domain/MIT
Feedback and patches are happily received: https://wanderinghorse.net/home/stephan/
Examples
The Library API:
cssminc_state cc = cssminc_state_empty; /* similar to memset() but uses some non-0 values */
int rc;
cc.in = cssminc_input_f_FILE; /* input source */
cc.inState = stdin; /* state for cc.in */
cc.out = cssminc_output_f_FILE; /* output destination */
cc.outState = stdout; /* State for cc.out */
rc = cssminc_process(&cc)
/* 0 on success, non-0 on error, noting that "error" only
means that it choked on some of the input or one of
the I/O routines failed. */;
The in
and out
members can be arbitrary implementations of the I/O abstraction functions, and the inState
/outState
members are opaque state pointers for use by those functions. e.g. they could be pointers to an app-specific memory buffer type, or the output routine might send its contents directly to a UI widget (nothing that it might do so a byte at a time, so the target needs to be prepared to accept, at least briefly, partial multi-byte characters).
The CLI App
~> time ./cssminc --metrics < big.css > foo.css
./cssminc metrics:
Input bytes = 260530, output = 137715, = 47.14% reduction
Output spaces = 4188, non-spaces = 133527
real 0m0.038s
user 0m0.031s
sys 0m0.001s
(That's from a Raspberry Pi 4B system running on an external USB 2.0 hard drive, not a fast workstation.)
Output samples:
Sample (admittedly trivial) input:
$ cat foo.css
/* This comment is normally retained. */
bläüp, bläüp.x bläüp.y-z,
bläüp.zzz > x
{x:"üö" , yy : zz }/* these non-ASCII characters must survive the ccssmin process. */
/*/ <--- this is a comment, despite
weird placement of that /. */
/* MOAR comments */
foo { y:z }
/* Not retained */
bar { foo: "barre baz" }
cssminc -f foo.css -bn
The -bn
tells it to add a newline after each closing brace (}
):
/* This comment is normally retained. */
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz}
foo{y:z}
bar{foo:"barre baz"}
cssminc -f foo.css -bs
-bs
tells it to add a space after each closing brace:
/* This comment is normally retained. */
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz} foo{y:z} bar{foo:"barre baz"}
cssminc -f foo.css -b0 -c
-b0
tells it to add nothing after closing braces and -c
tells it to strip the initial comment:
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz}foo{y:z}bar{foo:"barre baz"}
Normally the initial comment of a CSS file contains a license or attribution, and should not be stripped.
cssminc
only supports keeping the first comment if it is the first non-space content in the input. Comments appearing
after non-space content are unconditionally stripped.