Skip to content

Commit 26067af

Browse files
Option to remove empty rulesets. Closes #67.
1 parent f258e6c commit 26067af

6 files changed

Lines changed: 197 additions & 2 deletions

File tree

.csscomb.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"element-case": "lower",
1313
"eof-newline": true,
1414
"leading-zero": false,
15+
"remove-empty-rulesets": true,
1516
"rule-indent": true,
1617
"stick-brace": "\n",
1718
"strip-spaces": true,

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Example configuration:
4242
"exclude": ["node_modules/**"],
4343
"verbose": true,
4444

45+
"remove-empty-rulesets": true,
4546
"always-semicolon": true,
4647
"block-indent": true,
4748
"colon-space": true,
@@ -88,6 +89,14 @@ $ ./bin/csscomb ./test --verbose
8889
$ ./bin/csscomb ./test -v
8990
```
9091
92+
### remove-empty-rulesets
93+
94+
Available values: `{Boolean}` `true`
95+
96+
Example: `{ "remove-empty-rulesets": true }` - remove rulesets that have no declarations or comments.
97+
98+
`a { color: red; } p { /* hey */ } b { }` → `a { color: red; } p { /* hey */ } `
99+
91100
### always-semicolon
92101
93102
Available value: `{Boolean}` `true`

lib/csscomb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var vfs = require('vow-fs');
1111
*/
1212
var Comb = function() {
1313
this._options = [
14+
'remove-empty-rulesets',
1415
'always-semicolon',
1516
'color-case',
1617
'color-shorthand',
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
module.exports = {
2+
3+
/**
4+
* Sets handler value.
5+
*
6+
* @param {String} value Option value
7+
* @returns {Object|undefined}
8+
*/
9+
setValue: function(value) {
10+
if (value === true) {
11+
this._value = value;
12+
return this;
13+
}
14+
},
15+
16+
/**
17+
* Remove rulesets with no declarations.
18+
*
19+
* @param {String} nodeType
20+
* @param {Array} nodeContent
21+
*/
22+
process: function(nodeType, nodeContent) {
23+
if (nodeType === 'stylesheet') {
24+
this._processStylesheetContent(nodeContent);
25+
}
26+
},
27+
28+
_processStylesheetContent: function(nodeContent) {
29+
this._removeEmptyRulesets(nodeContent);
30+
this._mergeAdjacentWhitespace(nodeContent);
31+
},
32+
33+
_removeEmptyRulesets: function(nodeContent) {
34+
var i = nodeContent.length;
35+
while (i--) {
36+
if (this._isRuleset(nodeContent[i]) && this._isEmptyRuleset(nodeContent[i])) {
37+
nodeContent.splice(i, 1);
38+
}
39+
}
40+
},
41+
42+
/**
43+
* Removing ruleset nodes from tree may result in two adjacent whitespace nodes which is not correct AST:
44+
* [space, ruleset, space] => [space, space]
45+
* To ensure correctness of further processing we should merge such nodes into one.
46+
* [space, space] => [space]
47+
*/
48+
_mergeAdjacentWhitespace: function(nodeContent) {
49+
var i = nodeContent.length - 1;
50+
while (i-- > 0) {
51+
if (this._isWhitespace(nodeContent[i]) && this._isWhitespace(nodeContent[i + 1])) {
52+
nodeContent[i][1] += nodeContent[i + 1][1];
53+
nodeContent.splice(i + 1, 1);
54+
}
55+
}
56+
},
57+
58+
_isEmptyRuleset: function(ruleset) {
59+
return ruleset.filter(this._isBlock).every(this._isEmptyBlock, this);
60+
},
61+
62+
/**
63+
* Block considered empty when it has no declarations or comments.
64+
*/
65+
_isEmptyBlock: function(node) {
66+
return !node.some(this._isDeclarationOrComment);
67+
},
68+
69+
_isDeclarationOrComment: function(node) {
70+
return node[0] === 'declaration' || node[0] === 'comment';
71+
},
72+
73+
_isRuleset: function(node) {
74+
return node[0] === 'ruleset';
75+
},
76+
77+
_isBlock: function(node) {
78+
return node[0] === 'block';
79+
},
80+
81+
_isWhitespace: function(node) {
82+
return node[0] === 's';
83+
}
84+
85+
};

test/integral.origin.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ a, b, i /* foobar */ {
8080
outline: 0;
8181
padding: 0.4em 0;
8282
}
83-
}
83+
}.empty-rule{}
8484

8585
/* Фигурные скобки. Вариант 2 */
8686
div
@@ -120,7 +120,7 @@ top: 0;/* ololo */margin :0;}
120120
b
121121
{
122122
top :0/* trololo */;margin : 0}
123-
123+
.empty-rule{}
124124

125125

126126

test/remove-empty-rulesets.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
var Comb = require('../lib/csscomb');
2+
var assert = require('assert');
3+
4+
describe('options/remove-empty-rulesets', function() {
5+
var comb;
6+
7+
beforeEach(function() {
8+
comb = new Comb();
9+
});
10+
11+
describe('configured with invalid value', function() {
12+
beforeEach(function() {
13+
comb.configure({ 'remove-empty-rulesets': 'foobar' });
14+
});
15+
16+
it('should not remove empty ruleset', function() {
17+
assert.equal(comb.processString('a { width: 10px; } b {}'), 'a { width: 10px; } b {}');
18+
});
19+
});
20+
21+
describe('configured with Boolean "true" value', function() {
22+
beforeEach(function() {
23+
comb.configure({ 'remove-empty-rulesets': true });
24+
});
25+
26+
it('should remove empty ruleset', function() {
27+
assert.equal(comb.processString(' b {} '), ' ');
28+
});
29+
30+
it('should leave ruleset with declarations', function() {
31+
assert.equal(comb.processString('a { width: 10px; }\nb {} '), 'a { width: 10px; }\n ');
32+
});
33+
34+
it('should leave ruleset with comments', function() {
35+
assert.equal(comb.processString('a { /* comment */ }\nb {} '), 'a { /* comment */ }\n ');
36+
});
37+
});
38+
});
39+
40+
describe('options/remove-empty-rulesets AST manipulation', function() {
41+
var rule;
42+
var nodeContent;
43+
44+
beforeEach(function() {
45+
rule = require('../lib/options/remove-empty-rulesets.js');
46+
});
47+
48+
describe('merge adjacent whitespace', function() {
49+
it('should do nothing with empty content', function() {
50+
nodeContent = [];
51+
rule._mergeAdjacentWhitespace(nodeContent);
52+
assert.deepEqual(nodeContent, []);
53+
});
54+
55+
it('should do nothing with only one whitespace', function() {
56+
nodeContent = [['s', ' ']];
57+
rule._mergeAdjacentWhitespace(nodeContent);
58+
assert.deepEqual(nodeContent, [['s', ' ']]);
59+
});
60+
61+
it('should merge two adjacent whitespaces', function() {
62+
nodeContent = [['s', ' '], ['s', ' \n']];
63+
rule._mergeAdjacentWhitespace(nodeContent);
64+
assert.deepEqual(nodeContent, [['s', ' \n']]);
65+
});
66+
67+
it('should merge three adjacent whitespaces', function() {
68+
nodeContent = [['s', ' '], ['s', ' \n'], ['s', ' \n']];
69+
rule._mergeAdjacentWhitespace(nodeContent);
70+
assert.deepEqual(nodeContent, [['s', ' \n \n']]);
71+
});
72+
});
73+
74+
describe('remove empty rulesets', function() {
75+
it('should do nothing with empty content', function() {
76+
nodeContent = [];
77+
rule._removeEmptyRulesets(nodeContent);
78+
assert.deepEqual(nodeContent, []);
79+
});
80+
81+
it('should do nothing with no rulesets', function() {
82+
nodeContent = [['s', ' ']];
83+
rule._removeEmptyRulesets(nodeContent);
84+
assert.deepEqual(nodeContent, [['s', ' ']]);
85+
});
86+
87+
it('should remove empty ruleset', function() {
88+
nodeContent = [['ruleset', []]];
89+
rule._removeEmptyRulesets(nodeContent);
90+
assert.deepEqual(nodeContent, []);
91+
});
92+
93+
it('should remove two empty rulesets', function() {
94+
nodeContent = [['s', ' '], ['ruleset', []], ['s', ' \n'], ['ruleset', []]];
95+
rule._removeEmptyRulesets(nodeContent);
96+
assert.deepEqual(nodeContent, [['s', ' '], ['s', ' \n']]);
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)