UNPKG

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