Okay, so it seems like what I was after either doesn't exist or is very hard to come by, so I wrote a script that solves my particular problem. I'm pasting it here in the hope that someone sometime might find it useful.
<?php
$css = file_get_contents('test.css');
//Strip comments and whitespace. Tabs to spaces
$css = preg_replace("/\s{2,}/e", ' ', $css);
$css = preg_replace("/\/\*.*?\*\//", '', $css);
$css = str_replace("\t", " ", $css);
$css = str_replace(": ", ":", $css);
$css = str_replace(" }", "}", $css);
$css = str_replace("{", "{", $css);
$css = str_replace(";}", "}", $css);
//Break each rule out onto a new line
$css = preg_replace("/}\s*/", "}\r\n", $css);
//Break @ rules out onto new lines
$css = preg_replace('/(@.*?;\s*)/', '\0'."\r\n", $css);
//Parse CSS Rules
$parsed = array();
$css = explode("\r\n", $css);
foreach($css as $line => $rule){
if (preg_match('/(.*?)\{(.*?)\}/i', $rule, $regs)) {
$clean_selectors = preg_replace('/\s*,\s*/', ',', $regs[1]);
$clean_selectors = preg_replace('/,\s*$|\s$/', '', $clean_selectors);
$parsed[$line]['selectors'] = explode(',', $clean_selectors);
$parsed[$line]['properties'] = explode(';', $regs[2]);
} elseif(trim($rule) != '') {
$parsed[$line] = $rule;
}
}
//Group CSS by property
$groups = array();
foreach($parsed as $line => $css){
if(is_array($css)){
foreach($css['properties'] as $pline => $property){
if(isset($groups[$property])){
$groups[$property] = array_merge($groups[$property], $css['selectors']);
} else {
$groups[$property] = $css['selectors'];
}
}
} else {
$groups[$line] = $css;
}
}
//Output CSS sorted by property
foreach($groups as $property => $selectors){
if(is_array($selectors)){
asort($selectors);
echo implode(",\r\n", $selectors)." {\r\n\t".trim($property).";\r\n}\r\n\r\n";
} else {
echo $selectors."\r\n\r\n";
}
}
?>
Now, a couple of cavaets.
No, this is not the most beautiful code in the world, it was done quickly to solve one particular problem I was having once and it's tailored pretty heavily to the CSS I've been given to work with. That said, it should be generic enough to work with most CSS you throw at it.
It is the nature of CSS that sometimes the order in which a rule appears is important to the rendering of the final document. It is likely that if you just run all your CSS through this script that your page won't render as you expect anymore. I'm just using this script to group page-specific css on a web application that I have no layout control over. As each rule applies to a particular element on a particular page, I'm not expecting huge amounts of dodgyness when I group in this way - it's just going to make the CSS more maintainable.