Skip to content

Instantly share code, notes, and snippets.

@yisibl

yisibl/cssgrace.js

Last active Aug 29, 2015
Embed
What would you like to do?
CSSGrace
/**
cssgrace.pack(css)
*/
var path = require('path');
var http = require('http');
var url = require('url');
var postcss = require('postcss');
var sizeOf = require('image-size');
var reVALUE = /([\.0-9]+)(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|dpi|dpcm|dppx|fr)/i;
var reRGBA = /rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+\s*)/gi;
var reALL_PSEUDO = /::(before|after|first-line|first-letter)/gi;
var reBEFORE_AFTER = /::|:(before|after)/gi;
var reURL = /url\s*\(\s*(['"]?)([^\)'"]+)\1\s*\)\s+(\dx)/gi;
var reNO_SETURL = /url\(\s*(['"]?)([^\)'"]+)\1\s*\)/gi;
var reIMAGE_SIZE = /([-]?)((image-width)|(image-height))/i;
var reBLANK_LINE = /(\r\n|\n|\r)(\s*?\1)+/gm;
/**
* Remove display: block
* If float: left|right & position: absolute|fixed => remove display: block;
input:
.foo{position: absolute; display: block;}
output:
.foo{position: absolute;}
^
*/
var remove_display = function(decl) {
if (
((decl.prop == 'position') && (decl.value == 'absolute' || decl.value == 'fixed')) ||
(decl.prop == 'float' && decl.value != 'none')
) {
// Remove display
decl.parent.each(function(neighbor) {
if (neighbor.prop == 'display' && neighbor.value != 'none') {
//Remove
neighbor.removeSelf();
}
});
}
}
/**
* Remove float
* If position: absolute|fixed or display: flex, remove float
input:
.foo{position: absolute; float: right;}
output:
.foo{position: absolute;}
^
*/
var remove_float = function(decl) {
if (
((decl.prop == 'position') && (decl.value == 'absolute' || decl.value == 'fixed')) ||
((decl.prop == 'display') && (decl.value == 'flex'))
) {
decl.parent.each(function(neighbor) {
if (
(neighbor.prop == 'float')
) {
neighbor.removeSelf();
}
});
}
}
//Remove the colons
/**
input:
.foo::after{display: block;}
output:
.foo:after{display: block;}
^
*/
var remove_colons = function(rule, i) {
if (rule.selector.match(reALL_PSEUDO)) {
rule.selector = rule.selector.replace(/::/g, ':');
}
}
// Add content: ""
/**
input:
.foo:after{display: block;}
output:
.foo:after{content: ''; display: block;}
^
*/
var add_content = function(rule, i) {
if (rule.selector.match(reBEFORE_AFTER)) {
var hasContent = rule.some(function(i) {
return i.prop == 'content';
});
if (!hasContent) {
// Add content: ""
rule.prepend({
prop: 'content',
value: "''"
});
}
}
}
// position: center mixin
/**
input:
.foo{position: center; width: 200px; height: 100px;}
output:
.foo{position: absolute; width: 200px; height: 100px; left: 50%; right: 50%; margin-left: -100px; margin-top: -50px;}
^---->
*/
function position_center_mixin(decl, i) {
var hasPosition = decl.parent.some(function(i) {
return i.prop == 'position' && i.value == 'center';
});
var hasWidth = decl.parent.some(function(i) {
return i.prop == 'width';
});
var hasHeight = decl.parent.some(function(i) {
return i.prop == 'height';
});
if (hasPosition && hasWidth && hasHeight) {
var widthValue, heightValue;
if (decl.prop == 'position') {
decl.value = 'absolute';
decl.parent.eachDecl(function(decl) {
if (decl.prop == 'width') {
matchWidth = decl.value.match(reVALUE);
if (matchWidth && matchWidth != null) {
// console.log('matchW', matchWidth)
widthValue = (-matchWidth[1] / 2) + matchWidth[2];
}
}
if (decl.prop == 'height') {
matchHeight = decl.value.match(reVALUE);
if (matchHeight != null) {
heightValue = (-matchHeight[1] / 2) + matchHeight[2];
}
}
});
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
insertDecl(decl, i, {
before: reBefore,
prop: 'margin-left',
value: widthValue
});
insertDecl(decl, i, {
before: reBefore,
prop: 'margin-top',
value: heightValue
});
insertDecl(decl, i, {
before: reBefore,
prop: 'top',
value: '50%'
});
insertDecl(decl, i, {
before: reBefore,
prop: 'left',
value: '50%'
});
}
}
}
/**
* ellipsis mixin
*
*/
function ellipsis_mixin(decl, i) {
// var decl = decl.parent.childs[i];
if (decl.prop == 'text-overflow' && decl.value == 'ellipsis') {
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
var count_overflow = 0,
count_whitespace = 0;
decl.parent.eachDecl(function(decl) {
// if overflow ≠ hidden, add white-space
if (decl.prop == 'overflow') {
decl.value = 'hidden';
count_overflow++;
}
if (decl.prop == 'white-space') {
decl.value = 'nowrap';
count_whitespace++;
}
});
if (count_overflow == 0 && count_whitespace == 0) {
insertDecl(decl, i, {
before: reBefore,
prop: 'overflow',
value: 'hidden'
});
insertDecl(decl, i, {
before: reBefore,
prop: 'white-space',
value: 'nowrap'
});
} else if (count_overflow == 0) {
insertDecl(decl, i, {
before: reBefore,
prop: 'overflow',
value: 'hidden'
});
} else if (count_whitespace == 0) {
insertDecl(decl, i, {
before: reBefore,
prop: 'white-space',
value: 'nowrap'
});
}
}
}
/**
* resize mixin
* Add overflow auto
*/
function resize_mixin(decl, i) {
if (decl.prop == 'resize' && decl.value !== 'none') {
var count = 0;
decl.parent.eachDecl(function(decl) {
if (decl.prop == "overflow")
count++;
});
if (count === 0) {
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
insertDecl(decl, i, {
before: reBefore,
prop: 'overflow',
value: 'auto'
});
}
}
}
/**
* clearfix mixin
* Use clear: fix
*/
function clearfix_mixin(decl, i) {
if (decl.prop == 'clear' && decl.value == 'fix') {
decl.prop = '*zoom';
decl.value = '1';
var count = 0;
decl.parent.eachDecl(function(decl) {
if (
(decl.prop == "overflow" && decl.value != 'visible') ||
(decl.prop == "display" && decl.value == 'inline-block') ||
(decl.prop == "position" && decl.value == 'absolute') ||
(decl.prop == "position" && decl.value == 'fixed')
) {
count++;
}
});
if (count === 0) {
var bothSelector = '\n' + decl.parent.selector + ':before' + ',\n' + decl.parent.selector + ':after';
var afterSelector = '\n' + decl.parent.selector + ':after';
var bothRule = postcss.rule({
selector: bothSelector
});
var afterRule = postcss.rule({
selector: afterSelector
});
decl.parent.parent.insertAfter(decl.parent, bothRule);
decl.parent.parent.insertAfter(decl.parent, afterRule);
bothRule.append({
prop: 'content',
value: "''"
}).append({
prop: 'display',
value: 'table'
});
afterRule.append({
prop: 'clear',
value: 'both'
});
} else {
if (decl.parent.childs[i + 1] && decl.parent.childs[i + 1].type == "comment") {
decl.parent.childs[i + 1].removeSelf();
}
decl.removeSelf();
}
}
}
/**
* IE opacity hack
* Transform to IE filter
input:
.foo{opacity: .5;}
output:
.foo{opacity: .5; filter: alpha(opacity=50);}
*/
function ie_opacity_hack(decl, i) {
var amount = Math.round(decl.value * 100);
if (decl.prop == 'opacity') {
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
insertDecl(decl, i, {
before: reBefore,
prop: 'filter',
value: 'alpha(opacity=' + amount + ')'
});
}
}
/**
* IE rgba hack
* background rgba transform to IE ARGB
input:
.foo{
background: rgba(8, 22, 123, .8);
}
output:
.foo{
background: rgba(8, 22, 123, .8);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#cc08167b', endColorstr='#cc08167b');
}
:root .foo {
filter: none\9;
}
*/
}
function ie_rgba_hack(decl, i) {
function pad(str) {
return str.length == 1 ? '0' + str : '' + str;
}
if ((decl.prop == 'background' || decl.prop == 'background-color') &&
decl.value.match(reRGBA)) {
// rgba transform to AARRGGBB
var colorR = pad(parseInt(RegExp.$1).toString(16));
var colorG = pad(parseInt(RegExp.$2).toString(16));
var colorB = pad(parseInt(RegExp.$3).toString(16));
var colorA = pad(parseInt(RegExp.$4 * 255).toString(16));
var ARGB = "'" + "#" + colorA + colorR + colorG + colorB + "'";
// add filter
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
insertDecl(decl, i, {
before: reBefore,
prop: 'filter',
value: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=' + ARGB + ', endColorstr=' + ARGB + ')'
});
// add :root hack
var newSelector = ':root ' + decl.parent.selector;
var ieHack = '\\9;' + decl.parent.after;
var nextrule = postcss.rule({
selector: newSelector,
after: ieHack
});
decl.parent.parent.insertAfter(decl.parent, nextrule);
nextrule.append({
prop: 'filter',
value: 'none'
});
}
}
// IE 6/7 inline-block
function ie_inline_block_hack(decl, i) {
if (decl.prop == 'display' && decl.value == 'inline-block') {
// var decl = decl.parent.childs[i];
var reBefore = decl.before.replace(reBLANK_LINE, '$1')
insertDecl(decl, i, {
before: reBefore,
prop: '*zoom',
value: 1
});
insertDecl(decl, i, {
before: reBefore,
prop: '*display',
value: 'inline'
});
}
}
/**
* 1x.jpg width= 200px height= 100px
input:
.foo{
background: -webkit-image-set(url(./images/1x.jpg) 1x, url(./images/2x.jpg) 2x);
width: image-width;
height: image-height;
}
output:
.foo{
background-image: url(./images/1x.png);
background: -webkit-image-set(url(./images/1x.jpg) 1x, url(./images/2x.jpg) 2x);
width: 200px;
height: 100px;
}
@media
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min--moz-device-pixel-ratio: 2),
only screen and (-moz-min-device-pixel-ratio: 2),
only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
.foo {
background-image: url(./images/2x.jpg);
background-size: 200px 100px;
}
}
*/
function image_set_mixin(decl, i) {
/**
* Retina display background image
* 1x = 1dppx = 96dpi ≈0.39dpcm
* 1dpcm ≈ 2.54dpi
*/
// var reURL = /url\s*\(\s*(['"]?)([^\)'"]+)\1\s*\)\s+[\d]+|(x|dpi|dppx|dpcm)/gi;
if (decl.prop == 'background' || decl.prop == 'background-image') {
if (decl.value.indexOf('image-set(') != -1) {
var paths = returnURL(decl.value, reURL);
var obj = {};
for (var j = 0; j < paths.length; j++) {
obj.url = paths[j][2];
obj.path = "url(" + obj.url + ")";
obj.whichImg = paths[j][3];
if (obj.whichImg == "1x" || obj.whichImg == "1x") {
var normalSizes = sizeOf(path.join(process.cwd(), obj.url));
normalWidth = normalSizes.width + 'px';
normalHeight = normalSizes.height + 'px';
decl.parent.insertBefore(i, {
prop: 'background-image',
value: obj.path
});
} else if (obj.whichImg == "2x" || obj.whichImg == "2x") {
var rSizes = sizeOf(path.join(process.cwd(), obj.url)); //2x image size
rWidth = rSizes.width / 2 + 'px';
rHeight = rSizes.height / 2 + 'px';
bgSize = rWidth + ' ' + rHeight;
// add hack
var atRuleObj = {};
atRuleObj.name = "media";
var new_params = [
'\n only screen and (-o-min-device-pixel-ratio: 2/1)',
'\n only screen and (min--moz-device-pixel-ratio: 2)',
'\n only screen and (-moz-min-device-pixel-ratio: 2)',
'\n only screen and (-webkit-min-device-pixel-ratio: 2)',
'\n only screen and (min-resolution: 192dpi)',
'\n only screen and (min-resolution: 2dppx)'
];
var atRule = postcss.atRule({
name: 'media',
params: new_params
});
var atBefore = decl.parent.before;
var nextrule = postcss.rule({
selector: decl.parent.selector,
after: decl.parent.after,
// before: atBefore
});
//add @
nextrule.append({
prop: 'background-image',
value: obj.path
}).append({
prop: 'background-size',
value: bgSize
});
atRule.append(nextrule);
decl.parent.parent.insertAfter(decl.parent, atRule);
}
}
} else if (decl.value.indexOf('url(') != -1) {
//if image-set == -1
var paths2 = returnURL(decl.value, reNO_SETURL) //获取第一个url图片的路径
var normalSizes2 = sizeOf(path.join(process.cwd(), paths2[0][2]));
normalWidth = normalSizes2.width + 'px';
normalHeight = normalSizes2.height + 'px';
}
}
/**
* Get images size
.foo{
background: url(images/foo.png);
width: image-width;
height: image-height;
}
*/
if (decl.prop != 'content' && (/image-width/gi).test(decl.value) || (/image-height/gi).test(decl.value)) {
decl.value = decl.value.replace(/image-width/gi, normalWidth).replace(/image-height/gi, normalHeight);
}
}
//Keep the comments on the current line
// Here we need better API
function insertDecl(decl, i, newDecl) {
var next = decl.parent.childs[i + 1],
decl_after;
if (next && next.type == 'comment' && next.before.indexOf('\n') == -1) {
decl_after = next;
} else {
decl_after = decl;
}
decl.parent.insertAfter(decl_after, newDecl)
}
var cssgraceRule = function(rule, i) {
var normalWidth = "",
normalHeight = ""; //1x images;
remove_colons(rule, i);
add_content(rule, i);
rule.eachDecl(function(decl, i) {
remove_display(decl, i);
ie_inline_block_hack(decl, i);
ie_opacity_hack(decl, i);
ie_rgba_hack(decl, i);
resize_mixin(decl, i);
ellipsis_mixin(decl, i);
clearfix_mixin(decl, i);
image_set_mixin(decl, i);
});
rule.eachDecl(function(decl, i) {
position_center_mixin(decl, i);
remove_float(decl, i);
remove_display(decl, i);
})
};
function returnURL(val, reg) {
var result, paths = []
while ((result = reg.exec(val)) != null) {
paths.push(result);
}
return paths;
}
// PostCSS Processor
// var cssgrace = function(css) {
// return postcss(function(css) {
// css.eachRule(cssgraceRule);
// }).process(css);
// };
// module.exports = cssgrace;
var cssprocess = function(css) {
css.eachRule(cssgraceRule);
}
var pack = function(css, opts) {
return postcss().use(this.processor).process(css, opts);
}
exports.postcss = cssprocess
exports.cssgrace = pack
exports.pack = pack
exports.processor = cssprocess
@yisibl

This comment has been minimized.

Copy link
Owner Author

@yisibl yisibl commented Dec 30, 2014

About comments problems

Normal

input:

.bar/**/ {
  display: block;
}

output:

.bar /**/{
  /*     ^*/
  display: block;
}

Use ie_rgba_hack function

In the first brace

input:

.bar {/**/
  background: rgba(255, 221, 153, 0.8);
}

output:

.bar {/**/
  background: rgba(255, 221, 153, 0.8);
}
:root .bar {
filter: none;\9;
}

input:

.bar {
  /**/background: rgba(255, 221, 153, 0.8);
}

output:

.bar {
  /**/background: rgba(255, 221, 153, 0.8);
}
:root .bar {filter: none;\9;
}

In the last brace

input:

.bar/**/ {
  background: rgba(255, 221, 153, 0.8);/**/
}/* last comment*/

output:

.bar /**/{
  background: rgba(255, 221, 153, 0.8);/**/
}
:root .bar /**/{
  filter: none\9;
}/* last comment*/

Expected output

.bar {/**/
  /**/background: rgba(255, 221, 153, 0.8);/**/
}/* last comment */

:root .bar {
  filter: none\9;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment