简体   繁体   中英

CSS Class Renaming with php integration

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:

  1. How to compile sass with shortened class names.

    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.

  2. How to include these class names in php.

    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?

How to compile sass with shortened class names

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:

  1. Parse the css and get the Abstract Syntax Tree(ast).
  2. Walk the tree creating a map of class parts to counts (the number of occurenses within a css document).
  3. Pack the map into an array and sort it based on the counts.
  4. Iterate over the array creating the shortened class names.
  5. Create the final map in the form of oldName -> newName
  6. Walk the tree a second time actually replacing the old class names with the new ones.
  7. Return the compiled css along with the generated map.

It's worth pointing out that this function will give more frequently used classes shorter names, which will result in slightly smaller css files.

How to include these class names in php.

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' );
?>

JavaScript (bonus)

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*\)

正则表达式可视化

Debuggex Demo

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM