1 /* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.nashorn.internal.objects; 27 28 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError; 29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.List; 34 35 import jdk.nashorn.internal.objects.annotations.Attribute; 36 import jdk.nashorn.internal.objects.annotations.Constructor; 37 import jdk.nashorn.internal.objects.annotations.Function; 38 import jdk.nashorn.internal.objects.annotations.Getter; 39 import jdk.nashorn.internal.objects.annotations.Property; 40 import jdk.nashorn.internal.objects.annotations.ScriptClass; 41 import jdk.nashorn.internal.objects.annotations.SpecializedConstructor; 42 import jdk.nashorn.internal.objects.annotations.Where; 43 import jdk.nashorn.internal.runtime.BitVector; 44 import jdk.nashorn.internal.runtime.JSType; 45 import jdk.nashorn.internal.runtime.ParserException; 46 import jdk.nashorn.internal.runtime.PropertyMap; 47 import jdk.nashorn.internal.runtime.regexp.RegExp; 48 import jdk.nashorn.internal.runtime.regexp.RegExpFactory; 49 import jdk.nashorn.internal.runtime.regexp.RegExpResult; 50 import jdk.nashorn.internal.runtime.regexp.RegExpMatcher; 51 import jdk.nashorn.internal.runtime.ScriptFunction; 52 import jdk.nashorn.internal.runtime.ScriptObject; 53 import jdk.nashorn.internal.runtime.ScriptRuntime; 54 55 /** 56 * ECMA 15.10 RegExp Objects. 57 */ 58 @ScriptClass("RegExp") 59 public final class NativeRegExp extends ScriptObject { 60 /** ECMA 15.10.7.5 lastIndex property */ 61 @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE) 62 public Object lastIndex; 63 64 /** Compiled regexp */ 65 private RegExp regexp; 66 67 // Reference to global object needed to support static RegExp properties 68 private final Global globalObject; 69 70 // initialized by nasgen 71 private static PropertyMap $nasgenmap$; 72 73 private NativeRegExp(final Global global) { 74 super(global.getRegExpPrototype(), $nasgenmap$); 75 this.globalObject = global; 76 } 77 78 NativeRegExp(final String input, final String flagString, final Global global) { 79 this(global); 80 try { 81 this.regexp = RegExpFactory.create(input, flagString); 82 } catch (final ParserException e) { 83 // translate it as SyntaxError object and throw it 84 e.throwAsEcmaException(); 85 throw new AssertionError(); //guard against null warnings below 86 } 87 88 this.setLastIndex(0); 89 } 90 91 NativeRegExp(final String input, final String flagString) { 92 this(input, flagString, Global.instance()); 93 } 94 95 NativeRegExp(final String string, final Global global) { 96 this(string, "", global); 97 } 98 99 NativeRegExp(final String string) { 100 this(string, Global.instance()); 101 } 102 103 NativeRegExp(final NativeRegExp regExp) { 104 this(Global.instance()); 105 this.lastIndex = regExp.getLastIndexObject(); 106 this.regexp = regExp.getRegExp(); 107 } 108 109 @Override 110 public String getClassName() { 111 return "RegExp"; 112 } 113 114 /** 115 * ECMA 15.10.4 116 * 117 * Constructor 118 * 119 * @param isNew is the new operator used for instantiating this regexp 120 * @param self self reference 121 * @param args arguments (optional: pattern and flags) 122 * @return new NativeRegExp 123 */ 124 @Constructor(arity = 2) 125 public static Object constructor(final boolean isNew, final Object self, final Object... args) { 126 if (args.length > 1) { 127 return newRegExp(args[0], args[1]); 128 } else if (args.length > 0) { 129 return newRegExp(args[0], UNDEFINED); 130 } 131 132 return newRegExp(UNDEFINED, UNDEFINED); 133 } 134 135 /** 136 * ECMA 15.10.4 137 * 138 * Constructor - specialized version, no args, empty regexp 139 * 140 * @param isNew is the new operator used for instantiating this regexp 141 * @param self self reference 142 * @return new NativeRegExp 143 */ 144 @SpecializedConstructor 145 public static Object constructor(final boolean isNew, final Object self) { 146 return new NativeRegExp("", ""); 147 } 148 149 /** 150 * ECMA 15.10.4 151 * 152 * Constructor - specialized version, pattern, no flags 153 * 154 * @param isNew is the new operator used for instantiating this regexp 155 * @param self self reference 156 * @param pattern pattern 157 * @return new NativeRegExp 158 */ 159 @SpecializedConstructor 160 public static Object constructor(final boolean isNew, final Object self, final Object pattern) { 161 return newRegExp(pattern, UNDEFINED); 162 } 163 164 /** 165 * ECMA 15.10.4 166 * 167 * Constructor - specialized version, pattern and flags 168 * 169 * @param isNew is the new operator used for instantiating this regexp 170 * @param self self reference 171 * @param pattern pattern 172 * @param flags flags 173 * @return new NativeRegExp 174 */ 175 @SpecializedConstructor 176 public static Object constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) { 177 return newRegExp(pattern, flags); 178 } 179 180 /** 181 * External constructor used in generated code, which explains the public access 182 * 183 * @param regexp regexp 184 * @param flags flags 185 * @return new NativeRegExp 186 */ 187 public static NativeRegExp newRegExp(final Object regexp, final Object flags) { 188 String patternString = ""; 189 String flagString = ""; 190 191 if (regexp != UNDEFINED) { 192 if (regexp instanceof NativeRegExp) { 193 if (flags != UNDEFINED) { 194 throw typeError("regex.cant.supply.flags"); 195 } 196 return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as 197 } 198 patternString = JSType.toString(regexp); 199 } 200 201 if (flags != UNDEFINED) { 202 flagString = JSType.toString(flags); 203 } 204 205 return new NativeRegExp(patternString, flagString); 206 } 207 208 /** 209 * Build a regexp that matches {@code string} as-is. All meta-characters will be escaped. 210 * 211 * @param string pattern string 212 * @return flat regexp 213 */ 214 static NativeRegExp flatRegExp(String string) { 215 // escape special characters 216 StringBuilder sb = null; 217 final int length = string.length(); 218 219 for (int i = 0; i < length; i++) { 220 final char c = string.charAt(i); 221 switch (c) { 222 case '^': 223 case '$': 224 case '\\': 225 case '.': 226 case '*': 227 case '+': 228 case '?': 229 case '(': 230 case ')': 231 case '[': 232 case '{': 233 case '|': 234 if (sb == null) { 235 sb = new StringBuilder(length * 2); 236 sb.append(string, 0, i); 237 } 238 sb.append('\\'); 239 sb.append(c); 240 break; 241 default: 242 if (sb != null) { 243 sb.append(c); 244 } 245 break; 246 } 247 } 248 return new NativeRegExp(sb == null ? string : sb.toString(), ""); 249 } 250 251 private String getFlagString() { 252 final StringBuilder sb = new StringBuilder(3); 253 254 if (regexp.isGlobal()) { 255 sb.append('g'); 256 } 257 if (regexp.isIgnoreCase()) { 258 sb.append('i'); 259 } 260 if (regexp.isMultiline()) { 261 sb.append('m'); 262 } 263 264 return sb.toString(); 265 } 266 267 @Override 268 public String safeToString() { 269 return "[RegExp " + toString() + "]"; 270 } 271 272 @Override 273 public String toString() { 274 return "/" + regexp.getSource() + "/" + getFlagString(); 275 } 276 277 /** 278 * Nashorn extension: RegExp.prototype.compile - everybody implements this! 279 * 280 * @param self self reference 281 * @param pattern pattern 282 * @param flags flags 283 * @return new NativeRegExp 284 */ 285 @Function(attributes = Attribute.NOT_ENUMERABLE) 286 public static Object compile(final Object self, final Object pattern, final Object flags) { 287 final NativeRegExp regExp = checkRegExp(self); 288 final NativeRegExp compiled = newRegExp(pattern, flags); 289 // copy over regexp to 'self' 290 regExp.setRegExp(compiled.getRegExp()); 291 292 // Some implementations return undefined. Some return 'self'. Since return 293 // value is most likely be ignored, we can play safe and return 'self'. 294 return regExp; 295 } 296 297 /** 298 * ECMA 15.10.6.2 RegExp.prototype.exec(string) 299 * 300 * @param self self reference 301 * @param string string to match against regexp 302 * @return array containing the matches or {@code null} if no match 303 */ 304 @Function(attributes = Attribute.NOT_ENUMERABLE) 305 public static Object exec(final Object self, final Object string) { 306 return checkRegExp(self).exec(JSType.toString(string)); 307 } 308 309 /** 310 * ECMA 15.10.6.3 RegExp.prototype.test(string) 311 * 312 * @param self self reference 313 * @param string string to test for matches against regexp 314 * @return true if matches found, false otherwise 315 */ 316 @Function(attributes = Attribute.NOT_ENUMERABLE) 317 public static Object test(final Object self, final Object string) { 318 return checkRegExp(self).test(JSType.toString(string)); 319 } 320 321 /** 322 * ECMA 15.10.6.4 RegExp.prototype.toString() 323 * 324 * @param self self reference 325 * @return string version of regexp 326 */ 327 @Function(attributes = Attribute.NOT_ENUMERABLE) 328 public static Object toString(final Object self) { 329 return checkRegExp(self).toString(); 330 } 331 332 /** 333 * ECMA 15.10.7.1 source 334 * 335 * @param self self reference 336 * @return the input string for the regexp 337 */ 338 @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) 339 public static Object source(final Object self) { 340 return checkRegExp(self).getRegExp().getSource(); 341 } 342 343 /** 344 * ECMA 15.10.7.2 global 345 * 346 * @param self self reference 347 * @return true if this regexp is flagged global, false otherwise 348 */ 349 @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) 350 public static Object global(final Object self) { 351 return checkRegExp(self).getRegExp().isGlobal(); 352 } 353 354 /** 355 * ECMA 15.10.7.3 ignoreCase 356 * 357 * @param self self reference 358 * @return true if this regexp if flagged to ignore case, false otherwise 359 */ 360 @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) 361 public static Object ignoreCase(final Object self) { 362 return checkRegExp(self).getRegExp().isIgnoreCase(); 363 } 364 365 /** 366 * ECMA 15.10.7.4 multiline 367 * 368 * @param self self reference 369 * @return true if this regexp is flagged to be multiline, false otherwise 370 */ 371 @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) 372 public static Object multiline(final Object self) { 373 return checkRegExp(self).getRegExp().isMultiline(); 374 } 375 376 /** 377 * Getter for non-standard RegExp.input property. 378 * @param self self object 379 * @return last regexp input 380 */ 381 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input") 382 public static Object getLastInput(Object self) { 383 final RegExpResult match = Global.instance().getLastRegExpResult(); 384 return match == null ? "" : match.getInput(); 385 } 386 387 /** 388 * Getter for non-standard RegExp.multiline property. 389 * @param self self object 390 * @return last regexp input 391 */ 392 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline") 393 public static Object getLastMultiline(Object self) { 394 return false; // doesn't ever seem to become true and isn't documented anyhwere 395 } 396 397 /** 398 * Getter for non-standard RegExp.lastMatch property. 399 * @param self self object 400 * @return last regexp input 401 */ 402 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch") 403 public static Object getLastMatch(Object self) { 404 final RegExpResult match = Global.instance().getLastRegExpResult(); 405 return match == null ? "" : match.getGroup(0); 406 } 407 408 /** 409 * Getter for non-standard RegExp.lastParen property. 410 * @param self self object 411 * @return last regexp input 412 */ 413 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen") 414 public static Object getLastParen(Object self) { 415 final RegExpResult match = Global.instance().getLastRegExpResult(); 416 return match == null ? "" : match.getLastParen(); 417 } 418 419 /** 420 * Getter for non-standard RegExp.leftContext property. 421 * @param self self object 422 * @return last regexp input 423 */ 424 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext") 425 public static Object getLeftContext(Object self) { 426 final RegExpResult match = Global.instance().getLastRegExpResult(); 427 return match == null ? "" : match.getInput().substring(0, match.getIndex()); 428 } 429 430 /** 431 * Getter for non-standard RegExp.rightContext property. 432 * @param self self object 433 * @return last regexp input 434 */ 435 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext") 436 public static Object getRightContext(Object self) { 437 final RegExpResult match = Global.instance().getLastRegExpResult(); 438 return match == null ? "" : match.getInput().substring(match.getIndex() + match.length()); 439 } 440 441 /** 442 * Getter for non-standard RegExp.$1 property. 443 * @param self self object 444 * @return last regexp input 445 */ 446 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1") 447 public static Object getGroup1(Object self) { 448 final RegExpResult match = Global.instance().getLastRegExpResult(); 449 return match == null ? "" : match.getGroup(1); 450 } 451 452 /** 453 * Getter for non-standard RegExp.$2 property. 454 * @param self self object 455 * @return last regexp input 456 */ 457 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2") 458 public static Object getGroup2(Object self) { 459 final RegExpResult match = Global.instance().getLastRegExpResult(); 460 return match == null ? "" : match.getGroup(2); 461 } 462 463 /** 464 * Getter for non-standard RegExp.$3 property. 465 * @param self self object 466 * @return last regexp input 467 */ 468 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3") 469 public static Object getGroup3(Object self) { 470 final RegExpResult match = Global.instance().getLastRegExpResult(); 471 return match == null ? "" : match.getGroup(3); 472 } 473 474 /** 475 * Getter for non-standard RegExp.$4 property. 476 * @param self self object 477 * @return last regexp input 478 */ 479 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4") 480 public static Object getGroup4(Object self) { 481 final RegExpResult match = Global.instance().getLastRegExpResult(); 482 return match == null ? "" : match.getGroup(4); 483 } 484 485 /** 486 * Getter for non-standard RegExp.$5 property. 487 * @param self self object 488 * @return last regexp input 489 */ 490 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5") 491 public static Object getGroup5(Object self) { 492 final RegExpResult match = Global.instance().getLastRegExpResult(); 493 return match == null ? "" : match.getGroup(5); 494 } 495 496 /** 497 * Getter for non-standard RegExp.$6 property. 498 * @param self self object 499 * @return last regexp input 500 */ 501 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6") 502 public static Object getGroup6(Object self) { 503 final RegExpResult match = Global.instance().getLastRegExpResult(); 504 return match == null ? "" : match.getGroup(6); 505 } 506 507 /** 508 * Getter for non-standard RegExp.$7 property. 509 * @param self self object 510 * @return last regexp input 511 */ 512 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7") 513 public static Object getGroup7(Object self) { 514 final RegExpResult match = Global.instance().getLastRegExpResult(); 515 return match == null ? "" : match.getGroup(7); 516 } 517 518 /** 519 * Getter for non-standard RegExp.$8 property. 520 * @param self self object 521 * @return last regexp input 522 */ 523 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8") 524 public static Object getGroup8(Object self) { 525 final RegExpResult match = Global.instance().getLastRegExpResult(); 526 return match == null ? "" : match.getGroup(8); 527 } 528 529 /** 530 * Getter for non-standard RegExp.$9 property. 531 * @param self self object 532 * @return last regexp input 533 */ 534 @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9") 535 public static Object getGroup9(Object self) { 536 final RegExpResult match = Global.instance().getLastRegExpResult(); 537 return match == null ? "" : match.getGroup(9); 538 } 539 540 private RegExpResult execInner(final String string) { 541 final boolean isGlobal = regexp.isGlobal(); 542 int start = getLastIndex(); 543 if (!isGlobal) { 544 start = 0; 545 } 546 547 if (start < 0 || start > string.length()) { 548 if (isGlobal) { 549 setLastIndex(0); 550 } 551 return null; 552 } 553 554 final RegExpMatcher matcher = regexp.match(string); 555 if (matcher == null || !matcher.search(start)) { 556 if (isGlobal) { 557 setLastIndex(0); 558 } 559 return null; 560 } 561 562 if (isGlobal) { 563 setLastIndex(matcher.end()); 564 } 565 566 final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher)); 567 globalObject.setLastRegExpResult(match); 568 return match; 569 } 570 571 // String.prototype.split method ignores the global flag and should not update lastIndex property. 572 private RegExpResult execSplit(final String string, int start) { 573 if (start < 0 || start > string.length()) { 574 return null; 575 } 576 577 final RegExpMatcher matcher = regexp.match(string); 578 if (matcher == null || !matcher.search(start)) { 579 return null; 580 } 581 582 final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher)); 583 globalObject.setLastRegExpResult(match); 584 return match; 585 } 586 587 /** 588 * Convert java.util.regex.Matcher groups to JavaScript groups. 589 * That is, replace null and groups that didn't match with undefined. 590 */ 591 private Object[] groups(final RegExpMatcher matcher) { 592 final int groupCount = matcher.groupCount(); 593 final Object[] groups = new Object[groupCount + 1]; 594 final BitVector groupsInNegativeLookahead = regexp.getGroupsInNegativeLookahead(); 595 596 for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) { 597 final int groupStart = matcher.start(i); 598 if (lastGroupStart > groupStart 599 || (groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i))) { 600 // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated. 601 // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere 602 // in the pattern always return undefined because the negative lookahead must fail. 603 groups[i] = UNDEFINED; 604 continue; 605 } 606 final String group = matcher.group(i); 607 groups[i] = group == null ? UNDEFINED : group; 608 lastGroupStart = groupStart; 609 } 610 return groups; 611 } 612 613 /** 614 * Executes a search for a match within a string based on a regular 615 * expression. It returns an array of information or null if no match is 616 * found. 617 * 618 * @param string String to match. 619 * @return NativeArray of matches, string or null. 620 */ 621 public Object exec(final String string) { 622 final RegExpResult match = execInner(string); 623 624 if (match == null) { 625 return null; 626 } 627 628 return new NativeRegExpExecResult(match, globalObject); 629 } 630 631 /** 632 * Executes a search for a match within a string based on a regular 633 * expression. 634 * 635 * @param string String to match. 636 * @return True if a match is found. 637 */ 638 public Object test(final String string) { 639 return execInner(string) != null; 640 } 641 642 /** 643 * Searches and replaces the regular expression portion (match) with the 644 * replaced text instead. For the "replacement text" parameter, you can use 645 * the keywords $1 to $2 to replace the original text with values from 646 * sub-patterns defined within the main pattern. 647 * 648 * @param string String to match. 649 * @param replacement Replacement string. 650 * @return String with substitutions. 651 */ 652 Object replace(final String string, final String replacement, final ScriptFunction function) { 653 final RegExpMatcher matcher = regexp.match(string); 654 655 if (matcher == null) { 656 return string; 657 } 658 659 if (!regexp.isGlobal()) { 660 if (!matcher.search(0)) { 661 return string; 662 } 663 664 final StringBuilder sb = new StringBuilder(); 665 sb.append(string, 0, matcher.start()); 666 667 if (function != null) { 668 sb.append(callReplaceValue(function, matcher, string)); 669 } else { 670 appendReplacement(matcher, string, replacement, sb); 671 } 672 sb.append(string, matcher.end(), string.length()); 673 return sb.toString(); 674 } 675 676 setLastIndex(0); 677 678 if (!matcher.search(0)) { 679 return string; 680 } 681 682 int thisIndex = 0; 683 int previousLastIndex = 0; 684 final StringBuilder sb = new StringBuilder(); 685 686 do { 687 sb.append(string, thisIndex, matcher.start()); 688 if (function != null) { 689 sb.append(callReplaceValue(function, matcher, string)); 690 } else { 691 appendReplacement(matcher, string, replacement, sb); 692 } 693 694 thisIndex = matcher.end(); 695 if (thisIndex == string.length() && matcher.start() == matcher.end()) { 696 // Avoid getting empty match at end of string twice 697 break; 698 } 699 700 // ECMA 15.5.4.10 String.prototype.match(regexp) 701 if (thisIndex == previousLastIndex) { 702 setLastIndex(thisIndex + 1); 703 previousLastIndex = thisIndex + 1; 704 } else { 705 previousLastIndex = thisIndex; 706 } 707 } while (previousLastIndex <= string.length() && matcher.search(previousLastIndex)); 708 709 sb.append(string, thisIndex, string.length()); 710 711 return sb.toString(); 712 } 713 714 private void appendReplacement(final RegExpMatcher matcher, final String text, final String replacement, final StringBuilder sb) { 715 /* 716 * Process substitution patterns: 717 * 718 * $$ -> $ 719 * $& -> the matched substring 720 * $` -> the portion of string that preceeds matched substring 721 * $' -> the portion of string that follows the matched substring 722 * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit 723 * $nn -> the nnth capture, where nn is a two digit decimal number [01-99]. 724 */ 725 726 int cursor = 0; 727 Object[] groups = null; 728 729 while (cursor < replacement.length()) { 730 char nextChar = replacement.charAt(cursor); 731 if (nextChar == '$') { 732 // Skip past $ 733 cursor++; 734 nextChar = replacement.charAt(cursor); 735 final int firstDigit = nextChar - '0'; 736 737 if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) { 738 // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit. 739 int refNum = firstDigit; 740 cursor++; 741 if (cursor < replacement.length() && firstDigit < matcher.groupCount()) { 742 final int secondDigit = replacement.charAt(cursor) - '0'; 743 if ((secondDigit >= 0) && (secondDigit <= 9)) { 744 final int newRefNum = (firstDigit * 10) + secondDigit; 745 if (newRefNum <= matcher.groupCount() && newRefNum > 0) { 746 // $nn ($01-$99) 747 refNum = newRefNum; 748 cursor++; 749 } 750 } 751 } 752 if (refNum > 0) { 753 if (groups == null) { 754 groups = groups(matcher); 755 } 756 // Append group if matched. 757 if (groups[refNum] != UNDEFINED) { 758 sb.append((String) groups[refNum]); 759 } 760 } else { // $0. ignore. 761 assert refNum == 0; 762 sb.append("$0"); 763 } 764 } else if (nextChar == '$') { 765 sb.append('$'); 766 cursor++; 767 } else if (nextChar == '&') { 768 sb.append(matcher.group()); 769 cursor++; 770 } else if (nextChar == '`') { 771 sb.append(text, 0, matcher.start()); 772 cursor++; 773 } else if (nextChar == '\'') { 774 sb.append(text, matcher.end(), text.length()); 775 cursor++; 776 } else { 777 // unknown substitution or $n with n>m. skip. 778 sb.append('$'); 779 } 780 } else { 781 sb.append(nextChar); 782 cursor++; 783 } 784 } 785 } 786 787 private String callReplaceValue(final ScriptFunction function, final RegExpMatcher matcher, final String string) { 788 final Object[] groups = groups(matcher); 789 final Object[] args = Arrays.copyOf(groups, groups.length + 2); 790 791 args[groups.length] = matcher.start(); 792 args[groups.length + 1] = string; 793 794 final Object self = function.isStrict() ? UNDEFINED : Global.instance(); 795 796 return JSType.toString(ScriptRuntime.apply(function, self, args)); 797 } 798 799 /** 800 * Breaks up a string into an array of substrings based on a regular 801 * expression or fixed string. 802 * 803 * @param string String to match. 804 * @param limit Split limit. 805 * @return Array of substrings. 806 */ 807 Object split(final String string, final long limit) { 808 if (limit == 0L) { 809 return new NativeArray(); 810 } 811 812 final List<Object> matches = new ArrayList<>(); 813 814 RegExpResult match; 815 final int inputLength = string.length(); 816 int splitLastLength = -1; 817 int splitLastIndex = 0; 818 int splitLastLastIndex = 0; 819 820 while ((match = execSplit(string, splitLastIndex)) != null) { 821 splitLastIndex = match.getIndex() + match.length(); 822 823 if (splitLastIndex > splitLastLastIndex) { 824 matches.add(string.substring(splitLastLastIndex, match.getIndex())); 825 final Object[] groups = match.getGroups(); 826 if (groups.length > 1 && match.getIndex() < inputLength) { 827 for (int index = 1; index < groups.length && matches.size() < limit; index++) { 828 matches.add(groups[index]); 829 } 830 } 831 832 splitLastLength = match.length(); 833 834 if (matches.size() >= limit) { 835 break; 836 } 837 } 838 839 // bump the index to avoid infinite loop 840 if (splitLastIndex == splitLastLastIndex) { 841 splitLastIndex++; 842 } else { 843 splitLastLastIndex = splitLastIndex; 844 } 845 } 846 847 if (matches.size() < limit) { 848 // check special case if we need to append an empty string at the 849 // end of the match 850 // if the lastIndex was the entire string 851 if (splitLastLastIndex == string.length()) { 852 if (splitLastLength > 0 || execSplit("", 0) == null) { 853 matches.add(""); 854 } 855 } else { 856 matches.add(string.substring(splitLastLastIndex, inputLength)); 857 } 858 } 859 860 return new NativeArray(matches.toArray()); 861 } 862 863 /** 864 * Tests for a match in a string. It returns the index of the match, or -1 865 * if not found. 866 * 867 * @param string String to match. 868 * @return Index of match. 869 */ 870 Object search(final String string) { 871 final RegExpResult match = execInner(string); 872 873 if (match == null) { 874 return -1; 875 } 876 877 return match.getIndex(); 878 } 879 880 /** 881 * Fast lastIndex getter 882 * @return last index property as int 883 */ 884 public int getLastIndex() { 885 return JSType.toInteger(lastIndex); 886 } 887 888 /** 889 * Fast lastIndex getter 890 * @return last index property as boxed integer 891 */ 892 public Object getLastIndexObject() { 893 return lastIndex; 894 } 895 896 /** 897 * Fast lastIndex setter 898 * @param lastIndex lastIndex 899 */ 900 public void setLastIndex(final int lastIndex) { 901 this.lastIndex = JSType.toObject(lastIndex); 902 } 903 904 private static NativeRegExp checkRegExp(final Object self) { 905 Global.checkObjectCoercible(self); 906 if (self instanceof NativeRegExp) { 907 return (NativeRegExp)self; 908 } else if (self != null && self == Global.instance().getRegExpPrototype()) { 909 return Global.instance().DEFAULT_REGEXP; 910 } else { 911 throw typeError("not.a.regexp", ScriptRuntime.safeToString(self)); 912 } 913 } 914 915 boolean getGlobal() { 916 return regexp.isGlobal(); 917 } 918 919 private RegExp getRegExp() { 920 return regexp; 921 } 922 923 private void setRegExp(final RegExp regexp) { 924 this.regexp = regexp; 925 } 926 927 }