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.regexp.RegExp;
  47 import jdk.nashorn.internal.runtime.regexp.RegExpFactory;
  48 import jdk.nashorn.internal.runtime.regexp.RegExpResult;
  49 import jdk.nashorn.internal.runtime.regexp.RegExpMatcher;
  50 import jdk.nashorn.internal.runtime.ScriptFunction;
  51 import jdk.nashorn.internal.runtime.ScriptObject;
  52 import jdk.nashorn.internal.runtime.ScriptRuntime;
  53 
  54 /**
  55  * ECMA 15.10 RegExp Objects.
  56  */
  57 @ScriptClass("RegExp")
  58 public final class NativeRegExp extends ScriptObject {
  59     /** ECMA 15.10.7.5 lastIndex property */
  60     @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE)
  61     public Object lastIndex;
  62 
  63     /** Compiled regexp */
  64     private RegExp regexp;
  65 
  66     // Reference to global object needed to support static RegExp properties
  67     private Global globalObject;
  68 
  69     NativeRegExp(final String input, final String flagString) {
  70         try {
  71             this.regexp = RegExpFactory.create(input, flagString);
  72         } catch (final ParserException e) {
  73             // translate it as SyntaxError object and throw it
  74             e.throwAsEcmaException();
  75             throw new AssertionError(); //guard against null warnings below
  76         }
  77 
  78         this.setLastIndex(0);
  79         init();
  80     }
  81 
  82     NativeRegExp(final String string) {
  83         this(string, "");
  84     }
  85 
  86     NativeRegExp(final NativeRegExp regExp) {
  87         this.lastIndex  = regExp.getLastIndexObject();
  88         this.regexp      = regExp.getRegExp();
  89         init();
  90     }
  91 
  92     @Override
  93     public String getClassName() {
  94         return "RegExp";
  95     }
  96 
  97     /**
  98      * ECMA 15.10.4
  99      *
 100      * Constructor
 101      *
 102      * @param isNew is the new operator used for instantiating this regexp
 103      * @param self  self reference
 104      * @param args  arguments (optional: pattern and flags)
 105      * @return new NativeRegExp
 106      */
 107     @Constructor(arity = 2)
 108     public static Object constructor(final boolean isNew, final Object self, final Object... args) {
 109         if (args.length > 1) {
 110             return newRegExp(args[0], args[1]);
 111         } else if (args.length > 0) {
 112             return newRegExp(args[0], UNDEFINED);
 113         }
 114 
 115         return newRegExp(UNDEFINED, UNDEFINED);
 116     }
 117 
 118     /**
 119      * ECMA 15.10.4
 120      *
 121      * Constructor - specialized version, no args, empty regexp
 122      *
 123      * @param isNew is the new operator used for instantiating this regexp
 124      * @param self  self reference
 125      * @return new NativeRegExp
 126      */
 127     @SpecializedConstructor
 128     public static Object constructor(final boolean isNew, final Object self) {
 129         return new NativeRegExp("", "");
 130     }
 131 
 132     /**
 133      * ECMA 15.10.4
 134      *
 135      * Constructor - specialized version, pattern, no flags
 136      *
 137      * @param isNew is the new operator used for instantiating this regexp
 138      * @param self  self reference
 139      * @param pattern pattern
 140      * @return new NativeRegExp
 141      */
 142     @SpecializedConstructor
 143     public static Object constructor(final boolean isNew, final Object self, final Object pattern) {
 144         return newRegExp(pattern, UNDEFINED);
 145     }
 146 
 147     /**
 148      * ECMA 15.10.4
 149      *
 150      * Constructor - specialized version, pattern and flags
 151      *
 152      * @param isNew is the new operator used for instantiating this regexp
 153      * @param self  self reference
 154      * @param pattern pattern
 155      * @param flags  flags
 156      * @return new NativeRegExp
 157      */
 158     @SpecializedConstructor
 159     public static Object constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) {
 160         return newRegExp(pattern, flags);
 161     }
 162 
 163     /**
 164      * External constructor used in generated code, which explains the public access
 165      *
 166      * @param regexp regexp
 167      * @param flags  flags
 168      * @return new NativeRegExp
 169      */
 170     public static NativeRegExp newRegExp(final Object regexp, final Object flags) {
 171         String  patternString = "";
 172         String  flagString    = "";
 173         boolean flagsDefined  = false;
 174 
 175         if (flags != UNDEFINED) {
 176             flagsDefined = true;
 177             flagString = JSType.toString(flags);
 178         }
 179 
 180         if (regexp != UNDEFINED) {
 181             if (regexp instanceof NativeRegExp) {
 182                 if (!flagsDefined) {
 183                     return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as
 184                 }
 185                 throw typeError("regex.cant.supply.flags");
 186             }
 187             patternString = JSType.toString(regexp);
 188         }
 189 
 190         return new NativeRegExp(patternString, flagString);
 191     }
 192 
 193     /**
 194      * Build a regexp that matches {@code string} as-is. All meta-characters will be escaped.
 195      *
 196      * @param string pattern string
 197      * @return flat regexp
 198      */
 199     static NativeRegExp flatRegExp(String string) {
 200         // escape special characters
 201         StringBuilder sb = null;
 202         final int length = string.length();
 203 
 204         for (int i = 0; i < length; i++) {
 205             final char c = string.charAt(i);
 206             switch (c) {
 207                 case '^':
 208                 case '$':
 209                 case '\\':
 210                 case '.':
 211                 case '*':
 212                 case '+':
 213                 case '?':
 214                 case '(':
 215                 case ')':
 216                 case '[':
 217                 case '{':
 218                 case '|':
 219                     if (sb == null) {
 220                         sb = new StringBuilder(length * 2);
 221                         sb.append(string, 0, i);
 222                     }
 223                     sb.append('\\');
 224                     sb.append(c);
 225                     break;
 226                 default:
 227                     if (sb != null) {
 228                         sb.append(c);
 229                     }
 230                     break;
 231             }
 232         }
 233         return new NativeRegExp(sb == null ? string : sb.toString(), "");
 234     }
 235 
 236     private String getFlagString() {
 237         final StringBuilder sb = new StringBuilder(3);
 238 
 239         if (regexp.isGlobal()) {
 240             sb.append('g');
 241         }
 242         if (regexp.isIgnoreCase()) {
 243             sb.append('i');
 244         }
 245         if (regexp.isMultiline()) {
 246             sb.append('m');
 247         }
 248 
 249         return sb.toString();
 250     }
 251 
 252     @Override
 253     public String safeToString() {
 254         return "[RegExp " + toString() + "]";
 255     }
 256 
 257     @Override
 258     public String toString() {
 259         return "/" + regexp.getSource() + "/" + getFlagString();
 260     }
 261 
 262     /**
 263      * Nashorn extension: RegExp.prototype.compile - everybody implements this!
 264      *
 265      * @param self    self reference
 266      * @param pattern pattern
 267      * @param flags   flags
 268      * @return new NativeRegExp
 269      */
 270     @Function(attributes = Attribute.NOT_ENUMERABLE)
 271     public static Object compile(final Object self, final Object pattern, final Object flags) {
 272         final NativeRegExp regExp   = checkRegExp(self);
 273         final NativeRegExp compiled = newRegExp(pattern, flags);
 274         // copy over regexp to 'self'
 275         regExp.setRegExp(compiled.getRegExp());
 276 
 277         // Some implementations return undefined. Some return 'self'. Since return
 278         // value is most likely be ignored, we can play safe and return 'self'.
 279         return regExp;
 280     }
 281 
 282     /**
 283      * ECMA 15.10.6.2 RegExp.prototype.exec(string)
 284      *
 285      * @param self   self reference
 286      * @param string string to match against regexp
 287      * @return array containing the matches or {@code null} if no match
 288      */
 289     @Function(attributes = Attribute.NOT_ENUMERABLE)
 290     public static Object exec(final Object self, final Object string) {
 291         return checkRegExp(self).exec(JSType.toString(string));
 292     }
 293 
 294     /**
 295      * ECMA 15.10.6.3 RegExp.prototype.test(string)
 296      *
 297      * @param self   self reference
 298      * @param string string to test for matches against regexp
 299      * @return true if matches found, false otherwise
 300      */
 301     @Function(attributes = Attribute.NOT_ENUMERABLE)
 302     public static Object test(final Object self, final Object string) {
 303         return checkRegExp(self).test(JSType.toString(string));
 304     }
 305 
 306     /**
 307      * ECMA 15.10.6.4 RegExp.prototype.toString()
 308      *
 309      * @param self self reference
 310      * @return string version of regexp
 311      */
 312     @Function(attributes = Attribute.NOT_ENUMERABLE)
 313     public static Object toString(final Object self) {
 314         return checkRegExp(self).toString();
 315     }
 316 
 317     /**
 318      * ECMA 15.10.7.1 source
 319      *
 320      * @param self self reference
 321      * @return the input string for the regexp
 322      */
 323     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 324     public static Object source(final Object self) {
 325         return checkRegExp(self).getRegExp().getSource();
 326     }
 327 
 328     /**
 329      * ECMA 15.10.7.2 global
 330      *
 331      * @param self self reference
 332      * @return true if this regexp is flagged global, false otherwise
 333      */
 334     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 335     public static Object global(final Object self) {
 336         return checkRegExp(self).getRegExp().isGlobal();
 337     }
 338 
 339     /**
 340      * ECMA 15.10.7.3 ignoreCase
 341      *
 342      * @param self self reference
 343      * @return true if this regexp if flagged to ignore case, false otherwise
 344      */
 345     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 346     public static Object ignoreCase(final Object self) {
 347         return checkRegExp(self).getRegExp().isIgnoreCase();
 348     }
 349 
 350     /**
 351      * ECMA 15.10.7.4 multiline
 352      *
 353      * @param self self reference
 354      * @return true if this regexp is flagged to be multiline, false otherwise
 355      */
 356     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 357     public static Object multiline(final Object self) {
 358         return checkRegExp(self).getRegExp().isMultiline();
 359     }
 360 
 361     /**
 362      * Getter for non-standard RegExp.input property.
 363      * @param self self object
 364      * @return last regexp input
 365      */
 366     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input")
 367     public static Object getLastInput(Object self) {
 368         final RegExpResult match = Global.instance().getLastRegExpResult();
 369         return match == null ? "" : match.getInput();
 370     }
 371 
 372     /**
 373      * Getter for non-standard RegExp.multiline property.
 374      * @param self self object
 375      * @return last regexp input
 376      */
 377     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline")
 378     public static Object getLastMultiline(Object self) {
 379         return false; // doesn't ever seem to become true and isn't documented anyhwere
 380     }
 381 
 382     /**
 383      * Getter for non-standard RegExp.lastMatch property.
 384      * @param self self object
 385      * @return last regexp input
 386      */
 387     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch")
 388     public static Object getLastMatch(Object self) {
 389         final RegExpResult match = Global.instance().getLastRegExpResult();
 390         return match == null ? "" : match.getGroup(0);
 391     }
 392 
 393     /**
 394      * Getter for non-standard RegExp.lastParen property.
 395      * @param self self object
 396      * @return last regexp input
 397      */
 398     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen")
 399     public static Object getLastParen(Object self) {
 400         final RegExpResult match = Global.instance().getLastRegExpResult();
 401         return match == null ? "" : match.getLastParen();
 402     }
 403 
 404     /**
 405      * Getter for non-standard RegExp.leftContext property.
 406      * @param self self object
 407      * @return last regexp input
 408      */
 409     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext")
 410     public static Object getLeftContext(Object self) {
 411         final RegExpResult match = Global.instance().getLastRegExpResult();
 412         return match == null ? "" : match.getInput().substring(0, match.getIndex());
 413     }
 414 
 415     /**
 416      * Getter for non-standard RegExp.rightContext property.
 417      * @param self self object
 418      * @return last regexp input
 419      */
 420     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext")
 421     public static Object getRightContext(Object self) {
 422         final RegExpResult match = Global.instance().getLastRegExpResult();
 423         return match == null ? "" : match.getInput().substring(match.getIndex() + match.length());
 424     }
 425 
 426     /**
 427      * Getter for non-standard RegExp.$1 property.
 428      * @param self self object
 429      * @return last regexp input
 430      */
 431     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1")
 432     public static Object getGroup1(Object self) {
 433         final RegExpResult match = Global.instance().getLastRegExpResult();
 434         return match == null ? "" : match.getGroup(1);
 435     }
 436 
 437     /**
 438      * Getter for non-standard RegExp.$2 property.
 439      * @param self self object
 440      * @return last regexp input
 441      */
 442     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2")
 443     public static Object getGroup2(Object self) {
 444         final RegExpResult match = Global.instance().getLastRegExpResult();
 445         return match == null ? "" : match.getGroup(2);
 446     }
 447 
 448     /**
 449      * Getter for non-standard RegExp.$3 property.
 450      * @param self self object
 451      * @return last regexp input
 452      */
 453     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3")
 454     public static Object getGroup3(Object self) {
 455         final RegExpResult match = Global.instance().getLastRegExpResult();
 456         return match == null ? "" : match.getGroup(3);
 457     }
 458 
 459     /**
 460      * Getter for non-standard RegExp.$4 property.
 461      * @param self self object
 462      * @return last regexp input
 463      */
 464     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4")
 465     public static Object getGroup4(Object self) {
 466         final RegExpResult match = Global.instance().getLastRegExpResult();
 467         return match == null ? "" : match.getGroup(4);
 468     }
 469 
 470     /**
 471      * Getter for non-standard RegExp.$5 property.
 472      * @param self self object
 473      * @return last regexp input
 474      */
 475     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5")
 476     public static Object getGroup5(Object self) {
 477         final RegExpResult match = Global.instance().getLastRegExpResult();
 478         return match == null ? "" : match.getGroup(5);
 479     }
 480 
 481     /**
 482      * Getter for non-standard RegExp.$6 property.
 483      * @param self self object
 484      * @return last regexp input
 485      */
 486     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6")
 487     public static Object getGroup6(Object self) {
 488         final RegExpResult match = Global.instance().getLastRegExpResult();
 489         return match == null ? "" : match.getGroup(6);
 490     }
 491 
 492     /**
 493      * Getter for non-standard RegExp.$7 property.
 494      * @param self self object
 495      * @return last regexp input
 496      */
 497     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7")
 498     public static Object getGroup7(Object self) {
 499         final RegExpResult match = Global.instance().getLastRegExpResult();
 500         return match == null ? "" : match.getGroup(7);
 501     }
 502 
 503     /**
 504      * Getter for non-standard RegExp.$8 property.
 505      * @param self self object
 506      * @return last regexp input
 507      */
 508     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8")
 509     public static Object getGroup8(Object self) {
 510         final RegExpResult match = Global.instance().getLastRegExpResult();
 511         return match == null ? "" : match.getGroup(8);
 512     }
 513 
 514     /**
 515      * Getter for non-standard RegExp.$9 property.
 516      * @param self self object
 517      * @return last regexp input
 518      */
 519     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9")
 520     public static Object getGroup9(Object self) {
 521         final RegExpResult match = Global.instance().getLastRegExpResult();
 522         return match == null ? "" : match.getGroup(9);
 523     }
 524 
 525     private RegExpResult execInner(final String string) {

 526         int start = getLastIndex();
 527         if (! regexp.isGlobal()) {
 528             start = 0;
 529         }
 530 
 531         if (start < 0 || start > string.length()) {

 532             setLastIndex(0);

 533             return null;
 534         }
 535 
 536         final RegExpMatcher matcher = regexp.match(string);
 537         if (matcher == null || !matcher.search(start)) {

 538             setLastIndex(0);

 539             return null;
 540         }
 541 
 542         if (regexp.isGlobal()) {
 543             setLastIndex(matcher.end());
 544         }
 545 
 546         final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher));
 547         globalObject.setLastRegExpResult(match);
 548         return match;
 549     }
 550 
















 551     /**
 552      * Convert java.util.regex.Matcher groups to JavaScript groups.
 553      * That is, replace null and groups that didn't match with undefined.
 554      */
 555     private Object[] groups(final RegExpMatcher matcher) {
 556         final int groupCount = matcher.groupCount();
 557         final Object[] groups = new Object[groupCount + 1];
 558         final BitVector groupsInNegativeLookahead  = regexp.getGroupsInNegativeLookahead();
 559 
 560         for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) {
 561             final int groupStart = matcher.start(i);
 562             if (lastGroupStart > groupStart
 563                     || (groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i))) {
 564                 // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated.
 565                 // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere
 566                 // in the pattern always return undefined because the negative lookahead must fail.
 567                 groups[i] = UNDEFINED;
 568                 continue;
 569             }
 570             final String group = matcher.group(i);
 571             groups[i] = group == null ? UNDEFINED : group;
 572             lastGroupStart = groupStart;
 573         }
 574         return groups;
 575     }
 576 
 577     /**
 578      * Executes a search for a match within a string based on a regular
 579      * expression. It returns an array of information or null if no match is
 580      * found.
 581      *
 582      * @param string String to match.
 583      * @return NativeArray of matches, string or null.
 584      */
 585     public Object exec(final String string) {
 586         final RegExpResult match = execInner(string);
 587 
 588         if (match == null) {
 589             return null;
 590         }
 591 
 592         return new NativeRegExpExecResult(match);
 593     }
 594 
 595     /**
 596      * Executes a search for a match within a string based on a regular
 597      * expression.
 598      *
 599      * @param string String to match.
 600      * @return True if a match is found.
 601      */
 602     public Object test(final String string) {
 603         return exec(string) != null;
 604     }
 605 
 606     /**
 607      * Searches and replaces the regular expression portion (match) with the
 608      * replaced text instead. For the "replacement text" parameter, you can use
 609      * the keywords $1 to $2 to replace the original text with values from
 610      * sub-patterns defined within the main pattern.
 611      *
 612      * @param string String to match.
 613      * @param replacement Replacement string.
 614      * @return String with substitutions.
 615      */
 616     Object replace(final String string, final String replacement, final ScriptFunction function) {
 617         final RegExpMatcher matcher = regexp.match(string);
 618 
 619         if (matcher == null) {
 620             return string;
 621         }
 622 
 623         /*
 624          * $$ -> $
 625          * $& -> the matched substring
 626          * $` -> the portion of string that preceeds matched substring
 627          * $' -> the portion of string that follows the matched substring
 628          * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit
 629          * $nn -> the nnth capture, where nn is a two digit decimal number [01-99].
 630          */
 631         String replace = replacement;
 632 
 633         if (!regexp.isGlobal()) {
 634             if (!matcher.search(0)) {
 635                 return string;
 636             }
 637 
 638             final StringBuilder sb = new StringBuilder();
 639             if (function != null) {
 640                 replace = callReplaceValue(function, matcher, string);
 641             }
 642             appendReplacement(matcher, string, replace, sb, 0);
 643             sb.append(string, matcher.end(), string.length());
 644             return sb.toString();
 645         }
 646 
 647         setLastIndex(0);
 648 
 649         if (!matcher.search(0)) {
 650             return string;
 651         }
 652 
 653         int thisIndex = 0;
 654         int previousLastIndex = 0;
 655         final StringBuilder sb = new StringBuilder();
 656 
 657         do {
 658             if (function != null) {
 659                 replace = callReplaceValue(function, matcher, string);
 660             }
 661 
 662             appendReplacement(matcher, string, replace, sb, thisIndex);
 663 
 664             // ECMA 15.5.4.10 String.prototype.match(regexp)
 665             thisIndex = matcher.end();
 666             if (thisIndex == previousLastIndex) {
 667                 setLastIndex(thisIndex + 1);
 668                 previousLastIndex = thisIndex + 1;
 669             } else {
 670                 previousLastIndex = thisIndex;
 671             }
 672         } while (previousLastIndex <= string.length() && matcher.search(previousLastIndex));
 673 
 674         sb.append(string, thisIndex, string.length());
 675 
 676         return sb.toString();
 677     }
 678 
 679     private void appendReplacement(final RegExpMatcher matcher, final String text, final String replacement, final StringBuilder sb, final int lastAppendPosition) {
 680         // Process substitution string to replace group references with groups
 681         int cursor = 0;
 682         final StringBuilder result = new StringBuilder();
 683         Object[] groups = null;
 684 
 685         while (cursor < replacement.length()) {
 686             char nextChar = replacement.charAt(cursor);
 687             if (nextChar == '$') {
 688                 // Skip past $
 689                 cursor++;
 690                 nextChar = replacement.charAt(cursor);
 691                 final int firstDigit = nextChar - '0';
 692 
 693                 if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) {
 694                     // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit.
 695                     int refNum = firstDigit;
 696                     cursor++;
 697                     if (cursor < replacement.length() && firstDigit < matcher.groupCount()) {
 698                         final int secondDigit = replacement.charAt(cursor) - '0';
 699                         if ((secondDigit >= 0) && (secondDigit <= 9)) {
 700                             final int newRefNum = (firstDigit * 10) + secondDigit;
 701                             if (newRefNum <= matcher.groupCount() && newRefNum > 0) {
 702                                 // $nn ($01-$99)
 703                                 refNum = newRefNum;
 704                                 cursor++;
 705                             }
 706                         }
 707                     }
 708                     if (refNum > 0) {
 709                         if (groups == null) {
 710                             groups = groups(matcher);
 711                         }
 712                         // Append group if matched.
 713                         if (groups[refNum] != UNDEFINED) {
 714                             result.append((String) groups[refNum]);
 715                         }
 716                     } else { // $0. ignore.
 717                         assert refNum == 0;
 718                         result.append("$0");
 719                     }
 720                 } else if (nextChar == '$') {
 721                     result.append('$');
 722                     cursor++;
 723                 } else if (nextChar == '&') {
 724                     result.append(matcher.group());
 725                     cursor++;
 726                 } else if (nextChar == '`') {
 727                     result.append(text.substring(0, matcher.start()));
 728                     cursor++;
 729                 } else if (nextChar == '\'') {
 730                     result.append(text.substring(matcher.end()));
 731                     cursor++;
 732                 } else {
 733                     // unknown substitution or $n with n>m. skip.
 734                     result.append('$');
 735                 }
 736             } else {
 737                 result.append(nextChar);
 738                 cursor++;
 739             }
 740         }
 741         // Append the intervening text
 742         sb.append(text, lastAppendPosition, matcher.start());
 743         // Append the match substitution
 744         sb.append(result);
 745     }
 746 
 747     private String callReplaceValue(final ScriptFunction function, final RegExpMatcher matcher, final String string) {
 748         final Object[] groups = groups(matcher);
 749         final Object[] args   = Arrays.copyOf(groups, groups.length + 2);
 750 
 751         args[groups.length]     = matcher.start();
 752         args[groups.length + 1] = string;
 753 
 754         final Object self = function.isStrict() ? UNDEFINED : Global.instance();
 755 
 756         return JSType.toString(ScriptRuntime.apply(function, self, args));
 757     }
 758 
 759     /**
 760      * Breaks up a string into an array of substrings based on a regular
 761      * expression or fixed string.
 762      *
 763      * @param string String to match.
 764      * @param limit  Split limit.
 765      * @return Array of substrings.
 766      */
 767     Object split(final String string, final long limit) {
 768         return split(this, string, limit);
 769     }
 770 
 771     private static Object split(final NativeRegExp regexp0, final String input, final long limit) {
 772         final List<Object> matches = new ArrayList<>();
 773 
 774         final NativeRegExp regexp = new NativeRegExp(regexp0);
 775         regexp.setGlobal(true);
 776 
 777         if (limit == 0L) {
 778             return new NativeArray();
 779         }
 780 


 781         RegExpResult match;
 782         final int inputLength = input.length();
 783         int lastLength = -1;

 784         int lastLastIndex = 0;
 785 
 786         while ((match = regexp.execInner(input)) != null) {
 787             final int lastIndex = match.getIndex() + match.length();
 788 
 789             if (lastIndex > lastLastIndex) {
 790                 matches.add(input.substring(lastLastIndex, match.getIndex()));
 791                 if (match.getGroups().length > 1 && match.getIndex() < inputLength) {
 792                     matches.addAll(Arrays.asList(match.getGroups()).subList(1, match.getGroups().length));



 793                 }
 794 
 795                 lastLength = match.length();
 796                 lastLastIndex = lastIndex;
 797 
 798                 if (matches.size() >= limit) {
 799                     break;
 800                 }
 801             }
 802 
 803             // bump the index to avoid infinite loop
 804             if (regexp.getLastIndex() == match.getIndex()) {
 805                 regexp.setLastIndex(match.getIndex() + 1);


 806             }
 807         }
 808 
 809         if (matches.size() < limit) {
 810             // check special case if we need to append an empty string at the
 811             // end of the match
 812             // if the lastIndex was the entire string
 813             if (lastLastIndex == input.length()) {
 814                 if (lastLength > 0 || regexp.test("") == Boolean.FALSE) {
 815                     matches.add("");
 816                 }
 817             } else {
 818                 matches.add(input.substring(lastLastIndex, inputLength));
 819             }
 820         }
 821 
 822         return new NativeArray(matches.toArray());
 823     }
 824 
 825     /**
 826      * Tests for a match in a string. It returns the index of the match, or -1
 827      * if not found.
 828      *
 829      * @param string String to match.
 830      * @return Index of match.
 831      */
 832     Object search(final String string) {
 833         final RegExpResult match = execInner(string);
 834 
 835         if (match == null) {
 836             return -1;
 837         }
 838 
 839         return match.getIndex();
 840     }
 841 
 842     /**
 843      * Fast lastIndex getter
 844      * @return last index property as int
 845      */
 846     public int getLastIndex() {
 847         return JSType.toInt32(lastIndex);
 848     }
 849 
 850     /**
 851      * Fast lastIndex getter
 852      * @return last index property as boxed integer
 853      */
 854     public Object getLastIndexObject() {
 855         return lastIndex;
 856     }
 857 
 858     /**
 859      * Fast lastIndex setter
 860      * @param lastIndex lastIndex
 861      */
 862     public void setLastIndex(final int lastIndex) {
 863         this.lastIndex = JSType.toObject(lastIndex);
 864     }
 865 
 866     private void init() {
 867         // Keep reference to global object to support "static" properties of RegExp
 868         this.globalObject = Global.instance();
 869         this.setProto(globalObject.getRegExpPrototype());
 870     }
 871 
 872     private static NativeRegExp checkRegExp(final Object self) {
 873         Global.checkObjectCoercible(self);
 874         if (self instanceof NativeRegExp) {
 875             return (NativeRegExp)self;
 876         } else if (self != null && self == Global.instance().getRegExpPrototype()) {
 877             return Global.instance().DEFAULT_REGEXP;
 878         } else {
 879             throw typeError("not.a.regexp", ScriptRuntime.safeToString(self));
 880         }
 881     }
 882 
 883     private void setGlobal(final boolean global) {
 884         regexp.setGlobal(global);
 885     }
 886 
 887     boolean getGlobal() {
 888         return regexp.isGlobal();
 889     }
 890 
 891     private RegExp getRegExp() {
 892         return regexp;
 893     }
 894 
 895     private void setRegExp(final RegExp regexp) {
 896         this.regexp = regexp;
 897     }
 898 
 899 }
--- EOF ---