parsers.js
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 }