361 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			361 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var Node = require('snapdragon-node');
 | |
| var utils = require('./utils');
 | |
| 
 | |
| /**
 | |
|  * Braces parsers
 | |
|  */
 | |
| 
 | |
| module.exports = function(braces, options) {
 | |
|   braces.parser
 | |
|     .set('bos', function() {
 | |
|       if (!this.parsed) {
 | |
|         this.ast = this.nodes[0] = new Node(this.ast);
 | |
|       }
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Character parsers
 | |
|      */
 | |
| 
 | |
|     .set('escape', function() {
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^(?:\\(.)|\$\{)/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var last = utils.last(prev.nodes);
 | |
| 
 | |
|       var node = pos(new Node({
 | |
|         type: 'text',
 | |
|         multiplier: 1,
 | |
|         val: m[0]
 | |
|       }));
 | |
| 
 | |
|       if (node.val === '\\\\') {
 | |
|         return node;
 | |
|       }
 | |
| 
 | |
|       if (node.val === '${') {
 | |
|         var str = this.input;
 | |
|         var idx = -1;
 | |
|         var ch;
 | |
| 
 | |
|         while ((ch = str[++idx])) {
 | |
|           this.consume(1);
 | |
|           node.val += ch;
 | |
|           if (ch === '\\') {
 | |
|             node.val += str[++idx];
 | |
|             continue;
 | |
|           }
 | |
|           if (ch === '}') {
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (this.options.unescape !== false) {
 | |
|         node.val = node.val.replace(/\\([{}])/g, '$1');
 | |
|       }
 | |
| 
 | |
|       if (last.val === '"' && this.input.charAt(0) === '"') {
 | |
|         last.val = node.val;
 | |
|         this.consume(1);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       return concatNodes.call(this, pos, node, prev, options);
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Brackets: "[...]" (basic, this is overridden by
 | |
|      * other parsers in more advanced implementations)
 | |
|      */
 | |
| 
 | |
|     .set('bracket', function() {
 | |
|       var isInside = this.isInside('brace');
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^(?:\[([!^]?)([^\]]{2,}|\]-)(\]|[^*+?]+)|\[)/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var val = m[0];
 | |
|       var negated = m[1] ? '^' : '';
 | |
|       var inner = m[2] || '';
 | |
|       var close = m[3] || '';
 | |
| 
 | |
|       if (isInside && prev.type === 'brace') {
 | |
|         prev.text = prev.text || '';
 | |
|         prev.text += val;
 | |
|       }
 | |
| 
 | |
|       var esc = this.input.slice(0, 2);
 | |
|       if (inner === '' && esc === '\\]') {
 | |
|         inner += esc;
 | |
|         this.consume(2);
 | |
| 
 | |
|         var str = this.input;
 | |
|         var idx = -1;
 | |
|         var ch;
 | |
| 
 | |
|         while ((ch = str[++idx])) {
 | |
|           this.consume(1);
 | |
|           if (ch === ']') {
 | |
|             close = ch;
 | |
|             break;
 | |
|           }
 | |
|           inner += ch;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return pos(new Node({
 | |
|         type: 'bracket',
 | |
|         val: val,
 | |
|         escaped: close !== ']',
 | |
|         negated: negated,
 | |
|         inner: inner,
 | |
|         close: close
 | |
|       }));
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Empty braces (we capture these early to
 | |
|      * speed up processing in the compiler)
 | |
|      */
 | |
| 
 | |
|     .set('multiplier', function() {
 | |
|       var isInside = this.isInside('brace');
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^\{((?:,|\{,+\})+)\}/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       this.multiplier = true;
 | |
|       var prev = this.prev();
 | |
|       var val = m[0];
 | |
| 
 | |
|       if (isInside && prev.type === 'brace') {
 | |
|         prev.text = prev.text || '';
 | |
|         prev.text += val;
 | |
|       }
 | |
| 
 | |
|       var node = pos(new Node({
 | |
|         type: 'text',
 | |
|         multiplier: 1,
 | |
|         match: m,
 | |
|         val: val
 | |
|       }));
 | |
| 
 | |
|       return concatNodes.call(this, pos, node, prev, options);
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Open
 | |
|      */
 | |
| 
 | |
|     .set('brace.open', function() {
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^\{(?!(?:[^\\}]?|,+)\})/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var last = utils.last(prev.nodes);
 | |
| 
 | |
|       // if the last parsed character was an extglob character
 | |
|       // we need to _not optimize_ the brace pattern because
 | |
|       // it might be mistaken for an extglob by a downstream parser
 | |
|       if (last && last.val && isExtglobChar(last.val.slice(-1))) {
 | |
|         last.optimize = false;
 | |
|       }
 | |
| 
 | |
|       var open = pos(new Node({
 | |
|         type: 'brace.open',
 | |
|         val: m[0]
 | |
|       }));
 | |
| 
 | |
|       var node = pos(new Node({
 | |
|         type: 'brace',
 | |
|         nodes: []
 | |
|       }));
 | |
| 
 | |
|       node.push(open);
 | |
|       prev.push(node);
 | |
|       this.push('brace', node);
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Close
 | |
|      */
 | |
| 
 | |
|     .set('brace.close', function() {
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^\}/);
 | |
|       if (!m || !m[0]) return;
 | |
| 
 | |
|       var brace = this.pop('brace');
 | |
|       var node = pos(new Node({
 | |
|         type: 'brace.close',
 | |
|         val: m[0]
 | |
|       }));
 | |
| 
 | |
|       if (!this.isType(brace, 'brace')) {
 | |
|         if (this.options.strict) {
 | |
|           throw new Error('missing opening "{"');
 | |
|         }
 | |
|         node.type = 'text';
 | |
|         node.multiplier = 0;
 | |
|         node.escaped = true;
 | |
|         return node;
 | |
|       }
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var last = utils.last(prev.nodes);
 | |
|       if (last.text) {
 | |
|         var lastNode = utils.last(last.nodes);
 | |
|         if (lastNode.val === ')' && /[!@*?+]\(/.test(last.text)) {
 | |
|           var open = last.nodes[0];
 | |
|           var text = last.nodes[1];
 | |
|           if (open.type === 'brace.open' && text && text.type === 'text') {
 | |
|             text.optimize = false;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (brace.nodes.length > 2) {
 | |
|         var first = brace.nodes[1];
 | |
|         if (first.type === 'text' && first.val === ',') {
 | |
|           brace.nodes.splice(1, 1);
 | |
|           brace.nodes.push(first);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       brace.push(node);
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Capture boundary characters
 | |
|      */
 | |
| 
 | |
|     .set('boundary', function() {
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^[$^](?!\{)/);
 | |
|       if (!m) return;
 | |
|       return pos(new Node({
 | |
|         type: 'text',
 | |
|         val: m[0]
 | |
|       }));
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * One or zero, non-comma characters wrapped in braces
 | |
|      */
 | |
| 
 | |
|     .set('nobrace', function() {
 | |
|       var isInside = this.isInside('brace');
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^\{[^,]?\}/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var val = m[0];
 | |
| 
 | |
|       if (isInside && prev.type === 'brace') {
 | |
|         prev.text = prev.text || '';
 | |
|         prev.text += val;
 | |
|       }
 | |
| 
 | |
|       return pos(new Node({
 | |
|         type: 'text',
 | |
|         multiplier: 0,
 | |
|         val: val
 | |
|       }));
 | |
|     })
 | |
| 
 | |
|     /**
 | |
|      * Text
 | |
|      */
 | |
| 
 | |
|     .set('text', function() {
 | |
|       var isInside = this.isInside('brace');
 | |
|       var pos = this.position();
 | |
|       var m = this.match(/^((?!\\)[^${}[\]])+/);
 | |
|       if (!m) return;
 | |
| 
 | |
|       var prev = this.prev();
 | |
|       var val = m[0];
 | |
| 
 | |
|       if (isInside && prev.type === 'brace') {
 | |
|         prev.text = prev.text || '';
 | |
|         prev.text += val;
 | |
|       }
 | |
| 
 | |
|       var node = pos(new Node({
 | |
|         type: 'text',
 | |
|         multiplier: 1,
 | |
|         val: val
 | |
|       }));
 | |
| 
 | |
|       return concatNodes.call(this, pos, node, prev, options);
 | |
|     });
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Returns true if the character is an extglob character.
 | |
|  */
 | |
| 
 | |
| function isExtglobChar(ch) {
 | |
|   return ch === '!' || ch === '@' || ch === '*' || ch === '?' || ch === '+';
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Combine text nodes, and calculate empty sets (`{,,}`)
 | |
|  * @param {Function} `pos` Function to calculate node position
 | |
|  * @param {Object} `node` AST node
 | |
|  * @return {Object}
 | |
|  */
 | |
| 
 | |
| function concatNodes(pos, node, parent, options) {
 | |
|   node.orig = node.val;
 | |
|   var prev = this.prev();
 | |
|   var last = utils.last(prev.nodes);
 | |
|   var isEscaped = false;
 | |
| 
 | |
|   if (node.val.length > 1) {
 | |
|     var a = node.val.charAt(0);
 | |
|     var b = node.val.slice(-1);
 | |
| 
 | |
|     isEscaped = (a === '"' && b === '"')
 | |
|       || (a === "'" && b === "'")
 | |
|       || (a === '`' && b === '`');
 | |
|   }
 | |
| 
 | |
|   if (isEscaped && options.unescape !== false) {
 | |
|     node.val = node.val.slice(1, node.val.length - 1);
 | |
|     node.escaped = true;
 | |
|   }
 | |
| 
 | |
|   if (node.match) {
 | |
|     var match = node.match[1];
 | |
|     if (!match || match.indexOf('}') === -1) {
 | |
|       match = node.match[0];
 | |
|     }
 | |
| 
 | |
|     // replace each set with a single ","
 | |
|     var val = match.replace(/\{/g, ',').replace(/\}/g, '');
 | |
|     node.multiplier *= val.length;
 | |
|     node.val = '';
 | |
|   }
 | |
| 
 | |
|   var simpleText = last.type === 'text'
 | |
|     && last.multiplier === 1
 | |
|     && node.multiplier === 1
 | |
|     && node.val;
 | |
| 
 | |
|   if (simpleText) {
 | |
|     last.val += node.val;
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   prev.push(node);
 | |
| }
 |