Untitled diff

Created Diff never expires
3 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
382 lines
19 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
393 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;
} 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;


sc0.reverse();
sc0.reverse();
sc1.reverse();
sc1.reverse();
sc2.reverse();
sc2.reverse();


// Divide declarations from different groups with an empty line:
// Divide declarations from different groups with an empty line:
if (prevNode && currentNode.groupIndex > prevNode.groupIndex) {
if ((currentNode.groupComment && !prevNode) || (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];
sc0[0][1] = currentNode.groupComment ? '\n' + sc0[0][1] + currentNode.groupComment + sc0[0][1] : '\n' + sc0[0][1];
}
}
}
}


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]);
}
}
}
}
}
}
}
}
};
};