Created
March 31, 2017 10:49
-
-
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...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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