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