Skip to content

Instantly share code, notes, and snippets.

@Clarence-pan
Created March 31, 2017 10:49
Show Gist options
  • Save Clarence-pan/17798221f90ef6014a7adf1b7c55fc93 to your computer and use it in GitHub Desktop.
Save Clarence-pan/17798221f90ef6014a7adf1b7c55fc93 to your computer and use it in GitHub Desktop.
Make gulp-css-base64 run async... Optimized str.replace, fs.readFileSync, url.split...
// Make gulp-css-base64 run async...
// Inspired by gulp-css-base64
// Optimized str.replace, fs.readFileSync, url.split...
// fixed bug: global regex should not be used concurrently.
// NodeJS library
var fs = require('fs');
var path = require('path');
var mime = require('mime');
var util = require('util');
var Stream = require('stream').Stream;
// NPM library
var gutil = require('gulp-util');
var through = require('through2');
var request = require('request');
var buffers = require('buffers');
var async = require('async');
var chalk = require('chalk');
var rImages = /url(?:\(['|"]?)(.*?)(?:['|"]?\))(?!.*\/\*base64:skip\*\/)/ig;
function gulpCssBase64(opts) {
var ignoreCssFiles = opts.ignoreCssFiles
var ignoreImages = opts.ignoreImages
opts = JSON.parse(JSON.stringify(opts || {}));
opts.maxWeightResource = opts.maxWeightResource || 32768;
opts.extensionsAllowed = opts.extensionsAllowed || [];
opts.ignoreCssFiles = ignoreCssFiles || [];
opts.ignoreImages = ignoreImages || [];
opts.baseDir = opts.baseDir || '';
opts.verbose = opts.verbose || process.argv.indexOf('--verbose') >= 0;
// Creating a stream through which each file will pass
// returning the file stream
return through.obj(function (file, enc, callbackStream) {
var currentStream = this;
var cache = [];
//console.log("gulpCssBase64: " + file.path, file)
if (file.isNull()) {
// Do nothing if no contents
currentStream.push(file);
return callbackStream();
}
if (file.isBuffer()) {
// check ignores
if (file.path && doesStrMatch(file.path, opts.ignoreCssFiles)){
opts.verbose && log("Ignore css file: " + file.path + " patterns: ", opts.ignoreCssFiles);
currentStream.push(file);
return callbackStream();
}
var src = file.contents.toString();
var rawUrls = [];
scanRegex(rImages, src, function (whole, rawUrl) {
rawUrls.push(rawUrl);
});
var index = -1;
// console.log("gulpCssBase64: src: " + src)
// console.log("gulpCssBase64: " + file.path + " got raw urls: ", rawUrls)
// collect all replacements
async.whilst(
function () {
index++;
return index < rawUrls.length;
},
function (next) {
var rawUrl = rawUrls[index];
if (cache[rawUrl]) {
next();
return;
}
// console.log("gulpCssBase64: got raw url: " + rawUrl)
var pureUrl = leftOf(leftOf(rawUrl, '?'), '#');
if (opts.extensionsAllowed.length !== 0 && opts.extensionsAllowed.indexOf(path.extname(pureUrl)) === -1) {
opts.verbose && log('Ignores ' + chalk.yellow(rawUrl) + ', extension not allowed ' + chalk.yellow(path.extname(pureUrl)));
next();
return;
}
if (doesStrMatch(rawUrl, opts.ignoreImages)){
opts.verbose && log('Ignores ' + chalk.yellow(rawUrl) + ', image pattern not allowed: ' + opts.ignoreImages);
next();
return;
}
encodeResource(rawUrl, file, opts, function (fileRes) {
// console.log("encode url: ", {rawUrl, file, fileRes})
if (typeof fileRes !== 'undefined') {
if (!fileRes.isRelative && fileRes.contents.length > opts.maxWeightResource) {
opts.verbose && log('Ignores ' + chalk.yellow(rawUrl) + ', file is too big ' + chalk.yellow(fileRes.contents.length + ' bytes'));
} else {
// Store in cache
cache[rawUrl] = 'data:' + mime.lookup(fileRes.path) + ';base64,' + fileRes.contents.toString('base64');
}
}
next();
});
},
function () {
// actually replace all URLs
src = src.replace(rImages, function (whole, url) {
var dataUrl = cache[url];
if (!dataUrl) {
return whole;
}
return whole.replace(url, dataUrl);
});
file.contents = new Buffer(src);
currentStream.push(file);
return callbackStream();
}
);
}
if (file.isStream()) {
this.emit('error', new gutil.PluginError('gulp-css-base64', 'Stream not supported!'));
}
});
}
function encodeResource(img, file, opts, doneCallback) {
var fileRes = new gutil.File();
if (/^data:/.test(img)) {
opts.verbose && log('Ignores ' + chalk.yellow(img.substring(0, 30) + '...') + ', already encoded');
doneCallback();
return;
}
if (img[0] === '#') {
opts.verbose && log('Ignores ' + chalk.yellow(img.substring(0, 30) + '...') + ', SVG mask');
doneCallback();
return;
}
if (/^(http|https|\/\/)/.test(img)) {
opts.verbose && log('Fetch ' + chalk.yellow(img));
// different case for uri start '//'
if (img[0] + img[1] === '//') {
img = 'http:' + img;
}
fetchRemoteRessource(img, function (resultBuffer) {
if (resultBuffer === null) {
opts.verbose && log('Error: ' + chalk.red(img) + ', unable to fetch');
doneCallback();
} else {
fileRes.path = img;
fileRes.contents = resultBuffer;
doneCallback(fileRes);
}
});
} else {
var location = '';
var binRes = '';
// A ledding '/' means absolute path (base on opts.baseDir). Otherwise use relative path to css file
if (img.charAt(0) === '/'){
location = (opts.baseDir || '') + img
} else {
location = path.join(path.dirname(file.path), img)
fileRes.isRelative = true
}
location = location.replace(/([?#].*)$/, '');
fs.readFile(location, function (err, binRes) {
if (err) {
opts.verbose && log('Error: ' + chalk.red(location) + ' cannot read: ' + chalk.red(err + ''));
doneCallback();
} else {
fileRes.path = location;
fileRes.contents = binRes;
doneCallback(fileRes);
}
});
}
}
function fetchRemoteRessource(url, callback) {
var resultBuffer;
var buffList = buffers();
var imageStream = new Stream();
imageStream.writable = true;
imageStream.write = function (data) {
buffList.push(new Buffer(data));
};
imageStream.end = function () {
resultBuffer = buffList.toBuffer();
};
request(url, function (error, response) {
if (error) {
callback(null);
return;
}
// Bail if we get anything other than 200
if (response.statusCode !== 200) {
callback(null);
return;
}
callback(resultBuffer);
}).pipe(imageStream);
}
function log(message) {
gutil.log(message);
}
function leftOf(haystack, needle) {
var pos = haystack.indexOf(needle);
if (pos < 0) {
return haystack;
}
return haystack.substring(0, pos);
}
function scanRegex(regex, str, cb) {
var matches;
if (!regex) {
throw new Error("Only global regex can be used to scan a string!");
}
// reset the regex to avoid influence by others
regex.lastIndex = 0;
while (matches = regex.exec(str)) {
cb.apply(null, matches);
}
}
function doesStrMatch(str, patterns){
for(var i = 0, len = patterns.length; i < len; i++){
if (str.match(patterns[i])){
return true;
}
}
return false;
}
// Exporting the plugin main function
module.exports = gulpCssBase64;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment