var fs = require('fs'),
path = require('path');
//
// ### Usemin Task
//
// Replaces references ton non-optimized scripts / stylesheets into a
// set of html files (or any template / views).
//
// Right now the replacement is based on the filename parsed from
// content and the files present in accoding dir (eg. looking up
// matching revved filename into `intermediate/` dir to know the sha
// generated).
//
// Todo: Use a file dictionary during build process and rev task to
// store each optimized assets and their associated sha1.
//
// Thx to @krzychukula for the new, super handy replace helper.
//
module.exports = function(grunt) {
var linefeed = grunt.utils.linefeed;
grunt.registerMultiTask('usemin', 'Replaces references to non-minified scripts / stylesheets', function() {
var name = this.target,
data = this.data,
files = grunt.file.expand(data);
files.map(grunt.file.read).forEach(function(content, i) {
var p = files[i];
grunt.log.subhead('usemin - ' + 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();
// ext-specific directives handling and replacement of blocks
if(!!grunt.task._helpers['usemin:pre:' + name]) {
content = grunt.helper('usemin:pre:' + name, content);
}
// actual replacement of revved assets
if(!!grunt.task._helpers['usemin:post:' + name]) {
content = grunt.helper('usemin:post:' + name, content);
}
// write the new content to disk
grunt.file.write(p, content);
});
});
// usemin:pre:* are used to preprocess files with the blocks and directives
// before going through the global replace
grunt.registerHelper('usemin:pre:html', function(content) {
// XXX extension-specific for get blocks too.
//
// Eg. for each predefined extensions directives may vary. eg for html, /** directive **/ for css
var blocks = getBlocks(content);
// handle blocks
Object.keys(blocks).forEach(function(key) {
var block = blocks[key].join(linefeed),
parts = key.split(':'),
type = parts[0],
target = parts[1];
content = grunt.helper('usemin', content, block, target, type);
});
return content;
});
// usemin and usemin:* are used with the blocks parsed from directives
grunt.registerHelper('usemin', function(content, block, target, type) {
target = target || 'replace';
return grunt.helper('usemin:' + type, content, block, target);
});
grunt.registerHelper('usemin:css', function(content, block, target) {
var indent = (block.split(linefeed)[0].match(/^\s*/) || [])[0];
return content.replace(block, indent + '');
});
grunt.registerHelper('usemin:js', function(content, block, target) {
var indent = (block.split(linefeed)[0].match(/^\s*/) || [])[0];
return content.replace(block, indent + '');
});
grunt.registerHelper('usemin:post:css', function(content) {
grunt.log.writeln('Update the CSS with new img filenames');
content = grunt.helper('replace', content, /url\(\s*['"]([^"']+)["']\s*\)/gm);
return content;
});
// usemin:post:* are the global replace handlers, they delegate the regexp
// replace to the replace helper.
grunt.registerHelper('usemin:post:html', function(content) {
grunt.log.verbose.writeln('Update the HTML to reference our concat/min/revved script files');
content = grunt.helper('replace', content, /]?><[\\]?\/script>/gm);
content = grunt.helper('replace', content, /]?><[\\]?\/script>/gm);
if (grunt.config('rjs.almond')) {
content = content.replace(/]?><[\\]?\/script>/gm, function(match, src) {
var res = match.replace(/\s*src=['"].*["']/gm, '').replace('data-main', 'src');
grunt.log.ok('almond')
.writeln('was ' + match)
.writeln('now ' + res);
return res;
});
}
grunt.log.verbose.writeln('Update the HTML with the new css filenames');
content = grunt.helper('replace', content, //gm);
grunt.log.verbose.writeln('Update the HTML with the new img filenames');
content = grunt.helper('replace', content, /]+src=['"]([^"']+)["']/gm);
grunt.log.verbose.writeln('Update the HTML with background imgs, case there is some inline style');
content = grunt.helper('replace', content, /url\(\s*['"]([^"']+)["']\s*\)/gm);
return content;
});
grunt.registerHelper('usemin:post:css', function(content) {
grunt.log.verbose.writeln('Update the CSS with background imgs, case there is some inline style');
content = grunt.helper('replace', content, /url\(\s*['"]?([^'"\)]+)['"]?\s*\)/gm);
return content;
});
//
// global replace handler, takes a file content a regexp to macth with. The
// regexp should capture the assets relative filepath, it is then compared to
// the list of files on the filesystem to guess the actual revision of a file
//
grunt.registerHelper('replace', function(content, regexp) {
return content.replace(regexp, function(match, src) {
//do not touch external files
if(src.match(/\/\//)) return match;
var basename = path.basename(src);
var dirname = path.dirname(src);
// XXX files won't change, the filepath should filter the original list
// of cached files.
var filepath = grunt.file.expand(path.join('**/*') + basename)[0];
// not a file in intermediate, skip it
if(!filepath) return match;
var filename = path.basename(filepath);
// handle the relative prefix (with always unix like path even on win32)
filename = [dirname, filename].join('/');
// if file not exists probaly was concatenated into another file so skip it
if(!filename) return '';
var res = match.replace(src, filename);
// output some verbose info on what get replaced
grunt.log
.ok(src)
.writeln('was ' + match)
.writeln('now ' + res);
return res;
});
});
};
//
// Helpers: todo, register as grunt helper
//
// start build pattern -->
var regbuild = //;
// end build pattern --
var regend = //;
//
// Returns an hash object of all the directives for the given html. Results is
// of the following form:
//
// {
// 'css/site.css ':[
// ' ',
// ' ',
// ' '
// ],
// 'js/head.js ': [
// ' ',
// ' ',
// ' '
// ],
// 'js/site.js ': [
// ' ',
// ' ',
// ' ',
// ' '
// ]
// }
//
function getBlocks(body) {
var lines = body.replace(/\r\n/g, '\n').split(/\n/),
block = false,
sections = {},
last;
lines.forEach(function(l) {
var build = l.match(regbuild),
endbuild = regend.test(l);
if(build) {
block = true;
sections[[build[1], build[2].trim()].join(':')] = last = [];
}
// switch back block flag when endbuild
if(block && endbuild) {
last.push(l);
block = false;
}
if(block && last) {
last.push(l);
}
});
return sections;
}