332 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
'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');
 | 
						|
  }
 | 
						|
 | 
						|
  const opts = options || {};
 | 
						|
  const 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})`);
 | 
						|
  }
 | 
						|
 | 
						|
  const ast = { type: 'root', input, nodes: [] };
 | 
						|
  const stack = [ast];
 | 
						|
  let block = ast;
 | 
						|
  let prev = ast;
 | 
						|
  let brackets = 0;
 | 
						|
  const length = input.length;
 | 
						|
  let index = 0;
 | 
						|
  let depth = 0;
 | 
						|
  let value;
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 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) {
 | 
						|
      const 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++;
 | 
						|
 | 
						|
      const dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true;
 | 
						|
      const 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;
 | 
						|
      }
 | 
						|
 | 
						|
      const 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;
 | 
						|
        const 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) {
 | 
						|
      const 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();
 | 
						|
 | 
						|
        const 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)
 | 
						|
      const parent = stack[stack.length - 1];
 | 
						|
      const 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;
 |