Skip to content

Commit e06678e

Browse files
ravasthitonyganch
authored andcommitted
Add lines-between-rulesets option.
Why: Allow users to specify a new option, `lines-between-rulesets`, to separate rulesets and @rules from each other by the specified number of newlines. This change addresses the need by: Inserting newlines between rulesets and @rules for all syntaxes. Intelligently handles: * When the ruleset is the first in the file, don't insert newlines before it. * When there are comments in front of the ruleset; insert the newlines before the comments so that the ruleset and the comments stay together.
1 parent 1189886 commit e06678e

16 files changed

+696
-3
lines changed

doc/options.md

+52-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Configuration options
22

33
There are a number of options you can use, all of them are switched off by
4-
default.
4+
default.
55
Here is a full list in the same order they are applied while processing css:
66

77
- [always-semicolon](#always-semicolon)
@@ -29,6 +29,7 @@ Here is a full list in the same order they are applied while processing css:
2929
- [unitless-zero](#unitless-zero)
3030
- [tab-size](#tab-size)
3131
- [vendor-prefix-align](#vendor-prefix-align)
32+
- [lines-between-rulesets](#lines-between-rulesets)
3233

3334
Following options are ignored while processing `*.sass` files:
3435

@@ -397,8 +398,8 @@ everything would go into five groups: variables, then group with `position`, the
397398
## sort-order-fallback
398399

399400
Apply a special sort order for properties that are not specified in `sort-order`
400-
list.
401-
Works great with [leftovers](#sort-order-vs-leftovers).
401+
list.
402+
Works great with [leftovers](#sort-order-vs-leftovers).
402403
**Note:** This option is applied only if [sort order](#sort-order) list is
403404
provided.
404405

@@ -905,6 +906,54 @@ a
905906
}
906907
```
907908

909+
## lines-between-rulesets
910+
911+
Number of line breaks between rulesets or @rules.
912+
913+
Acceptable values:
914+
915+
* `{Number}` — number of newlines;
916+
917+
Example: `{ "lines-between-rulesets": 1}`
918+
919+
```scss
920+
// Before:
921+
.foo {
922+
@include border-radius(5px);
923+
background: red;
924+
.baz {
925+
.test {
926+
height: 50px;
927+
}
928+
}
929+
}.bar {
930+
border: 1px solid red;
931+
@media (min-width: 500px) {
932+
width: 50px;
933+
}
934+
}
935+
936+
// After:
937+
.foo {
938+
@include border-radius(5px);
939+
background: red;
940+
941+
.baz {
942+
.test {
943+
height: 50px;
944+
}
945+
}
946+
}
947+
948+
.bar {
949+
border: 1px solid red;
950+
951+
@media (min-width: 500px) {
952+
width: 50px;
953+
}
954+
}
955+
```
956+
908957
## verbose
909958

910959
Whether to use `--verbose` option in CLI.

src/options/lines-between-rulesets.js

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
'use strict';
2+
3+
let gonzales = require('gonzales-pe');
4+
5+
let option = {
6+
newLinesString: '',
7+
newLinesNode: null,
8+
9+
/**
10+
* Option's name as it's used in config.
11+
* @type {String}
12+
*/
13+
get name() {
14+
return 'lines-between-rulesets';
15+
},
16+
17+
/**
18+
* Name of option that must run after this option.
19+
* @type {String}
20+
*/
21+
get runBefore() {
22+
return 'block-indent';
23+
},
24+
25+
/**
26+
* List of syntaxes that are supported by this option.
27+
* @type {Array}
28+
*/
29+
get syntax() {
30+
return ['css', 'less', 'sass', 'scss'];
31+
},
32+
33+
/**
34+
* Types of values this option accepts in config.
35+
* @type {Object}
36+
*/
37+
get accepts() {
38+
return {
39+
number: true
40+
};
41+
},
42+
43+
/**
44+
* @param {number} value
45+
* @returns {number}
46+
*/
47+
/*
48+
** Still need to override, as the core implementation of setValue doesn't
49+
** pass numbers through, but creates a string of spaces of the same length.
50+
*/
51+
setValue(value) {
52+
let valueType = typeof value;
53+
54+
if (valueType !== 'number') {
55+
throw new Error('Value must be a number.');
56+
}
57+
58+
return value;
59+
},
60+
61+
buildSpacing(syntax) {
62+
let spacing = '';
63+
let numNewLines = 0;
64+
let newLinesOffset = 1;
65+
66+
if (syntax === 'sass') {
67+
newLinesOffset = 0;
68+
}
69+
70+
numNewLines = Math.round(this.value) + newLinesOffset;
71+
72+
for (var i = 0; i < numNewLines; i++) {
73+
spacing += '\n';
74+
}
75+
76+
return spacing;
77+
},
78+
79+
/**
80+
* Processes ast and fixes found code style errors.
81+
* @param {Node} ast
82+
*/
83+
process(ast) {
84+
this.newLinesString = this.buildSpacing(ast.syntax);
85+
this.newLinesNode = gonzales.createNode({
86+
type: 'space',
87+
content: this.newLinesString
88+
});
89+
this.processBlock(ast);
90+
},
91+
92+
processBlock(x) {
93+
if (x.is('stylesheet')) {
94+
// Check all @rules
95+
this.processAtRules(x);
96+
97+
// Check all rulesets
98+
this.processRuleSets(x);
99+
}
100+
101+
x.forEach((node) => {
102+
if (!node.is('block')) {
103+
return this.processBlock(node);
104+
}
105+
106+
// Check all @rules
107+
this.processAtRules(node);
108+
109+
// Check all rulesets
110+
this.processRuleSets(node);
111+
112+
this.processBlock(node);
113+
});
114+
},
115+
116+
processAtRules(node) {
117+
node.forEach('atrule', (atRuleNode, index) => {
118+
this.insertNewlines(node, index);
119+
});
120+
},
121+
122+
processRuleSets(node) {
123+
node.forEach('ruleset', (ruleSetNode, index) => {
124+
this.insertNewlines(node, index);
125+
});
126+
},
127+
128+
isComment(node) {
129+
if (!node) {
130+
return false;
131+
}
132+
return (node.is('singlelineComment') || node.is('multilineComment'));
133+
},
134+
135+
isNewline(node) {
136+
if (!node) {
137+
return false;
138+
}
139+
return (node.content === '\n');
140+
},
141+
142+
prevLineIsComment(parent, index) {
143+
let indexThreshold = 2;
144+
let prevChild;
145+
let prevMinusOneChild;
146+
let prevMinusTwoChild;
147+
let parentSyntax = parent ? parent.syntax : null;
148+
149+
// Sass is troublesome because newlines are counted as separate nodes
150+
if (parentSyntax === 'sass') {
151+
indexThreshold = 3;
152+
}
153+
154+
if (!parent || index < indexThreshold) {
155+
return false;
156+
}
157+
158+
prevChild = parent.get(index - 1);
159+
prevMinusOneChild = parent.get(index - 2);
160+
161+
if (parentSyntax === 'sass') {
162+
prevMinusTwoChild = parent.get(index - 3);
163+
return (
164+
this.isComment(prevMinusTwoChild) &&
165+
this.isNewline(prevMinusOneChild) &&
166+
prevChild.is('space')
167+
);
168+
}
169+
170+
return (this.isComment(prevMinusOneChild) && prevChild.is('space'));
171+
},
172+
173+
/*
174+
** Find the latest previous child that isn't a comment, and return its index.
175+
*/
176+
findLatestNonCommentNode(parent, index) {
177+
let prevChild;
178+
let lastNonCommentIndex = -1;
179+
let currentIndex = index;
180+
let jumpSize = 2;
181+
182+
if (parent.syntax === 'sass') {
183+
jumpSize = 3;
184+
}
185+
186+
while (currentIndex >= 0) {
187+
if (this.prevLineIsComment(parent, currentIndex)) {
188+
currentIndex -= jumpSize;
189+
continue;
190+
}
191+
192+
prevChild = parent.get(currentIndex - 1);
193+
194+
if (!this.isComment(prevChild)) {
195+
lastNonCommentIndex = currentIndex - 1;
196+
break;
197+
}
198+
199+
currentIndex--;
200+
}
201+
202+
return lastNonCommentIndex;
203+
},
204+
205+
insertNewlinesAsString(node) {
206+
let content = node.content;
207+
let lastNewline = content.lastIndexOf('\n');
208+
let newContent;
209+
210+
if (lastNewline > -1) {
211+
content = content.substring(lastNewline + 1);
212+
}
213+
214+
newContent = this.newLinesString + content;
215+
node.content = newContent;
216+
},
217+
218+
insertNewlinesAsNode(node) {
219+
node.insert(node.length, this.newLinesNode);
220+
},
221+
222+
insertNewlines(node, index) {
223+
let prevChild = node.get(index - 1);
224+
let shouldInsert = false;
225+
226+
// Check for previous nodes that are not a space
227+
// Do not insert if the ruleset is the first item
228+
for (var i = 0; i < index; i++) {
229+
if (!node.get(i).is('space')) {
230+
shouldInsert = true;
231+
break;
232+
}
233+
}
234+
235+
if (prevChild && shouldInsert) {
236+
if (this.prevLineIsComment(node, index) || this.isComment(prevChild)) {
237+
let lastNonCommentIndex = this.findLatestNonCommentNode(node, index);
238+
prevChild = node.get(lastNonCommentIndex);
239+
}
240+
241+
if (prevChild.is('space')) {
242+
this.insertNewlinesAsString(prevChild);
243+
} else {
244+
this.insertNewlinesAsNode(prevChild);
245+
}
246+
}
247+
}
248+
};
249+
250+
module.exports = option;

test/core/use/test.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe.skip('.use()', function() {
99
});
1010
var expected = [
1111
'always-semicolon',
12+
'lines-between-rulesets',
1213
'remove-empty-rulesets',
1314
'color-case',
1415
'color-shorthand',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {background: red;}
2+
3+
4+
/*comment*/.bar{border: 1px solid red;}
5+
6+
7+
.baz{color: #fff;}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.foo {background: red;}/*comment*/.bar{border: 1px solid red;}.baz{color: #fff;}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.foo {background: red;}
2+
3+
/*comment*/.bar{border: 1px solid red;}
4+
5+
.baz{color: #fff;}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.foo {
2+
.border-radius(5px);
3+
background: red;
4+
5+
6+
/* comment */
7+
.omg {
8+
.test {
9+
height: 50px;
10+
}
11+
}
12+
}
13+
14+
15+
.bar {
16+
border: 1px solid red;
17+
18+
19+
/*
20+
** another comment
21+
** spanning multiple lines
22+
*/
23+
@media (min-width: 500px) {
24+
width: 50px;
25+
}
26+
}
27+
28+
29+
.baz {
30+
@grey: #ccc;
31+
32+
33+
// a single-line comment
34+
// another single-line comment
35+
.wtf {
36+
@media only screen {
37+
background-color: @grey;
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)