| '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); |
| } |