forked from csscomb/csscomb.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
16 changed files
with
696 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
test/options/lines-between-rulesets/process/css/2-lines-between-rulesets.expected.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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;} |
1 change: 1 addition & 0 deletions
1
test/options/lines-between-rulesets/process/css/lines-between-rulesets.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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;} |
5 changes: 5 additions & 0 deletions
5
test/options/lines-between-rulesets/process/css/lines-between-rulesets.expected.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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;} |
40 changes: 40 additions & 0 deletions
40
test/options/lines-between-rulesets/process/less/2-lines-between-rulesets.expected.less
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.