1var balanced = require('balanced-match');
2
3module.exports = expandTop;
4
5var escSlash = '\0SLASH'+Math.random()+'\0';
6var escOpen = '\0OPEN'+Math.random()+'\0';
7var escClose = '\0CLOSE'+Math.random()+'\0';
8var escComma = '\0COMMA'+Math.random()+'\0';
9var escPeriod = '\0PERIOD'+Math.random()+'\0';
10
11function numeric(str) {
12  return parseInt(str, 10) == str
13    ? parseInt(str, 10)
14    : str.charCodeAt(0);
15}
16
17function escapeBraces(str) {
18  return str.split('\\\\').join(escSlash)
19            .split('\\{').join(escOpen)
20            .split('\\}').join(escClose)
21            .split('\\,').join(escComma)
22            .split('\\.').join(escPeriod);
23}
24
25function unescapeBraces(str) {
26  return str.split(escSlash).join('\\')
27            .split(escOpen).join('{')
28            .split(escClose).join('}')
29            .split(escComma).join(',')
30            .split(escPeriod).join('.');
31}
32
33
34// Basically just str.split(","), but handling cases
35// where we have nested braced sections, which should be
36// treated as individual members, like {a,{b,c},d}
37function parseCommaParts(str) {
38  if (!str)
39    return [''];
40
41  var parts = [];
42  var m = balanced('{', '}', str);
43
44  if (!m)
45    return str.split(',');
46
47  var pre = m.pre;
48  var body = m.body;
49  var post = m.post;
50  var p = pre.split(',');
51
52  p[p.length-1] += '{' + body + '}';
53  var postParts = parseCommaParts(post);
54  if (post.length) {
55    p[p.length-1] += postParts.shift();
56    p.push.apply(p, postParts);
57  }
58
59  parts.push.apply(parts, p);
60
61  return parts;
62}
63
64function expandTop(str) {
65  if (!str)
66    return [];
67
68  // I don't know why Bash 4.3 does this, but it does.
69  // Anything starting with {} will have the first two bytes preserved
70  // but *only* at the top level, so {},a}b will not expand to anything,
71  // but a{},b}c will be expanded to [a}c,abc].
72  // One could argue that this is a bug in Bash, but since the goal of
73  // this module is to match Bash's rules, we escape a leading {}
74  if (str.substr(0, 2) === '{}') {
75    str = '\\{\\}' + str.substr(2);
76  }
77
78  return expand(escapeBraces(str), true).map(unescapeBraces);
79}
80
81function embrace(str) {
82  return '{' + str + '}';
83}
84function isPadded(el) {
85  return /^-?0\d/.test(el);
86}
87
88function lte(i, y) {
89  return i <= y;
90}
91function gte(i, y) {
92  return i >= y;
93}
94
95function expand(str, isTop) {
96  var expansions = [];
97
98  var m = balanced('{', '}', str);
99  if (!m) return [str];
100
101  // no need to expand pre, since it is guaranteed to be free of brace-sets
102  var pre = m.pre;
103  var post = m.post.length
104    ? expand(m.post, false)
105    : [''];
106
107  if (/\$$/.test(m.pre)) {
108    for (var k = 0; k < post.length; k++) {
109      var expansion = pre+ '{' + m.body + '}' + post[k];
110      expansions.push(expansion);
111    }
112  } else {
113    var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
114    var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
115    var isSequence = isNumericSequence || isAlphaSequence;
116    var isOptions = m.body.indexOf(',') >= 0;
117    if (!isSequence && !isOptions) {
118      // {a},b}
119      if (m.post.match(/,.*\}/)) {
120        str = m.pre + '{' + m.body + escClose + m.post;
121        return expand(str);
122      }
123      return [str];
124    }
125
126    var n;
127    if (isSequence) {
128      n = m.body.split(/\.\./);
129    } else {
130      n = parseCommaParts(m.body);
131      if (n.length === 1) {
132        // x{{a,b}}y ==> x{a}y x{b}y
133        n = expand(n[0], false).map(embrace);
134        if (n.length === 1) {
135          return post.map(function(p) {
136            return m.pre + n[0] + p;
137          });
138        }
139      }
140    }
141
142    // at this point, n is the parts, and we know it's not a comma set
143    // with a single entry.
144    var N;
145
146    if (isSequence) {
147      var x = numeric(n[0]);
148      var y = numeric(n[1]);
149      var width = Math.max(n[0].length, n[1].length)
150      var incr = n.length == 3
151        ? Math.abs(numeric(n[2]))
152        : 1;
153      var test = lte;
154      var reverse = y < x;
155      if (reverse) {
156        incr *= -1;
157        test = gte;
158      }
159      var pad = n.some(isPadded);
160
161      N = [];
162
163      for (var i = x; test(i, y); i += incr) {
164        var c;
165        if (isAlphaSequence) {
166          c = String.fromCharCode(i);
167          if (c === '\\')
168            c = '';
169        } else {
170          c = String(i);
171          if (pad) {
172            var need = width - c.length;
173            if (need > 0) {
174              var z = new Array(need + 1).join('0');
175              if (i < 0)
176                c = '-' + z + c.slice(1);
177              else
178                c = z + c;
179            }
180          }
181        }
182        N.push(c);
183      }
184    } else {
185      N = [];
186
187      for (var j = 0; j < n.length; j++) {
188        N.push.apply(N, expand(n[j], false));
189      }
190    }
191
192    for (var j = 0; j < N.length; j++) {
193      for (var k = 0; k < post.length; k++) {
194        var expansion = pre + N[j] + post[k];
195        if (!isTop || isSequence || expansion)
196          expansions.push(expansion);
197      }
198    }
199  }
200
201  return expansions;
202}
203
204