Repository: sasstools/sass-lint (js)

Issue:
Enhance property-sort-order (other preset orders apart from alphabetical)
One for a rainy day.. It would be great if we could have a few preset orders to chose from (biased for Recess) when configuring the property-sort-order, similar to scss-lint (https://github.com/brigade/scss-lint/tree/master/data/property-sort-orders) and the config (https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md#propertysortorder)

Pull request description:
Enhance Property Sort Order with presets
This PR enhances `property sort order` rule with the addition of presets - RECESS, SMACSS and Concentric. It also adds a few helper functions to allow easy loading of config files, retrieval of file extensions and filenames.

Tests for presets have also been included.

Closes #20 

DCO 1.1 Signed-off-by: Ben Griffith <gt11687@gmail.com>

Patch:
diff --git a/lib/config/property-sort-orders/concentric.yml b/lib/config/property-sort-orders/concentric.yml
new file mode 100644
index 0000000..5fb6fae
--- /dev/null
+++ b/lib/config/property-sort-orders/concentric.yml
@@ -0,0 +1,137 @@
+# Concentric CSS
+# http://rhodesmill.org/brandon/2011/concentric-css/
+
+# Based on: https://github.com/brigade/scss-lint/blob/master/data/property-sort-orders/concentric.txt
+
+order:
+  - 'display'
+  - 'position'
+  - 'top'
+  - 'right'
+  - 'bottom'
+  - 'left'
+
+  - 'columns'
+  - 'column-gap'
+  - 'column-fill'
+  - 'column-rule'
+  - 'column-span'
+  - 'column-count'
+  - 'column-width'
+
+  - 'float'
+  - 'clear'
+
+  - 'transform'
+  - 'transition'
+
+  - 'animation'
+  - 'animation-name'
+  - 'animation-duration'
+  - 'animation-timing-function'
+  - 'animation-delay'
+  - 'animation-iteration-count'
+  - 'animation-direction'
+  - 'animation-fill-mode'
+  - 'animation-play-state'
+
+  - 'visibility'
+  - 'opacity'
+  - 'z-index'
+
+  - 'margin'
+  - 'margin-top'
+  - 'margin-right'
+  - 'margin-bottom'
+  - 'margin-left'
+
+  - 'outline'
+
+  - 'border'
+  - 'border-top'
+  - 'border-right'
+  - 'border-bottom'
+  - 'border-left'
+  - 'border-width'
+  - 'border-top-width'
+  - 'border-right-width'
+  - 'border-bottom-width'
+  - 'border-left-width'
+
+  - 'border-style'
+  - 'border-top-style'
+  - 'border-right-style'
+  - 'border-bottom-style'
+  - 'border-left-style'
+
+  - 'border-radius'
+  - 'border-top-left-radius'
+  - 'border-top-right-radius'
+  - 'border-bottom-left-radius'
+  - 'border-bottom-right-radius'
+
+  - 'border-color'
+  - 'border-top-color'
+  - 'border-right-color'
+  - 'border-bottom-color'
+  - 'border-left-color'
+
+  - 'box-shadow'
+
+  - 'background'
+  - 'background-color'
+  - 'background-image'
+  - 'background-repeat'
+  - 'background-position'
+  - 'background-size'
+  - 'cursor'
+
+  - 'padding'
+  - 'padding-top'
+  - 'padding-right'
+  - 'padding-bottom'
+  - 'padding-left'
+
+  - 'width'
+  - 'min-width'
+  - 'max-width'
+
+  - 'height'
+  - 'min-height'
+  - 'max-height'
+
+  - 'overflow'
+
+  - 'list-style'
+  - 'caption-side'
+
+  - 'table-layout'
+  - 'border-collapse'
+  - 'border-spacing'
+  - 'empty-cells'
+
+  - 'vertical-align'
+
+  - 'text-align'
+  - 'text-indent'
+  - 'text-transform'
+  - 'text-decoration'
+  - 'text-rendering'
+  - 'text-shadow'
+  - 'text-overflow'
+
+  - 'line-height'
+  - 'word-spacing'
+  - 'letter-spacing'
+  - 'white-space'
+  - 'color'
+
+  - 'font'
+  - 'font-family'
+  - 'font-size'
+  - 'font-weight'
+  - 'font-smoothing'
+  - 'font-style'
+
+  - 'content'
+  - 'quotes'
diff --git a/lib/config/property-sort-orders/recess.yml b/lib/config/property-sort-orders/recess.yml
new file mode 100644
index 0000000..de7ed1c
--- /dev/null
+++ b/lib/config/property-sort-orders/recess.yml
@@ -0,0 +1,149 @@
+# Twitter RECESS - Property order
+# https://github.com/twitter/recess
+order:
+  - 'position'
+  - 'top'
+  - 'right'
+  - 'bottom'
+  - 'left'
+  - 'z-index'
+  - 'display'
+  - 'float'
+  - 'width'
+  - 'height'
+  - 'max-width'
+  - 'max-height'
+  - 'min-width'
+  - 'min-height'
+  - 'padding'
+  - 'padding-top'
+  - 'padding-right'
+  - 'padding-bottom'
+  - 'padding-left'
+  - 'margin'
+  - 'margin-top'
+  - 'margin-right'
+  - 'margin-bottom'
+  - 'margin-left'
+  - 'margin-collapse'
+  - 'margin-top-collapse'
+  - 'margin-right-collapse'
+  - 'margin-bottom-collapse'
+  - 'margin-left-collapse'
+  - 'overflow'
+  - 'overflow-x'
+  - 'overflow-y'
+  - 'clip'
+  - 'clear'
+  - 'font'
+  - 'font-family'
+  - 'font-size'
+  - 'font-smoothing'
+  - 'osx-font-smoothing'
+  - 'font-style'
+  - 'font-weight'
+  - 'hyphens'
+  - 'src'
+  - 'line-height'
+  - 'letter-spacing'
+  - 'word-spacing'
+  - 'color'
+  - 'text-align'
+  - 'text-decoration'
+  - 'text-indent'
+  - 'text-overflow'
+  - 'text-rendering'
+  - 'text-size-adjust'
+  - 'text-shadow'
+  - 'text-transform'
+  - 'word-break'
+  - 'word-wrap'
+  - 'white-space'
+  - 'vertical-align'
+  - 'list-style'
+  - 'list-style-type'
+  - 'list-style-position'
+  - 'list-style-image'
+  - 'pointer-events'
+  - 'cursor'
+  - 'background'
+  - 'background-attachment'
+  - 'background-color'
+  - 'background-image'
+  - 'background-position'
+  - 'background-repeat'
+  - 'background-size'
+  - 'border'
+  - 'border-collapse'
+  - 'border-top'
+  - 'border-right'
+  - 'border-bottom'
+  - 'border-left'
+  - 'border-color'
+  - 'border-image'
+  - 'border-top-color'
+  - 'border-right-color'
+  - 'border-bottom-color'
+  - 'border-left-color'
+  - 'border-spacing'
+  - 'border-style'
+  - 'border-top-style'
+  - 'border-right-style'
+  - 'border-bottom-style'
+  - 'border-left-style'
+  - 'border-width'
+  - 'border-top-width'
+  - 'border-right-width'
+  - 'border-bottom-width'
+  - 'border-left-width'
+  - 'border-radius'
+  - 'border-top-right-radius'
+  - 'border-bottom-right-radius'
+  - 'border-bottom-left-radius'
+  - 'border-top-left-radius'
+  - 'border-radius-topright'
+  - 'border-radius-bottomright'
+  - 'border-radius-bottomleft'
+  - 'border-radius-topleft'
+  - 'content'
+  - 'quotes'
+  - 'outline'
+  - 'outline-offset'
+  - 'opacity'
+  - 'filter'
+  - 'visibility'
+  - 'size'
+  - 'zoom'
+  - 'transform'
+  - 'box-align'
+  - 'box-flex'
+  - 'box-orient'
+  - 'box-pack'
+  - 'box-shadow'
+  - 'box-sizing'
+  - 'table-layout'
+  - 'animation'
+  - 'animation-delay'
+  - 'animation-duration'
+  - 'animation-iteration-count'
+  - 'animation-name'
+  - 'animation-play-state'
+  - 'animation-timing-function'
+  - 'animation-fill-mode'
+  - 'transition'
+  - 'transition-delay'
+  - 'transition-duration'
+  - 'transition-property'
+  - 'transition-timing-function'
+  - 'background-clip'
+  - 'backface-visibility'
+  - 'resize'
+  - 'appearance'
+  - 'user-select'
+  - 'interpolation-mode'
+  - 'direction'
+  - 'marks'
+  - 'page'
+  - 'set-link-source'
+  - 'unicode-bidi'
+  - 'speak'
diff --git a/lib/config/property-sort-orders/smacss.yml b/lib/config/property-sort-orders/smacss.yml
new file mode 100644
index 0000000..55ea690
--- /dev/null
+++ b/lib/config/property-sort-orders/smacss.yml
@@ -0,0 +1,154 @@
+# SMACSS Property Order
+# http://smacss.com/book/formatting
+
+# Based on: https://github.com/brigade/scss-lint/blob/master/data/property-sort-orders/smacss.txt
+
+order:
+
+  # Box
+
+  - 'display'
+  - 'position'
+  - 'top'
+  - 'right'
+  - 'bottom'
+  - 'left'
+
+  - 'flex'
+  - 'flex-basis'
+  - 'flex-direction'
+  - 'flex-flow'
+  - 'flex-grow'
+  - 'flex-shrink'
+  - 'flex-wrap'
+  - 'align-content'
+  - 'align-items'
+  - 'align-self'
+  - 'justify-content'
+  - 'order'
+
+  - 'width'
+  - 'min-width'
+  - 'max-width'
+
+  - 'height'
+  - 'min-height'
+  - 'max-height'
+
+  - 'margin'
+  - 'margin-top'
+  - 'margin-right'
+  - 'margin-bottom'
+  - 'margin-left'
+
+  - 'padding'
+  - 'padding-top'
+  - 'padding-right'
+  - 'padding-bottom'
+  - 'padding-left'
+
+  - 'float'
+  - 'clear'
+
+  - 'columns'
+  - 'column-gap'
+  - 'column-fill'
+  - 'column-rule'
+  - 'column-span'
+  - 'column-count'
+  - 'column-width'
+
+  - 'transform'
+  - 'transition'
+
+  # Border
+
+  - 'border'
+  - 'border-top'
+  - 'border-right'
+  - 'border-bottom'
+  - 'border-left'
+  - 'border-width'
+  - 'border-top-width'
+  - 'border-right-width'
+  - 'border-bottom-width'
+  - 'border-left-width'
+
+  - 'border-style'
+  - 'border-top-style'
+  - 'border-right-style'
+  - 'border-bottom-style'
+  - 'border-left-style'
+
+  - 'border-radius'
+  - 'border-top-left-radius'
+  - 'border-top-right-radius'
+  - 'border-bottom-left-radius'
+  - 'border-bottom-right-radius'
+
+  - 'border-color'
+  - 'border-top-color'
+  - 'border-right-color'
+  - 'border-bottom-color'
+  - 'border-left-color'
+
+  - 'outline'
+  - 'outline-color'
+  - 'outline-offset'
+  - 'outline-style'
+  - 'outline-width'
+
+  # Background
+
+  - 'background'
+  - 'background-color'
+  - 'background-image'
+  - 'background-repeat'
+  - 'background-position'
+  - 'background-size'
+
+  # Text
+
+  - 'color'
+
+  - 'font'
+  - 'font-family'
+  - 'font-size'
+  - 'font-smoothing'
+  - 'font-style'
+  - 'font-variant'
+  - 'font-weight'
+
+  - 'letter-spacing'
+  - 'line-height'
+  - 'list-style'
+
+  - 'text-align'
+  - 'text-decoration'
+  - 'text-indent'
+  - 'text-overflow'
+  - 'text-rendering'
+  - 'text-shadow'
+  - 'text-transform'
+  - 'text-wrap'
+
+  - 'white-space'
+  - 'word-spacing'
+
+  # Other
+
+  - 'border-collapse'
+  - 'border-spacing'
+  - 'box-shadow'
+  - 'caption-side'
+  - 'content'
+  - 'cursor'
+  - 'empty-cells'
+  - 'opacity'
+  - 'overflow'
+  - 'quotes'
+  - 'speak'
+  - 'table-layout'
+  - 'vertical-align'
+  - 'visibility'
+  - 'z-index'
diff --git a/lib/helpers.js b/lib/helpers.js
index 4310303..2be0630 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -1,6 +1,9 @@
 'use strict';
 
-var util = require('util');
+var util = require('util'),
+    fs = require('fs'),
+    path = require('path'),
+    yaml = require('js-yaml');
 
 var helpers = {};
 
@@ -130,4 +133,20 @@ helpers.isValidHex = function (str) {
   return false;
 };
 
+helpers.loadConfigFile = function (configPath) {
+  var fileDir = path.dirname(configPath),
+      fileName = path.basename(configPath),
+      fileExtension = path.extname(fileName),
+      filePath = path.join(__dirname, 'config', fileDir, fileName),
+      file = fs.readFileSync(filePath, 'utf8') || false;
+
+  if (file) {
+    if (fileExtension === '.yml') {
+      return yaml.safeLoad(file);
+    }
+  }
+
+  return file;
+};
+
 module.exports = helpers;
diff --git a/lib/rules/clean-import-paths.js b/lib/rules/clean-import-paths.js
index f8f8992..d4bb8bb 100644
--- a/lib/rules/clean-import-paths.js
+++ b/lib/rules/clean-import-paths.js
@@ -1,25 +1,12 @@
 'use strict';
 
-var helpers = require('../helpers');
+var helpers = require('../helpers'),
+    path = require('path');
 
 var stripQuotes = function (str) {
   return str.substring(1, str.length - 1);
 };
 
-var getFilename = function (path) {
-  return path.split('/').pop().split('\\').pop();
-};
-
-var getExtension = function (filename) {
-  var lastIndex = filename.lastIndexOf('.');
-
-  if (lastIndex < 1) {
-    return false;
-  }
-
-  return filename.substr(lastIndex + 1);
-};
-
 var getImportPath = function (parent) {
   if (parent.first('string')) {
     return parent.first('string');
@@ -47,10 +34,10 @@ module.exports = {
 
           if (importPathNode && importPathNode.type === 'string') {
             var importPath = stripQuotes(importPathNode.content),
-                filename = getFilename(importPath),
-                fileExtension = getExtension(filename);
+                filename = path.basename(importPath),
+                fileExtension = path.extname(filename);
 
-            if (fileExtension !== 'css') {
+            if (fileExtension !== '.css') {
               if (filename.charAt(0) === '_') {
                 if (!parser.options['leading-underscore']) {
                   result = helpers.addUnique(result, {
diff --git a/lib/rules/property-sort-order.js b/lib/rules/property-sort-order.js
index c9108f8..1df6fbf 100644
--- a/lib/rules/property-sort-order.js
+++ b/lib/rules/property-sort-order.js
@@ -2,6 +2,25 @@
 
 var helpers = require('../helpers');
 
+var orderPresets = {
+  'recess': 'recess.yml',
+  'smacss': 'smacss.yml',
+  'concentric': 'concentric.yml'
+};
+
+var getOrderConfig = function (order) {
+  if (typeof order === 'string') {
+    if (orderPresets.hasOwnProperty(order)) {
+      var filename = orderPresets[order],
+          orderConfig = helpers.loadConfigFile('property-sort-orders/' + filename);
+
+      return orderConfig.order;
+    }
+  }
+
+  return false;
+};
+
 var sortProperties = function (obj, order) {
   var keys = Object.keys(obj),
       unknown = [],
@@ -13,6 +32,17 @@ var sortProperties = function (obj, order) {
       keys = keys.sort();
     }
   }
+  else if (typeof order === 'object') {
+    var orderedKeys = [];
+
+    for (i = 0; i < order.length; i++) {
+      if (keys.indexOf(order[i]) !== -1) {
+        orderedKeys.push(order[i]);
+      }
+    }
+
+    keys = orderedKeys;
+  }
   else {
     keys = keys.sort(function (a, b) {
       if (order.indexOf(a) === -1) {
@@ -57,7 +87,8 @@ module.exports = {
     'order': 'alphabetical'
   },
   'detect': function (ast, parser) {
-    var result = [];
+    var result = [],
+        order = getOrderConfig(parser.options.order) || parser.options.order;
 
     ast.traverseByType('block', function (block) {
       var properties = {},
@@ -75,7 +106,8 @@ module.exports = {
           }
         });
 
-        sorted = sortProperties(properties, parser.options.order);
+        sorted = sortProperties(properties, order);
+
         pKeys = Object.keys(properties);
         sKeys = Object.keys(sorted);
 


Test patch:
diff --git a/tests/helpers.js b/tests/helpers.js
index a5afff9..2dad1b3 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -520,4 +520,17 @@ describe('helpers', function () {
     assert.equal(false, result);
     done();
   });
+
+  //////////////////////////////
+  // loadConfigFile
+  //////////////////////////////
+
+  it('loadConfigFile', function (done) {
+
+    var result = helpers.loadConfigFile('../../tests/testFile.txt'),
+        expect = 'This is a test file that test\'s the loadConfigFile helper function.';
+
+    assert.equal(expect, result);
+    done();
+  });
 });
diff --git a/tests/main.js b/tests/main.js
index 6f53c00..7259c8c 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -310,7 +310,9 @@ describe('rule', function () {
   //////////////////////////////
   // Property Sort Order
   //////////////////////////////
-  it('property sort order', function (done) {
+
+  // Default
+  it('property sort order - [order: alphabetical]', function (done) {
     lintFile('property-sort-order.scss', {
       'options': {
         'merge-default-rules': false
@@ -324,6 +326,87 @@ describe('rule', function () {
     });
   });
 
+  it('property sort order - [order: custom]', function (done) {
+    lintFile('property-sort-order.scss', {
+      'options': {
+        'merge-default-rules': false
+      },
+      'rules': {
+        'property-sort-order': [
+          1,
+          {
+            'order': [
+              'height',
+              'width',
+              'display',
+              'color'
+            ]
+          }
+        ]
+      }
+    }, function (data) {
+      assert.equal(4, data.warningCount);
+      done();
+    });
+  });
+
+  it('property sort order - [order: recess]', function (done) {
+    lintFile('property-sort-order.scss', {
+      'options': {
+        'merge-default-rules': false
+      },
+      'rules': {
+        'property-sort-order': [
+          1,
+          {
+            'order': 'recess'
+          }
+        ]
+      }
+    }, function (data) {
+      assert.equal(5, data.warningCount);
+      done();
+    });
+  });
+
+  it('property sort order - [order: smacss]', function (done) {
+    lintFile('property-sort-order.scss', {
+      'options': {
+        'merge-default-rules': false
+      },
+      'rules': {
+        'property-sort-order': [
+          1,
+          {
+            'order': 'smacss'
+          }
+        ]
+      }
+    }, function (data) {
+      assert.equal(5, data.warningCount);
+      done();
+    });
+  });
+
+  it('property sort order - [order: concentric]', function (done) {
+    lintFile('property-sort-order.scss', {
+      'options': {
+        'merge-default-rules': false
+      },
+      'rules': {
+        'property-sort-order': [
+          1,
+          {
+            'order': 'concentric'
+          }
+        ]
+      }
+    }, function (data) {
+      assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
   //////////////////////////////
   // Single Line per Selector
   //////////////////////////////
diff --git a/tests/sass/property-sort-order.scss b/tests/sass/property-sort-order.scss
index ae4a7a3..92eae71 100644
--- a/tests/sass/property-sort-order.scss
+++ b/tests/sass/property-sort-order.scss
@@ -1,5 +1,6 @@
 .foo {
   height: 100vh;
+  display: block;
   width: 100vw;
   border: 1px;
 
diff --git a/tests/testFile.txt b/tests/testFile.txt
new file mode 100644
index 0000000..ca7d696
--- /dev/null
+++ b/tests/testFile.txt
@@ -0,0 +1,1 @@
+This is a test file that test's the loadConfigFile helper function.
\ No newline at end of file