diff --git a/.jshint-groups.js b/.jshint-groups.js index 70b48684..95aa4d7b 100644 --- a/.jshint-groups.js +++ b/.jshint-groups.js @@ -27,7 +27,7 @@ module.exports = { test: { options: { node: true, - predef: ['describe', 'beforeEach', 'it'] + predef: ['describe', 'beforeEach', 'afterEach', 'it'] }, includes: ['test/*.js'] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c8cb58..e967cb93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.X.X - 2013-XX-XX +- Use Gonzales PE to parse *.scss and *.less files +- Support sorting properties in *.scss and *.less files + ## 1.0.0 - 2013-11-06 - Option: vendor-prefix-align - Dependencies updated diff --git a/README.md b/README.md index 9adcfc26..f473a41a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ You can easily write your own [configuration](#configuration) to make your style The main feature is the [sorting properties](#sort-order) in specific order. It was inspired by the same-named [@miripiruni](https://github.com/miripiruni)'s [PHP-based tool](https://github.com/csscomb/csscomb). -This is the new JavaScript version, based on powerful CSS parser [Gonzales](https://github.com/css/gonzales). +This is the new JavaScript version, based on powerful CSS parser [Gonzales PE](https://github.com/tonyganch/gonzales-pe). ## Installation @@ -363,6 +363,35 @@ p { } ``` +If you sort properties in `*.scss` or `*.less` files, you can use one of 3 +keywords in your config: + * `$variable` for variable declarations (e.g. `$var` in Sass or `@var` in LESS); + * `$include` for included mixins (e.g. `@include ...` and `@extend ...` in Sass + or `.mixin()` in LESS); + * `$import` for `@import` rules. + +Example: `{ "sort-order": [ [ "$variable" ], [ "$include" ], [ "top", "padding" ] ] }` + +```scss +/* before */ +p { + padding: 0; + @include mixin($color); + $color: tomato; + top: 0; +} + +/* after */ +p { + $color: tomato; + + @include mixin($color); + + top: 0; + padding: 0; +} +``` + ### stick-brace Available values: @@ -465,5 +494,6 @@ This software is released under the terms of the [MIT license](https://github.co ## Other projects * https://github.com/senchalabs/cssbeautify * https://github.com/css/gonzales +* https://github.com/tonyganch/gonzales-pe * https://github.com/css/csso * https://github.com/nzakas/parser-lib diff --git a/lib/csscomb.js b/lib/csscomb.js index 937b9663..a4ad8d63 100644 --- a/lib/csscomb.js +++ b/lib/csscomb.js @@ -1,4 +1,4 @@ -var gonzales = require('gonzales'); +var gonzales = require('gonzales-pe'); var minimatch = require('minimatch'); var vow = require('vow'); var vfs = require('vow-fs'); @@ -103,14 +103,15 @@ Comb.prototype = { /** * Process file provided with a string. * @param {String} text + * @param {String} [syntax] Syntax name (e.g. `scss`) * @param {String} [filename] */ - processString: function(text, filename) { + processString: function(text, syntax, filename) { if (!text) return text; var tree; var string = JSON.stringify; try { - tree = gonzales.srcToCSSP(text); + tree = gonzales.cssToAST({ syntax: syntax, css: text }); } catch (e) { throw new Error('Parsing error at ' + filename + ': ' + e.message); } @@ -118,7 +119,7 @@ Comb.prototype = { throw new Error('Undefined tree at ' + filename + ': ' + string(text) + ' => ' + string(tree)); } tree = this.processTree(tree); - return gonzales.csspToSrc(tree); + return gonzales.astToCSS({ syntax: syntax, ast: tree }); }, /** @@ -129,9 +130,11 @@ Comb.prototype = { */ processFile: function(path) { var _this = this; - if (this._shouldProcess(path) && path.match(/\.css$/)) { + // TODO: Move extension check into `_shouldProcess` method + if (this._shouldProcess(path) && path.match(/\.[css, scss]$/)) { return vfs.read(path, 'utf8').then(function(data) { - var processedData = _this.processString(data, path); + var syntax = path.split('.').pop(); + var processedData = _this.processString(data, syntax, path); var changed = data !== processedData; var lint = _this._lint; diff --git a/lib/options/always-semicolon.js b/lib/options/always-semicolon.js index c0a32091..07e951ad 100644 --- a/lib/options/always-semicolon.js +++ b/lib/options/always-semicolon.js @@ -29,7 +29,7 @@ module.exports = { if (type === 'declaration') { var space = []; for (var j = value.length; j--;) { - if (value[j][0] !== 's' && value[j][0] !== 'comment') break; + if (['s', 'commentML', 'commentSL'].indexOf(value[j][0]) === -1) break; space.unshift(value.splice(j)[0]); } node.splice.apply(node, [i + 1, 0, ['decldelim']].concat(space)); diff --git a/lib/options/remove-empty-rulesets.js b/lib/options/remove-empty-rulesets.js index 10c818b5..51acdca9 100644 --- a/lib/options/remove-empty-rulesets.js +++ b/lib/options/remove-empty-rulesets.js @@ -67,7 +67,7 @@ module.exports = { }, _isDeclarationOrComment: function(node) { - return node[0] === 'declaration' || node[0] === 'comment'; + return ['declaration', 'commentML', 'commentSL'].indexOf(node[0]) > -1; }, _isRuleset: function(node) { diff --git a/lib/options/sort-order.js b/lib/options/sort-order.js index 45270142..beb620b9 100644 --- a/lib/options/sort-order.js +++ b/lib/options/sort-order.js @@ -25,126 +25,261 @@ module.exports = { * @param {node} node */ process: function(nodeType, node) { - if (nodeType !== 'block') return; + // Types of nodes that can be sorted: + var NODES = ['atruleb', 'atruler', 'atrules', 'commentML', 'commentSL', + 'declaration', 's', 'include']; + // Spaces and comments: + var SC = ['commentML', 'commentSL', 's']; + var currentNode; + // Sort order of properties: var order = this._order; - var nodeExt = this._createNodeExt(node); - var firstSymbols = nodeExt[0].decl || nodeExt.shift(); - var lastSymbols = nodeExt[nodeExt.length - 1].decl || nodeExt.pop(); + // List of declarations that should be sorted: + var sorted = []; + // list of nodes that should be removed from parent node: + var deleted = []; + // List of spaces and comments that go before declaration/@-rule: + var sc0 = []; + // Value to search in sort order: either a declaration's property name + // (e.g. `color`), or @-rule's special keyword (e.g. `$import`): + var propertyName; - nodeExt.sort(function(a, b) { - var indexA = order[a.decl] || { prop: -1 }; - var indexB = order[b.decl] || { prop: -1 }; - var groupDelta = indexA.group - indexB.group; - var propDelta = indexA.prop - indexB.prop; + // Counters for loops: + var i; + var l; + var j; + var nl; - return groupDelta !== 0 ? groupDelta : propDelta; - }); + /** + * Check if there are any comments or spaces before + * the declaration/@-rule. + * @returns {Array} List of nodes with spaces and comments + */ + var checkSC0 = function() { + // List of nodes with spaces and comments: + var sc = []; + // List of nodes that can be later deleted from parent node: + var d = []; - firstSymbols && nodeExt.unshift(firstSymbols); - lastSymbols && nodeExt.push(lastSymbols); + for (; i < l; i++) { + currentNode = node[i]; + // If there is no node left, + // stop and do nothing with previously found spaces/comments: + if (!currentNode) { + return false; + } - this._applyNodeExt(node, nodeExt); - }, + // If the node is declaration or @-rule, stop and return all + // found nodes with spaces and comments (if there are any): + if (SC.indexOf(currentNode[0]) === -1) break; - /** - * Internal. Smart split bunch on '\n' symbols; - * @param {Array} bunch - * @param {Boolean} isPrevDeclExists - */ - _splitBunch: function(bunch, isPrevDeclExists) { - var nextBunch = []; - var declAlready = false; - var flag = false; - var item; - var indexOf; + sc.push(currentNode); + d.push(i); + } - for (var i = 0; i < bunch.length; ++i) { - if (flag) { nextBunch.push(bunch[i]); continue; } - if (bunch[i][0] === 'declaration') { declAlready = true; } + deleted = deleted.concat(d); - if (isPrevDeclExists && !declAlready) continue; - if (bunch[i][0] !== 's') continue; + return sc; + }; - var indexOfNewLine = bunch[i][1].indexOf('\n'); + /** + * Check if there are any comments or spaces after + * the declaration/@-rule. + * @returns {Array} List of nodes with spaces and comments + * @private + */ + var checkSC1 = function() { + // List of nodes with spaces and comments: + var sc = []; + // List of nodes that can be later deleted from parent node: + var d = []; + // Position of `\n` symbol inside a node with spaces: + var lbIndex; - if (indexOfNewLine === -1) continue; + // Check every next node: + for (; i < l; i++) { + currentNode = node[i + 1]; + // If there is no node, or it is nor spaces neither comment, stop: + if (!currentNode || SC.indexOf(currentNode[0]) === -1) break; - nextBunch.push(['s', bunch[i][1].substr(indexOfNewLine + 1)]); - bunch[i][1] = bunch[i][1].substr(0, indexOfNewLine + 1); + if (['commentML', 'commentSL'].indexOf(currentNode[0]) > -1) { + sc.push(currentNode); + d.push(i + 1); + continue; + } - flag = i + 1; - } + lbIndex = currentNode[1].indexOf('\n'); - if (nextBunch.length === 1 && nextBunch[0][0] === 's') { - item = nextBunch[0]; - indexOf = item[1].lastIndexOf('\n'); + // If there are any line breaks in a node with spaces, stop and + // split the node into two: one with spaces before line break + // and one with `\n` symbol and everything that goes after. + // Combine the first one with declaration/@-rule's node: + if (lbIndex > -1) { + // TODO: Don't push an empty array + sc.push(['s', currentNode[1].substring(0, lbIndex)]); + currentNode[1] = currentNode[1].substring(lbIndex); + break; + } - indexOf !== -1 && (item[1] = item[1].substr(indexOf + 1)); - } + sc.push(currentNode); + d.push(i + 1); + } - flag && bunch.splice(flag); + deleted = deleted.concat(d); - return nextBunch; - }, + return sc; + }; - /** - * Internal. Create extended node in format of list - * { - * data:[,,declaration,] - * decl: declarationPropertyName - * }; - * @param {node} node - */ - _createNodeExt: function(node) { - var extNode = []; - var bunch = []; - var nextBunch; - var prevDeclPropertyName; - - node.forEach(function(node) { - if (node[0] === 'declaration') { - nextBunch = this._splitBunch(bunch, prevDeclPropertyName); - extNode.push({ data: bunch, decl: prevDeclPropertyName }); - bunch = nextBunch; - prevDeclPropertyName = node[1][1][1]; + /** + * Combine declaration/@-rule's node with other relevant information: + * property index, semicolon, spaces and comments. + * @returns {Object} Extended node + */ + var extendNode = function() { + currentNode = node[i]; + var nextNode = node[i + 1]; + // Object containing current node, all corresponding spaces, + // comments and other information: + var extendedNode; + // Check if current node's property name is in sort order. + // If it is, save information about its indices: + var orderProperty = order[propertyName]; + + extendedNode = { + node: currentNode, + sc0: sc0, + delim: [] + }; + + // If the declaration's property is in order's list, save its + // group and property indices. Otherwise set them to 10000, so + // declaration appears at the bottom of a sorted list: + extendedNode.groupIndex = orderProperty && orderProperty.group > -1 ? + orderProperty.group : 10000; + extendedNode.propertyIndex = orderProperty && orderProperty.prop > -1 ? + orderProperty.prop : 10000; + + // Mark current node to remove it later from parent node: + deleted.push(i); + + // If there is `;` right after the declaration, save it with the + // declaration and mark it for removing from parent node: + if (currentNode && nextNode && nextNode[0] === 'decldelim') { + extendedNode.delim.push(nextNode); + deleted.push(i + 1); + i++; } - bunch.push(node); - }, this); - nextBunch = this._splitBunch(bunch, prevDeclPropertyName); - extNode.push({ data: bunch, decl: prevDeclPropertyName }); - nextBunch.length && extNode.push({ data: nextBunch }); + // Save spaces and comments which follow right after the declaration + // and mark them for removing from parent node: + extendedNode.sc1 = checkSC1(); - return extNode; - }, + return extendedNode; + }; - /** - * Internal. Add delimiter at the end of groups of properties - * @param {extNode} extNodePrev - * @param {extNode} extNode - */ - _addGroupDelimiter: function(extNodePrev, extNode) { - if (!extNodePrev.decl || !extNode.decl) return; + // TODO: Think it through! + // Sort properties only inside blocks: + if (nodeType !== 'block') return; - var indexA = this._order[extNodePrev.decl]; - var indexB = this._order[extNode.decl]; - var isGroupBorder = indexA && indexB && indexA.group !== indexB.group; + // Check every child node. + // If it is declaration (property-value pair, e.g. `color: tomato`), + // or @-rule (e.g. `@include nani`), + // combine it with spaces, semicolon and comments and move them from + // current node to a separate list for further sorting: + for (i = 0, l = node.length; i < l; i++) { + if (NODES.indexOf(node[i][0]) === -1) continue; - if (isGroupBorder) extNode.data.unshift(['s', '\n']); - }, + // Save preceding spaces and comments, if there are any, and mark + // them for removing from parent node: + sc0 = checkSC0(); + if (!sc0) continue; - /** - * Internal. apply extNode back at common format node. - * @param {node} node - * @param {extNode} extNode - */ - _applyNodeExt: function(node, extNode) { - node.splice(0, node.length); - extNode.forEach(function(item, i) { - i > 0 && this._addGroupDelimiter(extNode[i - 1], item); - node.push.apply(node, item.data); - }, this); - } + // If spaces/comments are the last nodes, stop and go to sorting: + if (!node[i]) { + deleted.splice(deleted.length - sc0.length, deleted.length + 1); + break; + } + + // Check if the node needs to be sorted: + // it should be a special @-rule (e.g. `@include`) or a declaration + // with a valid property (e.g. `color` or `$width`). + // If not, proceed with the next node: + propertyName = null; + // Look for includes: + if (node[i][0] === 'include') { + propertyName = '$include'; + } else { + for (j = 1, nl = node[i].length; j < nl; j++) { + currentNode = node[i][j]; + if (currentNode[0] === 'property') { + propertyName = currentNode[1][0] === 'variable' ? + '$variable' : currentNode[1][1]; + break; + } else if (currentNode[0] === 'atkeyword' && + currentNode[1][1] === 'import') { // Look for imports + propertyName = '$import'; + break; + } + } + } + + if (!propertyName) { + deleted = []; + continue; + } + + // Make an extended node and move it to a separate list for further + // sorting: + sorted.push(extendNode()); + } + // Remove all nodes, that were moved to a `sorted` list, from parent node: + for (i = deleted.length - 1; i > -1; i--) { + node.splice(deleted[i], 1); + } + + // Sort declarations saved for sorting: + sorted.sort(function(a, b) { + // If a's group index is higher than b's group index, in a sorted + // list a appears after b: + if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex; + + // If a and b have the same group index, and a's property index is + // higher than b's property index, in a sorted list a appears after + // b: + return a.propertyIndex - b.propertyIndex; + }); + + // Build all nodes back together. First go sorted declarations, then + // everything else: + if (sorted.length > 0) { + for (i = sorted.length - 1, l = -1; i > l; i--) { + currentNode = sorted[i]; + var prevNode = sorted[i - 1]; + sc0 = currentNode.sc0; + var sc1 = currentNode.sc1; + + // Divide declarations from different groups with an empty line: + if (prevNode && currentNode.groupIndex > prevNode.groupIndex) { + if (sc0[0] && sc0[0][0] === 's' && + sc0[0][1].match(/\n/g) && + sc0[0][1].match(/\n/g).length < 2) { + sc0.unshift(['s', '\n']); + } + } + + sc0.reverse(); + sc1.reverse(); + + for (j = 0, nl = sc1.length; j < nl; j++) { + node.unshift(sc1[j]); + } + if (currentNode.delim.length > 0) node.unshift(['decldelim']); + node.unshift(currentNode.node); + for (j = 0, nl = sc0.length; j < nl; j++) { + node.unshift(sc0[j]); + } + } + } + } }; diff --git a/package.json b/package.json index 75d8ab3a..2f37b89f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "commander": "2.0.0", - "gonzales": "1.0.7", + "gonzales-pe": "2.0.0-rc0", "minimatch": "0.2.12", "vow": "0.3.11", "vow-fs": "0.2.3" diff --git a/test/integral.expect.css b/test/integral.expect.css index 9d161b4d..c65c07ab 100644 --- a/test/integral.expect.css +++ b/test/integral.expect.css @@ -8,6 +8,7 @@ background: -moz-linear-gradient(top, rgba(0,0,0,.2) 0, rgba(0,0,0,.4) 100%); background: -o-linear-gradient(top, rgba(0,0,0,.2) 0,rgba(0,0,0,.4) 100%); background: linear-gradient(to bottom, rgba(0,0,0,.2) 0,rgba(0,0,0,.4) 100%); + -moz-box-shadow: 0 1px 0 rgba(0,0,0,.07); box-shadow: 0 1px 0 rgba(0,0,0,.07); } diff --git a/test/less.js b/test/less.js new file mode 100644 index 00000000..0c35daee --- /dev/null +++ b/test/less.js @@ -0,0 +1,331 @@ +var Comb = require('../lib/csscomb'); +var assert = require('assert'); + +describe('LESS', function() { + var comb; + var config; + var input; + var expected; + + beforeEach(function() { + comb = new Comb(); + }); + + afterEach(function() { + comb.configure(config); + assert.equal(comb.processString(input, 'less'), expected); + }); + + describe('Parsing', function() { + it('Should parse nested rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; a { top: 0; } }'; + + expected = 'div { color: tomato; a { top: 0; } }'; + }); + + it('Should parse operations', function() { + config = {}; + + input = 'div {\n' + + ' @base: 5%;\n' + + ' @filler: @base * 2;\n' + + ' @other: @base + @filler;\n' + + ' color: #888 / 4;\n' + + ' background-color: @base-color + #111;\n' + + ' height: 100% / 2 + @filler;\n' + + ' }'; + + expected = 'div {\n' + + ' @base: 5%;\n' + + ' @filler: @base * 2;\n' + + ' @other: @base + @filler;\n' + + ' color: #888 / 4;\n' + + ' background-color: @base-color + #111;\n' + + ' height: 100% / 2 + @filler;\n' + + ' }'; + }); + + it('Should parse parent selector &', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div { color: tomato; &.top { color: nani; top: 0; } left: 0; }'; + + expected = 'div { left: 0; color: tomato; &.top {top: 0; color: nani; }}'; + }); + + it('Should parse variables', function() { + config = {}; + + input = '@red: tomato; div { color: @tomato; top: @@foo; }'; + + expected = '@red: tomato; div { color: @tomato; top: @@foo; }'; + }); + + it('Should parse interpolated variables inside selectors', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div.@{nani} {color:tomato;top:0;}'; + + expected = 'div.@{nani} {top:0;color:tomato;}'; + }); + + it('Should parse interpolated variables inside values', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div {color:@{tomato};top:0;}'; + + expected = 'div {top:0;color:@{tomato};}'; + }); + + it('Should parse @import', function() { + config = {}; + + input = 'div { @import "foo.css"; top: 0; }'; + + expected = 'div { @import "foo.css"; top: 0; }'; + }); + + it('Should parse included mixins', function() { + config = {}; + + input = 'div { .mixin; top: 0; }'; + + expected = 'div { .mixin; top: 0; }'; + }); + + it('Should parse nested @media', function() { + config = {}; + + input = 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}'; + + expected = 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}'; + }); + }); + + describe('Sorting', function() { + it('Should sort properties inside rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; top: 0; }'; + + expected = 'div {top: 0; color: tomato; }'; + }); + + it('Should sort properties inside nested rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; a { color: nani; top: 0; } }'; + + expected = 'div { color: tomato; a {top: 0; color: nani; } }'; + }); + + it('Should sort properties divided by nested rules', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div { color: tomato; a { color: nani; top: 0; } left: 0; }'; + + expected = 'div { left: 0; color: tomato; a {top: 0; color: nani; }}'; + }); + + it('Should group declarations with proper comments and spaces (single line)', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div {/* 1 */ color: tomato; /* 2 */ top: 0; /* 3 */ /* 4 */}'; + + expected = 'div {top: 0; /* 3 */ /* 4 *//* 1 */ color: tomato; /* 2 */ }'; + }); + + it('Should group declarations with proper comments and spaces (multiple lines). Test 1', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div {\n' + + ' color: tomato; /* 1 */\n' + + ' /* 2 */\n' + + ' /* 3 */\n' + + ' top: 0; /* 4 */\n' + + ' /* 5 */\n' + + '}'; + + expected = 'div {\n' + + ' /* 2 */\n' + + ' /* 3 */\n' + + ' top: 0; /* 4 */\n' + + ' color: tomato; /* 1 */\n' + + ' /* 5 */\n' + + '}'; + }); + + it('Should group declarations with proper comments and spaces (multiple lines). Test 2', function() { + config = { 'sort-order': [ + ['$variable', 'color'] + ] }; + + input = 'p {\n' + + ' /* One hell of a comment */\n' + + ' color: tomato;\n' + + ' // Get in line!\n' + + ' @var: white;\n' + + ' }'; + + expected = 'p {\n' + + ' // Get in line!\n' + + ' @var: white;\n' + + ' /* One hell of a comment */\n' + + ' color: tomato;\n' + + ' }'; + }); + + it('Should group declarations with proper comments and spaces (multiple lines). Test 3', function() { + config = { 'sort-order': [ + ['$variable', 'color'] + ] }; + + input = 'p {\n' + + ' color: tomato; /* One hell of a comment */\n' + + ' @var: white; // Get in line!\n' + + ' }'; + + expected = 'p {\n' + + ' @var: white; // Get in line!\n' + + ' color: tomato; /* One hell of a comment */\n' + + ' }'; + }); + + it('Should divide properties from different groups with an empty line', function() { + config = { 'sort-order': [ + ['top'], ['color'] + ] }; + + input = 'div {\n' + + ' color: tomato;\n' + + ' top: 0;\n' + + '}'; + + expected = 'div {\n' + + ' top: 0;\n' + + '\n' + + ' color: tomato;\n' + + '}'; + }); + + it('Should sort variables', function() { + config = { 'sort-order': [ + ['$variable', 'color'] + ] }; + + input = 'div { color: @red; @red: tomato; }'; + + expected = 'div {@red: tomato; color: @red; }'; + }); + + it('Should sort imports', function() { + config = { 'sort-order': [ + ['$import', 'color'] + ] }; + + input = 'div { color: tomato; @import "foo.css"; }'; + + expected = 'div {@import "foo.css"; color: tomato; }'; + }); + + it('Should sort included mixins. Test 1', function() { + config = { 'sort-order': [ + ['$include', 'color', 'border-top', 'border-bottom'] + ] }; + + input = '.bordered {\n' + + ' border-bottom: solid 2px black;\n' + + ' border-top: dotted 1px black;\n' + + ' }\n' + + '#menu a {\n' + + ' color: #111;\n' + + ' .bordered;\n' + + ' }\n' + + '.post a {\n' + + ' color: red;\n' + + ' .bordered;\n' + + ' }'; + + expected = '.bordered {\n' + + ' border-top: dotted 1px black;\n' + + ' border-bottom: solid 2px black;\n' + + ' }\n' + + '#menu a {\n' + + ' .bordered;\n' + + ' color: #111;\n' + + ' }\n' + + '.post a {\n' + + ' .bordered;\n' + + ' color: red;\n' + + ' }'; + }); + + it('Should sort included mixins. Test 2', function() { + config = { 'sort-order': [ + ['$include', 'top', 'color'] + ] }; + + input = '.test {\n' + + ' .test1();\n' + + ' color: tomato;\n' + + ' .test2();\n' + + ' top: 0;\n' + + ' }'; + + expected = '.test {\n' + + ' .test1();\n' + + ' .test2();\n' + + ' top: 0;\n' + + ' color: tomato;\n' + + ' }'; + }); + + it('Should sort included mixins. Test 3', function() { + config = { 'sort-order': [ + ['$include', 'border', 'color'] + ] }; + + input = '.foo {\n' + + ' color: #0f0;\n' + + ' border: 1px solid #f00;\n' + + ' .linear-gradient(#fff; #000);\n' + + '}'; + + expected = '.foo {\n' + + ' .linear-gradient(#fff; #000);\n' + + ' border: 1px solid #f00;\n' + + ' color: #0f0;\n' + + '}'; + }); + }); +}); diff --git a/test/scss.js b/test/scss.js new file mode 100644 index 00000000..0659215a --- /dev/null +++ b/test/scss.js @@ -0,0 +1,401 @@ +var Comb = require('../lib/csscomb'); +var assert = require('assert'); + +describe('SCSS', function() { + var comb; + var config; + var input; + var expected; + + beforeEach(function() { + comb = new Comb(); + }); + + afterEach(function() { + comb.configure(config); + assert.equal(comb.processString(input, 'scss'), expected); + }); + + describe('Parsing', function() { + it('Should parse nested rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; a { top: 0; } }'; + + expected = 'div { color: tomato; a { top: 0; } }'; + }); + + it('Should parse parent selector &', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div { color: tomato; &.top { color: nani; top: 0; } left: 0; }'; + + expected = 'div { left: 0; color: tomato; &.top {top: 0; color: nani; }}'; + }); + + it('Should parse nested properties', function() { + config = { 'sort-order': [ + ['left', 'color', 'font'] + ] }; + + input = 'div { color: tomato; font: 2px/3px { family: fantasy; size: 30em; } left: 0; }'; + + expected = 'div {left: 0; color: tomato; font: 2px/3px { family: fantasy; size: 30em; } }'; + }); + + it('Should parse variables', function() { + config = {}; + + input = '$red: tomato; div { color: $tomato; }'; + + expected = '$red: tomato; div { color: $tomato; }'; + }); + + it('Should parse interpolated variables inside selectors', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div.#{$nani} {color:tomato;top:0;}'; + + expected = 'div.#{$nani} {top:0;color:tomato;}'; + }); + + it('Should parse interpolated variables inside values', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div {color:#{$tomato};top:0;}'; + + expected = 'div {top:0;color:#{$tomato};}'; + }); + + it('Should parse defaults', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div { color: tomato !default; top: 0; }'; + + expected = 'div {top: 0; color: tomato !default; }'; + }); + + it('Should parse @import', function() { + config = {}; + + input = 'div { @import "foo.css"; top: 0; }'; + + expected = 'div { @import "foo.css"; top: 0; }'; + }); + + it('Should parse @include', function() { + config = {}; + + input = 'div { @include nani($panda); top: 0; }'; + + expected = 'div { @include nani($panda); top: 0; }'; + }); + + it('Should parse nested @media', function() { + config = {}; + + input = 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}'; + + expected = 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}'; + }); + + it('Should parse @extend with classes', function() { + config = {}; + + input = 'div { @extend .nani; top: 0; }'; + + expected = 'div { @extend .nani; top: 0; }'; + }); + + it('Should parse @extend with placeholders', function() { + config = {}; + + input = 'div { @extend %nani; top: 0; }'; + + expected = 'div { @extend %nani; top: 0; }'; + }); + + it('Should parse @warn', function() { + config = {}; + + input = 'div { @warn "nani"; top: 0; }'; + + expected = 'div { @warn "nani"; top: 0; }'; + }); + + it('Should parse @if', function() { + config = {}; + + input = 'div { @if $type == ocean { top: 0; } }'; + + expected = 'div { @if $type == ocean { top: 0; } }'; + }); + + it('Should parse @if and @else', function() { + config = {}; + + input = 'div { @if $type == ocean { top: 0; } @else { left: 0; } }'; + + expected = 'div { @if $type == ocean { top: 0; } @else { left: 0; } }'; + }); + + it('Should parse @if and @else if', function() { + config = {}; + + input = 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }'; + + expected = 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }'; + }); + + it('Should parse @for', function() { + config = {}; + + input = 'div {\n' + + ' @for $i from 1 through 3 {\n' + + ' .item-#{$i} { width: 2em * 1; }\n' + + ' }\n' + + '}'; + + expected = 'div {\n' + + ' @for $i from 1 through 3 {\n' + + ' .item-#{$i} { width: 2em * 1; }\n' + + ' }\n' + + '}'; + }); + + it('Should parse @each', function() { + config = {}; + + input = 'div {\n' + + ' @each $animal in puma, sea-slug, erget {\n' + + ' .#{$animal}-icon { background-image: url("/images/#{$animal}.png"); }\n' + + ' }\n' + + '}'; + + expected = 'div {\n' + + ' @each $animal in puma, sea-slug, erget {\n' + + ' .#{$animal}-icon { background-image: url("/images/#{$animal}.png"); }\n' + + ' }\n' + + '}'; + }); + + it('Should parse @while', function() { + config = {}; + + input = 'div {\n' + + ' @while $i > 6 {\n' + + ' .item { width: 2em * $i; }\n' + + ' $i: $i - 2;\n' + + ' }\n' + + '}'; + + expected = 'div {\n' + + ' @while $i > 6 {\n' + + ' .item { width: 2em * $i; }\n' + + ' $i: $i - 2;\n' + + ' }\n' + + '}'; + }); + + it('Should parse mixins', function() { + config = {}; + + input = '@mixin nani { color: tomato; } .foo { @include nani; }'; + + expected = '@mixin nani { color: tomato; } .foo { @include nani; }'; + }); + + it('Should parse passing several variables to a mixin', function() { + config = {}; + + input = '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }'; + + expected = '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }'; + }); + + it('Should parse passing a list of variables to a mixin', function() { + config = {}; + + input = '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + + '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }'; + + expected = '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + + '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }'; + }); + + it('Should parse passing a content block to a mixin', function() { + config = {}; + + input = '.foo { @include nani { color: tomato; top: 0 } }'; + + expected = '.foo { @include nani { color: tomato; top: 0 } }'; + }); + + it('Should parse @content', function() { + config = {}; + + input = '@mixin nani { a { @content; } }'; + + expected = '@mixin nani { a { @content; } }'; + }); + + it('Should parse functions', function() { + config = {}; + + input = '@function nani($n) { @return $n * 2; }'; + + expected = '@function nani($n) { @return $n * 2; }'; + }); + }); + + describe('Sorting', function() { + it('Should sort properties inside rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; top: 0; }'; + + expected = 'div {top: 0; color: tomato; }'; + }); + + it('Should sort properties inside nested rules', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div { color: tomato; a { color: nani; top: 0; } }'; + + expected = 'div { color: tomato; a {top: 0; color: nani; } }'; + }); + + it('Should sort properties divided by nested rules', function() { + config = { 'sort-order': [ + ['top', 'left', 'color'] + ] }; + + input = 'div { color: tomato; a { color: nani; top: 0; } left: 0; }'; + + expected = 'div { left: 0; color: tomato; a {top: 0; color: nani; }}'; + }); + + it('Should group declarations with proper comments and spaces (multiple lines)', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div {\n' + + ' color: tomato; /* 1 */\n' + + ' /* 2 */\n' + + ' /* 3 */\n' + + ' top: 0; /* 4 */\n' + + ' /* 5 */\n' + + '}'; + + expected = 'div {\n' + + ' /* 2 */\n' + + ' /* 3 */\n' + + ' top: 0; /* 4 */\n' + + ' color: tomato; /* 1 */\n' + + ' /* 5 */\n' + + '}'; + }); + + it('Should group declarations with proper comments and spaces (single line)', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = 'div {/* 1 */ color: tomato; /* 2 */ top: 0; /* 3 */ /* 4 */}'; + + expected = 'div {top: 0; /* 3 */ /* 4 *//* 1 */ color: tomato; /* 2 */ }'; + }); + + it('Should divide properties from different groups with an empty line', function() { + config = { 'sort-order': [ + ['top'], ['color'] + ] }; + + input = 'div {\n' + + ' color: tomato;\n' + + ' top: 0;\n' + + '}'; + + expected = 'div {\n' + + ' top: 0;\n' + + '\n' + + ' color: tomato;\n' + + '}'; + }); + + it('Should sort variables', function() { + config = { 'sort-order': [ + ['$variable', 'color'] + ] }; + + input = 'div { color: $tomato; $red: tomato; }'; + + expected = 'div {$red: tomato; color: $tomato; }'; + }); + + it('Should sort imports', function() { + config = { 'sort-order': [ + ['$import', 'color'] + ] }; + + input = 'div { color: tomato; @import "foo.css"; }'; + + expected = 'div {@import "foo.css"; color: tomato; }'; + }); + + it('Should sort @include-s', function() { + config = { 'sort-order': [ + ['$include', 'color'] + ] }; + + input = 'div { color: tomato; @include .nani; }'; + + expected = 'div {@include .nani; color: tomato; }'; + }); + + it('Should sort @extend-s', function() { + config = { 'sort-order': [ + ['$include', 'color'] + ] }; + + input = 'div { color: tomato; @extend %nani; }'; + + expected = 'div {@extend %nani; color: tomato; }'; + }); + + it('Should sort properties inside blocks passed to mixins', function() { + config = { 'sort-order': [ + ['top', 'color'] + ] }; + + input = '.foo { @include nani { color: tomato; top: 0; } }'; + + expected = '.foo { @include nani {top: 0; color: tomato; } }'; + }); + }); +}); diff --git a/test/sort-order.js b/test/sort-order.js index d1e1bbd6..21e310ad 100644 --- a/test/sort-order.js +++ b/test/sort-order.js @@ -76,11 +76,11 @@ describe('options/sort-order', function() { var input = 'div p em {\n' + '\t/* upline comment */\n' + '\tfont-style:italic;\n' + - '\tborder-bottom:1px solid red /* trololo */ /* trololo */\n' + + '\tborder-bottom:1px solid red; /* trololo */ /* trololo */\n' + '}'; var expected = 'div p em {\n' + - '\tborder-bottom:1px solid red /* trololo */ /* trololo */\n' + + '\tborder-bottom:1px solid red; /* trololo */ /* trololo */\n' + '\t/* upline comment */\n' + '\tfont-style:italic;\n' + '}'; @@ -116,31 +116,4 @@ describe('options/sort-order', function() { assert.equal(comb.processString(input), expected); }); - - it('Should replace custom delimiters by ours', function() { - - var config = { - 'sort-order': [ - ['margin'], - ['padding'] - ] - }; - - var input = 'div p em {\n' + - '\tpadding: 1px;\n' + - ' \n' + - '\tmargin: 1px;\n' + - '}'; - - var expected = 'div p em {\n' + - '\tmargin: 1px;\n' + - '\n' + - '\tpadding: 1px;\n' + - '}'; - - comb.configure(config); - assert.equal(comb.processString(input), expected); - - }); - });