Skip to content

Instantly share code, notes, and snippets.

@necolas
Created June 30, 2012 18:07
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save necolas/3024891 to your computer and use it in GitHub Desktop.
Save necolas/3024891 to your computer and use it in GitHub Desktop.
Grunt tasks to process HTML files and produce a deploy directory of optimized files
  • Avoid reprocessing the same block in different HTML files.
  • Throw warning when processing a different block to an existing destination file. Hashing will avoid collisions, but introduce confusion.
  • Add file versioning for inline media and CSS images.
  • Avoid need for 'usemin' task - get the replacement element pattern from the first/last HTML element in actual block being replaced. Added benefit of preserving other attributes that may exist (e.g. title, media).

Acknowledgements: This is an adaption of some of Mickael Daniel's work on h5bp/node-build-script

module.exports = function(grunt) {
// Grunt utilities
var config = grunt.config;
var utils = grunt.utils;
// Deploy directory
var deploydir = '_deploy/';
// Grunt configuration
config.init({
// final build output
deploydir: deploydir,
processhtml: {
files: ['templates/*.html']
}
});
grunt.loadTasks('./tasks/');
grunt.registerTask('build', 'processhtml');
};
<!DOCTYPE html>
<html>
<head>
<title>Grunt test</title>
<!-- build:css css/app.css -->
<link rel="stylesheet" href="css/app-1.css">
<link rel="stylesheet" href="css/app-2.css">
<!-- endbuild -->
<!-- build:js js/head.js -->
<script src="js/head-1.js"></script>
<script src="js/head-2.js"></script>
<!-- endbuild -->
</head>
<body>
<!-- build:js js/app.js -->
<script src="js/app-1.js"></script>
<script src="js/app-2.js"></script>
<script src="js/app-3.js"></script>
<script src="js/app-4.js"></script>
<script src="js/app-5.js"></script>
<!-- endbuild -->
</body>
</html>
module.exports = function(grunt) {
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
/**
* The 'md5' helper is a basic wrapper around crypto.createHash, with given
* `algorithm` and `encoding`. Both are optional and defaults to `md5` and
* `hex` values.
*/
grunt.registerHelper('md5', function(filepath, algorithm, encoding) {
algorithm = algorithm || 'md5';
encoding = encoding || 'hex';
var hash = crypto.createHash(algorithm);
hash.update(grunt.file.read(filepath));
grunt.log.verbose.write('Hashing ' + filepath + '...');
return hash.digest(encoding);
});
/**
* The 'md5:content' helper hashes string content directly
*/
grunt.registerHelper('md5:content', function(content, algorithm, encoding) {
content = content.toString();
algorithm = algorithm || 'md5';
encoding = encoding || 'hex';
var hash = crypto.createHash(algorithm);
hash.update(content);
return hash.digest(encoding);
});
};
module.exports = function(grunt) {
/**
* minify:* is used to minify static content
*/
grunt.registerHelper('minify', function(type, content, options) {
return grunt.helper('minify:' + type, content, options);
});
grunt.registerHelper('minify:js', function(content, options) {
return grunt.helper('uglify', content, options);
});
grunt.registerHelper('minify:css', function(content, options) {
return grunt.helper('clean-css', content, options);
});
grunt.registerHelper('minify:html', function(content, options) {
return grunt.helper('html-minifier', content, options);
});
};
module.exports = function(grunt) {
var fs = require('fs');
var path = require('path');
var linefeed = grunt.utils.linefeed;
var deploydir = grunt.config.get('deploydir');
/**
* Register the 'processhtml' task
*
* This task accepts files and steps through them to find a comment pattern
* that wraps a set of 'script' or 'link' elements. It replaces those
* elements within an indivudal comment block with a single element
* referencing a file into which they are all concatenated.
*/
grunt.registerMultiTask('processhtml', 'Replaces and concatenates scripts / stylesheets in special HTML comment blocks', function() {
var name = this.target;
var data = this.data;
var files = grunt.file.expand(data);
files.map(grunt.file.read).forEach(function(content, i) {
var p = files[i];
grunt.log.subhead('processhtml - ' + p);
// make sure to convert back into utf8, `file.read` when used as a
// forEach handler will take additional arguments, and thus trigger the
// raw buffer read
content = content.toString();
// Replace and concatenate blocks of CSS/JS in HTML files
if (!!grunt.task._helpers['processhtml:compressblocks']) {
content = grunt.helper('processhtml:compressblocks', content);
}
// Minify the HTML files that were processed
if (!!grunt.task._helpers['minify:html']) {
content = grunt.helper('minify:html', content);
}
// Write the new HTML content to disk in the deploy directory
grunt.file.write(deploydir + p, content);
grunt.log.writeln('File "' + deploydir + p + '" created.');
});
});
/**
* Process files with the blocks and compress the files within them.
*/
grunt.registerHelper('processhtml:compressblocks', function(content) {
var blocks = getBlocks(content);
// Handle blocks
blocks.forEach(function(el) {
var block = el.raw.join(linefeed);
var src = el.src;
var type = el.type;
var dest = el.dest;
// Concatenate the source files
var combined = grunt.helper('concat', src, { separator: '' });
// Minify the concatenated files
var minified = grunt.helper('minify', type, combined, {});
// Hash the compressed files
var filehash = grunt.helper('md5:content', minified).slice(0,8);
// Create a new versioned filepath for the destination file
var filepath = dest.replace(new RegExp('(.' + type + ')'), '.' + filehash + '$1');
// Write the concatenated, minified, and versioned file to the deploy directory
grunt.file.write(deploydir + filepath, minified);
// Fail task if errors were logged.
if (this.errorCount) { return false; }
// Otherwise, print a success message.
grunt.log.writeln('File "' + filepath + '" created.');
// Update the content to reference the concatenated and versioned files
content = grunt.helper('usemin', type, content, block, filepath);
});
return content;
});
};
/**
* TODO: convert to grunt helper
*
* Returns an array of all the directives for the given html. Results is
* of the following form:
*
* [{
* type: 'css',
* dest: 'css/site.css',
* src: [ 'css/normalize.css', 'css/main.css' ],
* raw: [ ' <!-- build:css css/site.css -->',
' <link rel="stylesheet" href="css/normalize.css">'
* ' <link rel="stylesheet" href="css/main.css">'
* ' <!-- endbuild -->' ]
* },
* {
* type: 'js',
* dest: 'js/site.js',
* src: [ 'js/plugins.js', 'js/main.js' ],
* raw: [ ' <!-- build:js js/site.js -->',
* ' <script src="js/plugins.js">'
* ' <script src="js/main.js">'
* ' <!-- endbuild -->' ]
* }]
*/
function getBlocks(body) {
// Start build pattern
// <!-- build:[type] destination -->
// TODO: use better regex for dest match
var regexBuildStart = /<!--\s*build:(\w+)\s*(.+)\s*-->/;
// End build pattern
// <!-- endbuild -->
var regexBuildEnd = /<!--\s*endbuild\s*-->/;
var regexComment = /<!--(.*)-->/;
// Match single or double quotes
var regexSrc = /src=['"]([^"']+)["']/;
var regexHref = /href=['"]([^"']+)["']/;
var lines = body.replace(/\r\n/g, '\n').split(/\n/);
var isBlock = false;
var sections = [];
var src;
var raw;
var i = 0;
lines.forEach(function(line) {
var buildParams = line.match(regexBuildStart);
var isBuild = regexBuildStart.test(line);
var isBuildEnd = regexBuildEnd.test(line);
var isComment = regexComment.test(line);
if (isBuild) {
isBlock = true;
sections[i] = {};
sections[i].type = buildParams[1].trim();
sections[i].dest = buildParams[2].trim();
sections[i].src = src = [];
sections[i].raw = raw = [];
i++;
}
if (isBlock && raw && src) {
raw.push(line);
if (!isComment) {
if (regexSrc.test(line)) {
src.push(line.match(regexSrc)[1]);
}
if (regexHref.test(line)) {
src.push(line.match(regexHref)[1]);
}
}
if (isBuildEnd) {
isBlock = false;
}
}
});
return sections;
}
module.exports = function(grunt) {
/**
* usemin and usemin:* are used to replace the blocks in HTML
*/
grunt.registerHelper('usemin', function(type, content, block, dest) {
var indent = (block.split(grunt.utils.linefeed)[0].match(/^\s*/) || [])[0];
if (type === 'css') {
return content.replace(block, indent + '<link rel="stylesheet" href="' + dest + '">');
}
if (type === 'js') {
return content.replace(block, indent + '<script src="' + dest + '"></script>');
}
return false;
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment