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