Created dir /src, and moved the source files into it. Left package.json, README.md and settings.json out. Modified package.json to point to the new src dir.
334 lines
6.8 KiB
JavaScript
334 lines
6.8 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');
|
|
}
|
|
|
|
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;
|