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;