You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
360 lines
7.8 KiB
360 lines
7.8 KiB
'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);
|
|
}
|
|
|