# CSS::Minifier

CSS minifier for Raku - parses, optimizes, and re-serializes CSS stylesheets.

## Features

- **Plugin pipeline** - modular optimization passes (values, colors, dedup, merging)
- **Two levels** - level 1 (safe normalizations) vs level 2 (structural optimizations)
- **Color optimization** - shorten hex colors, optionally convert to named colors (delegated to CSS::Writer/CSS::Properties)
- **Value normalization** - `bold` → `700`, `normal` → `400`
- **Duplicate removal** - identical rulesets and `@font-face` blocks deduplicated
- **Ruleset merging** - same-selector rules merged, same-declaration selectors combined
- **Shorthand consolidation** - leverages CSS::Properties optimizer for compact output
- **License preservation** - `/*! */` comments kept with `--preserve-licenses`
- **CLI tool** - `cssminify` with stdin/file input and output redirection

## Dependencies

- [CSS::Stylesheet](https://raku.land/zef:dwarring/CSS::Stylesheet) - CSS parsing and AST
- [CSS::Properties](https://raku.land/zef:dwarring/CSS::Properties) - property optimization
- [CSS::Writer](https://raku.land/zef:dwarring/CSS::Writer) - CSS serialization

## Installation

```bash
zef install CSS::Minifier
```

### Installing from source

```bash
cd /path/to/CSS-Minifier
zef install .
```

Requires Raku 6.d or later.

## Usage

### Command Line

```bash
# Read from file
cssminify input.css > output.css

# Level 2 (structural optimizations: dedup, merging)
cssminify -l2 input.css > output.css

# Write to file
cssminify -o output.css input.css

# Read from stdin
cat input.css | cssminify

# Preserve license comments
cssminify -p input.css

# Disable hex color shortening (keep #FF0000 instead of #F00)
cssminify --no-color-masks input.css

# Disable unknown-property preservation (declarations dropped by CSS::Properties stay dropped)
cssminify --no-preserve-unknown input.css

# Separate rules with newlines (readable output)
cssminify -r input.css

# Convert hex colors to named colors
cssminify -n input.css

# Verbose output (log plugin names)
cssminify -v input.css

# Print version
cssminify --version
```

#### Flags

| Short | Long | Description |
|---|---|---|
| `-o` | `--output` | Write to file instead of stdout |
| `-l` | `--level` | Optimization level: 1 (default) or 2 (structural) |
| `-p` | `--preserve-licenses` | Keep `/*! */` license comments |
| `-n` | `--color-names` | Convert hex colors to named colors |
| `-m` / `-/m` | `--color-masks` / `--no-color-masks` | Enable/disable hex color shortening |
| `-u` / `-/u` | `--preserve-unknown` / `--no-preserve-unknown` | Enable/disable unknown-property preservation (default: enabled) |
| `-r` | `--readable` | Separate rules with newlines |
| `-v` | `--verbose` | Log plugin names to stderr |
| | `--version` | Print version and exit |

Negatable booleans accept `--no-flag` (long) or `-/f` (short), e.g. `--no-color-masks` or `-/m`.

Short flags with values use `=` or fused syntax: `-l=2` or `-l2` (not `-l 2`).

### Library

```raku
use CSS::Minifier;

# Level 1 - safe normalizations
my $min = CSS::Minifier.minify($css);

# Level 2 - structural optimizations (dedup, merging)
my $min = CSS::Minifier.minify($css, :level(2));

# Preserve license comments
my $min = CSS::Minifier.minify($css, :preserve-licenses);

# Convert hex colors to named colors
my $min = CSS::Minifier.minify($css, :color-names);

# Disable hex color shortening
my $min = CSS::Minifier.minify($css, :!color-masks);

# Disable unknown-property preservation (dropped by CSS::Properties)
my $min = CSS::Minifier.minify($css, :!preserve-unknown);

# Add custom plugins (runs after all level plugins, in append order)
# Custom plugins must implement the CSS::Minifier::Plugin role.
my $min = CSS::Minifier.minify($css, :level(2), :extra-plugins[$my-plugin]);
```

## Optimization Levels

### Level 1 (default)
- Font-weight normalization (`bold` → `700`, `normal` → `400`)
- Color shortening (`#FF0000` → `#F00`, `red` → `#F00`)
- Shorthand consolidation (via CSS::Properties optimizer)
- Comment removal (`/*! */` kept with `--preserve-licenses`)
- Whitespace removal (via compact writer)

### Level 2
- Duplicate ruleset removal
- `@font-face` deduplication
- Same-selector ruleset merging
- Same-declaration selector combination

Note: at-rules (`@keyframes`, `@supports`, `@layer`) are preserved
with all their declarations intact via a pre-parse scan, but are not
subject to deduplication or value normalization.

## Examples

### Input
```css
h1 { color: red; }
h2 { color: red; }
h1 { margin: 0; }
```

### Level 1 Output
```css
h1{color:#F00}h2{color:#F00}h1{margin:0}
```

### Level 2 Output
```css
h1, h2{color:#F00}h1{margin:0}
```

## Limitations

**Warning:** The merging plugin can silently change cascade order. Review
level-2 output before deploying to production.

- **Non-adjacent same-declaration merging** - rulesets with identical
  declarations are merged even when propertyless rules separate them,
  potentially changing computed styles.
- **`@keyframes` / `@supports` / `@container` / `@layer`** - these at-rules are not
  processed by this minifier's pipeline (dedup, merging, value normalization).
  All declarations inside them are preserved via a pre-parse scan
  (both known and unknown properties), but values come through un-minified
  since they bypass the standard pipeline. Only `@media` and `@font-face`
  are structurally handled.
- **Propertyless rules** - rulesets without properties (parsing artifacts)
  are silently dropped during dedup and merging. Same-declaration merging
  skips them rather than treating them as barriers, which can merge
  rulesets that are not strictly adjacent.
- **Unknown properties** - CSS::Properties drops properties it doesn't recognize
  (e.g. `text-decoration-line`, `outline-offset`). `CSS::Minifier`
  works around this by re-injecting missing declarations after minification
  (enabled by default; disable with `--no-preserve-unknown`). Declarations
  inside at-rules (`@media`, `@supports`, etc.) are also preserved. Known
  properties inside at-rules that CSS::Stylesheet drops (`@keyframes`,
  `@supports`, `@container`, `@layer`) are also preserved via the
  pre-parse scan, though they come through without value normalization.
- **Comments** - CSS::Stylesheet strips all comments during parsing.
  The `--preserve-licenses` flag extracts `/*! */` comments from the raw
  CSS via regex before parsing, then prepends them to the output.
  A known limitation: the pattern may match inside string values
  (e.g. `content: "..."`).

# AUTHOR

Sasha Abbott <sashaa@disroot.org>

# LICENSE

This library is free software; you can redistribute it and/or modify it under CC0.
