blob: 8bf3e92b55f7c0a97f408484832eb66fefbfbccf [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2
3var Node = require('snapdragon-node');
4var utils = require('./utils');
5
6/**
7 * Braces parsers
8 */
9
10module.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
306function 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
317function 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}