parse.js
  1  'use strict';
  2  
  3  const stringify = require('./stringify');
  4  
  5  /**
  6   * Constants
  7   */
  8  
  9  const {
 10    MAX_LENGTH,
 11    CHAR_BACKSLASH, /* \ */
 12    CHAR_BACKTICK, /* ` */
 13    CHAR_COMMA, /* , */
 14    CHAR_DOT, /* . */
 15    CHAR_LEFT_PARENTHESES, /* ( */
 16    CHAR_RIGHT_PARENTHESES, /* ) */
 17    CHAR_LEFT_CURLY_BRACE, /* { */
 18    CHAR_RIGHT_CURLY_BRACE, /* } */
 19    CHAR_LEFT_SQUARE_BRACKET, /* [ */
 20    CHAR_RIGHT_SQUARE_BRACKET, /* ] */
 21    CHAR_DOUBLE_QUOTE, /* " */
 22    CHAR_SINGLE_QUOTE, /* ' */
 23    CHAR_NO_BREAK_SPACE,
 24    CHAR_ZERO_WIDTH_NOBREAK_SPACE
 25  } = require('./constants');
 26  
 27  /**
 28   * parse
 29   */
 30  
 31  const parse = (input, options = {}) => {
 32    if (typeof input !== 'string') {
 33      throw new TypeError('Expected a string');
 34    }
 35  
 36    let opts = options || {};
 37    let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH;
 38    if (input.length > max) {
 39      throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`);
 40    }
 41  
 42    let ast = { type: 'root', input, nodes: [] };
 43    let stack = [ast];
 44    let block = ast;
 45    let prev = ast;
 46    let brackets = 0;
 47    let length = input.length;
 48    let index = 0;
 49    let depth = 0;
 50    let value;
 51    let memo = {};
 52  
 53    /**
 54     * Helpers
 55     */
 56  
 57    const advance = () => input[index++];
 58    const push = node => {
 59      if (node.type === 'text' && prev.type === 'dot') {
 60        prev.type = 'text';
 61      }
 62  
 63      if (prev && prev.type === 'text' && node.type === 'text') {
 64        prev.value += node.value;
 65        return;
 66      }
 67  
 68      block.nodes.push(node);
 69      node.parent = block;
 70      node.prev = prev;
 71      prev = node;
 72      return node;
 73    };
 74  
 75    push({ type: 'bos' });
 76  
 77    while (index < length) {
 78      block = stack[stack.length - 1];
 79      value = advance();
 80  
 81      /**
 82       * Invalid chars
 83       */
 84  
 85      if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) {
 86        continue;
 87      }
 88  
 89      /**
 90       * Escaped chars
 91       */
 92  
 93      if (value === CHAR_BACKSLASH) {
 94        push({ type: 'text', value: (options.keepEscaping ? value : '') + advance() });
 95        continue;
 96      }
 97  
 98      /**
 99       * Right square bracket (literal): ']'
100       */
101  
102      if (value === CHAR_RIGHT_SQUARE_BRACKET) {
103        push({ type: 'text', value: '\\' + value });
104        continue;
105      }
106  
107      /**
108       * Left square bracket: '['
109       */
110  
111      if (value === CHAR_LEFT_SQUARE_BRACKET) {
112        brackets++;
113  
114        let closed = true;
115        let next;
116  
117        while (index < length && (next = advance())) {
118          value += next;
119  
120          if (next === CHAR_LEFT_SQUARE_BRACKET) {
121            brackets++;
122            continue;
123          }
124  
125          if (next === CHAR_BACKSLASH) {
126            value += advance();
127            continue;
128          }
129  
130          if (next === CHAR_RIGHT_SQUARE_BRACKET) {
131            brackets--;
132  
133            if (brackets === 0) {
134              break;
135            }
136          }
137        }
138  
139        push({ type: 'text', value });
140        continue;
141      }
142  
143      /**
144       * Parentheses
145       */
146  
147      if (value === CHAR_LEFT_PARENTHESES) {
148        block = push({ type: 'paren', nodes: [] });
149        stack.push(block);
150        push({ type: 'text', value });
151        continue;
152      }
153  
154      if (value === CHAR_RIGHT_PARENTHESES) {
155        if (block.type !== 'paren') {
156          push({ type: 'text', value });
157          continue;
158        }
159        block = stack.pop();
160        push({ type: 'text', value });
161        block = stack[stack.length - 1];
162        continue;
163      }
164  
165      /**
166       * Quotes: '|"|`
167       */
168  
169      if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) {
170        let open = value;
171        let next;
172  
173        if (options.keepQuotes !== true) {
174          value = '';
175        }
176  
177        while (index < length && (next = advance())) {
178          if (next === CHAR_BACKSLASH) {
179            value += next + advance();
180            continue;
181          }
182  
183          if (next === open) {
184            if (options.keepQuotes === true) value += next;
185            break;
186          }
187  
188          value += next;
189        }
190  
191        push({ type: 'text', value });
192        continue;
193      }
194  
195      /**
196       * Left curly brace: '{'
197       */
198  
199      if (value === CHAR_LEFT_CURLY_BRACE) {
200        depth++;
201  
202        let dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true;
203        let brace = {
204          type: 'brace',
205          open: true,
206          close: false,
207          dollar,
208          depth,
209          commas: 0,
210          ranges: 0,
211          nodes: []
212        };
213  
214        block = push(brace);
215        stack.push(block);
216        push({ type: 'open', value });
217        continue;
218      }
219  
220      /**
221       * Right curly brace: '}'
222       */
223  
224      if (value === CHAR_RIGHT_CURLY_BRACE) {
225        if (block.type !== 'brace') {
226          push({ type: 'text', value });
227          continue;
228        }
229  
230        let type = 'close';
231        block = stack.pop();
232        block.close = true;
233  
234        push({ type, value });
235        depth--;
236  
237        block = stack[stack.length - 1];
238        continue;
239      }
240  
241      /**
242       * Comma: ','
243       */
244  
245      if (value === CHAR_COMMA && depth > 0) {
246        if (block.ranges > 0) {
247          block.ranges = 0;
248          let open = block.nodes.shift();
249          block.nodes = [open, { type: 'text', value: stringify(block) }];
250        }
251  
252        push({ type: 'comma', value });
253        block.commas++;
254        continue;
255      }
256  
257      /**
258       * Dot: '.'
259       */
260  
261      if (value === CHAR_DOT && depth > 0 && block.commas === 0) {
262        let siblings = block.nodes;
263  
264        if (depth === 0 || siblings.length === 0) {
265          push({ type: 'text', value });
266          continue;
267        }
268  
269        if (prev.type === 'dot') {
270          block.range = [];
271          prev.value += value;
272          prev.type = 'range';
273  
274          if (block.nodes.length !== 3 && block.nodes.length !== 5) {
275            block.invalid = true;
276            block.ranges = 0;
277            prev.type = 'text';
278            continue;
279          }
280  
281          block.ranges++;
282          block.args = [];
283          continue;
284        }
285  
286        if (prev.type === 'range') {
287          siblings.pop();
288  
289          let before = siblings[siblings.length - 1];
290          before.value += prev.value + value;
291          prev = before;
292          block.ranges--;
293          continue;
294        }
295  
296        push({ type: 'dot', value });
297        continue;
298      }
299  
300      /**
301       * Text
302       */
303  
304      push({ type: 'text', value });
305    }
306  
307    // Mark imbalanced braces and brackets as invalid
308    do {
309      block = stack.pop();
310  
311      if (block.type !== 'root') {
312        block.nodes.forEach(node => {
313          if (!node.nodes) {
314            if (node.type === 'open') node.isOpen = true;
315            if (node.type === 'close') node.isClose = true;
316            if (!node.nodes) node.type = 'text';
317            node.invalid = true;
318          }
319        });
320  
321        // get the location of the block on parent.nodes (block's siblings)
322        let parent = stack[stack.length - 1];
323        let index = parent.nodes.indexOf(block);
324        // replace the (invalid) block with it's nodes
325        parent.nodes.splice(index, 1, ...block.nodes);
326      }
327    } while (stack.length > 0);
328  
329    push({ type: 'eos' });
330    return ast;
331  };
332  
333  module.exports = parse;