| /* |
| * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.nashorn.internal.objects; |
| |
| import static jdk.nashorn.internal.runtime.ECMAErrors.typeError; |
| import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; |
| |
| import java.lang.invoke.MethodHandle; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| |
| import jdk.nashorn.internal.objects.annotations.Attribute; |
| import jdk.nashorn.internal.objects.annotations.Constructor; |
| import jdk.nashorn.internal.objects.annotations.Function; |
| import jdk.nashorn.internal.objects.annotations.Getter; |
| import jdk.nashorn.internal.objects.annotations.Property; |
| import jdk.nashorn.internal.objects.annotations.ScriptClass; |
| import jdk.nashorn.internal.objects.annotations.SpecializedFunction; |
| import jdk.nashorn.internal.objects.annotations.Where; |
| import jdk.nashorn.internal.runtime.BitVector; |
| import jdk.nashorn.internal.runtime.JSType; |
| import jdk.nashorn.internal.runtime.ParserException; |
| import jdk.nashorn.internal.runtime.PropertyMap; |
| import jdk.nashorn.internal.runtime.ScriptObject; |
| import jdk.nashorn.internal.runtime.ScriptRuntime; |
| import jdk.nashorn.internal.runtime.linker.Bootstrap; |
| import jdk.nashorn.internal.runtime.regexp.RegExp; |
| import jdk.nashorn.internal.runtime.regexp.RegExpFactory; |
| import jdk.nashorn.internal.runtime.regexp.RegExpMatcher; |
| import jdk.nashorn.internal.runtime.regexp.RegExpResult; |
| |
| /** |
| * ECMA 15.10 RegExp Objects. |
| */ |
| @ScriptClass("RegExp") |
| public final class NativeRegExp extends ScriptObject { |
| /** ECMA 15.10.7.5 lastIndex property */ |
| @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE) |
| public Object lastIndex; |
| |
| /** Compiled regexp */ |
| private RegExp regexp; |
| |
| // Reference to global object needed to support static RegExp properties |
| private final Global globalObject; |
| |
| // initialized by nasgen |
| private static PropertyMap $nasgenmap$; |
| |
| private NativeRegExp(final Global global) { |
| super(global.getRegExpPrototype(), $nasgenmap$); |
| this.globalObject = global; |
| } |
| |
| NativeRegExp(final String input, final String flagString, final Global global, final ScriptObject proto) { |
| super(proto, $nasgenmap$); |
| try { |
| this.regexp = RegExpFactory.create(input, flagString); |
| } catch (final ParserException e) { |
| // translate it as SyntaxError object and throw it |
| e.throwAsEcmaException(); |
| throw new AssertionError(); //guard against null warnings below |
| } |
| this.globalObject = global; |
| this.setLastIndex(0); |
| } |
| |
| NativeRegExp(final String input, final String flagString, final Global global) { |
| this(input, flagString, global, global.getRegExpPrototype()); |
| } |
| |
| NativeRegExp(final String input, final String flagString) { |
| this(input, flagString, Global.instance()); |
| } |
| |
| NativeRegExp(final String string, final Global global) { |
| this(string, "", global); |
| } |
| |
| NativeRegExp(final String string) { |
| this(string, Global.instance()); |
| } |
| |
| NativeRegExp(final NativeRegExp regExp) { |
| this(Global.instance()); |
| this.lastIndex = regExp.getLastIndexObject(); |
| this.regexp = regExp.getRegExp(); |
| } |
| |
| @Override |
| public String getClassName() { |
| return "RegExp"; |
| } |
| |
| /** |
| * ECMA 15.10.4 |
| * |
| * Constructor |
| * |
| * @param isNew is the new operator used for instantiating this regexp |
| * @param self self reference |
| * @param args arguments (optional: pattern and flags) |
| * @return new NativeRegExp |
| */ |
| @Constructor(arity = 2) |
| public static NativeRegExp constructor(final boolean isNew, final Object self, final Object... args) { |
| if (args.length > 1) { |
| return newRegExp(args[0], args[1]); |
| } else if (args.length > 0) { |
| return newRegExp(args[0], UNDEFINED); |
| } |
| |
| return newRegExp(UNDEFINED, UNDEFINED); |
| } |
| |
| /** |
| * ECMA 15.10.4 |
| * |
| * Constructor - specialized version, no args, empty regexp |
| * |
| * @param isNew is the new operator used for instantiating this regexp |
| * @param self self reference |
| * @return new NativeRegExp |
| */ |
| @SpecializedFunction(isConstructor=true) |
| public static NativeRegExp constructor(final boolean isNew, final Object self) { |
| return new NativeRegExp("", ""); |
| } |
| |
| /** |
| * ECMA 15.10.4 |
| * |
| * Constructor - specialized version, pattern, no flags |
| * |
| * @param isNew is the new operator used for instantiating this regexp |
| * @param self self reference |
| * @param pattern pattern |
| * @return new NativeRegExp |
| */ |
| @SpecializedFunction(isConstructor=true) |
| public static NativeRegExp constructor(final boolean isNew, final Object self, final Object pattern) { |
| return newRegExp(pattern, UNDEFINED); |
| } |
| |
| /** |
| * ECMA 15.10.4 |
| * |
| * Constructor - specialized version, pattern and flags |
| * |
| * @param isNew is the new operator used for instantiating this regexp |
| * @param self self reference |
| * @param pattern pattern |
| * @param flags flags |
| * @return new NativeRegExp |
| */ |
| @SpecializedFunction(isConstructor=true) |
| public static NativeRegExp constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) { |
| return newRegExp(pattern, flags); |
| } |
| |
| /** |
| * External constructor used in generated code, which explains the public access |
| * |
| * @param regexp regexp |
| * @param flags flags |
| * @return new NativeRegExp |
| */ |
| public static NativeRegExp newRegExp(final Object regexp, final Object flags) { |
| String patternString = ""; |
| String flagString = ""; |
| |
| if (regexp != UNDEFINED) { |
| if (regexp instanceof NativeRegExp) { |
| if (flags != UNDEFINED) { |
| throw typeError("regex.cant.supply.flags"); |
| } |
| return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as |
| } |
| patternString = JSType.toString(regexp); |
| } |
| |
| if (flags != UNDEFINED) { |
| flagString = JSType.toString(flags); |
| } |
| |
| return new NativeRegExp(patternString, flagString); |
| } |
| |
| /** |
| * Build a regexp that matches {@code string} as-is. All meta-characters will be escaped. |
| * |
| * @param string pattern string |
| * @return flat regexp |
| */ |
| static NativeRegExp flatRegExp(final String string) { |
| // escape special characters |
| StringBuilder sb = null; |
| final int length = string.length(); |
| |
| for (int i = 0; i < length; i++) { |
| final char c = string.charAt(i); |
| switch (c) { |
| case '^': |
| case '$': |
| case '\\': |
| case '.': |
| case '*': |
| case '+': |
| case '?': |
| case '(': |
| case ')': |
| case '[': |
| case '{': |
| case '|': |
| if (sb == null) { |
| sb = new StringBuilder(length * 2); |
| sb.append(string, 0, i); |
| } |
| sb.append('\\'); |
| sb.append(c); |
| break; |
| default: |
| if (sb != null) { |
| sb.append(c); |
| } |
| break; |
| } |
| } |
| return new NativeRegExp(sb == null ? string : sb.toString(), ""); |
| } |
| |
| private String getFlagString() { |
| final StringBuilder sb = new StringBuilder(3); |
| |
| if (regexp.isGlobal()) { |
| sb.append('g'); |
| } |
| if (regexp.isIgnoreCase()) { |
| sb.append('i'); |
| } |
| if (regexp.isMultiline()) { |
| sb.append('m'); |
| } |
| |
| return sb.toString(); |
| } |
| |
| @Override |
| public String safeToString() { |
| return "[RegExp " + toString() + "]"; |
| } |
| |
| @Override |
| public String toString() { |
| return "/" + regexp.getSource() + "/" + getFlagString(); |
| } |
| |
| /** |
| * Nashorn extension: RegExp.prototype.compile - everybody implements this! |
| * |
| * @param self self reference |
| * @param pattern pattern |
| * @param flags flags |
| * @return new NativeRegExp |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE) |
| public static ScriptObject compile(final Object self, final Object pattern, final Object flags) { |
| final NativeRegExp regExp = checkRegExp(self); |
| final NativeRegExp compiled = newRegExp(pattern, flags); |
| // copy over regexp to 'self' |
| regExp.setRegExp(compiled.getRegExp()); |
| |
| // Some implementations return undefined. Some return 'self'. Since return |
| // value is most likely be ignored, we can play safe and return 'self'. |
| return regExp; |
| } |
| |
| /** |
| * ECMA 15.10.6.2 RegExp.prototype.exec(string) |
| * |
| * @param self self reference |
| * @param string string to match against regexp |
| * @return array containing the matches or {@code null} if no match |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE) |
| public static ScriptObject exec(final Object self, final Object string) { |
| return checkRegExp(self).exec(JSType.toString(string)); |
| } |
| |
| /** |
| * ECMA 15.10.6.3 RegExp.prototype.test(string) |
| * |
| * @param self self reference |
| * @param string string to test for matches against regexp |
| * @return true if matches found, false otherwise |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE) |
| public static boolean test(final Object self, final Object string) { |
| return checkRegExp(self).test(JSType.toString(string)); |
| } |
| |
| /** |
| * ECMA 15.10.6.4 RegExp.prototype.toString() |
| * |
| * @param self self reference |
| * @return string version of regexp |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE) |
| public static String toString(final Object self) { |
| return checkRegExp(self).toString(); |
| } |
| |
| /** |
| * ECMA 15.10.7.1 source |
| * |
| * @param self self reference |
| * @return the input string for the regexp |
| */ |
| @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) |
| public static Object source(final Object self) { |
| return checkRegExp(self).getRegExp().getSource(); |
| } |
| |
| /** |
| * ECMA 15.10.7.2 global |
| * |
| * @param self self reference |
| * @return true if this regexp is flagged global, false otherwise |
| */ |
| @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) |
| public static Object global(final Object self) { |
| return checkRegExp(self).getRegExp().isGlobal(); |
| } |
| |
| /** |
| * ECMA 15.10.7.3 ignoreCase |
| * |
| * @param self self reference |
| * @return true if this regexp if flagged to ignore case, false otherwise |
| */ |
| @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) |
| public static Object ignoreCase(final Object self) { |
| return checkRegExp(self).getRegExp().isIgnoreCase(); |
| } |
| |
| /** |
| * ECMA 15.10.7.4 multiline |
| * |
| * @param self self reference |
| * @return true if this regexp is flagged to be multiline, false otherwise |
| */ |
| @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT) |
| public static Object multiline(final Object self) { |
| return checkRegExp(self).getRegExp().isMultiline(); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.input property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input") |
| public static Object getLastInput(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getInput(); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.multiline property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline") |
| public static Object getLastMultiline(final Object self) { |
| return false; // doesn't ever seem to become true and isn't documented anyhwere |
| } |
| |
| /** |
| * Getter for non-standard RegExp.lastMatch property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch") |
| public static Object getLastMatch(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(0); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.lastParen property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen") |
| public static Object getLastParen(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getLastParen(); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.leftContext property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext") |
| public static Object getLeftContext(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getInput().substring(0, match.getIndex()); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.rightContext property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext") |
| public static Object getRightContext(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getInput().substring(match.getIndex() + match.length()); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$1 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1") |
| public static Object getGroup1(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(1); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$2 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2") |
| public static Object getGroup2(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(2); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$3 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3") |
| public static Object getGroup3(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(3); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$4 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4") |
| public static Object getGroup4(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(4); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$5 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5") |
| public static Object getGroup5(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(5); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$6 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6") |
| public static Object getGroup6(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(6); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$7 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7") |
| public static Object getGroup7(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(7); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$8 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8") |
| public static Object getGroup8(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(8); |
| } |
| |
| /** |
| * Getter for non-standard RegExp.$9 property. |
| * @param self self object |
| * @return last regexp input |
| */ |
| @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9") |
| public static Object getGroup9(final Object self) { |
| final RegExpResult match = Global.instance().getLastRegExpResult(); |
| return match == null ? "" : match.getGroup(9); |
| } |
| |
| private RegExpResult execInner(final String string) { |
| final boolean isGlobal = regexp.isGlobal(); |
| int start = getLastIndex(); |
| if (!isGlobal) { |
| start = 0; |
| } |
| |
| if (start < 0 || start > string.length()) { |
| if (isGlobal) { |
| setLastIndex(0); |
| } |
| return null; |
| } |
| |
| final RegExpMatcher matcher = regexp.match(string); |
| if (matcher == null || !matcher.search(start)) { |
| if (isGlobal) { |
| setLastIndex(0); |
| } |
| return null; |
| } |
| |
| if (isGlobal) { |
| setLastIndex(matcher.end()); |
| } |
| |
| final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher)); |
| globalObject.setLastRegExpResult(match); |
| return match; |
| } |
| |
| // String.prototype.split method ignores the global flag and should not update lastIndex property. |
| private RegExpResult execSplit(final String string, final int start) { |
| if (start < 0 || start > string.length()) { |
| return null; |
| } |
| |
| final RegExpMatcher matcher = regexp.match(string); |
| if (matcher == null || !matcher.search(start)) { |
| return null; |
| } |
| |
| final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher)); |
| globalObject.setLastRegExpResult(match); |
| return match; |
| } |
| |
| /** |
| * Convert java.util.regex.Matcher groups to JavaScript groups. |
| * That is, replace null and groups that didn't match with undefined. |
| */ |
| private Object[] groups(final RegExpMatcher matcher) { |
| final int groupCount = matcher.groupCount(); |
| final Object[] groups = new Object[groupCount + 1]; |
| final BitVector groupsInNegativeLookahead = regexp.getGroupsInNegativeLookahead(); |
| |
| for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) { |
| final int groupStart = matcher.start(i); |
| if (lastGroupStart > groupStart |
| || groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i)) { |
| // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated. |
| // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere |
| // in the pattern always return undefined because the negative lookahead must fail. |
| groups[i] = UNDEFINED; |
| continue; |
| } |
| final String group = matcher.group(i); |
| groups[i] = group == null ? UNDEFINED : group; |
| lastGroupStart = groupStart; |
| } |
| return groups; |
| } |
| |
| /** |
| * Executes a search for a match within a string based on a regular |
| * expression. It returns an array of information or null if no match is |
| * found. |
| * |
| * @param string String to match. |
| * @return NativeArray of matches, string or null. |
| */ |
| public NativeRegExpExecResult exec(final String string) { |
| final RegExpResult match = execInner(string); |
| |
| if (match == null) { |
| return null; |
| } |
| |
| return new NativeRegExpExecResult(match, globalObject); |
| } |
| |
| /** |
| * Executes a search for a match within a string based on a regular |
| * expression. |
| * |
| * @param string String to match. |
| * @return True if a match is found. |
| */ |
| public boolean test(final String string) { |
| return execInner(string) != null; |
| } |
| |
| /** |
| * Searches and replaces the regular expression portion (match) with the |
| * replaced text instead. For the "replacement text" parameter, you can use |
| * the keywords $1 to $2 to replace the original text with values from |
| * sub-patterns defined within the main pattern. |
| * |
| * @param string String to match. |
| * @param replacement Replacement string. |
| * @return String with substitutions. |
| */ |
| String replace(final String string, final String replacement, final Object function) throws Throwable { |
| final RegExpMatcher matcher = regexp.match(string); |
| |
| if (matcher == null) { |
| return string; |
| } |
| |
| if (!regexp.isGlobal()) { |
| if (!matcher.search(0)) { |
| return string; |
| } |
| |
| final StringBuilder sb = new StringBuilder(); |
| sb.append(string, 0, matcher.start()); |
| |
| if (function != null) { |
| final Object self = Bootstrap.isStrictCallable(function) ? UNDEFINED : Global.instance(); |
| sb.append(callReplaceValue(getReplaceValueInvoker(), function, self, matcher, string)); |
| } else { |
| appendReplacement(matcher, string, replacement, sb); |
| } |
| sb.append(string, matcher.end(), string.length()); |
| return sb.toString(); |
| } |
| |
| setLastIndex(0); |
| |
| if (!matcher.search(0)) { |
| return string; |
| } |
| |
| int thisIndex = 0; |
| int previousLastIndex = 0; |
| final StringBuilder sb = new StringBuilder(); |
| |
| final MethodHandle invoker = function == null ? null : getReplaceValueInvoker(); |
| final Object self = function == null || Bootstrap.isStrictCallable(function) ? UNDEFINED : Global.instance(); |
| |
| do { |
| sb.append(string, thisIndex, matcher.start()); |
| if (function != null) { |
| sb.append(callReplaceValue(invoker, function, self, matcher, string)); |
| } else { |
| appendReplacement(matcher, string, replacement, sb); |
| } |
| |
| thisIndex = matcher.end(); |
| if (thisIndex == string.length() && matcher.start() == matcher.end()) { |
| // Avoid getting empty match at end of string twice |
| break; |
| } |
| |
| // ECMA 15.5.4.10 String.prototype.match(regexp) |
| if (thisIndex == previousLastIndex) { |
| setLastIndex(thisIndex + 1); |
| previousLastIndex = thisIndex + 1; |
| } else { |
| previousLastIndex = thisIndex; |
| } |
| } while (previousLastIndex <= string.length() && matcher.search(previousLastIndex)); |
| |
| sb.append(string, thisIndex, string.length()); |
| |
| return sb.toString(); |
| } |
| |
| private void appendReplacement(final RegExpMatcher matcher, final String text, final String replacement, final StringBuilder sb) { |
| /* |
| * Process substitution patterns: |
| * |
| * $$ -> $ |
| * $& -> the matched substring |
| * $` -> the portion of string that precedes matched substring |
| * $' -> the portion of string that follows the matched substring |
| * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit |
| * $nn -> the nnth capture, where nn is a two digit decimal number [01-99]. |
| */ |
| |
| int cursor = 0; |
| Object[] groups = null; |
| |
| while (cursor < replacement.length()) { |
| char nextChar = replacement.charAt(cursor); |
| if (nextChar == '$') { |
| // Skip past $ |
| cursor++; |
| if (cursor == replacement.length()) { |
| // nothing after "$" |
| sb.append('$'); |
| break; |
| } |
| |
| nextChar = replacement.charAt(cursor); |
| final int firstDigit = nextChar - '0'; |
| |
| if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) { |
| // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit. |
| int refNum = firstDigit; |
| cursor++; |
| if (cursor < replacement.length() && firstDigit < matcher.groupCount()) { |
| final int secondDigit = replacement.charAt(cursor) - '0'; |
| if (secondDigit >= 0 && secondDigit <= 9) { |
| final int newRefNum = firstDigit * 10 + secondDigit; |
| if (newRefNum <= matcher.groupCount() && newRefNum > 0) { |
| // $nn ($01-$99) |
| refNum = newRefNum; |
| cursor++; |
| } |
| } |
| } |
| if (refNum > 0) { |
| if (groups == null) { |
| groups = groups(matcher); |
| } |
| // Append group if matched. |
| if (groups[refNum] != UNDEFINED) { |
| sb.append((String) groups[refNum]); |
| } |
| } else { // $0. ignore. |
| assert refNum == 0; |
| sb.append("$0"); |
| } |
| } else if (nextChar == '$') { |
| sb.append('$'); |
| cursor++; |
| } else if (nextChar == '&') { |
| sb.append(matcher.group()); |
| cursor++; |
| } else if (nextChar == '`') { |
| sb.append(text, 0, matcher.start()); |
| cursor++; |
| } else if (nextChar == '\'') { |
| sb.append(text, matcher.end(), text.length()); |
| cursor++; |
| } else { |
| // unknown substitution or $n with n>m. skip. |
| sb.append('$'); |
| } |
| } else { |
| sb.append(nextChar); |
| cursor++; |
| } |
| } |
| } |
| |
| private static final Object REPLACE_VALUE = new Object(); |
| |
| private static final MethodHandle getReplaceValueInvoker() { |
| return Global.instance().getDynamicInvoker(REPLACE_VALUE, |
| new Callable<MethodHandle>() { |
| @Override |
| public MethodHandle call() { |
| return Bootstrap.createDynamicInvoker("dyn:call", |
| String.class, Object.class, Object.class, Object[].class); |
| } |
| }); |
| } |
| |
| private String callReplaceValue(final MethodHandle invoker, final Object function, final Object self, final RegExpMatcher matcher, final String string) throws Throwable { |
| final Object[] groups = groups(matcher); |
| final Object[] args = Arrays.copyOf(groups, groups.length + 2); |
| |
| args[groups.length] = matcher.start(); |
| args[groups.length + 1] = string; |
| |
| return (String)invoker.invokeExact(function, self, args); |
| } |
| |
| /** |
| * Breaks up a string into an array of substrings based on a regular |
| * expression or fixed string. |
| * |
| * @param string String to match. |
| * @param limit Split limit. |
| * @return Array of substrings. |
| */ |
| NativeArray split(final String string, final long limit) { |
| if (limit == 0L) { |
| return new NativeArray(); |
| } |
| |
| final List<Object> matches = new ArrayList<>(); |
| |
| RegExpResult match; |
| final int inputLength = string.length(); |
| int splitLastLength = -1; |
| int splitLastIndex = 0; |
| int splitLastLastIndex = 0; |
| |
| while ((match = execSplit(string, splitLastIndex)) != null) { |
| splitLastIndex = match.getIndex() + match.length(); |
| |
| if (splitLastIndex > splitLastLastIndex) { |
| matches.add(string.substring(splitLastLastIndex, match.getIndex())); |
| final Object[] groups = match.getGroups(); |
| if (groups.length > 1 && match.getIndex() < inputLength) { |
| for (int index = 1; index < groups.length && matches.size() < limit; index++) { |
| matches.add(groups[index]); |
| } |
| } |
| |
| splitLastLength = match.length(); |
| |
| if (matches.size() >= limit) { |
| break; |
| } |
| } |
| |
| // bump the index to avoid infinite loop |
| if (splitLastIndex == splitLastLastIndex) { |
| splitLastIndex++; |
| } else { |
| splitLastLastIndex = splitLastIndex; |
| } |
| } |
| |
| if (matches.size() < limit) { |
| // check special case if we need to append an empty string at the |
| // end of the match |
| // if the lastIndex was the entire string |
| if (splitLastLastIndex == string.length()) { |
| if (splitLastLength > 0 || execSplit("", 0) == null) { |
| matches.add(""); |
| } |
| } else { |
| matches.add(string.substring(splitLastLastIndex, inputLength)); |
| } |
| } |
| |
| return new NativeArray(matches.toArray()); |
| } |
| |
| /** |
| * Tests for a match in a string. It returns the index of the match, or -1 |
| * if not found. |
| * |
| * @param string String to match. |
| * @return Index of match. |
| */ |
| int search(final String string) { |
| final RegExpResult match = execInner(string); |
| |
| if (match == null) { |
| return -1; |
| } |
| |
| return match.getIndex(); |
| } |
| |
| /** |
| * Fast lastIndex getter |
| * @return last index property as int |
| */ |
| public int getLastIndex() { |
| return JSType.toInteger(lastIndex); |
| } |
| |
| /** |
| * Fast lastIndex getter |
| * @return last index property as boxed integer |
| */ |
| public Object getLastIndexObject() { |
| return lastIndex; |
| } |
| |
| /** |
| * Fast lastIndex setter |
| * @param lastIndex lastIndex |
| */ |
| public void setLastIndex(final int lastIndex) { |
| this.lastIndex = JSType.toObject(lastIndex); |
| } |
| |
| private static NativeRegExp checkRegExp(final Object self) { |
| if (self instanceof NativeRegExp) { |
| return (NativeRegExp)self; |
| } else if (self != null && self == Global.instance().getRegExpPrototype()) { |
| return Global.instance().getDefaultRegExp(); |
| } else { |
| throw typeError("not.a.regexp", ScriptRuntime.safeToString(self)); |
| } |
| } |
| |
| boolean getGlobal() { |
| return regexp.isGlobal(); |
| } |
| |
| private RegExp getRegExp() { |
| return regexp; |
| } |
| |
| private void setRegExp(final RegExp regexp) { |
| this.regexp = regexp; |
| } |
| |
| } |