using System; using System.Collections.Generic; using System.Text; using Jint.Native.Array; using Jint.Native.Function; using Jint.Native.Object; using Jint.Native.RegExp; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; namespace Jint.Native.String { /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-15.5.4 /// public sealed class StringPrototype : StringInstance { private StringPrototype(Engine engine) : base(engine) { } public static StringPrototype CreatePrototypeObject(Engine engine, StringConstructor stringConstructor) { var obj = new StringPrototype(engine); obj.Prototype = engine.Object.PrototypeObject; obj.PrimitiveValue = ""; obj.Extensible = true; obj.FastAddProperty("length", 0, false, false, false); obj.FastAddProperty("constructor", stringConstructor, true, false, true); return obj; } public void Configure() { FastAddProperty("toString", new ClrFunctionInstance(Engine, ToStringString), true, false, true); FastAddProperty("valueOf", new ClrFunctionInstance(Engine, ValueOf), true, false, true); FastAddProperty("charAt", new ClrFunctionInstance(Engine, CharAt, 1), true, false, true); FastAddProperty("charCodeAt", new ClrFunctionInstance(Engine, CharCodeAt, 1), true, false, true); FastAddProperty("concat", new ClrFunctionInstance(Engine, Concat, 1), true, false, true); FastAddProperty("indexOf", new ClrFunctionInstance(Engine, IndexOf, 1), true, false, true); FastAddProperty("lastIndexOf", new ClrFunctionInstance(Engine, LastIndexOf, 1), true, false, true); FastAddProperty("localeCompare", new ClrFunctionInstance(Engine, LocaleCompare), true, false, true); FastAddProperty("match", new ClrFunctionInstance(Engine, Match, 1), true, false, true); FastAddProperty("replace", new ClrFunctionInstance(Engine, Replace, 2), true, false, true); FastAddProperty("search", new ClrFunctionInstance(Engine, Search, 1), true, false, true); FastAddProperty("slice", new ClrFunctionInstance(Engine, Slice, 2), true, false, true); FastAddProperty("split", new ClrFunctionInstance(Engine, Split, 2), true, false, true); FastAddProperty("substring", new ClrFunctionInstance(Engine, Substring, 2), true, false, true); FastAddProperty("toLowerCase", new ClrFunctionInstance(Engine, ToLowerCase), true, false, true); FastAddProperty("toLocaleLowerCase", new ClrFunctionInstance(Engine, ToLocaleLowerCase), true, false, true); FastAddProperty("toUpperCase", new ClrFunctionInstance(Engine, ToUpperCase), true, false, true); FastAddProperty("toLocaleUpperCase", new ClrFunctionInstance(Engine, ToLocaleUpperCase), true, false, true); FastAddProperty("trim", new ClrFunctionInstance(Engine, Trim), true, false, true); } private string ToStringString(object thisObj, object[] arguments) { var s = TypeConverter.ToObject(Engine, thisObj) as StringInstance; if (s == null) { throw new JavaScriptException(Engine.TypeError); } return s.PrimitiveValue; } private string Trim(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); return s.Trim(); } private static string ToLocaleUpperCase(object thisObj, object[] arguments) { var s = TypeConverter.ToString(thisObj); return s.ToUpper(); } private static string ToUpperCase(object thisObj, object[] arguments) { var s = TypeConverter.ToString(thisObj); return s.ToUpperInvariant(); } private static string ToLocaleLowerCase(object thisObj, object[] arguments) { var s = TypeConverter.ToString(thisObj); return s.ToLower(); } private static string ToLowerCase(object thisObj, object[] arguments) { var s = TypeConverter.ToString(thisObj); return s.ToLowerInvariant(); } private string Substring(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var start = TypeConverter.ToNumber(arguments.At(0)); var end = TypeConverter.ToNumber(arguments.At(1)); if (double.IsNaN(start) || start < 0) { start = 0; } if (double.IsNaN(end) || end < 0) { end = 0; } var len = s.Length; var intStart = (int) TypeConverter.ToInteger(start); var intEnd = arguments.At(1) == Undefined.Instance ? len : (int) TypeConverter.ToInteger(end); var finalStart = System.Math.Min(len, System.Math.Max(intStart, 0)); var finalEnd = System.Math.Min(len, System.Math.Max(intEnd, 0)); var from = System.Math.Min(finalStart, finalEnd); var to = System.Math.Max(finalStart, finalEnd); return s.Substring(from, to - from); } private ArrayInstance Split(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var separator = arguments.At(0); var l = arguments.At(1); var a = (ArrayInstance) Engine.Array.Construct(Arguments.Empty); var limit = l == Undefined.Instance ? UInt32.MaxValue : TypeConverter.ToUint32(l); var len = s.Length; if (limit == 0) { return a; } if (separator == Undefined.Instance) { return (ArrayInstance) Engine.Array.Construct(Arguments.From(s)); } var rx = TypeConverter.ToObject(Engine, separator) as RegExpInstance; if (rx != null) { var match = rx.Value.Match(s, 0); int lastIndex = 0; int index = 0; while (match.Success && index < limit) { if (match.Length == 0 && (match.Index == 0 || match.Index == len || match.Index == lastIndex)) { match = match.NextMatch(); continue; } // Add the match results to the array. a.DefineOwnProperty(index++.ToString(), new PropertyDescriptor(s.Substring(lastIndex, match.Index - lastIndex), true, true, true), false); if (index >= limit) { return a; } lastIndex = match.Index + match.Length; for (int i = 1; i < match.Groups.Count; i++) { var group = match.Groups[i]; object item = Undefined.Instance; if (group.Captures.Count > 0) { item = match.Groups[i].Value; } a.DefineOwnProperty(index++.ToString(), new PropertyDescriptor(item, true, true, true ), false); if (index >= limit) { return a; } } match = match.NextMatch(); } return a; } else { var sep = TypeConverter.ToString(separator); var segments = s.Split(new [] { sep }, StringSplitOptions.None); for (int i = 0; i < segments.Length && i < limit; i++) { a.DefineOwnProperty(i.ToString(), new PropertyDescriptor(segments[i], true, true, true), false); } return a; } } private string Slice(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var start = TypeConverter.ToNumber(arguments.At(0)); var end = TypeConverter.ToNumber(arguments.At(1)); var len = s.Length; var intStart = (int)TypeConverter.ToInteger(start); var intEnd = arguments.At(1) == Undefined.Instance ? len : (int)TypeConverter.ToInteger(end); var from = intStart < 0 ? System.Math.Max(len + intStart, 0) : System.Math.Min(intStart, len); var to = intEnd < 0 ? System.Math.Max(len + intEnd, 0) : System.Math.Min(intEnd, len); var span = System.Math.Max(to - from, 0); return s.Substring(from, span); } private double Search(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var regex = arguments.At(0); var rx = TypeConverter.ToObject(Engine, regex) as RegExpInstance ?? (RegExpInstance)Engine.RegExp.Construct(new[] { regex }); var match = rx.Value.Match(s); if (!match.Success) { return -1; } return match.Index; } private object Replace(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var thisString = TypeConverter.ToString(thisObj); var searchValue = arguments.At(0); var replaceValue = arguments.At(1); var replaceFunction = replaceValue as FunctionInstance; if (replaceFunction == null) { replaceFunction = new ClrFunctionInstance(Engine, (self, args) => { var replaceString = TypeConverter.ToString(replaceValue); var matchValue = TypeConverter.ToString(args.At(0)); var matchIndex = (int)TypeConverter.ToInteger(args.At(args.Length-2)); // Check if the replacement string contains any patterns. bool replaceTextContainsPattern = replaceString.IndexOf('$') >= 0; // If there is no pattern, replace the pattern as is. if (replaceTextContainsPattern == false) return replaceString; // Patterns // $$ Inserts a "$". // $& Inserts the matched substring. // $` Inserts the portion of the string that precedes the matched substring. // $' Inserts the portion of the string that follows the matched substring. // $n or $nn Where n or nn are decimal digits, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. var replacementBuilder = new StringBuilder(); for (int i = 0; i < replaceString.Length; i++) { char c = replaceString[i]; if (c == '$' && i < replaceString.Length - 1) { c = replaceString[++i]; if (c == '$') replacementBuilder.Append('$'); else if (c == '&') replacementBuilder.Append(matchValue); else if (c == '`') replacementBuilder.Append(thisString.Substring(0, matchIndex)); else if (c == '\'') replacementBuilder.Append(thisString.Substring(matchIndex + matchValue.Length)); else if (c >= '0' && c <= '9') { int matchNumber1 = c - '0'; // The match number can be one or two digits long. int matchNumber2 = 0; if (i < replaceString.Length - 1 && replaceString[i + 1] >= '0' && replaceString[i + 1] <= '9') matchNumber2 = matchNumber1 * 10 + (replaceString[i + 1] - '0'); // Try the two digit capture first. if (matchNumber2 > 0 && matchNumber2 < args.Length - 3) { // Two digit capture replacement. replacementBuilder.Append(TypeConverter.ToString(args[matchNumber2 + 1])); i++; } else if (matchNumber1 > 0 && matchNumber1 < args.Length - 3) { // Single digit capture replacement. replacementBuilder.Append(TypeConverter.ToString(args[matchNumber1 + 1])); } else { // Capture does not exist. replacementBuilder.Append('$'); i--; } } else { // Unknown replacement pattern. replacementBuilder.Append('$'); replacementBuilder.Append(c); } } else replacementBuilder.Append(c); } return replacementBuilder.ToString(); }); } // searchValue is a regular expression var rx = TypeConverter.ToObject(Engine, searchValue) as RegExpInstance; if (rx != null) { // Replace the input string with replaceText, recording the last match found. string result = rx.Value.Replace(thisString, match => { var args = new List(); args.Add(match.Value); for (var k = 0; k < match.Groups.Count; k++) { var group = match.Groups[k]; if (group.Success) args.Add(group.Value); } args.Add(match.Index); args.Add(thisString); return TypeConverter.ToString(replaceFunction.Call(Undefined.Instance, args.ToArray())); }, rx.Global == true ? -1 : 1); // Set the deprecated RegExp properties if at least one match was found. //if (lastMatch != null) // this.Engine.RegExp.SetDeprecatedProperties(input, lastMatch); return result; } // searchValue is a string else { var substr = TypeConverter.ToString(searchValue); // Find the first occurrance of substr. int start = thisString.IndexOf(substr, StringComparison.Ordinal); if (start == -1) return thisString; int end = start + substr.Length; var args = new List(); args.Add(substr); args.Add(start); args.Add(thisString); var replaceString = TypeConverter.ToString(replaceFunction.Call(Undefined.Instance, args.ToArray())); // Replace only the first match. var result = new StringBuilder(thisString.Length + (substr.Length - substr.Length)); result.Append(thisString, 0, start); result.Append(replaceString); result.Append(thisString, end, thisString.Length - end); return result.ToString(); } } private object Match(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var regex = arguments.At(0); RegExpInstance rx = null; if (TypeConverter.GetType(regex) == Types.Object) { rx = regex as RegExpInstance; } rx = rx ?? (RegExpInstance) Engine.RegExp.Construct(new[] {regex}); var global = (bool) rx.Get("global"); if (!global) { return Engine.RegExp.PrototypeObject.Exec(rx, Arguments.From(s)); } else { rx.Put("lastIndex", (double) 0, false); var a = Engine.Array.Construct(Arguments.Empty); double previousLastIndex = 0; var n = 0; var lastMatch = true; while (lastMatch) { var result = Engine.RegExp.PrototypeObject.Exec(rx, Arguments.From(s)) as ObjectInstance; if (result == null) { lastMatch = false; } else { var thisIndex = (double) rx.Get("lastIndex"); if (thisIndex == previousLastIndex) { rx.Put("lastIndex", thisIndex + 1, false); previousLastIndex = thisIndex; } var matchStr = result.Get("0"); a.DefineOwnProperty(TypeConverter.ToString(n), new PropertyDescriptor(matchStr, true, true, true), false); n++; } } if (n == 0) { return Null.Instance; } return a; } } private double LocaleCompare(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var that = TypeConverter.ToString(arguments.Length > 0 ? arguments[0] : Undefined.Instance); return string.CompareOrdinal(s, that); } private double LastIndexOf(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var searchStr = TypeConverter.ToString(arguments.At(0)); double numPos = arguments.At(0) == Undefined.Instance ? double.NaN : TypeConverter.ToNumber(arguments.At(0)); double pos = double.IsNaN(numPos) ? double.PositiveInfinity : TypeConverter.ToInteger(numPos); var len = s.Length; var start = System.Math.Min(len, System.Math.Max(pos, 0)); var searchLen = searchStr.Length; return s.LastIndexOf(searchStr, len - (int) start, StringComparison.Ordinal); } private double IndexOf(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var searchStr = TypeConverter.ToString(arguments.Length > 0 ? arguments[0] : Undefined.Instance); double pos = 0; if (arguments.Length > 1 && arguments[1] != Undefined.Instance) { pos = TypeConverter.ToInteger(arguments[1]); } if (pos >= s.Length) { return -1; } if (pos < 0) { pos = 0; } return s.IndexOf(searchStr, (int) pos, StringComparison.Ordinal); } private string Concat(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var sb = new StringBuilder(s); for (int i = 0; i < arguments.Length; i++) { sb.Append(TypeConverter.ToString(arguments[i])); } return sb.ToString(); } private object CharCodeAt(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); object pos = arguments.Length > 0 ? arguments[0] : 0; var s = TypeConverter.ToString(thisObj); var position = (int)TypeConverter.ToInteger(pos); if (position < 0 || position >= s.Length) { return double.NaN; } return (uint)s[position]; } private object CharAt(object thisObj, object[] arguments) { TypeConverter.CheckObjectCoercible(Engine, thisObj); var s = TypeConverter.ToString(thisObj); var position = TypeConverter.ToInteger(arguments.At(0)); var size = s.Length; if (position >= size || position < 0) { return ""; } return s[(int) position].ToString(); } private string ValueOf(object thisObj, object[] arguments) { var s = thisObj as StringInstance; if (s == null) { throw new JavaScriptException(Engine.TypeError); } return s.PrimitiveValue; } } }