1
|
'use strict';
|
2
|
|
3
|
var Node = require('snapdragon-node');
|
4
|
var utils = require('./utils');
|
5
|
|
6
|
/**
|
7
|
* Braces parsers
|
8
|
*/
|
9
|
|
10
|
module.exports = function(braces, options) {
|
11
|
braces.parser
|
12
|
.set('bos', function() {
|
13
|
if (!this.parsed) {
|
14
|
this.ast = this.nodes[0] = new Node(this.ast);
|
15
|
}
|
16
|
})
|
17
|
|
18
|
/**
|
19
|
* Character parsers
|
20
|
*/
|
21
|
|
22
|
.set('escape', function() {
|
23
|
var pos = this.position();
|
24
|
var m = this.match(/^(?:\\(.)|\$\{)/);
|
25
|
if (!m) return;
|
26
|
|
27
|
var prev = this.prev();
|
28
|
var last = utils.last(prev.nodes);
|
29
|
|
30
|
var node = pos(new Node({
|
31
|
type: 'text',
|
32
|
multiplier: 1,
|
33
|
val: m[0]
|
34
|
}));
|
35
|
|
36
|
if (node.val === '\\\\') {
|
37
|
return node;
|
38
|
}
|
39
|
|
40
|
if (node.val === '${') {
|
41
|
var str = this.input;
|
42
|
var idx = -1;
|
43
|
var ch;
|
44
|
|
45
|
while ((ch = str[++idx])) {
|
46
|
this.consume(1);
|
47
|
node.val += ch;
|
48
|
if (ch === '\\') {
|
49
|
node.val += str[++idx];
|
50
|
continue;
|
51
|
}
|
52
|
if (ch === '}') {
|
53
|
break;
|
54
|
}
|
55
|
}
|
56
|
}
|
57
|
|
58
|
if (this.options.unescape !== false) {
|
59
|
node.val = node.val.replace(/\\([{}])/g, '$1');
|
60
|
}
|
61
|
|
62
|
if (last.val === '"' && this.input.charAt(0) === '"') {
|
63
|
last.val = node.val;
|
64
|
this.consume(1);
|
65
|
return;
|
66
|
}
|
67
|
|
68
|
return concatNodes.call(this, pos, node, prev, options);
|
69
|
})
|
70
|
|
71
|
/**
|
72
|
* Brackets: "[...]" (basic, this is overridden by
|
73
|
* other parsers in more advanced implementations)
|
74
|
*/
|
75
|
|
76
|
.set('bracket', function() {
|
77
|
var isInside = this.isInside('brace');
|
78
|
var pos = this.position();
|
79
|
var m = this.match(/^(?:\[([!^]?)([^\]]{2,}|\]-)(\]|[^*+?]+)|\[)/);
|
80
|
if (!m) return;
|
81
|
|
82
|
var prev = this.prev();
|
83
|
var val = m[0];
|
84
|
var negated = m[1] ? '^' : '';
|
85
|
var inner = m[2] || '';
|
86
|
var close = m[3] || '';
|
87
|
|
88
|
if (isInside && prev.type === 'brace') {
|
89
|
prev.text = prev.text || '';
|
90
|
prev.text += val;
|
91
|
}
|
92
|
|
93
|
var esc = this.input.slice(0, 2);
|
94
|
if (inner === '' && esc === '\\]') {
|
95
|
inner += esc;
|
96
|
this.consume(2);
|
97
|
|
98
|
var str = this.input;
|
99
|
var idx = -1;
|
100
|
var ch;
|
101
|
|
102
|
while ((ch = str[++idx])) {
|
103
|
this.consume(1);
|
104
|
if (ch === ']') {
|
105
|
close = ch;
|
106
|
break;
|
107
|
}
|
108
|
inner += ch;
|
109
|
}
|
110
|
}
|
111
|
|
112
|
return pos(new Node({
|
113
|
type: 'bracket',
|
114
|
val: val,
|
115
|
escaped: close !== ']',
|
116
|
negated: negated,
|
117
|
inner: inner,
|
118
|
close: close
|
119
|
}));
|
120
|
})
|
121
|
|
122
|
/**
|
123
|
* Empty braces (we capture these early to
|
124
|
* speed up processing in the compiler)
|
125
|
*/
|
126
|
|
127
|
.set('multiplier', function() {
|
128
|
var isInside = this.isInside('brace');
|
129
|
var pos = this.position();
|
130
|
var m = this.match(/^\{((?:,|\{,+\})+)\}/);
|
131
|
if (!m) return;
|
132
|
|
133
|
this.multiplier = true;
|
134
|
var prev = this.prev();
|
135
|
var val = m[0];
|
136
|
|
137
|
if (isInside && prev.type === 'brace') {
|
138
|
prev.text = prev.text || '';
|
139
|
prev.text += val;
|
140
|
}
|
141
|
|
142
|
var node = pos(new Node({
|
143
|
type: 'text',
|
144
|
multiplier: 1,
|
145
|
match: m,
|
146
|
val: val
|
147
|
}));
|
148
|
|
149
|
return concatNodes.call(this, pos, node, prev, options);
|
150
|
})
|
151
|
|
152
|
/**
|
153
|
* Open
|
154
|
*/
|
155
|
|
156
|
.set('brace.open', function() {
|
157
|
var pos = this.position();
|
158
|
var m = this.match(/^\{(?!(?:[^\\}]?|,+)\})/);
|
159
|
if (!m) return;
|
160
|
|
161
|
var prev = this.prev();
|
162
|
var last = utils.last(prev.nodes);
|
163
|
|
164
|
// if the last parsed character was an extglob character
|
165
|
// we need to _not optimize_ the brace pattern because
|
166
|
// it might be mistaken for an extglob by a downstream parser
|
167
|
if (last && last.val && isExtglobChar(last.val.slice(-1))) {
|
168
|
last.optimize = false;
|
169
|
}
|
170
|
|
171
|
var open = pos(new Node({
|
172
|
type: 'brace.open',
|
173
|
val: m[0]
|
174
|
}));
|
175
|
|
176
|
var node = pos(new Node({
|
177
|
type: 'brace',
|
178
|
nodes: []
|
179
|
}));
|
180
|
|
181
|
node.push(open);
|
182
|
prev.push(node);
|
183
|
this.push('brace', node);
|
184
|
})
|
185
|
|
186
|
/**
|
187
|
* Close
|
188
|
*/
|
189
|
|
190
|
.set('brace.close', function() {
|
191
|
var pos = this.position();
|
192
|
var m = this.match(/^\}/);
|
193
|
if (!m || !m[0]) return;
|
194
|
|
195
|
var brace = this.pop('brace');
|
196
|
var node = pos(new Node({
|
197
|
type: 'brace.close',
|
198
|
val: m[0]
|
199
|
}));
|
200
|
|
201
|
if (!this.isType(brace, 'brace')) {
|
202
|
if (this.options.strict) {
|
203
|
throw new Error('missing opening "{"');
|
204
|
}
|
205
|
node.type = 'text';
|
206
|
node.multiplier = 0;
|
207
|
node.escaped = true;
|
208
|
return node;
|
209
|
}
|
210
|
|
211
|
var prev = this.prev();
|
212
|
var last = utils.last(prev.nodes);
|
213
|
if (last.text) {
|
214
|
var lastNode = utils.last(last.nodes);
|
215
|
if (lastNode.val === ')' && /[!@*?+]\(/.test(last.text)) {
|
216
|
var open = last.nodes[0];
|
217
|
var text = last.nodes[1];
|
218
|
if (open.type === 'brace.open' && text && text.type === 'text') {
|
219
|
text.optimize = false;
|
220
|
}
|
221
|
}
|
222
|
}
|
223
|
|
224
|
if (brace.nodes.length > 2) {
|
225
|
var first = brace.nodes[1];
|
226
|
if (first.type === 'text' && first.val === ',') {
|
227
|
brace.nodes.splice(1, 1);
|
228
|
brace.nodes.push(first);
|
229
|
}
|
230
|
}
|
231
|
|
232
|
brace.push(node);
|
233
|
})
|
234
|
|
235
|
/**
|
236
|
* Capture boundary characters
|
237
|
*/
|
238
|
|
239
|
.set('boundary', function() {
|
240
|
var pos = this.position();
|
241
|
var m = this.match(/^[$^](?!\{)/);
|
242
|
if (!m) return;
|
243
|
return pos(new Node({
|
244
|
type: 'text',
|
245
|
val: m[0]
|
246
|
}));
|
247
|
})
|
248
|
|
249
|
/**
|
250
|
* One or zero, non-comma characters wrapped in braces
|
251
|
*/
|
252
|
|
253
|
.set('nobrace', function() {
|
254
|
var isInside = this.isInside('brace');
|
255
|
var pos = this.position();
|
256
|
var m = this.match(/^\{[^,]?\}/);
|
257
|
if (!m) return;
|
258
|
|
259
|
var prev = this.prev();
|
260
|
var val = m[0];
|
261
|
|
262
|
if (isInside && prev.type === 'brace') {
|
263
|
prev.text = prev.text || '';
|
264
|
prev.text += val;
|
265
|
}
|
266
|
|
267
|
return pos(new Node({
|
268
|
type: 'text',
|
269
|
multiplier: 0,
|
270
|
val: val
|
271
|
}));
|
272
|
})
|
273
|
|
274
|
/**
|
275
|
* Text
|
276
|
*/
|
277
|
|
278
|
.set('text', function() {
|
279
|
var isInside = this.isInside('brace');
|
280
|
var pos = this.position();
|
281
|
var m = this.match(/^((?!\\)[^${}[\]])+/);
|
282
|
if (!m) return;
|
283
|
|
284
|
var prev = this.prev();
|
285
|
var val = m[0];
|
286
|
|
287
|
if (isInside && prev.type === 'brace') {
|
288
|
prev.text = prev.text || '';
|
289
|
prev.text += val;
|
290
|
}
|
291
|
|
292
|
var node = pos(new Node({
|
293
|
type: 'text',
|
294
|
multiplier: 1,
|
295
|
val: val
|
296
|
}));
|
297
|
|
298
|
return concatNodes.call(this, pos, node, prev, options);
|
299
|
});
|
300
|
};
|
301
|
|
302
|
/**
|
303
|
* Returns true if the character is an extglob character.
|
304
|
*/
|
305
|
|
306
|
function isExtglobChar(ch) {
|
307
|
return ch === '!' || ch === '@' || ch === '*' || ch === '?' || ch === '+';
|
308
|
}
|
309
|
|
310
|
/**
|
311
|
* Combine text nodes, and calculate empty sets (`{,,}`)
|
312
|
* @param {Function} `pos` Function to calculate node position
|
313
|
* @param {Object} `node` AST node
|
314
|
* @return {Object}
|
315
|
*/
|
316
|
|
317
|
function concatNodes(pos, node, parent, options) {
|
318
|
node.orig = node.val;
|
319
|
var prev = this.prev();
|
320
|
var last = utils.last(prev.nodes);
|
321
|
var isEscaped = false;
|
322
|
|
323
|
if (node.val.length > 1) {
|
324
|
var a = node.val.charAt(0);
|
325
|
var b = node.val.slice(-1);
|
326
|
|
327
|
isEscaped = (a === '"' && b === '"')
|
328
|
|| (a === "'" && b === "'")
|
329
|
|| (a === '`' && b === '`');
|
330
|
}
|
331
|
|
332
|
if (isEscaped && options.unescape !== false) {
|
333
|
node.val = node.val.slice(1, node.val.length - 1);
|
334
|
node.escaped = true;
|
335
|
}
|
336
|
|
337
|
if (node.match) {
|
338
|
var match = node.match[1];
|
339
|
if (!match || match.indexOf('}') === -1) {
|
340
|
match = node.match[0];
|
341
|
}
|
342
|
|
343
|
// replace each set with a single ","
|
344
|
var val = match.replace(/\{/g, ',').replace(/\}/g, '');
|
345
|
node.multiplier *= val.length;
|
346
|
node.val = '';
|
347
|
}
|
348
|
|
349
|
var simpleText = last.type === 'text'
|
350
|
&& last.multiplier === 1
|
351
|
&& node.multiplier === 1
|
352
|
&& node.val;
|
353
|
|
354
|
if (simpleText) {
|
355
|
last.val += node.val;
|
356
|
return;
|
357
|
}
|
358
|
|
359
|
prev.push(node);
|
360
|
}
|