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