Untitled diff

Created Diff never expires
10 removals
Words removed11
Total words1628
Words removed (%)0.68
382 lines
30 additions
Words added55
Total words1672
Words added (%)3.29
398 lines
module.exports = {
module.exports = {
name: 'sort-order',
name: 'sort-order',


runBefore: 'space-before-closing-brace',
runBefore: 'space-before-closing-brace',


syntax: ['css', 'less', 'sass', 'scss'],
syntax: ['css', 'less', 'sass', 'scss'],


/**
/**
* Sets handler value.
* Sets handler value.
*
*
* @param {Array} value Option value
* @param {Array} value Option value
* @returns {Array}
* @returns {Array}
*/
*/
setValue: function(value) {
setValue: function(value) {
if (!Array.isArray(value)) throw new Error('The option accepts only array of properties.');
if (!Array.isArray(value)) throw new Error('The option accepts only array of properties.');


var order = {};
var order = {};


if (typeof value[0] === 'string') {
if (typeof value[0] === 'string') {
value.forEach(function(prop, propIndex) {
value.forEach(function(prop, propIndex) {
order[prop] = { group: 0, prop: propIndex };
order[prop] = { group: 0, prop: propIndex };
});
});
} else {
} else {
value.forEach(function(group, groupIndex) {
value.forEach(function(group, groupIndex) {
var groupComment,
isComment = /\/\*(.*?)\*\//g;
group.forEach(function(prop, propIndex) {
group.forEach(function(prop, propIndex) {
order[prop] = { group: groupIndex, prop: propIndex };
if (isComment.test(prop)) {
groupComment = prop.replace(isComment, '$1');
} else {
order[prop] = {
comment: groupComment,
group: groupIndex,
prop: propIndex
};
}
});
});
});
});
}
}


return order;
return order;
},
},


/**
/**
* Processes tree node.
* Processes tree node.
* @param {String} nodeType
* @param {String} nodeType
* @param {node} node
* @param {node} node
*/
*/
process: function(nodeType, node) {
process: function(nodeType, node) {
var _this = this;
var _this = this;
// Types of nodes that can be sorted:
// Types of nodes that can be sorted:
var NODES = ['atruleb', 'atruler', 'atrules', 'commentML', 'commentSL',
var NODES = ['atruleb', 'atruler', 'atrules', 'commentML', 'commentSL',
'declaration', 's', 'include'];
'declaration', 's', 'include'];
// Spaces and comments:
// Spaces and comments:
var SC = ['commentML', 'commentSL', 's'];
var SC = ['commentML', 'commentSL', 's'];


var currentNode;
var currentNode;
// Sort order of properties:
// Sort order of properties:
var order = this.getValue('sort-order');
var order = this.getValue('sort-order');
var syntax = this.getSyntax();
var syntax = this.getSyntax();
// List of declarations that should be sorted:
// List of declarations that should be sorted:
var sorted = [];
var sorted = [];
// list of nodes that should be removed from parent node:
// list of nodes that should be removed from parent node:
var deleted = [];
var deleted = [];
// List of spaces and comments that go before declaration/@-rule:
// List of spaces and comments that go before declaration/@-rule:
var sc0 = [];
var sc0 = [];
// Value to search in sort order: either a declaration's property name
// Value to search in sort order: either a declaration's property name
// (e.g. `color`), or @-rule's special keyword (e.g. `$import`):
// (e.g. `color`), or @-rule's special keyword (e.g. `$import`):
var propertyName;
var propertyName;


// Index to place the nodes that shouldn't be sorted
// Index to place the nodes that shouldn't be sorted
var lastGroupIndex = order['...'] ? order['...'].group : Infinity;
var lastGroupIndex = order['...'] ? order['...'].group : Infinity;
var lastPropertyIndex = order['...'] ? order['...'].prop : Infinity;
var lastPropertyIndex = order['...'] ? order['...'].prop : Infinity;


// Counters for loops:
// Counters for loops:
var i;
var i;
var l;
var l;
var j;
var j;
var nl;
var nl;


/**
/**
* Check if there are any comments or spaces before
* Check if there are any comments or spaces before
* the declaration/@-rule.
* the declaration/@-rule.
* @returns {Array} List of nodes with spaces and comments
* @returns {Array} List of nodes with spaces and comments
*/
*/
var checkSC0 = function() {
var checkSC0 = function() {
// List of nodes with spaces and comments:
// List of nodes with spaces and comments:
var sc = [];
var sc = [];
// List of nodes that can be later deleted from parent node:
// List of nodes that can be later deleted from parent node:
var d = [];
var d = [];


for (; i < l; i++) {
for (; i < l; i++) {
currentNode = node[i];
currentNode = node[i];
// If there is no node left,
// If there is no node left,
// stop and do nothing with previously found spaces/comments:
// stop and do nothing with previously found spaces/comments:
if (!currentNode) {
if (!currentNode) {
return false;
return false;
}
}


// Remove any empty lines:
// Remove any empty lines:
if (currentNode[0] === 's') {
if (currentNode[0] === 's') {
currentNode[1] = currentNode[1].replace(/\n[\s\t\n\r]*\n/, '\n');
currentNode[1] = currentNode[1].replace(/\n[\s\t\n\r]*\n/, '\n');
}
}


// If the node is declaration or @-rule, stop and return all
// If the node is declaration or @-rule, stop and return all
// found nodes with spaces and comments (if there are any):
// found nodes with spaces and comments (if there are any):
if (SC.indexOf(currentNode[0]) === -1) break;
if (SC.indexOf(currentNode[0]) === -1) break;


sc.push(currentNode);
sc.push(currentNode);
d.push(i);
d.push(i);
}
}


deleted = deleted.concat(d);
deleted = deleted.concat(d);


return sc;
return sc;
};
};


/**
/**
* Check if there are any comments or spaces after
* Check if there are any comments or spaces after
* the declaration/@-rule.
* the declaration/@-rule.
* @returns {Array} List of nodes with spaces and comments
* @returns {Array} List of nodes with spaces and comments
* @private
* @private
*/
*/
var checkSC1 = function() {
var checkSC1 = function() {
// List of nodes with spaces and comments:
// List of nodes with spaces and comments:
var sc = [];
var sc = [];
// List of nodes that can be later deleted from parent node:
// List of nodes that can be later deleted from parent node:
var d = [];
var d = [];
// Position of `\n` symbol inside a node with spaces:
// Position of `\n` symbol inside a node with spaces:
var lbIndex;
var lbIndex;


// Check every next node:
// Check every next node:
for (; i < l; i++) {
for (; i < l; i++) {
currentNode = node[i + 1];
currentNode = node[i + 1];
// If there is no node, or it is nor spaces neither comment, stop:
// If there is no node, or it is nor spaces neither comment, stop:
if (!currentNode || SC.indexOf(currentNode[0]) === -1) break;
if (!currentNode || SC.indexOf(currentNode[0]) === -1) break;


// Remove any empty lines:
// Remove any empty lines:
if (currentNode[0] === 's') {
if (currentNode[0] === 's') {
currentNode[1] = currentNode[1].replace(/\n[\s\t\n\r]*\n/, '\n');
currentNode[1] = currentNode[1].replace(/\n[\s\t\n\r]*\n/, '\n');
}
}


if (['commentML', 'commentSL'].indexOf(currentNode[0]) > -1) {
if (['commentML', 'commentSL'].indexOf(currentNode[0]) > -1) {
sc.push(currentNode);
sc.push(currentNode);
d.push(i + 1);
d.push(i + 1);
continue;
continue;
}
}


lbIndex = currentNode[1].indexOf('\n');
lbIndex = currentNode[1].indexOf('\n');


// If there are any line breaks in a node with spaces, stop and
// If there are any line breaks in a node with spaces, stop and
// split the node into two: one with spaces before line break
// split the node into two: one with spaces before line break
// and one with `\n` symbol and everything that goes after.
// and one with `\n` symbol and everything that goes after.
// Combine the first one with declaration/@-rule's node:
// Combine the first one with declaration/@-rule's node:
if (lbIndex > -1) {
if (lbIndex > -1) {
// TODO: Don't push an empty array
// TODO: Don't push an empty array
sc.push(['s', currentNode[1].substring(0, lbIndex)]);
sc.push(['s', currentNode[1].substring(0, lbIndex)]);
currentNode[1] = currentNode[1].substring(lbIndex);
currentNode[1] = currentNode[1].substring(lbIndex);
break;
break;
}
}


sc.push(currentNode);
sc.push(currentNode);
d.push(i + 1);
d.push(i + 1);
}
}


deleted = deleted.concat(d);
deleted = deleted.concat(d);


return sc;
return sc;
};
};


/**
/**
* Combine declaration/@-rule's node with other relevant information:
* Combine declaration/@-rule's node with other relevant information:
* property index, semicolon, spaces and comments.
* property index, semicolon, spaces and comments.
* @returns {Object} Extended node
* @returns {Object} Extended node
*/
*/
var extendNode = function() {
var extendNode = function() {
currentNode = node[i];
currentNode = node[i];
var nextNode = node[i + 1];
var nextNode = node[i + 1];
// Object containing current node, all corresponding spaces,
// Object containing current node, all corresponding spaces,
// comments and other information:
// comments and other information:
var extendedNode;
var extendedNode;
// Check if current node's property name is in sort order.
// Check if current node's property name is in sort order.
// If it is, save information about its indices:
// If it is, save information about its indices:
var orderProperty = order[propertyName];
var orderProperty = order[propertyName];


extendedNode = {
extendedNode = {
i: i,
i: i,
node: currentNode,
node: currentNode,
sc0: sc0,
sc0: sc0,
sc1: [],
sc1: [],
sc2: [],
sc2: [],
delim: []
delim: [],
groupComment: orderProperty.comment
};
};


// If the declaration's property is in order's list, save its
// If the declaration's property is in order's list, save its
// group and property indices. Otherwise set them to 10000, so
// group and property indices. Otherwise set them to 10000, so
// declaration appears at the bottom of a sorted list:
// declaration appears at the bottom of a sorted list:


extendedNode.groupIndex = orderProperty && orderProperty.group > -1 ?
extendedNode.groupIndex = orderProperty && orderProperty.group > -1 ?
orderProperty.group : lastGroupIndex;
orderProperty.group : lastGroupIndex;
extendedNode.propertyIndex = orderProperty && orderProperty.prop > -1 ?
extendedNode.propertyIndex = orderProperty && orderProperty.prop > -1 ?
orderProperty.prop : lastPropertyIndex;
orderProperty.prop : lastPropertyIndex;


// Mark current node to remove it later from parent node:
// Mark current node to remove it later from parent node:
deleted.push(i);
deleted.push(i);


extendedNode.sc1 = checkSC1();
extendedNode.sc1 = checkSC1();


if (extendedNode.sc1.length) {
if (extendedNode.sc1.length) {
currentNode = node[i];
currentNode = node[i];
nextNode = node[i + 1];
nextNode = node[i + 1];
}
}


// If there is `;` right after the declaration, save it with the
// If there is `;` right after the declaration, save it with the
// declaration and mark it for removing from parent node:
// declaration and mark it for removing from parent node:
if (currentNode && nextNode && nextNode[0] === 'declDelim') {
if (currentNode && nextNode && nextNode[0] === 'declDelim') {
extendedNode.delim.push(nextNode);
extendedNode.delim.push(nextNode);
deleted.push(i + 1);
deleted.push(i + 1);
i++;
i++;


if (syntax === 'sass') return extendedNode;
if (syntax === 'sass') return extendedNode;


// Save spaces and comments which follow right after the declaration
// Save spaces and comments which follow right after the declaration
// and mark them for removing from parent node:
// and mark them for removing from parent node:
extendedNode.sc2 = checkSC1();
extendedNode.sc2 = checkSC1();
}
}


return extendedNode;
return extendedNode;
};
};


/**
/**
* Sorts properties alphabetically.
* Sorts properties alphabetically.
*
*
* @param {Object} a First extended node
* @param {Object} a First extended node
* @param {Object} b Second extended node
* @param {Object} b Second extended node
* @returns {Number} `-1` if properties should go in order `a, b`. `1`
* @returns {Number} `-1` if properties should go in order `a, b`. `1`
* if properties should go in order `b, a`.
* if properties should go in order `b, a`.
*/
*/
var sortLeftovers = function(a, b) {
var sortLeftovers = function(a, b) {
var prefixes = ['-webkit-', '-moz-', '-ms-', '-o-', ''];
var prefixes = ['-webkit-', '-moz-', '-ms-', '-o-', ''];
var prefixesRegExp = /^(-webkit-|-moz-|-ms-|-o-)(.*)$/;
var prefixesRegExp = /^(-webkit-|-moz-|-ms-|-o-)(.*)$/;


// Get property name (i.e. `color`, `-o-animation`):
// Get property name (i.e. `color`, `-o-animation`):
a = a.node[1][1][1];
a = a.node[1][1][1];
b = b.node[1][1][1];
b = b.node[1][1][1];


// Get prefix and unprefixed part. For example:
// Get prefix and unprefixed part. For example:
// ['-o-animation', '-o-', 'animation']
// ['-o-animation', '-o-', 'animation']
// ['color', '', 'color']
// ['color', '', 'color']
a = a.match(prefixesRegExp) || [a, '', a];
a = a.match(prefixesRegExp) || [a, '', a];
b = b.match(prefixesRegExp) || [b, '', b];
b = b.match(prefixesRegExp) || [b, '', b];


if (a[2] !== b[2]) {
if (a[2] !== b[2]) {
// If unprefixed parts are different (i.e. `border` and
// If unprefixed parts are different (i.e. `border` and
// `color`), compare them:
// `color`), compare them:
return a[2] < b[2] ? -1 : 1;
return a[2] < b[2] ? -1 : 1;
} else {
} else {
// If unprefixed parts are identical (i.e. `border` in
// If unprefixed parts are identical (i.e. `border` in
// `-moz-border` and `-o-border`), compare prefixes (they
// `-moz-border` and `-o-border`), compare prefixes (they
// should go in the same order they are set in `prefixes` array):
// should go in the same order they are set in `prefixes` array):
return prefixes.indexOf(a[1]) < prefixes.indexOf(b[1]) ? -1 : 1;
return prefixes.indexOf(a[1]) < prefixes.indexOf(b[1]) ? -1 : 1;
}
}
};
};


// TODO: Think it through!
// TODO: Think it through!
// Sort properties only inside blocks:
// Sort properties only inside blocks:
if (nodeType !== 'block') return;
if (nodeType !== 'block') return;


// Check every child node.
// Check every child node.
// If it is declaration (property-value pair, e.g. `color: tomato`),
// If it is declaration (property-value pair, e.g. `color: tomato`),
// or @-rule (e.g. `@include nani`),
// or @-rule (e.g. `@include nani`),
// combine it with spaces, semicolon and comments and move them from
// combine it with spaces, semicolon and comments and move them from
// current node to a separate list for further sorting:
// current node to a separate list for further sorting:
for (i = 0, l = node.length; i < l; i++) {
for (i = 0, l = node.length; i < l; i++) {
if (NODES.indexOf(node[i][0]) === -1) continue;
if (NODES.indexOf(node[i][0]) === -1) continue;


// Save preceding spaces and comments, if there are any, and mark
// Save preceding spaces and comments, if there are any, and mark
// them for removing from parent node:
// them for removing from parent node:
sc0 = checkSC0();
sc0 = checkSC0();
if (!sc0) continue;
if (!sc0) continue;


// If spaces/comments are the last nodes, stop and go to sorting:
// If spaces/comments are the last nodes, stop and go to sorting:
if (!node[i]) {
if (!node[i]) {
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
break;
break;
}
}


// Check if the node needs to be sorted:
// Check if the node needs to be sorted:
// it should be a special @-rule (e.g. `@include`) or a declaration
// it should be a special @-rule (e.g. `@include`) or a declaration
// with a valid property (e.g. `color` or `$width`).
// with a valid property (e.g. `color` or `$width`).
// If not, proceed with the next node:
// If not, proceed with the next node:
propertyName = null;
propertyName = null;
// Look for includes:
// Look for includes:
if (node[i][0] === 'include') {
if (node[i][0] === 'include') {
propertyName = '$include';
propertyName = '$include';
} else {
} else {
for (j = 1, nl = node[i].length; j < nl; j++) {
for (j = 1, nl = node[i].length; j < nl; j++) {
currentNode = node[i][j];
currentNode = node[i][j];
if (currentNode[0] === 'property') {
if (currentNode[0] === 'property') {
propertyName = currentNode[1][0] === 'variable' ?
propertyName = currentNode[1][0] === 'variable' ?
'$variable' : currentNode[1][1];
'$variable' : currentNode[1][1];
break;
break;
} else if (currentNode[0] === 'atkeyword' &&
} else if (currentNode[0] === 'atkeyword' &&
currentNode[1][1] === 'import') { // Look for imports
currentNode[1][1] === 'import') { // Look for imports
propertyName = '$import';
propertyName = '$import';
break;
break;
}
}
}
}
}
}


// If current node is not property-value pair or import or include,
// If current node is not property-value pair or import or include,
// skip it and continue with the next node:
// skip it and continue with the next node:
if (!propertyName) {
if (!propertyName) {
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
continue;
continue;
}
}


// Make an extended node and move it to a separate list for further
// Make an extended node and move it to a separate list for further
// sorting:
// sorting:
sorted.push(extendNode());
sorted.push(extendNode());
}
}


// Remove all nodes, that were moved to a `sorted` list, from parent node:
// Remove all nodes, that were moved to a `sorted` list, from parent node:
for (i = deleted.length - 1; i > -1; i--) {
for (i = deleted.length - 1; i > -1; i--) {
node.splice(deleted[i], 1);
node.splice(deleted[i], 1);
}
}


// Sort declarations saved for sorting:
// Sort declarations saved for sorting:
sorted.sort(function(a, b) {
sorted.sort(function(a, b) {
// If a's group index is higher than b's group index, in a sorted
// If a's group index is higher than b's group index, in a sorted
// list a appears after b:
// list a appears after b:
if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;


// If a and b belong to leftovers and `sort-order-fallback` option
// If a and b belong to leftovers and `sort-order-fallback` option
// is set to `abc`, sort properties alphabetically:
// is set to `abc`, sort properties alphabetically:
if (a.groupIndex === lastGroupIndex &&
if (a.groupIndex === lastGroupIndex &&
_this.getValue('sort-order-fallback')) {
_this.getValue('sort-order-fallback')) {
return sortLeftovers(a, b);
return sortLeftovers(a, b);
}
}


// If a and b have the same group index, and a's property index is
// 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
// higher than b's property index, in a sorted list a appears after
// b:
// b:
if (a.propertyIndex !== b.propertyIndex) return a.propertyIndex - b.propertyIndex;
if (a.propertyIndex !== b.propertyIndex) return a.propertyIndex - b.propertyIndex;


// If a and b have the same group index and the same property index,
// If a and b have the same group index and the same property index,
// in a sorted list they appear in the same order they were in
// in a sorted list they appear in the same order they were in
// original array:
// original array:
return a.i - b.i;
return a.i - b.i;
});
});


// Build all nodes back together. First go sorted declarations, then
// Build all nodes back together. First go sorted declarations, then
// everything else:
// everything else:
if (sorted.length > 0) {
if (sorted.length > 0) {
for (i = sorted.length - 1, l = -1; i > l; i--) {
for (i = sorted.length - 1, l = -1; i > l; i--) {
currentNode = sorted[i];
currentNode = sorted[i];
var prevNode = sorted[i - 1];
var prevNode = sorted[i - 1];
sc0 = currentNode.sc0;
sc0 = currentNode.sc0;
var sc1 = currentNode.sc1;
var sc1 = currentNode.sc1;
var sc2 = currentNode.sc2;
var sc2 = currentNode.sc2;


Text moved to lines 379-382
sc0.reverse();
// Divide declarations from different groups with group comments (if defined) and/or an empty line:
sc1.reverse();
if ((currentNode.groupComment && !prevNode) || (prevNode && currentNode.groupIndex > prevNode.groupIndex)) {
sc2.reverse();

// Divide declarations from different groups with an empty line:
if (prevNode && currentNode.groupIndex > prevNode.groupIndex) {
if (sc0[0] && sc0[0][0] === 's' &&
if (sc0[0] && sc0[0][0] === 's' &&
(this.syntax === 'sass' ||
(this.syntax === 'sass' ||
sc0[0][1].match(/\n/g) &&
sc0[0][1].match(/\n/g) &&
sc0[0][1].match(/\n/g).length < 2)) {
sc0[0][1].match(/\n/g).length < 2)) {
sc0[0][1] = '\n' + sc0[0][1];
if (currentNode.groupComment) {
if (!sc0[1] || (sc0[1] && !currentNode.groupComment.match(sc0[1][1]))) {
sc0.unshift([SC[0], currentNode.groupComment]);
sc0.unshift(sc0[sc0.length - 1]);
}
}
sc0.unshift(sc0[sc0.length - 1]);
}
}
}
}


Text moved from lines 352-355
sc0.reverse();
sc1.reverse();
sc2.reverse();

for (j = 0, nl = sc2.length; j < nl; j++) {
for (j = 0, nl = sc2.length; j < nl; j++) {
node.unshift(sc2[j]);
node.unshift(sc2[j]);
}
}
if (currentNode.delim.length > 0) node.unshift(['declDelim']);
if (currentNode.delim.length > 0) node.unshift(['declDelim']);
for (j = 0, nl = sc1.length; j < nl; j++) {
for (j = 0, nl = sc1.length; j < nl; j++) {
node.unshift(sc1[j]);
node.unshift(sc1[j]);
}
}
node.unshift(currentNode.node);
node.unshift(currentNode.node);

for (j = 0, nl = sc0.length; j < nl; j++) {
for (j = 0, nl = sc0.length; j < nl; j++) {
node.unshift(sc0[j]);
node.unshift(sc0[j]);
}
}
}
}
}
}
}
}
};
};