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.
		
		
		
		
		
			
		
			
				
					334 lines
				
				6.8 KiB
			
		
		
			
		
	
	
					334 lines
				
				6.8 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const stringify = require('./stringify');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Constants
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const {
							 | 
						||
| 
								 | 
							
								  MAX_LENGTH,
							 | 
						||
| 
								 | 
							
								  CHAR_BACKSLASH, /* \ */
							 | 
						||
| 
								 | 
							
								  CHAR_BACKTICK, /* ` */
							 | 
						||
| 
								 | 
							
								  CHAR_COMMA, /* , */
							 | 
						||
| 
								 | 
							
								  CHAR_DOT, /* . */
							 | 
						||
| 
								 | 
							
								  CHAR_LEFT_PARENTHESES, /* ( */
							 | 
						||
| 
								 | 
							
								  CHAR_RIGHT_PARENTHESES, /* ) */
							 | 
						||
| 
								 | 
							
								  CHAR_LEFT_CURLY_BRACE, /* { */
							 | 
						||
| 
								 | 
							
								  CHAR_RIGHT_CURLY_BRACE, /* } */
							 | 
						||
| 
								 | 
							
								  CHAR_LEFT_SQUARE_BRACKET, /* [ */
							 | 
						||
| 
								 | 
							
								  CHAR_RIGHT_SQUARE_BRACKET, /* ] */
							 | 
						||
| 
								 | 
							
								  CHAR_DOUBLE_QUOTE, /* " */
							 | 
						||
| 
								 | 
							
								  CHAR_SINGLE_QUOTE, /* ' */
							 | 
						||
| 
								 | 
							
								  CHAR_NO_BREAK_SPACE,
							 | 
						||
| 
								 | 
							
								  CHAR_ZERO_WIDTH_NOBREAK_SPACE
							 | 
						||
| 
								 | 
							
								} = require('./constants');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * parse
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const parse = (input, options = {}) => {
							 | 
						||
| 
								 | 
							
								  if (typeof input !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TypeError('Expected a string');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let opts = options || {};
							 | 
						||
| 
								 | 
							
								  let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH;
							 | 
						||
| 
								 | 
							
								  if (input.length > max) {
							 | 
						||
| 
								 | 
							
								    throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let ast = { type: 'root', input, nodes: [] };
							 | 
						||
| 
								 | 
							
								  let stack = [ast];
							 | 
						||
| 
								 | 
							
								  let block = ast;
							 | 
						||
| 
								 | 
							
								  let prev = ast;
							 | 
						||
| 
								 | 
							
								  let brackets = 0;
							 | 
						||
| 
								 | 
							
								  let length = input.length;
							 | 
						||
| 
								 | 
							
								  let index = 0;
							 | 
						||
| 
								 | 
							
								  let depth = 0;
							 | 
						||
| 
								 | 
							
								  let value;
							 | 
						||
| 
								 | 
							
								  let memo = {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Helpers
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const advance = () => input[index++];
							 | 
						||
| 
								 | 
							
								  const push = node => {
							 | 
						||
| 
								 | 
							
								    if (node.type === 'text' && prev.type === 'dot') {
							 | 
						||
| 
								 | 
							
								      prev.type = 'text';
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (prev && prev.type === 'text' && node.type === 'text') {
							 | 
						||
| 
								 | 
							
								      prev.value += node.value;
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    block.nodes.push(node);
							 | 
						||
| 
								 | 
							
								    node.parent = block;
							 | 
						||
| 
								 | 
							
								    node.prev = prev;
							 | 
						||
| 
								 | 
							
								    prev = node;
							 | 
						||
| 
								 | 
							
								    return node;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  push({ type: 'bos' });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  while (index < length) {
							 | 
						||
| 
								 | 
							
								    block = stack[stack.length - 1];
							 | 
						||
| 
								 | 
							
								    value = advance();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Invalid chars
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) {
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Escaped chars
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_BACKSLASH) {
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value: (options.keepEscaping ? value : '') + advance() });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Right square bracket (literal): ']'
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_RIGHT_SQUARE_BRACKET) {
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value: '\\' + value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Left square bracket: '['
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_LEFT_SQUARE_BRACKET) {
							 | 
						||
| 
								 | 
							
								      brackets++;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      let closed = true;
							 | 
						||
| 
								 | 
							
								      let next;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      while (index < length && (next = advance())) {
							 | 
						||
| 
								 | 
							
								        value += next;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (next === CHAR_LEFT_SQUARE_BRACKET) {
							 | 
						||
| 
								 | 
							
								          brackets++;
							 | 
						||
| 
								 | 
							
								          continue;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (next === CHAR_BACKSLASH) {
							 | 
						||
| 
								 | 
							
								          value += advance();
							 | 
						||
| 
								 | 
							
								          continue;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (next === CHAR_RIGHT_SQUARE_BRACKET) {
							 | 
						||
| 
								 | 
							
								          brackets--;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          if (brackets === 0) {
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Parentheses
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_LEFT_PARENTHESES) {
							 | 
						||
| 
								 | 
							
								      block = push({ type: 'paren', nodes: [] });
							 | 
						||
| 
								 | 
							
								      stack.push(block);
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_RIGHT_PARENTHESES) {
							 | 
						||
| 
								 | 
							
								      if (block.type !== 'paren') {
							 | 
						||
| 
								 | 
							
								        push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      block = stack.pop();
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								      block = stack[stack.length - 1];
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Quotes: '|"|`
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) {
							 | 
						||
| 
								 | 
							
								      let open = value;
							 | 
						||
| 
								 | 
							
								      let next;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (options.keepQuotes !== true) {
							 | 
						||
| 
								 | 
							
								        value = '';
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      while (index < length && (next = advance())) {
							 | 
						||
| 
								 | 
							
								        if (next === CHAR_BACKSLASH) {
							 | 
						||
| 
								 | 
							
								          value += next + advance();
							 | 
						||
| 
								 | 
							
								          continue;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (next === open) {
							 | 
						||
| 
								 | 
							
								          if (options.keepQuotes === true) value += next;
							 | 
						||
| 
								 | 
							
								          break;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        value += next;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Left curly brace: '{'
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_LEFT_CURLY_BRACE) {
							 | 
						||
| 
								 | 
							
								      depth++;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      let dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true;
							 | 
						||
| 
								 | 
							
								      let brace = {
							 | 
						||
| 
								 | 
							
								        type: 'brace',
							 | 
						||
| 
								 | 
							
								        open: true,
							 | 
						||
| 
								 | 
							
								        close: false,
							 | 
						||
| 
								 | 
							
								        dollar,
							 | 
						||
| 
								 | 
							
								        depth,
							 | 
						||
| 
								 | 
							
								        commas: 0,
							 | 
						||
| 
								 | 
							
								        ranges: 0,
							 | 
						||
| 
								 | 
							
								        nodes: []
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      block = push(brace);
							 | 
						||
| 
								 | 
							
								      stack.push(block);
							 | 
						||
| 
								 | 
							
								      push({ type: 'open', value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Right curly brace: '}'
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_RIGHT_CURLY_BRACE) {
							 | 
						||
| 
								 | 
							
								      if (block.type !== 'brace') {
							 | 
						||
| 
								 | 
							
								        push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      let type = 'close';
							 | 
						||
| 
								 | 
							
								      block = stack.pop();
							 | 
						||
| 
								 | 
							
								      block.close = true;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      push({ type, value });
							 | 
						||
| 
								 | 
							
								      depth--;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      block = stack[stack.length - 1];
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Comma: ','
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_COMMA && depth > 0) {
							 | 
						||
| 
								 | 
							
								      if (block.ranges > 0) {
							 | 
						||
| 
								 | 
							
								        block.ranges = 0;
							 | 
						||
| 
								 | 
							
								        let open = block.nodes.shift();
							 | 
						||
| 
								 | 
							
								        block.nodes = [open, { type: 'text', value: stringify(block) }];
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      push({ type: 'comma', value });
							 | 
						||
| 
								 | 
							
								      block.commas++;
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Dot: '.'
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (value === CHAR_DOT && depth > 0 && block.commas === 0) {
							 | 
						||
| 
								 | 
							
								      let siblings = block.nodes;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (depth === 0 || siblings.length === 0) {
							 | 
						||
| 
								 | 
							
								        push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (prev.type === 'dot') {
							 | 
						||
| 
								 | 
							
								        block.range = [];
							 | 
						||
| 
								 | 
							
								        prev.value += value;
							 | 
						||
| 
								 | 
							
								        prev.type = 'range';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (block.nodes.length !== 3 && block.nodes.length !== 5) {
							 | 
						||
| 
								 | 
							
								          block.invalid = true;
							 | 
						||
| 
								 | 
							
								          block.ranges = 0;
							 | 
						||
| 
								 | 
							
								          prev.type = 'text';
							 | 
						||
| 
								 | 
							
								          continue;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        block.ranges++;
							 | 
						||
| 
								 | 
							
								        block.args = [];
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (prev.type === 'range') {
							 | 
						||
| 
								 | 
							
								        siblings.pop();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let before = siblings[siblings.length - 1];
							 | 
						||
| 
								 | 
							
								        before.value += prev.value + value;
							 | 
						||
| 
								 | 
							
								        prev = before;
							 | 
						||
| 
								 | 
							
								        block.ranges--;
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      push({ type: 'dot', value });
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Text
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    push({ type: 'text', value });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Mark imbalanced braces and brackets as invalid
							 | 
						||
| 
								 | 
							
								  do {
							 | 
						||
| 
								 | 
							
								    block = stack.pop();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (block.type !== 'root') {
							 | 
						||
| 
								 | 
							
								      block.nodes.forEach(node => {
							 | 
						||
| 
								 | 
							
								        if (!node.nodes) {
							 | 
						||
| 
								 | 
							
								          if (node.type === 'open') node.isOpen = true;
							 | 
						||
| 
								 | 
							
								          if (node.type === 'close') node.isClose = true;
							 | 
						||
| 
								 | 
							
								          if (!node.nodes) node.type = 'text';
							 | 
						||
| 
								 | 
							
								          node.invalid = true;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // get the location of the block on parent.nodes (block's siblings)
							 | 
						||
| 
								 | 
							
								      let parent = stack[stack.length - 1];
							 | 
						||
| 
								 | 
							
								      let index = parent.nodes.indexOf(block);
							 | 
						||
| 
								 | 
							
								      // replace the (invalid) block with it's nodes
							 | 
						||
| 
								 | 
							
								      parent.nodes.splice(index, 1, ...block.nodes);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } while (stack.length > 0);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  push({ type: 'eos' });
							 | 
						||
| 
								 | 
							
								  return ast;
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = parse;
							 |