css2spritesmith
Version:
A plugin to help front engineer creating css sprite.
614 lines (495 loc) • 20.4 kB
JavaScript
// Deps
var fs = require('fs');
var path = require('path');
var async = require('async');
var assert = require('assert');
var spritesmith = require('spritesmith');
var imageSetSpriteCreator = require('./imageSetSpriteCreator');
var CSS_DATA_TMPL = '\n\n/* {imgDest} */\n{selectors}{\n{cssProps}\n}\n';
var MEDIA_QUERY_CSS_TMPL = '\n\n/* {imgDest} */\n@media only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 240dpi), only screen and (min-resolution: 2dppx) {\n{cssText} \n}\n';
var IMAGE_SET_CSS_TMPL = 'background-image: -webkit-image-set(url({spriteImg}) 1x, url({retinaSpriteImg}) 2x); background-image: -moz-image-set(url({spriteImg}) 1x, url({retinaSpriteImg}) 2x); background-image: -ms-image-set(url({spriteImg}) 1x, url({retinaSpriteImg}) 2x); background-image: image-set(url({spriteImg}) 1x, url({retinaSpriteImg}) 2x);';
var defaultOptions = {
// css file
cssfile: null,
// 编码,默认 String|Null default = 'utf8'
encoding: null,
//服务器根目录,用来定位绝对路径的图片引用
staticPath: '',
// sprite背景图源文件夹,只有匹配此路径才会处理,默认 images/slice/
imagepath: 'images/slice/',
// 映射CSS中背景路径,支持函数和数组,默认为 null
imagepath_map: null,
// 雪碧图输出目录,注意,会覆盖之前文件!默认 images/
spritedest: 'images/',
// 替换后的背景路径,默认 ../images/
spritepath: '../images/',
// 默认雪碧图名字会是所在css的名字+nameExtend,例如a.css, nameExtend:'-sprite' ——> a-sprite.png
nameExtend: '',
// 各图片间间距,如果设置为奇数,会强制+1以保证生成的2x图片为偶数宽高,默认 0
padding: 0,
// 是否使用 image-set 作为2x图片实现,默认不使用
useimageset: false,
// 是否以时间戳为文件名生成新的雪碧图文件,如果启用请注意清理之前生成的文件,默认不生成新文件
newsprite: false,
// 给雪碧图追加时间戳,默认不追加
spritestamp: false,
// 在CSS文件末尾追加时间戳,默认不追加
cssstamp: false,
// 默认使用二叉树最优排列算法
algorithm: 'binary-tree',
// 默认使用`pixelsmith`图像处理引擎
engine: 'pixelsmith',
// 扩展参数,不建议修改,image-set 模板,占位文件
IMAGE_SET_CSS_TMPL: IMAGE_SET_CSS_TMPL,
// 扩展参数,不建议修改, 配置模板
MEDIA_QUERY_CSS_TMPL: MEDIA_QUERY_CSS_TMPL,
CSS_DATA_TMPL: CSS_DATA_TMPL
};
function fixPath(path) {
return String(path).replace(/\\/g, '/').replace(/\/$/, '');
}
function fill(tmpl, data) {
for(var k in data) {
tmpl = tmpl.replace(new RegExp('\\{'+ k +'\\}', 'g'), data[k]);
}
return tmpl;
}
function escapeRegExp(str) {
var rreEscape = /[-\/\\^$*+?.()|[\]{}]/g;
return str.replace(rreEscape, '\\$&');
}
function createPlace() {
if(!createPlace.id) {
createPlace.id = 0;
}
var id = createPlace.id++;
id = '!$sprite_place_'+ id +'$!';
var pattern = new RegExp(escapeRegExp(id), 'g');
return {
id: id,
pattern: pattern
};
}
function filterNullFn(v) {
return !!v;
}
function uniqueCssSelectors(selectors) {
var str = selectors.filter(filterNullFn)
.join(',');
var selectorHash = {};
var retSelectors = [];
str.split(',').forEach(function(s) {
s = s.trim();
if(s && !selectorHash[s]) {
selectorHash[s] = true;
retSelectors.push(s);
}
});
return retSelectors;
}
function getBgPosCss(x, y, ratio) {
if(ratio && ratio > 1) {
x /= ratio;
y /= ratio;
}
var bgPos = ['-'+ x +'px', '-'+ y +'px'];
if(x === 0) {
bgPos[0] = 0;
}
if(y === 0) {
bgPos[1] = 0;
}
return 'background-position: '+ bgPos.join(' ') +';';
}
function padNum(str, len) {
var s = new Array(len + 1).join('0');
return (s + str).slice(-len);
}
function getTimeStamp(date) {
date = date || new Date();
return [
date.getFullYear(),
padNum(date.getMonth() + 1, 2),
padNum(date.getDate(), 2),
padNum(date.getHours(), 2),
padNum(date.getMinutes(), 2),
padNum(date.getSeconds(), 2)
].join('');
}
// 遍历css文件,收集切片数据
function getSliceData(src, options) {
var cssPath = path.resolve(path.dirname(src));
var cssData = fs.readFileSync(src, {
encoding: options.encoding
})
.toString();
var rurlParams = /\?.*$/;
var rabsUrl = /^(?:https?:|file:)/i; // 移除对“/”起头的绝对路径的排除
var rabsStaticUrl = /^(?:\/)/i;
var rbgs = /\bbackground(?:-image)?\s*:[^;]*?url\((["\']?)([^\)]+)\1\)[^};]*;?/ig;
// ignore comments 注释
var commentsData = {};
var commentRe = /\/\*[\s\S]*?\*\//g;
cssData = cssData.replace(commentRe, function(a) {
var place = createPlace();
commentsData[place.id] = {
place: place,
data: a
};
return place.id;
});
var staticPath = fixPath(options.staticPath); // 静态服务器路径
var slicePathMap = options.imagepath_map;
var slicePath = path.resolve(fixPath(options.imagepath));
// parse css data
var cssList = [], cssHash = {}, cssInx = -1;
var imgList = [], imgHash = {}, imgInx = -1;
cssData = cssData.replace(rbgs, function(css, b, uri) {
var imgUri = uri;
if(typeof slicePathMap === 'function') {
imgUri = slicePathMap(imgUri);
}
// absolute path
if(rabsUrl.test(imgUri)) {
return css;
}
var imgFullPath = imgUri.replace(rurlParams, '');
//绝对路径 / 开头,按照相对于服务器根目录 staticPath处理
if (rabsStaticUrl.test(imgUri)) {
imgFullPath = fixPath(path.join(staticPath, imgFullPath));
//相对路径
} else {
imgFullPath = fixPath(path.join(cssPath, imgFullPath));
}
imgFullPath = path.normalize(imgFullPath);
if(
// low call file.exists
!imgHash[imgFullPath] &&
// match path
(imgFullPath.indexOf(slicePath) !== 0 || !fs.existsSync(imgFullPath))
) {
return css;
}
var place = createPlace();
cssList[++cssInx] = {
place: place,
imgFullPath: imgFullPath,
imgPath: uri,
css: css
};
if(!imgHash[imgFullPath]) {
imgList[++imgInx] = imgFullPath;
imgHash[imgFullPath] = true;
}
return place.id;
});
return {
commentsData: commentsData,
cssData: cssData,
cssList: cssList,
cssHash: cssHash,
imgList: imgList,
imgHash: imgHash
};
}
function createSprite(list, options, callback) {
spritesmith({
algorithm: options.algorithm,
padding: options.padding,
engine: options.engine,
src: list
}, callback);
}
function cssSpriteSmith(options, callback) {
options = options || {};
for(var k in defaultOptions) {
if(options[k] === undefined) {
options[k] = defaultOptions[k];
}
}
// src css路径
var src = options.cssfile;
assert(src, 'An `cssfile` parameter was not provided');
assert(options.imagepath, 'An `imagepath` parameter was not provided');
if(!fs.existsSync(src)) {
throw new Error(src + ' not exists');
}
// `padding` must be even
if(options.padding % 2 !== 0){
options.padding += 1;
}
// imagepath map
var _imagepath_map = options.imagepath_map;
if(Array.isArray(options.imagepath_map)) {
options.imagepath_map = function(uri) {
return String(uri).replace(_imagepath_map[0], _imagepath_map[1]);
};
}
var sliceData = getSliceData(src, options);
var cssList = sliceData.cssList;
if(!cssList || !cssList.length) {
return callback(null, {
cssData: null
});
}
async.waterfall([
// base config
function baseConfig(cb) {
var cssFilename = path.basename(src, '.css');
var timeNow = getTimeStamp();
if(options.newsprite) {
cssFilename += '-' + timeNow;
}
sliceData.timestamp = options.spritestamp ? timeNow : '';
sliceData.timestamp = options.spritestamp ? ('?'+timeNow) : '';
sliceData.imgDest = fixPath(path.join(options.spritedest, cssFilename + options.nameExtend + '.png'));
sliceData.spriteImg = fixPath(path.join(options.spritepath, cssFilename + options.nameExtend + '.png')) +
sliceData.timestamp;
sliceData.retinaImgDest = fixPath(sliceData.imgDest.replace(/\.png$/, '@2x.png'));
sliceData.retinaSpriteImg = fixPath(path.join(options.spritepath, cssFilename + options.nameExtend + '@2x.png')) + sliceData.timestamp;
cb(null);
},
// create sprite image
function createSpriteImg(cb) {
createSprite(sliceData.imgList, options, cb);
},
// process sprite data
function processSpriteData(spriteData, cb) {
sliceData.spriteData = spriteData;
cb(null, spriteData.coordinates);
},
// set slice position
function setSlicePosition(coordinates, cb) {
var rspaces = /\s+/;
var rsemicolon = /;\s*$/;
var rbgUrl = /url\([^\)]+\)/i;
var rbgEmpty = /background(?:-image)?\s*:\s*;/;
sliceData.cssList.forEach(function(cssItem) {
var coords = coordinates[cssItem.imgFullPath];
var css = cssItem.css.replace(rbgUrl, '');
// Add a semicolon if needed
if(!rsemicolon.test(css)) {
css += ';';
}
if(!rbgEmpty.test(css)) {
css = css.replace(rspaces, ' ');
}
else {
css = '';
}
css += getBgPosCss(coords.x, coords.y);
cssItem.newCss = css;
});
cb(null);
},
// get retina image & add image-set, css
function getRetinaImg(cb) {
var useimageset = options.useimageset;
var retinaImgList = sliceData.retinaImgList = [];
var retinaImgHash = sliceData.retinaImgHash = {};
sliceData.cssList.forEach(function(cssItem, id) {
var extName = path.extname(cssItem.imgFullPath);
var filename = path.basename(cssItem.imgFullPath, extName);
var retinaImgFullPath = path.join(path.dirname(cssItem.imgFullPath), filename + '@2x' + extName);
if(fs.existsSync(retinaImgFullPath)) {
cssItem.retinaImgFullPath = retinaImgFullPath;
if(!retinaImgHash[retinaImgFullPath]) {
retinaImgList.push(retinaImgFullPath);
}
retinaImgHash[retinaImgFullPath] = true;
}
});
if(!retinaImgList.length) {
cb(null, null);
return;
}
var oldAlgorithm = options.algorithm;
if(useimageset) {
var spriteData = sliceData.spriteData;
var coordinates = spriteData.coordinates;
imageSetSpriteCreator.set2xData({
height: spriteData.properties.height,
width: spriteData.properties.width,
coordinates: coordinates,
list: retinaImgList
});
options.algorithm = 'xy-2x';
}
createSprite(retinaImgList, options, function() {
options.algorithm = oldAlgorithm;
cb.apply(null, arguments);
});
},
// process retina sprite data
function processRetinaSpriteData(retinaSpriteData, cb) {
if(retinaSpriteData) {
sliceData.retinaSpriteData = retinaSpriteData;
}
cb(null);
},
// get selectors
function getSelectors(cb) {
var cssData = sliceData.cssData;
// a[href*='}{']::after{ content:'}{';} 规避此类奇葩CSS
var tmpCss = cssData.replace(/[:=]\s*([\'\"]).*?\1/g, function(a){
return a.replace(/\}/g, '');
});
sliceData.cssList.forEach(function(cssItem) {
var place = cssItem.place;
var placeEscapeId = escapeRegExp(place.id);
var rselector = new RegExp('([^}\\/!]+)\\{[^\\}]*?' + placeEscapeId);
var selector;
tmpCss = tmpCss.replace(rselector, function(a, b) {
selector = b.trim();
return b + '{';
});
if(!selector) {
return;
}
cssItem.selector = selector;
});
cb(null);
},
// processCss
function processCss(cb) {
var cssData = sliceData.cssData;
var useImageSet = options.useimageset;
var retinaSpriteData = sliceData.retinaSpriteData;
var retinaCoordinates = retinaSpriteData ? retinaSpriteData.coordinates : {};
var lastIndex = -1;
var cssSelectors = [];
var cssSelectorsHash = {};
var retinaIndex = -1;
var retinaCssProps = [];
var retinaSelectors = [];
var retinaSelectorsHash = {};
sliceData.cssList.forEach(function(cssItem) {
var place = cssItem.place;
var selector = cssItem.selector;
cssData = cssData.replace(place.pattern, cssItem.newCss);
var inx = cssSelectorsHash[selector];
if(inx !== undefined) {
cssSelectors[inx] = null;
}
inx = ++lastIndex;
cssSelectors[inx] = selector;
cssSelectorsHash[selector] = inx;
// for media query
var retinaCoords;
var retinaImgFullPath = cssItem.retinaImgFullPath;
if(!useImageSet && retinaImgFullPath) {
retinaCoords = retinaCoordinates[retinaImgFullPath];
}
if(!retinaCoords) {
return;
}
inx = retinaSelectorsHash[selector];
if(inx !== undefined) {
retinaSelectors[inx] = null;
retinaCssProps[inx] = null;
}
inx = ++retinaIndex;
retinaSelectors[inx] = selector;
retinaSelectorsHash[selector] = inx;
var css = selector + '{\n'+ getBgPosCss(retinaCoords.x, retinaCoords.y, 2) +'\n}';
retinaCssProps[inx] = css;
});
sliceData.cssSelectors = uniqueCssSelectors(cssSelectors);
if(!useImageSet && retinaSelectors.length) {
sliceData.retinaCssProps = retinaCssProps.filter(filterNullFn);
sliceData.retinaSelectors = uniqueCssSelectors(retinaSelectors);
}
cb(null, cssData);
},
// buildCss
function buildCss(cssData, cb) {
var useImageSet = options.useimageset;
var nameExtend = options.nameExtend;
var spriteImg = sliceData.spriteImg;
var cssSelectors = sliceData.cssSelectors;
var retinaCssProps = sliceData.retinaCssProps;
var retinaSelectors = sliceData.retinaSelectors;
var retinaSpriteData = sliceData.retinaSpriteData;
var retinaSpriteImg = sliceData.retinaSpriteImg;
var nameExtendSpriteImg = spriteImg;
var nameExtendretinaSpriteImg = retinaSpriteImg;
var css = 'background-image: url('+ nameExtendSpriteImg +');';
// IE8 does not support unknow pseudo-element
// and it will broken selectors
var rNotSupportPseudo = /:(?:checked|disabled|empty)/i;
var isolatedSelectors = [];
for(var selector, i=cssSelectors.length-1; i>=0; --i) {
selector = cssSelectors[i];
if(rNotSupportPseudo.test(selector)) {
cssSelectors.splice(i, 1);
isolatedSelectors.unshift(selector);
}
}
if(useImageSet) {
css += '\n';
css += fill(options.IMAGE_SET_CSS_TMPL, {
retinaSpriteImg: retinaSpriteImg,
spriteImg: nameExtendSpriteImg
});
}
cssData += fill(options.CSS_DATA_TMPL, {
selectors: cssSelectors.join(',\n'),
imgDest: nameExtendSpriteImg,
cssProps: css
});
// isolatedSelectors, for IE8
if(isolatedSelectors.length) {
cssData += fill(options.CSS_DATA_TMPL, {
selectors: isolatedSelectors.join(',\n'),
imgDest: nameExtendSpriteImg,
cssProps: css
});
}
// media query css
if(!useImageSet && retinaSelectors && retinaSelectors.length) {
var retinaBgWidth = Math.floor(retinaSpriteData.properties.width / 2);
css = retinaSelectors.join(',\n') + '{\n';
css += 'background-image: url('+ nameExtendretinaSpriteImg +');\n';
css += 'background-size: '+ retinaBgWidth +'px auto;';
css += '\n}\n';
css += retinaCssProps.join('\n');
cssData += fill(options.MEDIA_QUERY_CSS_TMPL, {
imgDest: nameExtendretinaSpriteImg,
cssText: css
});
}
cb(null, cssData);
},
// restore comments
function restoreComments(cssData, cb) {
var commentsData = sliceData.commentsData;
Object.keys(commentsData).forEach(function(k) {
var commentsItem = commentsData[k];
var place = commentsItem.place;
cssData = cssData.replace(place.pattern, commentsItem.data);
});
cb(null, cssData);
},
// process result
function processResult(cssData, cb) {
// timestamp
if(options.cssstamp) {
cssData += '\n.css_stamp{ content:"'+ sliceData.timestamp.slice(1) +'";}';
}
// path info
var spriteData = sliceData.spriteData;
var retinaSpriteData = sliceData.retinaSpriteData;
spriteData.imagePath = sliceData.imgDest;
if(retinaSpriteData) {
retinaSpriteData.imagePath = sliceData.retinaImgDest;
}
cb(null, {
cssData: cssData,
spriteData: spriteData,
retinaSpriteData: retinaSpriteData
});
}
], callback);
}
cssSpriteSmith.defaultOptions = defaultOptions;
module.exports = cssSpriteSmith;