Spaces:
Running
Running
; | |
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; | |