I have a php project which uses grunt to compile sass files into css. I was wondering if there is a way of doing css class renaming similar to Google's Closure Stylesheets . So really this is a two-part question:
As far as I know sass currently doesn't have any feature like this, unless it can be added as an extension. However with grunt I could compile the sass files first, then run another task that does the class renaming and outputs a map file. Technically I could use Closure Stylesheets for this, but I am looking for something a little more lightweight that doesn't require installing another dependency.
Now I could just insert something like this for every css class: <?php echo getclassname("some-class-name") ?>
which would reference the map file generated above to get the correct class name. But that seems tedious. Is there a better way of doing this?
First compile the sass then passing it through a custom task to rename the classes. I am using the css node module to parse the css. Let's start by looking at the custom grunt task.
Disclaimer: I wrote this code quickly so it is probably not production ready.
var fs = require( 'fs' ),
rename = require( './rename.js' );
// Register the rename_css task.
grunt.registerMultiTask('rename_css', 'Shorten css class names', function () {
var options = this.options(); // Pass all options directly to css.parse
this.files.forEach(function ( file ) {
var renamed = rename.rename(
fs.readFileSync( file.src[ 0 ], 'utf8' ), options );
fs.writeFileSync( file.dest, renamed.text );
fs.writeFileSync( file.map, JSON.stringify( renamed.map, null, 2 ) );
});
});
The configuration for this task would look something like this:
grunt.initConfig({
rename_css: {
options: { compress: true }, // Minify the output css.
main: {
src: "style.css",
dest: "style.min.css",
map: "map.json"
}
}
});
rename.js is too long to show it all here, but you can see the entire file on github . Here is the main function:
function rename( s, options /* passed directly to css.parse */ ) {
/**
* Give the css classes short names like a-b instead of some-class
*
* Returns an object in the form {text: `newCss`, map: `partsMap`} whare text is
* the newly generated css and partsMap is a map in the {oldPart: newPart}.
*/
var
ast = css.parse( s, options ),
countMap = walkPass1( ast.stylesheet ), // Walk the first pass.
sortedCounts = [],
map = {}, // Final map.
part,
// List of charictor positions for the short class names.
// Each number corresponds to a charictor in the `chars` string.
charPosSet = [ 0 ];
// Unpack the count map.
for ( part in countMap ) {
sortedCounts.push({
name: part,
count: countMap[ part ],
replacment: undefined
});
}
// Sort based on the number of counts.
// That way we can give the most used classes the smallest names.
sortedCounts.sort(function( a, b ) { return b.count - a.count });
// Generate the small class names.
sortedCounts.forEach(function ( part ) {
var
s = '',
i = charPosSet.length;
// Build up the replacment name.
charPosSet.forEach(function ( pos ) {
s += chars[ pos ];
});
while ( i-- ) {
charPosSet[ i ]++;
// If the current char pos is greater then the lenght of `chars`
// Then we set it to zero.
if ( charPosSet[ i ] == chars.length ) {
charPosSet[ i ] = 0;
if ( i == 0 ) { // Time to add another digit.
charPosSet.push( 0 ); // The next digit will start at zero.
}
} else {
// Everything is in bounds so break the loop.
break;
}
}
part.replacment = s;
});
// Now we pack a basic map in the form of old -> new.
sortedCounts.forEach(function ( part ) {
map[ part.name ] = part.replacment;
});
// Walk the tree a second time actually renameing the classes.
walkPass2( ast.stylesheet, map );
return {
text: css.stringify( ast, options ), // Rebuild the css.
map: map
};
}
It looks compicated but here is a break-down of what it is doing:
It's worth pointing out that this function will give more frequently used classes shorter names, which will result in slightly smaller css files.
This can be done with an output buffer. It might look something like this (at the top of the page before the root html
tag):
<?php
define(DEV_MODE, false);
function build_class( $name, $map ) {
$parts = [];
foreach ( explode( '-', $name ) as $part ) {
$newPart = array_key_exists( $part, $map )? $map[ $part ] : $part;
array_push( $parts, $newPart );
}
return implode( '-', $parts );
}
function class_rename ( $content ) {
$string = file_get_contents( 'map.json' );
$classMap = json_decode( $string, true );
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false; // Remove unnesesary whitespace.
@$doc->loadHTML( $content );
foreach ( $doc->getElementsByTagName( '*' ) as $elem ) {
$classStr = $elem->getAttribute( 'class' );
if ( ! empty( $classStr ) ) { // No need setting empty classess all over the place.
$classes = []; // This is ware we put all the renamed classes.
foreach ( explode( ' ', $classStr ) as $class ) {
array_push( $classes, build_class( $class, $classMap ) );
}
$elem->setAttribute( 'class', implode( ' ', $classes ) );
}
}
return $doc->saveHTML();
}
if (!DEV_MODE)
ob_start( 'class_rename' );
?>
Although not a part of the original question, the solution is quite interesting and not exactly trivial so I decided to include it.
First of all register another grunt task:
var fnPattern = /(jQuery|\$|find|__)\s*\(\s*(["'])((?:\\.|(?!\2).)*)\2\s*\)/g;
grunt.registerMultiTask('rename_js', 'Use short css class names.', function () {
this.files.forEach(function ( file ) {
var
content = fs.readFileSync( file.src[ 0 ], 'utf8' ),
map = JSON.parse( fs.readFileSync( file.map ) ),
output = content.replace( fnPattern, function ( match, fn, delimiter, str ) {
var classes, i;
if ( fn == '__' ) {
classes = str.split( ' ' );
i = classes.length;
while ( i-- ) {
classes[ i ] = rename.getClassName( classes[i], map );
}
// We can safly assume that that the classes string won't contain any quotes.
return '"' + classes.join( ' ' ) + '"';
} else { // Must be a jQuery function.
return match.replace( str, rename.getClassSelector( str, map ) );
}
});
// Wrap the output in a function so that the `__` function can get removed by an optimizer.
fs.writeFileSync( file.dest, '!(function(window, undefined) {\n' + output + '\n})(window);' );
});
});
The JavaScript file might look something like this:
function __( s ) {
return s;
}
window.main = function () {
var elems = document.getElementsByClassName(__('some-class-name')),
i = elems.length;
while ( i-- ) {
elems[ i ].className += __(' some-other-class-name');
}
}
The important part is the __
function declaration. During development this function will do nothing, but when we build the app, this function will get replaced with the compiled class string. The regex expression used will find all occurrences of __
as well as the jQuery functions ( jQuery
, $
and jQuery.find
). It then creates three groups: The function name, the delimiter (either "
or '
) and the inside string. Here is a diagram to help better understand the what's going on:
(?:jQuery|\$|find)\s*\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)
If the function name is __
then we replace it the same way we did for the php. If not then it is probably a selector so we try to do a selector class replace.
(Note that this does not handle html text getting inputted into a jQuery function.)
You can get a full example here
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.