Skip to content

Commit

Permalink
Add lines-between-rulesets option.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ravasthi authored and tonyganch committed Jul 11, 2016
1 parent 1189886 commit e06678e
Show file tree
Hide file tree
Showing 16 changed files with 696 additions and 3 deletions.
55 changes: 52 additions & 3 deletions doc/options.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Configuration options

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

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

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

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

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

Expand Down Expand Up @@ -905,6 +906,54 @@ a
}
```

## lines-between-rulesets

Number of line breaks between rulesets or @rules.

Acceptable values:

* `{Number}` — number of newlines;

Example: `{ "lines-between-rulesets": 1}`

```scss
// Before:
.foo {
@include border-radius(5px);
background: red;
.baz {
.test {
height: 50px;
}
}
}.bar {
border: 1px solid red;
@media (min-width: 500px) {
width: 50px;
}
}

// After:
.foo {
@include border-radius(5px);
background: red;

.baz {
.test {
height: 50px;
}
}
}

.bar {
border: 1px solid red;

@media (min-width: 500px) {
width: 50px;
}
}
```

## verbose

Whether to use `--verbose` option in CLI.
Expand Down
250 changes: 250 additions & 0 deletions src/options/lines-between-rulesets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
'use strict';

let gonzales = require('gonzales-pe');

let option = {
newLinesString: '',
newLinesNode: null,

/**
* Option's name as it's used in config.
* @type {String}
*/
get name() {
return 'lines-between-rulesets';
},

/**
* Name of option that must run after this option.
* @type {String}
*/
get runBefore() {
return 'block-indent';
},

/**
* List of syntaxes that are supported by this option.
* @type {Array}
*/
get syntax() {
return ['css', 'less', 'sass', 'scss'];
},

/**
* Types of values this option accepts in config.
* @type {Object}
*/
get accepts() {
return {
number: true
};
},

/**
* @param {number} value
* @returns {number}
*/
/*
** Still need to override, as the core implementation of setValue doesn't
** pass numbers through, but creates a string of spaces of the same length.
*/
setValue(value) {
let valueType = typeof value;

if (valueType !== 'number') {
throw new Error('Value must be a number.');
}

return value;
},

buildSpacing(syntax) {
let spacing = '';
let numNewLines = 0;
let newLinesOffset = 1;

if (syntax === 'sass') {
newLinesOffset = 0;
}

numNewLines = Math.round(this.value) + newLinesOffset;

for (var i = 0; i < numNewLines; i++) {
spacing += '\n';
}

return spacing;
},

/**
* Processes ast and fixes found code style errors.
* @param {Node} ast
*/
process(ast) {
this.newLinesString = this.buildSpacing(ast.syntax);
this.newLinesNode = gonzales.createNode({
type: 'space',
content: this.newLinesString
});
this.processBlock(ast);
},

processBlock(x) {
if (x.is('stylesheet')) {
// Check all @rules
this.processAtRules(x);

// Check all rulesets
this.processRuleSets(x);
}

x.forEach((node) => {
if (!node.is('block')) {
return this.processBlock(node);
}

// Check all @rules
this.processAtRules(node);

// Check all rulesets
this.processRuleSets(node);

this.processBlock(node);
});
},

processAtRules(node) {
node.forEach('atrule', (atRuleNode, index) => {
this.insertNewlines(node, index);
});
},

processRuleSets(node) {
node.forEach('ruleset', (ruleSetNode, index) => {
this.insertNewlines(node, index);
});
},

isComment(node) {
if (!node) {
return false;
}
return (node.is('singlelineComment') || node.is('multilineComment'));
},

isNewline(node) {
if (!node) {
return false;
}
return (node.content === '\n');
},

prevLineIsComment(parent, index) {
let indexThreshold = 2;
let prevChild;
let prevMinusOneChild;
let prevMinusTwoChild;
let parentSyntax = parent ? parent.syntax : null;

// Sass is troublesome because newlines are counted as separate nodes
if (parentSyntax === 'sass') {
indexThreshold = 3;
}

if (!parent || index < indexThreshold) {
return false;
}

prevChild = parent.get(index - 1);
prevMinusOneChild = parent.get(index - 2);

if (parentSyntax === 'sass') {
prevMinusTwoChild = parent.get(index - 3);
return (
this.isComment(prevMinusTwoChild) &&
this.isNewline(prevMinusOneChild) &&
prevChild.is('space')
);
}

return (this.isComment(prevMinusOneChild) && prevChild.is('space'));
},

/*
** Find the latest previous child that isn't a comment, and return its index.
*/
findLatestNonCommentNode(parent, index) {
let prevChild;
let lastNonCommentIndex = -1;
let currentIndex = index;
let jumpSize = 2;

if (parent.syntax === 'sass') {
jumpSize = 3;
}

while (currentIndex >= 0) {
if (this.prevLineIsComment(parent, currentIndex)) {
currentIndex -= jumpSize;
continue;
}

prevChild = parent.get(currentIndex - 1);

if (!this.isComment(prevChild)) {
lastNonCommentIndex = currentIndex - 1;
break;
}

currentIndex--;
}

return lastNonCommentIndex;
},

insertNewlinesAsString(node) {
let content = node.content;
let lastNewline = content.lastIndexOf('\n');
let newContent;

if (lastNewline > -1) {
content = content.substring(lastNewline + 1);
}

newContent = this.newLinesString + content;
node.content = newContent;
},

insertNewlinesAsNode(node) {
node.insert(node.length, this.newLinesNode);
},

insertNewlines(node, index) {
let prevChild = node.get(index - 1);
let shouldInsert = false;

// Check for previous nodes that are not a space
// Do not insert if the ruleset is the first item
for (var i = 0; i < index; i++) {
if (!node.get(i).is('space')) {
shouldInsert = true;
break;
}
}

if (prevChild && shouldInsert) {
if (this.prevLineIsComment(node, index) || this.isComment(prevChild)) {
let lastNonCommentIndex = this.findLatestNonCommentNode(node, index);
prevChild = node.get(lastNonCommentIndex);
}

if (prevChild.is('space')) {
this.insertNewlinesAsString(prevChild);
} else {
this.insertNewlinesAsNode(prevChild);
}
}
}
};

module.exports = option;
1 change: 1 addition & 0 deletions test/core/use/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe.skip('.use()', function() {
});
var expected = [
'always-semicolon',
'lines-between-rulesets',
'remove-empty-rulesets',
'color-case',
'color-shorthand',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.foo {background: red;}


/*comment*/.bar{border: 1px solid red;}


.baz{color: #fff;}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.foo {background: red;}/*comment*/.bar{border: 1px solid red;}.baz{color: #fff;}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.foo {background: red;}

/*comment*/.bar{border: 1px solid red;}

.baz{color: #fff;}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.foo {
.border-radius(5px);
background: red;


/* comment */
.omg {
.test {
height: 50px;
}
}
}


.bar {
border: 1px solid red;


/*
** another comment
** spanning multiple lines
*/
@media (min-width: 500px) {
width: 50px;
}
}


.baz {
@grey: #ccc;


// a single-line comment
// another single-line comment
.wtf {
@media only screen {
background-color: @grey;
}
}
}
Loading

0 comments on commit e06678e

Please sign in to comment.