Skip to content

Commit fdaa624

Browse files
committed
Sass: Support sorting in *.scss files
1 parent 94c7aba commit fdaa624

1 file changed

Lines changed: 178 additions & 98 deletions

File tree

lib/options/sort-order.js

Lines changed: 178 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -25,126 +25,206 @@ module.exports = {
2525
* @param {node} node
2626
*/
2727
process: function(nodeType, node) {
28+
var order = this._order, // sort order of properties
29+
sorted = [], // list of declarations that should be sorted
30+
deleted = [], // list of nodes that should be removed from parent node
31+
p, // property's name
32+
NODES = ['declaration', 's', 'comment', 'atruleb', 'atrules', 'atruler'], // allowed nodes
33+
SC = ['s', 'comment'], // spaces and comments
34+
i, l, j, nl; // counters for loops
35+
36+
// TODO: Think it through!
2837
if (nodeType !== 'block') return;
2938

30-
var order = this._order;
31-
var nodeExt = this._createNodeExt(node);
32-
var firstSymbols = nodeExt[0].decl || nodeExt.shift();
33-
var lastSymbols = nodeExt[nodeExt.length - 1].decl || nodeExt.pop();
34-
35-
nodeExt.sort(function(a, b) {
36-
var indexA = order[a.decl] || { prop: -1 };
37-
var indexB = order[b.decl] || { prop: -1 };
38-
var groupDelta = indexA.group - indexB.group;
39-
var propDelta = indexA.prop - indexB.prop;
40-
41-
return groupDelta !== 0 ? groupDelta : propDelta;
42-
});
43-
44-
firstSymbols && nodeExt.unshift(firstSymbols);
45-
lastSymbols && nodeExt.push(lastSymbols);
46-
47-
this._applyNodeExt(node, nodeExt);
48-
},
39+
// Check every child node.
40+
// If it is declaration (property-value pair, e.g. `color: tomato`),
41+
// combine it with spaces, semicolon and comments and move them from
42+
// current node to a separate list for further sorting:
43+
for (i = 0, l = node.length; i < l; i++) {
44+
if (NODES.indexOf(node[i][0]) === -1) continue;
45+
46+
// Save preceding spaces and comments, if there are any, and mark
47+
// them for removing from parent node:
48+
var sc0 = _checkSC0(i);
49+
if (!sc0) continue;
50+
51+
if (!node[i]) {
52+
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
53+
break;
54+
}
4955

50-
/**
51-
* Internal. Smart split bunch on '\n' symbols;
52-
* @param {Array} bunch
53-
* @param {Boolean} isPrevDeclExists
54-
*/
55-
_splitBunch: function(bunch, isPrevDeclExists) {
56-
var nextBunch = [];
57-
var declAlready = false;
58-
var flag = false;
59-
var item;
60-
var indexOf;
56+
// Check if there is a property (e.g. `color`) inside
57+
// the declaration. If not, proceed with the next node:
58+
p = null;
59+
for (j = 1, nl = node[i].length; j < nl; j++) {
60+
if (node[i][j][0] === 'property') {
61+
if (node[i][j][1][0] === 'variable') {
62+
p = '$variable';
63+
} else {
64+
p = node[i][j][1][1];
65+
}
66+
break;
67+
} else if (node[i][j][0] === 'atkeyword') {
68+
if (['import', 'include', 'extend'].indexOf(node[i][j][1][1]) > -1) {
69+
p = '$' + node[i][j][1][1];
70+
}
71+
break;
72+
}
73+
}
6174

62-
for (var i = 0; i < bunch.length; ++i) {
63-
if (flag) { nextBunch.push(bunch[i]); continue; }
64-
if (bunch[i][0] === 'declaration') { declAlready = true; }
75+
if (!p) {
76+
deleted = [];
77+
continue;
78+
}
6579

66-
if (isPrevDeclExists && !declAlready) continue;
67-
if (bunch[i][0] !== 's') continue;
80+
// Combine declaration node with other relevant information
81+
// (property index, semicolon, spaces and comments):
82+
var n = {
83+
node: node[i],
84+
sc0: sc0,
85+
delim: [],
86+
// If the declaration's property is in order's list, save its
87+
// group and property indices. Otherwise set them to 10000, so
88+
// declaration appears at the bottom of a sorted list:
89+
gi: (order[p] && order[p].group > -1 ? order[p].group + 2 : 10000), // group index
90+
pi: (order[p] && order[p].prop > -1 ? order[p].prop + 2 : 10000) // property index
91+
};
92+
93+
// Mark the declaration node to remove it later from parent node:
94+
deleted.push(i);
95+
96+
// If there is `;` right after the declaration, save it with the
97+
// declaration and mark it for removing from parent node:
98+
if (node[i + 1] && node[i + 1][0] === 'decldelim') {
99+
n.delim.push(node[i + 1]);
100+
deleted.push(i + 1);
101+
i++;
102+
}
68103

69-
var indexOfNewLine = bunch[i][1].indexOf('\n');
70104

71-
if (indexOfNewLine === -1) continue;
105+
// Save spaces and comments which follow right after the declaration
106+
// and mark them for removing from parent node:
107+
n.sc1 = _checkSC1(i);
72108

73-
nextBunch.push(['s', bunch[i][1].substr(indexOfNewLine + 1)]);
74-
bunch[i][1] = bunch[i][1].substr(0, indexOfNewLine + 1);
109+
// Move the declaration node to a separate list for further sorting:
110+
sorted.push(n);
75111

76-
flag = i + 1;
77112
}
78113

79-
if (nextBunch.length === 1 && nextBunch[0][0] === 's') {
80-
item = nextBunch[0];
81-
indexOf = item[1].lastIndexOf('\n');
82114

83-
indexOf !== -1 && (item[1] = item[1].substr(indexOf + 1));
115+
116+
// Remove all nodes, that were moved to a `sorted` list, from parent node:
117+
for (i = 0, j = 0, l = deleted.length; i < l; i++, j++) {
118+
// Since every time we remove an element from an array, array's
119+
// length reduces by 1, save number of already removed elements (`j`)
120+
// and use it to find the index of a next element to remove:
121+
node.splice(deleted[i] - j, 1);
84122
}
85123

86-
flag && bunch.splice(flag);
124+
// Sort declarations saved for sorting:
125+
sorted.sort(function(a, b) {
126+
// If a's group index is higher than b's group index, in a sorted
127+
// list a appears after b:
128+
if (a.gi !== b.gi) return a.gi - b.gi;
87129

88-
return nextBunch;
89-
},
130+
// If a and b have the same group index, and a's property index is
131+
// higher than b's property index, in a sorted list a appears after
132+
// b:
133+
return a.pi - b.pi;
134+
});
90135

91-
/**
92-
* Internal. Create extended node in format of list
93-
* {
94-
* data:[,,declaration,]
95-
* decl: declarationPropertyName
96-
* };
97-
* @param {node} node
98-
*/
99-
_createNodeExt: function(node) {
100-
var extNode = [];
101-
var bunch = [];
102-
var nextBunch;
103-
var prevDeclPropertyName;
104-
105-
node.forEach(function(node) {
106-
if (node[0] === 'declaration') {
107-
nextBunch = this._splitBunch(bunch, prevDeclPropertyName);
108-
extNode.push({ data: bunch, decl: prevDeclPropertyName });
109-
bunch = nextBunch;
110-
prevDeclPropertyName = node[1][1][1];
136+
// Build all nodes back together. First go sorted declarations, then
137+
// everything else:
138+
if (sorted.length > 0) {
139+
for (i = sorted.length - 1, l = -1; i > l; i--) {
140+
141+
// Divide declarations from different groups with an empty line:
142+
if (sorted[i - 1] && sorted[i].gi > sorted[i - 1].gi) {
143+
if (sorted[i].sc0[0] && sorted[i].sc0[0][0] === 's' &&
144+
sorted[i].sc0[0][1].match(/\n/g) &&
145+
sorted[i].sc0[0][1].match(/\n/g).length < 2) {
146+
sorted[i].sc0.unshift(['s', '\n']);
147+
}
148+
}
149+
150+
sorted[i].sc0.reverse();
151+
sorted[i].sc1.reverse();
152+
153+
for (j = 0, nl = sorted[i].sc1.length; j < nl; j++) {
154+
node.unshift(sorted[i].sc1[j]);
155+
}
156+
if (sorted[i].delim.length > 0) node.unshift(['decldelim']);
157+
node.unshift(sorted[i].node);
158+
for (j = 0, nl = sorted[i].sc0.length; j < nl; j++) {
159+
node.unshift(sorted[i].sc0[j]);
160+
}
111161
}
112-
bunch.push(node);
113-
}, this);
162+
}
114163

115-
nextBunch = this._splitBunch(bunch, prevDeclPropertyName);
116-
extNode.push({ data: bunch, decl: prevDeclPropertyName });
117-
nextBunch.length && extNode.push({ data: nextBunch });
164+
/**
165+
* Check if there are any comments or spaces before the declaration
166+
* @returns {Array} List of nodes with spaces and comments
167+
* @private
168+
*/
169+
function _checkSC0 () {
170+
var sc = [],
171+
d = [];
172+
173+
for (; i < l; i++) {
174+
// If there is no node, or it is nor spaces neither comment, stop:
175+
if (!node[i] ||
176+
NODES.indexOf(node[i][0]) === -1) {
177+
return false;
178+
}
179+
180+
// If node is declaration or @-rule:
181+
if (SC.indexOf(node[i][0]) === -1) break;
182+
183+
sc.push(node[i]);
184+
d.push(i);
185+
}
118186

119-
return extNode;
120-
},
187+
deleted = deleted.concat(d);
121188

122-
/**
123-
* Internal. Add delimiter at the end of groups of properties
124-
* @param {extNode} extNodePrev
125-
* @param {extNode} extNode
126-
*/
127-
_addGroupDelimiter: function(extNodePrev, extNode) {
128-
if (!extNodePrev.decl || !extNode.decl) return;
189+
return sc;
190+
}
129191

130-
var indexA = this._order[extNodePrev.decl];
131-
var indexB = this._order[extNode.decl];
132-
var isGroupBorder = indexA && indexB && indexA.group !== indexB.group;
192+
/**
193+
* Check if there are any comments or spaces after the declaration
194+
* @returns {Array} List of nodes with spaces and comments
195+
* @private
196+
*/
197+
function _checkSC1 () {
198+
var sc = [], // nodes with spaces and comments
199+
d = [];
200+
201+
// Check every next node:
202+
for (; i < l; i++) {
203+
// If there is no node, or it is nor spaces neither comment, stop:
204+
if (!node[i + 1] || SC.indexOf(node[i + 1][0]) === -1) break;
205+
206+
if (node[i + 1][0] === 'comment') {
207+
sc.push(node[i + 1]);
208+
d.push(i + 1);
209+
continue;
210+
}
211+
212+
var lbIndex = node[i + 1][1].indexOf('\n');
213+
214+
if (lbIndex > -1) {
215+
// TODO: Don't push an empty array
216+
sc.push(['s', node[i + 1][1].substring(0, lbIndex)]);
217+
node[i + 1][1] = node[i + 1][1].substring(lbIndex);
218+
break;
219+
}
220+
221+
sc.push(node[i + 1]);
222+
d.push(i + 1);
223+
}
133224

134-
if (isGroupBorder) extNode.data.unshift(['s', '\n']);
135-
},
225+
deleted = deleted.concat(d);
136226

137-
/**
138-
* Internal. apply extNode back at common format node.
139-
* @param {node} node
140-
* @param {extNode} extNode
141-
*/
142-
_applyNodeExt: function(node, extNode) {
143-
node.splice(0, node.length);
144-
extNode.forEach(function(item, i) {
145-
i > 0 && this._addGroupDelimiter(extNode[i - 1], item);
146-
node.push.apply(node, item.data);
147-
}, this);
227+
return sc;
228+
}
148229
}
149-
150230
};

0 commit comments

Comments
 (0)