1/* 2 * JSINQ, JavaScript integrated query 3 * Copyright (c) 2009 Kai Jäger. Some rights reserved. 4 * 5 * Use of this source code is governed by an MIT-style license that can be 6 * found in the license.txt file. 7 */ 8 9 if (typeof jsinq == 'undefined') { 10 jsinq = {}; 11 } 12 13(function() { 14 /** 15 * An exception that is thrown when the translation of a query expression 16 * has failed because it contained syntactic errors. A description of 17 * the error is given in the message field of the exception object. The 18 * position of the error can be obtained via the line, offset and 19 * context fields of the exception. 20 * @constructor 21 * @param message The error message 22 * @param line The line at which the error occurred 23 * @param offset The character offset from the beginning og the line where 24 * the error occurred 25 * @param context A string that contains the part of the query expression 26 * where the error occurred 27 */ 28 function QueryTranslationException(message, line, offset, context) { 29 this.message = message; 30 this.line = line; 31 this.offset = offset; 32 this.context = context; 33 } 34 QueryTranslationException.prototype = new Error(); 35 QueryTranslationException.prototype.name = 'QueryTranslationException'; 36 37 // A list of keywords that are reserved in either ECMAScript or LINQ. This 38 // list is used to determine whether a string of characters may be used 39 // as an identifier (see function isIdentifier). 40 let reservedWords = ['break', 'else', 'new', 'let', 'case', 'finally', 41 'return', 'void', 'catch', 'for', 'switch', 'while', 'continue', 42 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 43 'in', 'try', 'do', 'instanceof', 'typeof', 'abstract', 'enum', 44 'int', 'short', 'boolean', 'export', 'interface', 'static', 'byte', 45 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized', 46 'class', 'float', 'package', 'throws', 'const', 'goto', 'private', 47 'transient', 'debugger', 'implements', 'protected', 'volatile', 48 'double', 'import', 'public', 'from', 'where', 'select', 'group', 49 'into', 'orderby', 'join', 'let', 'on', 'equals', 'by', 'ascending', 50 'descending']; 51 52 // Characters which according to the ECMAScript language specification are 53 // considered whitespace or line terminators. 54 let whitespaceCharacters = ['\u000A', '\u000D', '\u2028', '\u2029', 55 '\u0009', '\u000B', '\u000C', '\u0020', '\u00A0', '\u1680', '\u180E', 56 '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', 57 '\u2007', '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000']; 58 59 // A list of binary operators as specified by the ECMAScript language 60 // specification. 61 let binaryOperators = ['<', '>', '<=', '>=', '==', '!=', '===', '!==', 62 '+', '-', '*', '%', '<<', '>>', '>>>', '&', '|', '^', '&&', '||', 63 '=', '+=', '-=', '*=', '%=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', 64 '/', '/=']; 65 66 // Length in characters of the longest binary operator 67 let MAX_BINARY_OPERATOR_CHARACTERS = 4; 68 69 // Helper function that, given a linear array turns it into a look-up table 70 // and returns a function that, given a value, returns true if that value 71 // exists in the look-up table. 72 function createLookUpFunction(array) { 73 return (function() { 74 let lookUpTable = {}; 75 for (let i = 0; i < array.length; i++) { 76 lookUpTable[array[i]] = true; 77 } 78 79 return function(value) { 80 return typeof lookUpTable[value] != 'undefined'; 81 } 82 })(); 83 } 84 85 // Returns true if the given character is one which may appear at the 86 // beginning of an identifier. This specifically excludes numbers. 87 // Todo: The ECMAScript language specification allows for identifiers to 88 // start with any unicode letter or with a unicode escape sequence. This 89 // function does not take this into account. 90 function isIdentifierStart(value) { 91 value = value.charAt(0);
92 return value == '$' || value == '_' || 93 (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z'); 94 } 95 96 // Returns true if the given character is one which may appear anywhere 97 // in an identifier but not necessarily at the beginning. This specifically 98 // includes numbers. 99 // Todo: According to the ECMAScript language specification, identifier 100 // parts can also be unicode combining marks, unicode digits, unicode 101 // connector punctuation or unicode escape sequences. This function does 102 // not return true for any of those. 103 function isIdentifierPart(value) { 104 return isIdentifierStart(value) || isDecimalDigit(value); 105 } 106 107 // Returns true if the given character is decimal digit. 108 function isDecimalDigit(value) { 109 return value >= '0' && value <= '9'; 110 } 111 112 // Returns true if the given character is a hexadecimal digit. 113 function isHexDigit(value) { 114 return isDecimalDigit(value) || value >= 'a' && value <= 'f' || 115 value >= 'A' && value <= 'F'; 116 } 117 118 // Returns true if the specified string is a reserved keyword 119 let isReservedWord = createLookUpFunction(reservedWords); 120 121 // Returns true if the specified character is whitespace or a line 122 // terminator. 123 let isWhitespaceCharacter = createLookUpFunction(whitespaceCharacters); 124 125 // Returns ture if the specified string is a binary operator. 126 let isBinaryOperator = createLookUpFunction(binaryOperators); 127 128 // Constructor for a parse error object. An instance of ParseError is 129 // returned by the parsers when they fail to consume input. 130 // A parser error consists of an error message and a field 131 // "unparsedCharacters" which is used both to determine how much input 132 // the parser consumed before failing and to find the line number and 133 // offset of where the error happened. 134 function ParseError(message) { 135 this.message = message; 136 this.unparsedCharacters = -1; 137 } 138 139 // This function returns true if the parse error does not denote a 140 // complete failure, but a partial success (i.e. the parser consumed some 141 // input before failing). The parameter "rest" is the input originally fed 142 // to the parser. 143 ParseError.prototype.isPartialSuccess = function(rest) { 144 rest = rest.replace(/^\s+/g, ''); 145 return this.unparsedCharacters > -1 && 146 this.unparsedCharacters < rest.length; 147 }; 148 149 // Function that receives a string containing a query expression and 150 // that returns either the parse tree of that expression or an error 151 // message. 152 let parseQuery = (function() { 153 let parsers = this; 154 155 // A parser combinator for sequential composition which, given two 156 // parsers, succeeds only if both parsers succeed. It invokes the 157 // second parser with the input left over by the first parser. 158 // This function is called in an unusual manner to achieve something 159 // similar to operator overloading. It allows parsers to be combined 160 // using dot notation, i.e. terminal('a').terminal('b') will invoke 161 // terminal('a') and then terminal('b') with whatever is left of the 162 // input string. 163 // The label field is used to produce more meaningful error messages. 164 function sequence(right, label) { 165 let _this = this; 166 let func = null; 167 if (typeof _this == 'function') { 168 func = function(input) { 169 let result1 = _this(input); 170 if (result1 instanceof ParseError) { 171 return result1; 172 } 173 174 let result2 = right(result1.rest); 175 if (result2 instanceof ParseError) { 176 result2.unparsedCharacters = result1.rest.length; 177 return result2; 178 } 179 180 return { 181 parsed: result1.parsed.concat(result2.parsed), 182 rest: result2.rest 183 }; 184 }; 185 } else { 186 func = right; 187 } 188 for (let method in parsers) { 189 if (typeof parsers[method] == 'function') { 190 func[method] = parsers[method]; 191 } 192 } 193 func.label = label; 194 return func; 195 } 196 197 // A parser that consumes a specific character sequence (a terminal). 198 this.terminal = function(terminal) { 199 return sequence.call(this, function(input) { 200 let terminalLength = terminal.length; 201 if (input.substring(0, terminalLength) == terminal) { 202 return { 203 parsed: [terminal], 204 rest: input.substring(terminalLength) 205 }; 206 } else { 207 return new ParseError("'" + terminal + "' expected"); 208 } 209 }, "'" + terminal + "'"); 210 }; 211 212 // Receives any number of parsers and uses the first one that does not 213 // fail. Only fails if all the parsers passed to it fail. This 214 // corresponds to the following in EBNF notation: 215 // p = (p1 | p2 | p3 | ...) 216 this.oneOf = function() { 217 let parsers = []; 218 for (let i = 0; i < arguments.length; i++) { 219 parsers.push(arguments[i]); 220 } 221 return sequence.call(this, function(input) { 222 let result; 223 let minUnparsedChars = input.length; 224 let bestParserError = null; 225 for (let i = 0; i < parsers.length; i++) { 226 result = parsers[i](input); 227 if (!(result instanceof ParseError)) { 228 return result; 229 } else if (result.isPartialSuccess(input)) { 230 minUnparsedChars = result.unparsedCharacters; 231 bestParserError = result; 232 } 233 } 234 235 if (bestParserError) { 236 return bestParserError; 237 } 238 239 let labels = []; 240 for (let i = 0; i < parsers.length; i++) { 241 labels.push(parsers[i].label); 242 } 243 let last = labels.pop(); 244 if (labels.length > 0) { 245 last = 'or ' + last; 246 } 247 return new ParseError('Expecting ' + 248 labels.join(', ') + ' ' + last); 249 }); 250 }; 251 252 // Applies the specified parser zero or more times. This corresponds 253 // to the following in EBNF notation: 254 // p = {p1} 255 this.zeroOrMore = function(parser) { 256 return sequence.call(this, function(input) { 257 let last = input; 258 let accumulated = []; 259 let result; 260 do { 261 result = parser(last); 262 if (!(result instanceof ParseError)) { 263 last = result.rest; 264 accumulated = accumulated.concat(result.parsed);
265 } 266 } while (!(result instanceof ParseError)); 267 268 if (result.isPartialSuccess(last)) { 269 return result; 270 } 271 272 return { 273 parsed: accumulated, 274 rest: last 275 }; 276 }); 277 }; 278 279 // Applies the specified parser at least once and for as long as it 280 // continues to consume input. Corresponds to the following in EBNF: 281 // p = p1 {p1} 282 this.oneOrMore = function(parser) { 283 return sequence.call(this, function(input) { 284 let last = input; 285 let accumulated = []; 286 let result; 287 do { 288 result = parser(last); 289 if (!(result instanceof ParseError)) { 290 last = result.rest; 291 accumulated = accumulated.concat(result.parsed); 292 } 293 } while (!(result instanceof ParseError)); 294 295 if (result.isPartialSuccess(last)) { 296 return result; 297 } 298 299 if (accumulated.length > 0) { 300 return { 301 parsed: accumulated, 302 rest: last 303 }; 304 } else { 305 return new ParseError('Expecting one or more ' + 306 input.label); 307 } 308 }); 309 }; 310 311 // Optionally applies the specified parser. Corresponds to the 312 // following in EBNF: 313 // p = [p1] 314 this.optional = function(parser) { 315 return sequence.call(this, function(input) { 316 let result = parser(input); 317 if (!(result instanceof ParseError)) { 318 return result; 319 } else { 320 if (result.isPartialSuccess(input)) { 321 return result; 322 } 323 return { 324 parsed: [], 325 rest: input 326 }; 327 } 328 }); 329 }; 330 331 // Applies the specified parser but does not record its output. Fails 332 // if the specified parser fails. 333 this.skip = function(parser) { 334 return sequence.call(this, function(input) { 335 let result = parser(input); 336 if (!(result instanceof ParseError)) { 337 result.parsed = []; 338 } 339 return result; 340 }); 341 }; 342 343 // Wraps a parser into lambda. This is so that parsers defined using 344 // sequential composition can themselves be used in other sequentially 345 // composed parsers. Also used to give a label to a parser, which 346 // allows for better error messages. 347 this.asParser = function(label) { 348 let func = this; 349 return function() { 350 return sequence.call(this, function(input) { 351 return func(input); 352 }, label); 353 }; 354 }; 355 356 // Lazily invokes the specified parser. This is used to define 357 // recursive which would otherwise enter an infinite recursive loop 358 // upon their definition. Also used to refer to parsers which are 359 // not yet defined. 360 this.lazy = function(parser) { 361 return sequence.call(this, function(input) { 362 return parser()(input); 363 }); 364 }; 365 366 // Invokes the specified parser and groups the result. 367 this.group = function(parser) { 368 return sequence.call(this, function(input) { 369 let result = parser(input); 370 if (!(result instanceof ParseError)) { 371 return { 372 parsed: [result.parsed], 373 rest: result.rest 374 }; 375 } else { 376 return result; 377 } 378 }); 379 } 380 381 // Takes the result of a parser and flattens it into a single value. 382 this.flatten = function(parser) { 383 return sequence.call(this, function(input) { 384 let result = parser(input); 385 if (!(result instanceof ParseError)) { 386 return { 387 parsed: [result.parsed.join(' ')], 388 rest: result.rest 389 }; 390 } else { 391 return result; 392 } 393 }); 394 } 395 396 // Consumes a single whitespace character. 397 this.whitespace = function() { 398 return sequence.call(this, function(input) { 399 if (input.length > 0 && 400 isWhitespaceCharacter(input.charAt(0))) { 401 return { 402 parsed: [input.substring(0, 1)], 403 rest: input.substring(1) 404 }; 405 } else { 406 return new ParseError('Expecting whitespace'); 407 } 408 }, 'whitespace'); 409 }; 410 411 // Consumes any number of whitespace characters but fails if no 412 // whitespace is consumed. The whitespace is not added to the parse 413 // tree. 414 this.mandatoryWhitespace = 415 this.skip( 416 this.oneOrMore( 417 this.whitespace() 418 ) 419 ).asParser('whitespace'); 420 421 // Consumes any number of whitespace characters or none at all. 422 // Does not fail if no whitespace is consumed. THe whitespace is not 423 // added to the parse tree. 424 this.optionalWhitespace = 425 this.skip( 426 this.zeroOrMore( 427 this.whitespace() 428 ) 429 ).asParser('whitespace'); 430 431 // The following parsers are used to accept ECMAScript expressions. 432 // Note that JSINQ does not actually do anything with the expressions 433 // other than to put them into anonymous functions. Expressions are 434 // parsed to reliably separate them from the query keywords. 435 // The grammar used below is adapted from the ECMAScript language 436 // specification. However, it has been heavily simplified and is often 437 // more permissive than the original grammar. 438 // The expression parser may also be helpful for implementing nested 439 // queries in the future. 440 441 // Parses an identifier. Note that this does not comply with the 442 // ECMAScript language specification in that it neither supports 443 // unicode identifiers nor unicode escape sequences. 444 this.identifier = function() { 445 return sequence.call(this, function(input) { 446 if (input.length < 1) { 447 return new ParseError('Unexpected end of file'); 448 } 449 if (!isIdentifierStart(input.charAt(0))) { 450 return new ParseError('Invalid character found, ' + 451 'expecting identifier.'); 452 } 453 let index = 1; 454 while (index < input.length && 455 isIdentifierPart(input.charAt(index))) { 456 ++index; 457 } 458 let identifier = input.substring(0, index); 459 if (isReservedWord(identifier)) { 460 return new ParseError('Reserved word \'' + identifier + 461 '\' cannot be used as identifier'); 462 } 463 464 // Translate placeholders in the query expression into 465 // subscripting into the query parameters array. 466 // Todo: This is probably not the best place for this. 467 let regExp = new RegExp('^\\$[0-9]+$', 'g'); 468 if (regExp.test(identifier)) { 469 identifier = '_$qp[' + identifier.substring(1) + ']'; 470 } 471 472 return {
473 parsed: [identifier], 474 rest: input.substring(index) 475 }; 476 }, 'identifier'); 477 }; 478 479 // Parses a binary operator (see operator table at the beginning of 480 // this file). Only binary operators composed of special characters 481 // are consumed (this excludes 'in' and 'instanceof'). 482 this.binaryOperator = function() { 483 return sequence.call(this, function(input) { 484 for (let i = MAX_BINARY_OPERATOR_CHARACTERS; i >= 1; i--) { 485 let operator = input.substring(0, i); 486 if (isBinaryOperator(operator)) { 487 return { 488 parsed: operator, 489 rest: input.substring(i) 490 }; 491 } 492 } 493 return new ParseError('Expecting binary operator'); 494 }, 'binary operator'); 495 }; 496 497 // Parses a string literal, delimited by either single- or double- 498 // quotation marks. 499 this.stringLiteral = function() { 500 return sequence.call(this, function(input) { 501 if (input.length < 2 || input.charAt(0) != '"' && 502 input.charAt(0) != "'") { 503 return new ParseError('Malformed string literal'); 504 } 505 let delimiter = input.charAt(0);
506 let last = delimiter; 507 let current; 508 for (let i = 1; i < input.length; i++) { 509 current = input.charAt(i); 510 if (current == delimiter && last != '\\') { 511 ++i; 512 return { 513 parsed: [input.substring(0, i)], 514 rest: input.substring(i) 515 }; 516 } 517 last = current; 518 } 519 return new ParseError('Unterminated string literal'); 520 }, 'string literal'); 521 }; 522 523 // Parses a number literal, i.e. a decimal number, a hex number or 524 // a floating point number in either decimal or exponential notation. 525 this.numericLiteral = function() { 526 return sequence.call(this, function(input) { 527 if (input.length < 1) { 528 return new ParseError('Unexpected end of file'); 529 } 530 531 let digitFunction = isDecimalDigit; 532 let wantDecimalSeparator = true; 533 let wantExponent = false; 534 let wantSign = false; 535 let startIndex = 0; 536 537 if (input.substring(0, 2).toLowerCase() == '0x') { 538 digitFunction = isHexDigit; 539 wantDecimalSeparator = false; 540 startIndex = 2; 541 } else if (input.charAt(0) == '.') { 542 wantDecimalSeparator = false; 543 wantExponent = true; 544 startIndex = 1; 545 } 546 547 let index = startIndex; 548 for (let i = startIndex; i < input.length; i++) { 549 let current = input.charAt(i); 550 if (wantDecimalSeparator && current == '.') { 551 wantDecimalSeparator = false; 552 wantExponent = true; 553 } else if (wantExponent && (current == 'e' || 554 current == 'E')) { 555 wantExponent = false; 556 wantSign = true; 557 } else if (wantSign && (current == '+' || 558 current == '-')) { 559 wantSign = false; 560 } else if (digitFunction(current)) { 561 wantSign = false; 562 } else { 563 break; 564 } 565 ++index; 566 } 567 if (index == startIndex) { 568 return new ParseError('Invalid number'); 569 } 570 return { 571 parsed: [input.substring(0, index)], 572 rest: input.substring(index) 573 }; 574 }, 'number literal'); 575 }; 576 577 // Parses the body of a function expression (i.e. an inline function 578 // definition). The actual contents of the function body are ignored. 579 // (What this parser really does is to count curly braces). 580 this.functionExpressionBody = function() { 581 return sequence.call(this, function(input) { 582 if (input.charAt(0) != '{') { 583 return new ParseError("'{' expected"); 584 } 585 let last = ''; 586 let inString = false; 587 let stringTerminator = ''; 588 let curlyOpenCount = 1; 589 for (let i = 1; i < input.length; i++) { 590 let current = input.charAt(i); 591 if (!inString) { 592 if (current == '{') { 593 ++curlyOpenCount; 594 } else if (current == '}') { 595 --curlyOpenCount; 596 } else if (current == '"' || current == "'") { 597 inString = true; 598 stringTerminator = current; 599 } 600 } else { 601 if (current == stringTerminator && last != '\\') { 602 inString = false; 603 } 604 } 605 last = current; 606 if (curlyOpenCount == 0) { 607 ++i; 608 return { 609 parsed: [input.substring(0, i)], 610 rest: input.substring(i) 611 }; 612 } 613 } 614 return new ParseError('Unexpected end of file'); 615 }, 'function expression'); 616 }; 617 618 // The following productions are adapted from the ECMAScript grammar. 619 620 this.arrayLiteral = 621 this.terminal('['). 622 optionalWhitespace(). 623 optional( 624 this.optional( 625 this.lazy(function() { 626 return parsers.secondaryExpression(); 627 }) 628 ). 629 zeroOrMore( 630 this.optionalWhitespace(). 631 terminal(','). 632 optionalWhitespace(). 633 optional( 634 this.lazy(function() { 635 return parsers.secondaryExpression(); 636 }) 637 ) 638 ). 639 optionalWhitespace() 640 ). 641 optionalWhitespace(). 642 terminal(']'). 643 asParser('array literal'); 644 645 this.propertyName = 646 this.oneOf( 647 this.identifier(), 648 this.stringLiteral(), 649 this.numericLiteral() 650 ).asParser('property name'); 651 652 this.propertyNameAndValue = 653 this.propertyName(). 654 optionalWhitespace(). 655 terminal(':'). 656 optionalWhitespace(). 657 lazy(function() { 658 return parsers.secondaryExpression(); 659 }). 660 asParser('property name and value'); 661 662 this.objectLiteral = 663 this.terminal('{'). 664 optionalWhitespace(). 665 optional( 666 this.propertyNameAndValue(). 667 zeroOrMore( 668 this.optionalWhitespace(). 669 terminal(','). 670 optionalWhitespace(). 671 propertyNameAndValue() 672 ) 673 ). 674 optionalWhitespace(). 675 terminal('}'). 676 asParser('object literal'); 677 678 this.literal = 679 this.oneOf( 680 this.terminal('null'), 681 this.terminal('true'), 682 this.terminal('false'), 683 this.terminal('Infinity'), 684 this.numericLiteral(), 685 this.stringLiteral(), 686 this.arrayLiteral(), 687 this.objectLiteral() 688 ).asParser('literal'); 689 690 this.primaryExpression = 691 this.oneOf( 692 this.terminal('this'), 693 this.identifier(), 694 this.literal(), 695 this.terminal('('). 696 optionalWhitespace(). 697 lazy(function() { 698 return parsers.expression() 699 }). 700 optionalWhitespace(). 701 terminal(')') 702 ).asParser('primary expression'); 703 704 this.parameters = 705 this.terminal('('). 706 optionalWhitespace(). 707 optional( 708 this.lazy(function() { 709 return parsers.secondaryExpression(); 710 }). 711 zeroOrMore( 712 this.optionalWhitespace(). 713 terminal(','). 714 optionalWhitespace(). 715 lazy(function() { 716 return parsers.secondaryExpression(); 717 }) 718 ) 719 ). 720 optionalWhitespace(). 721 terminal(')'). 722 asParser('arguments'); 723 724 this.functionExpression = 725 this.terminal('function'). 726 optional( 727 this.mandatoryWhitespace(). 728 identifier() 729 ). 730 optionalWhitespace(). 731 terminal('('). 732 optionalWhitespace(). 733 optional( 734 this.identifier(). 735 zeroOrMore( 736 this.optionalWhitespace(). 737 terminal(','). 738 optionalWhitespace(). 739 identifier() 740 ) 741 ). 742 optionalWhitespace(). 743 terminal(')'). 744 optionalWhitespace(). 745 functionExpressionBody(). 746 asParser('function expression'); 747 748 this.leftHandSideExpression = 749 this.zeroOrMore( 750 this.terminal('new'). 751 mandatoryWhitespace() 752 ). 753 oneOf( 754 this.primaryExpression(), 755 this.functionExpression() 756 ). 757 zeroOrMore( 758 this.oneOf( 759 this.optionalWhitespace(). 760 parameters(), 761 this.optionalWhitespace(). 762 terminal('['). 763 optionalWhitespace(). 764 lazy(function() { 765 return parsers.secondaryExpression(); 766 }). 767 optionalWhitespace(). 768 terminal(']'), 769 this.optionalWhitespace(). 770 terminal('.'). 771 optionalWhitespace(). 772 identifier() 773 ) 774 ).asParser('left-hand-side expression'); 775 776 this.unaryExpression = 777 this.zeroOrMore( 778 this.oneOf( 779 this.terminal('delete'). 780 mandatoryWhitespace(), 781 this.terminal('void'). 782 mandatoryWhitespace(), 783 this.terminal('typeof'). 784 mandatoryWhitespace(), 785 this.terminal('++'), 786 this.terminal('--'), 787 this.terminal('+'), 788 this.terminal('-'), 789 this.terminal('~'), 790 this.terminal('!') 791 ). 792 optionalWhitespace() 793 ). 794 leftHandSideExpression(). 795 zeroOrMore( 796 this.optionalWhitespace(). 797 oneOf( 798 this.terminal('++'), 799 this.terminal('--') 800 ) 801 ). 802 asParser('unary expression'); 803 804 this.secondaryExpression = 805 this.flatten( 806 this.unaryExpression(). 807 zeroOrMore( 808 this.optionalWhitespace(). 809 oneOf( 810 this.binaryOperator(), 811 this.terminal('in'). 812 mandatoryWhitespace(), 813 this.terminal('instanceof'). 814 mandatoryWhitespace() 815 ). 816 optionalWhitespace(). 817 unaryExpression() 818 ) 819 ).asParser('expression'); 820 821 this.expression = 822 this.flatten( 823 this.secondaryExpression(). 824 zeroOrMore( 825 this.optionalWhitespace(). 826 terminal(','). 827 optionalWhitespace(). 828 secondaryExpression() 829 ) 830 ).asParser('expression'); 831 832 // The following is an almost direct translation of the LINQ grammar 833 // given in the official C# Language Specification into combinatory 834 // parsers. 835 836 this.fromClause = 837 this.terminal('from'). 838 group( 839 this.mandatoryWhitespace(). 840 identifier(). 841 mandatoryWhitespace(). 842 skip( 843 this.terminal('in') 844 ). 845 mandatoryWhitespace(). 846 expression() 847 ).asParser('from clause'); 848 849 this.letClause = 850 this.terminal('let'). 851 group( 852 this.mandatoryWhitespace(). 853 identifier(). 854 optionalWhitespace(). 855 skip( 856 this.terminal('=') 857 ). 858 optionalWhitespace(). 859 expression() 860 ).asParser('let clause'); 861 862 this.whereClause = 863 this.terminal('where'). 864 group( 865 this.mandatoryWhitespace(). 866 expression() 867 ).asParser('where clause'); 868 869 this.joinClause = 870 this.terminal('join'). 871 group( 872 this.mandatoryWhitespace(). 873 identifier(). 874 mandatoryWhitespace(). 875 skip( 876 this.terminal('in') 877 ). 878 mandatoryWhitespace(). 879 expression(). 880 mandatoryWhitespace(). 881 skip( 882 this.terminal('on') 883 ). 884 mandatoryWhitespace(). 885 expression(). 886 mandatoryWhitespace(). 887 skip( 888 this.terminal('equals') 889 ). 890 mandatoryWhitespace(). 891 expression() 892 ).asParser('join clause'); 893 894 this.joinIntoClause = 895 this.terminal('join'). 896 group( 897 this.mandatoryWhitespace(). 898 identifier(). 899 mandatoryWhitespace(). 900 skip( 901 this.terminal('in') 902 ). 903 mandatoryWhitespace(). 904 expression(). 905 mandatoryWhitespace(). 906 skip( 907 this.terminal('on') 908 ). 909 mandatoryWhitespace(). 910 expression(). 911 mandatoryWhitespace(). 912 skip( 913 this.terminal('equals') 914 ). 915 mandatoryWhitespace(). 916 expression(). 917 mandatoryWhitespace(). 918 skip( 919 this.terminal('into') 920 ). 921 mandatoryWhitespace(). 922 identifier() 923 ).asParser('join into clause'); 924 925 this.orderingDirection = 926 this.oneOf( 927 this.skip( 928 this.terminal('ascending') 929 ), 930 this.terminal('descending') 931 ).asParser('ordering direction'); 932 933 this.ordering = 934 this.group( 935 this.secondaryExpression(). 936 optional( 937 this.mandatoryWhitespace(). 938 orderingDirection() 939 ) 940 ).asParser('ordering'); 941 942 this.orderings = 943 this.ordering(). 944 zeroOrMore( 945 this.optionalWhitespace(). 946 skip( 947 this.terminal(',') 948 ). 949 optionalWhitespace(). 950 ordering() 951 ).asParser('orderings'); 952 953 this.orderByClause = 954 this.terminal('orderby'). 955 group( 956 this.mandatoryWhitespace(). 957 orderings() 958 ).asParser('orderby clause'); 959 960 this.selectClause = 961 this.terminal('select'). 962 group( 963 this.mandatoryWhitespace(). 964 expression() 965 ).asParser('select clause'); 966 967 this.groupClause = 968 this.terminal('group'). 969 group( 970 this.mandatoryWhitespace(). 971 expression(). 972 mandatoryWhitespace(). 973 skip( 974 this.terminal('by') 975 ). 976 mandatoryWhitespace(). 977 expression() 978 ).asParser('group by clause'); 979 980 this.selectOrGroupClause = 981 this.group( 982 this.oneOf( 983 this.selectClause(), 984 this.groupClause() 985 ) 986 ).asParser('select or group by clause'); 987 988 this.queryBodyClause = 989 this.group( 990 this.oneOf( 991 this.fromClause(), 992 this.letClause(), 993 this.whereClause(), 994 this.joinIntoClause(), 995 this.joinClause(), 996 this.orderByClause() 997 ) 998 ).asParser('query body'); 999 1000 this.queryBodyClauses = 1001 this.queryBodyClause(). 1002 zeroOrMore( 1003 this.mandatoryWhitespace(). 1004 queryBodyClause() 1005 ).asParser('query body'); 1006 1007 this.queryBody = 1008 this.optional( 1009 this.queryBodyClauses(). 1010 mandatoryWhitespace() 1011 ). 1012 selectOrGroupClause(). 1013 optional( 1014 this.group( 1015 this.mandatoryWhitespace(). 1016 lazy(function() { 1017 return parsers.queryContinuation(); 1018 }) 1019 ) 1020 ).asParser('query body'); 1021 1022 this.queryContinuation = 1023 this.terminal('into'). 1024 group( 1025 this.mandatoryWhitespace(). 1026 identifier(). 1027 mandatoryWhitespace(). 1028 lazy(function() { 1029 return parsers.queryBody(); 1030 }) 1031 ).asParser('into clause'); 1032 1033 this.queryExpression = 1034 this.optionalWhitespace(). 1035 group( 1036 this.fromClause() 1037 ). 1038 mandatoryWhitespace(). 1039 queryBody(); 1040 1041 return this.queryExpression; 1042 }).call({}); 1043 1044 // Function that receives a query expression tree (as produced by the 1045 // parseQuery function) and translates it into JavaScript code. 1046 let compileQuery = (function() { 1047 // Values returned by a visitor function that tell the tree- 1048 // traverser if and how to continue. 1049 // Parent = continue with parent node, Continue = continue with next 1050 // node, Abort = abort the traversal 1051 let TRAVERSE_PARENT = 1; 1052 let TRAVERSE_CONTINUE = 2; 1053 let TRAVERSE_ABORT = 3; 1054 1055 // Prefix for transparent identifiers injected during query 1056 // transformation 1057 let TRANSPARENT_IDENTIFIER_PREFIX = '_$ti'; 1058 1059 // Sequencing-mechanism for transparent identifiers 1060 let lastTransparentIdentifier = 0; 1061 1062 // Keeps track of transparent identifiers and which other transparent 1063 // identifiers they capture. 1064 let transparentIdentifiers = {}; 1065 1066 // Returns true if the specified identifier is "transparent" (i.e. 1067 // generated by the compiler) 1068 function isTransparentIdentifier(identifier) { 1069 return identifier.substring(0, 1070 TRANSPARENT_IDENTIFIER_PREFIX.length) == 1071 TRANSPARENT_IDENTIFIER_PREFIX; 1072 } 1073 1074 // Retrieves a transparent identifier for an object that captures 1075 // multiple range letiables. The names of the captured range variables 1076 // are passed to this function. 1077 function getTransparentIdentifierFor() { 1078 lastTransparentIdentifier++; 1079 let identifier = TRANSPARENT_IDENTIFIER_PREFIX + 1080 lastTransparentIdentifier; 1081 let transparentMembers = []; 1082 for (let i = 0; i < arguments.length; i++) { 1083 if (isTransparentIdentifier(arguments[i])) { 1084 transparentMembers.push(arguments[i]); 1085 } 1086 } 1087 transparentIdentifiers[identifier] = transparentMembers; 1088 return identifier; 1089 } 1090 1091 // Emits the JavaScript source code for a lambda expression with the 1092 // specified parameters and body. 1093 function emitLambda(parameters, body) { 1094 let code = ['function(', parameters.join(', '), ') {']; 1095 1096 let withOpen = []; 1097 let withClose = []; 1098 let resolveStack = []; 1099 1100 // If any of the parameters of the lambda expression is a 1101 // transparent identifier, put a with statement around the 1102 // body of the lambda so that its members are in scope. Do this 1103 // "recursively" if necessary. 1104 for (let i = 0; i < parameters.length; i++) { 1105 if (isTransparentIdentifier(parameters[i])) { 1106 resolveStack.push(parameters[i]); 1107 do { 1108 let topOfStack = resolveStack.pop(); 1109 withOpen.push('with (', topOfStack, ') {'); 1110 withClose.push('}');
1111 let child = transparentIdentifiers[topOfStack]; 1112 if (transparentIdentifiers[topOfStack].length > 0) { 1113 resolveStack.push(child[0]); 1114 } else { 1115 break; 1116 } 1117 } while (true); 1118 } 1119 } 1120 1121 code = code.concat(withOpen); 1122 code.push(body); 1123 code = code.concat(withClose); 1124 code.push('}'); 1125 return code.join(''); 1126 } 1127 1128 // The query compiler works by repeatedly applying transformations 1129 // to the expression tree. Each transformation is associated with a 1130 // specific pattern of query clauses and it is applied for as long as 1131 // that specific pattern occurs in the expression tree. Additionally, 1132 // transformations are grouped into sections and each section is again 1133 // applied repeatedly until all patterns it recognizes have either been 1134 // compiled to JavaScript code or transformed into something else. 1135 // The transformations are kept in the following array. They are 1136 // applied in the order in which they are specified. 1137 // Note that this corresponds directly to the point "Query expression 1138 // translation" in the C# Language Specification. 1139 1140 let transformations = [ 1141 // Select and groupby clauses with continuations 1142 [ 1143 { 1144 pattern: ['*', 'into', null], 1145 transformer: function(root, match) { 1146 let intoClause = root[match[1]]; 1147 1148 root[match[1]] = intoClause[1][1]; 1149 for (let j = 2; j < intoClause[1].length; j++) { 1150 root.splice(match[1] + (j - 1), 0, 1151 intoClause[1][j]); 1152 } 1153 root.unshift([ 1154 'from', 1155 [ 1156 intoClause[1][0], 1157 root.splice(0, match[0]) 1158 ] 1159 ]); 1160 return true; 1161 } 1162 } 1163 ], 1164 // Degenerate query expressions 1165 [ 1166 { 1167 pattern: ['from', 'select', null], 1168 transformer: function(root, match) { 1169 let fromClause = root[match[0]][1]; 1170 let selectClause = root[match[1]][1]; 1171 if (match[1] - match[0] > 1) { 1172 return false; 1173 } 1174 if (fromClause[0] == selectClause[0]) { 1175 root.splice(match[0], 2, [ 1176 'compiled', 1177 [ 1178 '(', fromClause[1], ').select(', 1179 emitLambda([fromClause[0]], 'return ' + 1180 fromClause[0] + ';'), ')' 1181 ] 1182 ]); 1183 return true; 1184 } 1185 return false; 1186 } 1187 } 1188 ], 1189 // From, let, where, join and orderby clauses 1190 [ 1191 { 1192 pattern: ['from', 'from', 'select', null], 1193 transformer: function(root, match) { 1194 let firstFromClause = root[match[0]][1]; 1195 let secondFromClause = root[match[1]][1]; 1196 let selectClause = root[match[2]][1]; 1197 1198 if (match[1] - match[0] > 1) { 1199 return false; 1200 } 1201 1202 root.splice(match[0], 2, [ 1203 'compiled', 1204 [ 1205 '(', firstFromClause[1], ')' 1206 ] 1207 ]); 1208 1209 root.splice(match[1], 1, [ 1210 'compiled', 1211 [ 1212 '.selectMany(', 1213 emitLambda([firstFromClause[0]], 'return ' + 1214 secondFromClause[1] + ';'), ', ', 1215 emitLambda([firstFromClause[0], 1216 secondFromClause[0]], 'return ' + 1217 selectClause[0] + ';'), ')' 1218 ] 1219 ]); 1220 return true; 1221 } 1222 }, 1223 { 1224 pattern: ['from', 'from'], 1225 transformer: function(root, match) { 1226 let firstFromClause = root[match[0]][1]; 1227 let secondFromClause = root[match[1]][1]; 1228 root.splice(match[0], 1, [ 1229 'from', 1230 [ 1231 getTransparentIdentifierFor(firstFromClause[0], 1232 secondFromClause[0]), 1233 firstFromClause[1] 1234 ] 1235 ]); 1236 root.splice(match[1], 1, [ 1237 'compiled', 1238 [ 1239 '.selectMany(', emitLambda( 1240 [firstFromClause[0]], 'return ' + 1241 secondFromClause[1] + ';'), 1242 ', ', emitLambda([firstFromClause[0], 1243 secondFromClause[0]], 'return { \'' + 1244 firstFromClause[0] + '\':' + 1245 firstFromClause[0] + ', \'' + 1246 secondFromClause[0] + '\':' + 1247 secondFromClause[0] + '};'), ')' 1248 ] 1249 ]); 1250 return true; 1251 } 1252 }, 1253 { 1254 pattern: ['from', 'let'], 1255 transformer: function(root, match) { 1256 let fromClause = root[match[0]][1]; 1257 let letClause = root[match[1]][1]; 1258 root.splice(match[0], 1, [ 1259 'from', 1260 [ 1261 getTransparentIdentifierFor(fromClause[0], 1262 letClause[0]), 1263 fromClause[1] 1264 ] 1265 ]); 1266 root.splice(match[1], 1, [ 1267 'compiled', 1268 [ 1269 '.select(', emitLambda([fromClause[0]], 1270 'return {\'' + fromClause[0] + '\': ' + 1271 fromClause[0] + ', \'' + letClause[0] + 1272 '\': ' + letClause[1] + '};'), ')' 1273 ] 1274 ]); 1275 return true; 1276 } 1277 }, 1278 { 1279 pattern: ['from', 'where'], 1280 transformer: function(root, match) { 1281 let fromClause = root[match[0]][1]; 1282 let whereClause = root[match[1]][1]; 1283 root.splice(match[1], 1, [ 1284 'compiled', 1285 [ 1286 '.where(', emitLambda([fromClause[0]], 1287 'return ' + whereClause[0] + ';'), ')' 1288 ] 1289 ]); 1290 return true; 1291 } 1292 }, 1293 { 1294 pattern: ['from', 'join', 'select', null], 1295 transformer: function(root, match) { 1296 let fromClause = root[match[0]][1]; 1297 let joinClause = root[match[1]][1]; 1298 let selectClause = root[match[2]][1]; 1299 1300 if (match[2] - match[1] > 1) { 1301 return false; 1302 } 1303 1304 // Exclude join-intos 1305 if (joinClause.length > 4) { 1306 return false; 1307 } 1308 root.splice(match[0], 1, [ 1309 'compiled', 1310 [ 1311 '(', fromClause[1], ')' 1312 ] 1313 ]); 1314 root.splice(match[1], 2, [ 1315 'compiled', 1316 [ 1317 '.join(', joinClause[1], ', ', 1318 emitLambda([fromClause[0]], 'return ' + 1319 joinClause[2] + ';'), ', ', 1320 emitLambda([joinClause[0]], 'return ' + 1321 joinClause[3] + ';'), ', ', 1322 emitLambda([fromClause[0], joinClause[0]], 1323 'return ' + selectClause[0] + ';'), ')' 1324 ] 1325 ]); 1326 return true; 1327 } 1328 }, 1329 { 1330 pattern: ['from', 'join'], 1331 transformer: function(root, match) { 1332 let fromClause = root[match[0]][1]; 1333 let joinClause = root[match[1]][1]; 1334 // Exclude join-intos 1335 if (joinClause.length > 4) { 1336 return false; 1337 } 1338 root.splice(match[0], 1, [ 1339 'from', 1340 [ 1341 getTransparentIdentifierFor(fromClause[0], 1342 joinClause[0]), 1343 fromClause[1] 1344 ] 1345 ]); 1346 root.splice(match[1], 1, [ 1347 'compiled', 1348 [ 1349 '.join(', joinClause[1], 1350 ', ', emitLambda([fromClause[0]], 'return ' + 1351 joinClause[2] + ';'), ', ', 1352 emitLambda([joinClause[0]], 'return ' + 1353 joinClause[3] + ';'), ', ', 1354 emitLambda([fromClause[0], joinClause[0]], 1355 'return {\'' + fromClause[0] + '\': ' + 1356 fromClause[0] + ', \'' + joinClause[0] + 1357 '\': ' + joinClause[0] + '};'), ')' 1358 ] 1359 ]); 1360 return true; 1361 } 1362 }, 1363 { 1364 pattern: ['from', 'join', 'select', null], 1365 transformer: function(root, match) { 1366 let fromClause = root[match[0]][1]; 1367 let joinClause = root[match[1]][1]; 1368 let selectClause = root[match[2]][1]; 1369 1370 if (match[2] - match[1] > 1) { 1371 return false; 1372 } 1373 if (joinClause.length < 5) { 1374 return false; 1375 } 1376 1377 root.splice(match[0], 1, [ 1378 'compiled', 1379 [ 1380 '(', fromClause[1], ')' 1381 ] 1382 ]); 1383 root.splice(match[1], 2, [ 1384 'compiled', 1385 [ 1386 '.groupJoin(', joinClause[1], ', ', 1387 emitLambda([fromClause[0]], 'return ' + 1388 joinClause[2] + ';'), ', ', 1389 emitLambda([joinClause[0]], 'return ' + 1390 joinClause[3] + ';'), ', ', 1391 emitLambda([fromClause[0], joinClause[4]], 1392 'return ' + selectClause[0] + ';'), ')' 1393 ] 1394 ]); 1395 return true; 1396 } 1397 }, 1398 { 1399 pattern: ['from', 'join'], 1400 transformer: function(root, match) { 1401 let fromClause = root[match[0]][1]; 1402 let joinClause = root[match[1]][1]; 1403 1404 if (joinClause.length < 5) { 1405 return false; 1406 } 1407 1408 root.splice(match[0], 1, [ 1409 'from', 1410 [ 1411 getTransparentIdentifierFor(fromClause[0], 1412 joinClause[4]), 1413 fromClause[1] 1414 ] 1415 ]); 1416 root.splice(match[1], 1, [ 1417 'compiled', 1418 [ 1419 '.groupJoin(', joinClause[1], ', ', 1420 emitLambda([fromClause[0]], 'return ' + 1421 joinClause[2] + ';'), ', ', 1422 emitLambda([joinClause[0]], 'return ' + 1423 joinClause[3] + ';'), ', ', 1424 emitLambda([fromClause[0], joinClause[4]], 1425 'return {\'' + fromClause[0] + '\': ' + 1426 fromClause[0] + ', \'' + joinClause[4] + 1427 '\': ' + joinClause[4] + '};'), ')' 1428 ] 1429 ]); 1430 return true; 1431 } 1432 }, 1433 { 1434 pattern: ['from', 'orderby'], 1435 transformer: function(root, match) { 1436 let fromClause = root[match[0]][1]; 1437 let orderbyClause = root[match[1]][1]; 1438 let orderingPostfix = ''; 1439 if (orderbyClause[0].length > 1) {
1440 orderingPostfix = 'Descending'; 1441 } 1442 1443 let code = ['.orderBy', orderingPostfix, '(', 1444 emitLambda([fromClause[0]], 'return ' + 1445 orderbyClause[0][0] + ';'), ')']; 1446 1447 for (let i = 1; i < orderbyClause.length; i++) { 1448 orderingPostfix = ''; 1449 if (orderbyClause[i].length > 1) { 1450 orderingPostfix = 'Descending'; 1451 } 1452 code.push('.thenBy', orderingPostfix, '(', 1453 emitLambda([fromClause[0]], 'return ' + 1454 orderbyClause[i][0] + ';'), ')'); 1455 } 1456 1457 root.splice(match[1], 1, [ 1458 'compiled', 1459 code 1460 ]); 1461 return true; 1462 } 1463 } 1464 ], 1465 // Select clauses 1466 [ 1467 { 1468 pattern: ['from', 'select', null], 1469 transformer: function(root, match) { 1470 let fromClause = root[match[0]][1]; 1471 let selectClause = root[match[1]][1]; 1472 root.splice(match[0], 1, [ 1473 'compiled', 1474 [ 1475 '(', fromClause[1], ')' 1476 ] 1477 ]); 1478 root.splice(match[1], 1, [ 1479 'compiled', 1480 [ 1481 '.select(', emitLambda([fromClause[0]], 1482 'return ' + selectClause[0] + ';'), ')' 1483 ] 1484 ]); 1485 return true; 1486 } 1487 } 1488 ], 1489 // Groupby clauses 1490 [ 1491 { 1492 pattern: ['from', 'group', null], 1493 transformer: function(root, match) { 1494 let fromClause = root[match[0]][1]; 1495 let groupClause = root[match[1]][1]; 1496 root.splice(match[0], 1, [ 1497 'compiled', 1498 [ 1499 '(', fromClause[1], ')' 1500 ] 1501 ]); 1502 let code = ['.groupBy(', emitLambda([fromClause[0]], 1503 'return ' + groupClause[1] + ';')]; 1504 1505 if (fromClause[0] != groupClause[0]) { 1506 code.push(', ', emitLambda([fromClause[0]], 1507 'return ' + groupClause[0] + ';')); 1508 } 1509 code.push(')'); 1510 1511 root.splice(match[1], 1, [ 1512 'compiled', 1513 code 1514 ]); 1515 return true; 1516 } 1517 } 1518 ] 1519 ]; 1520 1521 // Traverses the expression tree in post-order and invokes the 1522 // specified visitor function for each node in the tree. 1523 function postOrderTraverse(root, visitor) { 1524 let i; 1525 for (i = 0; i < root.length; i++) { 1526 if (root[i][0] == 'from' && root[i][1][1] instanceof Array) { 1527 postOrderTraverse(root[i][1][1], visitor); 1528 } else if (root[i][0] == 'compiled') { 1529 for (let j = 0; j < root[i][1].length; j++) { 1530 if (root[i][1][j] instanceof Array) { 1531 postOrderTraverse(root[i][1][j], visitor); 1532 } 1533 } 1534 } 1535 let result = visitor(root, i, root[i]); 1536 if (result == TRAVERSE_PARENT) { 1537 break; 1538 } else if (result == TRAVERSE_ABORT) { 1539 return; 1540 } 1541 } 1542 visitor(root, i, null); 1543 } 1544 1545 // Applies the transformations specified above. 1546 function applyTransformations(expressionTree) { 1547 let transformationBlockCount = transformations.length; 1548 let transformationCount; 1549 let transformation; 1550 let pattern; 1551 let patternIndex; 1552 let match; 1553 let transformationApplied; 1554 let blockApplied; 1555 1556 // This function performs the pattern matching used to determine 1557 // which transformation to apply. 1558 function visitor(root, index, node) { 1559 let nodeType = null; 1560 if (node != null) { 1561 nodeType = node[0]; 1562 } 1563 1564 // Ignore node that are already compiled 1565 if (nodeType == 'compiled') { 1566 return TRAVERSE_CONTINUE 1567 } 1568 1569 let expectedNodeType = pattern[patternIndex]; 1570 let nextExpectedNodeType = 1571 pattern[patternIndex + 1]; 1572 1573 if (nodeType == expectedNodeType) { 1574 match.push(index); 1575 ++patternIndex; 1576 } else if (expectedNodeType == '*') { 1577 if (nodeType == nextExpectedNodeType && 1578 typeof match[patternIndex] != 'undefined') { 1579 patternIndex += 2; 1580 match.push(index); 1581 } else { 1582 if (typeof match[patternIndex] == 'undefined') { 1583 match[patternIndex] = 0; 1584 } 1585 match[patternIndex]++; 1586 } 1587 } else { 1588 if (node == null) { 1589 patternIndex = 0; 1590 match = []; 1591 } 1592 return TRAVERSE_PARENT; 1593 } 1594 1595 if (node == null) { 1596 let returnCode = TRAVERSE_PARENT; 1597 if (patternIndex >= pattern.length) { 1598 let applied = transformation.transformer(root, match); 1599 if (applied) { 1600 returnCode = TRAVERSE_ABORT; 1601 transformationApplied = true; 1602 blockApplied = true; 1603 } 1604 } 1605 patternIndex = 0; 1606 match = []; 1607 return returnCode; 1608 } 1609 1610 return TRAVERSE_CONTINUE; 1611 } 1612 1613 for (let i = 0; i < transformationBlockCount; i++) { 1614 transformationBlock = transformations[i]; 1615 transformationCount = transformationBlock.length; 1616 do { 1617 blockApplied = false; 1618 for (let j = 0; j < transformationCount; j++) { 1619 transformation = transformationBlock[j]; 1620 do { 1621 pattern = transformation.pattern; 1622 patternIndex = 0; 1623 match = []; 1624 transformationApplied = false; 1625 postOrderTraverse(expressionTree, visitor); 1626 } while (transformationApplied); 1627 } 1628 } while (blockApplied); 1629 } 1630 1631 return expressionTree; 1632 } 1633 1634 // After all transformations have been applied, the expression tree 1635 // consists only of nodes that contain JavaScript code. This function 1636 // traverses the expression tree one last time, gathers the JavaScript 1637 // code and finally returns it as a string. 1638 function gatherCode(expressionTree) { 1639 let code = []; 1640 1641 function traverser(root) { 1642 let i; 1643 for (i = 0; i < root.length; i++) { 1644 if (root[i][0] == 'compiled') { 1645 for (let j = 0; j < root[i][1].length; j++) { 1646 if (root[i][1][j] instanceof Array) { 1647 traverser(root[i][1][j]); 1648 } else { 1649 code = code.concat(root[i][1][j]); 1650 } 1651 } 1652 } 1653 } 1654 } 1655 traverser(expressionTree); 1656 return code.join(''); 1657 } 1658 1659 return function(expressionTree) { 1660 // Reset the transparent identifier list and -counter 1661 lastTransparentIdentifier = 0; 1662 transparentIdentifiers = {}; 1663 1664 if (expressionTree instanceof ParseError) { 1665 throw expressionTree; 1666 } else if (expressionTree.rest.replace(/^\s+/, '').length > 0) { 1667 throw new ParseError('End of file expected', 1668 expressionTree.rest.length); 1669 } 1670 1671 expressionTree = applyTransformations(expressionTree.parsed); 1672 return 'return ' + gatherCode(expressionTree) + ';'; 1673 }; 1674 })(); 1675 1676 // Given a ParseError object, this function retrieves the line number, 1677 // offset and context of the error from the original query string. 1678 function getErrorContext(error, query) { 1679 let totalOffset = query.length - error.unparsedCharacters; 1680 let context = { 1681 line: 1, 1682 offset: 1, 1683 context: 0 1684 }; 1685 1686 let lastBreak = 0; 1687 let lastCharacter = ''; 1688 for (let i = 0; i < totalOffset; i++) { 1689 let character = query.charAt(i); 1690 if (character == "\r" || character == "\n" && 1691 lastCharacter != "\r") { 1692 ++context.line; 1693 lastBreak = i; 1694 } 1695 } 1696 context.offset = totalOffset - lastBreak; 1697 context.context = query.substring(totalOffset); 1698 return context; 1699 } 1700
1701 /** 1702 * Constructor for a query object. A query object works a lot like a 1703 * prepared statement in that invoking the constructor will compile but 1704 * not execute the query. Such a query object can be applied to the same 1705 * or even to different datasets any number of times without having to 1706 * re-compile the query. Inside the query expression, you may use 1707 * placeholders for letiables that you wish to pass into the query from 1708 * the outside. Such placeholders begin with a dollar-sign ($) and are 1709 * followed by an index. You can assign concrete values to these 1710 * placeholders by invoking the setValue method on the query object. 1711 * An examplary query that uses placeholders is the following: 1712 * from c in $0 select c.lastname 1713 * Upon invoking this constructor, the query is compiled into JavaScript 1714 * code. Syntactical errors in the query expression will cause the 1715 * constructor to throw a QueryTranslationException. Semantic errors 1716 * in the query expression (such as the use of undeclared letiables) do 1717 * not usually surface at this stage. Instead, their appearence is 1718 * generally deferred until the execute method is invoked. 1719 * @constructor 1720 * @param query The string containing the query 1721 */ 1722 function Query(query) { 1723 let queryParameters = []; 1724 let queryFunction = null; 1725 try { 1726 queryFunction = new Function('_$qp', 1727 compileQuery(parseQuery(query))); 1728 } catch (e) { 1729 if (e instanceof ParseError) { 1730 let errorContext = getErrorContext(e, query); 1731 throw new QueryTranslationException(e.message, 1732 errorContext.line, errorContext.offset, 1733 errorContext.context); 1734 } else { 1735 throw e; 1736 } 1737 } 1738 1739 /** 1740 * Binds a specified value to a placeholder 1741 * @param index Index of the placeholder 1742 * @param value The value to bind to the placeholder 1743 */ 1744 this.setValue = function(index, value) { 1745 if (queryFunction == null) { 1746 throw new jsinq.InvalidOperationException(); 1747 } 1748 queryParameters[index] = value; 1749 }; 1750 1751 /** 1752 * Executes the query and returns the result 1753 * @return The result of the query 1754 */ 1755 this.execute = function() { 1756 if (queryFunction == null) { 1757 throw new jsinq.InvalidOperationException(); 1758 } 1759 return queryFunction(queryParameters); 1760 }; 1761 1762 /** 1763 * Returns the anonymous function that executes the query 1764 * @return The query function 1765 */ 1766 this.getQueryFunction = function() { 1767 if (queryFunction == null) { 1768 throw new jsinq.InvalidOperationException(); 1769 } 1770 return queryFunction; 1771 }; 1772 } 1773 1774 this.QueryTranslationException = QueryTranslationException; 1775 this.Query = Query; 1776 }).call(jsinq);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.