From b9add65d72e688b4244cab159842e838b7b2c139 Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Sat, 5 Oct 2013 21:02:13 +0400 Subject: [PATCH 01/10] Use Gonzales PE to parse *.scss files --- CHANGELOG.md | 4 ++++ README.md | 3 ++- lib/csscomb.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) 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..2edcb2bc 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 @@ -465,5 +465,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..f417f011 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'); From 94c7abae73cf83ff89f277c53821beb648865f7f Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Mon, 7 Oct 2013 01:45:33 +0400 Subject: [PATCH 02/10] Sass: Add tests for scss syntax --- test/scss.js | 455 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 test/scss.js diff --git a/test/scss.js b/test/scss.js new file mode 100644 index 00000000..679937eb --- /dev/null +++ b/test/scss.js @@ -0,0 +1,455 @@ +var Comb = require('../lib/csscomb'); +var assert = require('assert'); + +describe('SCSS', function() { + var comb; + + beforeEach(function() { + comb = new Comb(); + }); + + describe('Parsing', function() { + it('Should parse nested rules', function() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; a { top: 0; } }' + ), + 'div { color: tomato; a { top: 0; } }' + ); + }); + + it('Should parse parent selector &', function() { + comb.configure({'sort-order': [ + ['top', 'left', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; &.top { color: nani; top: 0; } left: 0; }' + ), + 'div { left: 0; color: tomato; &.top {top: 0; color: nani; }}' + ); + }); + + it('Should parse nested properties', function() { + comb.configure({'sort-order': [ + ['left', 'color', 'font'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; font: 2px/3px { family: fantasy; size: 30em; } left: 0; }' + ), + 'div {left: 0; color: tomato; font: 2px/3px { family: fantasy; size: 30em; } }' + ); + }); + + it('Should parse variables', function() { + comb.configure({}); + assert.equal( + comb.processString( + '$red: tomato; div { color: $tomato; }' + ), + '$red: tomato; div { color: $tomato; }' + ); + }); + + it('Should parse interpolated variables inside selectors', function() { + comb.configure({'sort-order': [ + ['top', 'left', 'color'] + ]}); + assert.equal( + comb.processString( + 'div.#{$nani} {color:tomato;top:0;}' + ), + 'div.#{$nani} {top:0;color:tomato;}' + ); + }); + + it('Should parse interpolated variables inside values', function() { + comb.configure({'sort-order': [ + ['top', 'left', 'color'] + ]}); + assert.equal( + comb.processString( + 'div {color:#{$tomato};top:0;}' + ), + 'div {top:0;color:#{$tomato};}' + ); + }); + + it('Should parse defaults', function() { + comb.configure({'sort-order': [ + ['top', 'left', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato !default; top: 0; }' + ), + 'div {top: 0; color: tomato !default; }' + ); + }); + + it('Should parse @import', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @import "foo.css"; top: 0; }' + ), + 'div { @import "foo.css"; top: 0; }' + ); + }); + + it('Should parse @include', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @include nani($panda); top: 0; }' + ), + 'div { @include nani($panda); top: 0; }' + ); + }); + + it('Should parse nested @media', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}' + ), + 'div {\n' + + ' @media screen and (orientation: landscape) {\n' + + ' color: tomato;\n' + + ' }\n' + + ' top: 0;\n' + + '}' + ); + }); + + it('Should parse @extend with classes', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @extend .nani; top: 0; }' + ), + 'div { @extend .nani; top: 0; }' + ); + }); + + it('Should parse @extend with placeholders', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @extend %nani; top: 0; }' + ), + 'div { @extend %nani; top: 0; }' + ); + }); + + it('Should parse @warn', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @warn "nani"; top: 0; }' + ), + 'div { @warn "nani"; top: 0; }' + ); + }); + + it('Should parse @if', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @if $type == ocean { top: 0; } }' + ), + 'div { @if $type == ocean { top: 0; } }' + ); + }); + + it('Should parse @if and @else', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @if $type == ocean { top: 0; } @else { left: 0; } }' + ), + 'div { @if $type == ocean { top: 0; } @else { left: 0; } }' + ); + }); + + it('Should parse @if and @else if', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }' + ), + 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }' + ); + }); + + it('Should parse @for', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div {\n' + + ' @for $i from 1 through 3 {\n' + + ' .item-#{$i} { width: 2em * 1; }\n' + + ' }\n' + + '}' + ), + 'div {\n' + + ' @for $i from 1 through 3 {\n' + + ' .item-#{$i} { width: 2em * 1; }\n' + + ' }\n' + + '}' + ); + }); + + it('Should parse @each', function() { + comb.configure({}); + assert.equal( + comb.processString( + 'div {\n' + + ' @each $animal in puma, sea-slug, erget {\n' + + ' .#{$animal}-icon { background-image: url("/images/#{$animal}.png"); }\n' + + ' }\n' + + '}' + ), + '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() { + comb.configure({}); + assert.equal( + comb.processString( + 'div {\n' + + ' @while $i > 6 {\n' + + ' .item { width: 2em * $i; }\n' + + ' $i: $i - 2;\n' + + ' }\n' + + '}' + ), + 'div {\n' + + ' @while $i > 6 {\n' + + ' .item { width: 2em * $i; }\n' + + ' $i: $i - 2;\n' + + ' }\n' + + '}' + ); + }); + + it('Should parse mixins', function() { + comb.configure({}); + assert.equal( + comb.processString( + '@mixin nani { color: tomato; } .foo { @include nani; }' + ), + '@mixin nani { color: tomato; } .foo { @include nani; }' + ); + }); + + it('Should parse passing several variables to a mixin', function() { + comb.configure({}); + assert.equal( + comb.processString( + '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }' + ), + '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }' + ); + }); + + it('Should parse passing a list of variables to a mixin', function() { + comb.configure({}); + assert.equal( + comb.processString( + '@mixin nani($shadows...) { box-shadow: $shadows; } .foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' + ), + '@mixin nani($shadows...) { box-shadow: $shadows; } .foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' + ); + }); + + it('Should parse passing a content block to a mixin', function() { + comb.configure({}); + assert.equal( + comb.processString( + '.foo { @include nani { color: tomato; top: 0 } }' + ), + '.foo { @include nani { color: tomato; top: 0 } }' + ); + }); + + it('Should parse @content', function() { + comb.configure({}); + assert.equal( + comb.processString( + '@mixin nani { a { @content; } }' + ), + '@mixin nani { a { @content; } }' + ); + }); + + it('Should parse functions', function() { + comb.configure({}); + assert.equal( + comb.processString( + '@function nani($n) { @return $n * 2; }' + ), + '@function nani($n) { @return $n * 2; }' + ); + }); + }); + + describe('Sorting', function() { + it('Should sort properties inside rules', function() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; top: 0; }' + ), + 'div {top: 0; color: tomato; }' + ); + }); + + it('Should sort properties inside nested rules', function() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; a { color: nani; top: 0; } }' + ), + 'div { color: tomato; a {top: 0; color: nani; } }' + ); + }); + + it('Should sort properties divided by nested rules', function() { + comb.configure({'sort-order': [ + ['top', 'left', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: tomato; a { color: nani; top: 0; } left: 0; }' + ), + 'div { left: 0; color: tomato; a {top: 0; color: nani; }}' + ); + }); + + it('Should group declarations with proper comments and spaces (multiple lines)', function() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal( + comb.processString( + 'div {\n' + + ' color: tomato; /* 1 */\n' + + ' /* 2 */\n' + + ' /* 3 */\n' + + ' top: 0; /* 4 */\n' + + ' /* 5 */\n' + + '}' + ), + '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() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal( + comb.processString( + 'div {/* 1 */ color: tomato; /* 2 */ top: 0; /* 3 */ /* 4 */}' + ), + 'div {top: 0; /* 3 */ /* 4 *//* 1 */ color: tomato; /* 2 */ }' + ); + }); + + it('Should divide properties from different groups with an empty line', function() { + comb.configure({'sort-order': [ + ['top'], ['color'] + ]}); + assert.equal( + comb.processString( + 'div {\n' + + ' color: tomato;\n' + + ' top: 0;\n' + + '}' + ), + 'div {\n' + + ' top: 0;\n' + + '\n' + + ' color: tomato;\n' + + '}' + ); + }); + + it('Should sort variables', function() { + comb.configure({'sort-order': [ + ['$variable', 'color'] + ]}); + assert.equal( + comb.processString( + 'div { color: $tomato; $red: tomato; }' + ), + 'div {$red: tomato; color: $tomato; }' + ); + }); + + it('Should sort imports', function() { + comb.configure({'sort-order': [ + ['$import', 'color'] + ]}); + assert.equal(comb.processString( + 'div { color: tomato; @import "foo.css"; }' + ), + 'div {@import "foo.css"; color: tomato; }'); + }); + + it('Should sort @include-s', function() { + comb.configure({'sort-order': [ + ['$include', 'color'] + ]}); + assert.equal(comb.processString( + 'div { color: tomato; @include .nani; }' + ), + 'div {@include .nani; color: tomato; }'); + }); + + it('Should sort @extend-s', function() { + comb.configure({'sort-order': [ + ['$extend', 'color'] + ]}); + assert.equal(comb.processString( + 'div { color: tomato; @extend %nani; }' + ), + 'div {@extend %nani; color: tomato; }'); + }); + + it('Should sort properties inside blocks passed to mixins', function() { + comb.configure({'sort-order': [ + ['top', 'color'] + ]}); + assert.equal(comb.processString( + '.foo { @include nani { color: tomato; top: 0; } }' + ), + '.foo { @include nani {top: 0; color: tomato; } }'); + }); + }); +}); From fdaa624305d5dc4bd3998420dc654014945a525b Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Mon, 7 Oct 2013 01:47:15 +0400 Subject: [PATCH 03/10] Sass: Support sorting in *.scss files --- lib/options/sort-order.js | 276 ++++++++++++++++++++++++-------------- 1 file changed, 178 insertions(+), 98 deletions(-) diff --git a/lib/options/sort-order.js b/lib/options/sort-order.js index 45270142..231dc668 100644 --- a/lib/options/sort-order.js +++ b/lib/options/sort-order.js @@ -25,126 +25,206 @@ module.exports = { * @param {node} node */ process: function(nodeType, node) { + var order = this._order, // sort order of properties + sorted = [], // list of declarations that should be sorted + deleted = [], // list of nodes that should be removed from parent node + p, // property's name + NODES = ['declaration', 's', 'comment', 'atruleb', 'atrules', 'atruler'], // allowed nodes + SC = ['s', 'comment'], // spaces and comments + i, l, j, nl; // counters for loops + + // TODO: Think it through! if (nodeType !== 'block') return; - 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(); - - 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; - - return groupDelta !== 0 ? groupDelta : propDelta; - }); - - firstSymbols && nodeExt.unshift(firstSymbols); - lastSymbols && nodeExt.push(lastSymbols); - - this._applyNodeExt(node, nodeExt); - }, + // Check every child node. + // If it is declaration (property-value pair, e.g. `color: tomato`), + // 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; + + // Save preceding spaces and comments, if there are any, and mark + // them for removing from parent node: + var sc0 = _checkSC0(i); + if (!sc0) continue; + + if (!node[i]) { + deleted.splice(deleted.length - sc0.length, deleted.length + 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; + // Check if there is a property (e.g. `color`) inside + // the declaration. If not, proceed with the next node: + p = null; + for (j = 1, nl = node[i].length; j < nl; j++) { + if (node[i][j][0] === 'property') { + if (node[i][j][1][0] === 'variable') { + p = '$variable'; + } else { + p = node[i][j][1][1]; + } + break; + } else if (node[i][j][0] === 'atkeyword') { + if (['import', 'include', 'extend'].indexOf(node[i][j][1][1]) > -1) { + p = '$' + node[i][j][1][1]; + } + break; + } + } - for (var i = 0; i < bunch.length; ++i) { - if (flag) { nextBunch.push(bunch[i]); continue; } - if (bunch[i][0] === 'declaration') { declAlready = true; } + if (!p) { + deleted = []; + continue; + } - if (isPrevDeclExists && !declAlready) continue; - if (bunch[i][0] !== 's') continue; + // Combine declaration node with other relevant information + // (property index, semicolon, spaces and comments): + var n = { + node: node[i], + 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: + gi: (order[p] && order[p].group > -1 ? order[p].group + 2 : 10000), // group index + pi: (order[p] && order[p].prop > -1 ? order[p].prop + 2 : 10000) // property index + }; + + // Mark the declaration 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 (node[i + 1] && node[i + 1][0] === 'decldelim') { + n.delim.push(node[i + 1]); + deleted.push(i + 1); + i++; + } - var indexOfNewLine = bunch[i][1].indexOf('\n'); - if (indexOfNewLine === -1) continue; + // Save spaces and comments which follow right after the declaration + // and mark them for removing from parent node: + n.sc1 = _checkSC1(i); - nextBunch.push(['s', bunch[i][1].substr(indexOfNewLine + 1)]); - bunch[i][1] = bunch[i][1].substr(0, indexOfNewLine + 1); + // Move the declaration node to a separate list for further sorting: + sorted.push(n); - flag = i + 1; } - if (nextBunch.length === 1 && nextBunch[0][0] === 's') { - item = nextBunch[0]; - indexOf = item[1].lastIndexOf('\n'); - indexOf !== -1 && (item[1] = item[1].substr(indexOf + 1)); + + // Remove all nodes, that were moved to a `sorted` list, from parent node: + for (i = 0, j = 0, l = deleted.length; i < l; i++, j++) { + // Since every time we remove an element from an array, array's + // length reduces by 1, save number of already removed elements (`j`) + // and use it to find the index of a next element to remove: + node.splice(deleted[i] - j, 1); } - flag && bunch.splice(flag); + // 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.gi !== b.gi) return a.gi - b.gi; - return nextBunch; - }, + // 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.pi - b.pi; + }); - /** - * 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]; + // 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--) { + + // Divide declarations from different groups with an empty line: + if (sorted[i - 1] && sorted[i].gi > sorted[i - 1].gi) { + if (sorted[i].sc0[0] && sorted[i].sc0[0][0] === 's' && + sorted[i].sc0[0][1].match(/\n/g) && + sorted[i].sc0[0][1].match(/\n/g).length < 2) { + sorted[i].sc0.unshift(['s', '\n']); + } + } + + sorted[i].sc0.reverse(); + sorted[i].sc1.reverse(); + + for (j = 0, nl = sorted[i].sc1.length; j < nl; j++) { + node.unshift(sorted[i].sc1[j]); + } + if (sorted[i].delim.length > 0) node.unshift(['decldelim']); + node.unshift(sorted[i].node); + for (j = 0, nl = sorted[i].sc0.length; j < nl; j++) { + node.unshift(sorted[i].sc0[j]); + } } - bunch.push(node); - }, this); + } - nextBunch = this._splitBunch(bunch, prevDeclPropertyName); - extNode.push({ data: bunch, decl: prevDeclPropertyName }); - nextBunch.length && extNode.push({ data: nextBunch }); + /** + * Check if there are any comments or spaces before the declaration + * @returns {Array} List of nodes with spaces and comments + * @private + */ + function _checkSC0 () { + var sc = [], + d = []; + + for (; i < l; i++) { + // If there is no node, or it is nor spaces neither comment, stop: + if (!node[i] || + NODES.indexOf(node[i][0]) === -1) { + return false; + } + + // If node is declaration or @-rule: + if (SC.indexOf(node[i][0]) === -1) break; + + sc.push(node[i]); + d.push(i); + } - return extNode; - }, + deleted = deleted.concat(d); - /** - * 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; + return sc; + } - var indexA = this._order[extNodePrev.decl]; - var indexB = this._order[extNode.decl]; - var isGroupBorder = indexA && indexB && indexA.group !== indexB.group; + /** + * Check if there are any comments or spaces after the declaration + * @returns {Array} List of nodes with spaces and comments + * @private + */ + function _checkSC1 () { + var sc = [], // nodes with spaces and comments + d = []; + + // Check every next node: + for (; i < l; i++) { + // If there is no node, or it is nor spaces neither comment, stop: + if (!node[i + 1] || SC.indexOf(node[i + 1][0]) === -1) break; + + if (node[i + 1][0] === 'comment') { + sc.push(node[i + 1]); + d.push(i + 1); + continue; + } + + var lbIndex = node[i + 1][1].indexOf('\n'); + + if (lbIndex > -1) { + // TODO: Don't push an empty array + sc.push(['s', node[i + 1][1].substring(0, lbIndex)]); + node[i + 1][1] = node[i + 1][1].substring(lbIndex); + break; + } + + sc.push(node[i + 1]); + d.push(i + 1); + } - if (isGroupBorder) extNode.data.unshift(['s', '\n']); - }, + deleted = deleted.concat(d); - /** - * 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); + return sc; + } } - }; From 808c508b65891ee13716d2371fcac19e14bae529 Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Tue, 8 Oct 2013 16:47:10 +0400 Subject: [PATCH 04/10] Update tests for sort-order --- test/integral.expect.css | 1 + test/sort-order.js | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) 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/sort-order.js b/test/sort-order.js index d1e1bbd6..e7ac6a54 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' + '}'; @@ -117,7 +117,8 @@ describe('options/sort-order', function() { }); - it('Should replace custom delimiters by ours', function() { + /* TODO: We should not change code while sorting. If we want to replace delimeters, it should be done with another module, but NOT sort-order. + /*it('Should replace custom delimiters by ours', function() { var config = { 'sort-order': [ @@ -141,6 +142,6 @@ describe('options/sort-order', function() { comb.configure(config); assert.equal(comb.processString(input), expected); - }); + });*/ }); From 00442637a52356f8feb32c67e672537db4f5798e Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Thu, 10 Oct 2013 01:19:26 +0400 Subject: [PATCH 05/10] Clean up --- lib/options/sort-order.js | 335 ++++++++++++++++++++++---------------- test/scss.js | 74 +++++---- test/sort-order.js | 5 +- 3 files changed, 236 insertions(+), 178 deletions(-) diff --git a/lib/options/sort-order.js b/lib/options/sort-order.js index 231dc668..f9ec47c3 100644 --- a/lib/options/sort-order.js +++ b/lib/options/sort-order.js @@ -25,19 +25,167 @@ module.exports = { * @param {node} node */ process: function(nodeType, node) { - var order = this._order, // sort order of properties - sorted = [], // list of declarations that should be sorted - deleted = [], // list of nodes that should be removed from parent node - p, // property's name - NODES = ['declaration', 's', 'comment', 'atruleb', 'atrules', 'atruler'], // allowed nodes - SC = ['s', 'comment'], // spaces and comments - i, l, j, nl; // counters for loops + // Types of nodes that can be sorted: + var NODES = ['atruleb', 'atruler', 'atrules', 'comment', 'declaration', 's']; + // Special @-rules that can be sorted: + var ATRULES = ['extend', 'import', 'include']; + // Spaces and comments: + var SC = ['comment', 's']; + + var currentNode; + // Sort order of properties: + var order = this._order; + // 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; + + // Counters for loops: + var i; + var l; + var j; + var nl; + + /** + * 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 = []; + + for (; i < l; i++) { + currentNode = node[i]; + // If there is no node, or it is nor spaces neither comment, + // stop and do nothing with previously found spaces/comments: + if (!currentNode || + NODES.indexOf(currentNode[0]) === -1) { + return false; + } + + // 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; + + sc.push(currentNode); + d.push(i); + } + + deleted = deleted.concat(d); + + return sc; + }; + + /** + * 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; + + // 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; + + if (currentNode[0] === 'comment') { + sc.push(currentNode); + d.push(i + 1); + continue; + } + + lbIndex = currentNode[1].indexOf('\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; + } + + sc.push(currentNode); + d.push(i + 1); + } + + deleted = deleted.concat(d); + + return sc; + }; + + /** + * 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[0] === 'decldelim') { + extendedNode.delim.push(nextNode); + deleted.push(i + 1); + i++; + } + + // Save spaces and comments which follow right after the declaration + // and mark them for removing from parent node: + extendedNode.sc1 = checkSC1(i); + + return extendedNode; + }; // TODO: Think it through! + // Sort properties only inside blocks: if (nodeType !== 'block') return; // 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++) { @@ -45,186 +193,91 @@ module.exports = { // Save preceding spaces and comments, if there are any, and mark // them for removing from parent node: - var sc0 = _checkSC0(i); + sc0 = checkSC0(); if (!sc0) continue; + // 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 there is a property (e.g. `color`) inside - // the declaration. If not, proceed with the next node: - p = null; + // 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; for (j = 1, nl = node[i].length; j < nl; j++) { - if (node[i][j][0] === 'property') { - if (node[i][j][1][0] === 'variable') { - p = '$variable'; - } else { - p = node[i][j][1][1]; - } + currentNode = node[i][j]; + if (currentNode[0] === 'property') { + propertyName = currentNode[1][0] === 'variable' ? + '$variable' : currentNode[1][1]; break; - } else if (node[i][j][0] === 'atkeyword') { - if (['import', 'include', 'extend'].indexOf(node[i][j][1][1]) > -1) { - p = '$' + node[i][j][1][1]; + } else if (currentNode[0] === 'atkeyword') { + if (ATRULES.indexOf(currentNode[1][1]) > -1) { + propertyName = '$' + currentNode[1][1]; } break; } } - if (!p) { + if (!propertyName) { deleted = []; continue; } - // Combine declaration node with other relevant information - // (property index, semicolon, spaces and comments): - var n = { - node: node[i], - 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: - gi: (order[p] && order[p].group > -1 ? order[p].group + 2 : 10000), // group index - pi: (order[p] && order[p].prop > -1 ? order[p].prop + 2 : 10000) // property index - }; - - // Mark the declaration 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 (node[i + 1] && node[i + 1][0] === 'decldelim') { - n.delim.push(node[i + 1]); - deleted.push(i + 1); - i++; - } - - - // Save spaces and comments which follow right after the declaration - // and mark them for removing from parent node: - n.sc1 = _checkSC1(i); - - // Move the declaration node to a separate list for further sorting: - sorted.push(n); - + // 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 = 0, j = 0, l = deleted.length; i < l; i++, j++) { - // Since every time we remove an element from an array, array's - // length reduces by 1, save number of already removed elements (`j`) - // and use it to find the index of a next element to remove: - node.splice(deleted[i] - j, 1); + 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.gi !== b.gi) return a.gi - b.gi; + 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.pi - b.pi; + 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 (sorted[i - 1] && sorted[i].gi > sorted[i - 1].gi) { - if (sorted[i].sc0[0] && sorted[i].sc0[0][0] === 's' && - sorted[i].sc0[0][1].match(/\n/g) && - sorted[i].sc0[0][1].match(/\n/g).length < 2) { - sorted[i].sc0.unshift(['s', '\n']); + 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']); } } - sorted[i].sc0.reverse(); - sorted[i].sc1.reverse(); + sc0.reverse(); + sc1.reverse(); - for (j = 0, nl = sorted[i].sc1.length; j < nl; j++) { - node.unshift(sorted[i].sc1[j]); + for (j = 0, nl = sc1.length; j < nl; j++) { + node.unshift(sc1[j]); } - if (sorted[i].delim.length > 0) node.unshift(['decldelim']); - node.unshift(sorted[i].node); - for (j = 0, nl = sorted[i].sc0.length; j < nl; j++) { - node.unshift(sorted[i].sc0[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]); } } } - - /** - * Check if there are any comments or spaces before the declaration - * @returns {Array} List of nodes with spaces and comments - * @private - */ - function _checkSC0 () { - var sc = [], - d = []; - - for (; i < l; i++) { - // If there is no node, or it is nor spaces neither comment, stop: - if (!node[i] || - NODES.indexOf(node[i][0]) === -1) { - return false; - } - - // If node is declaration or @-rule: - if (SC.indexOf(node[i][0]) === -1) break; - - sc.push(node[i]); - d.push(i); - } - - deleted = deleted.concat(d); - - return sc; - } - - /** - * Check if there are any comments or spaces after the declaration - * @returns {Array} List of nodes with spaces and comments - * @private - */ - function _checkSC1 () { - var sc = [], // nodes with spaces and comments - d = []; - - // Check every next node: - for (; i < l; i++) { - // If there is no node, or it is nor spaces neither comment, stop: - if (!node[i + 1] || SC.indexOf(node[i + 1][0]) === -1) break; - - if (node[i + 1][0] === 'comment') { - sc.push(node[i + 1]); - d.push(i + 1); - continue; - } - - var lbIndex = node[i + 1][1].indexOf('\n'); - - if (lbIndex > -1) { - // TODO: Don't push an empty array - sc.push(['s', node[i + 1][1].substring(0, lbIndex)]); - node[i + 1][1] = node[i + 1][1].substring(lbIndex); - break; - } - - sc.push(node[i + 1]); - d.push(i + 1); - } - - deleted = deleted.concat(d); - - return sc; - } } }; diff --git a/test/scss.js b/test/scss.js index 679937eb..5a6b8c22 100644 --- a/test/scss.js +++ b/test/scss.js @@ -10,9 +10,9 @@ describe('SCSS', function() { describe('Parsing', function() { it('Should parse nested rules', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; a { top: 0; } }' @@ -22,9 +22,9 @@ describe('SCSS', function() { }); it('Should parse parent selector &', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'left', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; &.top { color: nani; top: 0; } left: 0; }' @@ -34,9 +34,9 @@ describe('SCSS', function() { }); it('Should parse nested properties', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['left', 'color', 'font'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; font: 2px/3px { family: fantasy; size: 30em; } left: 0; }' @@ -56,9 +56,9 @@ describe('SCSS', function() { }); it('Should parse interpolated variables inside selectors', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'left', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div.#{$nani} {color:tomato;top:0;}' @@ -68,9 +68,9 @@ describe('SCSS', function() { }); it('Should parse interpolated variables inside values', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'left', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div {color:#{$tomato};top:0;}' @@ -80,9 +80,9 @@ describe('SCSS', function() { }); it('Should parse defaults', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'left', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato !default; top: 0; }' @@ -271,9 +271,11 @@ describe('SCSS', function() { comb.configure({}); assert.equal( comb.processString( - '@mixin nani($shadows...) { box-shadow: $shadows; } .foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' + '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + + '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' ), - '@mixin nani($shadows...) { box-shadow: $shadows; } .foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' + '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + + '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' ); }); @@ -310,9 +312,9 @@ describe('SCSS', function() { describe('Sorting', function() { it('Should sort properties inside rules', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; top: 0; }' @@ -322,9 +324,9 @@ describe('SCSS', function() { }); it('Should sort properties inside nested rules', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; a { color: nani; top: 0; } }' @@ -334,9 +336,9 @@ describe('SCSS', function() { }); it('Should sort properties divided by nested rules', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'left', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: tomato; a { color: nani; top: 0; } left: 0; }' @@ -346,9 +348,9 @@ describe('SCSS', function() { }); it('Should group declarations with proper comments and spaces (multiple lines)', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div {\n' + @@ -370,9 +372,9 @@ describe('SCSS', function() { }); it('Should group declarations with proper comments and spaces (single line)', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div {/* 1 */ color: tomato; /* 2 */ top: 0; /* 3 */ /* 4 */}' @@ -382,9 +384,9 @@ describe('SCSS', function() { }); it('Should divide properties from different groups with an empty line', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top'], ['color'] - ]}); + ] }); assert.equal( comb.processString( 'div {\n' + @@ -401,9 +403,9 @@ describe('SCSS', function() { }); it('Should sort variables', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['$variable', 'color'] - ]}); + ] }); assert.equal( comb.processString( 'div { color: $tomato; $red: tomato; }' @@ -413,9 +415,9 @@ describe('SCSS', function() { }); it('Should sort imports', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['$import', 'color'] - ]}); + ] }); assert.equal(comb.processString( 'div { color: tomato; @import "foo.css"; }' ), @@ -423,9 +425,9 @@ describe('SCSS', function() { }); it('Should sort @include-s', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['$include', 'color'] - ]}); + ] }); assert.equal(comb.processString( 'div { color: tomato; @include .nani; }' ), @@ -433,9 +435,9 @@ describe('SCSS', function() { }); it('Should sort @extend-s', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['$extend', 'color'] - ]}); + ] }); assert.equal(comb.processString( 'div { color: tomato; @extend %nani; }' ), @@ -443,9 +445,9 @@ describe('SCSS', function() { }); it('Should sort properties inside blocks passed to mixins', function() { - comb.configure({'sort-order': [ + comb.configure({ 'sort-order': [ ['top', 'color'] - ]}); + ] }); assert.equal(comb.processString( '.foo { @include nani { color: tomato; top: 0; } }' ), diff --git a/test/sort-order.js b/test/sort-order.js index e7ac6a54..ef71325e 100644 --- a/test/sort-order.js +++ b/test/sort-order.js @@ -117,7 +117,10 @@ describe('options/sort-order', function() { }); - /* TODO: We should not change code while sorting. If we want to replace delimeters, it should be done with another module, but NOT sort-order. + /* TODO: We should not change code while sorting. + * If we want to replace delimeters, it should be done with another module, + * but NOT sort-order. + */ /*it('Should replace custom delimiters by ours', function() { var config = { From 39cd1d82366c922769d1a5d21c04573568b6eb1a Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Sun, 13 Oct 2013 17:32:16 +0200 Subject: [PATCH 06/10] Update gonzales to v2.0.0-rc0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Use new method names – Allow processing of *.scss files – Check syntax name (css or scss) - Use one `include` node for both `@include` and `@extend` – Update tests --- .jshint-groups.js | 2 +- lib/csscomb.js | 13 +- lib/options/always-semicolon.js | 2 +- lib/options/remove-empty-rulesets.js | 2 +- lib/options/sort-order.js | 42 +- package.json | 2 +- test/scss.js | 582 ++++++++++++--------------- 7 files changed, 297 insertions(+), 348 deletions(-) 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/lib/csscomb.js b/lib/csscomb.js index f417f011..a4ad8d63 100644 --- a/lib/csscomb.js +++ b/lib/csscomb.js @@ -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 f9ec47c3..beb620b9 100644 --- a/lib/options/sort-order.js +++ b/lib/options/sort-order.js @@ -26,11 +26,10 @@ module.exports = { */ process: function(nodeType, node) { // Types of nodes that can be sorted: - var NODES = ['atruleb', 'atruler', 'atrules', 'comment', 'declaration', 's']; - // Special @-rules that can be sorted: - var ATRULES = ['extend', 'import', 'include']; + var NODES = ['atruleb', 'atruler', 'atrules', 'commentML', 'commentSL', + 'declaration', 's', 'include']; // Spaces and comments: - var SC = ['comment', 's']; + var SC = ['commentML', 'commentSL', 's']; var currentNode; // Sort order of properties: @@ -64,10 +63,9 @@ module.exports = { for (; i < l; i++) { currentNode = node[i]; - // If there is no node, or it is nor spaces neither comment, + // If there is no node left, // stop and do nothing with previously found spaces/comments: - if (!currentNode || - NODES.indexOf(currentNode[0]) === -1) { + if (!currentNode) { return false; } @@ -104,7 +102,7 @@ module.exports = { // If there is no node, or it is nor spaces neither comment, stop: if (!currentNode || SC.indexOf(currentNode[0]) === -1) break; - if (currentNode[0] === 'comment') { + if (['commentML', 'commentSL'].indexOf(currentNode[0]) > -1) { sc.push(currentNode); d.push(i + 1); continue; @@ -166,7 +164,7 @@ module.exports = { // If there is `;` right after the declaration, save it with the // declaration and mark it for removing from parent node: - if (currentNode && nextNode[0] === 'decldelim') { + if (currentNode && nextNode && nextNode[0] === 'decldelim') { extendedNode.delim.push(nextNode); deleted.push(i + 1); i++; @@ -174,7 +172,7 @@ module.exports = { // Save spaces and comments which follow right after the declaration // and mark them for removing from parent node: - extendedNode.sc1 = checkSC1(i); + extendedNode.sc1 = checkSC1(); return extendedNode; }; @@ -207,17 +205,21 @@ module.exports = { // with a valid property (e.g. `color` or `$width`). // If not, proceed with the next node: propertyName = null; - 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') { - if (ATRULES.indexOf(currentNode[1][1]) > -1) { - propertyName = '$' + currentNode[1][1]; + // 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; } - break; } } 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/scss.js b/test/scss.js index 5a6b8c22..0659215a 100644 --- a/test/scss.js +++ b/test/scss.js @@ -3,455 +3,399 @@ 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; a { top: 0; } }' - ), - 'div { color: tomato; a { top: 0; } }' - ); + ] }; + + input = 'div { color: tomato; a { top: 0; } }'; + + expected = 'div { color: tomato; a { top: 0; } }'; }); it('Should parse parent selector &', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'left', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; &.top { color: nani; top: 0; } left: 0; }' - ), - 'div { left: 0; color: tomato; &.top {top: 0; color: nani; }}' - ); + ] }; + + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['left', 'color', 'font'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; font: 2px/3px { family: fantasy; size: 30em; } left: 0; }' - ), - 'div {left: 0; color: tomato; font: 2px/3px { family: fantasy; size: 30em; } }' - ); + ] }; + + 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() { - comb.configure({}); - assert.equal( - comb.processString( - '$red: tomato; div { color: $tomato; }' - ), - '$red: tomato; div { color: $tomato; }' - ); + config = {}; + + input = '$red: tomato; div { color: $tomato; }'; + + expected = '$red: tomato; div { color: $tomato; }'; }); it('Should parse interpolated variables inside selectors', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'left', 'color'] - ] }); - assert.equal( - comb.processString( - 'div.#{$nani} {color:tomato;top:0;}' - ), - 'div.#{$nani} {top:0;color:tomato;}' - ); + ] }; + + input = 'div.#{$nani} {color:tomato;top:0;}'; + + expected = 'div.#{$nani} {top:0;color:tomato;}'; }); it('Should parse interpolated variables inside values', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'left', 'color'] - ] }); - assert.equal( - comb.processString( - 'div {color:#{$tomato};top:0;}' - ), - 'div {top:0;color:#{$tomato};}' - ); + ] }; + + input = 'div {color:#{$tomato};top:0;}'; + + expected = 'div {top:0;color:#{$tomato};}'; }); it('Should parse defaults', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'left', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato !default; top: 0; }' - ), - 'div {top: 0; color: tomato !default; }' - ); + ] }; + + input = 'div { color: tomato !default; top: 0; }'; + + expected = 'div {top: 0; color: tomato !default; }'; }); it('Should parse @import', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @import "foo.css"; top: 0; }' - ), - 'div { @import "foo.css"; top: 0; }' - ); + config = {}; + + input = 'div { @import "foo.css"; top: 0; }'; + + expected = 'div { @import "foo.css"; top: 0; }'; }); it('Should parse @include', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @include nani($panda); top: 0; }' - ), - 'div { @include nani($panda); top: 0; }' - ); + config = {}; + + input = 'div { @include nani($panda); top: 0; }'; + + expected = 'div { @include nani($panda); top: 0; }'; }); it('Should parse nested @media', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div {\n' + - ' @media screen and (orientation: landscape) {\n' + - ' color: tomato;\n' + - ' }\n' + - ' top: 0;\n' + - '}' - ), - 'div {\n' + - ' @media screen and (orientation: landscape) {\n' + - ' color: tomato;\n' + - ' }\n' + - ' top: 0;\n' + - '}' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @extend .nani; top: 0; }' - ), - 'div { @extend .nani; top: 0; }' - ); + config = {}; + + input = 'div { @extend .nani; top: 0; }'; + + expected = 'div { @extend .nani; top: 0; }'; }); it('Should parse @extend with placeholders', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @extend %nani; top: 0; }' - ), - 'div { @extend %nani; top: 0; }' - ); + config = {}; + + input = 'div { @extend %nani; top: 0; }'; + + expected = 'div { @extend %nani; top: 0; }'; }); it('Should parse @warn', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @warn "nani"; top: 0; }' - ), - 'div { @warn "nani"; top: 0; }' - ); + config = {}; + + input = 'div { @warn "nani"; top: 0; }'; + + expected = 'div { @warn "nani"; top: 0; }'; }); it('Should parse @if', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @if $type == ocean { top: 0; } }' - ), - 'div { @if $type == ocean { top: 0; } }' - ); + config = {}; + + input = 'div { @if $type == ocean { top: 0; } }'; + + expected = 'div { @if $type == ocean { top: 0; } }'; }); it('Should parse @if and @else', function() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @if $type == ocean { top: 0; } @else { left: 0; } }' - ), - 'div { @if $type == ocean { top: 0; } @else { left: 0; } }' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }' - ), - 'div { @if $type == ocean { top: 0; } @else if $type == monster { left: 0; } }' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - 'div {\n' + - ' @for $i from 1 through 3 {\n' + - ' .item-#{$i} { width: 2em * 1; }\n' + - ' }\n' + - '}' - ), - 'div {\n' + - ' @for $i from 1 through 3 {\n' + - ' .item-#{$i} { width: 2em * 1; }\n' + - ' }\n' + - '}' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - 'div {\n' + - ' @each $animal in puma, sea-slug, erget {\n' + - ' .#{$animal}-icon { background-image: url("/images/#{$animal}.png"); }\n' + - ' }\n' + - '}' - ), - 'div {\n' + - ' @each $animal in puma, sea-slug, erget {\n' + - ' .#{$animal}-icon { background-image: url("/images/#{$animal}.png"); }\n' + - ' }\n' + - '}' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - 'div {\n' + - ' @while $i > 6 {\n' + - ' .item { width: 2em * $i; }\n' + - ' $i: $i - 2;\n' + - ' }\n' + - '}' - ), - 'div {\n' + - ' @while $i > 6 {\n' + - ' .item { width: 2em * $i; }\n' + - ' $i: $i - 2;\n' + - ' }\n' + - '}' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - '@mixin nani { color: tomato; } .foo { @include nani; }' - ), - '@mixin nani { color: tomato; } .foo { @include nani; }' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }' - ), - '@mixin nani($tomato) { color: $tomato; } .foo { @include nani(red); }' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + - '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' - ), - '@mixin nani($shadows...) { box-shadow: $shadows; }\n' + - '.foo { @include nani(0px 4px 5px #666, 2px 6px 10px #999); }' - ); + 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() { - comb.configure({}); - assert.equal( - comb.processString( - '.foo { @include nani { color: tomato; top: 0 } }' - ), - '.foo { @include nani { color: tomato; top: 0 } }' - ); + config = {}; + + input = '.foo { @include nani { color: tomato; top: 0 } }'; + + expected = '.foo { @include nani { color: tomato; top: 0 } }'; }); it('Should parse @content', function() { - comb.configure({}); - assert.equal( - comb.processString( - '@mixin nani { a { @content; } }' - ), - '@mixin nani { a { @content; } }' - ); + config = {}; + + input = '@mixin nani { a { @content; } }'; + + expected = '@mixin nani { a { @content; } }'; }); it('Should parse functions', function() { - comb.configure({}); - assert.equal( - comb.processString( - '@function nani($n) { @return $n * 2; }' - ), - '@function nani($n) { @return $n * 2; }' - ); + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; top: 0; }' - ), - 'div {top: 0; color: tomato; }' - ); + ] }; + + input = 'div { color: tomato; top: 0; }'; + + expected = 'div {top: 0; color: tomato; }'; }); it('Should sort properties inside nested rules', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; a { color: nani; top: 0; } }' - ), - 'div { color: tomato; a {top: 0; color: nani; } }' - ); + ] }; + + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'left', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: tomato; a { color: nani; top: 0; } left: 0; }' - ), - 'div { left: 0; color: tomato; a {top: 0; color: nani; }}' - ); + ] }; + + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal( - comb.processString( - 'div {\n' + - ' color: tomato; /* 1 */\n' + - ' /* 2 */\n' + - ' /* 3 */\n' + - ' top: 0; /* 4 */\n' + - ' /* 5 */\n' + - '}' - ), - 'div {\n' + - ' /* 2 */\n' + - ' /* 3 */\n' + - ' top: 0; /* 4 */\n' + - ' color: tomato; /* 1 */\n' + - ' /* 5 */\n' + - '}' - ); + ] }; + + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal( - comb.processString( - 'div {/* 1 */ color: tomato; /* 2 */ top: 0; /* 3 */ /* 4 */}' - ), - 'div {top: 0; /* 3 */ /* 4 *//* 1 */ color: tomato; /* 2 */ }' - ); + ] }; + + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top'], ['color'] - ] }); - assert.equal( - comb.processString( - 'div {\n' + - ' color: tomato;\n' + - ' top: 0;\n' + - '}' - ), - 'div {\n' + - ' top: 0;\n' + - '\n' + - ' color: tomato;\n' + - '}' - ); + ] }; + + input = 'div {\n' + + ' color: tomato;\n' + + ' top: 0;\n' + + '}'; + + expected = 'div {\n' + + ' top: 0;\n' + + '\n' + + ' color: tomato;\n' + + '}'; }); it('Should sort variables', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['$variable', 'color'] - ] }); - assert.equal( - comb.processString( - 'div { color: $tomato; $red: tomato; }' - ), - 'div {$red: tomato; color: $tomato; }' - ); + ] }; + + input = 'div { color: $tomato; $red: tomato; }'; + + expected = 'div {$red: tomato; color: $tomato; }'; }); it('Should sort imports', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['$import', 'color'] - ] }); - assert.equal(comb.processString( - 'div { color: tomato; @import "foo.css"; }' - ), - 'div {@import "foo.css"; color: tomato; }'); + ] }; + + input = 'div { color: tomato; @import "foo.css"; }'; + + expected = 'div {@import "foo.css"; color: tomato; }'; }); it('Should sort @include-s', function() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['$include', 'color'] - ] }); - assert.equal(comb.processString( - 'div { color: tomato; @include .nani; }' - ), - 'div {@include .nani; color: tomato; }'); + ] }; + + input = 'div { color: tomato; @include .nani; }'; + + expected = 'div {@include .nani; color: tomato; }'; }); it('Should sort @extend-s', function() { - comb.configure({ 'sort-order': [ - ['$extend', 'color'] - ] }); - assert.equal(comb.processString( - 'div { color: tomato; @extend %nani; }' - ), - 'div {@extend %nani; color: tomato; }'); + 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() { - comb.configure({ 'sort-order': [ + config = { 'sort-order': [ ['top', 'color'] - ] }); - assert.equal(comb.processString( - '.foo { @include nani { color: tomato; top: 0; } }' - ), - '.foo { @include nani {top: 0; color: tomato; } }'); + ] }; + + input = '.foo { @include nani { color: tomato; top: 0; } }'; + + expected = '.foo { @include nani {top: 0; color: tomato; } }'; }); }); }); From 2f37068b34cabc4bd73d8dd273f36203859ef4b7 Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Sat, 26 Oct 2013 14:56:01 +0400 Subject: [PATCH 07/10] Less: Add tests for sort-order option --- test/less.js | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 test/less.js diff --git a/test/less.js b/test/less.js new file mode 100644 index 00000000..8d8107e7 --- /dev/null +++ b/test/less.js @@ -0,0 +1,313 @@ +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' + + ' }'; + }); + }); +}); From 97196c73ac0d627da0ebf0e50e408efa7dd0159e Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Sat, 26 Oct 2013 15:46:28 +0400 Subject: [PATCH 08/10] Update README Add examples of: - sorting properties in *.scss files - using keywords (`$variable`, `$include` and `$import`) in sort-order config. --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 2edcb2bc..f473a41a 100644 --- a/README.md +++ b/README.md @@ -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: From a074488abfd75f5335e1f0af3ff02867741fa358 Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Thu, 31 Oct 2013 23:26:44 +0200 Subject: [PATCH 09/10] Sort order: Remove unnecessary test We should not change code while sorting. If we want to replace delimeters, it should be done with another module, but not sort-order. --- test/sort-order.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/test/sort-order.js b/test/sort-order.js index ef71325e..21e310ad 100644 --- a/test/sort-order.js +++ b/test/sort-order.js @@ -116,35 +116,4 @@ describe('options/sort-order', function() { assert.equal(comb.processString(input), expected); }); - - /* TODO: We should not change code while sorting. - * If we want to replace delimeters, it should be done with another module, - * but NOT sort-order. - */ - /*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); - - });*/ - }); From fa747e6ece88fade18dbe52ecd64ad2735eb72fd Mon Sep 17 00:00:00 2001 From: Tony Ganch Date: Sat, 2 Nov 2013 09:42:28 +0400 Subject: [PATCH 10/10] Less: Add test for csscomb/csscomb#225 --- test/less.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/less.js b/test/less.js index 8d8107e7..0c35daee 100644 --- a/test/less.js +++ b/test/less.js @@ -309,5 +309,23 @@ describe('LESS', function() { ' 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' + + '}'; + }); }); });