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